feat: push upozornění na doručení objednávky Bolt Food
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m48s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s

Nové zaškrtávátko v Nastavení → Notifikace pod výběrem času připomínky.
Když skupinová objednávka sledovaná přes Bolt Food přejde do stavu
delivered/finished, dostanou push členové skupiny, kteří si to zapnuli.
Zakladatel skupiny notifikaci nedostává — ten je informován aplikací Bolt.

Push subscription byla dosud svázaná s časem připomínky (subscribe endpoint
bez reminderTime vracel 400), takže by nový přepínač samostatně nefungoval.
Čas připomínky je nově v registru volitelný a scheduler připomínek záznamy
bez času přeskakuje.

Service worker nemá natvrdo tag a akci "Mám vlastní/neobědvám" — obojí se
řídí payloadem, aby je doručovací notifikace nedědila.
This commit is contained in:
2026-08-10 12:36:07 +02:00
parent 377a350211
commit 065ccaf38a
12 changed files with 440 additions and 41 deletions
+10 -7
View File
@@ -1,4 +1,4 @@
// Service Worker pro Web Push notifikace (připomínka výběru oběda)
// Service Worker pro Web Push notifikace (připomínka výběru oběda, doručení objednávky)
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? { title: 'Luncher', body: 'Ještě nemáte zvolený oběd!' };
@@ -6,11 +6,12 @@ self.addEventListener('push', (event) => {
self.registration.showNotification(data.title, {
body: data.body,
icon: '/favicon.ico',
tag: 'lunch-reminder',
data: { login: data.login, token: data.token },
actions: [
{ action: 'neobedvam', title: 'Mám vlastní/neobědvám' },
],
tag: data.tag ?? 'lunch-reminder',
data: { login: data.login, token: data.token, url: data.url },
// Token posílá jen připomínka oběda — ostatní notifikace tlačítko nemají.
actions: data.token
? [{ action: 'neobedvam', title: 'Mám vlastní/neobědvám' }]
: [],
})
);
});
@@ -32,14 +33,16 @@ self.addEventListener('notificationclick', (event) => {
return;
}
const url = event.notification.data?.url ?? '/';
event.waitUntil(
self.clients.matchAll({ type: 'window' }).then((clientList) => {
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
if ('navigate' in client) client.navigate(url).catch(() => {});
return client.focus();
}
}
return self.clients.openWindow('/');
return self.clients.openWindow(url);
})
);
});
+35 -12
View File
@@ -21,6 +21,7 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
const themeRef = useRef<HTMLSelectElement>(null);
const reminderTimeRef = useRef<HTMLInputElement>(null);
const boltDeliveredRef = useRef<HTMLInputElement>(null);
const ntfyTopicRef = useRef<HTMLInputElement>(null);
const discordWebhookRef = useRef<HTMLInputElement>(null);
const teamsWebhookRef = useRef<HTMLInputElement>(null);
@@ -47,22 +48,29 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
const handleSave = async () => {
const newReminderTime = reminderTimeRef.current?.value || undefined;
const oldReminderTime = notifSettings.reminderTime;
const newBoltDelivered = boltDeliveredRef.current?.checked ?? false;
const oldBoltDelivered = notifSettings.boltDeliveredPush ?? false;
// 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,
reminderTime: newReminderTime,
}
}).catch(() => {});
const newSettings: NotificationSettings = {
ntfyTopic: ntfyTopicRef.current?.value || undefined,
discordWebhookUrl: discordWebhookRef.current?.value || undefined,
teamsWebhookUrl: teamsWebhookRef.current?.value || undefined,
enabledEvents,
reminderTime: newReminderTime,
boltDeliveredPush: newBoltDelivered,
};
await updateNotificationSettings({ body: newSettings }).catch(() => {});
setNotifSettings(newSettings);
// Správa push subscription pro připomínky
if (newReminderTime && newReminderTime !== oldReminderTime) {
// Správa push subscription — drží ji naživu kterákoli z push funkcí.
// Záměrně bez await: subscribeToPush si vyžádá oprávnění prohlížeče a modal
// by na tu dobu zůstal otevřený.
const wantsPush = !!newReminderTime || newBoltDelivered;
const hadPush = !!oldReminderTime || oldBoltDelivered;
if (wantsPush && (newReminderTime !== oldReminderTime || newBoltDelivered !== oldBoltDelivered)) {
subscribeToPush(newReminderTime);
} else if (!newReminderTime && oldReminderTime) {
} else if (!wantsPush && hadPush) {
unsubscribeFromPush();
}
@@ -128,6 +136,21 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
</Form.Text>
</Form.Group>
<Form.Group className="mb-3">
<Form.Check
id="boltDeliveredCheckbox"
ref={boltDeliveredRef}
type="checkbox"
label="Upozornění na doručení objednávky (Bolt Food)"
defaultChecked={notifSettings.boltDeliveredPush ?? false}
key={`bolt-delivered-${notifSettings.boltDeliveredPush ?? false}`}
/>
<Form.Text className="text-muted">
bude skupinová objednávka sledovaná přes Bolt Food doručena, přijde vám push notifikace.
Zakladateli skupiny se neposílá ten dostane upozornění přímo z aplikace Bolt.
</Form.Text>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>ntfy téma (topic)</Form.Label>
<Form.Control
+5 -4
View File
@@ -27,9 +27,10 @@ async function pushApiFetch(path: string, options: RequestInit = {}): Promise<Re
/**
* Zaregistruje service worker, přihlásí se k push notifikacím
* a odešle subscription na server.
* a odešle subscription na server. `reminderTime` je volitelný — bez něj
* uživatel odebírá jen ostatní push notifikace (např. doručení objednávky).
*/
export async function subscribeToPush(reminderTime: string): Promise<boolean> {
export async function subscribeToPush(reminderTime?: string): Promise<boolean> {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
console.warn('Push notifikace nejsou v tomto prohlížeči podporovány');
return false;
@@ -75,7 +76,7 @@ export async function subscribeToPush(reminderTime: string): Promise<boolean> {
return false;
}
console.log('Push notifikace: úspěšně přihlášeno k připomínkám v', reminderTime);
console.log('Push notifikace: úspěšně přihlášeno', reminderTime ? `(připomínka v ${reminderTime})` : '(bez připomínky oběda)');
return true;
} catch (error) {
console.error('Push notifikace: chyba při registraci', error);
@@ -101,7 +102,7 @@ export async function unsubscribeFromPush(): Promise<void> {
}
await pushApiFetch('/unsubscribe', { method: 'POST' });
console.log('Push notifikace: úspěšně odhlášeno z připomínek');
console.log('Push notifikace: úspěšně odhlášeno');
} catch (error) {
console.error('Push notifikace: chyba při odhlášení', error);
}