"use client"; import { useState, useMemo } from "react"; import { ChevronDown, ChevronRight, Archive } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; import { type BookingStatus, type BookingFilter, type BaseBooking, type BookingGroup, countStatuses, sortByStatus } from "./types"; import { FilterTabs, EmptyState, BookingCard, ContactLinks, StatusBadge, StatusActions, DeleteBtn } from "./BookingComponents"; import { fmtDate } from "./types"; import { InlineNotes } from "./InlineNotes"; import { useToast } from "./Toast"; interface GenericBookingsListProps { items: T[]; endpoint: string; onItemsChange: (fn: (prev: T[]) => T[]) => void; groups?: BookingGroup[]; renderExtra?: (item: T) => React.ReactNode; onConfirm?: (id: number) => void; } export function GenericBookingsList({ items, endpoint, onItemsChange, groups, renderExtra, onConfirm, }: GenericBookingsListProps) { const [filter, setFilter] = useState("all"); const [showArchived, setShowArchived] = useState(false); const [expanded, setExpanded] = useState>({}); const { showError } = useToast(); const counts = useMemo(() => countStatuses(items), [items]); async function handleStatus(id: number, status: BookingStatus) { if (status === "confirmed" && onConfirm) { onConfirm(id); return; } const prev = items.find((b) => b.id === id); const prevStatus = prev?.status; onItemsChange((list) => list.map((b) => b.id === id ? { ...b, status } : b)); try { const res = await adminFetch(endpoint, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "set-status", id, status }), }); if (!res.ok) throw new Error(); } catch { if (prevStatus) onItemsChange((list) => list.map((b) => b.id === id ? { ...b, status: prevStatus } : b)); showError("Не удалось обновить статус"); } } async function handleDelete(id: number) { try { const res = await adminFetch(`${endpoint}?id=${id}`, { method: "DELETE" }); if (!res.ok) throw new Error(); onItemsChange((list) => list.filter((b) => b.id !== id)); } catch { showError("Не удалось удалить запись"); } } async function handleNotes(id: number, notes: string) { const prev = items.find((b) => b.id === id); const prevNotes = prev?.notes; onItemsChange((list) => list.map((b) => b.id === id ? { ...b, notes: notes || undefined } : b)); try { const res = await adminFetch(endpoint, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "set-notes", id, notes }), }); if (!res.ok) throw new Error(); } catch { onItemsChange((list) => list.map((b) => b.id === id ? { ...b, notes: prevNotes } : b)); showError("Не удалось сохранить заметку"); } } function renderItem(item: T, isArchived: boolean) { return (
{item.name} {renderExtra?.(item)}
{fmtDate(item.createdAt)} handleDelete(item.id)} name={item.name} />
{!isArchived && handleStatus(item.id, s)} />}
handleNotes(item.id, notes)} />
); } if (groups) { const filteredGroups = groups.map((g) => ({ ...g, items: filter === "all" ? sortByStatus(g.items) : sortByStatus(g.items.filter((b) => b.status === filter)), })).filter((g) => g.items.length > 0); const activeGroups = filteredGroups.filter((g) => !g.isArchived); const archivedGroups = filteredGroups.filter((g) => g.isArchived); const archivedCount = archivedGroups.reduce((sum, g) => sum + g.items.length, 0); function renderGroup(group: BookingGroup) { const isOpen = expanded[group.key] ?? !group.isArchived; const groupCounts = countStatuses(group.items); return (
{isOpen && (
{group.items.map((item) => renderItem(item, group.isArchived))}
)}
); } return (
{activeGroups.length === 0 && archivedGroups.length === 0 && } {activeGroups.map(renderGroup)}
{archivedCount > 0 && (
{showArchived && (
{archivedGroups.map(renderGroup)}
)}
)}
); } const filtered = useMemo(() => { const list = filter === "all" ? items : items.filter((b) => b.status === filter); return sortByStatus(list); }, [items, filter]); return (
{filtered.length === 0 && } {filtered.map((item) => renderItem(item, false))}
); }