feat: contacts and voucher assignment
This commit is contained in:
1 parent
c86ec8bd36
commit
f2b1d0293a
8 files changed
+154
-4
No files matched your search
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDb } from "@/lib/db";
|
||||
import { validateContact } from "@/lib/contacts";
|
||||
|
||||
export async function GET() {
|
||||
const db = getDb();
|
||||
const all = await db.contact.findMany({ orderBy: { name: "asc" } });
|
||||
return NextResponse.json(all);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const input = {
|
||||
name: String(body.name ?? ""),
|
||||
email: body.email ? String(body.email) : null,
|
||||
telegramChatId: body.telegramChatId ? String(body.telegramChatId) : null,
|
||||
};
|
||||
const errs = validateContact(input);
|
||||
if (errs.length) return NextResponse.json({ errors: errs }, { status: 400 });
|
||||
const db = getDb();
|
||||
const created = await db.contact.create({
|
||||
data: { name: input.name.trim(), email: input.email?.trim() || null, telegramChatId: input.telegramChatId?.trim() || null },
|
||||
});
|
||||
return NextResponse.json(created, { status: 201 });
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export async function GET(_req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const numId = Number(id);
|
||||
if (!Number.isInteger(numId)) return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
const db = getDb();
|
||||
const v = await db.voucher.findUnique({ where: { id: numId } });
|
||||
const v = await db.voucher.findUnique({ where: { id: numId }, include: { contacts: { include: { contact: true } } } });
|
||||
if (!v) return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
return NextResponse.json(v);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ export async function POST(req: NextRequest) {
|
||||
const errs = validateVoucher(input);
|
||||
if (errs.length) return NextResponse.json({ errors: errs }, { status: 400 });
|
||||
const db = getDb();
|
||||
const contactIds: number[] = Array.isArray(body.contactIds)
|
||||
? body.contactIds.map(Number).filter((n: number) => Number.isInteger(n))
|
||||
: [];
|
||||
const created = await db.voucher.create({
|
||||
data: {
|
||||
name: input.name.trim(),
|
||||
@@ -43,6 +46,7 @@ export async function POST(req: NextRequest) {
|
||||
codeText: input.codeText,
|
||||
kategorie: input.kategorie,
|
||||
notiz: input.notiz,
|
||||
contacts: contactIds.length ? { create: contactIds.map((contactId) => ({ contactId })) } : undefined,
|
||||
},
|
||||
});
|
||||
return NextResponse.json(created, { status: 201 });
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { validateContact } from "@/lib/contacts";
|
||||
|
||||
type Contact = { id: number; name: string; email: string | null; telegramChatId: string | null };
|
||||
|
||||
export default function ContactsPage() {
|
||||
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [telegramChatId, setTelegramChatId] = useState("");
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
async function load() {
|
||||
const res = await fetch("/api/contacts");
|
||||
if (res.ok) setContacts(await res.json());
|
||||
}
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const input = { name, email: email || null, telegramChatId: telegramChatId || null };
|
||||
const errs = validateContact(input);
|
||||
setErrors(errs);
|
||||
if (errs.length) return;
|
||||
const res = await fetch("/api/contacts", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setErrors(data.errors ?? ["speichern"]);
|
||||
return;
|
||||
}
|
||||
setName(""); setEmail(""); setTelegramChatId(""); setErrors([]);
|
||||
load();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-xl space-y-4 p-6">
|
||||
<Link href="/" className="text-sm underline">← Zurück</Link>
|
||||
<h1 className="text-xl font-bold">Kontakte</h1>
|
||||
<form onSubmit={onSubmit} className="space-y-3" noValidate>
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium">Name</label>
|
||||
<input id="name" value={name} onChange={(e) => setName(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium">E-Mail</label>
|
||||
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="telegramChatId" className="block text-sm font-medium">Telegram-Chat-ID</label>
|
||||
<input id="telegramChatId" value={telegramChatId} onChange={(e) => setTelegramChatId(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" />
|
||||
</div>
|
||||
{errors.length > 0 && <p className="text-sm text-red-600">Ungültig: {errors.join(", ")}</p>}
|
||||
<button type="submit" className="rounded bg-black px-4 py-2 text-white">Hinzufügen</button>
|
||||
</form>
|
||||
<ul className="divide-y">
|
||||
{contacts.map((c) => (
|
||||
<li key={c.id} className="py-2 text-sm">
|
||||
<span className="font-medium">{c.name}</span>
|
||||
{c.email && <span> · {c.email}</span>}
|
||||
{c.telegramChatId && <span> · Telegram {c.telegramChatId}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type Voucher = {
|
||||
kategorie: string | null;
|
||||
notiz: string | null;
|
||||
status: string;
|
||||
contacts?: { contact: { id: number; name: string; email: string | null; telegramChatId: string | null } }[];
|
||||
};
|
||||
|
||||
export default function VoucherDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
@@ -62,6 +63,9 @@ export default function VoucherDetailPage({ params }: { params: Promise<{ id: st
|
||||
{v.codeText && <div><dt className="font-medium">Code</dt><dd>{v.codeText}</dd></div>}
|
||||
{v.kategorie && <div><dt className="font-medium">Kategorie</dt><dd>{v.kategorie}</dd></div>}
|
||||
{v.notiz && <div><dt className="font-medium">Notiz</dt><dd>{v.notiz}</dd></div>}
|
||||
{v.contacts && v.contacts.length > 0 && (
|
||||
<div><dt className="font-medium">Empfänger</dt><dd>{v.contacts.map((c) => c.contact.name).join(", ")}</dd></div>
|
||||
)}
|
||||
</dl>
|
||||
{v.status !== "eingeloest" && (
|
||||
<button onClick={redeem} className="rounded bg-black px-4 py-2 text-white">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { validateVoucher } from "@/lib/vouchers";
|
||||
|
||||
@@ -21,6 +21,15 @@ export default function NewVoucherPage() {
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [serverError, setServerError] = useState("");
|
||||
const [contacts, setContacts] = useState<{ id: number; name: string }[]>([]);
|
||||
const [contactIds, setContactIds] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/contacts")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then(setContacts)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
function set(name: string, value: string) {
|
||||
setValues((v) => ({ ...v, [name]: value }));
|
||||
@@ -46,7 +55,7 @@ export default function NewVoucherPage() {
|
||||
const res = await fetch("/api/vouchers", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
body: JSON.stringify({ ...input, contactIds }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
@@ -78,7 +87,21 @@ export default function NewVoucherPage() {
|
||||
{errors.includes(f.name) && <p className="mt-1 text-sm text-red-600">Ungültig: {f.name}</p>}
|
||||
</div>
|
||||
))}
|
||||
{/* Task 5 hook: <section id="kontakte">…ContactSelect…</section> */}
|
||||
<section id="kontakte">
|
||||
<h2 className="text-sm font-medium">Empfänger</h2>
|
||||
{contacts.map((c) => (
|
||||
<label key={c.id} className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={contactIds.includes(c.id)}
|
||||
onChange={(e) =>
|
||||
setContactIds((ids) => (e.target.checked ? [...ids, c.id] : ids.filter((x) => x !== c.id)))
|
||||
}
|
||||
/>
|
||||
{c.name}
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
{serverError && <p className="text-sm text-red-600">{serverError}</p>}
|
||||
<button type="submit" className="rounded bg-black px-4 py-2 text-white">
|
||||
Speichern
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export function validateContact(c: { name: string; email: string | null; telegramChatId: string | null }): string[] {
|
||||
const errs: string[] = [];
|
||||
if (!c.name?.trim()) errs.push("name");
|
||||
const email = c.email?.trim() ?? "";
|
||||
const chat = c.telegramChatId?.trim() ?? "";
|
||||
if (!email && !chat) errs.push("kanal");
|
||||
if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) errs.push("email");
|
||||
return errs;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validateContact } from "../lib/contacts";
|
||||
describe("validateContact", () => {
|
||||
it("braucht mindestens einen Kanal", () => {
|
||||
expect(validateContact({ name: "Mama", email: null, telegramChatId: null })).toContain("kanal");
|
||||
});
|
||||
it("lehnt ungültige Mail ab", () => {
|
||||
expect(validateContact({ name: "Mama", email: "keine-mail", telegramChatId: null })).toContain("email");
|
||||
});
|
||||
it("akzeptiert nur Telegram", () => {
|
||||
expect(validateContact({ name: "Papa", email: null, telegramChatId: "12345" })).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user