feat: reminder due calculation
This commit is contained in:
1 parent
f2b1d0293a
commit
6e1d0a7568
4 files changed
+72
-1
No files matched your search
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDb } from "@/lib/db";
|
||||
import { getVoucherStatus } from "@/lib/voucher-status";
|
||||
import { validateVoucher, isExpiringSoon } from "@/lib/vouchers";
|
||||
import { parseReminderDays } from "@/lib/reminders";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const filter = req.nextUrl.searchParams.get("filter");
|
||||
@@ -47,6 +48,11 @@ export async function POST(req: NextRequest) {
|
||||
kategorie: input.kategorie,
|
||||
notiz: input.notiz,
|
||||
contacts: contactIds.length ? { create: contactIds.map((contactId) => ({ contactId })) } : undefined,
|
||||
rules: {
|
||||
create: parseReminderDays(
|
||||
typeof body.reminderDays === "string" ? body.reminderDays : Array.isArray(body.reminderDays) ? body.reminderDays.join(",") : undefined
|
||||
).map((tageVorher) => ({ tageVorher })),
|
||||
},
|
||||
},
|
||||
});
|
||||
return NextResponse.json(created, { status: 201 });
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function NewVoucherPage() {
|
||||
const [serverError, setServerError] = useState("");
|
||||
const [contacts, setContacts] = useState<{ id: number; name: string }[]>([]);
|
||||
const [contactIds, setContactIds] = useState<number[]>([]);
|
||||
const [reminderDays, setReminderDays] = useState("30,6,1");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/contacts")
|
||||
@@ -55,7 +56,7 @@ export default function NewVoucherPage() {
|
||||
const res = await fetch("/api/vouchers", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...input, contactIds }),
|
||||
body: JSON.stringify({ ...input, contactIds, reminderDays }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
@@ -87,6 +88,20 @@ export default function NewVoucherPage() {
|
||||
{errors.includes(f.name) && <p className="mt-1 text-sm text-red-600">Ungültig: {f.name}</p>}
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<label htmlFor="reminderDays" className="block text-sm font-medium">
|
||||
Erinnerung (Tage vorher, Kommaliste)
|
||||
</label>
|
||||
<input
|
||||
id="reminderDays"
|
||||
name="reminderDays"
|
||||
type="text"
|
||||
value={reminderDays}
|
||||
onChange={(e) => setReminderDays(e.target.value)}
|
||||
placeholder="30,6,1"
|
||||
className="mt-1 w-full rounded border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<section id="kontakte">
|
||||
<h2 className="text-sm font-medium">Empfänger</h2>
|
||||
{contacts.map((c) => (
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export const DEFAULT_REMINDER_DAYS = [30, 6, 1];
|
||||
|
||||
export function toDateKey(d: Date): string {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function dueRulesToday(
|
||||
v: { ablaufdatum: Date | null; status: string },
|
||||
rules: { id: number; tageVorher: number }[],
|
||||
heute: Date
|
||||
): number[] {
|
||||
if (v.status !== "aktiv" || !v.ablaufdatum) return [];
|
||||
const out: number[] = [];
|
||||
for (const r of rules) {
|
||||
const ziel = new Date(v.ablaufdatum);
|
||||
ziel.setDate(ziel.getDate() - r.tageVorher);
|
||||
if (toDateKey(ziel) === toDateKey(heute)) out.push(r.id);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function idempotencyKey(ruleId: number, kanal: string, empfaenger: string, zielDatum: string): string {
|
||||
return `${ruleId}|${kanal}|${empfaenger}|${zielDatum}`;
|
||||
}
|
||||
|
||||
export function parseReminderDays(raw: string | null | undefined): number[] {
|
||||
if (raw == null || !raw.trim()) return [...DEFAULT_REMINDER_DAYS];
|
||||
const days = raw
|
||||
.split(",")
|
||||
.map((s) => Number(s.trim()))
|
||||
.filter((n) => Number.isInteger(n) && n >= 0);
|
||||
return [...new Set(days)].sort((a, b) => b - a);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { dueRulesToday, idempotencyKey } from "../lib/reminders";
|
||||
describe("dueRulesToday", () => {
|
||||
it("30/6/1 trifft am richtigen Tag", () => {
|
||||
const ablauf = new Date("2026-12-01T00:00:00");
|
||||
const rules = [{ id: 1, tageVorher: 30 }, { id: 2, tageVorher: 6 }, { id: 3, tageVorher: 1 }];
|
||||
expect(dueRulesToday({ ablaufdatum: ablauf, status: "aktiv" }, rules, new Date("2026-11-01T12:00:00"))).toEqual([1]);
|
||||
expect(dueRulesToday({ ablaufdatum: ablauf, status: "aktiv" }, rules, new Date("2026-11-30T08:00:00"))).toEqual([3]);
|
||||
});
|
||||
it("kein Ablaufdatum und eingelöst nie fällig", () => {
|
||||
expect(dueRulesToday({ ablaufdatum: null, status: "aktiv" }, [{ id: 1, tageVorher: 7 }], new Date())).toEqual([]);
|
||||
expect(dueRulesToday({ ablaufdatum: new Date(), status: "eingeloest" }, [{ id: 1, tageVorher: 0 }], new Date())).toEqual([]);
|
||||
});
|
||||
it("key ist stabil", () => {
|
||||
expect(idempotencyKey(1, "mail", "a@b.de", "2026-12-01")).toBe("1|mail|a@b.de|2026-12-01");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user