| @@ -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 = { | ||||
| @@ -165,6 +165,16 @@ export const findAllShopsByTruckLanceCodeAndRemarkAction = cache(async (truckLan | |||||
| }); | }); | ||||
| }); | }); | ||||
| export const findAllShopsByTruckLanceCodeAction = cache(async (truckLanceCode: string) => { | |||||
| const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse`; | |||||
| const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`; | |||||
| return serverFetchJson<ShopAndTruck[]>(url, { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| }); | |||||
| export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => { | export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => { | ||||
| const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`; | const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`; | ||||
| @@ -8,6 +8,7 @@ import { | |||||
| createTruckAction, | createTruckAction, | ||||
| findAllUniqueTruckLaneCombinationsAction, | findAllUniqueTruckLaneCombinationsAction, | ||||
| findAllShopsByTruckLanceCodeAndRemarkAction, | findAllShopsByTruckLanceCodeAndRemarkAction, | ||||
| findAllShopsByTruckLanceCodeAction, | |||||
| updateLoadingSequenceAction, | updateLoadingSequenceAction, | ||||
| type SaveTruckLane, | type SaveTruckLane, | ||||
| type DeleteTruckLane, | type DeleteTruckLane, | ||||
| @@ -44,6 +45,10 @@ export const findAllShopsByTruckLanceCodeAndRemarkClient = async (truckLanceCode | |||||
| return await findAllShopsByTruckLanceCodeAndRemarkAction(truckLanceCode, remark); | return await findAllShopsByTruckLanceCodeAndRemarkAction(truckLanceCode, remark); | ||||
| }; | }; | ||||
| export const findAllShopsByTruckLanceCodeClient = async (truckLanceCode: string) => { | |||||
| return await findAllShopsByTruckLanceCodeAction(truckLanceCode); | |||||
| }; | |||||
| export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise<MessageResponse> => { | export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise<MessageResponse> => { | ||||
| return await updateLoadingSequenceAction(data); | return await updateLoadingSequenceAction(data); | ||||
| }; | }; | ||||
| @@ -17,7 +17,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
| "/settings/qrCodeHandle": "QR Code Handle", | "/settings/qrCodeHandle": "QR Code Handle", | ||||
| "/settings/rss": "Demand Forecast Setting", | "/settings/rss": "Demand Forecast Setting", | ||||
| "/settings/equipment": "Equipment", | "/settings/equipment": "Equipment", | ||||
| "/settings/shop": "Shop", | |||||
| "/settings/shop": "ShopAndTruck", | |||||
| "/settings/shop/detail": "Shop Detail", | "/settings/shop/detail": "Shop Detail", | ||||
| "/settings/shop/truckdetail": "Truck Lane Detail", | "/settings/shop/truckdetail": "Truck Lane Detail", | ||||
| "/scheduling/rough": "Demand Forecast", | "/scheduling/rough": "Demand Forecast", | ||||
| @@ -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}> | ||||
| @@ -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} | ||||
| @@ -266,7 +266,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> | ||||
| ); | ); | ||||
| })} | })} | ||||
| @@ -74,7 +74,15 @@ const TruckLane: React.FC = () => { | |||||
| setError(null); | setError(null); | ||||
| try { | try { | ||||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | ||||
| setTruckData(data || []); | |||||
| // 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) { | } catch (err: any) { | ||||
| console.error("Failed to load truck lanes:", err); | console.error("Failed to load truck lanes:", err); | ||||
| setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); | setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); | ||||
| @@ -140,9 +148,13 @@ const TruckLane: React.FC = () => { | |||||
| }; | }; | ||||
| const handleViewDetail = (truck: Truck) => { | const handleViewDetail = (truck: Truck) => { | ||||
| // Navigate to truck lane detail page | |||||
| if (truck.id) { | |||||
| router.push(`/settings/shop/truckdetail?id=${truck.id}`); | |||||
| // Navigate to truck lane detail page using truckLanceCode | |||||
| const truckLanceCode = String(truck.truckLanceCode || "").trim(); | |||||
| if (truckLanceCode) { | |||||
| // Use router.push with proper URL encoding | |||||
| const url = new URL(`/settings/shop/truckdetail`, window.location.origin); | |||||
| url.searchParams.set("truckLanceCode", truckLanceCode); | |||||
| router.push(url.pathname + url.search); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -166,7 +178,7 @@ const TruckLane: React.FC = () => { | |||||
| const criteria: Criterion<SearchParamNames>[] = [ | const criteria: Criterion<SearchParamNames>[] = [ | ||||
| { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" }, | { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" }, | ||||
| { type: "text", label: t("Departure Time"), paramName: "departureTime" }, | |||||
| { type: "time", label: t("Departure Time"), paramName: "departureTime" }, | |||||
| { type: "text", label: t("Store ID"), paramName: "storeId" }, | { type: "text", label: t("Store ID"), paramName: "storeId" }, | ||||
| ]; | ]; | ||||
| @@ -195,14 +207,13 @@ const TruckLane: React.FC = () => { | |||||
| <TableCell>{t("TruckLance Code")}</TableCell> | <TableCell>{t("TruckLance Code")}</TableCell> | ||||
| <TableCell>{t("Departure Time")}</TableCell> | <TableCell>{t("Departure Time")}</TableCell> | ||||
| <TableCell>{t("Store ID")}</TableCell> | <TableCell>{t("Store ID")}</TableCell> | ||||
| <TableCell>{t("Remark")}</TableCell> | |||||
| <TableCell align="right">{t("Actions")}</TableCell> | <TableCell align="right">{t("Actions")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| {paginatedRows.length === 0 ? ( | {paginatedRows.length === 0 ? ( | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={5} align="center"> | |||||
| <TableCell colSpan={4} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No Truck Lane data available")} | {t("No Truck Lane data available")} | ||||
| </Typography> | </Typography> | ||||
| @@ -231,9 +242,6 @@ const TruckLane: React.FC = () => { | |||||
| <TableCell> | <TableCell> | ||||
| {displayStoreId} | {displayStoreId} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| {String(truck.remark || "-")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Button | <Button | ||||
| size="small" | size="small" | ||||
| @@ -27,7 +27,7 @@ import CancelIcon from "@mui/icons-material/Cancel"; | |||||
| import { useState, useEffect } from "react"; | import { useState, useEffect } from "react"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeAndRemarkClient, deleteTruckLaneClient, updateLoadingSequenceClient } from "@/app/api/shop/client"; | |||||
| import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeClient, deleteTruckLaneClient, updateLoadingSequenceClient } from "@/app/api/shop/client"; | |||||
| import type { Truck, ShopAndTruck } from "@/app/api/shop/actions"; | import type { Truck, ShopAndTruck } from "@/app/api/shop/actions"; | ||||
| // Utility function to format departureTime to HH:mm format | // Utility function to format departureTime to HH:mm format | ||||
| @@ -60,7 +60,9 @@ const TruckLaneDetail: React.FC = () => { | |||||
| const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
| const truckId = searchParams.get("id"); | |||||
| 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 [truckData, setTruckData] = useState<Truck | null>(null); | ||||
| const [shopsData, setShopsData] = useState<ShopAndTruck[]>([]); | const [shopsData, setShopsData] = useState<ShopAndTruck[]>([]); | ||||
| @@ -77,9 +79,16 @@ const TruckLaneDetail: React.FC = () => { | |||||
| }); | }); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| // Wait a bit to ensure searchParams are fully available | |||||
| if (!truckLanceCodeParam) { | |||||
| setError(t("TruckLance Code is required")); | |||||
| setLoading(false); | |||||
| return; | |||||
| } | |||||
| const fetchTruckLaneDetail = async () => { | const fetchTruckLaneDetail = async () => { | ||||
| if (!truckId) { | |||||
| setError(t("Truck ID is required")); | |||||
| if (!truckLanceCode) { | |||||
| setError(t("TruckLance Code is required")); | |||||
| setLoading(false); | setLoading(false); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -88,17 +97,22 @@ const TruckLaneDetail: React.FC = () => { | |||||
| setError(null); | setError(null); | ||||
| try { | try { | ||||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | ||||
| const truck = data.find((t) => t.id?.toString() === truckId); | |||||
| const truck = data.find((t) => String(t.truckLanceCode || "").trim() === truckLanceCode.trim()); | |||||
| if (truck) { | if (truck) { | ||||
| setTruckData(truck); | setTruckData(truck); | ||||
| // Fetch shops using this truck lane | |||||
| await fetchShopsByTruckLane(String(truck.truckLanceCode || ""), String(truck.remark || "")); | |||||
| // Fetch shops using this truck lane code only | |||||
| await fetchShopsByTruckLane(truckLanceCode); | |||||
| } else { | } else { | ||||
| setError(t("Truck lane not found")); | setError(t("Truck lane not found")); | ||||
| } | } | ||||
| } catch (err: any) { | } catch (err: any) { | ||||
| console.error("Failed to load truck lane detail:", err); | 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")); | setError(err?.message ?? String(err) ?? t("Failed to load truck lane detail")); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| @@ -106,14 +120,31 @@ const TruckLaneDetail: React.FC = () => { | |||||
| }; | }; | ||||
| fetchTruckLaneDetail(); | fetchTruckLaneDetail(); | ||||
| }, [truckId]); | |||||
| }, [truckLanceCode, truckLanceCodeParam, t]); | |||||
| const fetchShopsByTruckLane = async (truckLanceCode: string, remark: string) => { | |||||
| const fetchShopsByTruckLane = async (truckLanceCode: string) => { | |||||
| setShopsLoading(true); | setShopsLoading(true); | ||||
| try { | try { | ||||
| const shops = await findAllShopsByTruckLanceCodeAndRemarkClient(truckLanceCode, remark || ""); | |||||
| setShopsData(shops || []); | |||||
| setEditedShopsData(shops || []); | |||||
| // 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) { | } catch (err: any) { | ||||
| console.error("Failed to load shops:", err); | console.error("Failed to load shops:", err); | ||||
| setSnackbar({ | setSnackbar({ | ||||
| @@ -178,8 +209,8 @@ const TruckLaneDetail: React.FC = () => { | |||||
| }); | }); | ||||
| // Refresh the shops list | // Refresh the shops list | ||||
| if (truckData) { | |||||
| await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || "")); | |||||
| if (truckLanceCode) { | |||||
| await fetchShopsByTruckLane(truckLanceCode); | |||||
| } | } | ||||
| setEditingRowIndex(null); | setEditingRowIndex(null); | ||||
| } catch (err: any) { | } catch (err: any) { | ||||
| @@ -218,8 +249,8 @@ const TruckLaneDetail: React.FC = () => { | |||||
| }); | }); | ||||
| // Refresh the shops list | // Refresh the shops list | ||||
| if (truckData) { | |||||
| await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || "")); | |||||
| if (truckLanceCode) { | |||||
| await fetchShopsByTruckLane(truckLanceCode); | |||||
| } | } | ||||
| } catch (err: any) { | } catch (err: any) { | ||||
| console.error("Failed to delete truck lane:", err); | console.error("Failed to delete truck lane:", err); | ||||
| @@ -323,14 +354,6 @@ const TruckLaneDetail: React.FC = () => { | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12} sm={6}> | |||||
| <Typography variant="subtitle2" color="text.secondary"> | |||||
| {t("Remark")} | |||||
| </Typography> | |||||
| <Typography variant="body1" sx={{ mt: 1 }}> | |||||
| {String(truckData.remark || "-")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| </Grid> | </Grid> | ||||
| </Paper> | </Paper> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -353,8 +376,8 @@ const TruckLaneDetail: React.FC = () => { | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Shop Name")}</TableCell> | <TableCell>{t("Shop Name")}</TableCell> | ||||
| <TableCell>{t("Shop Code")}</TableCell> | <TableCell>{t("Shop Code")}</TableCell> | ||||
| <TableCell>{t("Loading Sequence")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | <TableCell>{t("Remark")}</TableCell> | ||||
| <TableCell>{t("Loading Sequence")}</TableCell> | |||||
| <TableCell align="right">{t("Actions")}</TableCell> | <TableCell align="right">{t("Actions")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| @@ -376,6 +399,9 @@ const TruckLaneDetail: React.FC = () => { | |||||
| <TableCell> | <TableCell> | ||||
| {String(shop.code || "-")} | {String(shop.code || "-")} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| {String(shop.remark || "-")} | |||||
| </TableCell> | |||||
| <TableCell> | <TableCell> | ||||
| {editingRowIndex === index ? ( | {editingRowIndex === index ? ( | ||||
| <TextField | <TextField | ||||
| @@ -399,9 +425,6 @@ const TruckLaneDetail: React.FC = () => { | |||||
| })() | })() | ||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| {String(shop.remark || "-")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}> | <Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}> | ||||
| {editingRowIndex === index ? ( | {editingRowIndex === index ? ( | ||||
| @@ -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> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -1,5 +1,6 @@ | |||||
| { | { | ||||
| "Grade {{grade}}": "Grade {{grade}}", | "Grade {{grade}}": "Grade {{grade}}", | ||||
| <<<<<<< Updated upstream | |||||
| "General Data": "General Data", | "General Data": "General Data", | ||||
| "Repair and Maintenance": "Repair and Maintenance", | "Repair and Maintenance": "Repair and Maintenance", | ||||
| "Repair and Maintenance Status": "Repair and Maintenance Status", | "Repair and Maintenance Status": "Repair and Maintenance Status", | ||||
| @@ -17,4 +18,7 @@ | |||||
| "No": "No", | "No": "No", | ||||
| "Equipment Name": "Equipment Name", | "Equipment Name": "Equipment Name", | ||||
| "Equipment Code": "Equipment Code" | "Equipment Code": "Equipment Code" | ||||
| ======= | |||||
| "ShopAndTruck": "ShopAndTruck" | |||||
| >>>>>>> Stashed changes | |||||
| } | } | ||||
| @@ -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" | |||||
| } | |||||
| @@ -298,6 +298,7 @@ | |||||
| "Submitting...": "提交中...", | "Submitting...": "提交中...", | ||||
| "Batch Count": "批數", | "Batch Count": "批數", | ||||
| "Shop": "店鋪", | "Shop": "店鋪", | ||||
| "ShopAndTruck": "店鋪路線管理", | |||||
| "Shop Information": "店鋪資訊", | "Shop Information": "店鋪資訊", | ||||
| "Shop Name": "店鋪名稱", | "Shop Name": "店鋪名稱", | ||||
| "Shop Code": "店鋪編號", | "Shop Code": "店鋪編號", | ||||
| @@ -346,7 +347,7 @@ | |||||
| "Contact Name": "聯絡人", | "Contact Name": "聯絡人", | ||||
| "Addr1": "地址1", | "Addr1": "地址1", | ||||
| "Addr2": "地址2", | "Addr2": "地址2", | ||||
| "Addr3": "地址3", | |||||
| "Addr3": "地址", | |||||
| "Shop not found": "找不到店鋪", | "Shop not found": "找不到店鋪", | ||||
| "Shop ID is required": "需要店鋪ID", | "Shop ID is required": "需要店鋪ID", | ||||
| "Invalid Shop ID": "無效的店鋪ID", | "Invalid Shop ID": "無效的店鋪ID", | ||||
| @@ -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": "缺少資料" | |||||
| } | } | ||||