Explorar el Código

Merge remote-tracking branch 'origin/master'

master
CANCERYS\kw093 hace 1 mes
padre
commit
bcc7866c1c
Se han modificado 36 ficheros con 2717 adiciones y 403 borrados
  1. +52
    -0
      src/app/(main)/settings/equipment/EquipmentTabs.tsx
  2. +29
    -0
      src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx
  3. +7
    -14
      src/app/(main)/settings/equipment/page.tsx
  4. +2
    -2
      src/app/(main)/settings/shop/detail/page.tsx
  5. +2
    -2
      src/app/(main)/settings/shop/page.tsx
  6. +16
    -0
      src/app/(main)/settings/shop/truckdetail/page.tsx
  7. +14
    -4
      src/app/api/scheduling/actions.ts
  8. +5
    -0
      src/app/api/settings/equipment/index.ts
  9. +8
    -0
      src/app/api/settings/item/actions.ts
  10. +8
    -0
      src/app/api/settings/item/index.ts
  11. +48
    -2
      src/app/api/shop/actions.ts
  12. +21
    -0
      src/app/api/shop/client.ts
  13. +4
    -1
      src/components/Breadcrumb/Breadcrumb.tsx
  14. +46
    -6
      src/components/CreateItem/CreateItem.tsx
  15. +12
    -1
      src/components/CreateItem/CreateItemWrapper.tsx
  16. +120
    -83
      src/components/CreateItem/ProductDetails.tsx
  17. +55
    -85
      src/components/DetailedSchedule/DetailedScheduleSearchView.tsx
  18. +1
    -1
      src/components/DetailedScheduleDetail/DetailInfoCard.tsx
  19. +28
    -9
      src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx
  20. +229
    -40
      src/components/EquipmentSearch/EquipmentSearch.tsx
  21. +482
    -0
      src/components/EquipmentSearch/EquipmentSearchResults.tsx
  22. +24
    -17
      src/components/EquipmentSearch/EquipmentSearchWrapper.tsx
  23. +78
    -7
      src/components/ItemsSearch/ItemsSearch.tsx
  24. +1
    -1
      src/components/NavigationContent/NavigationContent.tsx
  25. +34
    -0
      src/components/SearchBox/SearchBox.tsx
  26. +171
    -55
      src/components/Shop/Shop.tsx
  27. +59
    -61
      src/components/Shop/ShopDetail.tsx
  28. +277
    -0
      src/components/Shop/TruckLane.tsx
  29. +497
    -0
      src/components/Shop/TruckLaneDetail.tsx
  30. +11
    -8
      src/components/StockIn/ShelfLifeInput.tsx
  31. +242
    -0
      src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx
  32. +21
    -1
      src/i18n/en/common.json
  33. +13
    -1
      src/i18n/en/items.json
  34. +88
    -1
      src/i18n/zh/common.json
  35. +12
    -1
      src/i18n/zh/items.json
  36. +0
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt

+ 52
- 0
src/app/(main)/settings/equipment/EquipmentTabs.tsx Ver fichero

@@ -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;

+ 29
- 0
src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx Ver fichero

@@ -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;

+ 7
- 14
src/app/(main)/settings/equipment/page.tsx Ver fichero

