"use client"; /* eslint-disable react-hooks/set-state-in-effect */ import React, { useState, useTransition, useEffect, useMemo, useCallback, useRef } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { publishStop, deleteStop } from "@/actions/stops"; import { AdminSearchInput, AdminFilterTabs, AdminButton, AdminIconButton, AdminViewModeTabs, useToast, Skeleton, AdminPagination, } from "@/components/admin/design-system"; import ScheduleImportModal from "@/components/admin/ScheduleImportModal"; import EditStopModal from "@/components/admin/EditStopModal"; type Stop = { id: string; city: string; state: string; date: string; time: string; location: string; active: boolean; brand_id: string; status?: string; address?: string | null; zip?: string | null; cutoff_time?: string | null; brands: { name: string } | { name: string }[] | null; }; type Props = { stops: Stop[]; hideInternalFilterBar?: boolean; }; type ViewMode = "table" | "cards"; type SortField = "date" | "city" | "location" | "status"; type SortDirection = "asc" | "desc"; const TAB_NUMERIC: Record<"all" | "active" | "inactive" | "draft", "all" | "active" | "inactive" | "draft"> = { all: "all", active: "active", inactive: "inactive", draft: "draft", }; // Icons const Icons = { search: (className: string) => ( ), plus: (className: string) => ( ), pin: (className: string) => ( ), upload: (className: string) => ( ), }; // Page header icon const StopIconHeader = () => ( ); export default function StopTableClient({ stops, hideInternalFilterBar = false }: Props) { const router = useRouter(); const { success: showSuccess, error: showError } = useToast(); const [, startTransition] = useTransition(); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState<"all" | "active" | "inactive" | "draft">("all"); const [deleteError, setDeleteError] = useState(null); const [page, setPage] = useState(0); const [isLoading, setIsLoading] = useState(false); const [selectedStops, setSelectedStops] = useState>(new Set()); const [bulkPublishing, setBulkPublishing] = useState(false); const [showImport, setShowImport] = useState(false); const [editingStop, setEditingStop] = useState(null); const [showAddModal, setShowAddModal] = useState(false); const [sortField, setSortField] = useState("date"); const [sortDirection, setSortDirection] = useState("asc"); const [viewMode, setViewMode] = useState("table"); const PAGE_SIZE = 25; useEffect(() => { setIsLoading(true); const timer = setTimeout(() => setIsLoading(false), 220); return () => clearTimeout(timer); }, [page, statusFilter, search, sortField, sortDirection]); // Compute stats const stats = useMemo(() => { const now = new Date(); const today = now.toISOString().split("T")[0]; const activeStops = stops.filter((s) => s.active && s.status !== "draft"); const upcomingStops = stops.filter((s) => s.date >= today && s.status !== "draft"); const draftStops = stops.filter((s) => s.status === "draft"); return { total: stops.length, active: activeStops.length, upcoming: upcomingStops.length, draft: draftStops.length, }; }, [stops]); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); return stops.filter((s) => { const matchesSearch = !q || s.city.toLowerCase().includes(q) || s.location.toLowerCase().includes(q) || s.state.toLowerCase().includes(q); const matchesStatus = statusFilter === "all" || (statusFilter === "active" && s.active && s.status !== "draft") || (statusFilter === "inactive" && !s.active && s.status !== "draft") || (statusFilter === "draft" && s.status === "draft"); return matchesSearch && matchesStatus; }); }, [stops, search, statusFilter]); const sorted = useMemo(() => { return [...filtered].sort((a, b) => { let comparison = 0; switch (sortField) { case "date": comparison = a.date.localeCompare(b.date); break; case "city": comparison = a.city.localeCompare(b.city); break; case "location": comparison = a.location.localeCompare(b.location); break; case "status": const statusOrder = { draft: 0, inactive: 1, active: 2 }; const aStatus = a.status === "draft" ? "draft" : a.active ? "active" : "inactive"; const bStatus = b.status === "draft" ? "draft" : b.active ? "active" : "inactive"; comparison = statusOrder[aStatus] - statusOrder[bStatus]; break; } return sortDirection === "asc" ? comparison : -comparison; }); }, [filtered, sortField, sortDirection]); const statusCounts = useMemo( () => ({ all: stops.length, active: stops.filter((s) => s.active && s.status !== "draft").length, inactive: stops.filter((s) => !s.active && s.status !== "draft").length, draft: stops.filter((s) => s.status === "draft").length, }), [stops] ); const tabs = useMemo( () => [ { value: "all", label: "All", count: statusCounts.all }, { value: "active", label: "Active", count: statusCounts.active }, { value: "inactive", label: "Inactive", count: statusCounts.inactive }, { value: "draft", label: "Draft", count: statusCounts.draft }, ], [statusCounts] ); const totalPages = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE)); const paginatedStops = sorted.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); const handleSort = useCallback((field: SortField) => { if (sortField === field) { setSortDirection((d) => (d === "asc" ? "desc" : "asc")); } else { setSortField(field); setSortDirection("asc"); } setPage(0); }, [sortField]); const toggleSelectAll = () => { if (selectedStops.size === paginatedStops.length) { setSelectedStops(new Set()); } else { setSelectedStops(new Set(paginatedStops.map((s) => s.id))); } }; const toggleStopSelection = (stopId: string) => { setSelectedStops((prev) => { const next = new Set(prev); if (next.has(stopId)) { next.delete(stopId); } else { next.add(stopId); } return next; }); }; async function handleBulkPublish() { if (selectedStops.size === 0) return; setBulkPublishing(true); let successCount = 0; let failCount = 0; for (const stopId of selectedStops) { const stop = stops.find((s) => s.id === stopId); if (stop && stop.status === "draft") { const result = await publishStop(stopId, stop.brand_id); if (result.success) { successCount++; } else { failCount++; } } } setBulkPublishing(false); setSelectedStops(new Set()); if (failCount === 0) { showSuccess(`${successCount} stop${successCount !== 1 ? "s" : ""} published`); } else { showError("Some stops failed", `${successCount} succeeded, ${failCount} failed`); } startTransition(() => router.refresh()); } function handleDeleted() { setDeleteError(null); startTransition(() => router.refresh()); } function refresh() { startTransition(() => router.refresh()); } // Sort icon component const SortIcon = ({ field }: { field: SortField }) => ( {sortField === field && sortDirection === "desc" ? ( ) : ( )} ); return ( <> {/* Page Header */}

