|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- "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<number>(0);
- const [rows, setRows] = useState<ShopRow[]>([]);
- const [loading, setLoading] = useState<boolean>(false);
- const [error, setError] = useState<string | null>(null);
- const [filters, setFilters] = useState<Record<string, string>>({});
- const [statusFilter, setStatusFilter] = useState<string>("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<string, string>) => {
- 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<number, { shop: Shop; trucks: ShopAndTruck[] }>();
-
- (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<string, string>) => {
- setFilters(inputs);
- const params: Record<string, string> = {};
- 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<SearchParamNames>[] = [
- { 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<ShopRow>[] = [
- {
- 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 <Chip label={t("Complete")} color="success" size="small" />;
- } else if (status === "missing") {
- return <Chip label={t("Missing Data")} color="warning" size="small" />;
- } else {
- return <Chip label={t("No TruckLance")} color="error" size="small" />;
- }
- },
- },
- {
- name: "actions",
- label: t("Actions"),
- align: "right",
- headerAlign: "right",
- sx: { width: "150px", minWidth: "150px", maxWidth: "150px" },
- renderCell: (item) => (
- <Button
- size="small"
- variant="outlined"
- onClick={() => handleViewDetail(item)}
- >
- {t("View Detail")}
- </Button>
- ),
- },
- ];
-
- // 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 (
- <Box>
- {/* Header section with title */}
- <Box sx={{
- p: 2,
- borderBottom: '1px solid #e0e0e0'
- }}>
- <Typography variant="h4">
- 店鋪路線管理
- </Typography>
- </Box>
-
- {/* Tabs section */}
- <Box sx={{
- borderBottom: '1px solid #e0e0e0'
- }}>
- <Tabs
- value={activeTab}
- onChange={handleTabChange}
- >
- <Tab label={t("Shop")} />
- <Tab label={t("Truck Lane")} />
- </Tabs>
- </Box>
-
- {/* Content section */}
- <Box sx={{ p: 2 }}>
- {activeTab === 0 && (
- <>
- <Card sx={{ mb: 2 }}>
- <CardContent>
- <SearchBox
- criteria={criteria as Criterion<string>[]}
- onSearch={handleSearch}
- onReset={() => {
- setRows([]);
- setFilters({});
- }}
- />
- </CardContent>
- </Card>
-
- <Card>
- <CardContent>
- <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
- <Typography variant="h6">{t("Shop")}</Typography>
- <FormControl size="small" sx={{ minWidth: 200 }}>
- <InputLabel>{t("Filter by Status")}</InputLabel>
- <Select
- value={statusFilter}
- label={t("Filter by Status")}
- onChange={(e) => setStatusFilter(e.target.value)}
- >
- <MenuItem value="all">{t("All")}</MenuItem>
- <MenuItem value="complete">{t("Complete")}</MenuItem>
- <MenuItem value="missing">{t("Missing Data")}</MenuItem>
- <MenuItem value="no-truck">{t("No TruckLance")}</MenuItem>
- </Select>
- </FormControl>
- </Stack>
- {error && (
- <Alert severity="error" sx={{ mb: 2 }}>
- {error}
- </Alert>
- )}
-
- {loading ? (
- <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
- <CircularProgress />
- </Box>
- ) : (
- <SearchResults
- items={filteredRows}
- columns={columns}
- pagingController={pagingController}
- setPagingController={setPagingController}
- />
- )}
- </CardContent>
- </Card>
- </>
- )}
-
- {activeTab === 1 && (
- <TruckLane />
- )}
- </Box>
- </Box>
- );
- };
-
- export default Shop;
|