feat: Benachrichtigungen in den Einstellungen bedienen
Anmelden dieses Browsers, Liste der angemeldeten Geraete, Auswahl der Anlaesse, Ruhezeit und Vorlauf. Ist ein Weg auf dem Server nicht eingerichtet, steht das direkt daneben statt in einem Fehler beim Speichern. Der Knopf "Probe senden" umgeht Ruhezeit und Sperre absichtlich: wer darauf drueckt, will jetzt wissen, ob es ankommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e0e11c85b2
commit
b622c1bb28
@@ -0,0 +1,195 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { einstellungSchema } from '@/lib/benachrichtigung'
|
||||
import { alsTagesschluessel } from '@/lib/zeit'
|
||||
import { auditSchreiben } from '@/server/audit'
|
||||
import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung'
|
||||
import { mailSenden } from '@/server/benachrichtigung/mail'
|
||||
import { pushSenden } from '@/server/benachrichtigung/push'
|
||||
import { alsEinstellung } from '@/server/benachrichtigung/versand'
|
||||
import { db } from '@/server/db'
|
||||
import { umgebung } from '@/server/umgebung'
|
||||
import { jetzt } from '@/server/zeitgeber'
|
||||
|
||||
export type Rueckmeldung =
|
||||
| { ok: true; meldung: string }
|
||||
| { ok: false; fehler: string; felder?: Record<string, string> }
|
||||
|
||||
async function akteurOderFehler() {
|
||||
try {
|
||||
return { akteur: await pruefeRolle('BENUTZER') }
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof NichtErlaubt) {
|
||||
return { antwort: { ok: false as const, fehler: fehler.message } }
|
||||
}
|
||||
throw fehler
|
||||
}
|
||||
}
|
||||
|
||||
/** Speichert die Benachrichtigungseinstellung des angemeldeten Benutzers. */
|
||||
export async function einstellungSpeichern(daten: FormData): Promise<Rueckmeldung> {
|
||||
const geprueft = await akteurOderFehler()
|
||||
if ('antwort' in geprueft) return geprueft.antwort
|
||||
const { akteur } = geprueft
|
||||
|
||||
const gelesen = einstellungSchema.safeParse({
|
||||
anlaesse: daten.getAll('anlaesse'),
|
||||
perPush: daten.get('perPush') !== null,
|
||||
perMail: daten.get('perMail') !== null,
|
||||
ueberblickUm: daten.get('ueberblickUm'),
|
||||
ruheVon: daten.get('ruheVon'),
|
||||
ruheBis: daten.get('ruheBis'),
|
||||
ruheAmWochenende: daten.get('ruheAmWochenende') !== null,
|
||||
zeitblockVorlaufMinuten: daten.get('zeitblockVorlaufMinuten'),
|
||||
})
|
||||
|
||||
if (!gelesen.success) {
|
||||
const felder: Record<string, string> = {}
|
||||
for (const problem of gelesen.error.issues) {
|
||||
const feld = problem.path.join('.')
|
||||
felder[feld] ??= problem.message
|
||||
}
|
||||
return { ok: false, fehler: 'Bitte die markierten Felder prüfen.', felder }
|
||||
}
|
||||
|
||||
const werte = {
|
||||
anlaesse: gelesen.data.anlaesse.join(','),
|
||||
perPush: gelesen.data.perPush,
|
||||
perMail: gelesen.data.perMail,
|
||||
ueberblickUm: gelesen.data.ueberblickUm,
|
||||
ruheVon: gelesen.data.ruheVon,
|
||||
ruheBis: gelesen.data.ruheBis,
|
||||
ruheAmWochenende: gelesen.data.ruheAmWochenende,
|
||||
zeitblockVorlaufMinuten: gelesen.data.zeitblockVorlaufMinuten,
|
||||
}
|
||||
|
||||
await db.benachrichtigungseinstellung.upsert({
|
||||
where: { benutzerId: akteur.id },
|
||||
update: werte,
|
||||
create: { benutzerId: akteur.id, ...werte },
|
||||
})
|
||||
|
||||
await auditSchreiben({
|
||||
akteur: { id: akteur.id, email: akteur.email },
|
||||
aktion: 'benachrichtigung.geaendert',
|
||||
objektTyp: 'Benachrichtigungseinstellung',
|
||||
objektId: akteur.id,
|
||||
details: {
|
||||
anlaesse: werte.anlaesse,
|
||||
perPush: werte.perPush,
|
||||
perMail: werte.perMail,
|
||||
},
|
||||
})
|
||||
|
||||
revalidatePath('/einstellungen')
|
||||
return { ok: true, meldung: 'Einstellungen gespeichert.' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Schickt eine Probemeldung über alle eingeschalteten Wege.
|
||||
*
|
||||
* Sie umgeht Ruhezeit und Wiederholungssperre bewusst — wer auf den Knopf
|
||||
* drückt, will jetzt wissen, ob es ankommt.
|
||||
*/
|
||||
export async function probeSenden(): Promise<Rueckmeldung> {
|
||||
const geprueft = await akteurOderFehler()
|
||||
if ('antwort' in geprueft) return geprueft.antwort
|
||||
const { akteur } = geprueft
|
||||
|
||||
const zeile = await db.benachrichtigungseinstellung.findUnique({
|
||||
where: { benutzerId: akteur.id },
|
||||
select: {
|
||||
anlaesse: true,
|
||||
perPush: true,
|
||||
perMail: true,
|
||||
ueberblickUm: true,
|
||||
ruheVon: true,
|
||||
ruheBis: true,
|
||||
ruheAmWochenende: true,
|
||||
zeitblockVorlaufMinuten: true,
|
||||
},
|
||||
})
|
||||
const einstellung = alsEinstellung(zeile)
|
||||
|
||||
if (!einstellung.perPush && !einstellung.perMail) {
|
||||
return { ok: false, fehler: 'Erst einen Weg einschalten — Push oder E-Mail.' }
|
||||
}
|
||||
|
||||
const meldung = {
|
||||
anlass: 'tagesueberblick' as const,
|
||||
// Die Probe umgeht Sperre und Ruhezeit; der Schlüssel dient nur der Form.
|
||||
schluessel: `probe:${akteur.id}:${alsTagesschluessel(jetzt())}`,
|
||||
titel: 'Probemeldung',
|
||||
text: 'Wenn du das liest, kommen Benachrichtigungen an.',
|
||||
pfad: '/einstellungen',
|
||||
}
|
||||
|
||||
const wege: string[] = []
|
||||
const einst = umgebung()
|
||||
|
||||
if (einstellung.perPush) {
|
||||
if (!einst.pushAktiv) {
|
||||
wege.push('Push ist auf diesem Server nicht eingerichtet')
|
||||
} else {
|
||||
const bericht = await pushSenden(akteur.id, meldung)
|
||||
wege.push(
|
||||
bericht.zugestellt > 0
|
||||
? `Push an ${bericht.zugestellt} Gerät(e)`
|
||||
: 'kein Gerät für Push angemeldet',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (einstellung.perMail) {
|
||||
if (!einst.mailAktiv) {
|
||||
wege.push('E-Mail ist auf diesem Server nicht eingerichtet')
|
||||
} else {
|
||||
wege.push(
|
||||
(await mailSenden(akteur.email, meldung))
|
||||
? 'E-Mail verschickt'
|
||||
: 'E-Mail fehlgeschlagen',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await auditSchreiben({
|
||||
akteur: { id: akteur.id, email: akteur.email },
|
||||
aktion: 'benachrichtigung.probe',
|
||||
objektTyp: 'Benachrichtigungseinstellung',
|
||||
objektId: akteur.id,
|
||||
})
|
||||
|
||||
return { ok: true, meldung: wege.join(' · ') }
|
||||
}
|
||||
|
||||
/** Entfernt ein angemeldetes Push-Gerät. */
|
||||
export async function geraetEntfernen(daten: FormData): Promise<Rueckmeldung> {
|
||||
const geprueft = await akteurOderFehler()
|
||||
if ('antwort' in geprueft) return geprueft.antwort
|
||||
const { akteur } = geprueft
|
||||
|
||||
const id = daten.get('id')
|
||||
if (typeof id !== 'string' || id.length === 0) {
|
||||
return { ok: false, fehler: 'Ungültige Anfrage.' }
|
||||
}
|
||||
|
||||
const geraet = await db.pushGeraet.findFirst({
|
||||
where: { id, benutzerId: akteur.id },
|
||||
select: { id: true, bezeichnung: true },
|
||||
})
|
||||
if (!geraet) return { ok: false, fehler: 'Dieses Gerät gibt es nicht.' }
|
||||
|
||||
await db.pushGeraet.delete({ where: { id: geraet.id } })
|
||||
|
||||
await auditSchreiben({
|
||||
akteur: { id: akteur.id, email: akteur.email },
|
||||
aktion: 'push.abgemeldet',
|
||||
objektTyp: 'PushGeraet',
|
||||
objektId: geraet.id,
|
||||
details: { bezeichnung: geraet.bezeichnung },
|
||||
})
|
||||
|
||||
revalidatePath('/einstellungen')
|
||||
return { ok: true, meldung: 'Gerät abgemeldet.' }
|
||||
}
|
||||
Reference in New Issue
Block a user