| @@ -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"; | |||
| import { serverFetchString } from "@/app/utils/fetchUtil"; | |||
| import { serverFetchString, serverFetchWithNoContent, serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| 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) => { | |||
| const importWarehouse = await serverFetchString<string>( | |||
| @@ -4,10 +4,17 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| export interface WarehouseResult { | |||
| action: any; | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| description: string; | |||
| store_id?: string; | |||
| warehouse?: string; | |||
| area?: string; | |||
| slot?: string; | |||
| order?: number; | |||
| stockTakeSection?: string; | |||
| } | |||
| 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 />, | |||
| label: "Warehouse", | |||
| path: "/settings/user", | |||
| path: "/settings/warehouse", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| @@ -52,6 +52,7 @@ interface OptionWithLabel<T extends string> { | |||
| interface TextCriterion<T extends string> extends BaseCriterion<T> { | |||
| type: "text"; | |||
| placeholder?: string; | |||
| } | |||
| interface SelectCriterion<T extends string> extends BaseCriterion<T> { | |||
| @@ -286,6 +287,7 @@ function SearchBox<T extends string>({ | |||
| <TextField | |||
| label={t(c.label)} | |||
| fullWidth | |||
| placeholder={c.placeholder} | |||
| onChange={makeInputChangeHandler(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", | |||
| "Error saving data": "Error saving data", | |||
| "Cancel": "Cancel", | |||
| "Do you want to delete?": "Do you want to delete?", | |||
| "Save": "Save", | |||
| "Yes": "Yes", | |||
| "No": "No", | |||
| @@ -14,5 +14,7 @@ | |||
| "User ID": "用戶ID", | |||
| "User Name": "用戶名稱", | |||
| "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": "生產前預備時間", | |||
| "Changeover Time": "生產後轉換時間", | |||
| "Warehouse": "倉庫", | |||
| "warehouse": "倉庫", | |||
| "Supplier": "供應商", | |||
| "Purchase Order": "採購單", | |||
| "Demand Forecast": "需求預測", | |||
| @@ -259,6 +260,7 @@ | |||
| "Seq No Remark": "序號明細", | |||
| "Stock Available": "庫存可用", | |||
| "Confirm": "確認", | |||
| "Do you want to delete?": "您確定要刪除嗎?", | |||
| "Stock Status": "庫存狀態", | |||
| "Target Production Date": "目標生產日期", | |||
| "id": "ID", | |||
| @@ -28,5 +28,7 @@ | |||
| "user": "用戶", | |||
| "qrcode": "二維碼", | |||
| "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": "搜尋" | |||
| } | |||