| @@ -4,9 +4,9 @@ import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import GeneralLoading from "@/components/General/GeneralLoading"; | import GeneralLoading from "@/components/General/GeneralLoading"; | ||||
| export default async function ShopDetailPage() { | export default async function ShopDetailPage() { | ||||
| const { t } = await getServerI18n("shop"); | |||||
| const { t } = await getServerI18n("shop", "common"); | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["shop"]}> | |||||
| <I18nProvider namespaces={["shop", "common"]}> | |||||
| <Suspense fallback={<GeneralLoading />}> | <Suspense fallback={<GeneralLoading />}> | ||||
| <ShopDetail /> | <ShopDetail /> | ||||
| </Suspense> | </Suspense> | ||||
| @@ -8,9 +8,9 @@ import { notFound } from "next/navigation"; | |||||
| export default async function ShopPage() { | export default async function ShopPage() { | ||||
| const { t } = await getServerI18n("shop"); | |||||
| const { t } = await getServerI18n("shop", "common"); | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["shop"]}> | |||||
| <I18nProvider namespaces={["shop", "common"]}> | |||||
| <Suspense fallback={<ShopWrapper.Loading />}> | <Suspense fallback={<ShopWrapper.Loading />}> | ||||
| <ShopWrapper /> | <ShopWrapper /> | ||||
| </Suspense> | </Suspense> | ||||
| @@ -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 ( | |||||
| <I18nProvider namespaces={["shop", "common"]}> | |||||
| <Suspense fallback={<GeneralLoading />}> | |||||
| <TruckLaneDetail /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| ); | |||||
| } | |||||
| @@ -24,9 +24,11 @@ export interface ShopAndTruck{ | |||||
| contactName: String; | contactName: String; | ||||
| truckLanceCode: String; | truckLanceCode: String; | ||||
| DepartureTime: String; | DepartureTime: String; | ||||
| LoadingSequence: number; | |||||
| LoadingSequence?: number | null; | |||||
| districtReference: Number; | districtReference: Number; | ||||
| Store_id: Number | |||||
| Store_id: Number; | |||||
| remark?: String | null; | |||||
| truckId?: number; | |||||
| } | } | ||||
| export interface Shop{ | export interface Shop{ | ||||
| @@ -60,6 +62,11 @@ export interface DeleteTruckLane { | |||||
| id: number; | id: number; | ||||
| } | } | ||||
| export interface UpdateLoadingSequenceRequest { | |||||
| id: number; | |||||
| loadingSequence: number; | |||||
| } | |||||
| export interface SaveTruckRequest { | export interface SaveTruckRequest { | ||||
| id?: number | null; | id?: number | null; | ||||
| store_id: string; | store_id: string; | ||||
| @@ -132,6 +139,35 @@ export const deleteTruckLaneAction = async (data: DeleteTruckLane) => { | |||||
| export const createTruckAction = async (data: SaveTruckRequest) => { | export const createTruckAction = async (data: SaveTruckRequest) => { | ||||
| const endpoint = `${BASE_API_URL}/truck/create`; | const endpoint = `${BASE_API_URL}/truck/create`; | ||||
| return serverFetchJson<MessageResponse>(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<Truck[]>(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<ShopAndTruck[]>(url, { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| }); | |||||
| export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => { | |||||
| const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`; | |||||
| return serverFetchJson<MessageResponse>(endpoint, { | return serverFetchJson<MessageResponse>(endpoint, { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| @@ -6,9 +6,13 @@ import { | |||||
| updateTruckLaneAction, | updateTruckLaneAction, | ||||
| deleteTruckLaneAction, | deleteTruckLaneAction, | ||||
| createTruckAction, | createTruckAction, | ||||
| findAllUniqueTruckLaneCombinationsAction, | |||||
| findAllShopsByTruckLanceCodeAndRemarkAction, | |||||
| updateLoadingSequenceAction, | |||||
| type SaveTruckLane, | type SaveTruckLane, | ||||
| type DeleteTruckLane, | type DeleteTruckLane, | ||||
| type SaveTruckRequest, | type SaveTruckRequest, | ||||
| type UpdateLoadingSequenceRequest, | |||||
| type MessageResponse | type MessageResponse | ||||
| } from "./actions"; | } from "./actions"; | ||||
| @@ -32,4 +36,16 @@ export const createTruckClient = async (data: SaveTruckRequest): Promise<Message | |||||
| return await createTruckAction(data); | return await createTruckAction(data); | ||||
| }; | }; | ||||
| export const findAllUniqueTruckLaneCombinationsClient = async () => { | |||||
| return await findAllUniqueTruckLaneCombinationsAction(); | |||||
| }; | |||||
| export const findAllShopsByTruckLanceCodeAndRemarkClient = async (truckLanceCode: string, remark: string) => { | |||||
| return await findAllShopsByTruckLanceCodeAndRemarkAction(truckLanceCode, remark); | |||||
| }; | |||||
| export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise<MessageResponse> => { | |||||
| return await updateLoadingSequenceAction(data); | |||||
| }; | |||||
| export default fetchAllShopsClient; | export default fetchAllShopsClient; | ||||
| @@ -16,7 +16,10 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
| "/settings/qcItem": "Qc Item", | "/settings/qcItem": "Qc Item", | ||||
| "/settings/qrCodeHandle": "QR Code Handle", | "/settings/qrCodeHandle": "QR Code Handle", | ||||
| "/settings/rss": "Demand Forecast Setting", | "/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": "Demand Forecast", | ||||
| "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", | "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", | ||||
| "/scheduling/detailed": "Detail Scheduling", | "/scheduling/detailed": "Detail Scheduling", | ||||
| @@ -10,14 +10,22 @@ import { | |||||
| Alert, | Alert, | ||||
| CircularProgress, | CircularProgress, | ||||
| Chip, | Chip, | ||||
| Tabs, | |||||
| Tab, | |||||
| Select, | |||||
| MenuItem, | |||||
| FormControl, | |||||
| InputLabel, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useState, useMemo, useCallback, useEffect } from "react"; | import { useState, useMemo, useCallback, useEffect } from "react"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| import { defaultPagingController } from "../SearchResults/SearchResults"; | import { defaultPagingController } from "../SearchResults/SearchResults"; | ||||
| import { fetchAllShopsClient } from "@/app/api/shop/client"; | import { fetchAllShopsClient } from "@/app/api/shop/client"; | ||||
| import type { Shop, ShopAndTruck } from "@/app/api/shop/actions"; | import type { Shop, ShopAndTruck } from "@/app/api/shop/actions"; | ||||
| import TruckLane from "./TruckLane"; | |||||
| type ShopRow = Shop & { | type ShopRow = Shop & { | ||||
| actions?: string; | actions?: string; | ||||
| @@ -33,17 +41,20 @@ type SearchQuery = { | |||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| const Shop: React.FC = () => { | const Shop: React.FC = () => { | ||||
| const { t } = useTranslation("common"); | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [activeTab, setActiveTab] = useState<number>(0); | |||||
| const [rows, setRows] = useState<ShopRow[]>([]); | const [rows, setRows] = useState<ShopRow[]>([]); | ||||
| const [loading, setLoading] = useState<boolean>(false); | const [loading, setLoading] = useState<boolean>(false); | ||||
| const [error, setError] = useState<string | null>(null); | const [error, setError] = useState<string | null>(null); | ||||
| const [filters, setFilters] = useState<Record<string, string>>({}); | const [filters, setFilters] = useState<Record<string, string>>({}); | ||||
| const [statusFilter, setStatusFilter] = useState<string>("all"); | |||||
| const [pagingController, setPagingController] = useState(defaultPagingController); | const [pagingController, setPagingController] = useState(defaultPagingController); | ||||
| // client-side filtered rows (contains-matching) | |||||
| // client-side filtered rows (contains-matching + status filter) | |||||
| const filteredRows = useMemo(() => { | const filteredRows = useMemo(() => { | ||||
| const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== ""); | 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 | // apply contains matching for each active filter | ||||
| for (const k of fKeys) { | for (const k of fKeys) { | ||||
| const v = String((filters as any)[k] ?? "").trim(); | const v = String((filters as any)[k] ?? "").trim(); | ||||
| @@ -63,8 +74,16 @@ const Shop: React.FC = () => { | |||||
| } | } | ||||
| return true; | return true; | ||||
| }); | }); | ||||
| // Apply status filter | |||||
| if (statusFilter !== "all") { | |||||
| normalized = normalized.filter((r) => { | |||||
| return r.truckLanceStatus === statusFilter; | |||||
| }); | |||||
| } | |||||
| return normalized; | return normalized; | ||||
| }, [rows, filters]); | |||||
| }, [rows, filters, statusFilter]); | |||||
| // Check if a shop has missing truckLanceCode data | // Check if a shop has missing truckLanceCode data | ||||
| const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => { | const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => { | ||||
| @@ -72,16 +91,73 @@ const Shop: React.FC = () => { | |||||
| return "no-truck"; | 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 | // Check each truckLanceCode entry for missing data | ||||
| for (const truck of shopTrucks) { | 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"; | return "missing"; | ||||
| } | } | ||||
| } | } | ||||
| @@ -149,50 +225,50 @@ const Shop: React.FC = () => { | |||||
| ); | ); | ||||
| const criteria: Criterion<SearchParamNames>[] = [ | const criteria: Criterion<SearchParamNames>[] = [ | ||||
| { 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<ShopRow>[] = [ | const columns: Column<ShopRow>[] = [ | ||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| label: "Id", | |||||
| label: t("id"), | |||||
| type: "integer", | type: "integer", | ||||
| renderCell: (item) => String(item.id ?? ""), | renderCell: (item) => String(item.id ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "code", | name: "code", | ||||
| label: "Code", | |||||
| label: t("Code"), | |||||
| renderCell: (item) => String(item.code ?? ""), | renderCell: (item) => String(item.code ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "name", | name: "name", | ||||
| label: "Name", | |||||
| label: t("Name"), | |||||
| renderCell: (item) => String(item.name ?? ""), | renderCell: (item) => String(item.name ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "addr3", | name: "addr3", | ||||
| label: "Addr3", | |||||
| label: t("Addr3"), | |||||
| renderCell: (item) => String((item as any).addr3 ?? ""), | renderCell: (item) => String((item as any).addr3 ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "truckLanceStatus", | name: "truckLanceStatus", | ||||
| label: "TruckLance Status", | |||||
| label: t("TruckLance Status"), | |||||
| renderCell: (item) => { | renderCell: (item) => { | ||||
| const status = item.truckLanceStatus; | const status = item.truckLanceStatus; | ||||
| if (status === "complete") { | if (status === "complete") { | ||||
| return <Chip label="Complete" color="success" size="small" />; | |||||
| return <Chip label={t("Complete")} color="success" size="small" />; | |||||
| } else if (status === "missing") { | } else if (status === "missing") { | ||||
| return <Chip label="Missing Data" color="warning" size="small" />; | |||||
| return <Chip label={t("Missing Data")} color="warning" size="small" />; | |||||
| } else { | } else { | ||||
| return <Chip label="No TruckLance" color="error" size="small" />; | |||||
| return <Chip label={t("No TruckLance")} color="error" size="small" />; | |||||
| } | } | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| name: "actions", | name: "actions", | ||||
| label: "Actions", | |||||
| label: t("Actions"), | |||||
| headerAlign: "right", | headerAlign: "right", | ||||
| renderCell: (item) => ( | renderCell: (item) => ( | ||||
| <Button | <Button | ||||
| @@ -200,56 +276,96 @@ const Shop: React.FC = () => { | |||||
| variant="outlined" | variant="outlined" | ||||
| onClick={() => handleViewDetail(item)} | onClick={() => handleViewDetail(item)} | ||||
| > | > | ||||
| View Detail | |||||
| {t("View Detail")} | |||||
| </Button> | </Button> | ||||
| ), | ), | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchAllShops(); | |||||
| }, []); | |||||
| if (activeTab === 0) { | |||||
| fetchAllShops(); | |||||
| } | |||||
| }, [activeTab]); | |||||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | |||||
| setActiveTab(newValue); | |||||
| }; | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Card sx={{ mb: 2 }}> | <Card sx={{ mb: 2 }}> | ||||
| <CardContent> | <CardContent> | ||||
| <SearchBox | |||||
| criteria={criteria as Criterion<string>[]} | |||||
| onSearch={handleSearch} | |||||
| onReset={() => { | |||||
| setRows([]); | |||||
| setFilters({}); | |||||
| <Tabs | |||||
| value={activeTab} | |||||
| onChange={handleTabChange} | |||||
| sx={{ | |||||
| mb: 3, | |||||
| borderBottom: 1, | |||||
| borderColor: 'divider' | |||||
| }} | }} | ||||
| /> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}> | |||||
| <Typography variant="h6">Shop</Typography> | |||||
| </Stack> | |||||
| {error && ( | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | |||||
| {error} | |||||
| </Alert> | |||||
| )} | |||||
| > | |||||
| <Tab label={t("Shop")} /> | |||||
| <Tab label={t("Truck Lane")} /> | |||||
| </Tabs> | |||||
| {loading ? ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <SearchResults | |||||
| items={filteredRows} | |||||
| columns={columns} | |||||
| pagingController={pagingController} | |||||
| setPagingController={setPagingController} | |||||
| {activeTab === 0 && ( | |||||
| <SearchBox | |||||
| criteria={criteria as Criterion<string>[]} | |||||
| onSearch={handleSearch} | |||||
| onReset={() => { | |||||
| setRows([]); | |||||
| setFilters({}); | |||||
| }} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| {activeTab === 0 && ( | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}> | |||||
| <Typography variant="h6">{t("Shop")}</Typography> | |||||
| <FormControl size="small" sx={{ minWidth: 200 }}> | |||||
| <InputLabel>{t("Filter by Status")}</InputLabel> | |||||
| <Select | |||||
| value={statusFilter} | |||||
| label={t("Filter by Status")} | |||||
| onChange={(e) => setStatusFilter(e.target.value)} | |||||
| > | |||||
| <MenuItem value="all">{t("All")}</MenuItem> | |||||
| <MenuItem value="complete">{t("Complete")}</MenuItem> | |||||
| <MenuItem value="missing">{t("Missing Data")}</MenuItem> | |||||
| <MenuItem value="no-truck">{t("No TruckLance")}</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Stack> | |||||
| {error && ( | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | |||||
| {error} | |||||
| </Alert> | |||||
| )} | |||||
| {loading ? ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <SearchResults | |||||
| items={filteredRows} | |||||
| columns={columns} | |||||
| pagingController={pagingController} | |||||
| setPagingController={setPagingController} | |||||
| /> | |||||
| )} | |||||
| </CardContent> | |||||
| </Card> | |||||
| )} | |||||
| {activeTab === 1 && ( | |||||
| <TruckLane /> | |||||
| )} | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -38,6 +38,7 @@ import AddIcon from "@mui/icons-material/Add"; | |||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { useState, useEffect } from "react"; | import { useState, useEffect } from "react"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions"; | import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions"; | ||||
| import { | import { | ||||
| fetchAllShopsClient, | fetchAllShopsClient, | ||||
| @@ -131,6 +132,7 @@ const parseDepartureTimeForBackend = (time: string): string => { | |||||
| }; | }; | ||||
| const ShopDetail: React.FC = () => { | const ShopDetail: React.FC = () => { | ||||
| const { t } = useTranslation("common"); | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
| const shopId = searchParams.get("id"); | 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 session is unauthenticated, don't make API calls (middleware will handle redirect) | ||||
| if (sessionStatus === "unauthenticated" || !session) { | if (sessionStatus === "unauthenticated" || !session) { | ||||
| setError("Please log in to view shop details"); | |||||
| setError(t("Please log in to view shop details")); | |||||
| setLoading(false); | setLoading(false); | ||||
| return; | return; | ||||
| } | } | ||||
| const fetchShopDetail = async () => { | const fetchShopDetail = async () => { | ||||
| if (!shopId) { | if (!shopId) { | ||||
| setError("Shop ID is required"); | |||||
| setError(t("Shop ID is required")); | |||||
| setLoading(false); | setLoading(false); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -178,7 +180,7 @@ const ShopDetail: React.FC = () => { | |||||
| // Convert shopId to number for proper filtering | // Convert shopId to number for proper filtering | ||||
| const shopIdNum = parseInt(shopId, 10); | const shopIdNum = parseInt(shopId, 10); | ||||
| if (isNaN(shopIdNum)) { | if (isNaN(shopIdNum)) { | ||||
| setError("Invalid Shop ID"); | |||||
| setError(t("Invalid Shop ID")); | |||||
| setLoading(false); | setLoading(false); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -212,7 +214,7 @@ const ShopDetail: React.FC = () => { | |||||
| contactName: shopData.contactName ?? "", | contactName: shopData.contactName ?? "", | ||||
| }); | }); | ||||
| } else { | } else { | ||||
| setError("Shop not found"); | |||||
| setError(t("Shop not found")); | |||||
| setLoading(false); | setLoading(false); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -233,7 +235,7 @@ const ShopDetail: React.FC = () => { | |||||
| } catch (err: any) { | } catch (err: any) { | ||||
| console.error("Failed to load shop detail:", err); | console.error("Failed to load shop detail:", err); | ||||
| // Handle errors gracefully - don't trigger auto-logout | // 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); | setError(errorMessage); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| @@ -273,13 +275,13 @@ const ShopDetail: React.FC = () => { | |||||
| const handleSave = async (index: number) => { | const handleSave = async (index: number) => { | ||||
| if (!shopId) { | if (!shopId) { | ||||
| setError("Shop ID is required"); | |||||
| setError(t("Shop ID is required")); | |||||
| return; | return; | ||||
| } | } | ||||
| const truck = editedTruckData[index]; | const truck = editedTruckData[index]; | ||||
| if (!truck || !truck.id) { | if (!truck || !truck.id) { | ||||
| setError("Invalid truck data"); | |||||
| setError(t("Invalid shop data")); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -335,7 +337,7 @@ const ShopDetail: React.FC = () => { | |||||
| setUniqueRemarks(remarks); | setUniqueRemarks(remarks); | ||||
| } catch (err: any) { | } catch (err: any) { | ||||
| console.error("Failed to save truck data:", err); | 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 { | } finally { | ||||
| setSaving(false); | setSaving(false); | ||||
| } | } | ||||
| @@ -351,12 +353,12 @@ const ShopDetail: React.FC = () => { | |||||
| }; | }; | ||||
| const handleDelete = async (truckId: number) => { | 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; | return; | ||||
| } | } | ||||
| if (!shopId) { | if (!shopId) { | ||||
| setError("Shop ID is required"); | |||||
| setError(t("Shop ID is required")); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -373,7 +375,7 @@ const ShopDetail: React.FC = () => { | |||||
| setEditingRowIndex(null); | setEditingRowIndex(null); | ||||
| } catch (err: any) { | } catch (err: any) { | ||||
| console.error("Failed to delete truck lane:", err); | 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 { | } finally { | ||||
| setSaving(false); | setSaving(false); | ||||
| } | } | ||||
| @@ -409,19 +411,19 @@ const ShopDetail: React.FC = () => { | |||||
| const missingFields: string[] = []; | const missingFields: string[] = []; | ||||
| if (!shopId || !shopDetail) { | if (!shopId || !shopDetail) { | ||||
| missingFields.push("Shop information"); | |||||
| missingFields.push(t("Shop Information")); | |||||
| } | } | ||||
| if (!newTruck.truckLanceCode.trim()) { | if (!newTruck.truckLanceCode.trim()) { | ||||
| missingFields.push("TruckLance Code"); | |||||
| missingFields.push(t("TruckLance Code")); | |||||
| } | } | ||||
| if (!newTruck.departureTime) { | if (!newTruck.departureTime) { | ||||
| missingFields.push("Departure Time"); | |||||
| missingFields.push(t("Departure Time")); | |||||
| } | } | ||||
| if (missingFields.length > 0) { | 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); | setSnackbarMessage(message); | ||||
| setSnackbarOpen(true); | setSnackbarOpen(true); | ||||
| return; | return; | ||||
| @@ -461,7 +463,7 @@ const ShopDetail: React.FC = () => { | |||||
| handleCloseAddDialog(); | handleCloseAddDialog(); | ||||
| } catch (err: any) { | } catch (err: any) { | ||||
| console.error("Failed to create truck:", err); | 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 { | } finally { | ||||
| setSaving(false); | setSaving(false); | ||||
| } | } | ||||
| @@ -475,12 +477,12 @@ const ShopDetail: React.FC = () => { | |||||
| ); | ); | ||||
| } | } | ||||
| if (error) { | if (error) { | ||||
| return ( | |||||
| return ( | |||||
| <Box> | <Box> | ||||
| <Alert severity="error" sx={{ mb: 2 }}> | <Alert severity="error" sx={{ mb: 2 }}> | ||||
| {error} | {error} | ||||
| </Alert> | </Alert> | ||||
| <Button onClick={() => router.back()}>Go Back</Button> | |||||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -489,9 +491,9 @@ const ShopDetail: React.FC = () => { | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Alert severity="warning" sx={{ mb: 2 }}> | <Alert severity="warning" sx={{ mb: 2 }}> | ||||
| Shop not found | |||||
| {t("Shop not found")} | |||||
| </Alert> | </Alert> | ||||
| <Button onClick={() => router.back()}>Go Back</Button> | |||||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -501,49 +503,45 @@ const ShopDetail: React.FC = () => { | |||||
| <Card sx={{ mb: 2 }}> | <Card sx={{ mb: 2 }}> | ||||
| <CardContent> | <CardContent> | ||||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | ||||
| <Typography variant="h6">Shop Information</Typography> | |||||
| <Button onClick={() => router.back()}>Back</Button> | |||||
| <Typography variant="h6">{t("Shop Information")}</Typography> | |||||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||||
| </Box> | </Box> | ||||
| <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">Shop ID</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">{t("Shop ID")}</Typography> | |||||
| <Typography variant="body1" fontWeight="medium">{shopDetail.id}</Typography> | <Typography variant="body1" fontWeight="medium">{shopDetail.id}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Name</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Name")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.name}</Typography> | <Typography variant="body1">{shopDetail.name}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Code</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Code")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.code}</Typography> | <Typography variant="body1">{shopDetail.code}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Addr1</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Addr1")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.addr1 || "-"}</Typography> | <Typography variant="body1">{shopDetail.addr1 || "-"}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Addr2</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Addr2")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.addr2 || "-"}</Typography> | <Typography variant="body1">{shopDetail.addr2 || "-"}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Addr3</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Addr3")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.addr3 || "-"}</Typography> | <Typography variant="body1">{shopDetail.addr3 || "-"}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Contact No</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Contact No")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.contactNo || "-"}</Typography> | <Typography variant="body1">{shopDetail.contactNo || "-"}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Type</Typography> | |||||
| <Typography variant="body1">{shopDetail.type || "-"}</Typography> | |||||
| </Box> | |||||
| <Box> | |||||
| <Typography variant="subtitle2" color="text.secondary">Contact Email</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Contact Email")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.contactEmail || "-"}</Typography> | <Typography variant="body1">{shopDetail.contactEmail || "-"}</Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle2" color="text.secondary">Contact Name</Typography> | |||||
| <Typography variant="subtitle2" color="text.secondary">{t("Contact Name")}</Typography> | |||||
| <Typography variant="body1">{shopDetail.contactName || "-"}</Typography> | <Typography variant="body1">{shopDetail.contactName || "-"}</Typography> | ||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| @@ -553,27 +551,27 @@ const ShopDetail: React.FC = () => { | |||||
| <Card> | <Card> | ||||
| <CardContent> | <CardContent> | ||||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | ||||
| <Typography variant="h6">Truck Information</Typography> | |||||
| <Typography variant="h6">{t("Truck Information")}</Typography> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| startIcon={<AddIcon />} | startIcon={<AddIcon />} | ||||
| onClick={handleOpenAddDialog} | onClick={handleOpenAddDialog} | ||||
| disabled={editingRowIndex !== null || saving} | disabled={editingRowIndex !== null || saving} | ||||
| > | > | ||||
| Add Truck Lane | |||||
| {t("Add Truck Lane")} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell>TruckLance Code</TableCell> | |||||
| <TableCell>Departure Time</TableCell> | |||||
| <TableCell>Loading Sequence</TableCell> | |||||
| <TableCell>District Reference</TableCell> | |||||
| <TableCell>Store ID</TableCell> | |||||
| <TableCell>Remark</TableCell> | |||||
| <TableCell>Actions</TableCell> | |||||
| <TableCell>{t("TruckLance Code")}</TableCell> | |||||
| <TableCell>{t("Departure Time")}</TableCell> | |||||
| <TableCell>{t("Loading Sequence")}</TableCell> | |||||
| <TableCell>{t("District Reference")}</TableCell> | |||||
| <TableCell>{t("Store ID")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | |||||
| <TableCell>{t("Actions")}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| @@ -581,7 +579,7 @@ const ShopDetail: React.FC = () => { | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={7} align="center"> | <TableCell colSpan={7} align="center"> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| No Truck data available | |||||
| {t("No Truck data available")} | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -725,7 +723,7 @@ const ShopDetail: React.FC = () => { | |||||
| <TextField | <TextField | ||||
| {...params} | {...params} | ||||
| fullWidth | fullWidth | ||||
| placeholder={isEditable ? "Enter or select remark" : "Not editable for this Store ID"} | |||||
| placeholder={isEditable ? t("Enter or select remark") : t("Not editable for this Store ID")} | |||||
| disabled={!isEditable} | disabled={!isEditable} | ||||
| /> | /> | ||||
| )} | )} | ||||
| @@ -745,7 +743,7 @@ const ShopDetail: React.FC = () => { | |||||
| size="small" | size="small" | ||||
| onClick={() => handleSave(index)} | onClick={() => handleSave(index)} | ||||
| disabled={saving} | disabled={saving} | ||||
| title="Save changes" | |||||
| title={t("Save changes")} | |||||
| > | > | ||||
| <SaveIcon /> | <SaveIcon /> | ||||
| </IconButton> | </IconButton> | ||||
| @@ -754,7 +752,7 @@ const ShopDetail: React.FC = () => { | |||||
| size="small" | size="small" | ||||
| onClick={() => handleCancel(index)} | onClick={() => handleCancel(index)} | ||||
| disabled={saving} | disabled={saving} | ||||
| title="Cancel editing" | |||||
| title={t("Cancel editing")} | |||||
| > | > | ||||
| <CancelIcon /> | <CancelIcon /> | ||||
| </IconButton> | </IconButton> | ||||
| @@ -766,7 +764,7 @@ const ShopDetail: React.FC = () => { | |||||
| size="small" | size="small" | ||||
| onClick={() => handleEdit(index)} | onClick={() => handleEdit(index)} | ||||
| disabled={editingRowIndex !== null} | disabled={editingRowIndex !== null} | ||||
| title="Edit truck lane" | |||||
| title={t("Edit truck lane")} | |||||
| > | > | ||||
| <EditIcon /> | <EditIcon /> | ||||
| </IconButton> | </IconButton> | ||||
| @@ -776,7 +774,7 @@ const ShopDetail: React.FC = () => { | |||||
| size="small" | size="small" | ||||
| onClick={() => handleDelete(truck.id!)} | onClick={() => handleDelete(truck.id!)} | ||||
| disabled={saving || editingRowIndex !== null} | disabled={saving || editingRowIndex !== null} | ||||
| title="Delete truck lane" | |||||
| title={t("Delete truck lane")} | |||||
| > | > | ||||
| <DeleteIcon /> | <DeleteIcon /> | ||||
| </IconButton> | </IconButton> | ||||
| @@ -797,13 +795,13 @@ const ShopDetail: React.FC = () => { | |||||
| {/* Add Truck Dialog */} | {/* Add Truck Dialog */} | ||||
| <Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth> | <Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth> | ||||
| <DialogTitle>Add New Truck Lane</DialogTitle> | |||||
| <DialogTitle>{t("Add New Truck Lane")}</DialogTitle> | |||||
| <DialogContent> | <DialogContent> | ||||
| <Box sx={{ pt: 2 }}> | <Box sx={{ pt: 2 }}> | ||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <TextField | <TextField | ||||
| label="TruckLance Code" | |||||
| label={t("TruckLance Code")} | |||||
| fullWidth | fullWidth | ||||
| required | required | ||||
| value={newTruck.truckLanceCode} | value={newTruck.truckLanceCode} | ||||
| @@ -813,7 +811,7 @@ const ShopDetail: React.FC = () => { | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <TextField | <TextField | ||||
| label="Departure Time" | |||||
| label={t("Departure Time")} | |||||
| type="time" | type="time" | ||||
| fullWidth | fullWidth | ||||
| required | required | ||||
| @@ -830,7 +828,7 @@ const ShopDetail: React.FC = () => { | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label="Loading Sequence" | |||||
| label={t("Loading Sequence")} | |||||
| type="number" | type="number" | ||||
| fullWidth | fullWidth | ||||
| value={newTruck.loadingSequence} | value={newTruck.loadingSequence} | ||||
| @@ -840,7 +838,7 @@ const ShopDetail: React.FC = () => { | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label="District Reference" | |||||
| label={t("District Reference")} | |||||
| type="number" | type="number" | ||||
| fullWidth | fullWidth | ||||
| value={newTruck.districtReference} | value={newTruck.districtReference} | ||||
| @@ -850,10 +848,10 @@ const ShopDetail: React.FC = () => { | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <InputLabel>Store ID</InputLabel> | |||||
| <InputLabel>{t("Store ID")}</InputLabel> | |||||
| <Select | <Select | ||||
| value={newTruck.storeId} | value={newTruck.storeId} | ||||
| label="Store ID" | |||||
| label={t("Store ID")} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const newStoreId = e.target.value; | const newStoreId = e.target.value; | ||||
| setNewTruck({ | setNewTruck({ | ||||
| @@ -884,9 +882,9 @@ const ShopDetail: React.FC = () => { | |||||
| renderInput={(params) => ( | renderInput={(params) => ( | ||||
| <TextField | <TextField | ||||
| {...params} | {...params} | ||||
| label="Remark" | |||||
| label={t("Remark")} | |||||
| fullWidth | fullWidth | ||||
| placeholder="Enter or select remark" | |||||
| placeholder={t("Enter or select remark")} | |||||
| disabled={saving} | disabled={saving} | ||||
| /> | /> | ||||
| )} | )} | ||||
| @@ -898,7 +896,7 @@ const ShopDetail: React.FC = () => { | |||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| <Button onClick={handleCloseAddDialog} disabled={saving}> | <Button onClick={handleCloseAddDialog} disabled={saving}> | ||||
| Cancel | |||||
| {t("Cancel")} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| onClick={handleCreateTruck} | onClick={handleCreateTruck} | ||||
| @@ -906,7 +904,7 @@ const ShopDetail: React.FC = () => { | |||||
| startIcon={<SaveIcon />} | startIcon={<SaveIcon />} | ||||
| disabled={saving} | disabled={saving} | ||||
| > | > | ||||
| {saving ? "Saving..." : "Save"} | |||||
| {saving ? t("Submitting...") : t("Save")} | |||||
| </Button> | </Button> | ||||
| </DialogActions> | </DialogActions> | ||||
| </Dialog> | </Dialog> | ||||
| @@ -0,0 +1,269 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Box, | |||||
| Card, | |||||
| CardContent, | |||||
| Typography, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TablePagination, | |||||
| TableRow, | |||||
| Paper, | |||||
| Button, | |||||
| CircularProgress, | |||||
| Alert, | |||||
| } from "@mui/material"; | |||||
| import { useState, useEffect, useMemo } from "react"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { findAllUniqueTruckLaneCombinationsClient } from "@/app/api/shop/client"; | |||||
| import type { Truck } from "@/app/api/shop/actions"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| // 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; | |||||
| }; | |||||
| type SearchQuery = { | |||||
| truckLanceCode: string; | |||||
| departureTime: string; | |||||
| storeId: string; | |||||
| }; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const TruckLane: React.FC = () => { | |||||
| const { t } = useTranslation("common"); | |||||
| const router = useRouter(); | |||||
| const [truckData, setTruckData] = useState<Truck[]>([]); | |||||
| const [loading, setLoading] = useState<boolean>(true); | |||||
| const [error, setError] = useState<string | null>(null); | |||||
| const [filters, setFilters] = useState<Record<string, string>>({}); | |||||
| const [page, setPage] = useState(0); | |||||
| const [rowsPerPage, setRowsPerPage] = useState(10); | |||||
| useEffect(() => { | |||||
| const fetchTruckLanes = async () => { | |||||
| setLoading(true); | |||||
| setError(null); | |||||
| try { | |||||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | |||||
| setTruckData(data || []); | |||||
| } catch (err: any) { | |||||
| console.error("Failed to load truck lanes:", err); | |||||
| setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }; | |||||
| fetchTruckLanes(); | |||||
| }, []); | |||||
| // Client-side filtered rows (contains-matching) | |||||
| const filteredRows = useMemo(() => { | |||||
| const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== ""); | |||||
| const normalized = (truckData || []).filter((r) => { | |||||
| // Apply contains matching for each active filter | |||||
| for (const k of fKeys) { | |||||
| const v = String((filters as any)[k] ?? "").trim(); | |||||
| if (k === "truckLanceCode") { | |||||
| const rv = String((r as any).truckLanceCode ?? "").trim(); | |||||
| if (!rv.toLowerCase().includes(v.toLowerCase())) return false; | |||||
| } else if (k === "departureTime") { | |||||
| const formattedTime = formatDepartureTime( | |||||
| Array.isArray(r.departureTime) | |||||
| ? r.departureTime | |||||
| : (r.departureTime ? String(r.departureTime) : null) | |||||
| ); | |||||
| if (!formattedTime.toLowerCase().includes(v.toLowerCase())) return false; | |||||
| } else if (k === "storeId") { | |||||
| const rv = String((r as any).storeId ?? "").trim(); | |||||
| const storeIdStr = typeof rv === 'string' ? rv : String(rv); | |||||
| // Convert numeric values to display format for comparison | |||||
| let displayStoreId = storeIdStr; | |||||
| if (storeIdStr === "2" || storeIdStr === "2F") displayStoreId = "2F"; | |||||
| if (storeIdStr === "4" || storeIdStr === "4F") displayStoreId = "4F"; | |||||
| if (!displayStoreId.toLowerCase().includes(v.toLowerCase())) return false; | |||||
| } | |||||
| } | |||||
| return true; | |||||
| }); | |||||
| return normalized; | |||||
| }, [truckData, filters]); | |||||
| // Paginated rows | |||||
| const paginatedRows = useMemo(() => { | |||||
| const startIndex = page * rowsPerPage; | |||||
| return filteredRows.slice(startIndex, startIndex + rowsPerPage); | |||||
| }, [filteredRows, page, rowsPerPage]); | |||||
| const handleSearch = (inputs: Record<string, string>) => { | |||||
| setFilters(inputs); | |||||
| setPage(0); // Reset to first page when searching | |||||
| }; | |||||
| const handlePageChange = (event: unknown, newPage: number) => { | |||||
| setPage(newPage); | |||||
| }; | |||||
| const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||||
| setRowsPerPage(parseInt(event.target.value, 10)); | |||||
| setPage(0); // Reset to first page when changing rows per page | |||||
| }; | |||||
| const handleViewDetail = (truck: Truck) => { | |||||
| // Navigate to truck lane detail page | |||||
| if (truck.id) { | |||||
| router.push(`/settings/shop/truckdetail?id=${truck.id}`); | |||||
| } | |||||
| }; | |||||
| if (loading) { | |||||
| return ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (error) { | |||||
| return ( | |||||
| <Box> | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | |||||
| {error} | |||||
| </Alert> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| const criteria: Criterion<SearchParamNames>[] = [ | |||||
| { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" }, | |||||
| { type: "text", label: t("Departure Time"), paramName: "departureTime" }, | |||||
| { type: "text", label: t("Store ID"), paramName: "storeId" }, | |||||
| ]; | |||||
| return ( | |||||
| <Box> | |||||
| <Card sx={{ mb: 2 }}> | |||||
| <CardContent> | |||||
| <SearchBox | |||||
| criteria={criteria as Criterion<string>[]} | |||||
| onSearch={handleSearch} | |||||
| onReset={() => { | |||||
| setFilters({}); | |||||
| }} | |||||
| /> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Typography variant="h6" sx={{ mb: 2 }}>{t("Truck Lane")}</Typography> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("TruckLance Code")}</TableCell> | |||||
| <TableCell>{t("Departure Time")}</TableCell> | |||||
| <TableCell>{t("Store ID")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | |||||
| <TableCell align="right">{t("Actions")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedRows.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={5} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No Truck Lane data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| 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 ( | |||||
| <TableRow key={truck.id ?? `truck-${index}`}> | |||||
| <TableCell> | |||||
| {String(truck.truckLanceCode || "-")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {formatDepartureTime( | |||||
| Array.isArray(truck.departureTime) | |||||
| ? truck.departureTime | |||||
| : (truck.departureTime ? String(truck.departureTime) : null) | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {displayStoreId} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {String(truck.remark || "-")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Button | |||||
| size="small" | |||||
| variant="outlined" | |||||
| onClick={() => handleViewDetail(truck)} | |||||
| > | |||||
| {t("View Detail")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ); | |||||
| }) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={filteredRows.length} | |||||
| page={page} | |||||
| onPageChange={handlePageChange} | |||||
| rowsPerPage={rowsPerPage} | |||||
| onRowsPerPageChange={handleRowsPerPageChange} | |||||
| rowsPerPageOptions={[5, 10, 25, 50]} | |||||
| /> | |||||
| </TableContainer> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default TruckLane; | |||||
| @@ -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<Truck | null>(null); | |||||
| const [shopsData, setShopsData] = useState<ShopAndTruck[]>([]); | |||||
| const [editedShopsData, setEditedShopsData] = useState<ShopAndTruck[]>([]); | |||||
| const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null); | |||||
| const [loading, setLoading] = useState<boolean>(true); | |||||
| const [shopsLoading, setShopsLoading] = useState<boolean>(false); | |||||
| const [saving, setSaving] = useState<boolean>(false); | |||||
| const [error, setError] = useState<string | null>(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 ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (error) { | |||||
| return ( | |||||
| <Box> | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | |||||
| {error} | |||||
| </Alert> | |||||
| <Button variant="contained" onClick={handleBack}> | |||||
| {t("Back to Truck Lane List")} | |||||
| </Button> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (!truckData) { | |||||
| return ( | |||||
| <Box> | |||||
| <Alert severity="warning" sx={{ mb: 2 }}> | |||||
| {t("No truck lane data available")} | |||||
| </Alert> | |||||
| <Button variant="contained" onClick={handleBack}> | |||||
| {t("Back to Truck Lane List")} | |||||
| </Button> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| 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 ( | |||||
| <Box> | |||||
| <Card sx={{ mb: 2 }}> | |||||
| <CardContent> | |||||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | |||||
| <Typography variant="h5">{t("Truck Lane Detail")}</Typography> | |||||
| <Button variant="outlined" onClick={handleBack}> | |||||
| {t("Back to Truck Lane List")} | |||||
| </Button> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Paper sx={{ p: 3 }}> | |||||
| <Grid container spacing={3}> | |||||
| <Grid item xs={12} sm={6}> | |||||
| <Typography variant="subtitle2" color="text.secondary"> | |||||
| {t("TruckLance Code")} | |||||
| </Typography> | |||||
| <Typography variant="body1" sx={{ mt: 1 }}> | |||||
| {String(truckData.truckLanceCode || "-")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12} sm={6}> | |||||
| <Typography variant="subtitle2" color="text.secondary"> | |||||
| {t("Departure Time")} | |||||
| </Typography> | |||||
| <Typography variant="body1" sx={{ mt: 1 }}> | |||||
| {formatDepartureTime( | |||||
| Array.isArray(truckData.departureTime) | |||||
| ? truckData.departureTime | |||||
| : (truckData.departureTime ? String(truckData.departureTime) : null) | |||||
| )} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12} sm={6}> | |||||
| <Typography variant="subtitle2" color="text.secondary"> | |||||
| {t("Store ID")} | |||||
| </Typography> | |||||
| <Typography variant="body1" sx={{ mt: 1 }}> | |||||
| {displayStoreId} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12} sm={6}> | |||||
| <Typography variant="subtitle2" color="text.secondary"> | |||||
| {t("Remark")} | |||||
| </Typography> | |||||
| <Typography variant="body1" sx={{ mt: 1 }}> | |||||
| {String(truckData.remark || "-")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Paper> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card sx={{ mt: 2 }}> | |||||
| <CardContent> | |||||
| <Typography variant="h6" sx={{ mb: 2 }}> | |||||
| {t("Shops Using This Truck Lane")} | |||||
| </Typography> | |||||
| {shopsLoading ? ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Shop Name")}</TableCell> | |||||
| <TableCell>{t("Shop Code")}</TableCell> | |||||
| <TableCell>{t("Loading Sequence")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | |||||
| <TableCell align="right">{t("Actions")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {shopsData.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={5} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No shops found using this truck lane")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| shopsData.map((shop, index) => ( | |||||
| <TableRow key={shop.id ?? `shop-${index}`}> | |||||
| <TableCell> | |||||
| {String(shop.name || "-")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {String(shop.code || "-")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {editingRowIndex === index ? ( | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={(() => { | |||||
| 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) | |||||
| : "-"; | |||||
| })() | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {String(shop.remark || "-")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}> | |||||
| {editingRowIndex === index ? ( | |||||
| <> | |||||
| <IconButton | |||||
| size="small" | |||||
| color="primary" | |||||
| onClick={() => handleSave(index)} | |||||
| disabled={saving} | |||||
| title={t("Save changes")} | |||||
| > | |||||
| <SaveIcon /> | |||||
| </IconButton> | |||||
| <IconButton | |||||
| size="small" | |||||
| color="default" | |||||
| onClick={() => handleCancel(index)} | |||||
| disabled={saving} | |||||
| title={t("Cancel editing")} | |||||
| > | |||||
| <CancelIcon /> | |||||
| </IconButton> | |||||
| </> | |||||
| ) : ( | |||||
| <> | |||||
| <IconButton | |||||
| size="small" | |||||
| color="primary" | |||||
| onClick={() => handleEdit(index)} | |||||
| title={t("Edit loading sequence")} | |||||
| > | |||||
| <EditIcon /> | |||||
| </IconButton> | |||||
| {shop.truckId && ( | |||||
| <IconButton | |||||
| size="small" | |||||
| color="error" | |||||
| onClick={() => handleDelete(shop.truckId!)} | |||||
| title={t("Delete truck lane")} | |||||
| > | |||||
| <DeleteIcon /> | |||||
| </IconButton> | |||||
| )} | |||||
| </> | |||||
| )} | |||||
| </Box> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| )} | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Snackbar | |||||
| open={snackbar.open} | |||||
| autoHideDuration={6000} | |||||
| onClose={() => setSnackbar({ ...snackbar, open: false })} | |||||
| message={snackbar.message} | |||||
| /> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default TruckLaneDetail; | |||||
| @@ -296,5 +296,75 @@ | |||||
| "Total lines: ": "總數量:", | "Total lines: ": "總數量:", | ||||
| "Balance": "可用數量", | "Balance": "可用數量", | ||||
| "Submitting...": "提交中...", | "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": "全部" | |||||
| } | } | ||||