| @@ -0,0 +1,52 @@ | |||||
| "use client"; | |||||
| import { useState, useEffect } from "react"; | |||||
| import Tab from "@mui/material/Tab"; | |||||
| import Tabs from "@mui/material/Tabs"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { useRouter, useSearchParams } from "next/navigation"; | |||||
| type EquipmentTabsProps = { | |||||
| onTabChange?: (tabIndex: number) => void; | |||||
| }; | |||||
| const EquipmentTabs: React.FC<EquipmentTabsProps> = ({ onTabChange }) => { | |||||
| const router = useRouter(); | |||||
| const searchParams = useSearchParams(); | |||||
| const { t } = useTranslation("common"); | |||||
| const tabFromUrl = searchParams.get("tab"); | |||||
| const initialTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; | |||||
| const [tabIndex, setTabIndex] = useState(initialTabIndex); | |||||
| useEffect(() => { | |||||
| const tabFromUrl = searchParams.get("tab"); | |||||
| const newTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; | |||||
| if (newTabIndex !== tabIndex) { | |||||
| setTabIndex(newTabIndex); | |||||
| onTabChange?.(newTabIndex); | |||||
| } | |||||
| }, [searchParams, tabIndex, onTabChange]); | |||||
| const handleTabChange = (_e: React.SyntheticEvent, newValue: number) => { | |||||
| setTabIndex(newValue); | |||||
| onTabChange?.(newValue); | |||||
| const params = new URLSearchParams(searchParams.toString()); | |||||
| if (newValue === 0) { | |||||
| params.delete("tab"); | |||||
| } else { | |||||
| params.set("tab", newValue.toString()); | |||||
| } | |||||
| router.push(`/settings/equipment?${params.toString()}`, { scroll: false }); | |||||
| }; | |||||
| return ( | |||||
| <Tabs value={tabIndex} onChange={handleTabChange}> | |||||
| <Tab label={t("General Data")} /> | |||||
| <Tab label={t("Repair and Maintenance")} /> | |||||
| </Tabs> | |||||
| ); | |||||
| }; | |||||
| export default EquipmentTabs; | |||||
| @@ -0,0 +1,29 @@ | |||||
| import React from "react"; | |||||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { Typography } from "@mui/material"; | |||||
| import isString from "lodash/isString"; | |||||
| import { notFound } from "next/navigation"; | |||||
| import UpdateMaintenanceForm from "@/components/UpdateMaintenance/UpdateMaintenanceForm"; | |||||
| type Props = {} & SearchParams; | |||||
| const MaintenanceEditPage: React.FC<Props> = async ({ searchParams }) => { | |||||
| const type = "common"; | |||||
| const { t } = await getServerI18n(type); | |||||
| const id = isString(searchParams["id"]) | |||||
| ? parseInt(searchParams["id"]) | |||||
| : undefined; | |||||
| if (!id) { | |||||
| notFound(); | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Update Equipment Maintenance and Repair")}</Typography> | |||||
| <I18nProvider namespaces={[type]}> | |||||
| <UpdateMaintenanceForm id={id} /> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default MaintenanceEditPage; | |||||
| @@ -1,15 +1,18 @@ | |||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import EquipmentSearch from "@/components/EquipmentSearch"; | |||||
| import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
| import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import Tab from "@mui/material/Tab"; | |||||
| import Tabs from "@mui/material/Tabs"; | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
| import { fetchAllEquipments } from "@/app/api/settings/equipment"; | import { fetchAllEquipments } from "@/app/api/settings/equipment"; | ||||
| import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
| import EquipmentSearchWrapper from "@/components/EquipmentSearch/EquipmentSearchWrapper"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Equipment Type", | title: "Equipment Type", | ||||
| }; | }; | ||||
| @@ -17,8 +20,6 @@ export const metadata: Metadata = { | |||||
| const productSetting: React.FC = async () => { | const productSetting: React.FC = async () => { | ||||
| const type = "common"; | const type = "common"; | ||||
| const { t } = await getServerI18n(type); | const { t } = await getServerI18n(type); | ||||
| const equipments = await fetchAllEquipments(); | |||||
| // preloadClaims(); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -31,22 +32,14 @@ const productSetting: React.FC = async () => { | |||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| {t("Equipment")} | {t("Equipment")} | ||||
| </Typography> | </Typography> | ||||
| {/* <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="product/create" | |||||
| > | |||||
| {t("Create product")} | |||||
| </Button> */} | |||||
| </Stack> | </Stack> | ||||
| <Suspense fallback={<EquipmentSearch.Loading />}> | |||||
| <Suspense fallback={<EquipmentSearchWrapper.Loading />}> | |||||
| <I18nProvider namespaces={["common", "project"]}> | <I18nProvider namespaces={["common", "project"]}> | ||||
| <EquipmentSearch /> | |||||
| <EquipmentSearchWrapper /> | |||||
| </I18nProvider> | </I18nProvider> | ||||
| </Suspense> | </Suspense> | ||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default productSetting; | |||||
| export default productSetting; | |||||
| @@ -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> | |||||
| ); | |||||
| } | |||||
| @@ -177,22 +177,32 @@ export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) => | |||||
| return response; | return response; | ||||
| }) | }) | ||||
| export const exportProdSchedule = async (token: string | null) => { | |||||
| export const exportProdSchedule = async ( | |||||
| token: string | null, | |||||
| inputs: any, | |||||
| prodHeaders: string[], | |||||
| matHeaders: string[] | |||||
| ) => { | |||||
| if (!token) throw new Error("No access token found"); | if (!token) throw new Error("No access token found"); | ||||
| const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, { | const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, { | ||||
| method: "POST", | method: "POST", | ||||
| headers: { | headers: { | ||||
| "Content-Type": "application/json", // Critical for @RequestBody | |||||
| "Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | "Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | ||||
| "Authorization": `Bearer ${token}` | "Authorization": `Bearer ${token}` | ||||
| } | |||||
| }, | |||||
| // Send everything in one object | |||||
| body: JSON.stringify({ | |||||
| ...inputs, | |||||
| prodHeaders, | |||||
| matHeaders | |||||
| }) | |||||
| }); | }); | ||||
| if (!response.ok) throw new Error(`Backend error: ${response.status}`); | if (!response.ok) throw new Error(`Backend error: ${response.status}`); | ||||
| const arrayBuffer = await response.arrayBuffer(); | const arrayBuffer = await response.arrayBuffer(); | ||||
| // Convert to Base64 so Next.js can send it safely over the wire | |||||
| return Buffer.from(arrayBuffer).toString('base64'); | return Buffer.from(arrayBuffer).toString('base64'); | ||||
| }; | }; | ||||
| @@ -13,7 +13,12 @@ export type EquipmentResult = { | |||||
| name: string; | name: string; | ||||
| description: string | undefined; | description: string | undefined; | ||||
| equipmentTypeId: string | number | undefined; | equipmentTypeId: string | number | undefined; | ||||
| equipmentCode?: string; | |||||
| action?: any; | action?: any; | ||||
| repairAndMaintenanceStatus?: boolean | number; | |||||
| latestRepairAndMaintenanceDate?: string | Date; | |||||
| lastRepairAndMaintenanceDate?: string | Date; | |||||
| repairAndMaintenanceRemarks?: string; | |||||
| }; | }; | ||||
| export type Result = { | export type Result = { | ||||
| @@ -37,6 +37,14 @@ export type CreateItemInputs = { | |||||
| qcChecks: QcChecksInputs[]; | qcChecks: QcChecksInputs[]; | ||||
| qcChecks_active: number[]; | qcChecks_active: number[]; | ||||
| qcCategoryId: number | undefined; | qcCategoryId: number | undefined; | ||||
| store_id?: string | undefined; | |||||
| warehouse?: string | undefined; | |||||
| area?: string | undefined; | |||||
| slot?: string | undefined; | |||||
| LocationCode?: string | undefined; | |||||
| isEgg?: boolean | undefined; | |||||
| isFee?: boolean | undefined; | |||||
| isBag?: boolean | undefined; | |||||
| }; | }; | ||||
| export const saveItem = async (data: CreateItemInputs) => { | export const saveItem = async (data: CreateItemInputs) => { | ||||
| @@ -53,6 +53,14 @@ export type ItemsResult = { | |||||
| fgName?: string; | fgName?: string; | ||||
| excludeDate?: string; | excludeDate?: string; | ||||
| qcCategory?: QcCategoryResult; | qcCategory?: QcCategoryResult; | ||||
| store_id?: string | undefined; | |||||
| warehouse?: string | undefined; | |||||
| area?: string | undefined; | |||||
| slot?: string | undefined; | |||||
| LocationCode?: string | undefined; | |||||
| isEgg?: boolean | undefined; | |||||
| isFee?: boolean | undefined; | |||||
| isBag?: boolean | undefined; | |||||
| }; | }; | ||||
| export type Result = { | export type Result = { | ||||
| @@ -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,45 @@ 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 findAllShopsByTruckLanceCodeAction = cache(async (truckLanceCode: string) => { | |||||
| const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse`; | |||||
| const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`; | |||||
| 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,14 @@ import { | |||||
| updateTruckLaneAction, | updateTruckLaneAction, | ||||
| deleteTruckLaneAction, | deleteTruckLaneAction, | ||||
| createTruckAction, | createTruckAction, | ||||
| findAllUniqueTruckLaneCombinationsAction, | |||||
| findAllShopsByTruckLanceCodeAndRemarkAction, | |||||
| findAllShopsByTruckLanceCodeAction, | |||||
| 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 +37,20 @@ 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 findAllShopsByTruckLanceCodeClient = async (truckLanceCode: string) => { | |||||
| return await findAllShopsByTruckLanceCodeAction(truckLanceCode); | |||||
| }; | |||||
| 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": "ShopAndTruck", | |||||
| "/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", | ||||
| @@ -21,7 +21,7 @@ import { | |||||
| TabsProps, | TabsProps, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { Check, Close, EditNote } from "@mui/icons-material"; | |||||
| import { Check, Close, EditNote, ArrowBack } from "@mui/icons-material"; | |||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import ProductDetails from "./ProductDetails"; | import ProductDetails from "./ProductDetails"; | ||||
| import { CreateItemResponse } from "@/app/api/utils"; | import { CreateItemResponse } from "@/app/api/utils"; | ||||
| @@ -30,13 +30,15 @@ import { ItemQc } from "@/app/api/settings/item"; | |||||
| import { saveItemQcChecks } from "@/app/api/settings/qcCheck/actions"; | import { saveItemQcChecks } from "@/app/api/settings/qcCheck/actions"; | ||||
| import { useGridApiRef } from "@mui/x-data-grid"; | import { useGridApiRef } from "@mui/x-data-grid"; | ||||
| import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; | import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; | ||||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||||
| type Props = { | type Props = { | ||||
| isEditMode: boolean; | isEditMode: boolean; | ||||
| // type: TypeEnum; | // type: TypeEnum; | ||||
| defaultValues: Partial<CreateItemInputs> | undefined; | defaultValues: Partial<CreateItemInputs> | undefined; | ||||
| qcChecks: ItemQc[]; | qcChecks: ItemQc[]; | ||||
| qcCategoryCombo: QcCategoryCombo[] | |||||
| qcCategoryCombo: QcCategoryCombo[]; | |||||
| warehouses: WarehouseResult[]; | |||||
| }; | }; | ||||
| const CreateItem: React.FC<Props> = ({ | const CreateItem: React.FC<Props> = ({ | ||||
| @@ -45,6 +47,7 @@ const CreateItem: React.FC<Props> = ({ | |||||
| defaultValues, | defaultValues, | ||||
| qcChecks, | qcChecks, | ||||
| qcCategoryCombo, | qcCategoryCombo, | ||||
| warehouses, | |||||
| }) => { | }) => { | ||||
| // console.log(type) | // console.log(type) | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| @@ -109,6 +112,26 @@ const CreateItem: React.FC<Props> = ({ | |||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| return false; | return false; | ||||
| } | } | ||||
| // Normalize LocationCode: convert empty string to null | |||||
| if (data.LocationCode && data.LocationCode.trim() !== "") { | |||||
| // Parse LocationCode and populate store_id, warehouse, area, slot | |||||
| const parts = data.LocationCode.split("-"); | |||||
| if (parts.length >= 4) { | |||||
| data.store_id = parts[0] || undefined; | |||||
| data.warehouse = parts[1] || undefined; | |||||
| data.area = parts[2] || undefined; | |||||
| data.slot = parts[3] || undefined; | |||||
| } | |||||
| } else { | |||||
| // If LocationCode is null or empty, set LocationCode to null and clear related fields | |||||
| data.LocationCode = undefined; | |||||
| data.store_id = undefined; | |||||
| data.warehouse = undefined; | |||||
| data.area = undefined; | |||||
| data.slot = undefined; | |||||
| } | |||||
| console.log("data posted"); | console.log("data posted"); | ||||
| console.log(data); | console.log(data); | ||||
| const qcCheck = | const qcCheck = | ||||
| @@ -178,9 +201,19 @@ const CreateItem: React.FC<Props> = ({ | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | ||||
| > | > | ||||
| <Grid> | <Grid> | ||||
| <Typography mb={2} variant="h4"> | |||||
| {t(`${mode} ${title}`)} | |||||
| </Typography> | |||||
| <Stack direction="column" spacing={1} mb={2}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<ArrowBack />} | |||||
| onClick={() => router.push("/settings/items")} | |||||
| sx={{ alignSelf: "flex-start", minWidth: "auto" }} | |||||
| > | |||||
| {t("Back")} | |||||
| </Button> | |||||
| <Typography variant="h4"> | |||||
| {t(`${mode} ${title}`)} | |||||
| </Typography> | |||||
| </Stack> | |||||
| </Grid> | </Grid> | ||||
| <Tabs | <Tabs | ||||
| value={tabIndex} | value={tabIndex} | ||||
| @@ -195,7 +228,14 @@ const CreateItem: React.FC<Props> = ({ | |||||
| {serverError} | {serverError} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| {tabIndex === 0 && <ProductDetails isEditMode={isEditMode} qcCategoryCombo={qcCategoryCombo}/>} | |||||
| {tabIndex === 0 && ( | |||||
| <ProductDetails | |||||
| isEditMode={isEditMode} | |||||
| qcCategoryCombo={qcCategoryCombo} | |||||
| warehouses={warehouses} | |||||
| defaultValues={defaultValues} | |||||
| /> | |||||
| )} | |||||
| {tabIndex === 1 && <QcDetails apiRef={apiRef} />} | {tabIndex === 1 && <QcDetails apiRef={apiRef} />} | ||||
| {/* {type === TypeEnum.MATERIAL && <MaterialDetails />} */} | {/* {type === TypeEnum.MATERIAL && <MaterialDetails />} */} | ||||
| {/* {type === TypeEnum.BYPRODUCT && <ByProductDetails />} */} | {/* {type === TypeEnum.BYPRODUCT && <ByProductDetails />} */} | ||||
| @@ -6,6 +6,7 @@ import { notFound } from "next/navigation"; | |||||
| import { fetchItem } from "@/app/api/settings/item"; | import { fetchItem } from "@/app/api/settings/item"; | ||||
| import { fetchQcItems } from "@/app/api/settings/qcItem"; | import { fetchQcItems } from "@/app/api/settings/qcItem"; | ||||
| import { fetchQcCategoryCombo } from "@/app/api/settings/qcCategory"; | import { fetchQcCategoryCombo } from "@/app/api/settings/qcCategory"; | ||||
| import { fetchWarehouseList } from "@/app/api/warehouse"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof CreateItemLoading; | Loading: typeof CreateItemLoading; | ||||
| } | } | ||||
| @@ -38,11 +39,20 @@ const CreateItemWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | |||||
| maxQty: item?.maxQty, | maxQty: item?.maxQty, | ||||
| qcChecks: qcChecks, | qcChecks: qcChecks, | ||||
| qcChecks_active: activeRows, | qcChecks_active: activeRows, | ||||
| qcCategoryId: item.qcCategory?.id | |||||
| qcCategoryId: item.qcCategory?.id, | |||||
| store_id: item?.store_id, | |||||
| warehouse: item?.warehouse, | |||||
| area: item?.area, | |||||
| slot: item?.slot, | |||||
| LocationCode: item?.LocationCode, | |||||
| isEgg: item?.isEgg, | |||||
| isFee: item?.isFee, | |||||
| isBag: item?.isBag, | |||||
| }; | }; | ||||
| } | } | ||||
| const qcCategoryCombo = await fetchQcCategoryCombo(); | const qcCategoryCombo = await fetchQcCategoryCombo(); | ||||
| const warehouses = await fetchWarehouseList(); | |||||
| return ( | return ( | ||||
| <CreateItem | <CreateItem | ||||
| @@ -50,6 +60,7 @@ const CreateItemWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | |||||
| defaultValues={defaultValues} | defaultValues={defaultValues} | ||||
| qcChecks={qcChecks || []} | qcChecks={qcChecks || []} | ||||
| qcCategoryCombo={qcCategoryCombo} | qcCategoryCombo={qcCategoryCombo} | ||||
| warehouses={warehouses} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -5,12 +5,20 @@ import { | |||||
| Button, | Button, | ||||
| Card, | Card, | ||||
| CardContent, | CardContent, | ||||
| FormControl, | |||||
| FormControlLabel, | |||||
| FormLabel, | |||||
| Grid, | Grid, | ||||
| InputLabel, | |||||
| MenuItem, | |||||
| Radio, | |||||
| RadioGroup, | |||||
| Select, | |||||
| Stack, | Stack, | ||||
| TextField, | TextField, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { Check, Close, EditNote } from "@mui/icons-material"; | |||||
| import { Check, EditNote } from "@mui/icons-material"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | import { Controller, useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import InputDataGrid from "../InputDataGrid"; | import InputDataGrid from "../InputDataGrid"; | ||||
| @@ -19,11 +27,10 @@ import { SyntheticEvent, useCallback, useMemo, useState } from "react"; | |||||
| import { GridColDef, GridRowModel } from "@mui/x-data-grid"; | import { GridColDef, GridRowModel } from "@mui/x-data-grid"; | ||||
| import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; | import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; | ||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import { NumberInputProps } from "./NumberInputProps"; | |||||
| import { CreateItemInputs } from "@/app/api/settings/item/actions"; | import { CreateItemInputs } from "@/app/api/settings/item/actions"; | ||||
| import { RestartAlt } from "@mui/icons-material"; | |||||
| import { ItemQc } from "@/app/api/settings/item"; | import { ItemQc } from "@/app/api/settings/item"; | ||||
| import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; | import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; | ||||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||||
| type Props = { | type Props = { | ||||
| // isEditMode: boolean; | // isEditMode: boolean; | ||||
| // type: TypeEnum; | // type: TypeEnum; | ||||
| @@ -32,9 +39,11 @@ type Props = { | |||||
| defaultValues?: Partial<CreateItemInputs> | undefined; | defaultValues?: Partial<CreateItemInputs> | undefined; | ||||
| qcChecks?: ItemQc[]; | qcChecks?: ItemQc[]; | ||||
| qcCategoryCombo: QcCategoryCombo[]; | qcCategoryCombo: QcCategoryCombo[]; | ||||
| warehouses: WarehouseResult[]; | |||||
| }; | }; | ||||
| const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => { | |||||
| const { | const { | ||||
| t, | t, | ||||
| i18n: { language }, | i18n: { language }, | ||||
| @@ -42,13 +51,11 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| const { | const { | ||||
| register, | register, | ||||
| formState: { errors, defaultValues, touchedFields }, | |||||
| formState: { errors, touchedFields }, | |||||
| watch, | watch, | ||||
| control, | control, | ||||
| setValue, | setValue, | ||||
| getValues, | getValues, | ||||
| reset, | |||||
| resetField, | |||||
| setError, | setError, | ||||
| clearErrors, | clearErrors, | ||||
| } = useFormContext<CreateItemInputs>(); | } = useFormContext<CreateItemInputs>(); | ||||
| @@ -103,11 +110,6 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| // }, | // }, | ||||
| // [] | // [] | ||||
| // ); | // ); | ||||
| const handleCancel = () => { | |||||
| // router.replace(`/settings/product`); | |||||
| console.log("cancel"); | |||||
| }; | |||||
| const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: QcCategoryCombo, onChange: (...event: any[]) => void) => { | const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: QcCategoryCombo, onChange: (...event: any[]) => void) => { | ||||
| onChange(value.id) | onChange(value.id) | ||||
| }, []) | }, []) | ||||
| @@ -124,6 +126,7 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| <TextField | <TextField | ||||
| label={t("Name")} | label={t("Name")} | ||||
| fullWidth | fullWidth | ||||
| disabled | |||||
| {...register("name", { | {...register("name", { | ||||
| required: "name required!", | required: "name required!", | ||||
| })} | })} | ||||
| @@ -135,6 +138,7 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| <TextField | <TextField | ||||
| label={t("Code")} | label={t("Code")} | ||||
| fullWidth | fullWidth | ||||
| disabled | |||||
| {...register("code", { | {...register("code", { | ||||
| required: "code required!", | required: "code required!", | ||||
| })} | })} | ||||
| @@ -143,73 +147,44 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | |||||
| label={t("Type")} | |||||
| fullWidth | |||||
| {...register("type", { | |||||
| <Controller | |||||
| control={control} | |||||
| name="type" | |||||
| rules={{ | |||||
| required: "type required!", | required: "type required!", | ||||
| })} | |||||
| error={Boolean(errors.type)} | |||||
| helperText={errors.type?.message} | |||||
| }} | |||||
| render={({ field }) => ( | |||||
| <FormControl fullWidth error={Boolean(errors.type)}> | |||||
| <InputLabel>{t("Type")}</InputLabel> | |||||
| <Select | |||||
| value={field.value || ""} | |||||
| label={t("Type")} | |||||
| onChange={field.onChange} | |||||
| onBlur={field.onBlur} | |||||
| > | |||||
| <MenuItem value="fg">FG</MenuItem> | |||||
| <MenuItem value="wip">WIP</MenuItem> | |||||
| <MenuItem value="mat">MAT</MenuItem> | |||||
| <MenuItem value="cmb">CMB</MenuItem> | |||||
| <MenuItem value="nm">NM</MenuItem> | |||||
| </Select> | |||||
| {errors.type && ( | |||||
| <Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.5 }}> | |||||
| {errors.type.message} | |||||
| </Typography> | |||||
| )} | |||||
| </FormControl> | |||||
| )} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("description")} | label={t("description")} | ||||
| fullWidth | fullWidth | ||||
| disabled | |||||
| {...register("description")} | {...register("description")} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("shelfLife")} | |||||
| type="number" | |||||
| fullWidth | |||||
| {...register("shelfLife", { | |||||
| valueAsNumber: true, | |||||
| required: "shelfLife required!", | |||||
| })} | |||||
| error={Boolean(errors.shelfLife)} | |||||
| helperText={errors.shelfLife?.message} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("countryOfOrigin")} | |||||
| fullWidth | |||||
| {...register("countryOfOrigin", { | |||||
| required: "countryOfOrigin required!", | |||||
| })} | |||||
| error={Boolean(errors.countryOfOrigin)} | |||||
| helperText={errors.countryOfOrigin?.message} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("remarks")} | |||||
| fullWidth | |||||
| {...register("remarks", { | |||||
| // required: "remarks required!", | |||||
| })} | |||||
| error={Boolean(errors.remarks)} | |||||
| helperText={errors.remarks?.message} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("maxQty")} | |||||
| type="number" | |||||
| fullWidth | |||||
| inputProps={NumberInputProps} | |||||
| {...register("maxQty", { | |||||
| valueAsNumber: true, | |||||
| min: 0, | |||||
| required: "maxQty required!", | |||||
| })} | |||||
| error={Boolean(errors.maxQty)} | |||||
| helperText={errors.maxQty?.message} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| @@ -234,6 +209,82 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| )} | )} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| control={control} | |||||
| name="LocationCode" | |||||
| render={({ field }) => ( | |||||
| <Autocomplete | |||||
| freeSolo | |||||
| options={warehouses.map((w) => ({ | |||||
| label: `${w.code}`, | |||||
| code: w.code, | |||||
| }))} | |||||
| getOptionLabel={(option) => | |||||
| typeof option === "string" | |||||
| ? option | |||||
| : option.label ?? option.code ?? "" | |||||
| } | |||||
| value={ | |||||
| warehouses | |||||
| .map((w) => ({ | |||||
| label: `${w.code}`, | |||||
| code: w.code, | |||||
| })) | |||||
| .find((opt) => opt.code === field.value) || | |||||
| (field.value | |||||
| ? { label: field.value as string, code: field.value as string } | |||||
| : null) | |||||
| } | |||||
| onChange={(_e, value) => { | |||||
| if (typeof value === "string") { | |||||
| field.onChange(value.trim() === "" ? undefined : value); | |||||
| } else { | |||||
| field.onChange(value?.code ? (value.code.trim() === "" ? undefined : value.code) : undefined); | |||||
| } | |||||
| }} | |||||
| onInputChange={(_e, value) => { | |||||
| // keep manual input synced - convert empty string to undefined | |||||
| field.onChange(value.trim() === "" ? undefined : value); | |||||
| }} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={t("DefaultLocationCode")} | |||||
| fullWidth | |||||
| error={Boolean(errors.LocationCode)} | |||||
| helperText={errors.LocationCode?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <FormControl component="fieldset"> | |||||
| <FormLabel component="legend">{t("Special Type")}</FormLabel> | |||||
| <RadioGroup | |||||
| row | |||||
| value={ | |||||
| watch("isEgg") === true ? "isEgg" : | |||||
| watch("isFee") === true ? "isFee" : | |||||
| watch("isBag") === true ? "isBag" : | |||||
| "none" | |||||
| } | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| setValue("isEgg", value === "isEgg", { shouldValidate: true }); | |||||
| setValue("isFee", value === "isFee", { shouldValidate: true }); | |||||
| setValue("isBag", value === "isBag", { shouldValidate: true }); | |||||
| }} | |||||
| > | |||||
| <FormControlLabel value="none" control={<Radio />} label={t("None")} /> | |||||
| <FormControlLabel value="isEgg" control={<Radio />} label={t("isEgg")} /> | |||||
| <FormControlLabel value="isFee" control={<Radio />} label={t("isFee")} /> | |||||
| <FormControlLabel value="isBag" control={<Radio />} label={t("isBag")} /> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Stack | <Stack | ||||
| direction="row" | direction="row" | ||||
| @@ -250,20 +301,6 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => { | |||||
| > | > | ||||
| {isEditMode ? t("Save") : t("Confirm")} | {isEditMode ? t("Save") : t("Confirm")} | ||||
| </Button> | </Button> | ||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Close />} | |||||
| onClick={handleCancel} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<RestartAlt />} | |||||
| onClick={() => reset()} | |||||
| > | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </Stack> | </Stack> | ||||
| </Grid> | </Grid> | ||||
| {/* <Grid item xs={6}> | {/* <Grid item xs={6}> | ||||
| @@ -50,6 +50,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| >([]); | >([]); | ||||
| const { t } = useTranslation("schedule"); | const { t } = useTranslation("schedule"); | ||||
| const { setIsUploading } = useUploadContext(); | const { setIsUploading } = useUploadContext(); | ||||
| const today = dayjs().format("YYYY-MM-DD"); | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| // const [filterObj, setFilterObj] = useState({}); | // const [filterObj, setFilterObj] = useState({}); | ||||
| @@ -58,7 +59,10 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| defaultPagingController, | defaultPagingController, | ||||
| ); | ); | ||||
| const [totalCount, setTotalCount] = useState(0); | const [totalCount, setTotalCount] = useState(0); | ||||
| const [inputs, setInputs] = useState(defaultInputs); | |||||
| const [inputs, setInputs] = useState({ | |||||
| ...defaultInputs, | |||||
| produceAt: dayjs().format("YYYY-MM-DD"), | |||||
| }); | |||||
| const typeOptions = [ | const typeOptions = [ | ||||
| { | { | ||||
| value: "detailed", | value: "detailed", | ||||
| @@ -78,7 +82,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| // paramName: "schedulePeriod", | // paramName: "schedulePeriod", | ||||
| // type: "dateRange", | // type: "dateRange", | ||||
| // }, | // }, | ||||
| { label: t("Production Date"), paramName: "scheduleAt", type: "date" }, | |||||
| { label: t("Production Date"), paramName: "produceAt", type: "date", defaultValue: dayjs().format("YYYY-MM-DD") }, | |||||
| //{ | //{ | ||||
| // label: t("Product Count"), | // label: t("Product Count"), | ||||
| // paramName: "totalEstProdCount", | // paramName: "totalEstProdCount", | ||||
| @@ -179,18 +183,9 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| ) as ScheduleType[]; | ) as ScheduleType[]; | ||||
| const params: SearchProdSchedule = { | const params: SearchProdSchedule = { | ||||
| //scheduleAt: dayjs(query?.scheduleAt).isValid() | |||||
| // ? query?.scheduleAt | |||||
| // : undefined, | |||||
| //schedulePeriod: dayjs(query?.schedulePeriod).isValid() | |||||
| // ? query?.schedulePeriod | |||||
| // : undefined, | |||||
| //schedulePeriodTo: dayjs(query?.schedulePeriodTo).isValid() | |||||
| // ? query?.schedulePeriodTo | |||||
| // : undefined, | |||||
| //totalEstProdCount: query?.totalEstProdCount | |||||
| // ? Number(query?.totalEstProdCount) | |||||
| // : undefined, | |||||
| produceAt: dayjs(query?.produceAt).isValid() | |||||
| ? query?.produceAt | |||||
| : undefined, | |||||
| types: convertedTypes, | types: convertedTypes, | ||||
| pageNum: pagingController.pageNum - 1, | pageNum: pagingController.pageNum - 1, | ||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| @@ -221,77 +216,24 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| refetchData(inputs, "paging"); | refetchData(inputs, "paging"); | ||||
| }, [pagingController]); | }, [pagingController]); | ||||
| // useEffect(() => { | |||||
| // refetchData(filterObj); | |||||
| // }, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||||
| // const refetchData = async (filterObj: SearchQuery | null) => { | |||||
| // const authHeader = axiosInstance.defaults.headers['Authorization']; | |||||
| // if (!authHeader) { | |||||
| // return; // Exit the function if the token is not set | |||||
| // } | |||||
| // const params = { | |||||
| // pageNum: pagingController.pageNum, | |||||
| // pageSize: pagingController.pageSize, | |||||
| // ...filterObj, | |||||
| // ...tempSelectedValue, | |||||
| // } | |||||
| // try { | |||||
| // const response = await axiosInstance.get<ItemsResult[]>(`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { | |||||
| // params, | |||||
| // paramsSerializer: (params) => { | |||||
| // return Qs.stringify(params, { arrayFormat: 'repeat' }); | |||||
| // }, | |||||
| // }); | |||||
| // //setFilteredItems(response.data.records); | |||||
| // setFilteredItems([ | |||||
| // { | |||||
| // id: 1, | |||||
| // scheduledPeriod: "2025-05-11 to 2025-05-17", | |||||
| // scheduledAt: "2025-05-07", | |||||
| // productCount: 13, | |||||
| // }, | |||||
| // { | |||||
| // id: 2, | |||||
| // scheduledPeriod: "2025-05-18 to 2025-05-24", | |||||
| // scheduledAt: "2025-05-14", | |||||
| // productCount: 15, | |||||
| // }, | |||||
| // { | |||||
| // id: 3, | |||||
| // scheduledPeriod: "2025-05-25 to 2025-05-31", | |||||
| // scheduledAt: "2025-05-21", | |||||
| // productCount: 13, | |||||
| // }, | |||||
| // ]) | |||||
| // setPagingController({ | |||||
| // ...pagingController, | |||||
| // totalCount: response.data.total | |||||
| // }) | |||||
| // return response; // Return the data from the response | |||||
| // } catch (error) { | |||||
| // console.error('Error fetching items:', error); | |||||
| // throw error; // Rethrow the error for further handling | |||||
| // } | |||||
| // }; | |||||
| const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
| //setFilteredItems(items ?? []); | |||||
| // setFilterObj({}); | |||||
| // setTempSelectedValue({}); | |||||
| refetchData(defaultInputs, "reset"); | |||||
| }, []); | |||||
| const resetWithToday = { | |||||
| ...defaultInputs, | |||||
| produceAt: dayjs().format("YYYY-MM-DD"), | |||||
| }; | |||||
| setInputs(resetWithToday); // Update state | |||||
| refetchData(resetWithToday, "reset"); // Fetch data | |||||
| }, [defaultInputs, refetchData]); | |||||
| const testDetailedScheduleClick = useCallback(async () => { | const testDetailedScheduleClick = useCallback(async () => { | ||||
| try { | try { | ||||
| setIsUploading(true) | setIsUploading(true) | ||||
| const response = await testDetailedSchedule(inputs.scheduleAt) | const response = await testDetailedSchedule(inputs.scheduleAt) | ||||
| if (response) { | if (response) { | ||||
| refetchData(inputs, "paging"); | |||||
| //refetchData(inputs, "paging"); | |||||
| setPagingController(prev => ({ ...prev, pageNum: 1 })); | |||||
| refetchData(inputs, "search"); | |||||
| } | } | ||||
| } catch(e) { | } catch(e) { | ||||
| console.log(e) | console.log(e) | ||||
| @@ -304,7 +246,35 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| try { | try { | ||||
| const token = localStorage.getItem("accessToken"); | const token = localStorage.getItem("accessToken"); | ||||
| // 1. Get Base64 string from server | // 1. Get Base64 string from server | ||||
| const base64String = await exportProdSchedule(token); | |||||
| // 1. Prepare translated headers using the t() function | |||||
| const prodHeaders = [ | |||||
| t("Item Name"), | |||||
| t("Avg Qty Last Month"), | |||||
| t("Stock Qty"), | |||||
| t("Days Left"), | |||||
| t("Output Qty"), | |||||
| t("Batch Need"), | |||||
| t("Priority") | |||||
| ]; | |||||
| const matHeaders = [ | |||||
| t("Mat Code"), | |||||
| t("Mat Name"), | |||||
| t("Required Qty"), | |||||
| t("Total Qty Need"), | |||||
| t("UoM"), | |||||
| t("Purchased Qty"), | |||||
| t("On Hand Qty"), | |||||
| t("Unavailable Qty"), | |||||
| t("Related Item Code"), | |||||
| t("Related Item Name"), | |||||
| t("Material Summary") // The last one can be used as the Sheet Name | |||||
| ]; | |||||
| // 2. Pass these arrays to your server action | |||||
| // 'inputs' contains your filters (scheduleAt, types, etc.) | |||||
| const base64String = await exportProdSchedule(token, inputs, prodHeaders, matHeaders); | |||||
| // 2. Convert Base64 back to Blob | // 2. Convert Base64 back to Blob | ||||
| const byteCharacters = atob(base64String); | const byteCharacters = atob(base64String); | ||||
| @@ -366,14 +336,14 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| setInputs(() => ({ | |||||
| scheduleAt: query?.scheduleAt, | |||||
| schedulePeriod: query?.schedulePeriod, | |||||
| schedulePeriodTo: query?.schedulePeriodTo, | |||||
| const updatedInputs = { | |||||
| ...inputs, | |||||
| produceAt: query?.produceAt, // Ensure this matches paramName in searchCriteria | |||||
| totalEstProdCount: Number(query?.totalEstProdCount), | totalEstProdCount: Number(query?.totalEstProdCount), | ||||
| types: query.types as unknown as ScheduleType[], | types: query.types as unknown as ScheduleType[], | ||||
| })); | |||||
| refetchData(query, "search"); | |||||
| }; | |||||
| setInputs(updatedInputs); | |||||
| refetchData(updatedInputs, "search"); | |||||
| }} | }} | ||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| @@ -70,7 +70,7 @@ const DetailInfoCard: React.FC<Props> = ({ | |||||
| // {...register("scheduleAt", { | // {...register("scheduleAt", { | ||||
| // required: "Schedule At required!", | // required: "Schedule At required!", | ||||
| // })} | // })} | ||||
| defaultValue={`${arrayToDateString(getValues("scheduleAt"))}`} | |||||
| defaultValue={`${arrayToDateString(getValues("produceAt"))}`} | |||||
| // defaultValue={details?.scheduledPeriod} | // defaultValue={details?.scheduledPeriod} | ||||
| disabled={!isEditing} | disabled={!isEditing} | ||||
| // error={Boolean(errors.name)} | // error={Boolean(errors.name)} | ||||
| @@ -163,36 +163,55 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| }, [formProps, setIsUploading]) | }, [formProps, setIsUploading]) | ||||
| // --- NEW FUNCTION: GLOBAL RELEASE FOR THE ENTIRE SCHEDULE --- | // --- NEW FUNCTION: GLOBAL RELEASE FOR THE ENTIRE SCHEDULE --- | ||||
| // --- UPDATED FUNCTION: GLOBAL RELEASE WITH produceAt CHECK --- | |||||
| const onGlobalReleaseClick = useCallback(async () => { | const onGlobalReleaseClick = useCallback(async () => { | ||||
| if (!scheduleId) { | if (!scheduleId) { | ||||
| setServerError(t("Cannot release. Schedule ID is missing.")); | setServerError(t("Cannot release. Schedule ID is missing.")); | ||||
| return; | return; | ||||
| } | } | ||||
| // Optional: Add a confirmation dialog here before proceeding | |||||
| // 1. Get the production date (produceAt) from the form values | |||||
| const produceAtValue = getValues("produceAt"); | |||||
| if (produceAtValue) { | |||||
| // Use standard JS Date comparison for YYYY-MM-DD | |||||
| const today = new Date().toISOString().split('T')[0]; | |||||
| // Handle various potential formats (string or array from API) | |||||
| let scheduleDate = ""; | |||||
| if (Array.isArray(produceAtValue)) { | |||||
| // If it's an array format [YYYY, MM, DD] common in Java backends | |||||
| const [year, month, day] = produceAtValue; | |||||
| scheduleDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; | |||||
| } else { | |||||
| scheduleDate = new Date(produceAtValue).toISOString().split('T')[0]; | |||||
| } | |||||
| // 2. Alert and block if the date is not today | |||||
| if (scheduleDate !== today) { | |||||
| alert(t("Only schedules for today can be released.")); | |||||
| return; | |||||
| } | |||||
| } | |||||
| setIsUploading(true); | setIsUploading(true); | ||||
| setServerError(""); // Clear previous errors | |||||
| setServerError(""); | |||||
| try { | try { | ||||
| // **IMPORTANT**: Ensure 'releaseProdSchedule' is implemented in your actions file | |||||
| // to call the '/productionSchedule/detail/detailed/release' endpoint. | |||||
| const response = await releaseProdSchedule({ | const response = await releaseProdSchedule({ | ||||
| id: Number(scheduleId), | id: Number(scheduleId), | ||||
| }) | |||||
| }); | |||||
| if (response) { | if (response) { | ||||
| router.refresh(); | |||||
| router.refresh(); | |||||
| } | } | ||||
| } catch (e) { | } catch (e) { | ||||
| console.error(e); | console.error(e); | ||||
| setServerError(t("An unexpected error occurred during global schedule release.")); | setServerError(t("An unexpected error occurred during global schedule release.")); | ||||
| } finally { | } finally { | ||||
| setIsUploading(false); | setIsUploading(false); | ||||
| } | } | ||||
| }, [scheduleId, setIsUploading, t, router]); | |||||
| }, [scheduleId, setIsUploading, t, router, getValues]); | |||||
| // -------------------------------------------------------------------- | // -------------------------------------------------------------------- | ||||
| const [tempValue, setTempValue] = useState<string | number | null>(null) | const [tempValue, setTempValue] = useState<string | number | null>(null) | ||||
| @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import { EquipmentResult } from "@/app/api/settings/equipment"; | import { EquipmentResult } from "@/app/api/settings/equipment"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | |||||
| import EquipmentSearchResults, { Column } from "./EquipmentSearchResults"; | |||||
| import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | import { GridDeleteIcon } from "@mui/x-data-grid"; | ||||
| @@ -12,32 +12,90 @@ import { TypeEnum } from "@/app/utils/typeEnum"; | |||||
| import axios from "axios"; | import axios from "axios"; | ||||
| import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | import axiosInstance from "@/app/(main)/axios/axiosInstance"; | ||||
| import { arrayToDateTimeString } from "@/app/utils/formatUtil"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| type Props = { | type Props = { | ||||
| equipments: EquipmentResult[]; | equipments: EquipmentResult[]; | ||||
| tabIndex?: number; | |||||
| }; | }; | ||||
| type SearchQuery = Partial<Omit<EquipmentResult, "id">>; | type SearchQuery = Partial<Omit<EquipmentResult, "id">>; | ||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| const EquipmentSearch: React.FC<Props> = ({ equipments, tabIndex = 0 }) => { | |||||
| const [filteredEquipments, setFilteredEquipments] = | const [filteredEquipments, setFilteredEquipments] = | ||||
| useState<EquipmentResult[]>(equipments); | |||||
| useState<EquipmentResult[]>([]); | |||||
| const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [filterObj, setFilterObj] = useState({}); | const [filterObj, setFilterObj] = useState({}); | ||||
| const [pagingController, setPagingController] = useState({ | const [pagingController, setPagingController] = useState({ | ||||
| pageNum: 1, | pageNum: 1, | ||||
| pageSize: 10, | pageSize: 10, | ||||
| // totalCount: 0, | |||||
| }); | }); | ||||
| const [totalCount, setTotalCount] = useState(0); | const [totalCount, setTotalCount] = useState(0); | ||||
| const [isLoading, setIsLoading] = useState(true); | |||||
| const [isReady, setIsReady] = useState(false); | |||||
| useEffect(() => { | |||||
| const checkReady = () => { | |||||
| try { | |||||
| const token = localStorage.getItem("accessToken"); | |||||
| const hasAuthHeader = axiosInstance.defaults.headers?.common?.Authorization || | |||||
| axiosInstance.defaults.headers?.Authorization; | |||||
| if (token && hasAuthHeader) { | |||||
| setIsReady(true); | |||||
| } else if (token) { | |||||
| setTimeout(checkReady, 50); | |||||
| } else { | |||||
| setTimeout(checkReady, 100); | |||||
| } | |||||
| } catch (e) { | |||||
| console.warn("localStorage unavailable", e); | |||||
| } | |||||
| }; | |||||
| const timer = setTimeout(checkReady, 100); | |||||
| return () => clearTimeout(timer); | |||||
| }, []); | |||||
| const displayDateTime = useCallback((dateValue: string | Date | number[] | null | undefined): string => { | |||||
| if (!dateValue) return "-"; | |||||
| if (Array.isArray(dateValue)) { | |||||
| return arrayToDateTimeString(dateValue); | |||||
| } | |||||
| if (typeof dateValue === "string") { | |||||
| return dateValue; | |||||
| } | |||||
| return String(dateValue); | |||||
| }, []); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | ||||
| const searchCriteria: Criterion<SearchParamNames>[] = [ | |||||
| if (tabIndex === 1) { | |||||
| return [ | |||||
| { | |||||
| label: "設備名稱/設備編號", | |||||
| paramName: "equipmentCode", | |||||
| type: "text" | |||||
| }, | |||||
| { | |||||
| label: t("Repair and Maintenance Status"), | |||||
| paramName: "repairAndMaintenanceStatus", | |||||
| type: "select", | |||||
| options: ["正常使用中", "正在維護中"] | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| return [ | |||||
| { label: t("Code"), paramName: "code", type: "text" }, | { label: t("Code"), paramName: "code", type: "text" }, | ||||
| { label: t("Description"), paramName: "description", type: "text" }, | { label: t("Description"), paramName: "description", type: "text" }, | ||||
| ]; | ]; | ||||
| return searchCriteria; | |||||
| }, [t, equipments]); | |||||
| }, [t, tabIndex]); | |||||
| const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
| (equipment: EquipmentResult) => { | (equipment: EquipmentResult) => { | ||||
| @@ -46,12 +104,19 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| [router], | [router], | ||||
| ); | ); | ||||
| const onMaintenanceEditClick = useCallback( | |||||
| (equipment: EquipmentResult) => { | |||||
| router.push(`/settings/equipment/MaintenanceEdit?id=${equipment.id}`); | |||||
| }, | |||||
| [router], | |||||
| ); | |||||
| const onDeleteClick = useCallback( | const onDeleteClick = useCallback( | ||||
| (equipment: EquipmentResult) => {}, | (equipment: EquipmentResult) => {}, | ||||
| [router], | [router], | ||||
| ); | ); | ||||
| const columns = useMemo<Column<EquipmentResult>[]>( | |||||
| const generalDataColumns = useMemo<Column<EquipmentResult>[]>( | |||||
| () => [ | () => [ | ||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| @@ -78,9 +143,91 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| onClick: onDeleteClick, | onClick: onDeleteClick, | ||||
| }, | }, | ||||
| ], | ], | ||||
| [filteredEquipments], | |||||
| [onDetailClick, onDeleteClick, t], | |||||
| ); | |||||
| const repairMaintenanceColumns = useMemo<Column<EquipmentResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "id", | |||||
| label: "編輯", | |||||
| onClick: onMaintenanceEditClick, | |||||
| buttonIcon: <EditNote />, | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "60px", minWidth: "60px" }, | |||||
| }, | |||||
| { | |||||
| name: "code", | |||||
| label: "設備名稱", | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "200px", minWidth: "200px" }, | |||||
| }, | |||||
| { | |||||
| name: "equipmentCode", | |||||
| label: "設備編號", | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "150px", minWidth: "150px" }, | |||||
| renderCell: (item) => { | |||||
| return item.equipmentCode || "-"; | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "repairAndMaintenanceStatus", | |||||
| label: t("Repair and Maintenance Status"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "150px", minWidth: "150px" }, | |||||
| renderCell: (item) => { | |||||
| const status = item.repairAndMaintenanceStatus; | |||||
| if (status === 1 || status === true) { | |||||
| return ( | |||||
| <Typography sx={{ color: "red", fontWeight: 500 }}> | |||||
| 正在維護中 | |||||
| </Typography> | |||||
| ); | |||||
| } else if (status === 0 || status === false) { | |||||
| return ( | |||||
| <Typography sx={{ color: "green", fontWeight: 500 }}> | |||||
| 正常使用中 | |||||
| </Typography> | |||||
| ); | |||||
| } | |||||
| return "-"; | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "latestRepairAndMaintenanceDate", | |||||
| label: t("Latest Repair and Maintenance Date"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "200px", minWidth: "200px" }, | |||||
| renderCell: (item) => displayDateTime(item.latestRepairAndMaintenanceDate), | |||||
| }, | |||||
| { | |||||
| name: "lastRepairAndMaintenanceDate", | |||||
| label: t("Last Repair and Maintenance Date"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "200px", minWidth: "200px" }, | |||||
| renderCell: (item) => displayDateTime(item.lastRepairAndMaintenanceDate), | |||||
| }, | |||||
| { | |||||
| name: "repairAndMaintenanceRemarks", | |||||
| label: t("Repair and Maintenance Remarks"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "200px", minWidth: "200px" }, | |||||
| }, | |||||
| ], | |||||
| [onMaintenanceEditClick, t, displayDateTime], | |||||
| ); | ); | ||||
| const columns = useMemo(() => { | |||||
| return tabIndex === 1 ? repairMaintenanceColumns : generalDataColumns; | |||||
| }, [tabIndex, repairMaintenanceColumns, generalDataColumns]); | |||||
| interface ApiResponse<T> { | interface ApiResponse<T> { | ||||
| records: T[]; | records: T[]; | ||||
| @@ -89,73 +236,115 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| const refetchData = useCallback( | const refetchData = useCallback( | ||||
| async (filterObj: SearchQuery) => { | async (filterObj: SearchQuery) => { | ||||
| const authHeader = axiosInstance.defaults.headers["Authorization"]; | |||||
| if (!authHeader) { | |||||
| const token = localStorage.getItem("accessToken"); | |||||
| const hasAuthHeader = axiosInstance.defaults.headers?.common?.Authorization || | |||||
| axiosInstance.defaults.headers?.Authorization; | |||||
| if (!token || !hasAuthHeader) { | |||||
| console.warn("Token or auth header not ready, skipping API call"); | |||||
| setIsLoading(false); | |||||
| return; | return; | ||||
| } | } | ||||
| setIsLoading(true); | |||||
| const transformedFilter: any = { ...filterObj }; | |||||
| // For maintenance tab (tabIndex === 1), if equipmentCode is provided, | |||||
| // also search by code (equipment name) with the same value | |||||
| if (tabIndex === 1 && transformedFilter.equipmentCode) { | |||||
| transformedFilter.code = transformedFilter.equipmentCode; | |||||
| } | |||||
| if (transformedFilter.repairAndMaintenanceStatus) { | |||||
| if (transformedFilter.repairAndMaintenanceStatus === "正常使用中") { | |||||
| transformedFilter.repairAndMaintenanceStatus = false; | |||||
| } else if (transformedFilter.repairAndMaintenanceStatus === "正在維護中") { | |||||
| transformedFilter.repairAndMaintenanceStatus = true; | |||||
| } else if (transformedFilter.repairAndMaintenanceStatus === "All") { | |||||
| delete transformedFilter.repairAndMaintenanceStatus; | |||||
| } | |||||
| } | |||||
| const params = { | const params = { | ||||
| pageNum: pagingController.pageNum, | pageNum: pagingController.pageNum, | ||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| ...filterObj, | |||||
| ...transformedFilter, | |||||
| }; | }; | ||||
| try { | try { | ||||
| const endpoint = tabIndex === 1 | |||||
| ? `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage` | |||||
| : `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`; | |||||
| const response = await axiosInstance.get<ApiResponse<EquipmentResult>>( | const response = await axiosInstance.get<ApiResponse<EquipmentResult>>( | ||||
| `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`, | |||||
| endpoint, | |||||
| { params }, | { params }, | ||||
| ); | ); | ||||
| console.log(response); | |||||
| console.log("API Response:", response); | |||||
| console.log("Records:", response.data.records); | |||||
| console.log("Total:", response.data.total); | |||||
| if (response.status == 200) { | if (response.status == 200) { | ||||
| setFilteredEquipments(response.data.records); | |||||
| setTotalCount(response.data.total); | |||||
| return response; | |||||
| setFilteredEquipments(response.data.records || []); | |||||
| setTotalCount(response.data.total || 0); | |||||
| } else { | } else { | ||||
| throw "400"; | throw "400"; | ||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error fetching equipment types:", error); | console.error("Error fetching equipment types:", error); | ||||
| throw error; | |||||
| setFilteredEquipments([]); | |||||
| setTotalCount(0); | |||||
| } finally { | |||||
| setIsLoading(false); | |||||
| } | } | ||||
| }, | }, | ||||
| [axiosInstance, pagingController.pageNum, pagingController.pageSize], | |||||
| [pagingController.pageNum, pagingController.pageSize, tabIndex], | |||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| refetchData(filterObj); | |||||
| }, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||||
| if (isReady) { | |||||
| refetchData(filterObj); | |||||
| } | |||||
| }, [filterObj, pagingController.pageNum, pagingController.pageSize, tabIndex, isReady, refetchData]); | |||||
| const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
| setFilteredEquipments(equipments); | |||||
| }, [equipments]); | |||||
| setFilterObj({}); | |||||
| setPagingController({ | |||||
| pageNum: 1, | |||||
| pageSize: pagingController.pageSize, | |||||
| }); | |||||
| }, [pagingController.pageSize]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| // setFilteredItems( | |||||
| // equipmentTypes.filter((pm) => { | |||||
| // return ( | |||||
| // pm.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| // pm.name.toLowerCase().includes(query.name.toLowerCase()) | |||||
| // ); | |||||
| // }) | |||||
| // ); | |||||
| setFilterObj({ | setFilterObj({ | ||||
| ...query, | ...query, | ||||
| }); | }); | ||||
| }} | }} | ||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| <SearchResults<EquipmentResult> | |||||
| items={filteredEquipments} | |||||
| columns={columns} | |||||
| setPagingController={setPagingController} | |||||
| pagingController={pagingController} | |||||
| totalCount={totalCount} | |||||
| isAutoPaging={false} | |||||
| /> | |||||
| <Box sx={{ | |||||
| "& .MuiTableContainer-root": { | |||||
| overflowY: "auto", | |||||
| "&::-webkit-scrollbar": { | |||||
| width: "17px" | |||||
| } | |||||
| } | |||||
| }}> | |||||
| <EquipmentSearchResults<EquipmentResult> | |||||
| items={filteredEquipments} | |||||
| columns={columns} | |||||
| setPagingController={setPagingController} | |||||
| pagingController={pagingController} | |||||
| totalCount={totalCount} | |||||
| isAutoPaging={false} | |||||
| /> | |||||
| </Box> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default EquipmentSearch; | |||||
| export default EquipmentSearch; | |||||
| @@ -0,0 +1,482 @@ | |||||
| "use client"; | |||||
| import React, { | |||||
| ChangeEvent, | |||||
| Dispatch, | |||||
| MouseEvent, | |||||
| SetStateAction, | |||||
| useCallback, | |||||
| useMemo, | |||||
| useState, | |||||
| } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import Paper from "@mui/material/Paper"; | |||||
| import Table from "@mui/material/Table"; | |||||
| import TableBody from "@mui/material/TableBody"; | |||||
| import TableCell, { TableCellProps } from "@mui/material/TableCell"; | |||||
| import TableContainer from "@mui/material/TableContainer"; | |||||
| import TableHead from "@mui/material/TableHead"; | |||||
| import TablePagination, { | |||||
| TablePaginationProps, | |||||
| } from "@mui/material/TablePagination"; | |||||
| import TableRow from "@mui/material/TableRow"; | |||||
| import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton"; | |||||
| import { | |||||
| ButtonOwnProps, | |||||
| Checkbox, | |||||
| Icon, | |||||
| IconOwnProps, | |||||
| SxProps, | |||||
| Theme, | |||||
| } from "@mui/material"; | |||||
| import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; | |||||
| import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | |||||
| import { filter, remove, uniq } from "lodash"; | |||||
| export interface ResultWithId { | |||||
| id: string | number; | |||||
| } | |||||
| type ColumnType = "icon" | "decimal" | "integer" | "checkbox"; | |||||
| interface BaseColumn<T extends ResultWithId> { | |||||
| name: keyof T; | |||||
| label: string; | |||||
| align?: TableCellProps["align"]; | |||||
| headerAlign?: TableCellProps["align"]; | |||||
| sx?: SxProps<Theme> | undefined; | |||||
| style?: Partial<HTMLElement["style"]> & { [propName: string]: string }; | |||||
| type?: ColumnType; | |||||
| renderCell?: (params: T) => React.ReactNode; | |||||
| } | |||||
| interface IconColumn<T extends ResultWithId> extends BaseColumn<T> { | |||||
| name: keyof T; | |||||
| type: "icon"; | |||||
| icon?: React.ReactNode; | |||||
| icons?: { [columnValue in keyof T]: React.ReactNode }; | |||||
| color?: IconOwnProps["color"]; | |||||
| colors?: { [columnValue in keyof T]: IconOwnProps["color"] }; | |||||
| } | |||||
| interface DecimalColumn<T extends ResultWithId> extends BaseColumn<T> { | |||||
| type: "decimal"; | |||||
| } | |||||
| interface IntegerColumn<T extends ResultWithId> extends BaseColumn<T> { | |||||
| type: "integer"; | |||||
| } | |||||
| interface CheckboxColumn<T extends ResultWithId> extends BaseColumn<T> { | |||||
| type: "checkbox"; | |||||
| disabled?: (params: T) => boolean; | |||||
| // checkboxIds: readonly (string | number)[], | |||||
| // setCheckboxIds: (ids: readonly (string | number)[]) => void | |||||
| } | |||||
| interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||||
| onClick: (item: T) => void; | |||||
| buttonIcon: React.ReactNode; | |||||
| buttonIcons: { [columnValue in keyof T]: React.ReactNode }; | |||||
| buttonColor?: IconButtonOwnProps["color"]; | |||||
| } | |||||
| export type Column<T extends ResultWithId> = | |||||
| | BaseColumn<T> | |||||
| | IconColumn<T> | |||||
| | DecimalColumn<T> | |||||
| | CheckboxColumn<T> | |||||
| | ColumnWithAction<T>; | |||||
| interface Props<T extends ResultWithId> { | |||||
| totalCount?: number; | |||||
| items: T[]; | |||||
| columns: Column<T>[]; | |||||
| noWrapper?: boolean; | |||||
| setPagingController?: Dispatch< | |||||
| SetStateAction<{ | |||||
| pageNum: number; | |||||
| pageSize: number; | |||||
| }> | |||||
| >; | |||||
| pagingController?: { pageNum: number; pageSize: number }; | |||||
| isAutoPaging?: boolean; | |||||
| checkboxIds?: (string | number)[]; | |||||
| setCheckboxIds?: Dispatch<SetStateAction<(string | number)[]>>; | |||||
| onRowClick?: (item: T) => void; | |||||
| } | |||||
| function isActionColumn<T extends ResultWithId>( | |||||
| column: Column<T>, | |||||
| ): column is ColumnWithAction<T> { | |||||
| return Boolean((column as ColumnWithAction<T>).onClick); | |||||
| } | |||||
| function isIconColumn<T extends ResultWithId>( | |||||
| column: Column<T>, | |||||
| ): column is IconColumn<T> { | |||||
| return column.type === "icon"; | |||||
| } | |||||
| function isDecimalColumn<T extends ResultWithId>( | |||||
| column: Column<T>, | |||||
| ): column is DecimalColumn<T> { | |||||
| return column.type === "decimal"; | |||||
| } | |||||
| function isIntegerColumn<T extends ResultWithId>( | |||||
| column: Column<T>, | |||||
| ): column is IntegerColumn<T> { | |||||
| return column.type === "integer"; | |||||
| } | |||||
| function isCheckboxColumn<T extends ResultWithId>( | |||||
| column: Column<T>, | |||||
| ): column is CheckboxColumn<T> { | |||||
| return column.type === "checkbox"; | |||||
| } | |||||
| // Icon Component Functions | |||||
| function convertObjectKeysToLowercase<T extends object>( | |||||
| obj: T, | |||||
| ): object | undefined { | |||||
| return obj | |||||
| ? Object.fromEntries( | |||||
| Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), | |||||
| ) | |||||
| : undefined; | |||||
| } | |||||
| function handleIconColors<T extends ResultWithId>( | |||||
| column: IconColumn<T>, | |||||
| value: T[keyof T], | |||||
| ): IconOwnProps["color"] { | |||||
| const colors = convertObjectKeysToLowercase(column.colors ?? {}); | |||||
| const valueKey = String(value).toLowerCase() as keyof typeof colors; | |||||
| if (colors && valueKey in colors) { | |||||
| return colors[valueKey]; | |||||
| } | |||||
| return column.color ?? "primary"; | |||||
| } | |||||
| function handleIconIcons<T extends ResultWithId>( | |||||
| column: IconColumn<T>, | |||||
| value: T[keyof T], | |||||
| ): React.ReactNode { | |||||
| const icons = convertObjectKeysToLowercase(column.icons ?? {}); | |||||
| const valueKey = String(value).toLowerCase() as keyof typeof icons; | |||||
| if (icons && valueKey in icons) { | |||||
| return icons[valueKey]; | |||||
| } | |||||
| return column.icon ?? <CheckCircleOutlineIcon fontSize="small" />; | |||||
| } | |||||
| export const defaultPagingController: { pageNum: number; pageSize: number } = { | |||||
| pageNum: 1, | |||||
| pageSize: 10, | |||||
| }; | |||||
| export type defaultSetPagingController = Dispatch< | |||||
| SetStateAction<{ | |||||
| pageNum: number; | |||||
| pageSize: number; | |||||
| }> | |||||
| > | |||||
| function EquipmentSearchResults<T extends ResultWithId>({ | |||||
| items, | |||||
| columns, | |||||
| noWrapper, | |||||
| pagingController, | |||||
| setPagingController, | |||||
| isAutoPaging = true, | |||||
| totalCount, | |||||
| checkboxIds = [], | |||||
| setCheckboxIds = undefined, | |||||
| onRowClick = undefined, | |||||
| }: Props<T>) { | |||||
| const { t } = useTranslation("common"); | |||||
| const [page, setPage] = React.useState(0); | |||||
| const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||||
| /// this | |||||
| const handleChangePage: TablePaginationProps["onPageChange"] = ( | |||||
| _event, | |||||
| newPage, | |||||
| ) => { | |||||
| console.log(_event); | |||||
| setPage(newPage); | |||||
| if (setPagingController) { | |||||
| setPagingController({ | |||||
| ...(pagingController ?? defaultPagingController), | |||||
| pageNum: newPage + 1, | |||||
| }); | |||||
| } | |||||
| }; | |||||
| const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( | |||||
| event, | |||||
| ) => { | |||||
| console.log(event); | |||||
| const newSize = +event.target.value; | |||||
| setRowsPerPage(newSize); | |||||
| setPage(0); | |||||
| if (setPagingController) { | |||||
| setPagingController({ | |||||
| ...(pagingController ?? defaultPagingController), | |||||
| pageNum: 1, | |||||
| pageSize: newSize, | |||||
| }); | |||||
| } | |||||
| }; | |||||
| // checkbox | |||||
| const currItems = useMemo(() => { | |||||
| return items.length > 10 ? items | |||||
| .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||||
| .map((i) => i.id) | |||||
| : items.map((i) => i.id) | |||||
| }, [items, page, rowsPerPage]) | |||||
| const currItemsWithChecked = useMemo(() => { | |||||
| return filter(checkboxIds, function (c) { | |||||
| return currItems.includes(c); | |||||
| }) | |||||
| }, [checkboxIds, items, page, rowsPerPage]) | |||||
| const handleRowClick = useCallback( | |||||
| (event: MouseEvent<unknown>, item: T, columns: Column<T>[]) => { | |||||
| // check is disabled or not | |||||
| let disabled = false; | |||||
| columns.forEach((col) => { | |||||
| if (isCheckboxColumn(col) && col.disabled) { | |||||
| disabled = col.disabled(item); | |||||
| if (disabled) { | |||||
| return; | |||||
| } | |||||
| } | |||||
| }); | |||||
| if (disabled) { | |||||
| return; | |||||
| } | |||||
| // set id | |||||
| const id = item.id; | |||||
| if (setCheckboxIds) { | |||||
| const selectedIndex = checkboxIds.indexOf(id); | |||||
| let newSelected: (string | number)[] = []; | |||||
| if (selectedIndex === -1) { | |||||
| newSelected = newSelected.concat(checkboxIds, id); | |||||
| } else if (selectedIndex === 0) { | |||||
| newSelected = newSelected.concat(checkboxIds.slice(1)); | |||||
| } else if (selectedIndex === checkboxIds.length - 1) { | |||||
| newSelected = newSelected.concat(checkboxIds.slice(0, -1)); | |||||
| } else if (selectedIndex > 0) { | |||||
| newSelected = newSelected.concat( | |||||
| checkboxIds.slice(0, selectedIndex), | |||||
| checkboxIds.slice(selectedIndex + 1), | |||||
| ); | |||||
| } | |||||
| setCheckboxIds(newSelected); | |||||
| } | |||||
| }, | |||||
| [checkboxIds, setCheckboxIds], | |||||
| ); | |||||
| const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => { | |||||
| if (setCheckboxIds) { | |||||
| const pageItemId = currItems | |||||
| if (event.target.checked) { | |||||
| setCheckboxIds((prev) => uniq([...prev, ...pageItemId])) | |||||
| } else { | |||||
| setCheckboxIds((prev) => filter(prev, function (p) { return !pageItemId.includes(p); })) | |||||
| } | |||||
| } | |||||
| } | |||||
| const table = ( | |||||
| <> | |||||
| <TableContainer sx={{ maxHeight: 440 }}> | |||||
| <Table stickyHeader> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| {columns.map((column, idx) => ( | |||||
| isCheckboxColumn(column) ? | |||||
| <TableCell | |||||
| align={column.headerAlign} | |||||
| sx={column.sx} | |||||
| key={`${column.name.toString()}${idx}`} | |||||
| > | |||||
| <Checkbox | |||||
| color="primary" | |||||
| indeterminate={currItemsWithChecked.length > 0 && currItemsWithChecked.length < currItems.length} | |||||
| checked={currItems.length > 0 && currItemsWithChecked.length >= currItems.length} | |||||
| onChange={handleSelectAllClick} | |||||
| /> | |||||
| </TableCell> | |||||
| : <TableCell | |||||
| align={column.headerAlign} | |||||
| sx={column.sx} | |||||
| key={`${column.name.toString()}${idx}`} | |||||
| > | |||||
| {column.label.split('\n').map((line, index) => ( | |||||
| <div key={index}>{line}</div> // Render each line in a div | |||||
| ))} | |||||
| </TableCell> | |||||
| ))} | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {isAutoPaging | |||||
| ? items | |||||
| .slice((pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage), | |||||
| (pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage) + (pagingController?.pageSize ?? rowsPerPage)) | |||||
| .map((item) => { | |||||
| return ( | |||||
| <TableRow | |||||
| hover | |||||
| tabIndex={-1} | |||||
| key={item.id} | |||||
| onClick={(event) => { | |||||
| setCheckboxIds | |||||
| ? handleRowClick(event, item, columns) | |||||
| : undefined | |||||
| if (onRowClick) { | |||||
| onRowClick(item) | |||||
| } | |||||
| } | |||||
| } | |||||
| role={setCheckboxIds ? "checkbox" : undefined} | |||||
| > | |||||
| {columns.map((column, idx) => { | |||||
| const columnName = column.name; | |||||
| return ( | |||||
| <TabelCells | |||||
| key={`${columnName.toString()}-${idx}`} | |||||
| column={column} | |||||
| columnName={columnName} | |||||
| idx={idx} | |||||
| item={item} | |||||
| checkboxIds={checkboxIds} | |||||
| /> | |||||
| ); | |||||
| })} | |||||
| </TableRow> | |||||
| ); | |||||
| }) | |||||
| : items.map((item) => { | |||||
| return ( | |||||
| <TableRow hover tabIndex={-1} key={item.id} | |||||
| onClick={(event) => { | |||||
| setCheckboxIds | |||||
| ? handleRowClick(event, item, columns) | |||||
| : undefined | |||||
| if (onRowClick) { | |||||
| onRowClick(item) | |||||
| } | |||||
| } | |||||
| } | |||||
| role={setCheckboxIds ? "checkbox" : undefined} | |||||
| > | |||||
| {columns.map((column, idx) => { | |||||
| const columnName = column.name; | |||||
| return ( | |||||
| <TabelCells | |||||
| key={`${columnName.toString()}-${idx}`} | |||||
| column={column} | |||||
| columnName={columnName} | |||||
| idx={idx} | |||||
| item={item} | |||||
| checkboxIds={checkboxIds} | |||||
| /> | |||||
| ); | |||||
| })} | |||||
| </TableRow> | |||||
| ); | |||||
| })} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| rowsPerPageOptions={[10, 25, 100]} | |||||
| component="div" | |||||
| count={!totalCount || totalCount == 0 ? items.length : totalCount} | |||||
| rowsPerPage={pagingController?.pageSize ? pagingController?.pageSize : rowsPerPage} | |||||
| page={pagingController?.pageNum ? pagingController?.pageNum - 1 : page} | |||||
| onPageChange={handleChangePage} | |||||
| onRowsPerPageChange={handleChangeRowsPerPage} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>; | |||||
| } | |||||
| // Table cells | |||||
| interface TableCellsProps<T extends ResultWithId> { | |||||
| column: Column<T>; | |||||
| columnName: keyof T; | |||||
| idx: number; | |||||
| item: T; | |||||
| checkboxIds: (string | number)[]; | |||||
| } | |||||
| function TabelCells<T extends ResultWithId>({ | |||||
| column, | |||||
| columnName, | |||||
| idx, | |||||
| item, | |||||
| checkboxIds = [], | |||||
| }: TableCellsProps<T>) { | |||||
| const isItemSelected = checkboxIds.includes(item.id); | |||||
| return ( | |||||
| <TableCell | |||||
| align={column.align} | |||||
| sx={column.sx} | |||||
| key={`${columnName.toString()}-${idx}`} | |||||
| > | |||||
| {isActionColumn(column) ? ( | |||||
| <IconButton | |||||
| color={column.buttonColor ?? "primary"} | |||||
| onClick={() => column.onClick(item)} | |||||
| > | |||||
| {column.buttonIcon} | |||||
| </IconButton> | |||||
| ) : isIconColumn(column) ? ( | |||||
| <Icon color={handleIconColors(column, item[columnName])}> | |||||
| {handleIconIcons(column, item[columnName])} | |||||
| </Icon> | |||||
| ) : isDecimalColumn(column) ? ( | |||||
| <>{decimalFormatter.format(Number(item[columnName]))}</> | |||||
| ) : isIntegerColumn(column) ? ( | |||||
| <>{integerFormatter.format(Number(item[columnName]))}</> | |||||
| ) : isCheckboxColumn(column) ? ( | |||||
| <Checkbox | |||||
| disabled={column.disabled ? column.disabled(item) : undefined} | |||||
| checked={isItemSelected} | |||||
| /> | |||||
| ) : column.renderCell ? ( | |||||
| column.renderCell(item) | |||||
| ) : ( | |||||
| <>{item[columnName] as string}</> | |||||
| )} | |||||
| </TableCell> | |||||
| ); | |||||
| } | |||||
| export default EquipmentSearchResults; | |||||
| @@ -1,28 +1,35 @@ | |||||
| import { fetchAllEquipments } from "@/app/api/settings/equipment"; | |||||
| import EquipmentSearchLoading from "./EquipmentSearchLoading"; | |||||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||||
| import { notFound } from "next/navigation"; | |||||
| "use client"; | |||||
| import { useState, useEffect } from "react"; | |||||
| import EquipmentSearch from "./EquipmentSearch"; | import EquipmentSearch from "./EquipmentSearch"; | ||||
| import EquipmentSearchLoading from "./EquipmentSearchLoading"; | |||||
| import EquipmentTabs from "@/app/(main)/settings/equipment/EquipmentTabs"; | |||||
| import { useSearchParams } from "next/navigation"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof EquipmentSearchLoading; | Loading: typeof EquipmentSearchLoading; | ||||
| } | } | ||||
| type Props = { | |||||
| // type: TypeEnum; | |||||
| }; | |||||
| const EquipmentSearchWrapper: React.FC & SubComponents = () => { | |||||
| const searchParams = useSearchParams(); | |||||
| const tabFromUrl = searchParams.get("tab"); | |||||
| const initialTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; | |||||
| const [tabIndex, setTabIndex] = useState(initialTabIndex); | |||||
| useEffect(() => { | |||||
| const tabFromUrl = searchParams.get("tab"); | |||||
| const newTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0; | |||||
| setTabIndex(newTabIndex); | |||||
| }, [searchParams]); | |||||
| const EquipmentSearchWrapper: React.FC<Props> & SubComponents = async ( | |||||
| { | |||||
| // type, | |||||
| }, | |||||
| ) => { | |||||
| // console.log(type) | |||||
| // var result = await fetchAllEquipmentTypes() | |||||
| return <EquipmentSearch equipments={[]} />; | |||||
| return ( | |||||
| <> | |||||
| <EquipmentTabs onTabChange={setTabIndex} /> | |||||
| <EquipmentSearch equipments={[]} tabIndex={tabIndex} /> | |||||
| </> | |||||
| ); | |||||
| }; | }; | ||||
| EquipmentSearchWrapper.Loading = EquipmentSearchLoading; | EquipmentSearchWrapper.Loading = EquipmentSearchLoading; | ||||
| export default EquipmentSearchWrapper; | |||||
| export default EquipmentSearchWrapper; | |||||
| @@ -8,6 +8,7 @@ import SearchResults, { Column } from "../SearchResults"; | |||||
| import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | import { GridDeleteIcon } from "@mui/x-data-grid"; | ||||
| import { Chip } from "@mui/material"; | |||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import axios from "axios"; | import axios from "axios"; | ||||
| import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| @@ -19,6 +20,10 @@ type Props = { | |||||
| type SearchQuery = Partial<Omit<ItemsResult, "id">>; | type SearchQuery = Partial<Omit<ItemsResult, "id">>; | ||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| type ItemsResultWithStatus = ItemsResult & { | |||||
| status?: "complete" | "missing"; | |||||
| }; | |||||
| const ItemsSearch: React.FC<Props> = ({ items }) => { | const ItemsSearch: React.FC<Props> = ({ items }) => { | ||||
| const [filteredItems, setFilteredItems] = useState<ItemsResult[]>(items); | const [filteredItems, setFilteredItems] = useState<ItemsResult[]>(items); | ||||
| const { t } = useTranslation("items"); | const { t } = useTranslation("items"); | ||||
| @@ -47,7 +52,31 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| const onDeleteClick = useCallback((item: ItemsResult) => {}, [router]); | const onDeleteClick = useCallback((item: ItemsResult) => {}, [router]); | ||||
| const columns = useMemo<Column<ItemsResult>[]>( | |||||
| const checkItemStatus = useCallback((item: ItemsResult): "complete" | "missing" => { | |||||
| // Check if type exists and is not empty | |||||
| const hasType = item.type != null && String(item.type).trim() !== ""; | |||||
| // Check if qcCategory exists (can be object or id) - handle case sensitivity | |||||
| const itemAny = item as any; | |||||
| const hasQcCategory = item.qcCategory != null || | |||||
| itemAny.qcCategoryId != null || | |||||
| itemAny.qcCategoryid != null || | |||||
| itemAny.qccategoryid != null; | |||||
| // Check if LocationCode exists and is not empty - handle case sensitivity | |||||
| const hasLocationCode = (item.LocationCode != null && String(item.LocationCode).trim() !== "") || | |||||
| (itemAny.LocationCode != null && String(itemAny.LocationCode).trim() !== "") || | |||||
| (itemAny.locationCode != null && String(itemAny.locationCode).trim() !== "") || | |||||
| (itemAny.locationcode != null && String(itemAny.locationcode).trim() !== ""); | |||||
| // If all three are present, return "complete", otherwise "missing" | |||||
| if (hasType && hasQcCategory && hasLocationCode) { | |||||
| return "complete"; | |||||
| } | |||||
| return "missing"; | |||||
| }, []); | |||||
| const columns = useMemo<Column<ItemsResultWithStatus>[]>( | |||||
| () => [ | () => [ | ||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| @@ -63,6 +92,22 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| name: "name", | name: "name", | ||||
| label: t("Name"), | label: t("Name"), | ||||
| }, | }, | ||||
| { | |||||
| name: "type", | |||||
| label: t("Type"), | |||||
| }, | |||||
| { | |||||
| name: "status", | |||||
| label: t("Status"), | |||||
| renderCell: (item) => { | |||||
| const status = item.status || checkItemStatus(item); | |||||
| if (status === "complete") { | |||||
| return <Chip label={t("Complete")} color="success" size="small" />; | |||||
| } else { | |||||
| return <Chip label={t("Missing Data")} color="warning" size="small" />; | |||||
| } | |||||
| }, | |||||
| }, | |||||
| { | { | ||||
| name: "action", | name: "action", | ||||
| label: t(""), | label: t(""), | ||||
| @@ -70,7 +115,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| onClick: onDeleteClick, | onClick: onDeleteClick, | ||||
| }, | }, | ||||
| ], | ], | ||||
| [onDeleteClick, onDetailClick, t], | |||||
| [onDeleteClick, onDetailClick, t, checkItemStatus], | |||||
| ); | ); | ||||
| const refetchData = useCallback( | const refetchData = useCallback( | ||||
| @@ -89,9 +134,35 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| `${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, | `${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, | ||||
| { params }, | { params }, | ||||
| ); | ); | ||||
| console.log(response); | |||||
| console.log("API Response:", response); | |||||
| console.log("First record keys:", response.data?.records?.[0] ? Object.keys(response.data.records[0]) : "No records"); | |||||
| if (response.status == 200) { | if (response.status == 200) { | ||||
| setFilteredItems(response.data.records); | |||||
| // Normalize field names and add status to each item | |||||
| const itemsWithStatus: ItemsResultWithStatus[] = response.data.records.map((item: any) => { | |||||
| // Normalize field names (handle case sensitivity from MySQL) | |||||
| // Check all possible case variations | |||||
| const locationCode = item.LocationCode || item.locationCode || item.locationcode || item.Locationcode || item.Location_Code || item.location_code; | |||||
| const qcCategoryId = item.qcCategoryId || item.qcCategoryid || item.qccategoryid || item.QcCategoryId || item.qc_category_id; | |||||
| const normalizedItem: ItemsResult = { | |||||
| ...item, | |||||
| LocationCode: locationCode, | |||||
| qcCategory: item.qcCategory || (qcCategoryId ? { id: qcCategoryId } : undefined), | |||||
| }; | |||||
| console.log("Normalized item:", { | |||||
| id: normalizedItem.id, | |||||
| LocationCode: normalizedItem.LocationCode, | |||||
| qcCategoryId: qcCategoryId, | |||||
| qcCategory: normalizedItem.qcCategory | |||||
| }); | |||||
| return { | |||||
| ...normalizedItem, | |||||
| status: checkItemStatus(normalizedItem), | |||||
| }; | |||||
| }); | |||||
| setFilteredItems(itemsWithStatus as ItemsResult[]); | |||||
| setTotalCount(response.data.total); | setTotalCount(response.data.total); | ||||
| return response; // Return the data from the response | return response; // Return the data from the response | ||||
| } else { | } else { | ||||
| @@ -102,7 +173,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| throw error; // Rethrow the error for further handling | throw error; // Rethrow the error for further handling | ||||
| } | } | ||||
| }, | }, | ||||
| [pagingController.pageNum, pagingController.pageSize], | |||||
| [pagingController.pageNum, pagingController.pageSize, checkItemStatus], | |||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -137,8 +208,8 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| }} | }} | ||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| <SearchResults<ItemsResult> | |||||
| items={filteredItems} | |||||
| <SearchResults<ItemsResultWithStatus> | |||||
| items={filteredItems as ItemsResultWithStatus[]} | |||||
| columns={columns} | columns={columns} | ||||
| setPagingController={setPagingController} | setPagingController={setPagingController} | ||||
| pagingController={pagingController} | pagingController={pagingController} | ||||
| @@ -276,7 +276,7 @@ const NavigationContent: React.FC = () => { | |||||
| }, | }, | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "Shop", | |||||
| label: "ShopAndTruck", | |||||
| path: "/settings/shop", | path: "/settings/shop", | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -18,6 +18,7 @@ import Search from "@mui/icons-material/Search"; | |||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| import "dayjs/locale/zh-hk"; | import "dayjs/locale/zh-hk"; | ||||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | ||||
| import { TimePicker } from "@mui/x-date-pickers/TimePicker"; | |||||
| import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | ||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import { | import { | ||||
| @@ -96,6 +97,10 @@ interface DateCriterion<T extends string> extends BaseCriterion<T> { | |||||
| type: "date"; | type: "date"; | ||||
| } | } | ||||
| interface TimeCriterion<T extends string> extends BaseCriterion<T> { | |||||
| type: "time"; | |||||
| } | |||||
| export type Criterion<T extends string> = | export type Criterion<T extends string> = | ||||
| | TextCriterion<T> | | TextCriterion<T> | ||||
| | SelectCriterion<T> | | SelectCriterion<T> | ||||
| @@ -103,6 +108,7 @@ export type Criterion<T extends string> = | |||||
| | DateRangeCriterion<T> | | DateRangeCriterion<T> | ||||
| | DatetimeRangeCriterion<T> | | DatetimeRangeCriterion<T> | ||||
| | DateCriterion<T> | | DateCriterion<T> | ||||
| | TimeCriterion<T> | |||||
| | MultiSelectCriterion<T> | | MultiSelectCriterion<T> | ||||
| | AutocompleteCriterion<T>; | | AutocompleteCriterion<T>; | ||||
| @@ -249,6 +255,15 @@ function SearchBox<T extends string>({ | |||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| const makeTimeChangeHandler = useCallback((paramName: T) => { | |||||
| return (value: Dayjs | null) => { | |||||
| setInputs((i) => ({ | |||||
| ...i, | |||||
| [paramName]: value ? value.format("HH:mm") : "" | |||||
| })); | |||||
| }; | |||||
| }, []); | |||||
| const handleReset = () => { | const handleReset = () => { | ||||
| setInputs(defaultInputs); | setInputs(defaultInputs); | ||||
| onReset?.(); | onReset?.(); | ||||
| @@ -524,6 +539,25 @@ function SearchBox<T extends string>({ | |||||
| </Box> | </Box> | ||||
| </LocalizationProvider> | </LocalizationProvider> | ||||
| )} | )} | ||||
| {c.type === "time" && ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale="zh-hk" | |||||
| > | |||||
| <FormControl fullWidth> | |||||
| <TimePicker | |||||
| format="HH:mm" | |||||
| label={t(c.label)} | |||||
| onChange={makeTimeChangeHandler(c.paramName)} | |||||
| value={ | |||||
| inputs[c.paramName] && dayjs(inputs[c.paramName], "HH:mm").isValid() | |||||
| ? dayjs(inputs[c.paramName], "HH:mm") | |||||
| : null | |||||
| } | |||||
| /> | |||||
| </FormControl> | |||||
| </LocalizationProvider> | |||||
| )} | |||||
| </Grid> | </Grid> | ||||
| ); | ); | ||||
| })} | })} | ||||
| @@ -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("Shop 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,277 @@ | |||||
| "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[]; | |||||
| // Get unique truckLanceCodes only | |||||
| const uniqueCodes = new Map<string, Truck>(); | |||||
| (data || []).forEach((truck) => { | |||||
| const code = String(truck.truckLanceCode || "").trim(); | |||||
| if (code && !uniqueCodes.has(code)) { | |||||
| uniqueCodes.set(code, truck); | |||||
| } | |||||
| }); | |||||
| setTruckData(Array.from(uniqueCodes.values())); | |||||
| } catch (err: any) { | |||||
| console.error("Failed to load truck lanes:", err); | |||||
| setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); | |||||
| } 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 using truckLanceCode | |||||
| const truckLanceCode = String(truck.truckLanceCode || "").trim(); | |||||
| if (truckLanceCode) { | |||||
| // Use router.push with proper URL encoding | |||||
| const url = new URL(`/settings/shop/truckdetail`, window.location.origin); | |||||
| url.searchParams.set("truckLanceCode", truckLanceCode); | |||||
| router.push(url.pathname + url.search); | |||||
| } | |||||
| }; | |||||
| 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: "time", 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 align="right">{t("Actions")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedRows.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={4} 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 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,497 @@ | |||||
| "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, findAllShopsByTruckLanceCodeClient, 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 truckLanceCodeParam = searchParams.get("truckLanceCode"); | |||||
| // Decode the truckLanceCode to handle special characters properly | |||||
| const truckLanceCode = truckLanceCodeParam ? decodeURIComponent(truckLanceCodeParam) : null; | |||||
| 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(() => { | |||||
| // Wait a bit to ensure searchParams are fully available | |||||
| if (!truckLanceCodeParam) { | |||||
| setError(t("TruckLance Code is required")); | |||||
| setLoading(false); | |||||
| return; | |||||
| } | |||||
| const fetchTruckLaneDetail = async () => { | |||||
| if (!truckLanceCode) { | |||||
| setError(t("TruckLance Code is required")); | |||||
| setLoading(false); | |||||
| return; | |||||
| } | |||||
| setLoading(true); | |||||
| setError(null); | |||||
| try { | |||||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | |||||
| const truck = data.find((t) => String(t.truckLanceCode || "").trim() === truckLanceCode.trim()); | |||||
| if (truck) { | |||||
| setTruckData(truck); | |||||
| // Fetch shops using this truck lane code only | |||||
| await fetchShopsByTruckLane(truckLanceCode); | |||||
| } else { | |||||
| setError(t("Truck lane not found")); | |||||
| } | |||||
| } catch (err: any) { | |||||
| console.error("Failed to load truck lane detail:", err); | |||||
| // Don't show error if it's a 401 - that will be handled by the auth system | |||||
| if (err?.message?.includes("401") || err?.status === 401) { | |||||
| // Let the auth system handle the redirect | |||||
| return; | |||||
| } | |||||
| setError(err?.message ?? String(err) ?? t("Failed to load truck lane detail")); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }; | |||||
| fetchTruckLaneDetail(); | |||||
| }, [truckLanceCode, truckLanceCodeParam, t]); | |||||
| const fetchShopsByTruckLane = async (truckLanceCode: string) => { | |||||
| setShopsLoading(true); | |||||
| try { | |||||
| // Fetch shops by truckLanceCode only | |||||
| const shops = await findAllShopsByTruckLanceCodeClient(truckLanceCode); | |||||
| // Sort by remarks, then loadingSequence, then code | |||||
| const sortedShops = (shops || []).sort((a, b) => { | |||||
| const remarkA = String(a.remark || "").trim(); | |||||
| const remarkB = String(b.remark || "").trim(); | |||||
| if (remarkA !== remarkB) { | |||||
| return remarkA.localeCompare(remarkB); | |||||
| } | |||||
| const seqA = (a as any).LoadingSequence ?? (a as any).loadingSequence ?? 0; | |||||
| const seqB = (b as any).LoadingSequence ?? (b as any).loadingSequence ?? 0; | |||||
| if (Number(seqA) !== Number(seqB)) { | |||||
| return Number(seqA) - Number(seqB); | |||||
| } | |||||
| const codeA = String(a.code || "").trim(); | |||||
| const codeB = String(b.code || "").trim(); | |||||
| return codeA.localeCompare(codeB); | |||||
| }); | |||||
| setShopsData(sortedShops); | |||||
| setEditedShopsData(sortedShops); | |||||
| } 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 (truckLanceCode) { | |||||
| await fetchShopsByTruckLane(truckLanceCode); | |||||
| } | |||||
| 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 (truckLanceCode) { | |||||
| await fetchShopsByTruckLane(truckLanceCode); | |||||
| } | |||||
| } 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> | |||||
| </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("Remark")}</TableCell> | |||||
| <TableCell>{t("Loading Sequence")}</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> | |||||
| {String(shop.remark || "-")} | |||||
| </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 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; | |||||
| @@ -9,6 +9,7 @@ interface ShelfLifeInputProps { | |||||
| onChange?: (value: number) => void; | onChange?: (value: number) => void; | ||||
| label?: string; | label?: string; | ||||
| sx?: any; | sx?: any; | ||||
| showHelperText?: boolean; // Option to show/hide the helper text | |||||
| } | } | ||||
| const ShelfLifeContainer = styled(Box)(({ theme }) => ({ | const ShelfLifeContainer = styled(Box)(({ theme }) => ({ | ||||
| @@ -61,7 +62,7 @@ const formatDuration = (years: number, months: number, days: number) => { | |||||
| return parts.length > 0 ? parts.join(' ') : '0 日'; | return parts.length > 0 ? parts.join(' ') : '0 日'; | ||||
| }; | }; | ||||
| const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = () => {}, label = 'Shelf Life', sx }) => { | |||||
| const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = () => {}, label = 'Shelf Life', sx, showHelperText = true }) => { | |||||
| const { t } = useTranslation("purchaseOrder"); | const { t } = useTranslation("purchaseOrder"); | ||||
| const { years, months, days } = daysToDuration(value); | const { years, months, days } = daysToDuration(value); | ||||
| @@ -101,7 +102,7 @@ const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = ( | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <Box sx={{ width: '100%', paddingLeft: '1rem' }}> | |||||
| <Box sx={{ width: '100%' }}> | |||||
| <ShelfLifeContainer> | <ShelfLifeContainer> | ||||
| <TextField | <TextField | ||||
| label="年" | label="年" | ||||
| @@ -140,12 +141,14 @@ const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = ( | |||||
| size="small" | size="small" | ||||
| /> | /> | ||||
| </ShelfLifeContainer> | </ShelfLifeContainer> | ||||
| <FormHelperText sx={{ fontSize: '2rem', mt: 1 }}> | |||||
| {label}: <span style={{ color: totalDays < 1 ? 'red':'inherit' }}> | |||||
| {/* {formatDuration(duration.years, duration.months, duration.days)} */} | |||||
| {totalDays} 日 | |||||
| </span> | |||||
| </FormHelperText> | |||||
| {showHelperText && ( | |||||
| <FormHelperText sx={{ fontSize: '2rem', mt: 1 }}> | |||||
| {label}: <span style={{ color: totalDays < 1 ? 'red':'inherit' }}> | |||||
| {/* {formatDuration(duration.years, duration.months, duration.days)} */} | |||||
| {totalDays} 日 | |||||
| </span> | |||||
| </FormHelperText> | |||||
| )} | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -0,0 +1,242 @@ | |||||
| "use client"; | |||||
| import { useCallback, useEffect, useState } from "react"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| Button, | |||||
| Card, | |||||
| CardContent, | |||||
| FormControl, | |||||
| InputLabel, | |||||
| MenuItem, | |||||
| Select, | |||||
| TextField, | |||||
| Typography, | |||||
| Stack, | |||||
| Grid, | |||||
| } from "@mui/material"; | |||||
| import { Check, Close } from "@mui/icons-material"; | |||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||||
| type Props = { | |||||
| id: number; | |||||
| }; | |||||
| type EquipmentDetailData = { | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| equipmentCode?: string; | |||||
| repairAndMaintenanceStatus?: boolean | null; | |||||
| repairAndMaintenanceRemarks?: string | null; | |||||
| }; | |||||
| const UpdateMaintenanceForm: React.FC<Props> = ({ id }) => { | |||||
| const { t } = useTranslation("common"); | |||||
| const router = useRouter(); | |||||
| const [loading, setLoading] = useState(false); | |||||
| const [fetching, setFetching] = useState(true); | |||||
| const [equipmentData, setEquipmentData] = useState<EquipmentDetailData | null>(null); | |||||
| const [status, setStatus] = useState<boolean | null>(null); | |||||
| const [remarks, setRemarks] = useState<string>(""); | |||||
| useEffect(() => { | |||||
| const fetchEquipmentDetail = async () => { | |||||
| try { | |||||
| setFetching(true); | |||||
| const response = await axiosInstance.get<EquipmentDetailData>( | |||||
| `${NEXT_PUBLIC_API_URL}/EquipmentDetail/details/${id}` | |||||
| ); | |||||
| if (response.data) { | |||||
| setEquipmentData(response.data); | |||||
| setStatus(response.data.repairAndMaintenanceStatus ?? null); | |||||
| setRemarks(response.data.repairAndMaintenanceRemarks ?? ""); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error fetching equipment detail:", error); | |||||
| } finally { | |||||
| setFetching(false); | |||||
| } | |||||
| }; | |||||
| fetchEquipmentDetail(); | |||||
| }, [id]); | |||||
| const handleSave = useCallback(async () => { | |||||
| if (!equipmentData) return; | |||||
| try { | |||||
| setLoading(true); | |||||
| const updateData = { | |||||
| repairAndMaintenanceStatus: status, | |||||
| repairAndMaintenanceRemarks: remarks, | |||||
| }; | |||||
| await axiosInstance.put( | |||||
| `${NEXT_PUBLIC_API_URL}/EquipmentDetail/update/${id}`, | |||||
| updateData, | |||||
| { | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| } | |||||
| ); | |||||
| router.push("/settings/equipment?tab=1"); | |||||
| } catch (error) { | |||||
| console.error("Error updating maintenance:", error); | |||||
| alert(t("Error saving data") || "Error saving data"); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }, [equipmentData, status, remarks, id, router, t]); | |||||
| const handleCancel = useCallback(() => { | |||||
| router.push("/settings/equipment?tab=1"); | |||||
| }, [router]); | |||||
| if (fetching) { | |||||
| return ( | |||||
| <Stack sx={{ p: 3 }}> | |||||
| <Typography>{t("Loading") || "Loading..."}</Typography> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| if (!equipmentData) { | |||||
| return ( | |||||
| <Stack sx={{ p: 3 }}> | |||||
| <Typography>{t("Equipment not found") || "Equipment not found"}</Typography> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <Stack | |||||
| spacing={2} | |||||
| component="form" | |||||
| > | |||||
| <Card> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Equipment Information")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Equipment Name") || "設備名稱"} | |||||
| value={equipmentData.code || ""} | |||||
| disabled | |||||
| fullWidth | |||||
| variant="filled" | |||||
| InputLabelProps={{ | |||||
| shrink: !!equipmentData.code, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| InputProps={{ | |||||
| sx: { paddingTop: "8px" }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Equipment Code") || "設備編號"} | |||||
| value={equipmentData.equipmentCode || ""} | |||||
| disabled | |||||
| fullWidth | |||||
| variant="filled" | |||||
| InputLabelProps={{ | |||||
| shrink: !!equipmentData.equipmentCode, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| InputProps={{ | |||||
| sx: { paddingTop: "8px" }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <FormControl fullWidth variant="filled"> | |||||
| <InputLabel | |||||
| shrink={status !== null} | |||||
| sx={{ fontSize: "0.9375rem" }} | |||||
| > | |||||
| {t("Repair and Maintenance Status")} | |||||
| </InputLabel> | |||||
| <Select | |||||
| value={status === null ? "" : status ? "yes" : "no"} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| if (value === "yes") { | |||||
| setStatus(true); | |||||
| } else if (value === "no") { | |||||
| setStatus(false); | |||||
| } else { | |||||
| setStatus(null); | |||||
| } | |||||
| }} | |||||
| sx={{ paddingTop: "8px" }} | |||||
| > | |||||
| <MenuItem value="yes">{t("Yes")}</MenuItem> | |||||
| <MenuItem value="no">{t("No")}</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Repair and Maintenance Remarks")} | |||||
| value={remarks} | |||||
| onChange={(e) => setRemarks(e.target.value)} | |||||
| fullWidth | |||||
| multiline | |||||
| rows={4} | |||||
| variant="filled" | |||||
| InputLabelProps={{ | |||||
| shrink: true, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| InputProps={{ | |||||
| sx: { | |||||
| paddingTop: "8px", | |||||
| alignItems: "flex-start", | |||||
| paddingBottom: "8px", | |||||
| }, | |||||
| }} | |||||
| sx={{ | |||||
| "& .MuiInputBase-input": { | |||||
| paddingTop: "16px", | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Close />} | |||||
| onClick={handleCancel} | |||||
| disabled={loading} | |||||
| type="button" | |||||
| > | |||||
| {t("Cancel") || "取消"} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| onClick={handleSave} | |||||
| disabled={loading} | |||||
| type="button" | |||||
| > | |||||
| {t("Save") || "保存"} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| ); | |||||
| }; | |||||
| export default UpdateMaintenanceForm; | |||||
| @@ -1,3 +1,23 @@ | |||||
| { | { | ||||
| "Grade {{grade}}": "Grade {{grade}}" | |||||
| "Grade {{grade}}": "Grade {{grade}}", | |||||
| "General Data": "General Data", | |||||
| "Repair and Maintenance": "Repair and Maintenance", | |||||
| "Repair and Maintenance Status": "Repair and Maintenance Status", | |||||
| "Latest Repair and Maintenance Date": "Latest Repair and Maintenance Date", | |||||
| "Last Repair and Maintenance Date": "Last Repair and Maintenance Date", | |||||
| "Repair and Maintenance Remarks": "Repair and Maintenance Remarks", | |||||
| "Update Equipment Maintenance and Repair": "Update Equipment Maintenance and Repair", | |||||
| "Equipment Information": "Equipment Information", | |||||
| "Loading": "Loading...", | |||||
| "Equipment not found": "Equipment not found", | |||||
| "Error saving data": "Error saving data", | |||||
| "Cancel": "Cancel", | |||||
| "Save": "Save", | |||||
| "Yes": "Yes", | |||||
| "No": "No", | |||||
| "Equipment Name": "Equipment Name", | |||||
| "Equipment Code": "Equipment Code", | |||||
| "ShopAndTruck": "ShopAndTruck" | |||||
| } | } | ||||
| @@ -1 +1,13 @@ | |||||
| {} | |||||
| { | |||||
| "LocationCode": "Location Code", | |||||
| "DefaultLocationCode": "Default Location Code", | |||||
| "Special Type": "Special Type", | |||||
| "None": "None", | |||||
| "isEgg": "Egg", | |||||
| "isFee": "Fee", | |||||
| "isBag": "Bag", | |||||
| "Back": "Back", | |||||
| "Status": "Status", | |||||
| "Complete": "Complete", | |||||
| "Missing Data": "Missing Data" | |||||
| } | |||||
| @@ -301,5 +301,92 @@ | |||||
| "Total lines: ": "總數量:", | "Total lines: ": "總數量:", | ||||
| "Balance": "可用數量", | "Balance": "可用數量", | ||||
| "Submitting...": "提交中...", | "Submitting...": "提交中...", | ||||
| "Batch Count": "批數" | |||||
| "Batch Count": "批數", | |||||
| "Shop": "店鋪", | |||||
| "ShopAndTruck": "店鋪路線管理", | |||||
| "Shop Information": "店鋪資訊", | |||||
| "Shop Name": "店鋪名稱", | |||||
| "Shop Code": "店鋪編號", | |||||
| "Truck Lane": "卡車路線", | |||||
| "Truck Lane Detail": "卡車路線詳情", | |||||
| "TruckLance Code": "卡車路線編號", | |||||
| "TruckLance Status": "卡車路線狀態", | |||||
| "Departure Time": "出發時間", | |||||
| "Loading Sequence": "裝載順序", | |||||
| "District Reference": "區域參考", | |||||
| "Store 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": "地址", | |||||
| "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": "此樓層不可編輯", | |||||
| "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": "全部", | |||||
| "General Data": "基本資料", | |||||
| "Repair and Maintenance": "維修和保養", | |||||
| "Repair and Maintenance Status": "維修和保養狀態", | |||||
| "Latest Repair and Maintenance Date": "最新維修和保養日期", | |||||
| "Last Repair and Maintenance Date": "上次維修和保養日期", | |||||
| "Repair and Maintenance Remarks": "維修和保養備註", | |||||
| "Rows per page": "每頁行數", | |||||
| "Equipment Name": "設備名稱", | |||||
| "Equipment Code": "設備編號", | |||||
| "Yes": "是", | |||||
| "No": "否", | |||||
| "Update Equipment Maintenance and Repair": "更新設備的維修和保養", | |||||
| "Equipment Information": "設備資訊", | |||||
| "Loading": "載入中...", | |||||
| "Equipment not found": "找不到設備", | |||||
| "Error saving data": "保存數據時出錯" | |||||
| } | } | ||||
| @@ -32,5 +32,16 @@ | |||||
| "Reset": "重置", | "Reset": "重置", | ||||
| "Search": "搜尋", | "Search": "搜尋", | ||||
| "Release": "發佈", | "Release": "發佈", | ||||
| "Actions": "操作" | |||||
| "Actions": "操作", | |||||
| "LocationCode": "位置", | |||||
| "DefaultLocationCode": "預設位置", | |||||
| "Special Type": "特殊類型", | |||||
| "None": "無", | |||||
| "isEgg": "雞蛋", | |||||
| "isFee": "費用", | |||||
| "isBag": "袋子", | |||||
| "Back": "返回", | |||||
| "Status": "狀態", | |||||
| "Complete": "完成", | |||||
| "Missing Data": "缺少資料" | |||||
| } | } | ||||