| @@ -0,0 +1,21 @@ | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import React, { Suspense } from "react"; | |||||
| import { Typography } from "@mui/material"; | |||||
| import CreateWarehouse from "@/components/CreateWarehouse"; | |||||
| const CreateWarehousePage: React.FC = async () => { | |||||
| const { t } = await getServerI18n("warehouse"); | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Create Warehouse")}</Typography> | |||||
| <I18nProvider namespaces={["warehouse", "common"]}> | |||||
| <Suspense fallback={<CreateWarehouse.Loading />}> | |||||
| <CreateWarehouse /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreateWarehousePage; | |||||
| @@ -0,0 +1,45 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n, I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { Suspense } from "react"; | |||||
| import { Stack } from "@mui/material"; | |||||
| import { Button } from "@mui/material"; | |||||
| import Link from "next/link"; | |||||
| import WarehouseHandle from "@/components/WarehouseHandle"; | |||||
| import Add from "@mui/icons-material/Add"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Warehouse Management", | |||||
| }; | |||||
| const Warehouse: React.FC = async () => { | |||||
| const { t } = await getServerI18n("warehouse"); | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Warehouse")} | |||||
| </Typography> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/settings/warehouse/create" | |||||
| > | |||||
| {t("Create Warehouse")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <I18nProvider namespaces={["warehouse", "common", "dashboard"]}> | |||||
| <Suspense fallback={<WarehouseHandle.Loading />}> | |||||
| <WarehouseHandle /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Warehouse; | |||||
| @@ -1,7 +1,63 @@ | |||||
| "use server"; | "use server"; | ||||
| import { serverFetchString } from "@/app/utils/fetchUtil"; | |||||
| import { serverFetchString, serverFetchWithNoContent, serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { revalidateTag } from "next/cache"; | |||||
| import { WarehouseResult } from "./index"; | |||||
| import { cache } from "react"; | |||||
| export interface WarehouseInputs { | |||||
| code?: string; | |||||
| name?: string; | |||||
| description?: string; | |||||
| capacity?: number; | |||||
| store_id?: string; | |||||
| warehouse?: string; | |||||
| area?: string; | |||||
| slot?: string; | |||||
| stockTakeSection?: string; | |||||
| } | |||||
| export const fetchWarehouseDetail = cache(async (id: number) => { | |||||
| return serverFetchJson<WarehouseResult>(`${BASE_API_URL}/warehouse/${id}`, { | |||||
| next: { tags: ["warehouse"] }, | |||||
| }); | |||||
| }); | |||||
| export const createWarehouse = async (data: WarehouseInputs) => { | |||||
| const newWarehouse = await serverFetchWithNoContent(`${BASE_API_URL}/warehouse/save`, { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| revalidateTag("warehouse"); | |||||
| return newWarehouse; | |||||
| }; | |||||
| export const editWarehouse = async (id: number, data: WarehouseInputs) => { | |||||
| const updatedWarehouse = await serverFetchWithNoContent(`${BASE_API_URL}/warehouse/${id}`, { | |||||
| method: "PUT", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| revalidateTag("warehouse"); | |||||
| return updatedWarehouse; | |||||
| }; | |||||
| export const deleteWarehouse = async (id: number) => { | |||||
| try { | |||||
| const result = await serverFetchJson<WarehouseResult[]>(`${BASE_API_URL}/warehouse/${id}`, { | |||||
| method: "DELETE", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| revalidateTag("warehouse"); | |||||
| return result; | |||||
| } catch (error) { | |||||
| console.error("Error deleting warehouse:", error); | |||||
| revalidateTag("warehouse"); | |||||
| throw error; | |||||
| } | |||||
| }; | |||||
| export const importWarehouse = async (data: FormData) => { | export const importWarehouse = async (data: FormData) => { | ||||
| const importWarehouse = await serverFetchString<string>( | const importWarehouse = await serverFetchString<string>( | ||||
| @@ -4,10 +4,17 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| export interface WarehouseResult { | export interface WarehouseResult { | ||||
| action: any; | |||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| name: string; | name: string; | ||||
| description: string; | description: string; | ||||
| store_id?: string; | |||||
| warehouse?: string; | |||||
| area?: string; | |||||
| slot?: string; | |||||
| order?: number; | |||||
| stockTakeSection?: string; | |||||
| } | } | ||||
| export interface WarehouseCombo { | export interface WarehouseCombo { | ||||
| @@ -0,0 +1,148 @@ | |||||
| "use client"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import React, { | |||||
| useCallback, | |||||
| useEffect, | |||||
| useState, | |||||
| } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| Button, | |||||
| Stack, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { | |||||
| FormProvider, | |||||
| SubmitErrorHandler, | |||||
| SubmitHandler, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import { Check, Close, RestartAlt } from "@mui/icons-material"; | |||||
| import { | |||||
| WarehouseInputs, | |||||
| createWarehouse, | |||||
| } from "@/app/api/warehouse/actions"; | |||||
| import WarehouseDetail from "./WarehouseDetail"; | |||||
| const CreateWarehouse: React.FC = () => { | |||||
| const { t } = useTranslation(["warehouse", "common"]); | |||||
| const formProps = useForm<WarehouseInputs>(); | |||||
| const router = useRouter(); | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const resetForm = React.useCallback((e?: React.MouseEvent<HTMLButtonElement>) => { | |||||
| e?.preventDefault(); | |||||
| e?.stopPropagation(); | |||||
| try { | |||||
| formProps.reset({ | |||||
| store_id: "", | |||||
| warehouse: "", | |||||
| area: "", | |||||
| slot: "", | |||||
| stockTakeSection: "", | |||||
| }); | |||||
| } catch (error) { | |||||
| console.log(error); | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| }, [formProps, t]); | |||||
| useEffect(() => { | |||||
| resetForm(); | |||||
| }, []); | |||||
| const handleCancel = () => { | |||||
| router.back(); | |||||
| }; | |||||
| const onSubmit = useCallback<SubmitHandler<WarehouseInputs>>( | |||||
| async (data) => { | |||||
| try { | |||||
| // Automatically append "F" to store_id if not already present | |||||
| // Remove any existing "F" to avoid duplication, then append it | |||||
| const cleanStoreId = (data.store_id || "").replace(/F$/i, "").trim(); | |||||
| const storeIdWithF = cleanStoreId ? `${cleanStoreId}F` : ""; | |||||
| // Generate code, name, description from the input fields | |||||
| // Format: store_idF-warehouse-area-slot (F is automatically appended) | |||||
| const code = storeIdWithF | |||||
| ? `${storeIdWithF}-${data.warehouse || ""}-${data.area || ""}-${data.slot || ""}` | |||||
| : `${data.warehouse || ""}-${data.area || ""}-${data.slot || ""}`; | |||||
| const name = storeIdWithF | |||||
| ? `${storeIdWithF}-${data.warehouse || ""}` | |||||
| : `${data.warehouse || ""}`; | |||||
| const description = storeIdWithF | |||||
| ? `${storeIdWithF}-${data.warehouse || ""}` | |||||
| : `${data.warehouse || ""}`; | |||||
| const warehouseData: WarehouseInputs = { | |||||
| ...data, | |||||
| store_id: storeIdWithF, // Save with F (F is automatically appended) | |||||
| code: code.trim(), | |||||
| name: name.trim(), | |||||
| description: description.trim(), | |||||
| capacity: 10000, // Default capacity | |||||
| }; | |||||
| await createWarehouse(warehouseData); | |||||
| router.replace("/settings/warehouse"); | |||||
| } catch (e) { | |||||
| console.log(e); | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| }, | |||||
| [router, t], | |||||
| ); | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<WarehouseInputs>>( | |||||
| (errors) => { | |||||
| console.log(errors); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| {serverError && ( | |||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
| {serverError} | |||||
| </Typography> | |||||
| )} | |||||
| <FormProvider {...formProps}> | |||||
| <Stack | |||||
| spacing={2} | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
| > | |||||
| <WarehouseDetail /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="text" | |||||
| startIcon={<RestartAlt />} | |||||
| onClick={(e) => { | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| resetForm(e); | |||||
| }} | |||||
| type="button" | |||||
| > | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Close />} | |||||
| onClick={handleCancel} | |||||
| type="button" | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button variant="contained" startIcon={<Check />} type="submit"> | |||||
| {t("Confirm")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreateWarehouse; | |||||
| @@ -0,0 +1,29 @@ | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Skeleton from "@mui/material/Skeleton"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import React from "react"; | |||||
| export const CreateWarehouseLoading: React.FC = () => { | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreateWarehouseLoading; | |||||
| @@ -0,0 +1,15 @@ | |||||
| import React from "react"; | |||||
| import CreateWarehouse from "./CreateWarehouse"; | |||||
| import CreateWarehouseLoading from "./CreateWarehouseLoading"; | |||||
| interface SubComponents { | |||||
| Loading: typeof CreateWarehouseLoading; | |||||
| } | |||||
| const CreateWarehouseWrapper: React.FC & SubComponents = async () => { | |||||
| return <CreateWarehouse />; | |||||
| }; | |||||
| CreateWarehouseWrapper.Loading = CreateWarehouseLoading; | |||||
| export default CreateWarehouseWrapper; | |||||
| @@ -0,0 +1,139 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Card, | |||||
| CardContent, | |||||
| Stack, | |||||
| TextField, | |||||
| Typography, | |||||
| Box, | |||||
| InputAdornment, | |||||
| } from "@mui/material"; | |||||
| import { useFormContext, Controller } from "react-hook-form"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { WarehouseInputs } from "@/app/api/warehouse/actions"; | |||||
| const WarehouseDetail: React.FC = () => { | |||||
| const { t } = useTranslation("warehouse"); | |||||
| const { | |||||
| register, | |||||
| control, | |||||
| formState: { errors }, | |||||
| } = useFormContext<WarehouseInputs>(); | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Warehouse Detail")} | |||||
| </Typography> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| alignItems: "flex-start", | |||||
| gap: 1, | |||||
| flexWrap: "nowrap", | |||||
| justifyContent: "flex-start", | |||||
| }} | |||||
| > | |||||
| {/* 樓層 field with F inside on the right - F is automatically generated */} | |||||
| <Controller | |||||
| name="store_id" | |||||
| control={control} | |||||
| rules={{ required: t("store_id") + " " + t("is required") }} | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| label={t("store_id")} | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| InputProps={{ | |||||
| endAdornment: ( | |||||
| <InputAdornment position="end">F</InputAdornment> | |||||
| ), | |||||
| }} | |||||
| onChange={(e) => { | |||||
| // Automatically remove "F" if user tries to type it (F is auto-generated) | |||||
| const value = e.target.value.replace(/F/gi, "").trim(); | |||||
| field.onChange(value); | |||||
| }} | |||||
| error={Boolean(errors.store_id)} | |||||
| helperText={errors.store_id?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| <Typography variant="body1" sx={{ mx: 0.5, mt: 1.5 }}> | |||||
| - | |||||
| </Typography> | |||||
| {/* 倉庫 field */} | |||||
| <Controller | |||||
| name="warehouse" | |||||
| control={control} | |||||
| rules={{ required: t("warehouse") + " " + t("is required") }} | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| label={t("warehouse")} | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| error={Boolean(errors.warehouse)} | |||||
| helperText={errors.warehouse?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| <Typography variant="body1" sx={{ mx: 0.5, mt: 1.5 }}> | |||||
| - | |||||
| </Typography> | |||||
| {/* 區域 field */} | |||||
| <Controller | |||||
| name="area" | |||||
| control={control} | |||||
| rules={{ required: t("area") + " " + t("is required") }} | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| label={t("area")} | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| error={Boolean(errors.area)} | |||||
| helperText={errors.area?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| <Typography variant="body1" sx={{ mx: 0.5, mt: 1.5 }}> | |||||
| - | |||||
| </Typography> | |||||
| {/* 儲位 field */} | |||||
| <Controller | |||||
| name="slot" | |||||
| control={control} | |||||
| rules={{ required: t("slot") + " " + t("is required") }} | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| label={t("slot")} | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| error={Boolean(errors.slot)} | |||||
| helperText={errors.slot?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| {/* stockTakeSection field in the same row */} | |||||
| <Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}> | |||||
| <TextField | |||||
| label={t("stockTakeSection")} | |||||
| fullWidth | |||||
| size="small" | |||||
| {...register("stockTakeSection")} | |||||
| error={Boolean(errors.stockTakeSection)} | |||||
| helperText={errors.stockTakeSection?.message} | |||||
| /> | |||||
| </Box> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default WarehouseDetail; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./CreateWarehouseWrapper"; | |||||
| @@ -299,7 +299,7 @@ const NavigationContent: React.FC = () => { | |||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "Warehouse", | label: "Warehouse", | ||||
| path: "/settings/user", | |||||
| path: "/settings/warehouse", | |||||
| }, | }, | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| @@ -52,6 +52,7 @@ interface OptionWithLabel<T extends string> { | |||||
| interface TextCriterion<T extends string> extends BaseCriterion<T> { | interface TextCriterion<T extends string> extends BaseCriterion<T> { | ||||
| type: "text"; | type: "text"; | ||||
| placeholder?: string; | |||||
| } | } | ||||
| interface SelectCriterion<T extends string> extends BaseCriterion<T> { | interface SelectCriterion<T extends string> extends BaseCriterion<T> { | ||||
| @@ -286,6 +287,7 @@ function SearchBox<T extends string>({ | |||||
| <TextField | <TextField | ||||
| label={t(c.label)} | label={t(c.label)} | ||||
| fullWidth | fullWidth | ||||
| placeholder={c.placeholder} | |||||
| onChange={makeInputChangeHandler(c.paramName)} | onChange={makeInputChangeHandler(c.paramName)} | ||||
| value={inputs[c.paramName]} | value={inputs[c.paramName]} | ||||
| /> | /> | ||||
| @@ -0,0 +1,364 @@ | |||||
| "use client"; | |||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import SearchResults, { Column } from "../SearchResults/index"; | |||||
| import DeleteIcon from "@mui/icons-material/Delete"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | |||||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||||
| import { deleteWarehouse } from "@/app/api/warehouse/actions"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import CardActions from "@mui/material/CardActions"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
| import Search from "@mui/icons-material/Search"; | |||||
| import InputAdornment from "@mui/material/InputAdornment"; | |||||
| interface Props { | |||||
| warehouses: WarehouseResult[]; | |||||
| } | |||||
| type SearchQuery = Partial<Omit<WarehouseResult, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const { t } = useTranslation(["warehouse", "common"]); | |||||
| const [filteredWarehouse, setFilteredWarehouse] = useState(warehouses); | |||||
| const [pagingController, setPagingController] = useState({ | |||||
| pageNum: 1, | |||||
| pageSize: 10, | |||||
| }); | |||||
| const router = useRouter(); | |||||
| const [isSearching, setIsSearching] = useState(false); | |||||
| const [searchInputs, setSearchInputs] = useState({ | |||||
| store_id: "", | |||||
| warehouse: "", | |||||
| area: "", | |||||
| slot: "", | |||||
| stockTakeSection: "", | |||||
| }); | |||||
| const onDeleteClick = useCallback((warehouse: WarehouseResult) => { | |||||
| deleteDialog(async () => { | |||||
| try { | |||||
| await deleteWarehouse(warehouse.id); | |||||
| setFilteredWarehouse(prev => prev.filter(w => w.id !== warehouse.id)); | |||||
| router.refresh(); | |||||
| successDialog(t("Delete Success"), t); | |||||
| } catch (error) { | |||||
| console.error("Failed to delete warehouse:", error); | |||||
| // Don't redirect on error, just show error message | |||||
| // The error will be logged but user stays on the page | |||||
| } | |||||
| }, t); | |||||
| }, [t, router]); | |||||
| const handleReset = useCallback(() => { | |||||
| setSearchInputs({ | |||||
| store_id: "", | |||||
| warehouse: "", | |||||
| area: "", | |||||
| slot: "", | |||||
| stockTakeSection: "", | |||||
| }); | |||||
| setFilteredWarehouse(warehouses); | |||||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | |||||
| }, [warehouses, pagingController.pageSize]); | |||||
| const handleSearch = useCallback(() => { | |||||
| setIsSearching(true); | |||||
| try { | |||||
| let results: WarehouseResult[] = warehouses; | |||||
| // Build search pattern from the four fields: store_idF-warehouse-area-slot | |||||
| // Only search by code field - match the code that follows this pattern | |||||
| const storeId = searchInputs.store_id?.trim() || ""; | |||||
| const warehouse = searchInputs.warehouse?.trim() || ""; | |||||
| const area = searchInputs.area?.trim() || ""; | |||||
| const slot = searchInputs.slot?.trim() || ""; | |||||
| const stockTakeSection = searchInputs.stockTakeSection?.trim() || ""; | |||||
| // If any field has a value, filter by code pattern and stockTakeSection | |||||
| if (storeId || warehouse || area || slot || stockTakeSection) { | |||||
| results = warehouses.filter((warehouseItem) => { | |||||
| // Filter by stockTakeSection if provided | |||||
| if (stockTakeSection) { | |||||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | |||||
| if (!itemStockTakeSection.includes(stockTakeSection.toLowerCase())) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| // Filter by code pattern if any code-related field is provided | |||||
| if (storeId || warehouse || area || slot) { | |||||
| if (!warehouseItem.code) { | |||||
| return false; | |||||
| } | |||||
| const codeValue = String(warehouseItem.code).toLowerCase(); | |||||
| // Check if code matches the pattern: store_id-warehouse-area-slot | |||||
| // Match each part if provided | |||||
| const codeParts = codeValue.split("-"); | |||||
| if (codeParts.length >= 4) { | |||||
| const codeStoreId = codeParts[0] || ""; | |||||
| const codeWarehouse = codeParts[1] || ""; | |||||
| const codeArea = codeParts[2] || ""; | |||||
| const codeSlot = codeParts[3] || ""; | |||||
| const storeIdMatch = !storeId || codeStoreId.includes(storeId.toLowerCase()); | |||||
| const warehouseMatch = !warehouse || codeWarehouse.includes(warehouse.toLowerCase()); | |||||
| const areaMatch = !area || codeArea.includes(area.toLowerCase()); | |||||
| const slotMatch = !slot || codeSlot.includes(slot.toLowerCase()); | |||||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | |||||
| } | |||||
| // Fallback: if code doesn't follow the pattern, check if it contains any of the search terms | |||||
| const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase()); | |||||
| const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase()); | |||||
| const areaMatch = !area || codeValue.includes(area.toLowerCase()); | |||||
| const slotMatch = !slot || codeValue.includes(slot.toLowerCase()); | |||||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | |||||
| } | |||||
| // If only stockTakeSection is provided, return true (already filtered above) | |||||
| return true; | |||||
| }); | |||||
| } else { | |||||
| // If no search terms, show all warehouses | |||||
| results = warehouses; | |||||
| } | |||||
| setFilteredWarehouse(results); | |||||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | |||||
| } catch (error) { | |||||
| console.error("Error searching warehouses:", error); | |||||
| // Fallback: filter by code pattern and stockTakeSection | |||||
| const storeId = searchInputs.store_id?.trim().toLowerCase() || ""; | |||||
| const warehouse = searchInputs.warehouse?.trim().toLowerCase() || ""; | |||||
| const area = searchInputs.area?.trim().toLowerCase() || ""; | |||||
| const slot = searchInputs.slot?.trim().toLowerCase() || ""; | |||||
| const stockTakeSection = searchInputs.stockTakeSection?.trim().toLowerCase() || ""; | |||||
| setFilteredWarehouse( | |||||
| warehouses.filter((warehouseItem) => { | |||||
| // Filter by stockTakeSection if provided | |||||
| if (stockTakeSection) { | |||||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | |||||
| if (!itemStockTakeSection.includes(stockTakeSection)) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| // Filter by code if any code-related field is provided | |||||
| if (storeId || warehouse || area || slot) { | |||||
| if (!warehouseItem.code) { | |||||
| return false; | |||||
| } | |||||
| const codeValue = String(warehouseItem.code).toLowerCase(); | |||||
| const codeParts = codeValue.split("-"); | |||||
| if (codeParts.length >= 4) { | |||||
| const storeIdMatch = !storeId || codeParts[0].includes(storeId); | |||||
| const warehouseMatch = !warehouse || codeParts[1].includes(warehouse); | |||||
| const areaMatch = !area || codeParts[2].includes(area); | |||||
| const slotMatch = !slot || codeParts[3].includes(slot); | |||||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | |||||
| } | |||||
| return (!storeId || codeValue.includes(storeId)) && | |||||
| (!warehouse || codeValue.includes(warehouse)) && | |||||
| (!area || codeValue.includes(area)) && | |||||
| (!slot || codeValue.includes(slot)); | |||||
| } | |||||
| return true; | |||||
| }) | |||||
| ); | |||||
| } finally { | |||||
| setIsSearching(false); | |||||
| } | |||||
| }, [searchInputs, warehouses, pagingController.pageSize]); | |||||
| const columns = useMemo<Column<WarehouseResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "code", | |||||
| label: t("code"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "store_id", | |||||
| label: t("store_id"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "warehouse", | |||||
| label: t("warehouse"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "area", | |||||
| label: t("area"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "slot", | |||||
| label: t("slot"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "order", | |||||
| label: t("order"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "stockTakeSection", | |||||
| label: t("stockTakeSection"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "action", | |||||
| label: t("Delete"), | |||||
| onClick: onDeleteClick, | |||||
| buttonIcon: <DeleteIcon />, | |||||
| color: "error", | |||||
| sx: { width: "10%", minWidth: "80px" }, | |||||
| }, | |||||
| ], | |||||
| [t, onDeleteClick], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
| <Typography variant="overline">{t("Search Criteria")}</Typography> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| alignItems: "center", | |||||
| gap: 1, | |||||
| flexWrap: "nowrap", | |||||
| justifyContent: "flex-start", | |||||
| }} | |||||
| > | |||||
| {/* 樓層 field with F inside on the right */} | |||||
| <TextField | |||||
| label={t("store_id")} | |||||
| value={searchInputs.store_id} | |||||
| onChange={(e) => | |||||
| setSearchInputs((prev) => ({ ...prev, store_id: e.target.value })) | |||||
| } | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| InputProps={{ | |||||
| endAdornment: ( | |||||
| <InputAdornment position="end">F</InputAdornment> | |||||
| ), | |||||
| }} | |||||
| /> | |||||
| <Typography variant="body1" sx={{ mx: 0.5 }}> | |||||
| - | |||||
| </Typography> | |||||
| {/* 倉庫 field */} | |||||
| <TextField | |||||
| label={t("warehouse")} | |||||
| value={searchInputs.warehouse} | |||||
| onChange={(e) => | |||||
| setSearchInputs((prev) => ({ ...prev, warehouse: e.target.value })) | |||||
| } | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| /> | |||||
| <Typography variant="body1" sx={{ mx: 0.5 }}> | |||||
| - | |||||
| </Typography> | |||||
| {/* 區域 field */} | |||||
| <TextField | |||||
| label={t("area")} | |||||
| value={searchInputs.area} | |||||
| onChange={(e) => | |||||
| setSearchInputs((prev) => ({ ...prev, area: e.target.value })) | |||||
| } | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| /> | |||||
| <Typography variant="body1" sx={{ mx: 0.5 }}> | |||||
| - | |||||
| </Typography> | |||||
| {/* 儲位 field */} | |||||
| <TextField | |||||
| label={t("slot")} | |||||
| value={searchInputs.slot} | |||||
| onChange={(e) => | |||||
| setSearchInputs((prev) => ({ ...prev, slot: e.target.value })) | |||||
| } | |||||
| size="small" | |||||
| sx={{ width: "150px", minWidth: "120px" }} | |||||
| /> | |||||
| {/* 盤點區域 field */} | |||||
| <Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}> | |||||
| <TextField | |||||
| label={t("stockTakeSection")} | |||||
| value={searchInputs.stockTakeSection} | |||||
| onChange={(e) => | |||||
| setSearchInputs((prev) => ({ ...prev, stockTakeSection: e.target.value })) | |||||
| } | |||||
| size="small" | |||||
| fullWidth | |||||
| /> | |||||
| </Box> | |||||
| </Box> | |||||
| <CardActions sx={{ justifyContent: "flex-start", px: 0, pt: 1 }}> | |||||
| <Button | |||||
| variant="text" | |||||
| startIcon={<RestartAlt />} | |||||
| onClick={handleReset} | |||||
| > | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Search />} | |||||
| onClick={handleSearch} | |||||
| > | |||||
| {t("Search")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <SearchResults<WarehouseResult> | |||||
| items={filteredWarehouse} | |||||
| columns={columns} | |||||
| pagingController={pagingController} | |||||
| setPagingController={setPagingController} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default WarehouseHandle; | |||||
| @@ -0,0 +1,40 @@ | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Skeleton from "@mui/material/Skeleton"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import React from "react"; | |||||
| // Can make this nicer | |||||
| export const WarehouseHandleLoading: React.FC = () => { | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default WarehouseHandleLoading; | |||||
| @@ -0,0 +1,19 @@ | |||||
| import React from "react"; | |||||
| import WarehouseHandle from "./WarehouseHandle"; | |||||
| import WarehouseHandleLoading from "./WarehouseHandleLoading"; | |||||
| import { WarehouseResult, fetchWarehouseList } from "@/app/api/warehouse"; | |||||
| interface SubComponents { | |||||
| Loading: typeof WarehouseHandleLoading; | |||||
| } | |||||
| const WarehouseHandleWrapper: React.FC & SubComponents = async () => { | |||||
| const warehouses = await fetchWarehouseList(); | |||||
| console.log(warehouses); | |||||
| return <WarehouseHandle warehouses={warehouses} />; | |||||
| }; | |||||
| WarehouseHandleWrapper.Loading = WarehouseHandleLoading; | |||||
| export default WarehouseHandleWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./WarehouseHandleWrapper"; | |||||
| @@ -12,6 +12,7 @@ | |||||
| "Equipment not found": "Equipment not found", | "Equipment not found": "Equipment not found", | ||||
| "Error saving data": "Error saving data", | "Error saving data": "Error saving data", | ||||
| "Cancel": "Cancel", | "Cancel": "Cancel", | ||||
| "Do you want to delete?": "Do you want to delete?", | |||||
| "Save": "Save", | "Save": "Save", | ||||
| "Yes": "Yes", | "Yes": "Yes", | ||||
| "No": "No", | "No": "No", | ||||
| @@ -14,5 +14,7 @@ | |||||
| "User ID": "用戶ID", | "User ID": "用戶ID", | ||||
| "User Name": "用戶名稱", | "User Name": "用戶名稱", | ||||
| "User Group": "用戶群組", | "User Group": "用戶群組", | ||||
| "Authority": "權限" | |||||
| "Authority": "權限", | |||||
| "Delete Success": "Delete Success", | |||||
| "Do you want to delete?": "Do you want to delete?" | |||||
| } | } | ||||
| @@ -0,0 +1,27 @@ | |||||
| { | |||||
| "Create Warehouse": "Create Warehouse", | |||||
| "Edit Warehouse": "Edit Warehouse", | |||||
| "Warehouse Detail": "Warehouse Detail", | |||||
| "code": "Code", | |||||
| "name": "Name", | |||||
| "description": "Description", | |||||
| "Edit": "Edit", | |||||
| "Delete": "Delete", | |||||
| "Delete Success": "Delete Success", | |||||
| "Warehouse": "Warehouse", | |||||
| "warehouse": "warehouse", | |||||
| "Rows per page": "Rows per page", | |||||
| "capacity": "Capacity", | |||||
| "store_id": "Store ID", | |||||
| "area": "Area", | |||||
| "slot": "Slot", | |||||
| "order": "Order", | |||||
| "stockTakeSection": "Stock Take Section", | |||||
| "Do you want to delete?": "Do you want to delete?", | |||||
| "Cancel": "Cancel", | |||||
| "Reset": "Reset", | |||||
| "Confirm": "Confirm", | |||||
| "is required": "is required", | |||||
| "Search Criteria": "Search Criteria", | |||||
| "Search": "Search" | |||||
| } | |||||
| @@ -68,6 +68,7 @@ | |||||
| "Setup Time": "生產前預備時間", | "Setup Time": "生產前預備時間", | ||||
| "Changeover Time": "生產後轉換時間", | "Changeover Time": "生產後轉換時間", | ||||
| "Warehouse": "倉庫", | "Warehouse": "倉庫", | ||||
| "warehouse": "倉庫", | |||||
| "Supplier": "供應商", | "Supplier": "供應商", | ||||
| "Purchase Order": "採購單", | "Purchase Order": "採購單", | ||||
| "Demand Forecast": "需求預測", | "Demand Forecast": "需求預測", | ||||
| @@ -259,6 +260,7 @@ | |||||
| "Seq No Remark": "序號明細", | "Seq No Remark": "序號明細", | ||||
| "Stock Available": "庫存可用", | "Stock Available": "庫存可用", | ||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| "Do you want to delete?": "您確定要刪除嗎?", | |||||
| "Stock Status": "庫存狀態", | "Stock Status": "庫存狀態", | ||||
| "Target Production Date": "目標生產日期", | "Target Production Date": "目標生產日期", | ||||
| "id": "ID", | "id": "ID", | ||||
| @@ -28,5 +28,7 @@ | |||||
| "user": "用戶", | "user": "用戶", | ||||
| "qrcode": "二維碼", | "qrcode": "二維碼", | ||||
| "staffNo": "員工編號", | "staffNo": "員工編號", | ||||
| "Rows per page": "每頁行數" | |||||
| "Rows per page": "每頁行數", | |||||
| "Delete Success": "刪除成功", | |||||
| "Do you want to delete?": "您確定要刪除嗎?" | |||||
| } | } | ||||
| @@ -0,0 +1,27 @@ | |||||
| { | |||||
| "Create Warehouse": "新增倉庫", | |||||
| "Edit Warehouse": "編輯倉庫資料", | |||||
| "Warehouse Detail": "倉庫詳細資料", | |||||
| "code": "編號", | |||||
| "name": "名稱", | |||||
| "description": "描述", | |||||
| "Edit": "編輯", | |||||
| "Delete": "刪除", | |||||
| "Delete Success": "刪除成功", | |||||
| "Warehouse": "倉庫", | |||||
| "warehouse": "倉庫", | |||||
| "Rows per page": "每頁行數", | |||||
| "capacity": "容量", | |||||
| "store_id": "樓層", | |||||
| "area": "區域", | |||||
| "slot": "位置", | |||||
| "order": "提料單次序", | |||||
| "stockTakeSection": "盤點區域", | |||||
| "Do you want to delete?": "您確定要刪除嗎?", | |||||
| "Cancel": "取消", | |||||
| "Reset": "重置", | |||||
| "Confirm": "確認", | |||||
| "is required": "必填", | |||||
| "Search Criteria": "搜尋條件", | |||||
| "Search": "搜尋" | |||||
| } | |||||