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
+ }
+ onClick={handleOpenAddDialog}
+ disabled={editingRowIndex !== null || saving}
+ >
+ Add Truck Lane
+
+
+
+
+
+
+ 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 */}
+
+
+ {/* 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";