Pārlūkot izejas kodu

update shop and truck , item

master
Tommy\2Fi-Staff pirms 5 dienas
vecāks
revīzija
54dde3968d
18 mainītis faili ar 434 papildinājumiem un 148 dzēšanām
  1. +8
    -0
      src/app/api/settings/item/actions.ts
  2. +8
    -0
      src/app/api/settings/item/index.ts
  3. +10
    -0
      src/app/api/shop/actions.ts
  4. +5
    -0
      src/app/api/shop/client.ts
  5. +1
    -1
      src/components/Breadcrumb/Breadcrumb.tsx
  6. +46
    -6
      src/components/CreateItem/CreateItem.tsx
  7. +12
    -1
      src/components/CreateItem/CreateItemWrapper.tsx
  8. +120
    -83
      src/components/CreateItem/ProductDetails.tsx
  9. +78
    -7
      src/components/ItemsSearch/ItemsSearch.tsx
  10. +1
    -1
      src/components/NavigationContent/NavigationContent.tsx
  11. +34
    -0
      src/components/SearchBox/SearchBox.tsx
  12. +18
    -10
      src/components/Shop/TruckLane.tsx
  13. +51
    -28
      src/components/Shop/TruckLaneDetail.tsx
  14. +11
    -8
      src/components/StockIn/ShelfLifeInput.tsx
  15. +4
    -0
      src/i18n/en/common.json
  16. +13
    -1
      src/i18n/en/items.json
  17. +2
    -1
      src/i18n/zh/common.json
  18. +12
    -1
      src/i18n/zh/items.json

+ 8
- 0
src/app/api/settings/item/actions.ts Parādīt failu

