Procházet zdrojové kódy

Supporting Function: Warehouse Handle

master
B.E.N.S.O.N před 3 týdny
rodič
revize
7c22768bd2
21 změnil soubory, kde provedl 952 přidání a 4 odebrání
  1. +21
    -0
      src/app/(main)/settings/warehouse/create/page.tsx
  2. +45
    -0
      src/app/(main)/settings/warehouse/page.tsx
  3. +57
    -1
      src/app/api/warehouse/actions.ts
  4. +7
    -0
      src/app/api/warehouse/index.ts
  5. +148
    -0
      src/components/CreateWarehouse/CreateWarehouse.tsx
  6. +29
    -0
      src/components/CreateWarehouse/CreateWarehouseLoading.tsx
  7. +15
    -0
      src/components/CreateWarehouse/CreateWarehouseWrapper.tsx
  8. +139
    -0
      src/components/CreateWarehouse/WarehouseDetail.tsx
  9. +1
    -0
      src/components/CreateWarehouse/index.ts
  10. +1
    -1
      src/components/NavigationContent/NavigationContent.tsx
  11. +2
    -0
      src/components/SearchBox/SearchBox.tsx
  12. +364
    -0
      src/components/WarehouseHandle/WarehouseHandle.tsx
  13. +40
    -0
      src/components/WarehouseHandle/WarehouseHandleLoading.tsx
  14. +19
    -0
      src/components/WarehouseHandle/WarehouseHandleWrapper.tsx
  15. +1
    -0
      src/components/WarehouseHandle/index.ts
  16. +1
    -0
      src/i18n/en/common.json
  17. +3
    -1
      src/i18n/en/user.json
  18. +27
    -0
      src/i18n/en/warehouse.json
  19. +2
    -0
      src/i18n/zh/common.json
  20. +3
    -1
      src/i18n/zh/user.json
  21. +27
    -0
      src/i18n/zh/warehouse.json

+ 21
- 0
src/app/(main)/settings/warehouse/create/page.tsx Zobrazit soubor

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

+ 45
- 0
src/app/(main)/settings/warehouse/page.tsx Zobrazit soubor

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

+ 57
- 1
src/app/api/warehouse/actions.ts Zobrazit soubor

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


+ 7
- 0
src/app/api/warehouse/index.ts Zobrazit soubor

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


+ 148
- 0
src/components/CreateWarehouse/CreateWarehouse.tsx Zobrazit soubor

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

+ 29
- 0
src/components/CreateWarehouse/CreateWarehouseLoading.tsx Zobrazit soubor

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

+ 15
- 0
src/components/CreateWarehouse/CreateWarehouseWrapper.tsx Zobrazit soubor

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

+ 139
- 0
src/components/CreateWarehouse/WarehouseDetail.tsx Zobrazit soubor

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

+ 1
- 0
src/components/CreateWarehouse/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./CreateWarehouseWrapper";

+ 1
- 1
src/components/NavigationContent/NavigationContent.tsx Zobrazit soubor

@@ -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 />,


+ 2
- 0
src/components/SearchBox/SearchBox.tsx Zobrazit soubor

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


+ 364
- 0
src/components/WarehouseHandle/WarehouseHandle.tsx Zobrazit soubor

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

+ 40
- 0
src/components/WarehouseHandle/WarehouseHandleLoading.tsx Zobrazit soubor

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

+ 19
- 0
src/components/WarehouseHandle/WarehouseHandleWrapper.tsx Zobrazit soubor

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

+ 1
- 0
src/components/WarehouseHandle/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./WarehouseHandleWrapper";

+ 1
- 0
src/i18n/en/common.json Zobrazit soubor

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


+ 3
- 1
src/i18n/en/user.json Zobrazit soubor

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

+ 27
- 0
src/i18n/en/warehouse.json Zobrazit soubor

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

+ 2
- 0
src/i18n/zh/common.json Zobrazit soubor

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


+ 3
- 1
src/i18n/zh/user.json Zobrazit soubor

@@ -28,5 +28,7 @@
"user": "用戶", "user": "用戶",
"qrcode": "二維碼", "qrcode": "二維碼",
"staffNo": "員工編號", "staffNo": "員工編號",
"Rows per page": "每頁行數"
"Rows per page": "每頁行數",
"Delete Success": "刪除成功",
"Do you want to delete?": "您確定要刪除嗎?"
} }

+ 27
- 0
src/i18n/zh/warehouse.json Zobrazit soubor

@@ -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": "搜尋"
}

Načítá se…
Zrušit
Uložit