Database: - notification_preferences table (user_id, tenant_id, reminder days, email/digest toggles) - notifications table (type, entity link, read/sent tracking, dedup index) Backend: - NotificationService with background goroutine checking reminders hourly - CheckDeadlineReminders: finds deadlines due in N days per user prefs, creates notifications - Overdue deadline detection and notification - Daily digest at 8am: compiles pending notifications into one email - SendEmail via `m mail send` CLI command - Deduplication: same notification type + entity + day = skip - API: GET/PATCH notifications, unread count, mark read/all-read - API: GET/PUT notification-preferences with upsert Frontend: - NotificationBell in header with unread count badge (polls every 30s) - Dropdown panel with notification list, type-colored dots, time-ago, entity links - Mark individual/all as read - NotificationSettings in Einstellungen page: reminder day toggles, email toggle, digest toggle
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Settings, Calendar, Users, Bell } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { api } from "@/lib/api";
|
|
import type { Tenant } from "@/lib/types";
|
|
import { CalDAVSettings } from "@/components/settings/CalDAVSettings";
|
|
import { NotificationSettings } from "@/components/settings/NotificationSettings";
|
|
import { SkeletonCard } from "@/components/ui/Skeleton";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
|
|
export default function EinstellungenPage() {
|
|
const tenantId =
|
|
typeof window !== "undefined"
|
|
? localStorage.getItem("kanzlai_tenant_id")
|
|
: null;
|
|
|
|
const {
|
|
data: tenant,
|
|
isLoading,
|
|
error,
|
|
refetch,
|
|
} = useQuery({
|
|
queryKey: ["tenant-current", tenantId],
|
|
queryFn: () => api.get<Tenant>(`/tenants/${tenantId}`),
|
|
enabled: !!tenantId,
|
|
});
|
|
|
|
return (
|
|
<div className="mx-auto max-w-3xl space-y-6 p-4 sm:p-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-lg font-semibold text-neutral-900">
|
|
Einstellungen
|
|
</h1>
|
|
<Link
|
|
href="/einstellungen/team"
|
|
className="inline-flex items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
|
|
>
|
|
<Users className="h-3.5 w-3.5" />
|
|
Team verwalten
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Tenant Info */}
|
|
{isLoading ? (
|
|
<>
|
|
<SkeletonCard />
|
|
<SkeletonCard />
|
|
</>
|
|
) : error ? (
|
|
<EmptyState
|
|
icon={Settings}
|
|
title="Fehler beim Laden"
|
|
description="Einstellungen konnten nicht geladen werden."
|
|
action={
|
|
<button
|
|
onClick={() => refetch()}
|
|
className="rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-neutral-800"
|
|
>
|
|
Erneut versuchen
|
|
</button>
|
|
}
|
|
/>
|
|
) : tenant ? (
|
|
<>
|
|
{/* Kanzlei Info */}
|
|
<section className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<div className="flex items-center gap-2.5 border-b border-neutral-100 pb-3">
|
|
<Settings className="h-4 w-4 text-neutral-500" />
|
|
<h2 className="text-sm font-semibold text-neutral-900">
|
|
Kanzlei
|
|
</h2>
|
|
</div>
|
|
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
|
<div>
|
|
<p className="text-xs text-neutral-500">Name</p>
|
|
<p className="text-sm font-medium text-neutral-900">
|
|
{tenant.name}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-neutral-500">Slug</p>
|
|
<p className="text-sm font-medium text-neutral-900">
|
|
{tenant.slug}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-neutral-500">Erstellt am</p>
|
|
<p className="text-sm text-neutral-700">
|
|
{new Date(tenant.created_at).toLocaleDateString("de-DE", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Notification Settings */}
|
|
<section className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<div className="flex items-center gap-2.5 border-b border-neutral-100 pb-3">
|
|
<Bell className="h-4 w-4 text-neutral-500" />
|
|
<h2 className="text-sm font-semibold text-neutral-900">
|
|
Benachrichtigungen
|
|
</h2>
|
|
</div>
|
|
<div className="mt-4">
|
|
<NotificationSettings />
|
|
</div>
|
|
</section>
|
|
|
|
{/* CalDAV Settings */}
|
|
<section className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<div className="flex items-center gap-2.5 border-b border-neutral-100 pb-3">
|
|
<Calendar className="h-4 w-4 text-neutral-500" />
|
|
<h2 className="text-sm font-semibold text-neutral-900">
|
|
CalDAV-Synchronisierung
|
|
</h2>
|
|
</div>
|
|
<div className="mt-4">
|
|
<CalDAVSettings tenant={tenant} />
|
|
</div>
|
|
</section>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|