"use client"; import { Box, Button, Card, CardContent, Stack, Typography, Alert, CircularProgress, Chip, Tabs, Tab, Select, MenuItem, FormControl, InputLabel, } from "@mui/material"; import { useState, useMemo, useCallback, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useTranslation } from "react-i18next"; import SearchBox, { Criterion } from "../SearchBox"; import SearchResults, { Column } from "../SearchResults"; import { defaultPagingController } from "../SearchResults/SearchResults"; import { fetchAllShopsClient } from "@/app/api/shop/client"; import type { Shop, ShopAndTruck } from "@/app/api/shop/actions"; import TruckLane from "./TruckLane"; type ShopRow = Shop & { actions?: string; truckLanceStatus?: "complete" | "missing" | "no-truck"; }; type SearchQuery = { id: string; name: string; code: string; }; type SearchParamNames = keyof SearchQuery; const Shop: React.FC = () => { const { t } = useTranslation("common"); const router = useRouter(); const searchParams = useSearchParams(); const [activeTab, setActiveTab] = useState(0); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [filters, setFilters] = useState>({}); const [statusFilter, setStatusFilter] = useState("all"); const [pagingController, setPagingController] = useState(defaultPagingController); // client-side filtered rows (contains-matching + status filter) const filteredRows = useMemo(() => { const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== ""); let normalized = (rows || []).filter((r) => { // apply contains matching for each active filter for (const k of fKeys) { const v = String((filters as any)[k] ?? "").trim(); const rv = String((r as any)[k] ?? "").trim(); // Use exact matching for id field, contains matching for others if (k === "id") { const numValue = Number(v); const rvNum = Number(rv); if (!isNaN(numValue) && !isNaN(rvNum)) { if (numValue !== rvNum) return false; } else { if (v !== rv) return false; } } else { if (!rv.toLowerCase().includes(v.toLowerCase())) return false; } } return true; }); // Apply status filter if (statusFilter !== "all") { normalized = normalized.filter((r) => { return r.truckLanceStatus === statusFilter; }); } return normalized; }, [rows, filters, statusFilter]); // Check if a shop has missing truckLanceCode data const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => { if (!shopTrucks || shopTrucks.length === 0) { return "no-truck"; } // Check if shop has any actual truck lanes (not just null entries from LEFT JOIN) // A shop with no trucks will have entries with null truckLanceCode const hasAnyTruckLane = shopTrucks.some((truck) => { const truckLanceCode = (truck as any).truckLanceCode; return truckLanceCode != null && String(truckLanceCode).trim() !== ""; }); if (!hasAnyTruckLane) { return "no-truck"; } // Check each truckLanceCode entry for missing data for (const truck of shopTrucks) { // Skip entries without truckLanceCode (they're from LEFT JOIN when no trucks exist) const truckLanceCode = (truck as any).truckLanceCode; if (!truckLanceCode || String(truckLanceCode).trim() === "") { continue; // Skip this entry, it's not a real truck lane } // Check truckLanceCode: must exist and not be empty (already validated above) const hasTruckLanceCode = truckLanceCode != null && String(truckLanceCode).trim() !== ""; // Check departureTime: must exist and not be empty // Can be array format [hours, minutes] or string format const departureTime = (truck as any).departureTime || (truck as any).DepartureTime; let hasDepartureTime = false; if (departureTime != null) { if (Array.isArray(departureTime) && departureTime.length >= 2) { // Array format [hours, minutes] hasDepartureTime = true; } else { // String format const timeStr = String(departureTime).trim(); hasDepartureTime = timeStr !== "" && timeStr !== "-"; } } // Check loadingSequence: must exist and not be 0 const loadingSeq = (truck as any).loadingSequence || (truck as any).LoadingSequence; const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null; const hasLoadingSequence = loadingSeqNum !== null && !isNaN(loadingSeqNum) && loadingSeqNum !== 0; // Check districtReference: must exist and not be 0 const districtRef = (truck as any).districtReference; const districtRefNum = districtRef != null && districtRef !== undefined ? Number(districtRef) : null; const hasDistrictReference = districtRefNum !== null && !isNaN(districtRefNum) && districtRefNum !== 0; // Check storeId: must exist and not be 0 (can be string "2F"/"4F" or number) // Actual field name in JSON is store_id (underscore, lowercase) const storeId = (truck as any).store_id || (truck as any).storeId || (truck as any).Store_id; let storeIdValid = false; if (storeId != null && storeId !== undefined && storeId !== "") { const storeIdStr = String(storeId).trim(); // If it's "2F" or "4F", it's valid (not 0) if (storeIdStr === "2F" || storeIdStr === "4F") { storeIdValid = true; } else { const storeIdNum = Number(storeId); // If it's a valid number and not 0, it's valid if (!isNaN(storeIdNum) && storeIdNum !== 0) { storeIdValid = true; } } } // If any required field is missing or equals 0, return "missing" if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !storeIdValid) { return "missing"; } } return "complete"; }, []); const fetchAllShops = async (params?: Record) => { setLoading(true); setError(null); try { const data = await fetchAllShopsClient(params) as ShopAndTruck[]; console.log("Fetched shops data:", data); // Group data by shop ID (one shop can have multiple TruckLanceCode entries) const shopMap = new Map(); (data || []).forEach((item: ShopAndTruck) => { const shopId = item.id; if (!shopMap.has(shopId)) { shopMap.set(shopId, { shop: { id: item.id, name: item.name, code: item.code, addr3: item.addr3 ?? "", }, trucks: [], }); } shopMap.get(shopId)!.trucks.push(item); }); // Convert to ShopRow array with truckLanceStatus const mapped: ShopRow[] = Array.from(shopMap.values()).map(({ shop, trucks }) => ({ ...shop, truckLanceStatus: checkTruckLanceStatus(trucks), })); setRows(mapped); } catch (err: any) { console.error("Failed to load shops:", err); setError(err?.message ?? String(err)); } finally { setLoading(false); } }; // SearchBox onSearch will call this const handleSearch = (inputs: Record) => { setFilters(inputs); const params: Record = {}; Object.entries(inputs || {}).forEach(([k, v]) => { if (v != null && String(v).trim() !== "") params[k] = String(v).trim(); }); if (Object.keys(params).length === 0) fetchAllShops(); else fetchAllShops(params); }; const handleViewDetail = useCallback( (shop: ShopRow) => { router.push(`/settings/shop/detail?id=${shop.id}`); }, [router] ); const criteria: Criterion[] = [ { type: "text", label: t("id"), paramName: "id" }, { type: "text", label: t("code"), paramName: "code" }, { type: "text", label: t("Shop Name"), paramName: "name" }, ]; const columns: Column[] = [ { name: "id", label: t("id"), type: "integer", sx: { width: "100px", minWidth: "100px", maxWidth: "100px" }, renderCell: (item) => String(item.id ?? ""), }, { name: "code", label: t("Code"), sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, renderCell: (item) => String(item.code ?? ""), }, { name: "name", label: t("Name"), sx: { width: "200px", minWidth: "200px", maxWidth: "200px" }, renderCell: (item) => String(item.name ?? ""), }, { name: "addr3", label: t("Addr3"), sx: { width: "200px", minWidth: "200px", maxWidth: "200px" }, renderCell: (item) => String((item as any).addr3 ?? ""), }, { name: "truckLanceStatus", label: t("TruckLance Status"), align: "center", headerAlign: "center", sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, renderCell: (item) => { const status = item.truckLanceStatus; if (status === "complete") { return ; } else if (status === "missing") { return ; } else { return ; } }, }, { name: "actions", label: t("Actions"), align: "right", headerAlign: "right", sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, renderCell: (item) => ( ), }, ]; // Initialize activeTab from URL parameter useEffect(() => { const tabParam = searchParams.get("tab"); if (tabParam !== null) { const tabIndex = parseInt(tabParam, 10); if (!isNaN(tabIndex) && (tabIndex === 0 || tabIndex === 1)) { setActiveTab(tabIndex); } } }, [searchParams]); const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setActiveTab(newValue); // Update URL to reflect the selected tab const url = new URL(window.location.href); url.searchParams.set("tab", String(newValue)); router.push(url.pathname + url.search); }; return ( {/* Header section with title */} 店鋪路線管理 {/* Tabs section */} {/* Content section */} {activeTab === 0 && ( <> []} onSearch={handleSearch} onReset={() => { setRows([]); setFilters({}); }} /> {t("Shop")} {t("Filter by Status")} {error && ( {error} )} {loading ? ( ) : ( )} )} {activeTab === 1 && ( )} ); }; export default Shop;