Stops & Routes

{sorted.length} stop{sorted.length !== 1 ? "s" : ""} {search || statusFilter !== "all" ? " matching filters" : ""}

setShowImport(true)} icon={Icons.upload("h-4 w-4")} > Upload setShowAddModal(true)} icon={Icons.plus("h-4 w-4")} > Add Stop
{/* Stats Cards */}

Total

{isLoading ? : stats.total}

Active

{isLoading ? : stats.active}

Upcoming

{isLoading ? : stats.upcoming}

Drafts

{isLoading ? : stats.draft}

{/* Filters */} {!hideInternalFilterBar && (
{ setStatusFilter(TAB_NUMERIC[value as keyof typeof TAB_NUMERIC] ?? "all"); setPage(0); }} tabs={tabs} size="md" /> { setSearch(e.target.value); setPage(0); }} onClear={() => { setSearch(""); setPage(0); }} placeholder="Search stops..." containerClassName="flex-1" /> setViewMode(value as ViewMode)} />
)} {/* Bulk actions bar */} {selectedStops.size > 0 && (
{selectedStops.size} stop{selectedStops.size !== 1 ? "s" : ""} selected
Publish Selected
)} {/* Delete error */} {deleteError && (
{deleteError}{" "}
)} {/* Content */} {viewMode === "table" ? ( { const stop = stops.find((s) => s.id === id); if (stop) { setEditingStop(stop); } }} onDeleted={handleDeleted} onDeleteError={setDeleteError} selectedStops={selectedStops} onToggleSelectAll={toggleSelectAll} onToggleSelect={toggleStopSelection} isLoading={isLoading} search={search} statusFilter={statusFilter} /> ) : ( )} {/* Pagination */} {totalPages > 1 && (
Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, sorted.length)} of {sorted.length}
)} {showImport && ( setShowImport(false)} onComplete={refresh} /> )} { setShowAddModal(false); setEditingStop(null); }} brandId={stops[0]?.brand_id ?? ""} stop={editingStop} onSuccess={refresh} /> ); } // ── Table View ───────────────────────────────────────────────────────────────── function TableView({ stops, sortField, sortDirection, onSort, onEdit, onDelete, onDeleted, onDeleteError, selectedStops, onToggleSelectAll, onToggleSelect, isLoading, search, statusFilter, }: { stops: Stop[]; sortField: SortField; sortDirection: SortDirection; onSort: (field: SortField) => void; onEdit: (stop: Stop) => void; onDelete: (id: string) => void; onDeleted: () => void; onDeleteError: (msg: string) => void; selectedStops: Set; onToggleSelectAll: () => void; onToggleSelect: (id: string) => void; isLoading: boolean; search: string; statusFilter: string; }) { return (
{isLoading ? ( Array.from({ length: 8 }).map((_, i) => ( )) ) : stops.length === 0 ? ( ) : ( stops.map((stop) => ( onEdit(stop)} onDelete={() => onDelete(stop.id)} onDeleted={onDeleted} onDeleteError={onDeleteError} isSelected={selectedStops.has(stop.id)} onToggleSelect={() => onToggleSelect(stop.id)} /> )) )}
0} onChange={onToggleSelectAll} className="h-4 w-4 rounded border-stone-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer" aria-label="Select all" /> onSort("date")}> When onSort("city")}> Where onSort("location")}> Venue onSort("status")}> Status
{Icons.pin("h-8 w-8 text-stone-400")}

