From 9ed78c4c2f5e557047331f5d8c68200dd14fed85 Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Mon, 5 Jan 2026 18:44:04 +0800 Subject: [PATCH 1/8] update handle shop/ missing trucklane, update manage by shop, added manage by trucklaneCode. Applied Translation. --- src/app/(main)/settings/shop/detail/page.tsx | 4 +- src/app/(main)/settings/shop/page.tsx | 4 +- .../(main)/settings/shop/truckdetail/page.tsx | 16 + src/app/api/shop/actions.ts | 40 +- src/app/api/shop/client.ts | 16 + src/components/Breadcrumb/Breadcrumb.tsx | 5 +- src/components/Shop/Shop.tsx | 226 +++++++-- src/components/Shop/ShopDetail.tsx | 120 +++-- src/components/Shop/TruckLane.tsx | 269 ++++++++++ src/components/Shop/TruckLaneDetail.tsx | 474 ++++++++++++++++++ src/i18n/zh/common.json | 72 ++- 11 files changed, 1122 insertions(+), 124 deletions(-) create mode 100644 src/app/(main)/settings/shop/truckdetail/page.tsx create mode 100644 src/components/Shop/TruckLane.tsx create mode 100644 src/components/Shop/TruckLaneDetail.tsx diff --git a/src/app/(main)/settings/shop/detail/page.tsx b/src/app/(main)/settings/shop/detail/page.tsx index bf37a8e..02c2334 100644 --- a/src/app/(main)/settings/shop/detail/page.tsx +++ b/src/app/(main)/settings/shop/detail/page.tsx @@ -4,9 +4,9 @@ import { I18nProvider, getServerI18n } from "@/i18n"; import GeneralLoading from "@/components/General/GeneralLoading"; export default async function ShopDetailPage() { - const { t } = await getServerI18n("shop"); + const { t } = await getServerI18n("shop", "common"); return ( - + }> diff --git a/src/app/(main)/settings/shop/page.tsx b/src/app/(main)/settings/shop/page.tsx index c3996b9..c4e8175 100644 --- a/src/app/(main)/settings/shop/page.tsx +++ b/src/app/(main)/settings/shop/page.tsx @@ -8,9 +8,9 @@ import { notFound } from "next/navigation"; export default async function ShopPage() { - const { t } = await getServerI18n("shop"); + const { t } = await getServerI18n("shop", "common"); return ( - + }> diff --git a/src/app/(main)/settings/shop/truckdetail/page.tsx b/src/app/(main)/settings/shop/truckdetail/page.tsx new file mode 100644 index 0000000..8c50c6d --- /dev/null +++ b/src/app/(main)/settings/shop/truckdetail/page.tsx @@ -0,0 +1,16 @@ +import { Suspense } from "react"; +import TruckLaneDetail from "@/components/Shop/TruckLaneDetail"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import GeneralLoading from "@/components/General/GeneralLoading"; + +export default async function TruckLaneDetailPage() { + const { t } = await getServerI18n("shop", "common"); + return ( + + }> + + + + ); +} + diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts index 559a269..19231be 100644 --- a/src/app/api/shop/actions.ts +++ b/src/app/api/shop/actions.ts @@ -24,9 +24,11 @@ export interface ShopAndTruck{ contactName: String; truckLanceCode: String; DepartureTime: String; - LoadingSequence: number; + LoadingSequence?: number | null; districtReference: Number; - Store_id: Number + Store_id: Number; + remark?: String | null; + truckId?: number; } export interface Shop{ @@ -60,6 +62,11 @@ export interface DeleteTruckLane { id: number; } +export interface UpdateLoadingSequenceRequest { + id: number; + loadingSequence: number; +} + export interface SaveTruckRequest { id?: number | null; store_id: string; @@ -132,6 +139,35 @@ export const deleteTruckLaneAction = async (data: DeleteTruckLane) => { export const createTruckAction = async (data: SaveTruckRequest) => { const endpoint = `${BASE_API_URL}/truck/create`; + return serverFetchJson(endpoint, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +}; + +export const findAllUniqueTruckLaneCombinationsAction = cache(async () => { + const endpoint = `${BASE_API_URL}/truck/findAllUniqueTruckLanceCodeAndRemarkCombinations`; + + return serverFetchJson(endpoint, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const findAllShopsByTruckLanceCodeAndRemarkAction = cache(async (truckLanceCode: string, remark: string) => { + const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndRemarkAndDeletedFalse`; + const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}&remark=${encodeURIComponent(remark)}`; + + return serverFetchJson(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => { + const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`; + return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), diff --git a/src/app/api/shop/client.ts b/src/app/api/shop/client.ts index bbd3fe5..6f2da57 100644 --- a/src/app/api/shop/client.ts +++ b/src/app/api/shop/client.ts @@ -6,9 +6,13 @@ import { updateTruckLaneAction, deleteTruckLaneAction, createTruckAction, + findAllUniqueTruckLaneCombinationsAction, + findAllShopsByTruckLanceCodeAndRemarkAction, + updateLoadingSequenceAction, type SaveTruckLane, type DeleteTruckLane, type SaveTruckRequest, + type UpdateLoadingSequenceRequest, type MessageResponse } from "./actions"; @@ -32,4 +36,16 @@ export const createTruckClient = async (data: SaveTruckRequest): Promise { + return await findAllUniqueTruckLaneCombinationsAction(); +}; + +export const findAllShopsByTruckLanceCodeAndRemarkClient = async (truckLanceCode: string, remark: string) => { + return await findAllShopsByTruckLanceCodeAndRemarkAction(truckLanceCode, remark); +}; + +export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise => { + return await updateLoadingSequenceAction(data); +}; + export default fetchAllShopsClient; diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 4043043..0a872e4 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -16,7 +16,10 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/qcItem": "Qc Item", "/settings/qrCodeHandle": "QR Code Handle", "/settings/rss": "Demand Forecast Setting", - "/settings/equipment": "Equipment", + "/settings/equipment": "Equipment", + "/settings/shop": "Shop", + "/settings/shop/detail": "Shop Detail", + "/settings/shop/truckdetail": "Truck Lane Detail", "/scheduling/rough": "Demand Forecast", "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", "/scheduling/detailed": "Detail Scheduling", diff --git a/src/components/Shop/Shop.tsx b/src/components/Shop/Shop.tsx index 0eb2605..ff4136c 100644 --- a/src/components/Shop/Shop.tsx +++ b/src/components/Shop/Shop.tsx @@ -10,14 +10,22 @@ import { 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; @@ -33,17 +41,20 @@ type SearchQuery = { type SearchParamNames = keyof SearchQuery; const Shop: React.FC = () => { + const { t } = useTranslation("common"); const router = useRouter(); + 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) + // client-side filtered rows (contains-matching + status filter) const filteredRows = useMemo(() => { const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== ""); - const normalized = (rows || []).filter((r) => { + 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(); @@ -63,8 +74,16 @@ const Shop: React.FC = () => { } return true; }); + + // Apply status filter + if (statusFilter !== "all") { + normalized = normalized.filter((r) => { + return r.truckLanceStatus === statusFilter; + }); + } + return normalized; - }, [rows, filters]); + }, [rows, filters, statusFilter]); // Check if a shop has missing truckLanceCode data const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => { @@ -72,16 +91,73 @@ const Shop: React.FC = () => { 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) { - const hasTruckLanceCode = truck.truckLanceCode && String(truck.truckLanceCode).trim() !== ""; - const hasDepartureTime = truck.DepartureTime && String(truck.DepartureTime).trim() !== ""; - const hasLoadingSequence = truck.LoadingSequence !== null && truck.LoadingSequence !== undefined; - const hasDistrictReference = truck.districtReference !== null && truck.districtReference !== undefined; - const hasStoreId = truck.Store_id !== null && truck.Store_id !== undefined; + // 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, return "missing" - if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !hasStoreId) { + // If any required field is missing or equals 0, return "missing" + if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !storeIdValid) { return "missing"; } } @@ -149,50 +225,50 @@ const Shop: React.FC = () => { ); const criteria: Criterion[] = [ - { type: "text", label: "id", paramName: "id" }, - { type: "text", label: "code", paramName: "code" }, - { type: "text", label: "name", paramName: "name" }, + { type: "text", label: t("id"), paramName: "id" }, + { type: "text", label: t("code"), paramName: "code" }, + { type: "text", label: t("name"), paramName: "name" }, ]; const columns: Column[] = [ { name: "id", - label: "Id", + label: t("id"), type: "integer", renderCell: (item) => String(item.id ?? ""), }, { name: "code", - label: "Code", + label: t("Code"), renderCell: (item) => String(item.code ?? ""), }, { name: "name", - label: "Name", + label: t("Name"), renderCell: (item) => String(item.name ?? ""), }, { name: "addr3", - label: "Addr3", + label: t("Addr3"), renderCell: (item) => String((item as any).addr3 ?? ""), }, { name: "truckLanceStatus", - label: "TruckLance Status", + label: t("TruckLance Status"), renderCell: (item) => { const status = item.truckLanceStatus; if (status === "complete") { - return ; + return ; } else if (status === "missing") { - return ; + return ; } else { - return ; + return ; } }, }, { name: "actions", - label: "Actions", + label: t("Actions"), headerAlign: "right", renderCell: (item) => ( ), }, ]; useEffect(() => { - fetchAllShops(); - }, []); + if (activeTab === 0) { + fetchAllShops(); + } + }, [activeTab]); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue); + }; return ( - []} - onSearch={handleSearch} - onReset={() => { - setRows([]); - setFilters({}); + - - - - - - - Shop - - {error && ( - - {error} - - )} + > + + + - {loading ? ( - - - - ) : ( - []} + onSearch={handleSearch} + onReset={() => { + setRows([]); + setFilters({}); + }} /> )} + + {activeTab === 0 && ( + + + + {t("Shop")} + + {t("Filter by Status")} + + + + {error && ( + + {error} + + )} + + {loading ? ( + + + + ) : ( + + )} + + + )} + + {activeTab === 1 && ( + + )} ); }; diff --git a/src/components/Shop/ShopDetail.tsx b/src/components/Shop/ShopDetail.tsx index f2e4282..c03d153 100644 --- a/src/components/Shop/ShopDetail.tsx +++ b/src/components/Shop/ShopDetail.tsx @@ -38,6 +38,7 @@ import AddIcon from "@mui/icons-material/Add"; import { useRouter, useSearchParams } from "next/navigation"; import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; +import { useTranslation } from "react-i18next"; import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions"; import { fetchAllShopsClient, @@ -131,6 +132,7 @@ const parseDepartureTimeForBackend = (time: string): string => { }; const ShopDetail: React.FC = () => { + const { t } = useTranslation("common"); const router = useRouter(); const searchParams = useSearchParams(); const shopId = searchParams.get("id"); @@ -163,14 +165,14 @@ const ShopDetail: React.FC = () => { // If session is unauthenticated, don't make API calls (middleware will handle redirect) if (sessionStatus === "unauthenticated" || !session) { - setError("Please log in to view shop details"); + setError(t("Please log in to view shop details")); setLoading(false); return; } const fetchShopDetail = async () => { if (!shopId) { - setError("Shop ID is required"); + setError(t("Shop ID is required")); setLoading(false); return; } @@ -178,7 +180,7 @@ const ShopDetail: React.FC = () => { // Convert shopId to number for proper filtering const shopIdNum = parseInt(shopId, 10); if (isNaN(shopIdNum)) { - setError("Invalid Shop ID"); + setError(t("Invalid Shop ID")); setLoading(false); return; } @@ -212,7 +214,7 @@ const ShopDetail: React.FC = () => { contactName: shopData.contactName ?? "", }); } else { - setError("Shop not found"); + setError(t("Shop not found")); setLoading(false); return; } @@ -233,7 +235,7 @@ const ShopDetail: React.FC = () => { } catch (err: any) { console.error("Failed to load shop detail:", err); // Handle errors gracefully - don't trigger auto-logout - const errorMessage = err?.message ?? String(err) ?? "Failed to load shop details"; + const errorMessage = err?.message ?? String(err) ?? t("Failed to load shop details"); setError(errorMessage); } finally { setLoading(false); @@ -273,13 +275,13 @@ const ShopDetail: React.FC = () => { const handleSave = async (index: number) => { if (!shopId) { - setError("Shop ID is required"); + setError(t("Shop ID is required")); return; } const truck = editedTruckData[index]; if (!truck || !truck.id) { - setError("Invalid truck data"); + setError(t("Invalid shop data")); return; } @@ -335,7 +337,7 @@ const ShopDetail: React.FC = () => { setUniqueRemarks(remarks); } catch (err: any) { console.error("Failed to save truck data:", err); - setError(err?.message ?? String(err) ?? "Failed to save truck data"); + setError(err?.message ?? String(err) ?? t("Failed to save truck data")); } finally { setSaving(false); } @@ -351,12 +353,12 @@ const ShopDetail: React.FC = () => { }; const handleDelete = async (truckId: number) => { - if (!window.confirm("Are you sure you want to delete this truck lane?")) { + if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) { return; } if (!shopId) { - setError("Shop ID is required"); + setError(t("Shop ID is required")); return; } @@ -373,7 +375,7 @@ const ShopDetail: React.FC = () => { setEditingRowIndex(null); } catch (err: any) { console.error("Failed to delete truck lane:", err); - setError(err?.message ?? String(err) ?? "Failed to delete truck lane"); + setError(err?.message ?? String(err) ?? t("Failed to delete truck lane")); } finally { setSaving(false); } @@ -409,19 +411,19 @@ const ShopDetail: React.FC = () => { const missingFields: string[] = []; if (!shopId || !shopDetail) { - missingFields.push("Shop information"); + missingFields.push(t("Shop Information")); } if (!newTruck.truckLanceCode.trim()) { - missingFields.push("TruckLance Code"); + missingFields.push(t("TruckLance Code")); } if (!newTruck.departureTime) { - missingFields.push("Departure Time"); + missingFields.push(t("Departure Time")); } if (missingFields.length > 0) { - const message = `Please fill in the following required fields: ${missingFields.join(", ")}`; + const message = `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`; setSnackbarMessage(message); setSnackbarOpen(true); return; @@ -461,7 +463,7 @@ const ShopDetail: React.FC = () => { handleCloseAddDialog(); } catch (err: any) { console.error("Failed to create truck:", err); - setError(err?.message ?? String(err) ?? "Failed to create truck"); + setError(err?.message ?? String(err) ?? t("Failed to create truck")); } finally { setSaving(false); } @@ -475,12 +477,12 @@ const ShopDetail: React.FC = () => { ); } if (error) { - return ( + return ( {error} - + ); } @@ -489,9 +491,9 @@ const ShopDetail: React.FC = () => { return ( - Shop not found + {t("Shop not found")} - + ); } @@ -501,49 +503,45 @@ const ShopDetail: React.FC = () => { - Shop Information - + {t("Shop Information")} + - Shop ID + {t("Shop ID")} {shopDetail.id} - Name + {t("Name")} {shopDetail.name} - Code + {t("Code")} {shopDetail.code} - Addr1 + {t("Addr1")} {shopDetail.addr1 || "-"} - Addr2 + {t("Addr2")} {shopDetail.addr2 || "-"} - Addr3 + {t("Addr3")} {shopDetail.addr3 || "-"} - Contact No + {t("Contact No")} {shopDetail.contactNo || "-"} - Type - {shopDetail.type || "-"} - - - Contact Email + {t("Contact Email")} {shopDetail.contactEmail || "-"} - Contact Name + {t("Contact Name")} {shopDetail.contactName || "-"} @@ -553,27 +551,27 @@ const ShopDetail: React.FC = () => { - Truck Information + {t("Truck Information")} - TruckLance Code - Departure Time - Loading Sequence - District Reference - Store ID - Remark - Actions + {t("TruckLance Code")} + {t("Departure Time")} + {t("Loading Sequence")} + {t("District Reference")} + {t("Store ID")} + {t("Remark")} + {t("Actions")} @@ -581,7 +579,7 @@ const ShopDetail: React.FC = () => { - No Truck data available + {t("No Truck data available")} @@ -725,7 +723,7 @@ const ShopDetail: React.FC = () => { )} @@ -745,7 +743,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleSave(index)} disabled={saving} - title="Save changes" + title={t("Save changes")} > @@ -754,7 +752,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleCancel(index)} disabled={saving} - title="Cancel editing" + title={t("Cancel editing")} > @@ -766,7 +764,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleEdit(index)} disabled={editingRowIndex !== null} - title="Edit truck lane" + title={t("Edit truck lane")} > @@ -776,7 +774,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleDelete(truck.id!)} disabled={saving || editingRowIndex !== null} - title="Delete truck lane" + title={t("Delete truck lane")} > @@ -797,13 +795,13 @@ const ShopDetail: React.FC = () => { {/* Add Truck Dialog */} - Add New Truck Lane + {t("Add New Truck Lane")} { { { { - Store ID + {t("Store ID")}
+ + + {t("TruckLance Code")} + {t("Departure Time")} + {t("Store ID")} + {t("Remark")} + {t("Actions")} + + + + {paginatedRows.length === 0 ? ( + + + + {t("No Truck Lane data available")} + + + + ) : ( + paginatedRows.map((truck, index) => { + const storeId = truck.storeId; + const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "-"; + const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F" + : storeIdStr === "4" || storeIdStr === "4F" ? "4F" + : storeIdStr; + + return ( + + + {String(truck.truckLanceCode || "-")} + + + {formatDepartureTime( + Array.isArray(truck.departureTime) + ? truck.departureTime + : (truck.departureTime ? String(truck.departureTime) : null) + )} + + + {displayStoreId} + + + {String(truck.remark || "-")} + + + + + + ); + }) + )} + +
+ +
+
+
+ + ); +}; + +export default TruckLane; + diff --git a/src/components/Shop/TruckLaneDetail.tsx b/src/components/Shop/TruckLaneDetail.tsx new file mode 100644 index 0000000..b903087 --- /dev/null +++ b/src/components/Shop/TruckLaneDetail.tsx @@ -0,0 +1,474 @@ +"use client"; + +import { + Box, + Card, + CardContent, + Typography, + Button, + CircularProgress, + Alert, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + IconButton, + Snackbar, + TextField, +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; +import SaveIcon from "@mui/icons-material/Save"; +import CancelIcon from "@mui/icons-material/Cancel"; +import { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeAndRemarkClient, deleteTruckLaneClient, updateLoadingSequenceClient } from "@/app/api/shop/client"; +import type { Truck, ShopAndTruck } from "@/app/api/shop/actions"; + +// Utility function to format departureTime to HH:mm format +const formatDepartureTime = (time: string | number[] | null | undefined): string => { + if (!time) return "-"; + + // Handle array format [hours, minutes] from API + if (Array.isArray(time) && time.length >= 2) { + const hours = time[0]; + const minutes = time[1]; + if (typeof hours === 'number' && typeof minutes === 'number' && + hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) { + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; + } + } + + const timeStr = String(time).trim(); + if (!timeStr || timeStr === "-") return "-"; + + // If already in HH:mm format, return as is + if (/^\d{1,2}:\d{2}$/.test(timeStr)) { + const [hours, minutes] = timeStr.split(":"); + return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`; + } + + return timeStr; +}; + +const TruckLaneDetail: React.FC = () => { + const { t } = useTranslation("common"); + const router = useRouter(); + const searchParams = useSearchParams(); + const truckId = searchParams.get("id"); + + const [truckData, setTruckData] = useState(null); + const [shopsData, setShopsData] = useState([]); + const [editedShopsData, setEditedShopsData] = useState([]); + const [editingRowIndex, setEditingRowIndex] = useState(null); + const [loading, setLoading] = useState(true); + const [shopsLoading, setShopsLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({ + open: false, + message: "", + severity: "success", + }); + + useEffect(() => { + const fetchTruckLaneDetail = async () => { + if (!truckId) { + setError(t("Truck ID is required")); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + try { + const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; + const truck = data.find((t) => t.id?.toString() === truckId); + + if (truck) { + setTruckData(truck); + // Fetch shops using this truck lane + await fetchShopsByTruckLane(String(truck.truckLanceCode || ""), String(truck.remark || "")); + } else { + setError(t("Truck lane not found")); + } + } catch (err: any) { + console.error("Failed to load truck lane detail:", err); + setError(err?.message ?? String(err) ?? t("Failed to load truck lane detail")); + } finally { + setLoading(false); + } + }; + + fetchTruckLaneDetail(); + }, [truckId]); + + const fetchShopsByTruckLane = async (truckLanceCode: string, remark: string) => { + setShopsLoading(true); + try { + const shops = await findAllShopsByTruckLanceCodeAndRemarkClient(truckLanceCode, remark || ""); + setShopsData(shops || []); + setEditedShopsData(shops || []); + } catch (err: any) { + console.error("Failed to load shops:", err); + setSnackbar({ + open: true, + message: err?.message ?? String(err) ?? t("Failed to load shops"), + severity: "error", + }); + } finally { + setShopsLoading(false); + } + }; + + const handleEdit = (index: number) => { + setEditingRowIndex(index); + const updated = [...shopsData]; + updated[index] = { ...updated[index] }; + setEditedShopsData(updated); + }; + + const handleCancel = (index: number) => { + setEditingRowIndex(null); + setEditedShopsData([...shopsData]); + }; + + const handleSave = async (index: number) => { + const shop = editedShopsData[index]; + if (!shop || !shop.truckId) { + setSnackbar({ + open: true, + message: t("Invalid shop data"), + severity: "error", + }); + return; + } + + setSaving(true); + setError(null); + try { + // Get LoadingSequence from edited data - handle both PascalCase and camelCase + const editedShop = editedShopsData[index]; + const loadingSeq = (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence; + const loadingSequenceValue = (loadingSeq !== null && loadingSeq !== undefined) ? Number(loadingSeq) : 0; + + if (!shop.truckId) { + setSnackbar({ + open: true, + message: "Truck ID is required", + severity: "error", + }); + return; + } + + await updateLoadingSequenceClient({ + id: shop.truckId, + loadingSequence: loadingSequenceValue, + }); + + setSnackbar({ + open: true, + message: t("Loading sequence updated successfully"), + severity: "success", + }); + + // Refresh the shops list + if (truckData) { + await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || "")); + } + setEditingRowIndex(null); + } catch (err: any) { + console.error("Failed to save loading sequence:", err); + setSnackbar({ + open: true, + message: err?.message ?? String(err) ?? t("Failed to save loading sequence"), + severity: "error", + }); + } finally { + setSaving(false); + } + }; + + const handleLoadingSequenceChange = (index: number, value: string) => { + const updated = [...editedShopsData]; + const numValue = parseInt(value, 10); + updated[index] = { + ...updated[index], + LoadingSequence: isNaN(numValue) ? 0 : numValue, + }; + setEditedShopsData(updated); + }; + + const handleDelete = async (truckIdToDelete: number) => { + if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) { + return; + } + + try { + await deleteTruckLaneClient({ id: truckIdToDelete }); + setSnackbar({ + open: true, + message: t("Truck lane deleted successfully"), + severity: "success", + }); + + // Refresh the shops list + if (truckData) { + await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || "")); + } + } catch (err: any) { + console.error("Failed to delete truck lane:", err); + setSnackbar({ + open: true, + message: err?.message ?? String(err) ?? t("Failed to delete truck lane"), + severity: "error", + }); + } + }; + + const handleBack = () => { + router.push("/settings/shop"); + }; + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + {error} + + + + ); + } + + if (!truckData) { + return ( + + + {t("No truck lane data available")} + + + + ); + } + + const storeId = truckData.storeId; + const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "-"; + const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F" + : storeIdStr === "4" || storeIdStr === "4F" ? "4F" + : storeIdStr; + + return ( + + + + + {t("Truck Lane Detail")} + + + + + + + + + + + + {t("TruckLance Code")} + + + {String(truckData.truckLanceCode || "-")} + + + + + + {t("Departure Time")} + + + {formatDepartureTime( + Array.isArray(truckData.departureTime) + ? truckData.departureTime + : (truckData.departureTime ? String(truckData.departureTime) : null) + )} + + + + + + {t("Store ID")} + + + {displayStoreId} + + + + + + {t("Remark")} + + + {String(truckData.remark || "-")} + + + + + + + + + + + {t("Shops Using This Truck Lane")} + + + {shopsLoading ? ( + + + + ) : ( + + + + + {t("Shop Name")} + {t("Shop Code")} + {t("Loading Sequence")} + {t("Remark")} + {t("Actions")} + + + + {shopsData.length === 0 ? ( + + + + {t("No shops found using this truck lane")} + + + + ) : ( + shopsData.map((shop, index) => ( + + + {String(shop.name || "-")} + + + {String(shop.code || "-")} + + + {editingRowIndex === index ? ( + { + const editedShop = editedShopsData[index]; + return (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence ?? 0; + })()} + onChange={(e) => handleLoadingSequenceChange(index, e.target.value)} + disabled={saving} + sx={{ width: 100 }} + /> + ) : ( + (() => { + // Handle both PascalCase and camelCase, and check for 0 as valid value + const loadingSeq = (shop as any).LoadingSequence ?? (shop as any).loadingSequence; + return (loadingSeq !== null && loadingSeq !== undefined) + ? String(loadingSeq) + : "-"; + })() + )} + + + {String(shop.remark || "-")} + + + + {editingRowIndex === index ? ( + <> + handleSave(index)} + disabled={saving} + title={t("Save changes")} + > + + + handleCancel(index)} + disabled={saving} + title={t("Cancel editing")} + > + + + + ) : ( + <> + handleEdit(index)} + title={t("Edit loading sequence")} + > + + + {shop.truckId && ( + handleDelete(shop.truckId!)} + title={t("Delete truck lane")} + > + + + )} + + )} + + + + )) + )} + +
+
+ )} +
+
+ + setSnackbar({ ...snackbar, open: false })} + message={snackbar.message} + /> +
+ ); +}; + +export default TruckLaneDetail; + + diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 82f74f4..7e8a122 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -296,5 +296,75 @@ "Total lines: ": "總數量:", "Balance": "可用數量", "Submitting...": "提交中...", - "Batch Count": "批數" + "Batch Count": "批數", + "Shop": "店鋪", + "Shop Information": "店鋪資訊", + "Shop Name": "店鋪名稱", + "Shop Code": "店鋪編號", + "Truck Lane": "卡車路線", + "Truck Lane Detail": "卡車路線詳情", + "TruckLance Code": "卡車路線編號", + "TruckLance Status": "卡車路線狀態", + "Departure Time": "出發時間", + "Loading Sequence": "裝載順序", + "District Reference": "區域參考", + "Store ID": "店鋪ID", + "Remark": "備註", + "Actions": "操作", + "View Detail": "查看詳情", + "Back": "返回", + "Back to Truck Lane List": "返回卡車路線列表", + "Back to List": "返回列表", + "Add Truck Lane": "新增卡車路線", + "Add New Truck Lane": "新增卡車路線", + "Truck Information": "卡車資訊", + "No Truck data available": "沒有卡車資料", + "No shops found using this truck lane": "沒有找到使用此卡車路線的店鋪", + "Shops Using This Truck Lane": "使用此卡車路線的店鋪", + "Complete": "完成", + "Missing Data": "缺少資料", + "No TruckLance": "無卡車路線", + "Edit shop truck lane": "編輯店鋪卡車路線", + "Delete truck lane": "刪除卡車路線", + "Edit loading sequence": "編輯裝載順序", + "Save changes": "儲存變更", + "Cancel editing": "取消編輯", + "Edit truck lane": "編輯卡車路線", + "Truck ID is required": "需要卡車ID", + "Truck lane not found": "找不到卡車路線", + "No truck lane data available": "沒有卡車路線資料", + "Failed to load truck lanes": "載入卡車路線失敗", + "Failed to load shops": "載入店鋪失敗", + "Loading sequence updated successfully": "裝載順序更新成功", + "Failed to save loading sequence": "儲存裝載順序失敗", + "Truck lane deleted successfully": "卡車路線刪除成功", + "Failed to delete truck lane": "刪除卡車路線失敗", + "Are you sure you want to delete this truck lane?": "您確定要刪除此卡車路線嗎?", + "Invalid shop data": "無效的店鋪資料", + "Contact No": "聯絡電話", + "Contact Email": "聯絡郵箱", + "Contact Name": "聯絡人", + "Addr1": "地址1", + "Addr2": "地址2", + "Addr3": "地址3", + "Shop not found": "找不到店鋪", + "Shop ID is required": "需要店鋪ID", + "Invalid Shop ID": "無效的店鋪ID", + "Failed to load shop detail": "載入店鋪詳情失敗", + "Failed to load shop details": "載入店鋪詳情失敗", + "Failed to save truck data": "儲存卡車資料失敗", + "Failed to delete truck lane": "刪除卡車路線失敗", + "Failed to create truck": "建立卡車失敗", + "Please fill in the following required fields:": "請填寫以下必填欄位:", + "TruckLance Code": "卡車路線編號", + "Enter or select remark": "輸入或選擇備註", + "Not editable for this Store ID": "此店鋪ID不可編輯", + "No Truck Lane data available": "沒有卡車路線資料", + "Please log in to view shop details": "請登入以查看店鋪詳情", + "Invalid truck data": "無效的卡車資料", + "Failed to load truck lane detail": "載入卡車路線詳情失敗", + "Shop Detail": "店鋪詳情", + "Truck Lane Detail": "卡車路線詳情", + "Filter by Status": "按狀態篩選", + "All": "全部" } \ No newline at end of file From eb020cc5d8520b0d02825711c001be4feade7e92 Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Mon, 5 Jan 2026 18:57:39 +0800 Subject: [PATCH 2/8] update shop and truck --- src/components/Shop/Shop.tsx | 2 +- src/i18n/zh/common.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Shop/Shop.tsx b/src/components/Shop/Shop.tsx index ff4136c..7c827ab 100644 --- a/src/components/Shop/Shop.tsx +++ b/src/components/Shop/Shop.tsx @@ -227,7 +227,7 @@ const Shop: React.FC = () => { const criteria: Criterion[] = [ { type: "text", label: t("id"), paramName: "id" }, { type: "text", label: t("code"), paramName: "code" }, - { type: "text", label: t("name"), paramName: "name" }, + { type: "text", label: t("Shop Name"), paramName: "name" }, ]; const columns: Column[] = [ diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 7e8a122..61cc8e9 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -308,7 +308,7 @@ "Departure Time": "出發時間", "Loading Sequence": "裝載順序", "District Reference": "區域參考", - "Store ID": "店鋪ID", + "Store ID": "樓層", "Remark": "備註", "Actions": "操作", "View Detail": "查看詳情", @@ -358,7 +358,7 @@ "Please fill in the following required fields:": "請填寫以下必填欄位:", "TruckLance Code": "卡車路線編號", "Enter or select remark": "輸入或選擇備註", - "Not editable for this Store ID": "此店鋪ID不可編輯", + "Not editable for this Store ID": "此樓層不可編輯", "No Truck Lane data available": "沒有卡車路線資料", "Please log in to view shop details": "請登入以查看店鋪詳情", "Invalid truck data": "無效的卡車資料", From 1c6368540efcca40586223968116d18b31ea52c4 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Mon, 5 Jan 2026 19:05:23 +0800 Subject: [PATCH 3/8] no message --- src/app/api/scheduling/actions.ts | 18 +++++++-- .../DetailedScheduleSearchView.tsx | 38 ++++++++++++++++--- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/app/api/scheduling/actions.ts b/src/app/api/scheduling/actions.ts index 14c891d..0ed01bb 100644 --- a/src/app/api/scheduling/actions.ts +++ b/src/app/api/scheduling/actions.ts @@ -177,22 +177,32 @@ export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) => return response; }) -export const exportProdSchedule = async (token: string | null) => { +export const exportProdSchedule = async ( + token: string | null, + inputs: any, + prodHeaders: string[], + matHeaders: string[] +) => { if (!token) throw new Error("No access token found"); const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, { method: "POST", headers: { + "Content-Type": "application/json", // Critical for @RequestBody "Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Authorization": `Bearer ${token}` - } + }, + // Send everything in one object + body: JSON.stringify({ + ...inputs, + prodHeaders, + matHeaders + }) }); if (!response.ok) throw new Error(`Backend error: ${response.status}`); const arrayBuffer = await response.arrayBuffer(); - - // Convert to Base64 so Next.js can send it safely over the wire return Buffer.from(arrayBuffer).toString('base64'); }; diff --git a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx index 12e17c3..79832fd 100644 --- a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx +++ b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx @@ -78,7 +78,7 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { // paramName: "schedulePeriod", // type: "dateRange", // }, - { label: t("Production Date"), paramName: "scheduleAt", type: "date" }, + { label: t("Production Date"), paramName: "produceAt", type: "date" }, //{ // label: t("Product Count"), // paramName: "totalEstProdCount", @@ -179,9 +179,9 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { ) as ScheduleType[]; const params: SearchProdSchedule = { - //scheduleAt: dayjs(query?.scheduleAt).isValid() - // ? query?.scheduleAt - // : undefined, + produceAt: dayjs(query?.produceAt).isValid() + ? query?.produceAt + : undefined, //schedulePeriod: dayjs(query?.schedulePeriod).isValid() // ? query?.schedulePeriod // : undefined, @@ -304,7 +304,35 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { try { const token = localStorage.getItem("accessToken"); // 1. Get Base64 string from server - const base64String = await exportProdSchedule(token); + // 1. Prepare translated headers using the t() function + + const prodHeaders = [ + t("Item Name"), + t("Avg Qty Last Month"), + t("Stock Qty"), + t("Days Left"), + t("Output Qty"), + t("Batch Need"), + t("Priority") + ]; + + const matHeaders = [ + t("Mat Code"), + t("Mat Name"), + t("Required Qty"), + t("Total Qty Need"), + t("UoM"), + t("Purchased Qty"), + t("On Hand Qty"), + t("Unavailable Qty"), + t("Related Item Code"), + t("Related Item Name"), + t("Material Summary") // The last one can be used as the Sheet Name + ]; + + // 2. Pass these arrays to your server action + // 'inputs' contains your filters (scheduleAt, types, etc.) + const base64String = await exportProdSchedule(token, inputs, prodHeaders, matHeaders); // 2. Convert Base64 back to Blob const byteCharacters = atob(base64String); From 7cc2716b400cc226171703f268893be30287be53 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 5 Jan 2026 19:28:16 +0800 Subject: [PATCH 4/8] Supporting Function: Equipment Repair & Maintenance --- .../settings/equipment/EquipmentTabs.tsx | 52 ++ .../equipment/MaintenanceEdit/page.tsx | 29 ++ src/app/(main)/settings/equipment/page.tsx | 21 +- src/app/api/settings/equipment/index.ts | 5 + .../EquipmentSearch/EquipmentSearch.tsx | 269 ++++++++-- .../EquipmentSearchResults.tsx | 482 ++++++++++++++++++ .../EquipmentSearchWrapper.tsx | 41 +- .../UpdateMaintenanceForm.tsx | 242 +++++++++ src/i18n/en/common.json | 19 +- src/i18n/zh/common.json | 18 +- .../master/service/EquipmentService.kt | 0 11 files changed, 1105 insertions(+), 73 deletions(-) create mode 100644 src/app/(main)/settings/equipment/EquipmentTabs.tsx create mode 100644 src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx create mode 100644 src/components/EquipmentSearch/EquipmentSearchResults.tsx create mode 100644 src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx create mode 100644 src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt diff --git a/src/app/(main)/settings/equipment/EquipmentTabs.tsx b/src/app/(main)/settings/equipment/EquipmentTabs.tsx new file mode 100644 index 0000000..d4e6a5b --- /dev/null +++ b/src/app/(main)/settings/equipment/EquipmentTabs.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; +import { useTranslation } from "react-i18next"; +import { useRouter, useSearchParams } from "next/navigation"; + +type EquipmentTabsProps = { + onTabChange?: (tabIndex: number) => void; +}; + +const EquipmentTabs: React.FC = ({ onTabChange }) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const { t } = useTranslation("common"); + + const tabFromUrl = searchParams.get("tab"); + const initialTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; + const [tabIndex, setTabIndex] = useState(initialTabIndex); + + useEffect(() => { + const tabFromUrl = searchParams.get("tab"); + const newTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; + if (newTabIndex !== tabIndex) { + setTabIndex(newTabIndex); + onTabChange?.(newTabIndex); + } + }, [searchParams, tabIndex, onTabChange]); + + const handleTabChange = (_e: React.SyntheticEvent, newValue: number) => { + setTabIndex(newValue); + onTabChange?.(newValue); + + const params = new URLSearchParams(searchParams.toString()); + if (newValue === 0) { + params.delete("tab"); + } else { + params.set("tab", newValue.toString()); + } + router.push(`/settings/equipment?${params.toString()}`, { scroll: false }); + }; + + return ( + + + + + ); +}; + +export default EquipmentTabs; \ No newline at end of file diff --git a/src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx b/src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx new file mode 100644 index 0000000..65c233f --- /dev/null +++ b/src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { SearchParams } from "@/app/utils/fetchUtil"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { Typography } from "@mui/material"; +import isString from "lodash/isString"; +import { notFound } from "next/navigation"; +import UpdateMaintenanceForm from "@/components/UpdateMaintenance/UpdateMaintenanceForm"; + +type Props = {} & SearchParams; + +const MaintenanceEditPage: React.FC = async ({ searchParams }) => { + const type = "common"; + const { t } = await getServerI18n(type); + const id = isString(searchParams["id"]) + ? parseInt(searchParams["id"]) + : undefined; + if (!id) { + notFound(); + } + return ( + <> + {t("Update Equipment Maintenance and Repair")} + + + + + ); +}; +export default MaintenanceEditPage; \ No newline at end of file diff --git a/src/app/(main)/settings/equipment/page.tsx b/src/app/(main)/settings/equipment/page.tsx index 4456a31..f55631c 100644 --- a/src/app/(main)/settings/equipment/page.tsx +++ b/src/app/(main)/settings/equipment/page.tsx @@ -1,15 +1,18 @@ import { TypeEnum } from "@/app/utils/typeEnum"; -import EquipmentSearch from "@/components/EquipmentSearch"; import { getServerI18n } from "@/i18n"; import Add from "@mui/icons-material/Add"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; import { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; import { fetchAllEquipments } from "@/app/api/settings/equipment"; import { I18nProvider } from "@/i18n"; +import EquipmentSearchWrapper from "@/components/EquipmentSearch/EquipmentSearchWrapper"; + export const metadata: Metadata = { title: "Equipment Type", }; @@ -17,8 +20,6 @@ export const metadata: Metadata = { const productSetting: React.FC = async () => { const type = "common"; const { t } = await getServerI18n(type); - const equipments = await fetchAllEquipments(); - // preloadClaims(); return ( <> @@ -31,22 +32,14 @@ const productSetting: React.FC = async () => { {t("Equipment")} - {/* */} - }> + }> - + ); }; -export default productSetting; +export default productSetting; \ No newline at end of file diff --git a/src/app/api/settings/equipment/index.ts b/src/app/api/settings/equipment/index.ts index 748f076..c64251a 100644 --- a/src/app/api/settings/equipment/index.ts +++ b/src/app/api/settings/equipment/index.ts @@ -13,7 +13,12 @@ export type EquipmentResult = { name: string; description: string | undefined; equipmentTypeId: string | number | undefined; + equipmentCode?: string; action?: any; + repairAndMaintenanceStatus?: boolean | number; + latestRepairAndMaintenanceDate?: string | Date; + lastRepairAndMaintenanceDate?: string | Date; + repairAndMaintenanceRemarks?: string; }; export type Result = { diff --git a/src/components/EquipmentSearch/EquipmentSearch.tsx b/src/components/EquipmentSearch/EquipmentSearch.tsx index 4f00dc6..735b2a8 100644 --- a/src/components/EquipmentSearch/EquipmentSearch.tsx +++ b/src/components/EquipmentSearch/EquipmentSearch.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { EquipmentResult } from "@/app/api/settings/equipment"; import { useTranslation } from "react-i18next"; -import SearchResults, { Column } from "../SearchResults"; +import EquipmentSearchResults, { Column } from "./EquipmentSearchResults"; import { EditNote } from "@mui/icons-material"; import { useRouter, useSearchParams } from "next/navigation"; import { GridDeleteIcon } from "@mui/x-data-grid"; @@ -12,32 +12,90 @@ import { TypeEnum } from "@/app/utils/typeEnum"; import axios from "axios"; import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; import axiosInstance from "@/app/(main)/axios/axiosInstance"; +import { arrayToDateTimeString } from "@/app/utils/formatUtil"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; type Props = { equipments: EquipmentResult[]; + tabIndex?: number; }; type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; -const EquipmentSearch: React.FC = ({ equipments }) => { +const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { const [filteredEquipments, setFilteredEquipments] = - useState(equipments); + useState([]); const { t } = useTranslation("common"); const router = useRouter(); const [filterObj, setFilterObj] = useState({}); const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10, - // totalCount: 0, }); const [totalCount, setTotalCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + const checkReady = () => { + try { + const token = localStorage.getItem("accessToken"); + const hasAuthHeader = axiosInstance.defaults.headers?.common?.Authorization || + axiosInstance.defaults.headers?.Authorization; + + if (token && hasAuthHeader) { + setIsReady(true); + } else if (token) { + setTimeout(checkReady, 50); + } else { + setTimeout(checkReady, 100); + } + } catch (e) { + console.warn("localStorage unavailable", e); + } + }; + + const timer = setTimeout(checkReady, 100); + return () => clearTimeout(timer); + }, []); + + const displayDateTime = useCallback((dateValue: string | Date | number[] | null | undefined): string => { + if (!dateValue) return "-"; + + if (Array.isArray(dateValue)) { + return arrayToDateTimeString(dateValue); + } + + if (typeof dateValue === "string") { + return dateValue; + } + + return String(dateValue); + }, []); + const searchCriteria: Criterion[] = useMemo(() => { - const searchCriteria: Criterion[] = [ + if (tabIndex === 1) { + return [ + { + label: "設備名稱/設備編號", + paramName: "equipmentCode", + type: "text" + }, + { + label: t("Repair and Maintenance Status"), + paramName: "repairAndMaintenanceStatus", + type: "select", + options: ["正常使用中", "正在維護中"] + }, + ]; + } + + return [ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Description"), paramName: "description", type: "text" }, ]; - return searchCriteria; - }, [t, equipments]); + }, [t, tabIndex]); const onDetailClick = useCallback( (equipment: EquipmentResult) => { @@ -46,12 +104,19 @@ const EquipmentSearch: React.FC = ({ equipments }) => { [router], ); + const onMaintenanceEditClick = useCallback( + (equipment: EquipmentResult) => { + router.push(`/settings/equipment/MaintenanceEdit?id=${equipment.id}`); + }, + [router], + ); + const onDeleteClick = useCallback( (equipment: EquipmentResult) => {}, [router], ); - const columns = useMemo[]>( + const generalDataColumns = useMemo[]>( () => [ { name: "id", @@ -78,9 +143,91 @@ const EquipmentSearch: React.FC = ({ equipments }) => { onClick: onDeleteClick, }, ], - [filteredEquipments], + [onDetailClick, onDeleteClick, t], + ); + + const repairMaintenanceColumns = useMemo[]>( + () => [ + { + name: "id", + label: "編輯", + onClick: onMaintenanceEditClick, + buttonIcon: , + align: "left", + headerAlign: "left", + sx: { width: "60px", minWidth: "60px" }, + }, + { + name: "code", + label: "設備名稱", + align: "left", + headerAlign: "left", + sx: { width: "200px", minWidth: "200px" }, + }, + { + name: "equipmentCode", + label: "設備編號", + align: "left", + headerAlign: "left", + sx: { width: "150px", minWidth: "150px" }, + renderCell: (item) => { + return item.equipmentCode || "-"; + }, + }, + { + name: "repairAndMaintenanceStatus", + label: t("Repair and Maintenance Status"), + align: "left", + headerAlign: "left", + sx: { width: "150px", minWidth: "150px" }, + renderCell: (item) => { + const status = item.repairAndMaintenanceStatus; + if (status === 1 || status === true) { + return ( + + 正在維護中 + + ); + } else if (status === 0 || status === false) { + return ( + + 正常使用中 + + ); + } + return "-"; + }, + }, + { + name: "latestRepairAndMaintenanceDate", + label: t("Latest Repair and Maintenance Date"), + align: "left", + headerAlign: "left", + sx: { width: "200px", minWidth: "200px" }, + renderCell: (item) => displayDateTime(item.latestRepairAndMaintenanceDate), + }, + { + name: "lastRepairAndMaintenanceDate", + label: t("Last Repair and Maintenance Date"), + align: "left", + headerAlign: "left", + sx: { width: "200px", minWidth: "200px" }, + renderCell: (item) => displayDateTime(item.lastRepairAndMaintenanceDate), + }, + { + name: "repairAndMaintenanceRemarks", + label: t("Repair and Maintenance Remarks"), + align: "left", + headerAlign: "left", + sx: { width: "200px", minWidth: "200px" }, + }, + ], + [onMaintenanceEditClick, t, displayDateTime], ); + const columns = useMemo(() => { + return tabIndex === 1 ? repairMaintenanceColumns : generalDataColumns; + }, [tabIndex, repairMaintenanceColumns, generalDataColumns]); interface ApiResponse { records: T[]; @@ -89,73 +236,115 @@ const EquipmentSearch: React.FC = ({ equipments }) => { const refetchData = useCallback( async (filterObj: SearchQuery) => { - const authHeader = axiosInstance.defaults.headers["Authorization"]; - if (!authHeader) { + const token = localStorage.getItem("accessToken"); + const hasAuthHeader = axiosInstance.defaults.headers?.common?.Authorization || + axiosInstance.defaults.headers?.Authorization; + + if (!token || !hasAuthHeader) { + console.warn("Token or auth header not ready, skipping API call"); + setIsLoading(false); return; } + + setIsLoading(true); + + const transformedFilter: any = { ...filterObj }; + + // For maintenance tab (tabIndex === 1), if equipmentCode is provided, + // also search by code (equipment name) with the same value + if (tabIndex === 1 && transformedFilter.equipmentCode) { + transformedFilter.code = transformedFilter.equipmentCode; + } + + if (transformedFilter.repairAndMaintenanceStatus) { + if (transformedFilter.repairAndMaintenanceStatus === "正常使用中") { + transformedFilter.repairAndMaintenanceStatus = false; + } else if (transformedFilter.repairAndMaintenanceStatus === "正在維護中") { + transformedFilter.repairAndMaintenanceStatus = true; + } else if (transformedFilter.repairAndMaintenanceStatus === "All") { + delete transformedFilter.repairAndMaintenanceStatus; + } + } + const params = { pageNum: pagingController.pageNum, pageSize: pagingController.pageSize, - ...filterObj, + ...transformedFilter, }; + try { + const endpoint = tabIndex === 1 + ? `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage` + : `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`; + const response = await axiosInstance.get>( - `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`, + endpoint, { params }, ); - console.log(response); + console.log("API Response:", response); + console.log("Records:", response.data.records); + console.log("Total:", response.data.total); if (response.status == 200) { - setFilteredEquipments(response.data.records); - setTotalCount(response.data.total); - return response; + setFilteredEquipments(response.data.records || []); + setTotalCount(response.data.total || 0); } else { throw "400"; } } catch (error) { console.error("Error fetching equipment types:", error); - throw error; + setFilteredEquipments([]); + setTotalCount(0); + } finally { + setIsLoading(false); } }, - [axiosInstance, pagingController.pageNum, pagingController.pageSize], + [pagingController.pageNum, pagingController.pageSize, tabIndex], ); useEffect(() => { - refetchData(filterObj); - }, [filterObj, pagingController.pageNum, pagingController.pageSize]); + if (isReady) { + refetchData(filterObj); + } + }, [filterObj, pagingController.pageNum, pagingController.pageSize, tabIndex, isReady, refetchData]); const onReset = useCallback(() => { - setFilteredEquipments(equipments); - }, [equipments]); + setFilterObj({}); + setPagingController({ + pageNum: 1, + pageSize: pagingController.pageSize, + }); + }, [pagingController.pageSize]); return ( <> { - // setFilteredItems( - // equipmentTypes.filter((pm) => { - // return ( - // pm.code.toLowerCase().includes(query.code.toLowerCase()) && - // pm.name.toLowerCase().includes(query.name.toLowerCase()) - // ); - // }) - // ); setFilterObj({ ...query, }); }} onReset={onReset} /> - - items={filteredEquipments} - columns={columns} - setPagingController={setPagingController} - pagingController={pagingController} - totalCount={totalCount} - isAutoPaging={false} - /> + + + items={filteredEquipments} + columns={columns} + setPagingController={setPagingController} + pagingController={pagingController} + totalCount={totalCount} + isAutoPaging={false} + /> + ); }; -export default EquipmentSearch; +export default EquipmentSearch; \ No newline at end of file diff --git a/src/components/EquipmentSearch/EquipmentSearchResults.tsx b/src/components/EquipmentSearch/EquipmentSearchResults.tsx new file mode 100644 index 0000000..7f84a41 --- /dev/null +++ b/src/components/EquipmentSearch/EquipmentSearchResults.tsx @@ -0,0 +1,482 @@ +"use client"; + +import React, { + ChangeEvent, + Dispatch, + MouseEvent, + SetStateAction, + useCallback, + useMemo, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import Paper from "@mui/material/Paper"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell, { TableCellProps } from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination, { + TablePaginationProps, +} from "@mui/material/TablePagination"; +import TableRow from "@mui/material/TableRow"; +import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton"; +import { + ButtonOwnProps, + Checkbox, + Icon, + IconOwnProps, + SxProps, + Theme, +} from "@mui/material"; +import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; +import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; +import { filter, remove, uniq } from "lodash"; + +export interface ResultWithId { + id: string | number; +} + +type ColumnType = "icon" | "decimal" | "integer" | "checkbox"; + +interface BaseColumn { + name: keyof T; + label: string; + align?: TableCellProps["align"]; + headerAlign?: TableCellProps["align"]; + sx?: SxProps | undefined; + style?: Partial & { [propName: string]: string }; + type?: ColumnType; + renderCell?: (params: T) => React.ReactNode; +} + +interface IconColumn extends BaseColumn { + name: keyof T; + type: "icon"; + icon?: React.ReactNode; + icons?: { [columnValue in keyof T]: React.ReactNode }; + color?: IconOwnProps["color"]; + colors?: { [columnValue in keyof T]: IconOwnProps["color"] }; +} + +interface DecimalColumn extends BaseColumn { + type: "decimal"; +} + +interface IntegerColumn extends BaseColumn { + type: "integer"; +} + +interface CheckboxColumn extends BaseColumn { + type: "checkbox"; + disabled?: (params: T) => boolean; + // checkboxIds: readonly (string | number)[], + // setCheckboxIds: (ids: readonly (string | number)[]) => void +} + +interface ColumnWithAction extends BaseColumn { + onClick: (item: T) => void; + buttonIcon: React.ReactNode; + buttonIcons: { [columnValue in keyof T]: React.ReactNode }; + buttonColor?: IconButtonOwnProps["color"]; +} + +export type Column = + | BaseColumn + | IconColumn + | DecimalColumn + | CheckboxColumn + | ColumnWithAction; + +interface Props { + totalCount?: number; + items: T[]; + columns: Column[]; + noWrapper?: boolean; + setPagingController?: Dispatch< + SetStateAction<{ + pageNum: number; + pageSize: number; + }> + >; + pagingController?: { pageNum: number; pageSize: number }; + isAutoPaging?: boolean; + checkboxIds?: (string | number)[]; + setCheckboxIds?: Dispatch>; + onRowClick?: (item: T) => void; +} + +function isActionColumn( + column: Column, +): column is ColumnWithAction { + return Boolean((column as ColumnWithAction).onClick); +} + +function isIconColumn( + column: Column, +): column is IconColumn { + return column.type === "icon"; +} + +function isDecimalColumn( + column: Column, +): column is DecimalColumn { + return column.type === "decimal"; +} + +function isIntegerColumn( + column: Column, +): column is IntegerColumn { + return column.type === "integer"; +} + +function isCheckboxColumn( + column: Column, +): column is CheckboxColumn { + return column.type === "checkbox"; +} + +// Icon Component Functions +function convertObjectKeysToLowercase( + obj: T, +): object | undefined { + return obj + ? Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), + ) + : undefined; +} + +function handleIconColors( + column: IconColumn, + value: T[keyof T], +): IconOwnProps["color"] { + const colors = convertObjectKeysToLowercase(column.colors ?? {}); + const valueKey = String(value).toLowerCase() as keyof typeof colors; + + if (colors && valueKey in colors) { + return colors[valueKey]; + } + + return column.color ?? "primary"; +} + +function handleIconIcons( + column: IconColumn, + value: T[keyof T], +): React.ReactNode { + const icons = convertObjectKeysToLowercase(column.icons ?? {}); + const valueKey = String(value).toLowerCase() as keyof typeof icons; + + if (icons && valueKey in icons) { + return icons[valueKey]; + } + + return column.icon ?? ; +} +export const defaultPagingController: { pageNum: number; pageSize: number } = { + pageNum: 1, + pageSize: 10, +}; + +export type defaultSetPagingController = Dispatch< + SetStateAction<{ + pageNum: number; + pageSize: number; + }> +> + +function EquipmentSearchResults({ + items, + columns, + noWrapper, + pagingController, + setPagingController, + isAutoPaging = true, + totalCount, + checkboxIds = [], + setCheckboxIds = undefined, + onRowClick = undefined, +}: Props) { + const { t } = useTranslation("common"); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(10); + + /// this + const handleChangePage: TablePaginationProps["onPageChange"] = ( + _event, + newPage, + ) => { + console.log(_event); + setPage(newPage); + if (setPagingController) { + setPagingController({ + ...(pagingController ?? defaultPagingController), + pageNum: newPage + 1, + }); + } + }; + + const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( + event, + ) => { + console.log(event); + const newSize = +event.target.value; + setRowsPerPage(newSize); + setPage(0); + if (setPagingController) { + setPagingController({ + ...(pagingController ?? defaultPagingController), + pageNum: 1, + pageSize: newSize, + }); + } + }; + + // checkbox + const currItems = useMemo(() => { + return items.length > 10 ? items + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((i) => i.id) + : items.map((i) => i.id) + }, [items, page, rowsPerPage]) + + const currItemsWithChecked = useMemo(() => { + return filter(checkboxIds, function (c) { + return currItems.includes(c); + }) + }, [checkboxIds, items, page, rowsPerPage]) + + const handleRowClick = useCallback( + (event: MouseEvent, item: T, columns: Column[]) => { + // check is disabled or not + let disabled = false; + columns.forEach((col) => { + if (isCheckboxColumn(col) && col.disabled) { + disabled = col.disabled(item); + if (disabled) { + return; + } + } + }); + + if (disabled) { + return; + } + + // set id + const id = item.id; + if (setCheckboxIds) { + const selectedIndex = checkboxIds.indexOf(id); + let newSelected: (string | number)[] = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(checkboxIds, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(checkboxIds.slice(1)); + } else if (selectedIndex === checkboxIds.length - 1) { + newSelected = newSelected.concat(checkboxIds.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + checkboxIds.slice(0, selectedIndex), + checkboxIds.slice(selectedIndex + 1), + ); + } + setCheckboxIds(newSelected); + } + }, + [checkboxIds, setCheckboxIds], + ); + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (setCheckboxIds) { + const pageItemId = currItems + + if (event.target.checked) { + setCheckboxIds((prev) => uniq([...prev, ...pageItemId])) + } else { + setCheckboxIds((prev) => filter(prev, function (p) { return !pageItemId.includes(p); })) + } + } + } + + const table = ( + <> + + + + + {columns.map((column, idx) => ( + isCheckboxColumn(column) ? + + 0 && currItemsWithChecked.length < currItems.length} + checked={currItems.length > 0 && currItemsWithChecked.length >= currItems.length} + onChange={handleSelectAllClick} + /> + + : + {column.label.split('\n').map((line, index) => ( +
{line}
// Render each line in a div + ))} +
+ ))} +
+
+ + {isAutoPaging + ? items + .slice((pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage), + (pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage) + (pagingController?.pageSize ?? rowsPerPage)) + .map((item) => { + return ( + { + setCheckboxIds + ? handleRowClick(event, item, columns) + : undefined + + if (onRowClick) { + onRowClick(item) + } + } + } + role={setCheckboxIds ? "checkbox" : undefined} + > + {columns.map((column, idx) => { + const columnName = column.name; + + return ( + + ); + })} + + ); + }) + : items.map((item) => { + return ( + { + setCheckboxIds + ? handleRowClick(event, item, columns) + : undefined + + if (onRowClick) { + onRowClick(item) + } + } + } + role={setCheckboxIds ? "checkbox" : undefined} + > + {columns.map((column, idx) => { + const columnName = column.name; + + return ( + + ); + })} + + ); + })} + +
+
+ + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } +/> + + ); + + return noWrapper ? table : {table}; +} + +// Table cells +interface TableCellsProps { + column: Column; + columnName: keyof T; + idx: number; + item: T; + checkboxIds: (string | number)[]; +} + +function TabelCells({ + column, + columnName, + idx, + item, + checkboxIds = [], +}: TableCellsProps) { + const isItemSelected = checkboxIds.includes(item.id); + + return ( + + {isActionColumn(column) ? ( + column.onClick(item)} + > + {column.buttonIcon} + + ) : isIconColumn(column) ? ( + + {handleIconIcons(column, item[columnName])} + + ) : isDecimalColumn(column) ? ( + <>{decimalFormatter.format(Number(item[columnName]))} + ) : isIntegerColumn(column) ? ( + <>{integerFormatter.format(Number(item[columnName]))} + ) : isCheckboxColumn(column) ? ( + + ) : column.renderCell ? ( + column.renderCell(item) + ) : ( + <>{item[columnName] as string} + )} + + ); +} + +export default EquipmentSearchResults; \ No newline at end of file diff --git a/src/components/EquipmentSearch/EquipmentSearchWrapper.tsx b/src/components/EquipmentSearch/EquipmentSearchWrapper.tsx index 0efe9a7..a1cf35d 100644 --- a/src/components/EquipmentSearch/EquipmentSearchWrapper.tsx +++ b/src/components/EquipmentSearch/EquipmentSearchWrapper.tsx @@ -1,28 +1,35 @@ -import { fetchAllEquipments } from "@/app/api/settings/equipment"; -import EquipmentSearchLoading from "./EquipmentSearchLoading"; -import { SearchParams } from "@/app/utils/fetchUtil"; -import { TypeEnum } from "@/app/utils/typeEnum"; -import { notFound } from "next/navigation"; +"use client"; + +import { useState, useEffect } from "react"; import EquipmentSearch from "./EquipmentSearch"; +import EquipmentSearchLoading from "./EquipmentSearchLoading"; +import EquipmentTabs from "@/app/(main)/settings/equipment/EquipmentTabs"; +import { useSearchParams } from "next/navigation"; interface SubComponents { Loading: typeof EquipmentSearchLoading; } -type Props = { - // type: TypeEnum; -}; +const EquipmentSearchWrapper: React.FC & SubComponents = () => { + const searchParams = useSearchParams(); + const tabFromUrl = searchParams.get("tab"); + const initialTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; + const [tabIndex, setTabIndex] = useState(initialTabIndex); + + useEffect(() => { + const tabFromUrl = searchParams.get("tab"); + const newTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; + setTabIndex(newTabIndex); + }, [searchParams]); -const EquipmentSearchWrapper: React.FC & SubComponents = async ( - { - // type, - }, -) => { - // console.log(type) - // var result = await fetchAllEquipmentTypes() - return ; + return ( + <> + + + + ); }; EquipmentSearchWrapper.Loading = EquipmentSearchLoading; -export default EquipmentSearchWrapper; +export default EquipmentSearchWrapper; \ No newline at end of file diff --git a/src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx b/src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx new file mode 100644 index 0000000..1d319f2 --- /dev/null +++ b/src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx @@ -0,0 +1,242 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { + Button, + Card, + CardContent, + FormControl, + InputLabel, + MenuItem, + Select, + TextField, + Typography, + Stack, + Grid, +} from "@mui/material"; +import { Check, Close } from "@mui/icons-material"; +import axiosInstance from "@/app/(main)/axios/axiosInstance"; +import { NEXT_PUBLIC_API_URL } from "@/config/api"; + +type Props = { + id: number; +}; + +type EquipmentDetailData = { + id: number; + code: string; + name: string; + equipmentCode?: string; + repairAndMaintenanceStatus?: boolean | null; + repairAndMaintenanceRemarks?: string | null; +}; + +const UpdateMaintenanceForm: React.FC = ({ id }) => { + const { t } = useTranslation("common"); + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(true); + const [equipmentData, setEquipmentData] = useState(null); + const [status, setStatus] = useState(null); + const [remarks, setRemarks] = useState(""); + + useEffect(() => { + const fetchEquipmentDetail = async () => { + try { + setFetching(true); + const response = await axiosInstance.get( + `${NEXT_PUBLIC_API_URL}/EquipmentDetail/details/${id}` + ); + if (response.data) { + setEquipmentData(response.data); + setStatus(response.data.repairAndMaintenanceStatus ?? null); + setRemarks(response.data.repairAndMaintenanceRemarks ?? ""); + } + } catch (error) { + console.error("Error fetching equipment detail:", error); + } finally { + setFetching(false); + } + }; + + fetchEquipmentDetail(); + }, [id]); + + const handleSave = useCallback(async () => { + if (!equipmentData) return; + + try { + setLoading(true); + const updateData = { + repairAndMaintenanceStatus: status, + repairAndMaintenanceRemarks: remarks, + }; + + await axiosInstance.put( + `${NEXT_PUBLIC_API_URL}/EquipmentDetail/update/${id}`, + updateData, + { + headers: { "Content-Type": "application/json" }, + } + ); + + router.push("/settings/equipment?tab=1"); + } catch (error) { + console.error("Error updating maintenance:", error); + alert(t("Error saving data") || "Error saving data"); + } finally { + setLoading(false); + } + }, [equipmentData, status, remarks, id, router, t]); + + const handleCancel = useCallback(() => { + router.push("/settings/equipment?tab=1"); + }, [router]); + + if (fetching) { + return ( + + {t("Loading") || "Loading..."} + + ); + } + + if (!equipmentData) { + return ( + + {t("Equipment not found") || "Equipment not found"} + + ); + } + + return ( + + + + + {t("Equipment Information")} + + + + + + + + + + + + + + {t("Repair and Maintenance Status")} + + + + + + + setRemarks(e.target.value)} + fullWidth + multiline + rows={4} + variant="filled" + InputLabelProps={{ + shrink: true, + sx: { fontSize: "0.9375rem" }, + }} + InputProps={{ + sx: { + paddingTop: "8px", + alignItems: "flex-start", + paddingBottom: "8px", + }, + }} + sx={{ + "& .MuiInputBase-input": { + paddingTop: "16px", + }, + }} + /> + + + + + + + + + + + ); +}; + +export default UpdateMaintenanceForm; \ No newline at end of file diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 2b2f3a3..d9bdc2e 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -1,3 +1,20 @@ { - "Grade {{grade}}": "Grade {{grade}}" + "Grade {{grade}}": "Grade {{grade}}", + "General Data": "General Data", + "Repair and Maintenance": "Repair and Maintenance", + "Repair and Maintenance Status": "Repair and Maintenance Status", + "Latest Repair and Maintenance Date": "Latest Repair and Maintenance Date", + "Last Repair and Maintenance Date": "Last Repair and Maintenance Date", + "Repair and Maintenance Remarks": "Repair and Maintenance Remarks", + "Update Equipment Maintenance and Repair": "Update Equipment Maintenance and Repair", + "Equipment Information": "Equipment Information", + "Loading": "Loading...", + "Equipment not found": "Equipment not found", + "Error saving data": "Error saving data", + "Cancel": "Cancel", + "Save": "Save", + "Yes": "Yes", + "No": "No", + "Equipment Name": "Equipment Name", + "Equipment Code": "Equipment Code" } \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 61cc8e9..fa723e6 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -366,5 +366,21 @@ "Shop Detail": "店鋪詳情", "Truck Lane Detail": "卡車路線詳情", "Filter by Status": "按狀態篩選", - "All": "全部" + "All": "全部", + "General Data": "基本資料", + "Repair and Maintenance": "維修和保養", + "Repair and Maintenance Status": "維修和保養狀態", + "Latest Repair and Maintenance Date": "最新維修和保養日期", + "Last Repair and Maintenance Date": "上次維修和保養日期", + "Repair and Maintenance Remarks": "維修和保養備註", + "Rows per page": "每頁行數", + "Equipment Name": "設備名稱", + "Equipment Code": "設備編號", + "Yes": "是", + "No": "否", + "Update Equipment Maintenance and Repair": "更新設備的維修和保養", + "Equipment Information": "設備資訊", + "Loading": "載入中...", + "Equipment not found": "找不到設備", + "Error saving data": "保存數據時出錯" } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt new file mode 100644 index 0000000..e69de29 From 54dde3968dbd57e6e3fb0c650e404bdd5dddc5bc Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Wed, 7 Jan 2026 03:54:42 +0800 Subject: [PATCH 5/8] update shop and truck , item --- src/app/api/settings/item/actions.ts | 8 + src/app/api/settings/item/index.ts | 8 + src/app/api/shop/actions.ts | 10 + src/app/api/shop/client.ts | 5 + src/components/Breadcrumb/Breadcrumb.tsx | 2 +- src/components/CreateItem/CreateItem.tsx | 52 ++++- .../CreateItem/CreateItemWrapper.tsx | 13 +- src/components/CreateItem/ProductDetails.tsx | 203 +++++++++++------- src/components/ItemsSearch/ItemsSearch.tsx | 85 +++++++- .../NavigationContent/NavigationContent.tsx | 2 +- src/components/SearchBox/SearchBox.tsx | 34 +++ src/components/Shop/TruckLane.tsx | 28 ++- src/components/Shop/TruckLaneDetail.tsx | 79 ++++--- src/components/StockIn/ShelfLifeInput.tsx | 19 +- src/i18n/en/common.json | 4 + src/i18n/en/items.json | 14 +- src/i18n/zh/common.json | 3 +- src/i18n/zh/items.json | 13 +- 18 files changed, 434 insertions(+), 148 deletions(-) diff --git a/src/app/api/settings/item/actions.ts b/src/app/api/settings/item/actions.ts index 154f14c..d74c089 100644 --- a/src/app/api/settings/item/actions.ts +++ b/src/app/api/settings/item/actions.ts @@ -37,6 +37,14 @@ export type CreateItemInputs = { qcChecks: QcChecksInputs[]; qcChecks_active: number[]; qcCategoryId: number | undefined; + store_id?: string | undefined; + warehouse?: string | undefined; + area?: string | undefined; + slot?: string | undefined; + LocationCode?: string | undefined; + isEgg?: boolean | undefined; + isFee?: boolean | undefined; + isBag?: boolean | undefined; }; export const saveItem = async (data: CreateItemInputs) => { diff --git a/src/app/api/settings/item/index.ts b/src/app/api/settings/item/index.ts index 57bd464..a5bf7de 100644 --- a/src/app/api/settings/item/index.ts +++ b/src/app/api/settings/item/index.ts @@ -53,6 +53,14 @@ export type ItemsResult = { fgName?: string; excludeDate?: string; qcCategory?: QcCategoryResult; + store_id?: string | undefined; + warehouse?: string | undefined; + area?: string | undefined; + slot?: string | undefined; + LocationCode?: string | undefined; + isEgg?: boolean | undefined; + isFee?: boolean | undefined; + isBag?: boolean | undefined; }; export type Result = { diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts index 19231be..ab12a8d 100644 --- a/src/app/api/shop/actions.ts +++ b/src/app/api/shop/actions.ts @@ -165,6 +165,16 @@ export const findAllShopsByTruckLanceCodeAndRemarkAction = cache(async (truckLan }); }); +export const findAllShopsByTruckLanceCodeAction = cache(async (truckLanceCode: string) => { + const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse`; + const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`; + + return serverFetchJson(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => { const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`; diff --git a/src/app/api/shop/client.ts b/src/app/api/shop/client.ts index 6f2da57..4d046d6 100644 --- a/src/app/api/shop/client.ts +++ b/src/app/api/shop/client.ts @@ -8,6 +8,7 @@ import { createTruckAction, findAllUniqueTruckLaneCombinationsAction, findAllShopsByTruckLanceCodeAndRemarkAction, + findAllShopsByTruckLanceCodeAction, updateLoadingSequenceAction, type SaveTruckLane, type DeleteTruckLane, @@ -44,6 +45,10 @@ export const findAllShopsByTruckLanceCodeAndRemarkClient = async (truckLanceCode return await findAllShopsByTruckLanceCodeAndRemarkAction(truckLanceCode, remark); }; +export const findAllShopsByTruckLanceCodeClient = async (truckLanceCode: string) => { + return await findAllShopsByTruckLanceCodeAction(truckLanceCode); +}; + export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise => { return await updateLoadingSequenceAction(data); }; diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 0a872e4..23d378d 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -17,7 +17,7 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/qrCodeHandle": "QR Code Handle", "/settings/rss": "Demand Forecast Setting", "/settings/equipment": "Equipment", - "/settings/shop": "Shop", + "/settings/shop": "ShopAndTruck", "/settings/shop/detail": "Shop Detail", "/settings/shop/truckdetail": "Truck Lane Detail", "/scheduling/rough": "Demand Forecast", diff --git a/src/components/CreateItem/CreateItem.tsx b/src/components/CreateItem/CreateItem.tsx index 1572e3f..69b8e9e 100644 --- a/src/components/CreateItem/CreateItem.tsx +++ b/src/components/CreateItem/CreateItem.tsx @@ -21,7 +21,7 @@ import { TabsProps, Typography, } from "@mui/material"; -import { Check, Close, EditNote } from "@mui/icons-material"; +import { Check, Close, EditNote, ArrowBack } from "@mui/icons-material"; import { TypeEnum } from "@/app/utils/typeEnum"; import ProductDetails from "./ProductDetails"; import { CreateItemResponse } from "@/app/api/utils"; @@ -30,13 +30,15 @@ import { ItemQc } from "@/app/api/settings/item"; import { saveItemQcChecks } from "@/app/api/settings/qcCheck/actions"; import { useGridApiRef } from "@mui/x-data-grid"; import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; +import { WarehouseResult } from "@/app/api/warehouse"; type Props = { isEditMode: boolean; // type: TypeEnum; defaultValues: Partial | undefined; qcChecks: ItemQc[]; - qcCategoryCombo: QcCategoryCombo[] + qcCategoryCombo: QcCategoryCombo[]; + warehouses: WarehouseResult[]; }; const CreateItem: React.FC = ({ @@ -45,6 +47,7 @@ const CreateItem: React.FC = ({ defaultValues, qcChecks, qcCategoryCombo, + warehouses, }) => { // console.log(type) const apiRef = useGridApiRef(); @@ -109,6 +112,26 @@ const CreateItem: React.FC = ({ setServerError(t("An error has occurred. Please try again later.")); return false; } + + // Normalize LocationCode: convert empty string to null + if (data.LocationCode && data.LocationCode.trim() !== "") { + // Parse LocationCode and populate store_id, warehouse, area, slot + const parts = data.LocationCode.split("-"); + if (parts.length >= 4) { + data.store_id = parts[0] || undefined; + data.warehouse = parts[1] || undefined; + data.area = parts[2] || undefined; + data.slot = parts[3] || undefined; + } + } else { + // If LocationCode is null or empty, set LocationCode to null and clear related fields + data.LocationCode = undefined; + data.store_id = undefined; + data.warehouse = undefined; + data.area = undefined; + data.slot = undefined; + } + console.log("data posted"); console.log(data); const qcCheck = @@ -178,9 +201,19 @@ const CreateItem: React.FC = ({ onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > - - {t(`${mode} ${title}`)} - + + + + {t(`${mode} ${title}`)} + + = ({ {serverError} )} - {tabIndex === 0 && } + {tabIndex === 0 && ( + + )} {tabIndex === 1 && } {/* {type === TypeEnum.MATERIAL && } */} {/* {type === TypeEnum.BYPRODUCT && } */} diff --git a/src/components/CreateItem/CreateItemWrapper.tsx b/src/components/CreateItem/CreateItemWrapper.tsx index c8aeb35..b3e2d6c 100644 --- a/src/components/CreateItem/CreateItemWrapper.tsx +++ b/src/components/CreateItem/CreateItemWrapper.tsx @@ -6,6 +6,7 @@ import { notFound } from "next/navigation"; import { fetchItem } from "@/app/api/settings/item"; import { fetchQcItems } from "@/app/api/settings/qcItem"; import { fetchQcCategoryCombo } from "@/app/api/settings/qcCategory"; +import { fetchWarehouseList } from "@/app/api/warehouse"; interface SubComponents { Loading: typeof CreateItemLoading; } @@ -38,11 +39,20 @@ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { maxQty: item?.maxQty, qcChecks: qcChecks, qcChecks_active: activeRows, - qcCategoryId: item.qcCategory?.id + qcCategoryId: item.qcCategory?.id, + store_id: item?.store_id, + warehouse: item?.warehouse, + area: item?.area, + slot: item?.slot, + LocationCode: item?.LocationCode, + isEgg: item?.isEgg, + isFee: item?.isFee, + isBag: item?.isBag, }; } const qcCategoryCombo = await fetchQcCategoryCombo(); + const warehouses = await fetchWarehouseList(); return ( & SubComponents = async ({ id }) => { defaultValues={defaultValues} qcChecks={qcChecks || []} qcCategoryCombo={qcCategoryCombo} + warehouses={warehouses} /> ); }; diff --git a/src/components/CreateItem/ProductDetails.tsx b/src/components/CreateItem/ProductDetails.tsx index 12a05d7..2bdda49 100644 --- a/src/components/CreateItem/ProductDetails.tsx +++ b/src/components/CreateItem/ProductDetails.tsx @@ -5,12 +5,20 @@ import { Button, Card, CardContent, + FormControl, + FormControlLabel, + FormLabel, Grid, + InputLabel, + MenuItem, + Radio, + RadioGroup, + Select, Stack, TextField, Typography, } from "@mui/material"; -import { Check, Close, EditNote } from "@mui/icons-material"; +import { Check, EditNote } from "@mui/icons-material"; import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import InputDataGrid from "../InputDataGrid"; @@ -19,11 +27,10 @@ import { SyntheticEvent, useCallback, useMemo, useState } from "react"; import { GridColDef, GridRowModel } from "@mui/x-data-grid"; import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; import { TypeEnum } from "@/app/utils/typeEnum"; -import { NumberInputProps } from "./NumberInputProps"; import { CreateItemInputs } from "@/app/api/settings/item/actions"; -import { RestartAlt } from "@mui/icons-material"; import { ItemQc } from "@/app/api/settings/item"; import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; +import { WarehouseResult } from "@/app/api/warehouse"; type Props = { // isEditMode: boolean; // type: TypeEnum; @@ -32,9 +39,11 @@ type Props = { defaultValues?: Partial | undefined; qcChecks?: ItemQc[]; qcCategoryCombo: QcCategoryCombo[]; + warehouses: WarehouseResult[]; }; -const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo }) => { +const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => { + const { t, i18n: { language }, @@ -42,13 +51,11 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo }) => { const { register, - formState: { errors, defaultValues, touchedFields }, + formState: { errors, touchedFields }, watch, control, setValue, getValues, - reset, - resetField, setError, clearErrors, } = useFormContext(); @@ -103,11 +110,6 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo }) => { // }, // [] // ); - const handleCancel = () => { - // router.replace(`/settings/product`); - console.log("cancel"); - }; - const handleAutoCompleteChange = useCallback((event: SyntheticEvent, value: QcCategoryCombo, onChange: (...event: any[]) => void) => { onChange(value.id) }, []) @@ -124,6 +126,7 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo }) => { = ({ isEditMode, qcCategoryCombo }) => { = ({ isEditMode, qcCategoryCombo }) => { /> - ( + + {t("Type")} + + {errors.type && ( + + {errors.type.message} + + )} + + )} /> - - - - - - - - - - - - = ({ isEditMode, qcCategoryCombo }) => { )} /> + + ( + ({ + label: `${w.code}`, + code: w.code, + }))} + getOptionLabel={(option) => + typeof option === "string" + ? option + : option.label ?? option.code ?? "" + } + value={ + warehouses + .map((w) => ({ + label: `${w.code}`, + code: w.code, + })) + .find((opt) => opt.code === field.value) || + (field.value + ? { label: field.value as string, code: field.value as string } + : null) + } + onChange={(_e, value) => { + if (typeof value === "string") { + field.onChange(value.trim() === "" ? undefined : value); + } else { + field.onChange(value?.code ? (value.code.trim() === "" ? undefined : value.code) : undefined); + } + }} + onInputChange={(_e, value) => { + // keep manual input synced - convert empty string to undefined + field.onChange(value.trim() === "" ? undefined : value); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + + + {t("Special Type")} + { + const value = e.target.value; + setValue("isEgg", value === "isEgg", { shouldValidate: true }); + setValue("isFee", value === "isFee", { shouldValidate: true }); + setValue("isBag", value === "isBag", { shouldValidate: true }); + }} + > + } label={t("None")} /> + } label={t("isEgg")} /> + } label={t("isFee")} /> + } label={t("isBag")} /> + + + = ({ isEditMode, qcCategoryCombo }) => { > {isEditMode ? t("Save") : t("Confirm")} - - {/* diff --git a/src/components/ItemsSearch/ItemsSearch.tsx b/src/components/ItemsSearch/ItemsSearch.tsx index cce9878..a178cd2 100644 --- a/src/components/ItemsSearch/ItemsSearch.tsx +++ b/src/components/ItemsSearch/ItemsSearch.tsx @@ -8,6 +8,7 @@ import SearchResults, { Column } from "../SearchResults"; import { EditNote } from "@mui/icons-material"; import { useRouter, useSearchParams } from "next/navigation"; import { GridDeleteIcon } from "@mui/x-data-grid"; +import { Chip } from "@mui/material"; import { TypeEnum } from "@/app/utils/typeEnum"; import axios from "axios"; import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; @@ -19,6 +20,10 @@ type Props = { type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; +type ItemsResultWithStatus = ItemsResult & { + status?: "complete" | "missing"; +}; + const ItemsSearch: React.FC = ({ items }) => { const [filteredItems, setFilteredItems] = useState(items); const { t } = useTranslation("items"); @@ -47,7 +52,31 @@ const ItemsSearch: React.FC = ({ items }) => { const onDeleteClick = useCallback((item: ItemsResult) => {}, [router]); - const columns = useMemo[]>( + const checkItemStatus = useCallback((item: ItemsResult): "complete" | "missing" => { + // Check if type exists and is not empty + const hasType = item.type != null && String(item.type).trim() !== ""; + + // Check if qcCategory exists (can be object or id) - handle case sensitivity + const itemAny = item as any; + const hasQcCategory = item.qcCategory != null || + itemAny.qcCategoryId != null || + itemAny.qcCategoryid != null || + itemAny.qccategoryid != null; + + // Check if LocationCode exists and is not empty - handle case sensitivity + const hasLocationCode = (item.LocationCode != null && String(item.LocationCode).trim() !== "") || + (itemAny.LocationCode != null && String(itemAny.LocationCode).trim() !== "") || + (itemAny.locationCode != null && String(itemAny.locationCode).trim() !== "") || + (itemAny.locationcode != null && String(itemAny.locationcode).trim() !== ""); + + // If all three are present, return "complete", otherwise "missing" + if (hasType && hasQcCategory && hasLocationCode) { + return "complete"; + } + return "missing"; + }, []); + + const columns = useMemo[]>( () => [ { name: "id", @@ -63,6 +92,22 @@ const ItemsSearch: React.FC = ({ items }) => { name: "name", label: t("Name"), }, + { + name: "type", + label: t("Type"), + }, + { + name: "status", + label: t("Status"), + renderCell: (item) => { + const status = item.status || checkItemStatus(item); + if (status === "complete") { + return ; + } else { + return ; + } + }, + }, { name: "action", label: t(""), @@ -70,7 +115,7 @@ const ItemsSearch: React.FC = ({ items }) => { onClick: onDeleteClick, }, ], - [onDeleteClick, onDetailClick, t], + [onDeleteClick, onDetailClick, t, checkItemStatus], ); const refetchData = useCallback( @@ -89,9 +134,35 @@ const ItemsSearch: React.FC = ({ items }) => { `${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { params }, ); - console.log(response); + console.log("API Response:", response); + console.log("First record keys:", response.data?.records?.[0] ? Object.keys(response.data.records[0]) : "No records"); if (response.status == 200) { - setFilteredItems(response.data.records); + // Normalize field names and add status to each item + const itemsWithStatus: ItemsResultWithStatus[] = response.data.records.map((item: any) => { + // Normalize field names (handle case sensitivity from MySQL) + // Check all possible case variations + const locationCode = item.LocationCode || item.locationCode || item.locationcode || item.Locationcode || item.Location_Code || item.location_code; + const qcCategoryId = item.qcCategoryId || item.qcCategoryid || item.qccategoryid || item.QcCategoryId || item.qc_category_id; + + const normalizedItem: ItemsResult = { + ...item, + LocationCode: locationCode, + qcCategory: item.qcCategory || (qcCategoryId ? { id: qcCategoryId } : undefined), + }; + + console.log("Normalized item:", { + id: normalizedItem.id, + LocationCode: normalizedItem.LocationCode, + qcCategoryId: qcCategoryId, + qcCategory: normalizedItem.qcCategory + }); + + return { + ...normalizedItem, + status: checkItemStatus(normalizedItem), + }; + }); + setFilteredItems(itemsWithStatus as ItemsResult[]); setTotalCount(response.data.total); return response; // Return the data from the response } else { @@ -102,7 +173,7 @@ const ItemsSearch: React.FC = ({ items }) => { throw error; // Rethrow the error for further handling } }, - [pagingController.pageNum, pagingController.pageSize], + [pagingController.pageNum, pagingController.pageSize, checkItemStatus], ); useEffect(() => { @@ -137,8 +208,8 @@ const ItemsSearch: React.FC = ({ items }) => { }} onReset={onReset} /> - - items={filteredItems} + + items={filteredItems as ItemsResultWithStatus[]} columns={columns} setPagingController={setPagingController} pagingController={pagingController} diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 6345995..bbe1caa 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -266,7 +266,7 @@ const NavigationContent: React.FC = () => { }, { icon: , - label: "Shop", + label: "ShopAndTruck", path: "/settings/shop", }, { diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 7a92a1d..f4722ae 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -18,6 +18,7 @@ import Search from "@mui/icons-material/Search"; import dayjs, { Dayjs } from "dayjs"; import "dayjs/locale/zh-hk"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { TimePicker } from "@mui/x-date-pickers/TimePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { @@ -96,6 +97,10 @@ interface DateCriterion extends BaseCriterion { type: "date"; } +interface TimeCriterion extends BaseCriterion { + type: "time"; +} + export type Criterion = | TextCriterion | SelectCriterion @@ -103,6 +108,7 @@ export type Criterion = | DateRangeCriterion | DatetimeRangeCriterion | DateCriterion + | TimeCriterion | MultiSelectCriterion | AutocompleteCriterion; @@ -249,6 +255,15 @@ function SearchBox({ }; }, []); + const makeTimeChangeHandler = useCallback((paramName: T) => { + return (value: Dayjs | null) => { + setInputs((i) => ({ + ...i, + [paramName]: value ? value.format("HH:mm") : "" + })); + }; + }, []); + const handleReset = () => { setInputs(defaultInputs); onReset?.(); @@ -524,6 +539,25 @@ function SearchBox({ )} + {c.type === "time" && ( + + + + + + )} ); })} diff --git a/src/components/Shop/TruckLane.tsx b/src/components/Shop/TruckLane.tsx index ebb5ecf..dd29e6a 100644 --- a/src/components/Shop/TruckLane.tsx +++ b/src/components/Shop/TruckLane.tsx @@ -74,7 +74,15 @@ const TruckLane: React.FC = () => { setError(null); try { const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; - setTruckData(data || []); + // Get unique truckLanceCodes only + const uniqueCodes = new Map(); + (data || []).forEach((truck) => { + const code = String(truck.truckLanceCode || "").trim(); + if (code && !uniqueCodes.has(code)) { + uniqueCodes.set(code, truck); + } + }); + setTruckData(Array.from(uniqueCodes.values())); } catch (err: any) { console.error("Failed to load truck lanes:", err); setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); @@ -140,9 +148,13 @@ const TruckLane: React.FC = () => { }; const handleViewDetail = (truck: Truck) => { - // Navigate to truck lane detail page - if (truck.id) { - router.push(`/settings/shop/truckdetail?id=${truck.id}`); + // Navigate to truck lane detail page using truckLanceCode + const truckLanceCode = String(truck.truckLanceCode || "").trim(); + if (truckLanceCode) { + // Use router.push with proper URL encoding + const url = new URL(`/settings/shop/truckdetail`, window.location.origin); + url.searchParams.set("truckLanceCode", truckLanceCode); + router.push(url.pathname + url.search); } }; @@ -166,7 +178,7 @@ const TruckLane: React.FC = () => { const criteria: Criterion[] = [ { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" }, - { type: "text", label: t("Departure Time"), paramName: "departureTime" }, + { type: "time", label: t("Departure Time"), paramName: "departureTime" }, { type: "text", label: t("Store ID"), paramName: "storeId" }, ]; @@ -195,14 +207,13 @@ const TruckLane: React.FC = () => { {t("TruckLance Code")} {t("Departure Time")} {t("Store ID")} - {t("Remark")} {t("Actions")} {paginatedRows.length === 0 ? ( - + {t("No Truck Lane data available")} @@ -231,9 +242,6 @@ const TruckLane: React.FC = () => { {displayStoreId} - - {String(truck.remark || "-")} -