feat(geplante-nachrichten): scheduled message rule engine with bot delivery, admin UI, and manual trigger
This commit is contained in:
@@ -136,8 +136,8 @@ export default function ModuleSettingsIssues() {
|
||||
});
|
||||
const deleteStatusMut = useMutation({
|
||||
mutationFn: (id: number) => issuesApi.deleteStatus(id),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-statuses'] }); showSuccess('Status deaktiviert'); },
|
||||
onError: () => showError('Fehler beim Deaktivieren'),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-statuses'] }); showSuccess('Status gelöscht'); },
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
// ── Priority mutations ──
|
||||
@@ -153,8 +153,8 @@ export default function ModuleSettingsIssues() {
|
||||
});
|
||||
const deletePrioMut = useMutation({
|
||||
mutationFn: (id: number) => issuesApi.deletePriority(id),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-priorities'] }); showSuccess('Priorität deaktiviert'); },
|
||||
onError: () => showError('Fehler beim Deaktivieren'),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-priorities'] }); showSuccess('Priorität gelöscht'); },
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
// ── Type mutations ──
|
||||
@@ -170,8 +170,8 @@ export default function ModuleSettingsIssues() {
|
||||
});
|
||||
const deleteTypeMut = useMutation({
|
||||
mutationFn: (id: number) => issuesApi.deleteType(id),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie deaktiviert'); },
|
||||
onError: () => showError('Fehler beim Deaktivieren'),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie gelöscht'); },
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
const flatTypes = useMemo(() => {
|
||||
|
||||
@@ -16,6 +16,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { usePermissionContext } from '../../contexts/PermissionContext';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import { toolConfigApi } from '../../services/toolConfig';
|
||||
import type { ToolConfig } from '../../types/toolConfig.types';
|
||||
import { scheduledMessagesApi } from '../../services/scheduledMessages';
|
||||
|
||||
export default function ToolSettingsNextcloud() {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -28,11 +30,16 @@ export default function ToolSettingsNextcloud() {
|
||||
});
|
||||
|
||||
const [url, setUrl] = useState('');
|
||||
const [botUsername, setBotUsername] = useState('');
|
||||
const [botAppPassword, setBotAppPassword] = useState('');
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string; latencyMs: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setUrl(data.url ?? '');
|
||||
const d = data as unknown as Record<string, unknown>;
|
||||
setBotUsername((d.bot_username as string) ?? '');
|
||||
setBotAppPassword((d.bot_app_password as string) ?? '');
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
@@ -45,6 +52,19 @@ export default function ToolSettingsNextcloud() {
|
||||
onError: () => showError('Fehler beim Speichern der Konfiguration'),
|
||||
});
|
||||
|
||||
const saveBotMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
toolConfigApi.update('nextcloud', {
|
||||
bot_username: botUsername,
|
||||
bot_app_password: botAppPassword,
|
||||
} as unknown as Partial<ToolConfig>),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tool-config', 'nextcloud'] });
|
||||
showSuccess('Bot-Konfiguration gespeichert');
|
||||
},
|
||||
onError: () => showError('Fehler beim Speichern der Bot-Konfiguration'),
|
||||
});
|
||||
|
||||
const testMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
toolConfigApi.test('nextcloud', { url: url || undefined }),
|
||||
@@ -61,58 +81,108 @@ export default function ToolSettingsNextcloud() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>Nextcloud</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Stack spacing={3}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>Nextcloud</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{!isFeatureEnabled('nextcloud') && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
Dieses Werkzeug befindet sich im Wartungsmodus.
|
||||
</Alert>
|
||||
)}
|
||||
{!isFeatureEnabled('nextcloud') && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
Dieses Werkzeug befindet sich im Wartungsmodus.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="URL"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="https://nextcloud.example.com"
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="URL"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
size="small"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setTestResult(null);
|
||||
testMutation.mutate();
|
||||
}}
|
||||
disabled={testMutation.isPending}
|
||||
>
|
||||
{testMutation.isPending ? <CircularProgress size={18} sx={{ mr: 1 }} /> : null}
|
||||
Verbindung testen
|
||||
</Button>
|
||||
{testResult && (
|
||||
<Chip
|
||||
label={testResult.success ? `Verbunden (${testResult.latencyMs}ms)` : `Fehler: ${testResult.message}`}
|
||||
color={testResult.success ? 'success' : 'error'}
|
||||
fullWidth
|
||||
placeholder="https://nextcloud.example.com"
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setTestResult(null);
|
||||
testMutation.mutate();
|
||||
}}
|
||||
disabled={testMutation.isPending}
|
||||
>
|
||||
{testMutation.isPending ? <CircularProgress size={18} sx={{ mr: 1 }} /> : null}
|
||||
Verbindung testen
|
||||
</Button>
|
||||
{testResult && (
|
||||
<Chip
|
||||
label={testResult.success ? `Verbunden (${testResult.latencyMs}ms)` : `Fehler: ${testResult.message}`}
|
||||
color={testResult.success ? 'success' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" gutterBottom>Bot-Konto</Typography>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="Bot-Benutzername"
|
||||
value={botUsername}
|
||||
onChange={(e) => setBotUsername(e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
/>
|
||||
<TextField
|
||||
label="Bot-App-Passwort"
|
||||
type="password"
|
||||
value={botAppPassword}
|
||||
onChange={(e) => setBotAppPassword(e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => saveBotMutation.mutate()}
|
||||
disabled={saveBotMutation.isPending}
|
||||
>
|
||||
Bot speichern
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await scheduledMessagesApi.getRooms();
|
||||
if (result.configured) {
|
||||
showSuccess(`Verbindung erfolgreich (${result.data?.length ?? 0} Räume)`);
|
||||
} else {
|
||||
showError('Bot nicht konfiguriert');
|
||||
}
|
||||
} catch {
|
||||
showError('Verbindungstest fehlgeschlagen');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Verbindung testen
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
AssignmentTurnedIn,
|
||||
AccountBalance as AccountBalanceIcon,
|
||||
Checkroom as CheckroomIcon,
|
||||
Schedule,
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -134,6 +135,12 @@ const baseNavigationItems: NavigationItem[] = [
|
||||
path: '/issues',
|
||||
permission: 'issues:view_own',
|
||||
},
|
||||
{
|
||||
text: 'Geplante Nachrichten',
|
||||
icon: <Schedule />,
|
||||
path: '/geplante-nachrichten',
|
||||
permission: 'scheduled_messages:view',
|
||||
},
|
||||
];
|
||||
|
||||
const adminItem: NavigationItem = {
|
||||
|
||||
Reference in New Issue
Block a user