fix(geplante-nachrichten): add /api prefix to all API paths, fix subscribe room token, unmask empty bot credentials, add Einzelnachrichten tab
This commit is contained in:
@@ -88,12 +88,12 @@ class ScheduledMessagesController {
|
||||
try {
|
||||
const { id } = req.params as Record<string, string>;
|
||||
const userId = req.user!.id;
|
||||
const { roomToken } = req.body;
|
||||
if (!roomToken) {
|
||||
res.status(400).json({ success: false, message: 'roomToken ist erforderlich' });
|
||||
const { room_token } = req.body as Record<string, string>;
|
||||
if (!room_token) {
|
||||
res.status(400).json({ success: false, message: 'room_token ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
await scheduledMessagesService.subscribe(id, userId, roomToken);
|
||||
await scheduledMessagesService.subscribe(id, userId, room_token);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('ScheduledMessagesController.subscribe error', { error, id: req.params.id });
|
||||
@@ -101,6 +101,25 @@ class ScheduledMessagesController {
|
||||
}
|
||||
}
|
||||
|
||||
async getMyBotRoom(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
const result = await scheduledMessagesService.getOrCreateBotRoom(userId);
|
||||
if (result === null) {
|
||||
res.status(400).json({ success: false, configured: false, message: 'Bot nicht konfiguriert' });
|
||||
return;
|
||||
}
|
||||
if (result === 'not_connected') {
|
||||
res.status(400).json({ success: false, connected: false, message: 'Nextcloud-Konto nicht verbunden' });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: { room_token: result } });
|
||||
} catch (error) {
|
||||
logger.error('ScheduledMessagesController.getMyBotRoom error', { error });
|
||||
res.status(500).json({ success: false, message: 'Bot-Raum konnte nicht ermittelt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params as Record<string, string>;
|
||||
|
||||
@@ -20,7 +20,7 @@ function maskValue(value: string): string {
|
||||
function maskConfig(config: Record<string, string>): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
result[k] = MASKED_FIELDS.includes(k) ? maskValue(v) : v;
|
||||
result[k] = (MASKED_FIELDS.includes(k) && v) ? maskValue(v) : v;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// /rooms and /one-time MUST come before /:id to avoid being captured as an id param
|
||||
// /rooms, /my-bot-room and /one-time MUST come before /:id to avoid being captured as an id param
|
||||
router.get(
|
||||
'/rooms',
|
||||
authenticate,
|
||||
@@ -13,6 +13,13 @@ router.get(
|
||||
scheduledMessagesController.getRooms.bind(scheduledMessagesController),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/my-bot-room',
|
||||
authenticate,
|
||||
requirePermission('scheduled_messages:subscribe'),
|
||||
scheduledMessagesController.getMyBotRoom.bind(scheduledMessagesController),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/one-time',
|
||||
authenticate,
|
||||
|
||||
@@ -521,7 +521,37 @@ async function sendVehicleEvent(vehicleId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── One-Time Messages ─────────────────────────────────────────────────────────
|
||||
// ── Bot Room ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Finds or creates a 1:1 Nextcloud Talk room between the bot and the given user.
|
||||
* Returns the room token, null if bot isn't configured, or 'not_connected' if the
|
||||
* user hasn't linked their Nextcloud account.
|
||||
*/
|
||||
async function getOrCreateBotRoom(userId: string): Promise<string | null | 'not_connected'> {
|
||||
const creds = await getBotCredentials();
|
||||
if (!creds) return null;
|
||||
|
||||
// Look up the user's Nextcloud login name
|
||||
const userResult = await pool.query(
|
||||
'SELECT nextcloud_login_name FROM users WHERE id = $1',
|
||||
[userId],
|
||||
);
|
||||
const nextcloudLoginName = userResult.rows[0]?.nextcloud_login_name as string | null;
|
||||
if (!nextcloudLoginName) return 'not_connected';
|
||||
|
||||
// Use createRoom(type=1) — Nextcloud returns existing 1:1 if it already exists
|
||||
const { token } = await nextcloudService.createRoom(
|
||||
1,
|
||||
nextcloudLoginName,
|
||||
undefined,
|
||||
creds.username,
|
||||
creds.appPassword,
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface OneTimeMessage {
|
||||
id: string;
|
||||
@@ -598,6 +628,7 @@ const scheduledMessagesService = {
|
||||
getSubscriptionsForUser,
|
||||
buildAndSend,
|
||||
sendVehicleEvent,
|
||||
getOrCreateBotRoom,
|
||||
getOneTimeMessages,
|
||||
createOneTimeMessage,
|
||||
deleteOneTimeMessage,
|
||||
|
||||
@@ -15,12 +15,22 @@ import {
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import { Add as AddIcon, Edit as EditIcon } from '@mui/icons-material';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon, Send as SendIcon } from '@mui/icons-material';
|
||||
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { scheduledMessagesApi } from '../services/scheduledMessages';
|
||||
import type { MessageType, ScheduledMessageRule } from '../types/scheduledMessages.types';
|
||||
|
||||
@@ -54,12 +64,211 @@ function triggerSummary(rule: ScheduledMessageRule): string {
|
||||
return TRIGGER_LABELS[rule.trigger_mode] ?? rule.trigger_mode;
|
||||
}
|
||||
|
||||
function formatSendAt(iso: string): string {
|
||||
return new Date(iso).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function EinzelNachrichtenTab() {
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const { data: messagesData, isLoading: messagesLoading, isError: messagesError } = useQuery({
|
||||
queryKey: ['scheduled-one-time-messages'],
|
||||
queryFn: scheduledMessagesApi.getOneTimeMessages,
|
||||
});
|
||||
|
||||
const { data: roomsData, isLoading: roomsLoading } = useQuery({
|
||||
queryKey: ['scheduled-messages-rooms'],
|
||||
queryFn: scheduledMessagesApi.getRooms,
|
||||
});
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
const [roomToken, setRoomToken] = useState('');
|
||||
const [sendAt, setSendAt] = useState('');
|
||||
|
||||
const rooms = roomsData?.data ?? [];
|
||||
const roomsConfigured = roomsData?.configured ?? false;
|
||||
const roomsErr = roomsData?.error;
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const room = rooms.find(r => r.token === roomToken);
|
||||
return scheduledMessagesApi.createOneTimeMessage({
|
||||
message,
|
||||
target_room_token: roomToken,
|
||||
target_room_name: room?.displayName,
|
||||
send_at: new Date(sendAt).toISOString(),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccess('Einzelnachricht geplant');
|
||||
setMessage('');
|
||||
setRoomToken('');
|
||||
setSendAt('');
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-one-time-messages'] });
|
||||
},
|
||||
onError: () => showError('Fehler beim Speichern'),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => scheduledMessagesApi.deleteOneTimeMessage(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-one-time-messages'] });
|
||||
},
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
const canCreate = !!message.trim() && !!roomToken && !!sendAt;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom fontWeight={500}>Neue Einzelnachricht</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{/* Room picker */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<FormLabel sx={{ mb: 0.5 }}>Zielraum</FormLabel>
|
||||
{roomsLoading ? (
|
||||
<CircularProgress size={20} />
|
||||
) : !roomsConfigured ? (
|
||||
<Alert severity="warning" sx={{ mt: 1 }}>
|
||||
Bot-Konto nicht konfiguriert. Bitte in den Admin-Einstellungen unter Nextcloud konfigurieren.
|
||||
</Alert>
|
||||
) : roomsErr ? (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
Nextcloud nicht erreichbar — Raumliste kann nicht geladen werden.
|
||||
</Alert>
|
||||
) : (
|
||||
<Select
|
||||
value={roomToken}
|
||||
onChange={(e) => setRoomToken(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="" disabled>Raum auswählen...</MenuItem>
|
||||
{rooms.map((room) => (
|
||||
<MenuItem key={room.token} value={room.token}>
|
||||
{room.displayName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{/* Send time */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
label="Sendezeitpunkt"
|
||||
type="datetime-local"
|
||||
value={sendAt}
|
||||
onChange={(e) => setSendAt(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Message */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
label="Nachricht"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={8}
|
||||
fullWidth
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={createMutation.isPending ? <CircularProgress size={16} /> : <SendIcon />}
|
||||
onClick={() => createMutation.mutate()}
|
||||
disabled={!canCreate || createMutation.isPending || !roomsConfigured}
|
||||
>
|
||||
Planen
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
{/* Pending messages list */}
|
||||
<Typography variant="subtitle1" gutterBottom fontWeight={500}>Ausstehende Einzelnachrichten</Typography>
|
||||
|
||||
{messagesError && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>Fehler beim Laden der Nachrichten.</Alert>
|
||||
)}
|
||||
|
||||
{messagesLoading ? (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
{[1, 2].map((i) => (
|
||||
<TableRow key={i}>
|
||||
{[1, 2, 3, 4].map((j) => <TableCell key={j}><Skeleton /></TableCell>)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : !messagesData?.data?.length ? (
|
||||
<Typography color="text.secondary" sx={{ py: 2 }}>
|
||||
Keine ausstehenden Einzelnachrichten.
|
||||
</Typography>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nachricht</TableCell>
|
||||
<TableCell>Zielraum</TableCell>
|
||||
<TableCell>Sendezeitpunkt</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{messagesData.data.map((msg) => (
|
||||
<TableRow key={msg.id}>
|
||||
<TableCell sx={{ maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{msg.message}
|
||||
</TableCell>
|
||||
<TableCell>{msg.target_room_name ?? msg.target_room_token}</TableCell>
|
||||
<TableCell>{formatSendAt(msg.send_at)}</TableCell>
|
||||
<TableCell padding="checkbox">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => deleteMutation.mutate(msg.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GeplanteMachrichten() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const canEdit = hasPermission('scheduled_messages:edit');
|
||||
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['scheduled-messages'],
|
||||
queryFn: scheduledMessagesApi.getAll,
|
||||
@@ -85,7 +294,7 @@ export default function GeplanteMachrichten() {
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h5">Geplante Nachrichten</Typography>
|
||||
{canEdit && (
|
||||
{canEdit && tab === 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
@@ -96,121 +305,132 @@ export default function GeplanteMachrichten() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isError && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
Fehler beim Laden der Regeln.
|
||||
</Alert>
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }}>
|
||||
<Tab label="Regeln" />
|
||||
{canEdit && <Tab label="Einzelnachrichten" />}
|
||||
</Tabs>
|
||||
|
||||
{tab === 0 && (
|
||||
<>
|
||||
{isError && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
Fehler beim Laden der Regeln.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell>Trigger</TableCell>
|
||||
<TableCell>Zielraum</TableCell>
|
||||
<TableCell>Abonnierbar</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<TableRow key={i}>
|
||||
{[1, 2, 3, 4, 5, 6, 7].map((j) => (
|
||||
<TableCell key={j}><Skeleton /></TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : rules.length === 0 ? (
|
||||
<Typography sx={{ textAlign: 'center', py: 4 }} color="text.secondary">
|
||||
Keine Regeln konfiguriert
|
||||
</Typography>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell>Trigger</TableCell>
|
||||
<TableCell>Zielraum</TableCell>
|
||||
<TableCell>Abonnierbar</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rules.map((rule) => (
|
||||
<TableRow
|
||||
key={rule.id}
|
||||
hover
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate(`/geplante-nachrichten/${rule.id}`)}
|
||||
>
|
||||
<TableCell>{rule.name}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={MESSAGE_TYPE_LABELS[rule.message_type] ?? rule.message_type}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{triggerSummary(rule)}</TableCell>
|
||||
<TableCell>{rule.target_room_name ?? rule.target_room_token}</TableCell>
|
||||
<TableCell>{rule.subscribable ? 'Ja' : 'Nein'}</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={rule.active}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={() => handleToggleActive(rule)}
|
||||
size="small"
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }} onClick={(e) => e.stopPropagation()}>
|
||||
{canEdit && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => navigate(`/geplante-nachrichten/${rule.id}/bearbeiten`)}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
{canEdit && (
|
||||
deleteConfirmId === rule.id ? (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={() => handleDelete(rule.id)}
|
||||
>
|
||||
Löschen?
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => setDeleteConfirmId(rule.id)}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell>Trigger</TableCell>
|
||||
<TableCell>Zielraum</TableCell>
|
||||
<TableCell>Abonnierbar</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<TableRow key={i}>
|
||||
{[1, 2, 3, 4, 5, 6, 7].map((j) => (
|
||||
<TableCell key={j}><Skeleton /></TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : rules.length === 0 ? (
|
||||
<Typography sx={{ textAlign: 'center', py: 4 }} color="text.secondary">
|
||||
Keine Regeln konfiguriert
|
||||
</Typography>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell>Trigger</TableCell>
|
||||
<TableCell>Zielraum</TableCell>
|
||||
<TableCell>Abonnierbar</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rules.map((rule) => (
|
||||
<TableRow
|
||||
key={rule.id}
|
||||
hover
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate(`/geplante-nachrichten/${rule.id}`)}
|
||||
>
|
||||
<TableCell>{rule.name}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={MESSAGE_TYPE_LABELS[rule.message_type] ?? rule.message_type}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{triggerSummary(rule)}</TableCell>
|
||||
<TableCell>{rule.target_room_name ?? rule.target_room_token}</TableCell>
|
||||
<TableCell>{rule.subscribable ? 'Ja' : 'Nein'}</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={rule.active}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={() => handleToggleActive(rule)}
|
||||
size="small"
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }} onClick={(e) => e.stopPropagation()}>
|
||||
{canEdit && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => navigate(`/geplante-nachrichten/${rule.id}/bearbeiten`)}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
{canEdit && (
|
||||
deleteConfirmId === rule.id ? (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={() => handleDelete(rule.id)}
|
||||
>
|
||||
Löschen?
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => setDeleteConfirmId(rule.id)}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{tab === 1 && canEdit && <EinzelNachrichtenTab />}
|
||||
</Box>
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
@@ -75,8 +75,13 @@ export default function GeplanteMachrichtenDetail() {
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
if (!id) return;
|
||||
await scheduledMessagesApi.subscribe(id, '');
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-messages', id] });
|
||||
try {
|
||||
const roomResult = await scheduledMessagesApi.getMyBotRoom();
|
||||
await scheduledMessagesApi.subscribe(id, roomResult.data.room_token);
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-messages', id] });
|
||||
} catch {
|
||||
showError('Abonnieren fehlgeschlagen. Bitte Nextcloud-Konto in den Einstellungen verbinden.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnsubscribe = async () => {
|
||||
|
||||
@@ -81,6 +81,7 @@ const ORDERABLE_NAV_ITEMS = [
|
||||
{ text: 'Checklisten', path: '/checklisten', permission: 'checklisten:view' },
|
||||
{ text: 'Buchhaltung', path: '/buchhaltung', permission: 'buchhaltung:view' },
|
||||
{ text: 'Issues', path: '/issues', permission: 'issues:view_own' },
|
||||
{ text: 'Geplante Nachrichten', path: '/geplante-nachrichten', permission: 'scheduled_messages:view' },
|
||||
];
|
||||
|
||||
function SortableNavItem({ id, text }: { id: string; text: string }) {
|
||||
@@ -130,7 +131,12 @@ function SubscriptionsCard() {
|
||||
if (isSubscribed) {
|
||||
await scheduledMessagesApi.unsubscribe(ruleId);
|
||||
} else {
|
||||
await scheduledMessagesApi.subscribe(ruleId, '');
|
||||
try {
|
||||
const roomResult = await scheduledMessagesApi.getMyBotRoom();
|
||||
await scheduledMessagesApi.subscribe(ruleId, roomResult.data.room_token);
|
||||
} catch {
|
||||
return; // silently skip — user will see no change in toggle
|
||||
}
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-messages'] });
|
||||
};
|
||||
|
||||
@@ -9,34 +9,37 @@ import type {
|
||||
|
||||
export const scheduledMessagesApi = {
|
||||
getAll: () =>
|
||||
api.get<ScheduledMessagesListResponse>('/scheduled-messages').then(r => r.data),
|
||||
api.get<ScheduledMessagesListResponse>('/api/scheduled-messages').then(r => r.data),
|
||||
|
||||
getById: (id: string) =>
|
||||
api.get<ScheduledMessageDetailResponse>(`/scheduled-messages/${id}`).then(r => r.data),
|
||||
api.get<ScheduledMessageDetailResponse>(`/api/scheduled-messages/${id}`).then(r => r.data),
|
||||
|
||||
getRooms: () =>
|
||||
api.get<RoomsResponse>('/scheduled-messages/rooms').then(r => r.data),
|
||||
api.get<RoomsResponse>('/api/scheduled-messages/rooms').then(r => r.data),
|
||||
|
||||
create: (data: Partial<ScheduledMessageRule>) =>
|
||||
api.post<ScheduledMessageDetailResponse>('/scheduled-messages', data).then(r => r.data),
|
||||
api.post<ScheduledMessageDetailResponse>('/api/scheduled-messages', data).then(r => r.data),
|
||||
|
||||
update: (id: string, data: Partial<ScheduledMessageRule>) =>
|
||||
api.patch<ScheduledMessageDetailResponse>(`/scheduled-messages/${id}`, data).then(r => r.data),
|
||||
api.patch<ScheduledMessageDetailResponse>(`/api/scheduled-messages/${id}`, data).then(r => r.data),
|
||||
|
||||
delete: (id: string) =>
|
||||
api.delete(`/scheduled-messages/${id}`).then(r => r.data),
|
||||
api.delete(`/api/scheduled-messages/${id}`).then(r => r.data),
|
||||
|
||||
subscribe: (id: string, roomToken: string) =>
|
||||
api.post(`/scheduled-messages/${id}/subscribe`, { room_token: roomToken }).then(r => r.data),
|
||||
api.post(`/api/scheduled-messages/${id}/subscribe`, { room_token: roomToken }).then(r => r.data),
|
||||
|
||||
unsubscribe: (id: string) =>
|
||||
api.delete(`/scheduled-messages/${id}/subscribe`).then(r => r.data),
|
||||
api.delete(`/api/scheduled-messages/${id}/subscribe`).then(r => r.data),
|
||||
|
||||
trigger: (id: string) =>
|
||||
api.post(`/scheduled-messages/${id}/trigger`).then(r => r.data),
|
||||
api.post(`/api/scheduled-messages/${id}/trigger`).then(r => r.data),
|
||||
|
||||
getMyBotRoom: () =>
|
||||
api.get<{ data: { room_token: string } }>('/api/scheduled-messages/my-bot-room').then(r => r.data),
|
||||
|
||||
getOneTimeMessages: () =>
|
||||
api.get<{ data: OneTimeMessage[] }>('/scheduled-messages/one-time').then(r => r.data),
|
||||
api.get<{ data: OneTimeMessage[] }>('/api/scheduled-messages/one-time').then(r => r.data),
|
||||
|
||||
createOneTimeMessage: (data: {
|
||||
message: string;
|
||||
@@ -44,8 +47,8 @@ export const scheduledMessagesApi = {
|
||||
target_room_name?: string;
|
||||
send_at: string;
|
||||
}) =>
|
||||
api.post<{ data: OneTimeMessage }>('/scheduled-messages/one-time', data).then(r => r.data),
|
||||
api.post<{ data: OneTimeMessage }>('/api/scheduled-messages/one-time', data).then(r => r.data),
|
||||
|
||||
deleteOneTimeMessage: (id: string) =>
|
||||
api.delete(`/scheduled-messages/one-time/${id}`).then(r => r.data),
|
||||
api.delete(`/api/scheduled-messages/one-time/${id}`).then(r => r.data),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user