@@ -37,6 +37,14 @@ export type CreateItemInputs = {
qcChecks: QcChecksInputs[];
qcChecks_active: number[];
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) => {


+ 8
- 0
src/app/api/settings/item/index.ts Parādīt failu

@@ -53,6 +53,14 @@ export type ItemsResult = {
fgName?: string;
excludeDate?: string;
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 = {


+ 10
- 0
src/app/api/shop/actions.ts Parādīt failu

@@ -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) => {
const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`;


+ 5
- 0
src/app/api/shop/client.ts Parādīt failu

@@ -8,6 +8,7 @@ import {
createTruckAction,
findAllUniqueTruckLaneCombinationsAction,
findAllShopsByTruckLanceCodeAndRemarkAction,
findAllShopsByTruckLanceCodeAction,
updateLoadingSequenceAction,
type SaveTruckLane,
type DeleteTruckLane,
@@ -44,6 +45,10 @@ export const findAllShopsByTruckLanceCodeAndRemarkClient = async (truckLanceCode
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);
};


+ 1
- 1
src/components/Breadcrumb/Breadcrumb.tsx Parādīt failu

@@ -17,7 +17,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/settings/qrCodeHandle": "QR Code Handle",
"/settings/rss": "Demand Forecast Setting",
"/settings/equipment": "Equipment",
"/settings/shop": "Shop",
"/settings/shop": "ShopAndTruck",
"/settings/shop/detail": "Shop Detail",
"/settings/shop/truckdetail": "Truck Lane Detail",
"/scheduling/rough": "Demand Forecast",


+ 46
- 6
src/components/CreateItem/CreateItem.tsx Parādīt failu

@@ -21,7 +21,7 @@ import {
TabsProps,
Typography,
} 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 ProductDetails from "./ProductDetails";
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 { useGridApiRef } from "@mui/x-data-grid";
import { QcCategoryCombo } from "@/app/api/settings/qcCategory";
import { WarehouseResult } from "@/app/api/warehouse";

type Props = {
isEditMode: boolean;
// type: TypeEnum;
defaultValues: Partial<CreateItemInputs> | undefined;
qcChecks: ItemQc[];
qcCategoryCombo: QcCategoryCombo[]
qcCategoryCombo: QcCategoryCombo[];
warehouses: WarehouseResult[];
};

const CreateItem: React.FC<Props> = ({
@@ -45,6 +47,7 @@ const CreateItem: React.FC<Props> = ({
defaultValues,
qcChecks,
qcCategoryCombo,
warehouses,
}) => {
// console.log(type)
const apiRef = useGridApiRef();
@@ -109,6 +112,26 @@ const CreateItem: React.FC<Props> = ({
setServerError(t("An error has occurred. Please try again later."));
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);
const qcCheck =
@@ -178,9 +201,19 @@ const CreateItem: React.FC<Props> = ({
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<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>
<Tabs
value={tabIndex}
@@ -195,7 +228,14 @@ const CreateItem: React.FC<Props> = ({
{serverError}
</Typography>
)}
{tabIndex === 0 && <ProductDetails isEditMode={isEditMode} qcCategoryCombo={qcCategoryCombo}/>}
{tabIndex === 0 && (
<ProductDetails
isEditMode={isEditMode}
qcCategoryCombo={qcCategoryCombo}
warehouses={warehouses}
defaultValues={defaultValues}
/>
)}
{tabIndex === 1 && <QcDetails apiRef={apiRef} />}
{/* {type === TypeEnum.MATERIAL && <MaterialDetails />} */}
{/* {type === TypeEnum.BYPRODUCT && <ByProductDetails />} */}


+ 12
- 1
src/components/CreateItem/CreateItemWrapper.tsx Parādīt failu

@@ -6,6 +6,7 @@ import { notFound } from "next/navigation";
import { fetchItem } from "@/app/api/settings/item";
import { fetchQcItems } from "@/app/api/settings/qcItem";
import { fetchQcCategoryCombo } from "@/app/api/settings/qcCategory";
import { fetchWarehouseList } from "@/app/api/warehouse";
interface SubComponents {
Loading: typeof CreateItemLoading;
}
@@ -38,11 +39,20 @@ const CreateItemWrapper: React.FC<Props> & SubComponents = async ({ id }) => {
maxQty: item?.maxQty,
qcChecks: qcChecks,
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 warehouses = await fetchWarehouseList();

return (
<CreateItem
@@ -50,6 +60,7 @@ const CreateItemWrapper: React.FC<Props> & SubComponents = async ({ id }) => {
defaultValues={defaultValues}
qcChecks={qcChecks || []}
qcCategoryCombo={qcCategoryCombo}
warehouses={warehouses}
/>
);
};


+ 120
- 83
src/components/CreateItem/ProductDetails.tsx Parādīt failu

@@ -5,12 +5,20 @@ import {
Button,
Card,
CardContent,
FormControl,
FormControlLabel,
FormLabel,
Grid,
InputLabel,
MenuItem,
Radio,
RadioGroup,
Select,
Stack,
TextField,
Typography,
} 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 { useTranslation } from "react-i18next";
import InputDataGrid from "../InputDataGrid";
@@ -19,11 +27,10 @@ import { SyntheticEvent, useCallback, useMemo, useState } from "react";
import { GridColDef, GridRowModel } from "@mui/x-data-grid";
import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid";
import { TypeEnum } from "@/app/utils/typeEnum";
import { NumberInputProps } from "./NumberInputProps";
import { CreateItemInputs } from "@/app/api/settings/item/actions";
import { RestartAlt } from "@mui/icons-material";
import { ItemQc } from "@/app/api/settings/item";
import { QcCategoryCombo } from "@/app/api/settings/qcCategory";
import { WarehouseResult } from "@/app/api/warehouse";
type Props = {
// isEditMode: boolean;
// type: TypeEnum;
@@ -32,9 +39,11 @@ type Props = {
defaultValues?: Partial<CreateItemInputs> | undefined;
qcChecks?: ItemQc[];
qcCategoryCombo: QcCategoryCombo[];
warehouses: WarehouseResult[];
};

const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => {
const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => {

const {
t,
i18n: { language },
@@ -42,13 +51,11 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => {

const {
register,
formState: { errors, defaultValues, touchedFields },
formState: { errors, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = 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) => {
onChange(value.id)
}, [])
@@ -124,6 +126,7 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => {
<TextField
label={t("Name")}
fullWidth
disabled
{...register("name", {
required: "name required!",
})}
@@ -135,6 +138,7 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => {
<TextField
label={t("Code")}
fullWidth
disabled
{...register("code", {
required: "code required!",
})}
@@ -143,73 +147,44 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => {
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Type")}
fullWidth
{...register("type", {
<Controller
control={control}
name="type"
rules={{
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 item xs={6}>
<TextField
label={t("description")}
fullWidth
disabled
{...register("description")}
/>
</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}>
<Controller
control={control}
@@ -234,6 +209,82 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => {
)}
/>
</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}>
<Stack
direction="row"
@@ -250,20 +301,6 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo }) => {
>
{isEditMode ? t("Save") : t("Confirm")}
</Button>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button
variant="outlined"
startIcon={<RestartAlt />}
onClick={() => reset()}
>
{t("Reset")}
</Button>
</Stack>
</Grid>
{/* <Grid item xs={6}>


+ 78
- 7
src/components/ItemsSearch/ItemsSearch.tsx Parādīt failu

@@ -8,6 +8,7 @@ import SearchResults, { Column } from "../SearchResults";
import { EditNote } from "@mui/icons-material";
import { useRouter, useSearchParams } from "next/navigation";
import { GridDeleteIcon } from "@mui/x-data-grid";
import { Chip } from "@mui/material";
import { TypeEnum } from "@/app/utils/typeEnum";
import axios from "axios";
import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api";
@@ -19,6 +20,10 @@ type Props = {
type SearchQuery = Partial<Omit<ItemsResult, "id">>;
type SearchParamNames = keyof SearchQuery;

type ItemsResultWithStatus = ItemsResult & {
status?: "complete" | "missing";
};

const ItemsSearch: React.FC<Props> = ({ items }) => {
const [filteredItems, setFilteredItems] = useState<ItemsResult[]>(items);
const { t } = useTranslation("items");
@@ -47,7 +52,31 @@ const ItemsSearch: React.FC<Props> = ({ items }) => {

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",
@@ -63,6 +92,22 @@ const ItemsSearch: React.FC<Props> = ({ items }) => {
name: "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",
label: t(""),
@@ -70,7 +115,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => {
onClick: onDeleteClick,
},
],
[onDeleteClick, onDetailClick, t],
[onDeleteClick, onDetailClick, t, checkItemStatus],
);

const refetchData = useCallback(
@@ -89,9 +134,35 @@ const ItemsSearch: React.FC<Props> = ({ items }) => {
`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`,
{ 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) {
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);
return response; // Return the data from the response
} else {
@@ -102,7 +173,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => {
throw error; // Rethrow the error for further handling
}
},
[pagingController.pageNum, pagingController.pageSize],
[pagingController.pageNum, pagingController.pageSize, checkItemStatus],
);

useEffect(() => {
@@ -137,8 +208,8 @@ const ItemsSearch: React.FC<Props> = ({ items }) => {
}}
onReset={onReset}
/>
<SearchResults<ItemsResult>
items={filteredItems}
<SearchResults<ItemsResultWithStatus>
items={filteredItems as ItemsResultWithStatus[]}
columns={columns}
setPagingController={setPagingController}
pagingController={pagingController}


+ 1
- 1
src/components/NavigationContent/NavigationContent.tsx Parādīt failu

@@ -266,7 +266,7 @@ const NavigationContent: React.FC = () => {
},
{
icon: <RequestQuote />,
label: "Shop",
label: "ShopAndTruck",
path: "/settings/shop",
},
{


+ 34
- 0
src/components/SearchBox/SearchBox.tsx Parādīt failu

@@ -18,6 +18,7 @@ import Search from "@mui/icons-material/Search";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-hk";
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 { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import {
@@ -96,6 +97,10 @@ interface DateCriterion<T extends string> extends BaseCriterion<T> {
type: "date";
}

interface TimeCriterion<T extends string> extends BaseCriterion<T> {
type: "time";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
@@ -103,6 +108,7 @@ export type Criterion<T extends string> =
| DateRangeCriterion<T>
| DatetimeRangeCriterion<T>
| DateCriterion<T>
| TimeCriterion<T>
| MultiSelectCriterion<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 = () => {
setInputs(defaultInputs);
onReset?.();
@@ -524,6 +539,25 @@ function SearchBox<T extends string>({
</Box>
</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>
);
})}


+ 18
- 10
src/components/Shop/TruckLane.tsx Parādīt failu

@@ -74,7 +74,15 @@ const TruckLane: React.FC = () => {
setError(null);
try {
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) {
console.error("Failed to load truck lanes:", err);
setError(err?.message ?? String(err) ?? t("Failed to load truck lanes"));
@@ -140,9 +148,13 @@ const TruckLane: React.FC = () => {
};

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>[] = [
{ 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" },
];

@@ -195,14 +207,13 @@ const TruckLane: React.FC = () => {
<TableCell>{t("TruckLance Code")}</TableCell>
<TableCell>{t("Departure Time")}</TableCell>
<TableCell>{t("Store ID")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedRows.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
<TableCell colSpan={4} align="center">
<Typography variant="body2" color="text.secondary">
{t("No Truck Lane data available")}
</Typography>
@@ -231,9 +242,6 @@ const TruckLane: React.FC = () => {
<TableCell>
{displayStoreId}
</TableCell>
<TableCell>
{String(truck.remark || "-")}
</TableCell>
<TableCell align="right">
<Button
size="small"


+ 51
- 28
src/components/Shop/TruckLaneDetail.tsx Parādīt failu

@@ -27,7 +27,7 @@ import CancelIcon from "@mui/icons-material/Cancel";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeAndRemarkClient, deleteTruckLaneClient, updateLoadingSequenceClient } from "@/app/api/shop/client";
import { 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
@@ -60,7 +60,9 @@ const TruckLaneDetail: React.FC = () => {
const { t } = useTranslation("common");
const router = useRouter();
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 [shopsData, setShopsData] = useState<ShopAndTruck[]>([]);
@@ -77,9 +79,16 @@ const TruckLaneDetail: React.FC = () => {
});

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 (!truckId) {
setError(t("Truck ID is required"));
if (!truckLanceCode) {
setError(t("TruckLance Code is required"));
setLoading(false);
return;
}
@@ -88,17 +97,22 @@ const TruckLaneDetail: React.FC = () => {
setError(null);
try {
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) {
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 {
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);
@@ -106,14 +120,31 @@ const TruckLaneDetail: React.FC = () => {
};

fetchTruckLaneDetail();
}, [truckId]);
}, [truckLanceCode, truckLanceCodeParam, t]);

const fetchShopsByTruckLane = async (truckLanceCode: string, remark: string) => {
const fetchShopsByTruckLane = async (truckLanceCode: string) => {
setShopsLoading(true);
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) {
console.error("Failed to load shops:", err);
setSnackbar({
@@ -178,8 +209,8 @@ const TruckLaneDetail: React.FC = () => {
});

// Refresh the shops list
if (truckData) {
await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || ""));
if (truckLanceCode) {
await fetchShopsByTruckLane(truckLanceCode);
}
setEditingRowIndex(null);
} catch (err: any) {
@@ -218,8 +249,8 @@ const TruckLaneDetail: React.FC = () => {
});
// Refresh the shops list
if (truckData) {
await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || ""));
if (truckLanceCode) {
await fetchShopsByTruckLane(truckLanceCode);
}
} catch (err: any) {
console.error("Failed to delete truck lane:", err);
@@ -323,14 +354,6 @@ const TruckLaneDetail: React.FC = () => {
</Typography>
</Grid>

<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
{t("Remark")}
</Typography>
<Typography variant="body1" sx={{ mt: 1 }}>
{String(truckData.remark || "-")}
</Typography>
</Grid>
</Grid>
</Paper>
</CardContent>
@@ -353,8 +376,8 @@ const TruckLaneDetail: React.FC = () => {
<TableRow>
<TableCell>{t("Shop Name")}</TableCell>
<TableCell>{t("Shop Code")}</TableCell>
<TableCell>{t("Loading Sequence")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Loading Sequence")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
</TableRow>
</TableHead>
@@ -376,6 +399,9 @@ const TruckLaneDetail: React.FC = () => {
<TableCell>
{String(shop.code || "-")}
</TableCell>
<TableCell>
{String(shop.remark || "-")}
</TableCell>
<TableCell>
{editingRowIndex === index ? (
<TextField
@@ -399,9 +425,6 @@ const TruckLaneDetail: React.FC = () => {
})()
)}
</TableCell>
<TableCell>
{String(shop.remark || "-")}
</TableCell>
<TableCell align="right">
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
{editingRowIndex === index ? (


+ 11
- 8
src/components/StockIn/ShelfLifeInput.tsx Parādīt failu

@@ -9,6 +9,7 @@ interface ShelfLifeInputProps {
onChange?: (value: number) => void;
label?: string;
sx?: any;
showHelperText?: boolean; // Option to show/hide the helper text
}

const ShelfLifeContainer = styled(Box)(({ theme }) => ({
@@ -61,7 +62,7 @@ const formatDuration = (years: number, months: number, days: number) => {
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 { years, months, days } = daysToDuration(value);
@@ -101,7 +102,7 @@ const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = (
};

return (
<Box sx={{ width: '100%', paddingLeft: '1rem' }}>
<Box sx={{ width: '100%' }}>
<ShelfLifeContainer>
<TextField
label="年"
@@ -140,12 +141,14 @@ const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = (
size="small"
/>
</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>
);
};


+ 4
- 0
src/i18n/en/common.json Parādīt failu

@@ -1,5 +1,6 @@
{
"Grade {{grade}}": "Grade {{grade}}",
<<<<<<< Updated upstream
"General Data": "General Data",
"Repair and Maintenance": "Repair and Maintenance",
"Repair and Maintenance Status": "Repair and Maintenance Status",
@@ -17,4 +18,7 @@
"No": "No",
"Equipment Name": "Equipment Name",
"Equipment Code": "Equipment Code"
=======
"ShopAndTruck": "ShopAndTruck"
>>>>>>> Stashed changes
}

+ 13
- 1
src/i18n/en/items.json Parādīt failu

@@ -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"
}

+ 2
- 1
src/i18n/zh/common.json Parādīt failu

@@ -298,6 +298,7 @@
"Submitting...": "提交中...",
"Batch Count": "批數",
"Shop": "店鋪",
"ShopAndTruck": "店鋪路線管理",
"Shop Information": "店鋪資訊",
"Shop Name": "店鋪名稱",
"Shop Code": "店鋪編號",
@@ -346,7 +347,7 @@
"Contact Name": "聯絡人",
"Addr1": "地址1",
"Addr2": "地址2",
"Addr3": "地址3",
"Addr3": "地址",
"Shop not found": "找不到店鋪",
"Shop ID is required": "需要店鋪ID",
"Invalid Shop ID": "無效的店鋪ID",


+ 12
- 1
src/i18n/zh/items.json Parādīt failu

@@ -32,5 +32,16 @@
"Reset": "重置",
"Search": "搜尋",
"Release": "發佈",
"Actions": "操作"
"Actions": "操作",
"LocationCode": "位置",
"DefaultLocationCode": "預設位置",
"Special Type": "特殊類型",
"None": "無",
"isEgg": "雞蛋",
"isFee": "費用",
"isBag": "袋子",
"Back": "返回",
"Status": "狀態",
"Complete": "完成",
"Missing Data": "缺少資料"
}

Notiek ielāde…
Atcelt
Saglabāt