feat: podpora per-user notifikací s Discord, ntfy a Teams (#39)

Uživatelé mohou v nastavení konfigurovat vlastní webhook URL/topic
pro Discord, MS Teams a ntfy, a zvolit události k odběru.
Notifikace jsou odesílány pouze uživatelům se stejnou zvolenou lokalitou.
This commit is contained in:
2026-02-04 17:08:23 +01:00
parent fdd42dc46a
commit f36afe129a
6 changed files with 1824 additions and 12 deletions

View File

@@ -1,6 +1,8 @@
import { useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Modal, Button, Form } from "react-bootstrap"
import { useSettings, ThemePreference } from "../../context/settings";
import { NotificationSettings, UdalostEnum, getNotificationSettings, updateNotificationSettings } from "../../../../types";
import { useAuth } from "../../context/auth";
type Props = {
isOpen: boolean,
@@ -10,12 +12,56 @@ type Props = {
/** Modální dialog pro uživatelská nastavení. */
export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Props>) {
const auth = useAuth();
const settings = useSettings();
const bankAccountRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const hideSoupsRef = useRef<HTMLInputElement>(null);
const themeRef = useRef<HTMLSelectElement>(null);
const ntfyTopicRef = useRef<HTMLInputElement>(null);
const discordWebhookRef = useRef<HTMLInputElement>(null);
const teamsWebhookRef = useRef<HTMLInputElement>(null);
const [notifSettings, setNotifSettings] = useState<NotificationSettings>({});
const [enabledEvents, setEnabledEvents] = useState<UdalostEnum[]>([]);
useEffect(() => {
if (isOpen && auth?.login) {
getNotificationSettings().then(response => {
if (response.data) {
setNotifSettings(response.data);
setEnabledEvents(response.data.enabledEvents ?? []);
}
}).catch(() => {});
}
}, [isOpen, auth?.login]);
const toggleEvent = (event: UdalostEnum) => {
setEnabledEvents(prev =>
prev.includes(event) ? prev.filter(e => e !== event) : [...prev, event]
);
};
const handleSave = async () => {
// Uložení notifikačních nastavení na server
await updateNotificationSettings({
body: {
ntfyTopic: ntfyTopicRef.current?.value || undefined,
discordWebhookUrl: discordWebhookRef.current?.value || undefined,
teamsWebhookUrl: teamsWebhookRef.current?.value || undefined,
enabledEvents,
}
}).catch(() => {});
// Uložení ostatních nastavení (localStorage)
onSave(
bankAccountRef.current?.value,
nameRef.current?.value,
hideSoupsRef.current?.checked,
themeRef.current?.value as ThemePreference,
);
};
return (
<Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton>
@@ -51,6 +97,75 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
<hr />
<h4>Notifikace</h4>
<p>
Nastavením notifikací budete dostávat upozornění o událostech (např. "Jdeme na oběd") přímo do vámi zvoleného komunikačního kanálu.
</p>
<Form.Group className="mb-3">
<Form.Label>ntfy téma (topic)</Form.Label>
<Form.Control
ref={ntfyTopicRef}
type="text"
placeholder="moje-tema"
defaultValue={notifSettings.ntfyTopic}
key={notifSettings.ntfyTopic ?? 'ntfy-empty'}
onKeyDown={e => e.stopPropagation()}
/>
<Form.Text className="text-muted">
Téma pro ntfy push notifikace. Nechte prázdné pro vypnutí.
</Form.Text>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Discord webhook URL</Form.Label>
<Form.Control
ref={discordWebhookRef}
type="text"
placeholder="https://discord.com/api/webhooks/..."
defaultValue={notifSettings.discordWebhookUrl}
key={notifSettings.discordWebhookUrl ?? 'discord-empty'}
onKeyDown={e => e.stopPropagation()}
/>
<Form.Text className="text-muted">
URL webhooku Discord kanálu. Nechte prázdné pro vypnutí.
</Form.Text>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>MS Teams webhook URL</Form.Label>
<Form.Control
ref={teamsWebhookRef}
type="text"
placeholder="https://outlook.office.com/webhook/..."
defaultValue={notifSettings.teamsWebhookUrl}
key={notifSettings.teamsWebhookUrl ?? 'teams-empty'}
onKeyDown={e => e.stopPropagation()}
/>
<Form.Text className="text-muted">
URL webhooku MS Teams kanálu. Nechte prázdné pro vypnutí.
</Form.Text>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Události k odběru</Form.Label>
{Object.values(UdalostEnum).map(event => (
<Form.Check
key={event}
id={`notif-event-${event}`}
type="checkbox"
label={event}
checked={enabledEvents.includes(event)}
onChange={() => toggleEvent(event)}
/>
))}
<Form.Text className="text-muted">
Zvolte události, o kterých chcete být notifikováni. Notifikace jsou odesílány pouze uživatelům se stejnou zvolenou lokalitou.
</Form.Text>
</Form.Group>
<hr />
<h4>Bankovní účet</h4>
<p>
Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.
@@ -88,7 +203,7 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
<Button variant="secondary" onClick={onClose}>
Storno
</Button>
<Button onClick={() => onSave(bankAccountRef.current?.value, nameRef.current?.value, hideSoupsRef.current?.checked, themeRef.current?.value as ThemePreference)}>
<Button onClick={handleSave}>
Uložit
</Button>
</Modal.Footer>