feat: verschlüsselte Integrationseinstellungen, Haushaltsmitglieder mit Ersteller, Backup-Download
This commit is contained in:
1 parent
a1195223e0
commit
267f4fcf70
21 files changed
+724
-30
No files matched your search
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Member = { id: number; name: string };
|
||||
|
||||
export default function MembersSection() {
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function load() {
|
||||
const res = await fetch("/api/members");
|
||||
if (res.ok) setMembers(await res.json());
|
||||
}
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function add() {
|
||||
setError("");
|
||||
const value = name.trim();
|
||||
if (!value) {
|
||||
setError("Bitte einen Namen eingeben.");
|
||||
return;
|
||||
}
|
||||
const res = await fetch("/api/members", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ name: value }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setError(res.status === 409 ? "Dieser Name existiert bereits." : "Speichern fehlgeschlagen.");
|
||||
return;
|
||||
}
|
||||
setName("");
|
||||
load();
|
||||
}
|
||||
|
||||
async function remove(id: number, memberName: string) {
|
||||
if (!window.confirm(`„${memberName}“ aus der Haushaltsliste entfernen? Bereits erfasste Gutscheine behalten den Namen.`)) return;
|
||||
const res = await fetch(`/api/members?id=${id}`, { method: "DELETE" });
|
||||
if (res.ok) load();
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-card border border-line bg-white p-5">
|
||||
<h2 className="text-[15px] font-semibold tracking-tight">Haushaltsmitglieder</h2>
|
||||
<p className="mt-1 text-sm text-muted">Wer einen Gutschein anlegt, wählt hier seinen Namen aus.</p>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
add();
|
||||
}
|
||||
}}
|
||||
placeholder="Name hinzufügen"
|
||||
aria-label="Name hinzufügen"
|
||||
maxLength={100}
|
||||
className="h-11 min-w-0 flex-1 rounded-field border border-line bg-white px-3.5 text-sm text-ink"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={add}
|
||||
className="h-11 shrink-0 rounded-field bg-brand-600 px-5 text-sm font-semibold text-white transition-colors hover:bg-brand-700 active:scale-[0.99]"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<p role="alert" className="mt-2 text-[13px] font-medium text-danger">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{members.length > 0 && (
|
||||
<ul className="mt-3 divide-y divide-line">
|
||||
{members.map((m) => (
|
||||
<li key={m.id} className="flex items-center justify-between py-2 text-sm">
|
||||
<span className="font-medium">{m.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(m.id, m.name)}
|
||||
className="rounded-lg px-2 py-1 text-[13px] font-medium text-muted hover:text-danger"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user