intermittent commit

This commit is contained in:
2026-07-14 13:22:11 +02:00
parent 3444a4920f
commit 6163c8f733
24 changed files with 2367 additions and 0 deletions
@@ -0,0 +1,150 @@
import { useEffect, useMemo, useState } from "react";
import { Mail, Save } from "lucide-react";
import { Button, Card, DismissibleAlert, FormField, ToggleSwitch, type ApiSettings, type AuthInfo } from "@govoplan/core-webui";
import { getNotificationPreferences, updateNotificationPreferences, type NotificationPreferences } from "../../api/notifications";
type Draft = Pick<NotificationPreferences, "show_unread_badge" | "email_enabled" | "email_digest_enabled"> & {
muted_source_modules_text: string;
};
const DEFAULT_DRAFT: Draft = {
show_unread_badge: true,
email_enabled: false,
email_digest_enabled: false,
muted_source_modules_text: ""
};
export default function NotificationSettingsPanel({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
const [loaded, setLoaded] = useState<NotificationPreferences | null>(null);
const [draft, setDraft] = useState<Draft>(DEFAULT_DRAFT);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState("");
const [messageTone, setMessageTone] = useState<"success" | "warning">("success");
const dirty = useMemo(() => {
if (!loaded) return false;
return (
draft.show_unread_badge !== loaded.show_unread_badge ||
draft.email_enabled !== loaded.email_enabled ||
draft.email_digest_enabled !== loaded.email_digest_enabled ||
normalizeMutedModules(draft.muted_source_modules_text).join(",") !== loaded.muted_source_modules.join(",")
);
}, [draft, loaded]);
useEffect(() => {
void loadPreferences();
}, [auth.user.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
async function loadPreferences() {
setLoading(true);
setMessage("");
try {
const preferences = await getNotificationPreferences(settings);
setLoaded(preferences);
setDraft({
show_unread_badge: preferences.show_unread_badge,
email_enabled: preferences.email_enabled,
email_digest_enabled: preferences.email_digest_enabled,
muted_source_modules_text: preferences.muted_source_modules.join(", ")
});
} catch (error) {
setMessageTone("warning");
setMessage(error instanceof Error ? error.message : "Loading notification preferences failed");
} finally {
setLoading(false);
}
}
async function savePreferences() {
setSaving(true);
setMessage("");
try {
const next = await updateNotificationPreferences(settings, {
show_unread_badge: draft.show_unread_badge,
email_enabled: draft.email_enabled,
email_digest_enabled: draft.email_digest_enabled,
muted_source_modules: normalizeMutedModules(draft.muted_source_modules_text)
});
setLoaded(next);
setDraft({
show_unread_badge: next.show_unread_badge,
email_enabled: next.email_enabled,
email_digest_enabled: next.email_digest_enabled,
muted_source_modules_text: next.muted_source_modules.join(", ")
});
window.dispatchEvent(new CustomEvent("govoplan:notifications-changed"));
setMessageTone("success");
setMessage("Notification preferences saved.");
} catch (error) {
setMessageTone("warning");
setMessage(error instanceof Error ? error.message : "Saving notification preferences failed");
} finally {
setSaving(false);
}
}
return (
<div className="dashboard-grid settings-dashboard-grid notifications-settings-panel">
<Card title="Notifications">
<div className="form-grid">
<ToggleSwitch
label="Unread badge"
help="Show unread notification counts in the top bar."
checked={draft.show_unread_badge}
disabled={loading}
onChange={(value) => setDraft((current) => ({ ...current, show_unread_badge: value }))}
/>
<FormField label="Muted source modules" help="Comma-separated module IDs. Filtering will be applied by later delivery rules.">
<input
value={draft.muted_source_modules_text}
onChange={(event) => setDraft((current) => ({ ...current, muted_source_modules_text: event.target.value }))}
placeholder="calendar, campaign"
disabled={loading}
/>
</FormField>
<div className="button-row compact-actions">
<Button variant="primary" onClick={() => void savePreferences()} disabled={loading || saving || !dirty}>
<Save size={16} /> {saving ? "Saving" : "Save preferences"}
</Button>
</div>
{message ? <DismissibleAlert tone={messageTone} resetKey={message} floating>{message}</DismissibleAlert> : null}
</div>
</Card>
<Card title="Delivery">
<div className="form-grid">
<div className="notifications-settings-inline-title">
<Mail size={16} />
<strong>Email notifications</strong>
</div>
<ToggleSwitch
label="Email notifications"
help="Prepared for mail-backed notification delivery."
checked={draft.email_enabled}
disabled={loading}
onChange={(value) => setDraft((current) => ({ ...current, email_enabled: value, email_digest_enabled: value ? current.email_digest_enabled : false }))}
/>
<ToggleSwitch
label="Email digest"
help="Batch eligible notifications into summary messages when delivery rules support it."
checked={draft.email_digest_enabled}
disabled={loading || !draft.email_enabled}
onChange={(value) => setDraft((current) => ({ ...current, email_digest_enabled: value }))}
/>
</div>
</Card>
</div>
);
}
function normalizeMutedModules(value: string): string[] {
const seen = new Set<string>();
return value
.split(",")
.map((item) => item.trim().toLowerCase())
.filter((item) => {
if (!item || seen.has(item)) return false;
seen.add(item);
return true;
});
}