|
- "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 } 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 [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",
- renderCell: (item) => String(item.id ?? ""),
- },
- {
- name: "code",
- label: t("Code"),
- renderCell: (item) => String(item.code ?? ""),
- },
- {
- name: "name",
- label: t("Name"),
- renderCell: (item) => String(item.name ?? ""),
- },
- {
- name: "addr3",
- label: t("Addr3"),
- renderCell: (item) => String((item as any).addr3 ?? ""),
- },
- {
- name: "truckLanceStatus",
- label: t("TruckLance Status"),
- 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"),
- headerAlign: "right",
- renderCell: (item) => (
- <Button
- size="small"
- variant="outlined"
- onClick={() => handleViewDetail(item)}
- >
- {t("View Detail")}
- </Button>
- ),
- },
- ];
-
- useEffect(() => {
- if (activeTab === 0) {
- fetchAllShops();
- }
- }, [activeTab]);
-
- const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
- setActiveTab(newValue);
- };
-
- return (
- <Box>
- <Card sx={{ mb: 2 }}>
- <CardContent>
- <Tabs
- value={activeTab}
- onChange={handleTabChange}
- sx={{
- mb: 3,
- borderBottom: 1,
- borderColor: 'divider'
- }}
- >
- <Tab label={t("Shop")} />
- <Tab label={t("Truck Lane")} />
- </Tabs>
-
- {activeTab === 0 && (
- <SearchBox
- criteria={criteria as Criterion<string>[]}
- onSearch={handleSearch}
- onReset={() => {
- setRows([]);
- setFilters({});
- }}
- />
- )}
- </CardContent>
- </Card>
-
- {activeTab === 0 && (
- <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>
- );
- };
-
- export default Shop;
|