{search || statusFilter !== "all" ? "No stops match your filters" : "No stops yet"}

{search || statusFilter !== "all" ? "Try a different search or clear the filters" : "Add your first stop to get started"}

); } // ── Card View ────────────────────────────────────────────────────────────────── function CardView({ stops, onEdit, onDeleted, onDeleteError, isLoading, search, statusFilter, }: { stops: Stop[]; onEdit: (stop: Stop) => void; onDeleted: () => void; onDeleteError: (msg: string) => void; isLoading: boolean; search: string; statusFilter: string; }) { return (
{stops.length === 0 ? (
{Icons.pin("h-8 w-8 text-stone-400")}

{search || statusFilter !== "all" ? "No stops match your filters" : "No stops yet"}

{search || statusFilter !== "all" ? "Try a different search or clear the filters" : "Add your first stop to get started"}

) : ( stops.map((stop) => ( onEdit(stop)} onDeleted={onDeleted} onDeleteError={onDeleteError} /> )) )}
); } // ── Shared helpers ───────────────────────────────────────────────────────────── function SortIcon({ field, sortField, sortDirection }: { field: SortField; sortField: SortField; sortDirection: SortDirection }) { return ( {sortField === field && sortDirection === "desc" ? ( ) : ( )} ); } function formatDate(iso: string): { date: string; weekday: string; isToday: boolean; isPast: boolean } { const [y, m, d] = iso.split("-").map((s) => parseInt(s, 10)); if (!y || !m || !d) return { date: iso, weekday: "", isToday: false, isPast: false }; const today = new Date(); const todayStr = today.toISOString().split("T")[0]; const dt = new Date(y, m - 1, d); const weekday = dt.toLocaleDateString("en-US", { weekday: "short" }); const month = dt.toLocaleDateString("en-US", { month: "short" }); return { date: `${month} ${d}`, weekday, isToday: iso === todayStr, isPast: iso < todayStr }; } function formatTime(t: string): string { if (!t) return ""; const [hStr, mStr] = t.split(":"); const h = parseInt(hStr, 10); if (Number.isNaN(h)) return t; const period = h >= 12 ? "PM" : "AM"; const hh = h === 0 ? 12 : h > 12 ? h - 12 : h; return `${hh}:${mStr} ${period}`; } // ── Row Component ─────────────────────────────────────────────────────────────── function StopRow({ stop, onEdit, onDelete, onDeleted, onDeleteError, isSelected, onToggleSelect, }: { stop: Stop; onEdit: () => void; onDelete: () => void; onDeleted: () => void; onDeleteError: (msg: string) => void; isSelected: boolean; onToggleSelect: () => void; }) { const { success: showSuccess, error: showError } = useToast(); const [openMenu, setOpenMenu] = useState(null); const [deletingId, setDeletingId] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const [publishingId, setPublishingId] = useState(null); const { date, weekday, isToday, isPast } = formatDate(stop.date); const time = formatTime(stop.time); async function handlePublish() { setPublishingId(stop.id); const result = await publishStop(stop.id, stop.brand_id); setPublishingId(null); if (result.success) { showSuccess("Stop published", "The stop is now visible on the storefront"); onDeleted(); } else { showError("Failed to publish", result.error ?? "Please try again"); } } async function handleDelete() { setDeletingId(stop.id); const result = await deleteStop(stop.id, stop.brand_id); setDeletingId(null); setConfirmDelete(null); setOpenMenu(null); if (result.success) { showSuccess("Stop deleted", "The stop has been removed"); onDeleted(); } else { onDeleteError(result.error ?? "Delete failed"); } } return ( e.stopPropagation()}> {}} onClick={(e) => { e.stopPropagation(); onToggleSelect(); }} className="h-4 w-4 rounded border-stone-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer" aria-label={`Select stop in ${stop.city}`} />
{isToday && ( Today )} {date}
{weekday} {time && `· ${time}`}
{stop.city} {stop.state}
{stop.location} {stop.address && ( {stop.address} )}
{stop.status === "draft" ? "Draft" : stop.active ? "Active" : "Inactive"} e.stopPropagation()}>
Edit {openMenu === stop.id && ( <>
{ e.stopPropagation(); setOpenMenu(null); setConfirmDelete(null); }} />
{stop.status === "draft" && ( )} e.stopPropagation()} className="flex items-center gap-2 px-4 py-2.5 text-sm text-stone-600 hover:bg-stone-50 transition-colors" > Duplicate
)} {confirmDelete === stop.id && ( <>
{ e.stopPropagation(); setConfirmDelete(null); setOpenMenu(null); }} />

Delete “{stop.city}, {stop.state}”?

This will remove the stop. If it has active orders, you must resolve those first.

{ e.stopPropagation(); setConfirmDelete(null); setOpenMenu(null); }} > Cancel { e.stopPropagation(); handleDelete(); }} disabled={deletingId === stop.id} isLoading={deletingId === stop.id} > {deletingId === stop.id ? "Deleting…" : "Delete"}
)}
); } // ── Card Component ───────────────────────────────────────────────────────────── function StopCard({ stop, onEdit, onDeleted, onDeleteError, }: { stop: Stop; onEdit: () => void; onDeleted: () => void; onDeleteError: (msg: string) => void; }) { const { success: showSuccess, error: showError } = useToast(); const [confirmDelete, setConfirmDelete] = useState(false); const [deleting, setDeleting] = useState(false); const { date, weekday, isToday, isPast } = formatDate(stop.date); const time = formatTime(stop.time); async function handleDelete() { setDeleting(true); const result = await deleteStop(stop.id, stop.brand_id); setDeleting(false); if (result.success) { showSuccess("Stop deleted", "The stop has been removed"); onDeleted(); } else { onDeleteError(result.error ?? "Delete failed"); } } return (
{/* Header with date */}
{isToday && ( Today )}

{date}

{weekday} {time && `· ${time}`}

{stop.status === "draft" ? "Draft" : stop.active ? "Active" : "Inactive"}
{/* Content */}
Edit
{/* Delete confirm */} {confirmDelete && (

Delete “{stop.city}, {stop.state}”?

This will remove the stop. If it has active orders, you must resolve those first.

)}
); }