import { FormEvent, useEffect, useState } from "react"; import { Pencil, Plus, Repeat, Trash2, X } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { HelpLink } from "@/components/HelpLink"; import { OrganizationSelect } from "@/components/OrganizationSelect"; import { createRecurringBilling, deleteRecurringBilling, getRecurringBillings, updateRecurringBilling } from "../api"; import { currentDay, formatMinutes } from "../format"; import { ticketPattern } from "../timers"; import type { RecurringBilling, WorkType } from "../types"; type RecurringSlotFormState = { id?: string; weekday: number | null; startTime: string; durationHours: string; }; type RecurringFormState = { ticketNumber: string; organizationId: string; organizationName: string | null; activity: string; workType: WorkType; recurrenceType: "weekly" | "every_n_weeks"; intervalValue: number; validFrom: string; validUntil: string; slots: RecurringSlotFormState[]; }; const weekdays = [ { value: 1, label: "Mo" }, { value: 2, label: "Di" }, { value: 3, label: "Mi" }, { value: 4, label: "Do" }, { value: 5, label: "Fr" }, { value: 6, label: "Sa" }, { value: 0, label: "So" } ]; function emptyRecurringForm(): RecurringFormState { return { ticketNumber: "", organizationId: "", organizationName: null, activity: "", workType: "support", recurrenceType: "weekly", intervalValue: 1, validFrom: currentDay(), validUntil: "", slots: [{ weekday: 4, startTime: "09:00", durationHours: "4" }] }; } function formatDateLabel(value: string) { return new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" }).format(new Date(`${value}T00:00:00`)); } export function RecurringBillingsPage() { const [recurringBillings, setRecurringBillings] = useState([]); const [recurringForm, setRecurringForm] = useState(() => emptyRecurringForm()); const [loadingRecurring, setLoadingRecurring] = useState(false); const [creatingRecurring, setCreatingRecurring] = useState(false); const [updatingRecurringId, setUpdatingRecurringId] = useState(null); const [editingBillingId, setEditingBillingId] = useState(null); useEffect(() => { void loadRecurringBillings(); }, []); async function loadRecurringBillings() { setLoadingRecurring(true); try { const result = await getRecurringBillings(); setRecurringBillings(result.recurringBillings); } catch (error) { toast.error("Fixe Abrechnungen konnten nicht geladen werden", { description: error instanceof Error ? error.message : "Unbekannter Fehler" }); } finally { setLoadingRecurring(false); } } function changeRecurringType(nextType: "weekly" | "every_n_weeks") { setRecurringForm((current) => ({ ...current, recurrenceType: nextType, intervalValue: nextType === "weekly" ? 1 : current.intervalValue || 1, slots: nextType === "weekly" ? [{ weekday: 4, startTime: current.slots[0]?.startTime ?? "09:00", durationHours: current.slots[0]?.durationHours ?? "4" }] : [{ weekday: null, startTime: current.slots[0]?.startTime ?? "09:00", durationHours: current.slots[0]?.durationHours ?? "4" }] })); } function durationHoursLabel(minutes: number) { const hours = minutes / 60; return Number.isInteger(hours) ? String(hours) : String(hours).replace(".", ","); } function startEditing(billing: RecurringBilling) { setEditingBillingId(billing.id); setRecurringForm({ ticketNumber: billing.configured_ticket_number ?? "", organizationId: billing.organization_id, organizationName: billing.organization_name, activity: billing.activity, workType: billing.work_type, recurrenceType: billing.recurrence_type, intervalValue: billing.recurrence_type === "weekly" ? 1 : billing.interval_value, validFrom: billing.valid_from, validUntil: billing.valid_until ?? "", slots: billing.slots.map((slot) => ({ id: slot.id, weekday: billing.recurrence_type === "weekly" ? slot.weekday : null, startTime: slot.start_time?.slice(0, 5) ?? "09:00", durationHours: durationHoursLabel(slot.duration_minutes) })) }); window.scrollTo({ top: 0, behavior: "smooth" }); } function cancelEditing() { setEditingBillingId(null); setRecurringForm(emptyRecurringForm()); } function updateRecurringSlot(index: number, patch: Partial) { setRecurringForm((current) => ({ ...current, slots: current.slots.map((slot, slotIndex) => (slotIndex === index ? { ...slot, ...patch } : slot)) })); } async function submitRecurringBilling(event: FormEvent) { event.preventDefault(); if (!recurringForm.organizationId) { toast.error("Organisation wählen"); return; } const trimmedTicketNumber = recurringForm.ticketNumber.trim(); if (trimmedTicketNumber && !ticketPattern.test(trimmedTicketNumber)) { toast.error("Ticketnummer prüfen", { description: "Leer lassen für Fix#ID oder Ticket#XXXXXX eintragen." }); return; } let slots: Array<{ id?: string; weekday: number | null; startTime: string; durationMinutes: number }>; try { slots = recurringForm.slots.map((slot) => { const hours = Number(slot.durationHours.replace(",", ".")); if (!Number.isFinite(hours) || hours <= 0) { throw new Error("Dauer prüfen"); } return { id: slot.id, weekday: recurringForm.recurrenceType === "weekly" ? slot.weekday : null, startTime: slot.startTime, durationMinutes: Math.round(hours * 60) }; }); } catch (error) { toast.error(error instanceof Error ? error.message : "Dauer prüfen"); return; } setCreatingRecurring(true); try { const payload = { ticketNumber: trimmedTicketNumber || null, organizationId: recurringForm.organizationId, activity: recurringForm.activity, workType: recurringForm.workType, recurrenceType: recurringForm.recurrenceType, intervalValue: recurringForm.recurrenceType === "weekly" ? 1 : recurringForm.intervalValue, validFrom: recurringForm.validFrom, validUntil: recurringForm.validUntil || null, slots }; const result = editingBillingId ? await updateRecurringBilling(editingBillingId, payload) : await createRecurringBilling(payload); setRecurringBillings(result.recurringBillings); setRecurringForm(emptyRecurringForm()); setEditingBillingId(null); toast.success(editingBillingId ? "Fixe Abrechnung gespeichert" : "Fixe Abrechnung angelegt", { description: "Die Sessions erscheinen automatisch in Tickets, sobald du den Zeitraum öffnest. Nicht mehr gültige erzeugte Sessions werden entfernt." }); } catch (error) { toast.error("Fixe Abrechnung konnte nicht angelegt werden", { description: error instanceof Error ? error.message : "Unbekannter Fehler" }); } finally { setCreatingRecurring(false); } } async function removeRecurring(billing: RecurringBilling) { if (!window.confirm(`Fixe Abrechnung ${billing.ticket_number} wirklich löschen? Dadurch werden auch alle daraus erzeugten Sessions entfernt.`)) { return; } setUpdatingRecurringId(billing.id); try { const result = await deleteRecurringBilling(billing.id); setRecurringBillings(result.recurringBillings); toast.success("Fixe Abrechnung gelöscht", { description: "Die daraus erzeugten Sessions wurden entfernt." }); } catch (error) { toast.error("Fixe Abrechnung konnte nicht gelöscht werden", { description: error instanceof Error ? error.message : "Unbekannter Fehler" }); } finally { setUpdatingRecurringId(null); } } return (

Fixe Abrechnungen

Wiederkehrende Zeiten für deinen Account verwalten.

{recurringBillings.length} Regel(n)
{editingBillingId ? "Regel bearbeiten" : "Neue Regel"}
Ticket ist optional. Leer bedeutet automatische Gruppierung als Fix#ID.
setRecurringForm({ ...recurringForm, ticketNumber: event.currentTarget.value })} /> {recurringForm.ticketNumber.trim() && !ticketPattern.test(recurringForm.ticketNumber.trim()) ? (

Leer lassen oder Ticket#XXXXXX

) : null}
setRecurringForm({ ...recurringForm, organizationId: organization.id, organizationName: organization.name })} required />
setRecurringForm({ ...recurringForm, validFrom: event.currentTarget.value })} required />
setRecurringForm({ ...recurringForm, validUntil: event.currentTarget.value })} />
{recurringForm.recurrenceType === "every_n_weeks" ? (
setRecurringForm({ ...recurringForm, intervalValue: Number(event.currentTarget.value) || 1 })} required />
) : null}