@@ -1,15 +1,18 @@
import { TypeEnum } from "@/app/utils/typeEnum";
import EquipmentSearch from "@/components/EquipmentSearch";
import { getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
import { fetchAllEquipments } from "@/app/api/settings/equipment";
import { I18nProvider } from "@/i18n";
import EquipmentSearchWrapper from "@/components/EquipmentSearch/EquipmentSearchWrapper";

export const metadata: Metadata = {
title: "Equipment Type",
};
@@ -17,8 +20,6 @@ export const metadata: Metadata = {
const productSetting: React.FC = async () => {
const type = "common";
const { t } = await getServerI18n(type);
const equipments = await fetchAllEquipments();
// preloadClaims();

return (
<>
@@ -31,22 +32,14 @@ const productSetting: React.FC = async () => {
<Typography variant="h4" marginInlineEnd={2}>
{t("Equipment")}
</Typography>
{/* <Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="product/create"
>
{t("Create product")}
</Button> */}
</Stack>
<Suspense fallback={<EquipmentSearch.Loading />}>
<Suspense fallback={<EquipmentSearchWrapper.Loading />}>
<I18nProvider namespaces={["common", "project"]}>
<EquipmentSearch />
<EquipmentSearchWrapper />
</I18nProvider>
</Suspense>
</>
);
};

export default productSetting;
export default productSetting;

+ 2
- 2
src/app/(main)/settings/shop/detail/page.tsx Ver fichero

@@ -4,9 +4,9 @@ import { I18nProvider, getServerI18n } from "@/i18n";
import GeneralLoading from "@/components/General/GeneralLoading";

export default async function ShopDetailPage() {
const { t } = await getServerI18n("shop");
const { t } = await getServerI18n("shop", "common");
return (
<I18nProvider namespaces={["shop"]}>
<I18nProvider namespaces={["shop", "common"]}>
<Suspense fallback={<GeneralLoading />}>
<ShopDetail />
</Suspense>


+ 2
- 2
src/app/(main)/settings/shop/page.tsx Ver fichero

@@ -8,9 +8,9 @@ import { notFound } from "next/navigation";


export default async function ShopPage() {
const { t } = await getServerI18n("shop");
const { t } = await getServerI18n("shop", "common");
return (
<I18nProvider namespaces={["shop"]}>
<I18nProvider namespaces={["shop", "common"]}>
<Suspense fallback={<ShopWrapper.Loading />}>
<ShopWrapper />
</Suspense>


+ 16
- 0
src/app/(main)/settings/shop/truckdetail/page.tsx Ver fichero

@@ -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>
);
}


+ 14
- 4
src/app/api/scheduling/actions.ts Ver fichero

@@ -177,22 +177,32 @@ export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) =>
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");

const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, {
method: "POST",
headers: {
"Content-Type": "application/json", // Critical for @RequestBody
"Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Authorization": `Bearer ${token}`
}
},
// Send everything in one object
body: JSON.stringify({
...inputs,
prodHeaders,
matHeaders
})
});

if (!response.ok) throw new Error(`Backend error: ${response.status}`);

const arrayBuffer = await response.arrayBuffer();
// Convert to Base64 so Next.js can send it safely over the wire
return Buffer.from(arrayBuffer).toString('base64');
};



+ 5
- 0
src/app/api/settings/equipment/index.ts Ver fichero

@@ -13,7 +13,12 @@ export type EquipmentResult = {
name: string;
description: string | undefined;
equipmentTypeId: string | number | undefined;
equipmentCode?: string;
action?: any;
repairAndMaintenanceStatus?: boolean | number;
latestRepairAndMaintenanceDate?: string | Date;
lastRepairAndMaintenanceDate?: string | Date;
repairAndMaintenanceRemarks?: string;
};

export type Result = {


+ 8
- 0
src/app/api/settings/item/actions.ts Ver fichero

@@ -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 Ver fichero

@@ -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 = {


+ 48
- 2
src/app/api/shop/actions.ts Ver fichero

@@ -24,9 +24,11 @@ export interface ShopAndTruck{
contactName: String;
truckLanceCode: String;
DepartureTime: String;
LoadingSequence: number;
LoadingSequence?: number | null;
districtReference: Number;
Store_id: Number
Store_id: Number;
remark?: String | null;
truckId?: number;
}

export interface Shop{
@@ -60,6 +62,11 @@ export interface DeleteTruckLane {
id: number;
}

export interface UpdateLoadingSequenceRequest {
id: number;
loadingSequence: number;
}

export interface SaveTruckRequest {
id?: number | null;
store_id: string;
@@ -132,6 +139,45 @@ export const deleteTruckLaneAction = async (data: DeleteTruckLane) => {
export const createTruckAction = async (data: SaveTruckRequest) => {
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, {
method: "POST",
body: JSON.stringify(data),


+ 21
- 0
src/app/api/shop/client.ts Ver fichero

@@ -6,9 +6,14 @@ import {
updateTruckLaneAction,
deleteTruckLaneAction,
createTruckAction,
findAllUniqueTruckLaneCombinationsAction,
findAllShopsByTruckLanceCodeAndRemarkAction,
findAllShopsByTruckLanceCodeAction,
updateLoadingSequenceAction,
type SaveTruckLane,
type DeleteTruckLane,
type SaveTruckRequest,
type UpdateLoadingSequenceRequest,
type MessageResponse
} from "./actions";

@@ -32,4 +37,20 @@ export const createTruckClient = async (data: SaveTruckRequest): Promise<Message
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;

+ 4
- 1
src/components/Breadcrumb/Breadcrumb.tsx Ver fichero

@@ -16,7 +16,10 @@ const pathToLabelMap: { [path: string]: string } = {
"/settings/qcItem": "Qc Item",
"/settings/qrCodeHandle": "QR Code Handle",
"/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/edit": "FG & Material Demand Forecast Detail",
"/scheduling/detailed": "Detail Scheduling",


+ 46
- 6
src/components/CreateItem/CreateItem.tsx Ver fichero

@@ -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 Ver fichero

@@ -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 Ver fichero

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


+ 55
- 85
src/components/DetailedSchedule/DetailedScheduleSearchView.tsx Ver fichero

@@ -50,6 +50,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
>([]);
const { t } = useTranslation("schedule");
const { setIsUploading } = useUploadContext();
const today = dayjs().format("YYYY-MM-DD");

const router = useRouter();
// const [filterObj, setFilterObj] = useState({});
@@ -58,7 +59,10 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
defaultPagingController,
);
const [totalCount, setTotalCount] = useState(0);
const [inputs, setInputs] = useState(defaultInputs);
const [inputs, setInputs] = useState({
...defaultInputs,
produceAt: dayjs().format("YYYY-MM-DD"),
});
const typeOptions = [
{
value: "detailed",
@@ -78,7 +82,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
// paramName: "schedulePeriod",
// 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"),
// paramName: "totalEstProdCount",
@@ -179,18 +183,9 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
) as ScheduleType[];

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,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
@@ -221,77 +216,24 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
refetchData(inputs, "paging");
}, [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(() => {
//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 () => {
try {
setIsUploading(true)
const response = await testDetailedSchedule(inputs.scheduleAt)
if (response) {
refetchData(inputs, "paging");
//refetchData(inputs, "paging");
setPagingController(prev => ({ ...prev, pageNum: 1 }));
refetchData(inputs, "search");
}
} catch(e) {
console.log(e)
@@ -304,7 +246,35 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
try {
const token = localStorage.getItem("accessToken");
// 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
const byteCharacters = atob(base64String);
@@ -366,14 +336,14 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
<SearchBox
criteria={searchCriteria}
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),
types: query.types as unknown as ScheduleType[],
}));
refetchData(query, "search");
};
setInputs(updatedInputs);
refetchData(updatedInputs, "search");
}}
onReset={onReset}
/>


+ 1
- 1
src/components/DetailedScheduleDetail/DetailInfoCard.tsx Ver fichero

@@ -70,7 +70,7 @@ const DetailInfoCard: React.FC<Props> = ({
// {...register("scheduleAt", {
// required: "Schedule At required!",
// })}
defaultValue={`${arrayToDateString(getValues("scheduleAt"))}`}
defaultValue={`${arrayToDateString(getValues("produceAt"))}`}
// defaultValue={details?.scheduledPeriod}
disabled={!isEditing}
// error={Boolean(errors.name)}


+ 28
- 9
src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx Ver fichero

@@ -163,36 +163,55 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
}, [formProps, setIsUploading])

// --- NEW FUNCTION: GLOBAL RELEASE FOR THE ENTIRE SCHEDULE ---
// --- UPDATED FUNCTION: GLOBAL RELEASE WITH produceAt CHECK ---
const onGlobalReleaseClick = useCallback(async () => {
if (!scheduleId) {
setServerError(t("Cannot release. Schedule ID is missing."));
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);
setServerError(""); // Clear previous errors
setServerError("");

try {
// **IMPORTANT**: Ensure 'releaseProdSchedule' is implemented in your actions file
// to call the '/productionSchedule/detail/detailed/release' endpoint.
const response = await releaseProdSchedule({
id: Number(scheduleId),
})
});

if (response) {
router.refresh();
router.refresh();
}

} catch (e) {
console.error(e);
setServerError(t("An unexpected error occurred during global schedule release."));
} finally {
setIsUploading(false);
}
}, [scheduleId, setIsUploading, t, router]);
}, [scheduleId, setIsUploading, t, router, getValues]);
// --------------------------------------------------------------------
const [tempValue, setTempValue] = useState<string | number | null>(null)


+ 229
- 40
src/components/EquipmentSearch/EquipmentSearch.tsx Ver fichero

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { EquipmentResult } from "@/app/api/settings/equipment";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EquipmentSearchResults, { Column } from "./EquipmentSearchResults";
import { EditNote } from "@mui/icons-material";
import { useRouter, useSearchParams } from "next/navigation";
import { GridDeleteIcon } from "@mui/x-data-grid";
@@ -12,32 +12,90 @@ import { TypeEnum } from "@/app/utils/typeEnum";
import axios from "axios";
import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api";
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 = {
equipments: EquipmentResult[];
tabIndex?: number;
};
type SearchQuery = Partial<Omit<EquipmentResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const EquipmentSearch: React.FC<Props> = ({ equipments }) => {
const EquipmentSearch: React.FC<Props> = ({ equipments, tabIndex = 0 }) => {
const [filteredEquipments, setFilteredEquipments] =
useState<EquipmentResult[]>(equipments);
useState<EquipmentResult[]>([]);
const { t } = useTranslation("common");
const router = useRouter();
const [filterObj, setFilterObj] = useState({});
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 10,
// totalCount: 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>[] = [
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("Description"), paramName: "description", type: "text" },
];
return searchCriteria;
}, [t, equipments]);
}, [t, tabIndex]);

