Files
gutscheintracker/app/settings/tabs.tsx
T

221 lines
8.5 KiB
TypeScript

"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense } from "react";
import Link from "next/link";
import UsersSection from "./users";
import { HouseholdPanel } from "./household-panel";
import ServerHouseholds from "./server-households";
import { TestButtons } from "./test-buttons";
import { SmtpSection, TelegramSection, LogoSection, OidcSection, useIntegrationStatus } from "./integrations-form";
export type SettingsTab = "haushalt" | "benachrichtigungen" | "dienste" | "daten";
const TABS: { key: SettingsTab; label: string; adminOnly: boolean }[] = [
{ key: "haushalt", label: "Haushalt", adminOnly: false },
{ key: "benachrichtigungen", label: "Benachrichtigungen", adminOnly: false },
{ key: "dienste", label: "Dienste", adminOnly: true },
{ key: "daten", label: "Daten & Backup", adminOnly: true },
];
export default function SettingsTabs({
isAdmin,
isHouseholdAdmin,
ownId,
ownEmail,
householdId,
householdName,
defaultReminderText,
lastCronText,
}: {
isAdmin: boolean;
isHouseholdAdmin: boolean;
ownId: number | null;
ownEmail: string | null;
householdId: number | null;
householdName: string | null;
defaultReminderText: string;
lastCronText: string | null;
}) {
return (
<Suspense>
<TabsInner
isAdmin={isAdmin}
isHouseholdAdmin={isHouseholdAdmin}
ownId={ownId}
ownEmail={ownEmail}
householdId={householdId}
householdName={householdName}
defaultReminderText={defaultReminderText}
lastCronText={lastCronText}
/>
</Suspense>
);
}
function TabsInner({
isAdmin,
isHouseholdAdmin,
ownId,
ownEmail,
householdId,
householdName,
defaultReminderText,
lastCronText,
}: {
isAdmin: boolean;
isHouseholdAdmin: boolean;
ownId: number | null;
ownEmail: string | null;
householdId: number | null;
householdName: string | null;
defaultReminderText: string;
lastCronText: string | null;
}) {
const router = useRouter();
const params = useSearchParams();
const raw = params.get("tab");
const active: SettingsTab =
raw === "benachrichtigungen" || raw === "dienste" || raw === "daten" ? raw : "haushalt";
const visible = TABS.filter((t) => isAdmin || !t.adminOnly);
const shown = visible.some((t) => t.key === active) ? active : "haushalt";
function select(tab: SettingsTab) {
const q = new URLSearchParams(params.toString());
if (tab === "haushalt") q.delete("tab");
else q.set("tab", tab);
const s = q.toString();
router.replace(s ? `/settings?${s}` : "/settings", { scroll: false });
}
const { status, setStatus, loadError } = useIntegrationStatus();
const integrationsReady = isAdmin && status !== null;
return (
<div className="mt-5">
<div className="flex flex-wrap gap-2" role="tablist" aria-label="Einstellungsbereiche">
{visible.map((t) => (
<button
key={t.key}
type="button"
role="tab"
aria-selected={shown === t.key}
onClick={() => select(t.key)}
className={`rounded-full border px-4 py-2 text-[13px] font-semibold transition-colors ${
shown === t.key
? "border-brand-700 bg-brand-600 text-white"
: "border-line bg-white text-ink hover:border-brand-600 hover:text-brand-700"
}`}
>
{t.label}
</button>
))}
</div>
<div className="mt-4 grid gap-4" role="tabpanel">
{shown === "haushalt" && (
<>
<section className="rounded-card border border-line bg-white p-5">
<h2 className="text-[15px] font-semibold tracking-tight">Kontakte</h2>
<p className="mt-1 text-sm text-muted">Wer soll an eure Gutscheine erinnert werden?</p>
<Link
href="/contacts"
className="mt-3 inline-block rounded-field bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-700"
>
Kontakte verwalten
</Link>
</section>
{isAdmin && <ServerHouseholds />}
{isAdmin && ownId !== null && <UsersSection ownId={ownId} ownEmail={ownEmail} />}
{!isAdmin && isHouseholdAdmin && householdId !== null && householdName !== null && ownId !== null && (
<HouseholdPanel householdId={householdId} householdName={householdName} ownId={ownId} ownEmail={ownEmail} />
)}
{!isAdmin && !isHouseholdAdmin && (
<section className="rounded-card border border-line bg-white p-5">
<h2 className="text-[15px] font-semibold tracking-tight">Noch kein Haushalt</h2>
<p className="mt-1 text-sm text-muted">
Deine Gutscheine sind privat. Bitte einen Haushalts-Admin um eine Einladung oder warte auf die Zuordnung durch den Server-Admin.
</p>
</section>
)}
</>
)}
{shown === "benachrichtigungen" && (
<>
<section className="rounded-card border border-line bg-white p-5">
<h2 className="text-[15px] font-semibold tracking-tight">Standard-Erinnerungstage</h2>
<p className="mt-1 text-sm text-muted">{defaultReminderText}</p>
</section>
{isAdmin ? (
<>
{loadError ? (
<p role="alert" className="text-sm font-medium text-danger">{loadError}</p>
) : !integrationsReady ? (
<p className="text-sm text-muted">Wird geladen …</p>
) : (
<>
<SmtpSection status={status} onSaved={setStatus} />
<TelegramSection status={status} onSaved={setStatus} />
</>
)}
<TestButtons />
</>
) : (
<p className="text-sm text-muted">SMTP und Telegram werden vom Admin eingerichtet.</p>
)}
<section className="rounded-card border border-line bg-white p-5">
<h2 className="text-[15px] font-semibold tracking-tight">Letzter Cron-Lauf</h2>
<p className="mt-1 text-sm text-muted">{lastCronText ?? "Noch keine Benachrichtigung versendet."}</p>
</section>
</>
)}
{shown === "dienste" && isAdmin && (
<>
{loadError ? (
<p role="alert" className="text-sm font-medium text-danger">{loadError}</p>
) : !integrationsReady ? (
<p className="text-sm text-muted">Wird geladen …</p>
) : (
<>
<LogoSection status={status} onSaved={setStatus} />
<OidcSection status={status} onSaved={setStatus} />
</>
)}
</>
)}
{shown === "daten" && isAdmin && (
<>
<section className="rounded-card border border-line bg-white p-5">
<h2 className="text-[15px] font-semibold tracking-tight">Backup</h2>
<p className="mt-1 text-sm text-muted">
Gesamte Datenbank als Datei sichern, inklusive Gutscheine, Kontakte, Mitglieder und verschlüsselte Einstellungen.
</p>
<a
href="/api/settings/backup"
download
className="mt-3 inline-block rounded-field bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-700"
>
Datenbank-Backup herunterladen
</a>
<p className="mt-2 text-sm text-muted">
Wiederherstellen am Server: Datei als <code>/data/gutscheine.db</code> ablegen. Derselbe{" "}
<code>SETTINGS_ENCRYPTION_KEY</code> muss erhalten bleiben.
</p>
</section>
<section className="rounded-card border border-line bg-white p-5">
<h2 className="text-[15px] font-semibold tracking-tight">Konfiguration (.env)</h2>
<div className="mt-1 text-sm text-muted">
<p>APP_USER, APP_PASSWORD_HASH, ADMIN_EMAIL, SESSION_SECRET, SETTINGS_ENCRYPTION_KEY, DATABASE_URL, CRON_SECRET, APP_URL, ALLOW_PASSWORD_LOGIN. SMTP, Telegram, Logo.dev und OIDC lassen sich oben in der App verwalten; <code>.env</code> dient als Rückfall.</p>
<p className="mt-2">Zusätzlich manuell: Volume <code>gutschein-data</code> bzw. Datei <code>/data/gutscheine.db</code> regelmäßig kopieren.</p>
</div>
</section>
</>
)}
</div>
</div>
);
}