feat: verschlüsselte Integrationseinstellungen, Haushaltsmitglieder mit Ersteller, Backup-Download

This commit is contained in:
opencode committed 2026-10-18 14:19:03 +02:00
1 parent a1195223e0
commit 267f4fcf70
21 files changed
+724 -30

No files matched your search

+2
View File
@@ -17,6 +17,7 @@ type Voucher = {
kategorie: string | null;
notiz: string | null;
status: string;
createdBy?: string | null;
providerDomain?: string | null;
logoUrl?: string | null;
contacts?: { contact: { id: number; name: string; email: string | null; telegramChatId: string | null } }[];
@@ -94,6 +95,7 @@ export default function VoucherDetailPage({ params }: { params: Promise<{ id: st
<Row label="Wert">{formatEuro(v.wertCents)}</Row>
<Row label="Restguthaben">{formatEuro(v.restCents)}</Row>
<Row label="Status">{status === "aktiv" ? "Aktiv" : status === "abgelaufen" ? "Abgelaufen" : "Eingelöst"}</Row>
<Row label="Angelegt von">{v.createdBy ? v.createdBy : "Nicht erfasst"}</Row>
{v.ablaufdatum && (
<Row label="Ablaufdatum">{new Date(v.ablaufdatum).toLocaleDateString("de-DE")}</Row>
)}
+31 -1
View File
@@ -31,6 +31,8 @@ export default function NewVoucherPage() {
const [serverError, setServerError] = useState("");
const [contacts, setContacts] = useState<{ id: number; name: string }[]>([]);
const [contactIds, setContactIds] = useState<number[]>([]);
const [members, setMembers] = useState<{ id: number; name: string }[]>([]);
const [memberId, setMemberId] = useState("");
const [reminderDays, setReminderDays] = useState<number[]>([...DEFAULT_REMINDER_DAYS]);
const [candidates, setCandidates] = useState<LogoCandidate[]>([]);
const [logoSearching, setLogoSearching] = useState(false);
@@ -43,6 +45,10 @@ export default function NewVoucherPage() {
.then((r) => (r.ok ? r.json() : []))
.then(setContacts)
.catch(() => {});
fetch("/api/members")
.then((r) => (r.ok ? r.json() : []))
.then(setMembers)
.catch(() => {});
}, []);
function set(name: string, value: string) {
@@ -101,7 +107,7 @@ export default function NewVoucherPage() {
const res = await fetch("/api/vouchers", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...input, contactIds, reminderDays, providerDomain: selectedLogo?.domain ?? null }),
body: JSON.stringify({ ...input, contactIds, reminderDays, providerDomain: selectedLogo?.domain ?? null, memberId: memberId === "" ? null : Number(memberId) }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
@@ -121,6 +127,30 @@ export default function NewVoucherPage() {
<h1 className="mt-2 text-2xl font-semibold tracking-tight">Gutschein anlegen</h1>
<p className="mt-1 text-sm text-muted">Alle Angaben lassen sich später noch ändern.</p>
<form onSubmit={onSubmit} className="mt-5 rounded-card border border-line bg-white p-5 sm:p-6" noValidate>
<div className="mb-4">
<label htmlFor="memberId" className="block text-[13px] font-semibold">
Angelegt von
</label>
{members.length === 0 ? (
<p className="mt-1.5 text-[13px] text-muted">
Noch keine Haushaltsmitglieder. Lege sie unter <Link href="/settings" className="font-semibold text-brand-700 underline">Einstellungen</Link> an.
</p>
) : (
<select
id="memberId"
value={memberId}
onChange={(e) => setMemberId(e.target.value)}
className={`${inputClass} appearance-none`}
>
<option value="">Nicht erfasst</option>
{members.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
{FIELDS.map((f) => (
<div key={f.name} className={f.name === "notiz" ? "sm:col-span-2" : undefined}>