From c146944a1931e72736ba5c0a4692d22ec8ccbedb Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Tue, 30 Dec 2025 19:26:50 +0800 Subject: [PATCH] Update for the shop and truck function --- src/app/(main)/settings/shop/detail/page.tsx | 15 + src/app/(main)/settings/shop/page.tsx | 19 + src/app/api/shop/actions.ts | 136 ++++ src/app/api/shop/client.ts | 35 + src/app/api/shop/index.ts | 5 +- src/components/Shop/Shop.tsx | 257 +++++++ src/components/Shop/ShopDetail.tsx | 761 +++++++++++++++++++ src/components/Shop/ShopWrapper.tsx | 15 + src/components/Shop/index.ts | 1 + 9 files changed, 1243 insertions(+), 1 deletion(-) create mode 100644 src/app/(main)/settings/shop/detail/page.tsx create mode 100644 src/app/(main)/settings/shop/page.tsx create mode 100644 src/app/api/shop/actions.ts create mode 100644 src/app/api/shop/client.ts create mode 100644 src/components/Shop/Shop.tsx create mode 100644 src/components/Shop/ShopDetail.tsx create mode 100644 src/components/Shop/ShopWrapper.tsx create mode 100644 src/components/Shop/index.ts diff --git a/src/app/(main)/settings/shop/detail/page.tsx b/src/app/(main)/settings/shop/detail/page.tsx new file mode 100644 index 0000000..bf37a8e --- /dev/null +++ b/src/app/(main)/settings/shop/detail/page.tsx @@ -0,0 +1,15 @@ +import { Suspense } from "react"; +import ShopDetail from "@/components/Shop/ShopDetail"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import GeneralLoading from "@/components/General/GeneralLoading"; + +export default async function ShopDetailPage() { + const { t } = await getServerI18n("shop"); + return ( + + }> + + + + ); +} diff --git a/src/app/(main)/settings/shop/page.tsx b/src/app/(main)/settings/shop/page.tsx new file mode 100644 index 0000000..c3996b9 --- /dev/null +++ b/src/app/(main)/settings/shop/page.tsx @@ -0,0 +1,19 @@ +import { Suspense } from "react"; +import ShopWrapper from "@/components/Shop"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { Typography } from "@mui/material"; +import { isArray } from "lodash"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; + + +export default async function ShopPage() { + const { t } = await getServerI18n("shop"); + return ( + + }> + + + + ); +} \ No newline at end of file diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts new file mode 100644 index 0000000..8479ddd --- /dev/null +++ b/src/app/api/shop/actions.ts @@ -0,0 +1,136 @@ +"use server"; + +// import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +// import { BASE_API_URL } from "@/config/api"; +import { + serverFetchJson, + serverFetchWithNoContent, +} from "../../utils/fetchUtil"; +import { BASE_API_URL } from "../../../config/api"; +import { revalidateTag } from "next/cache"; +import { cache } from "react"; + + +export interface ShopAndTruck{ + id: number; + name: String; + code: String; + addr1: String; + addr2: String; + addr3: String; + contactNo: number; + type: String; + contactEmail: String; + contactName: String; + truckLanceCode: String; + DepartureTime: String; + LoadingSequence: number; + districtReference: Number; + Store_id: Number +} + +export interface Shop{ + id: number; + name: String; + code: String; + addr3: String; +} + +export interface Truck{ + id?: number; + truckLanceCode: String; + departureTime: String | number[]; + loadingSequence: number; + districtReference: Number; + storeId: Number; +} + +export interface SaveTruckLane { + id: number; + truckLanceCode: string; + departureTime: string; + loadingSequence: number; + districtReference: number; +} + +export interface DeleteTruckLane { + id: number; +} + +export interface SaveTruckRequest { + id?: number | null; + store_id: number; + truckLanceCode: string; + departureTime: string; + shopId: number; + shopName: string; + shopCode: string; + loadingSequence: number; + districtReference?: number | null; +} + +export interface MessageResponse { + id: number | null; + name: string | null; + code: string | null; + type: string; + message: string; + errorPosition: string | null; + entity: Truck | null; +} + +export const fetchAllShopsAction = cache(async (params?: Record) => { + const endpoint = `${BASE_API_URL}/shop/combo/allShop`; + const qs = params + ? Object.entries(params) + .filter(([, v]) => v !== null && v !== undefined && String(v).trim() !== "") + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) + .join("&") + : ""; + + const url = qs ? `${endpoint}?${qs}` : endpoint; + + return serverFetchJson(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const findTruckLaneByShopIdAction = cache(async (shopId: number | string) => { + const endpoint = `${BASE_API_URL}/truck/findTruckLane/${shopId}`; + + return serverFetchJson(endpoint, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const updateTruckLaneAction = async (data: SaveTruckLane) => { + const endpoint = `${BASE_API_URL}/truck/updateTruckLane`; + + return serverFetchJson(endpoint, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +}; + +export const deleteTruckLaneAction = async (data: DeleteTruckLane) => { + const endpoint = `${BASE_API_URL}/truck/deleteTruckLane`; + + return serverFetchJson(endpoint, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +}; + +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" }, + }); +}; \ No newline at end of file diff --git a/src/app/api/shop/client.ts b/src/app/api/shop/client.ts new file mode 100644 index 0000000..bbd3fe5 --- /dev/null +++ b/src/app/api/shop/client.ts @@ -0,0 +1,35 @@ +"use client"; + +import { + fetchAllShopsAction, + findTruckLaneByShopIdAction, + updateTruckLaneAction, + deleteTruckLaneAction, + createTruckAction, + type SaveTruckLane, + type DeleteTruckLane, + type SaveTruckRequest, + type MessageResponse +} from "./actions"; + +export const fetchAllShopsClient = async (params?: Record) => { + return await fetchAllShopsAction(params); +}; + +export const findTruckLaneByShopIdClient = async (shopId: number | string) => { + return await findTruckLaneByShopIdAction(shopId); +}; + +export const updateTruckLaneClient = async (data: SaveTruckLane): Promise => { + return await updateTruckLaneAction(data); +}; + +export const deleteTruckLaneClient = async (data: DeleteTruckLane): Promise => { + return await deleteTruckLaneAction(data); +}; + +export const createTruckClient = async (data: SaveTruckRequest): Promise => { + return await createTruckAction(data); +}; + +export default fetchAllShopsClient; diff --git a/src/app/api/shop/index.ts b/src/app/api/shop/index.ts index d74cc1b..bdde782 100644 --- a/src/app/api/shop/index.ts +++ b/src/app/api/shop/index.ts @@ -9,6 +9,8 @@ export interface ShopCombo { label: string; } + + export const fetchSupplierCombo = cache(async() => { return serverFetchJson(`${BASE_API_URL}/shop/combo/supplier`, { method: "GET", @@ -17,4 +19,5 @@ export const fetchSupplierCombo = cache(async() => { tags: ["supplierCombo"] } }) -}) \ No newline at end of file +}) + diff --git a/src/components/Shop/Shop.tsx b/src/components/Shop/Shop.tsx new file mode 100644 index 0000000..0eb2605 --- /dev/null +++ b/src/components/Shop/Shop.tsx @@ -0,0 +1,257 @@ +"use client"; + +import { + Box, + Button, + Card, + CardContent, + Stack, + Typography, + Alert, + CircularProgress, + Chip, +} from "@mui/material"; +import { useState, useMemo, useCallback, useEffect } from "react"; +import { useRouter } from "next/navigation"; +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"; + +type ShopRow = Shop & { + actions?: string; + truckLanceStatus?: "complete" | "missing" | "no-truck"; +}; + +type SearchQuery = { + id: string; + name: string; + code: string; +}; + +type SearchParamNames = keyof SearchQuery; + +const Shop: React.FC = () => { + const router = useRouter(); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [filters, setFilters] = useState>({}); + const [pagingController, setPagingController] = useState(defaultPagingController); + + // client-side filtered rows (contains-matching) + const filteredRows = useMemo(() => { + const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== ""); + const normalized = (rows || []).filter((r) => { + // apply contains matching for each active filter + for (const k of fKeys) { + const v = String((filters as any)[k] ?? "").trim(); + const rv = String((r as any)[k] ?? "").trim(); + // Use exact matching for id field, contains matching for others + if (k === "id") { + const numValue = Number(v); + const rvNum = Number(rv); + if (!isNaN(numValue) && !isNaN(rvNum)) { + if (numValue !== rvNum) return false; + } else { + if (v !== rv) return false; + } + } else { + if (!rv.toLowerCase().includes(v.toLowerCase())) return false; + } + } + return true; + }); + return normalized; + }, [rows, filters]); + + // Check if a shop has missing truckLanceCode data + const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => { + if (!shopTrucks || shopTrucks.length === 0) { + return "no-truck"; + } + + // Check 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; + + // If any required field is missing, return "missing" + if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !hasStoreId) { + return "missing"; + } + } + + return "complete"; + }, []); + + const fetchAllShops = async (params?: Record) => { + setLoading(true); + setError(null); + try { + const data = await fetchAllShopsClient(params) as ShopAndTruck[]; + console.log("Fetched shops data:", data); + + // Group data by shop ID (one shop can have multiple TruckLanceCode entries) + const shopMap = new Map(); + + (data || []).forEach((item: ShopAndTruck) => { + const shopId = item.id; + if (!shopMap.has(shopId)) { + shopMap.set(shopId, { + shop: { + id: item.id, + name: item.name, + code: item.code, + addr3: item.addr3 ?? "", + }, + trucks: [], + }); + } + shopMap.get(shopId)!.trucks.push(item); + }); + + // Convert to ShopRow array with truckLanceStatus + const mapped: ShopRow[] = Array.from(shopMap.values()).map(({ shop, trucks }) => ({ + ...shop, + truckLanceStatus: checkTruckLanceStatus(trucks), + })); + + setRows(mapped); + } catch (err: any) { + console.error("Failed to load shops:", err); + setError(err?.message ?? String(err)); + } finally { + setLoading(false); + } + }; + + // SearchBox onSearch will call this + const handleSearch = (inputs: Record) => { + setFilters(inputs); + const params: Record = {}; + Object.entries(inputs || {}).forEach(([k, v]) => { + if (v != null && String(v).trim() !== "") params[k] = String(v).trim(); + }); + if (Object.keys(params).length === 0) fetchAllShops(); + else fetchAllShops(params); + }; + + const handleViewDetail = useCallback( + (shop: ShopRow) => { + router.push(`/settings/shop/detail?id=${shop.id}`); + }, + [router] + ); + + const criteria: Criterion[] = [ + { type: "text", label: "id", paramName: "id" }, + { type: "text", label: "code", paramName: "code" }, + { type: "text", label: "name", paramName: "name" }, + ]; + + const columns: Column[] = [ + { + name: "id", + label: "Id", + type: "integer", + renderCell: (item) => String(item.id ?? ""), + }, + { + name: "code", + label: "Code", + renderCell: (item) => String(item.code ?? ""), + }, + { + name: "name", + label: "Name", + renderCell: (item) => String(item.name ?? ""), + }, + { + name: "addr3", + label: "Addr3", + renderCell: (item) => String((item as any).addr3 ?? ""), + }, + { + name: "truckLanceStatus", + label: "TruckLance Status", + renderCell: (item) => { + const status = item.truckLanceStatus; + if (status === "complete") { + return ; + } else if (status === "missing") { + return ; + } else { + return ; + } + }, + }, + { + name: "actions", + label: "Actions", + headerAlign: "right", + renderCell: (item) => ( + + ), + }, + ]; + + useEffect(() => { + fetchAllShops(); + }, []); + + return ( + + + + []} + onSearch={handleSearch} + onReset={() => { + setRows([]); + setFilters({}); + }} + /> + + + + + + + Shop + + {error && ( + + {error} + + )} + + {loading ? ( + + + + ) : ( + + )} + + + + ); +}; + +export default Shop; \ No newline at end of file diff --git a/src/components/Shop/ShopDetail.tsx b/src/components/Shop/ShopDetail.tsx new file mode 100644 index 0000000..2518afb --- /dev/null +++ b/src/components/Shop/ShopDetail.tsx @@ -0,0 +1,761 @@ +"use client"; + +import { + Box, + Card, + CardContent, + Typography, + CircularProgress, + Alert, + Button, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TextField, + Stack, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Grid, + Snackbar, +} 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 AddIcon from "@mui/icons-material/Add"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { useSession } from "next-auth/react"; +import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions"; +import { + fetchAllShopsClient, + findTruckLaneByShopIdClient, + updateTruckLaneClient, + deleteTruckLaneClient, + createTruckClient +} from "@/app/api/shop/client"; +import type { SessionWithTokens } from "@/config/authConfig"; + +type ShopDetailData = { + id: number; + name: String; + code: String; + addr1: String; + addr2: String; + addr3: String; + contactNo: number; + type: String; + contactEmail: String; + contactName: String; +}; + +// 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")}`; + } + + // Handle decimal format (e.g., "17,0" or "17.0" representing hours) + const decimalMatch = timeStr.match(/^(\d+)[,.](\d+)$/); + if (decimalMatch) { + const hours = parseInt(decimalMatch[1], 10); + const minutes = Math.round(parseFloat(`0.${decimalMatch[2]}`) * 60); + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; + } + + // Handle single number as hours (e.g., "17" -> "17:00") + const hoursOnly = parseInt(timeStr, 10); + if (!isNaN(hoursOnly) && hoursOnly >= 0 && hoursOnly <= 23) { + return `${hoursOnly.toString().padStart(2, "0")}:00`; + } + + // Try to parse as ISO time string or other formats + try { + // If it's already a valid time string, try to extract hours and minutes + const parts = timeStr.split(/[:,\s]/); + if (parts.length >= 2) { + const h = parseInt(parts[0], 10); + const m = parseInt(parts[1], 10); + if (!isNaN(h) && !isNaN(m) && h >= 0 && h <= 23 && m >= 0 && m <= 59) { + return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}`; + } + } + } catch (e) { + // If parsing fails, return original string + } + + return timeStr; +}; + +// Utility function to convert HH:mm format to the format expected by backend +const parseDepartureTimeForBackend = (time: string): string => { + if (!time) return ""; + + const timeStr = String(time).trim(); + // If already in HH:mm format, return as is + if (/^\d{1,2}:\d{2}$/.test(timeStr)) { + return timeStr; + } + + // Try to format it + return formatDepartureTime(timeStr); +}; + +const ShopDetail: React.FC = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const shopId = searchParams.get("id"); + const { data: session, status: sessionStatus } = useSession() as { data: SessionWithTokens | null; status: string }; + const [shopDetail, setShopDetail] = useState(null); + const [truckData, setTruckData] = useState([]); + const [editedTruckData, setEditedTruckData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [editingRowIndex, setEditingRowIndex] = useState(null); + const [saving, setSaving] = useState(false); + const [addDialogOpen, setAddDialogOpen] = useState(false); + const [newTruck, setNewTruck] = useState({ + truckLanceCode: "", + departureTime: "", + loadingSequence: 0, + districtReference: 0, + storeId: 2, + }); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + + useEffect(() => { + // Wait for session to be ready before making API calls + if (sessionStatus === "loading") { + return; // Still loading session + } + + // 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"); + setLoading(false); + return; + } + + const fetchShopDetail = async () => { + if (!shopId) { + setError("Shop ID is required"); + setLoading(false); + return; + } + + // Convert shopId to number for proper filtering + const shopIdNum = parseInt(shopId, 10); + if (isNaN(shopIdNum)) { + setError("Invalid Shop ID"); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + try { + // Fetch shop information - try with ID parameter first, then filter if needed + let shopDataResponse = await fetchAllShopsClient({ id: shopIdNum }) as ShopAndTruck[]; + + // If no results with ID parameter, fetch all and filter client-side + if (!shopDataResponse || shopDataResponse.length === 0) { + shopDataResponse = await fetchAllShopsClient() as ShopAndTruck[]; + } + + // Filter to find the shop with matching ID (in case API doesn't filter properly) + const shopData = shopDataResponse?.find((item) => item.id === shopIdNum); + + if (shopData) { + // Set shop detail info + setShopDetail({ + id: shopData.id ?? 0, + name: shopData.name ?? "", + code: shopData.code ?? "", + addr1: shopData.addr1 ?? "", + addr2: shopData.addr2 ?? "", + addr3: shopData.addr3 ?? "", + contactNo: shopData.contactNo ?? 0, + type: shopData.type ?? "", + contactEmail: shopData.contactEmail ?? "", + contactName: shopData.contactName ?? "", + }); + } else { + setError("Shop not found"); + setLoading(false); + return; + } + + // Fetch truck information using the Truck interface with numeric ID + const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; + setTruckData(trucks || []); + setEditedTruckData(trucks || []); + setEditingRowIndex(null); + } 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"; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + fetchShopDetail(); + }, [shopId, sessionStatus, session]); + + const handleEdit = (index: number) => { + setEditingRowIndex(index); + const updated = [...truckData]; + updated[index] = { ...updated[index] }; + setEditedTruckData(updated); + setError(null); + }; + + const handleCancel = (index: number) => { + setEditingRowIndex(null); + setEditedTruckData([...truckData]); + setError(null); + }; + + const handleSave = async (index: number) => { + if (!shopId) { + setError("Shop ID is required"); + return; + } + + const truck = editedTruckData[index]; + if (!truck || !truck.id) { + setError("Invalid truck data"); + return; + } + + setSaving(true); + setError(null); + try { + // Convert departureTime to proper format if needed + const departureTime = parseDepartureTimeForBackend(String(truck.departureTime || "")); + + await updateTruckLaneClient({ + id: truck.id, + truckLanceCode: String(truck.truckLanceCode || ""), + departureTime: departureTime, + loadingSequence: Number(truck.loadingSequence) || 0, + districtReference: Number(truck.districtReference) || 0, + }); + + // Refresh truck data after update + const shopIdNum = parseInt(shopId, 10); + const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; + setTruckData(trucks || []); + setEditedTruckData(trucks || []); + setEditingRowIndex(null); + } catch (err: any) { + console.error("Failed to save truck data:", err); + setError(err?.message ?? String(err) ?? "Failed to save truck data"); + } finally { + setSaving(false); + } + }; + + const handleTruckFieldChange = (index: number, field: keyof Truck, value: string | number) => { + const updated = [...editedTruckData]; + updated[index] = { + ...updated[index], + [field]: value, + }; + setEditedTruckData(updated); + }; + + const handleDelete = async (truckId: number) => { + if (!window.confirm("Are you sure you want to delete this truck lane?")) { + return; + } + + if (!shopId) { + setError("Shop ID is required"); + return; + } + + setSaving(true); + setError(null); + try { + await deleteTruckLaneClient({ id: truckId }); + + // Refresh truck data after delete + const shopIdNum = parseInt(shopId, 10); + const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; + setTruckData(trucks || []); + setEditedTruckData(trucks || []); + setEditingRowIndex(null); + } catch (err: any) { + console.error("Failed to delete truck lane:", err); + setError(err?.message ?? String(err) ?? "Failed to delete truck lane"); + } finally { + setSaving(false); + } + }; + + const handleOpenAddDialog = () => { + setNewTruck({ + truckLanceCode: "", + departureTime: "", + loadingSequence: 0, + districtReference: 0, + storeId: 2, + }); + setAddDialogOpen(true); + setError(null); + }; + + const handleCloseAddDialog = () => { + setAddDialogOpen(false); + setNewTruck({ + truckLanceCode: "", + departureTime: "", + loadingSequence: 0, + districtReference: 0, + storeId: 2, + }); + }; + + const handleCreateTruck = async () => { + // Validate all required fields + const missingFields: string[] = []; + + if (!shopId || !shopDetail) { + missingFields.push("Shop information"); + } + + if (!newTruck.truckLanceCode.trim()) { + missingFields.push("TruckLance Code"); + } + + if (!newTruck.departureTime) { + missingFields.push("Departure Time"); + } + + if (missingFields.length > 0) { + const message = `Please fill in the following required fields: ${missingFields.join(", ")}`; + setSnackbarMessage(message); + setSnackbarOpen(true); + return; + } + + setSaving(true); + setError(null); + try { + const departureTime = parseDepartureTimeForBackend(newTruck.departureTime); + + await createTruckClient({ + store_id: newTruck.storeId, + truckLanceCode: newTruck.truckLanceCode.trim(), + departureTime: departureTime, + shopId: shopDetail!.id, + shopName: String(shopDetail!.name), + shopCode: String(shopDetail!.code), + loadingSequence: newTruck.loadingSequence, + districtReference: newTruck.districtReference, + }); + + // Refresh truck data after create + const shopIdNum = parseInt(shopId || "0", 10); + const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; + setTruckData(trucks || []); + setEditedTruckData(trucks || []); + handleCloseAddDialog(); + } catch (err: any) { + console.error("Failed to create truck:", err); + setError(err?.message ?? String(err) ?? "Failed to create truck"); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( + + + + ); + } + if (error) { + return ( + + + {error} + + + + ); + } + + if (!shopDetail) { + return ( + + + Shop not found + + + + ); + } + + return ( + + + + + Shop Information + + + + + + Shop ID + {shopDetail.id} + + + Name + {shopDetail.name} + + + Code + {shopDetail.code} + + + Addr1 + {shopDetail.addr1 || "-"} + + + Addr2 + {shopDetail.addr2 || "-"} + + + Addr3 + {shopDetail.addr3 || "-"} + + + Contact No + {shopDetail.contactNo || "-"} + + + Type + {shopDetail.type || "-"} + + + Contact Email + {shopDetail.contactEmail || "-"} + + + Contact Name + {shopDetail.contactName || "-"} + + + + + + + + + Truck Information + + + + + + + TruckLance Code + Departure Time + Loading Sequence + District Reference + Store ID + Actions + + + + {truckData.length === 0 ? ( + + + + No Truck data available + + + + ) : ( + truckData.map((truck, index) => { + const isEditing = editingRowIndex === index; + const displayTruck = isEditing ? editedTruckData[index] : truck; + + return ( + + + {isEditing ? ( + handleTruckFieldChange(index, "truckLanceCode", e.target.value)} + fullWidth + /> + ) : ( + String(truck.truckLanceCode || "-") + )} + + + {isEditing ? ( + { + const timeValue = displayTruck?.departureTime; + const formatted = formatDepartureTime( + Array.isArray(timeValue) ? timeValue : (timeValue ? String(timeValue) : null) + ); + return formatted !== "-" ? formatted : ""; + })()} + onChange={(e) => handleTruckFieldChange(index, "departureTime", e.target.value)} + fullWidth + InputLabelProps={{ + shrink: true, + }} + inputProps={{ + step: 300, // 5 minutes + }} + /> + ) : ( + formatDepartureTime( + Array.isArray(truck.departureTime) ? truck.departureTime : (truck.departureTime ? String(truck.departureTime) : null) + ) + )} + + + {isEditing ? ( + handleTruckFieldChange(index, "loadingSequence", parseInt(e.target.value) || 0)} + fullWidth + /> + ) : ( + truck.loadingSequence !== null && truck.loadingSequence !== undefined ? String(truck.loadingSequence) : "-" + )} + + + {isEditing ? ( + handleTruckFieldChange(index, "districtReference", parseInt(e.target.value) || 0)} + fullWidth + /> + ) : ( + truck.districtReference !== null && truck.districtReference !== undefined ? String(truck.districtReference) : "-" + )} + + + {isEditing ? ( + handleTruckFieldChange(index, "storeId", parseInt(e.target.value) || 0)} + fullWidth + /> + ) : ( + truck.storeId !== null && truck.storeId !== undefined ? String(truck.storeId) : "-" + )} + + + + {isEditing ? ( + <> + handleSave(index)} + disabled={saving} + title="Save changes" + > + + + handleCancel(index)} + disabled={saving} + title="Cancel editing" + > + + + + ) : ( + <> + handleEdit(index)} + disabled={editingRowIndex !== null} + title="Edit truck lane" + > + + + {truck.id && ( + handleDelete(truck.id!)} + disabled={saving || editingRowIndex !== null} + title="Delete truck lane" + > + + + )} + + )} + + + + ); + }) + )} + +
+
+
+
+ + {/* Add Truck Dialog */} + + Add New Truck Lane + + + + + setNewTruck({ ...newTruck, truckLanceCode: e.target.value })} + disabled={saving} + /> + + + setNewTruck({ ...newTruck, departureTime: e.target.value })} + disabled={saving} + InputLabelProps={{ + shrink: true, + }} + inputProps={{ + step: 300, // 5 minutes + }} + /> + + + setNewTruck({ ...newTruck, loadingSequence: parseInt(e.target.value) || 0 })} + disabled={saving} + /> + + + setNewTruck({ ...newTruck, districtReference: parseInt(e.target.value) || 0 })} + disabled={saving} + /> + + + setNewTruck({ ...newTruck, storeId: parseInt(e.target.value) || 2 })} + disabled={saving} + /> + + + + + + + + + + + {/* Snackbar for notifications */} + setSnackbarOpen(false)} + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} + > + setSnackbarOpen(false)} + severity="warning" + sx={{ width: '100%' }} + > + {snackbarMessage} + + +
+ ); +}; + +export default ShopDetail; + diff --git a/src/components/Shop/ShopWrapper.tsx b/src/components/Shop/ShopWrapper.tsx new file mode 100644 index 0000000..7ebdcb9 --- /dev/null +++ b/src/components/Shop/ShopWrapper.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import GeneralLoading from "../General/GeneralLoading"; +import Shop from "./Shop"; + +interface SubComponents { + Loading: typeof GeneralLoading; +} + +const ShopWrapper: React.FC & SubComponents = async () => { + return ; +}; + +ShopWrapper.Loading = GeneralLoading; + +export default ShopWrapper; \ No newline at end of file diff --git a/src/components/Shop/index.ts b/src/components/Shop/index.ts new file mode 100644 index 0000000..878c2fd --- /dev/null +++ b/src/components/Shop/index.ts @@ -0,0 +1 @@ +export { default } from "./ShopWrapper";