"use client"; import { useState, useMemo, useRef, useEffect, useCallback } from "react"; import { ChevronDown, ChevronRight, Archive } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; import { type BookingStatus, type BookingFilter, type BaseBooking, type BookingGroup, sortByStatus } from "./types"; import { 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; filter: BookingFilter; onItemsChange: (fn: (prev: T[]) => T[]) => void; onDataChange?: () => void; groups?: BookingGroup[]; renderExtra?: (item: T) => React.ReactNode; onConfirm?: (id: number) => void; } export function GenericBookingsList({ items, endpoint, filter, onItemsChange, onDataChange, groups, renderExtra, onConfirm, }: GenericBookingsListProps) { const [showArchived, setShowArchived] = useState(false); const [expanded, setExpanded] = useState>({}); const [highlightId, setHighlightId] = useState(null); const highlightRef = useRef(null); const { showError } = useToast(); // Scroll to highlighted card and clear highlight after animation useEffect(() => { if (highlightId === null) return; const timer = setTimeout(() => { highlightRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }); }, 50); const clear = setTimeout(() => setHighlightId(null), 2000); return () => { clearTimeout(timer); clearTimeout(clear); }; }, [highlightId]); 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; // Move changed item to front so it appears first in its status group after sort onItemsChange((list) => { const item = list.find((b) => b.id === id); if (!item) return list; return [{ ...item, status }, ...list.filter((b) => b.id !== id)]; }); setHighlightId(id); 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(); onDataChange?.(); } 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)); onDataChange?.(); } 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) { const isHighlighted = highlightId === item.id; 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); const allArchived = activeGroups.length === 0 && archivedCount > 0 && filter === "all"; function renderGroup(group: BookingGroup) { const isOpen = expanded[group.key] ?? !group.isArchived; const groupCounts = { new: 0, contacted: 0, confirmed: 0, declined: 0 }; for (const item of group.items) groupCounts[item.status] = (groupCounts[item.status] || 0) + 1; return (
{isOpen && (() => { const regular = group.items.filter((i) => !i.notes?.includes("Лист ожидания")); const waiting = group.items.filter((i) => i.notes?.includes("Лист ожидания")); return (
{regular.map((item) => renderItem(item, group.isArchived))} {waiting.length > 0 && ( <>
лист ожидания
{waiting.map((item) => renderItem(item, group.isArchived))} )}
); })()}
); } return (
{allArchived && (

Все записи в архиве

)} {!allArchived && (
{activeGroups.length === 0 && archivedGroups.length === 0 && } {activeGroups.map(renderGroup)}
)} {archivedCount > 0 && (
{(showArchived || allArchived) && (
{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))}
); }