const onDetailClick = useCallback(
(equipment: EquipmentResult) => {
@@ -46,12 +104,19 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => {
[router],
);

const onMaintenanceEditClick = useCallback(
(equipment: EquipmentResult) => {
router.push(`/settings/equipment/MaintenanceEdit?id=${equipment.id}`);
},
[router],
);

const onDeleteClick = useCallback(
(equipment: EquipmentResult) => {},
[router],
);

const columns = useMemo<Column<EquipmentResult>[]>(
const generalDataColumns = useMemo<Column<EquipmentResult>[]>(
() => [
{
name: "id",
@@ -78,9 +143,91 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => {
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> {
records: T[];
@@ -89,73 +236,115 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => {

const refetchData = useCallback(
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;
}

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 = {
pageNum: pagingController.pageNum,
pageSize: pagingController.pageSize,
...filterObj,
...transformedFilter,
};
try {
const endpoint = tabIndex === 1
? `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage`
: `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`;
const response = await axiosInstance.get<ApiResponse<EquipmentResult>>(
`${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`,
endpoint,
{ 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) {
setFilteredEquipments(response.data.records);
setTotalCount(response.data.total);
return response;
setFilteredEquipments(response.data.records || []);
setTotalCount(response.data.total || 0);
} else {
throw "400";
}
} catch (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(() => {
refetchData(filterObj);
}, [filterObj, pagingController.pageNum, pagingController.pageSize]);
if (isReady) {
refetchData(filterObj);
}
}, [filterObj, pagingController.pageNum, pagingController.pageSize, tabIndex, isReady, refetchData]);

const onReset = useCallback(() => {
setFilteredEquipments(equipments);
}, [equipments]);
setFilterObj({});
setPagingController({
pageNum: 1,
pageSize: pagingController.pageSize,
});
}, [pagingController.pageSize]);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// setFilteredItems(
// equipmentTypes.filter((pm) => {
// return (
// pm.code.toLowerCase().includes(query.code.toLowerCase()) &&
// pm.name.toLowerCase().includes(query.name.toLowerCase())
// );
// })
// );
setFilterObj({
...query,
});
}}
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;

+ 482
- 0
src/components/EquipmentSearch/EquipmentSearchResults.tsx Ver fichero

@@ -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;

+ 24
- 17
src/components/EquipmentSearch/EquipmentSearchWrapper.tsx Ver fichero

@@ -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 EquipmentSearchLoading from "./EquipmentSearchLoading";
import EquipmentTabs from "@/app/(main)/settings/equipment/EquipmentTabs";
import { useSearchParams } from "next/navigation";

interface SubComponents {
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;

export default EquipmentSearchWrapper;
export default EquipmentSearchWrapper;

+ 78
- 7
src/components/ItemsSearch/ItemsSearch.tsx Ver fichero

@@ -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 Ver fichero

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


+ 34
- 0
src/components/SearchBox/SearchBox.tsx Ver fichero

@@ -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>
);
})}


+ 171
- 55
src/components/Shop/Shop.tsx Ver fichero

@@ -10,14 +10,22 @@ import {
Alert,
CircularProgress,
Chip,
Tabs,
Tab,
Select,
MenuItem,
FormControl,
InputLabel,
} from "@mui/material";
import { useState, useMemo, useCallback, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import SearchBox, { Criterion } from "../SearchBox";
import SearchResults, { Column } from "../SearchResults";
import { defaultPagingController } from "../SearchResults/SearchResults";
import { fetchAllShopsClient } from "@/app/api/shop/client";
import type { Shop, ShopAndTruck } from "@/app/api/shop/actions";
import TruckLane from "./TruckLane";

type ShopRow = Shop & {
actions?: string;
@@ -33,17 +41,20 @@ type SearchQuery = {
type SearchParamNames = keyof SearchQuery;

const Shop: React.FC = () => {
const { t } = useTranslation("common");
const router = useRouter();
const [activeTab, setActiveTab] = useState<number>(0);
const [rows, setRows] = useState<ShopRow[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [filters, setFilters] = useState<Record<string, string>>({});
const [statusFilter, setStatusFilter] = useState<string>("all");
const [pagingController, setPagingController] = useState(defaultPagingController);

// client-side filtered rows (contains-matching)
// client-side filtered rows (contains-matching + status filter)
const filteredRows = useMemo(() => {
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
for (const k of fKeys) {
const v = String((filters as any)[k] ?? "").trim();
@@ -63,8 +74,16 @@ const Shop: React.FC = () => {
}
return true;
});
// Apply status filter
if (statusFilter !== "all") {
normalized = normalized.filter((r) => {
return r.truckLanceStatus === statusFilter;
});
}
return normalized;
}, [rows, filters]);
}, [rows, filters, statusFilter]);

// Check if a shop has missing truckLanceCode data
const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => {
@@ -72,16 +91,73 @@ const Shop: React.FC = () => {
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
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";
}
}
@@ -149,50 +225,50 @@ const Shop: React.FC = () => {
);

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>[] = [
{
name: "id",
label: "Id",
label: t("id"),
type: "integer",
renderCell: (item) => String(item.id ?? ""),
},
{
name: "code",
label: "Code",
label: t("Code"),
renderCell: (item) => String(item.code ?? ""),
},
{
name: "name",
label: "Name",
label: t("Name"),
renderCell: (item) => String(item.name ?? ""),
},
{
name: "addr3",
label: "Addr3",
label: t("Addr3"),
renderCell: (item) => String((item as any).addr3 ?? ""),
},
{
name: "truckLanceStatus",
label: "TruckLance Status",
label: t("TruckLance Status"),
renderCell: (item) => {
const status = item.truckLanceStatus;
if (status === "complete") {
return <Chip label="Complete" color="success" size="small" />;
return <Chip label={t("Complete")} color="success" size="small" />;
} else if (status === "missing") {
return <Chip label="Missing Data" color="warning" size="small" />;
return <Chip label={t("Missing Data")} color="warning" size="small" />;
} else {
return <Chip label="No TruckLance" color="error" size="small" />;
return <Chip label={t("No TruckLance")} color="error" size="small" />;
}
},
},
{
name: "actions",
label: "Actions",
label: t("Actions"),
headerAlign: "right",
renderCell: (item) => (
<Button
@@ -200,56 +276,96 @@ const Shop: React.FC = () => {
variant="outlined"
onClick={() => handleViewDetail(item)}
>
View Detail
{t("View Detail")}
</Button>
),
},
];

useEffect(() => {
fetchAllShops();
}, []);
if (activeTab === 0) {
fetchAllShops();
}
}, [activeTab]);

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};

return (
<Box>
<Card sx={{ mb: 2 }}>
<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>
</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>
);
};


+ 59
- 61
src/components/Shop/ShopDetail.tsx Ver fichero

@@ -38,6 +38,7 @@ import AddIcon from "@mui/icons-material/Add";
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useTranslation } from "react-i18next";
import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions";
import {
fetchAllShopsClient,
@@ -131,6 +132,7 @@ const parseDepartureTimeForBackend = (time: string): string => {
};

const ShopDetail: React.FC = () => {
const { t } = useTranslation("common");
const router = useRouter();
const searchParams = useSearchParams();
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 (sessionStatus === "unauthenticated" || !session) {
setError("Please log in to view shop details");
setError(t("Please log in to view shop details"));
setLoading(false);
return;
}

const fetchShopDetail = async () => {
if (!shopId) {
setError("Shop ID is required");
setError(t("Shop ID is required"));
setLoading(false);
return;
}
@@ -178,7 +180,7 @@ const ShopDetail: React.FC = () => {
// Convert shopId to number for proper filtering
const shopIdNum = parseInt(shopId, 10);
if (isNaN(shopIdNum)) {
setError("Invalid Shop ID");
setError(t("Invalid Shop ID"));
setLoading(false);
return;
}
@@ -212,7 +214,7 @@ const ShopDetail: React.FC = () => {
contactName: shopData.contactName ?? "",
});
} else {
setError("Shop not found");
setError(t("Shop not found"));
setLoading(false);
return;
}
@@ -233,7 +235,7 @@ const ShopDetail: React.FC = () => {
} catch (err: any) {
console.error("Failed to load shop detail:", err);
// Handle errors gracefully - don't trigger auto-logout
const errorMessage = err?.message ?? String(err) ?? "Failed to load shop details";
const errorMessage = err?.message ?? String(err) ?? t("Failed to load shop details");
setError(errorMessage);
} finally {
setLoading(false);
@@ -273,13 +275,13 @@ const ShopDetail: React.FC = () => {

const handleSave = async (index: number) => {
if (!shopId) {
setError("Shop ID is required");
setError(t("Shop ID is required"));
return;
}

const truck = editedTruckData[index];
if (!truck || !truck.id) {
setError("Invalid truck data");
setError(t("Invalid shop data"));
return;
}

@@ -335,7 +337,7 @@ const ShopDetail: React.FC = () => {
setUniqueRemarks(remarks);
} catch (err: any) {
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 {
setSaving(false);
}
@@ -351,12 +353,12 @@ const ShopDetail: React.FC = () => {
};

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;
}

if (!shopId) {
setError("Shop ID is required");
setError(t("Shop ID is required"));
return;
}

@@ -373,7 +375,7 @@ const ShopDetail: React.FC = () => {
setEditingRowIndex(null);
} catch (err: any) {
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 {
setSaving(false);
}
@@ -409,19 +411,19 @@ const ShopDetail: React.FC = () => {
const missingFields: string[] = [];

if (!shopId || !shopDetail) {
missingFields.push("Shop information");
missingFields.push(t("Shop Information"));
}

if (!newTruck.truckLanceCode.trim()) {
missingFields.push("TruckLance Code");
missingFields.push(t("TruckLance Code"));
}

if (!newTruck.departureTime) {
missingFields.push("Departure Time");
missingFields.push(t("Departure Time"));
}

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);
setSnackbarOpen(true);
return;
@@ -461,7 +463,7 @@ const ShopDetail: React.FC = () => {
handleCloseAddDialog();
} catch (err: any) {
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 {
setSaving(false);
}
@@ -475,12 +477,12 @@ const ShopDetail: React.FC = () => {
);
}
if (error) {
return (
return (
<Box>
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
<Button onClick={() => router.back()}>Go Back</Button>
<Button onClick={() => router.back()}>{t("Back")}</Button>
</Box>
);
}
@@ -489,9 +491,9 @@ const ShopDetail: React.FC = () => {
return (
<Box>
<Alert severity="warning" sx={{ mb: 2 }}>
Shop not found
{t("Shop not found")}
</Alert>
<Button onClick={() => router.back()}>Go Back</Button>
<Button onClick={() => router.back()}>{t("Back")}</Button>
</Box>
);
}
@@ -501,49 +503,45 @@ const ShopDetail: React.FC = () => {
<Card sx={{ mb: 2 }}>
<CardContent>
<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 sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<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>
</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>
</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>
</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>
</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>
</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>
</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>
</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>
</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>
</Box>
</Box>
@@ -553,27 +551,27 @@ const ShopDetail: React.FC = () => {
<Card>
<CardContent>
<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
variant="contained"
startIcon={<AddIcon />}
onClick={handleOpenAddDialog}
disabled={editingRowIndex !== null || saving}
>
Add Truck Lane
{t("Add Truck Lane")}
</Button>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<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>
</TableHead>
<TableBody>
@@ -581,7 +579,7 @@ const ShopDetail: React.FC = () => {
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
No Truck data available
{t("No Truck data available")}
</Typography>
</TableCell>
</TableRow>
@@ -725,7 +723,7 @@ const ShopDetail: React.FC = () => {
<TextField
{...params}
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}
/>
)}
@@ -745,7 +743,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleSave(index)}
disabled={saving}
title="Save changes"
title={t("Save changes")}
>
<SaveIcon />
</IconButton>
@@ -754,7 +752,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleCancel(index)}
disabled={saving}
title="Cancel editing"
title={t("Cancel editing")}
>
<CancelIcon />
</IconButton>
@@ -766,7 +764,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleEdit(index)}
disabled={editingRowIndex !== null}
title="Edit truck lane"
title={t("Edit truck lane")}
>
<EditIcon />
</IconButton>
@@ -776,7 +774,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleDelete(truck.id!)}
disabled={saving || editingRowIndex !== null}
title="Delete truck lane"
title={t("Delete truck lane")}
>
<DeleteIcon />
</IconButton>
@@ -797,13 +795,13 @@ const ShopDetail: React.FC = () => {

{/* Add Truck Dialog */}
<Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth>
<DialogTitle>Add New Truck Lane</DialogTitle>
<DialogTitle>{t("Add New Truck Lane")}</DialogTitle>
<DialogContent>
<Box sx={{ pt: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
label="TruckLance Code"
label={t("TruckLance Code")}
fullWidth
required
value={newTruck.truckLanceCode}
@@ -813,7 +811,7 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={12}>
<TextField
label="Departure Time"
label={t("Departure Time")}
type="time"
fullWidth
required
@@ -830,7 +828,7 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={6}>
<TextField
label="Loading Sequence"
label={t("Loading Sequence")}
type="number"
fullWidth
value={newTruck.loadingSequence}
@@ -840,7 +838,7 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={6}>
<TextField
label="District Reference"
label={t("District Reference")}
type="number"
fullWidth
value={newTruck.districtReference}
@@ -850,10 +848,10 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>Store ID</InputLabel>
<InputLabel>{t("Store ID")}</InputLabel>
<Select
value={newTruck.storeId}
label="Store ID"
label={t("Store ID")}
onChange={(e) => {
const newStoreId = e.target.value;
setNewTruck({
@@ -884,9 +882,9 @@ const ShopDetail: React.FC = () => {
renderInput={(params) => (
<TextField
{...params}
label="Remark"
label={t("Remark")}
fullWidth
placeholder="Enter or select remark"
placeholder={t("Enter or select remark")}
disabled={saving}
/>
)}
@@ -898,7 +896,7 @@ const ShopDetail: React.FC = () => {
</DialogContent>
<DialogActions>
<Button onClick={handleCloseAddDialog} disabled={saving}>
Cancel
{t("Cancel")}
</Button>
<Button
onClick={handleCreateTruck}
@@ -906,7 +904,7 @@ const ShopDetail: React.FC = () => {
startIcon={<SaveIcon />}
disabled={saving}
>
{saving ? "Saving..." : "Save"}
{saving ? t("Submitting...") : t("Save")}
</Button>
</DialogActions>
</Dialog>


+ 277
- 0
src/components/Shop/TruckLane.tsx Ver fichero

@@ -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;


+ 497
- 0
src/components/Shop/TruckLaneDetail.tsx Ver fichero

@@ -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;



+ 11
- 8
src/components/StockIn/ShelfLifeInput.tsx Ver fichero

@@ -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>
);
};


+ 242
- 0
src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx Ver fichero

@@ -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;

+ 21
- 1
src/i18n/en/common.json Ver fichero

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

}

+ 13
- 1
src/i18n/en/items.json Ver fichero

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

+ 88
- 1
src/i18n/zh/common.json Ver fichero

@@ -301,5 +301,92 @@
"Total lines: ": "總數量:",
"Balance": "可用數量",
"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": "保存數據時出錯"
}

+ 12
- 1
src/i18n/zh/items.json Ver fichero

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

+ 0
- 0
src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt Ver fichero


Cargando…
Cancelar
Guardar