Bläddra i källkod

product process list and warehouse

reset-do-picking-order
CANCERYS\kw093 1 vecka sedan
förälder
incheckning
e4f0273a0e
18 ändrade filer med 1272 tillägg och 34 borttagningar
  1. +10
    -10
      src/app/(main)/settings/warehouse/page.tsx
  2. +7
    -2
      src/app/api/jo/actions.ts
  3. +61
    -2
      src/app/api/warehouse/actions.ts
  4. +7
    -0
      src/app/api/warehouse/index.ts
  5. +3
    -1
      src/components/CreateWarehouse/CreateWarehouse.tsx
  6. +8
    -0
      src/components/CreateWarehouse/WarehouseDetail.tsx
  7. +30
    -3
      src/components/ProductionProcess/ProductionProcessList.tsx
  8. +18
    -1
      src/components/StockIssue/SearchPage.tsx
  9. +67
    -6
      src/components/StockTakeManagement/PickerCardList.tsx
  10. +355
    -0
      src/components/Warehouse/TabStockTakeSectionMapping.tsx
  11. +520
    -0
      src/components/Warehouse/WarehouseHandle.tsx
  12. +40
    -0
      src/components/Warehouse/WarehouseHandleLoading.tsx
  13. +19
    -0
      src/components/Warehouse/WarehouseHandleWrapper.tsx
  14. +67
    -0
      src/components/Warehouse/WarehouseTabs.tsx
  15. +1
    -0
      src/components/Warehouse/index.ts
  16. +39
    -9
      src/components/WarehouseHandle/WarehouseHandle.tsx
  17. +2
    -0
      src/i18n/zh/common.json
  18. +18
    -0
      src/i18n/zh/warehouse.json

+ 10
- 10
src/app/(main)/settings/warehouse/page.tsx Visa fil

@@ -5,8 +5,10 @@ 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";
import WarehouseTabs from "@/components/Warehouse/WarehouseTabs";
import WarehouseHandleWrapper from "@/components/WarehouseHandle/WarehouseHandleWrapper";
import TabStockTakeSectionMapping from "@/components/Warehouse/TabStockTakeSectionMapping";

export const metadata: Metadata = {
title: "Warehouse Management",
@@ -16,12 +18,7 @@ const Warehouse: React.FC = async () => {
const { t } = await getServerI18n("warehouse");
return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Stack direction="row" justifyContent="space-between" flexWrap="wrap" rowGap={2}>
<Typography variant="h4" marginInlineEnd={2}>
{t("Warehouse")}
</Typography>
@@ -35,11 +32,14 @@ const Warehouse: React.FC = async () => {
</Button>
</Stack>
<I18nProvider namespaces={["warehouse", "common", "dashboard"]}>
<Suspense fallback={<WarehouseHandle.Loading />}>
<WarehouseHandle />
<Suspense fallback={null}>
<WarehouseTabs
tab0Content={<WarehouseHandleWrapper />}
tab1Content={<TabStockTakeSectionMapping />}
/>
</Suspense>
</I18nProvider>
</>
);
};
export default Warehouse;
export default Warehouse;

+ 7
- 2
src/app/api/jo/actions.ts Visa fil

@@ -349,6 +349,7 @@ export interface AllJoborderProductProcessInfoResponse {
jobOrderId: number;
timeNeedToComplete: number;
uom: string;
isDrink?: boolean | null;
stockInLineId: number;
jobOrderCode: string;
productProcessLineCount: number;
@@ -737,9 +738,13 @@ export const newUpdateProductProcessLineQrscan = cache(async (request: NewProduc
}
);
});
export const fetchAllJoborderProductProcessInfo = cache(async () => {
export const fetchAllJoborderProductProcessInfo = cache(async (isDrink?: boolean | null) => {
const query = isDrink !== undefined && isDrink !== null
? `?isDrink=${isDrink}`
: "";

return serverFetchJson<AllJoborderProductProcessInfoResponse[]>(
`${BASE_API_URL}/product-process/Demo/Process/all`,
`${BASE_API_URL}/product-process/Demo/Process/all${query}`,
{
method: "GET",
next: { tags: ["productProcess"] },


+ 61
- 2
src/app/api/warehouse/actions.ts Visa fil

@@ -3,7 +3,7 @@
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 { WarehouseResult, StockTakeSectionInfo } from "./index";
import { cache } from "react";

export interface WarehouseInputs {
@@ -17,6 +17,7 @@ export interface WarehouseInputs {
slot?: string;
order?: string;
stockTakeSection?: string;
stockTakeSectionDescription?: string;
}

export const fetchWarehouseDetail = cache(async (id: number) => {
@@ -81,4 +82,62 @@ export const importNewWarehouse = async (data: FormData) => {
},
);
return importWarehouse;
}
}

export const fetchStockTakeSections = cache(async () => {
return serverFetchJson<StockTakeSectionInfo[]>(`${BASE_API_URL}/warehouse/stockTakeSections`, {
next: { tags: ["warehouse"] },
});
});

export const updateSectionDescription = async (section: string, stockTakeSectionDescription: string | null) => {
await serverFetchWithNoContent(
`${BASE_API_URL}/warehouse/section/${encodeURIComponent(section)}/description`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ stockTakeSectionDescription }),
}
);
revalidateTag("warehouse");
};

export const clearWarehouseSection = async (warehouseId: number) => {
const result = await serverFetchJson<WarehouseResult>(
`${BASE_API_URL}/warehouse/${warehouseId}/clearSection`,
{ method: "POST" }
);
revalidateTag("warehouse");
return result;
};
export const getWarehousesBySection = cache(async (stockTakeSection: string) => {
const list = await serverFetchJson<WarehouseResult[]>(`${BASE_API_URL}/warehouse`, {
next: { tags: ["warehouse"] },
});
const items = Array.isArray(list) ? list : [];
return items.filter((w) => w.stockTakeSection === stockTakeSection);
});
export const searchWarehousesForAddToSection = cache(async (
params: { store_id?: string; warehouse?: string; area?: string; slot?: string },
currentSection: string
) => {
const list = await serverFetchJson<WarehouseResult[]>(`${BASE_API_URL}/warehouse`, {
next: { tags: ["warehouse"] },
});
const items = Array.isArray(list) ? list : [];
const storeId = params.store_id?.trim();
const warehouse = params.warehouse?.trim();
const area = params.area?.trim();
const slot = params.slot?.trim();

return items.filter((w) => {
if (w.stockTakeSection != null && w.stockTakeSection !== currentSection) return false;
if (!w.code) return true;
const parts = w.code.split("-");
if (storeId && parts[0] !== storeId) return false;
if (warehouse && parts[1] !== warehouse) return false;
if (area && parts[2] !== area) return false;
if (slot && parts[3] !== slot) return false;
return true;
});
});

+ 7
- 0
src/app/api/warehouse/index.ts Visa fil

@@ -15,6 +15,7 @@ export interface WarehouseResult {
slot?: string;
order?: string;
stockTakeSection?: string;
stockTakeSectionDescription?: string;
}

export interface WarehouseCombo {
@@ -34,3 +35,9 @@ export const fetchWarehouseCombo = cache(async () => {
next: { tags: ["warehouseCombo"] },
});
});
export interface StockTakeSectionInfo {
id: string;
stockTakeSection: string;
stockTakeSectionDescription: string | null;
warehouseCount: number;
}

+ 3
- 1
src/components/CreateWarehouse/CreateWarehouse.tsx Visa fil

@@ -41,6 +41,7 @@ const CreateWarehouse: React.FC = () => {
slot: "",
order: "",
stockTakeSection: "",
stockTakeSectionDescription: "",
});
} catch (error) {
console.log(error);
@@ -89,7 +90,8 @@ const CreateWarehouse: React.FC = () => {
router.replace("/settings/warehouse");
} catch (e) {
console.log(e);
setServerError(t("An error has occurred. Please try again later."));
const message = e instanceof Error ? e.message : t("An error has occurred. Please try again later.");
setServerError(message);
}
},
[router, t],


+ 8
- 0
src/components/CreateWarehouse/WarehouseDetail.tsx Visa fil

@@ -153,6 +153,14 @@ const WarehouseDetail: React.FC = () => {
helperText={errors.stockTakeSection?.message}
/>
</Box>
<Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}>
<TextField
label={t("stockTakeSectionDescription")}
fullWidth
size="small"
{...register("stockTakeSectionDescription")}
/>
</Box>

</Box>
</CardContent>


+ 30
- 3
src/components/ProductionProcess/ProductionProcessList.tsx Visa fil

@@ -52,7 +52,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
const [openModal, setOpenModal] = useState<boolean>(false);
const [modalInfo, setModalInfo] = useState<StockInLineInput>();
const currentUserId = session?.id ? parseInt(session.id) : undefined;

type ProcessFilter = "all" | "drink" | "other";
const [filter, setFilter] = useState<ProcessFilter>("all");
const [suggestedLocationCode, setSuggestedLocationCode] = useState<string | null>(null);
const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => {
if (!currentUserId) {
@@ -108,7 +109,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
const fetchProcesses = useCallback(async () => {
setLoading(true);
try {
const data = await fetchAllJoborderProductProcessInfo();
const isDrinkParam =
filter === "all" ? undefined : filter === "drink" ? true : false;
const data = await fetchAllJoborderProductProcessInfo(isDrinkParam);
setProcesses(data || []);
setPage(0);
} catch (e) {
@@ -117,7 +121,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
} finally {
setLoading(false);
}
}, []);
}, [filter]);

useEffect(() => {
fetchProcesses();
@@ -176,6 +180,29 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
</Box>
) : (
<Box>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap', mb: 2 }}>
<Button
variant={filter === 'all' ? 'contained' : 'outlined'}
size="small"
onClick={() => setFilter('all')}
>
{t("All")}
</Button>
<Button
variant={filter === 'drink' ? 'contained' : 'outlined'}
size="small"
onClick={() => setFilter('drink')}
>
{t("Drink")}
</Button>
<Button
variant={filter === 'other' ? 'contained' : 'outlined'}
size="small"
onClick={() => setFilter('other')}
>
{t("Other")}
</Button>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total processes")}: {processes.length}
</Typography>


+ 18
- 1
src/components/StockIssue/SearchPage.tsx Visa fil

@@ -98,6 +98,23 @@ const SearchPage: React.FC<Props> = ({ dataList }) => {
lotId = item.lotId;
itemId = item.itemId;
}
} else if (tab === "expiry") {
const item = expiryItems.find((i) => i.id === id);
if (!item) {
alert(t("Item not found"));
return;
}
try {
// 如果想要 loading 效果,可以这里把 id 加进 submittingIds
await submitExpiryItem(item.id, currentUserId);
// 成功后,从列表移除这一行,或直接 reload
// setExpiryItems(prev => prev.filter(i => i.id !== id));
window.location.reload();
} catch (e) {
alert(t("Failed to submit expiry item"));
}
return; // 记得 return,避免再走到下面的 lotId/itemId 分支
}

if (lotId && itemId) {
@@ -109,7 +126,7 @@ const SearchPage: React.FC<Props> = ({ dataList }) => {
alert(t("Item not found"));
}
},
[tab, currentUserId, t, missItems, badItems]
[tab, currentUserId, t, missItems, badItems, expiryItems]
);

const handleFormSuccess = useCallback(() => {


+ 67
- 6
src/components/StockTakeManagement/PickerCardList.tsx Visa fil

@@ -19,6 +19,7 @@ import {
DialogContentText,
DialogActions,
} from "@mui/material";
import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import duration from "dayjs/plugin/duration";
@@ -50,6 +51,58 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
const [total, setTotal] = useState(0);
const [creating, setCreating] = useState(false);
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All");
const [filterStockTakeSession, setFilterStockTakeSession] = useState<string>("");
type PickerSearchKey = "sectionDescription" | "stockTakeSession";
const sectionDescriptionOptions = Array.from(
new Set(
stockTakeSessions
.map((s) => s.stockTakeSectionDescription)
.filter((v): v is string => !!v)
)
);

// 按 description + section 双条件过滤
const filteredSessions = stockTakeSessions.filter((s) => {
const matchDesc =
filterSectionDescription === "All" ||
s.stockTakeSectionDescription === filterSectionDescription;

const matchSession =
!filterStockTakeSession ||
(s.stockTakeSession ?? "")
.toString()
.toLowerCase()
.includes(filterStockTakeSession.toLowerCase());

return matchDesc && matchSession;
});

// SearchBox 的条件配置
const criteria: Criterion<PickerSearchKey>[] = [
{
type: "select",
label: "Stock Take Section Description",
paramName: "sectionDescription",
options: sectionDescriptionOptions,
},
{
type: "text",
label: "Stock Take Section",
paramName: "stockTakeSession",
placeholder: "e.g. A01",
},
];

const handleSearch = (inputs: Record<PickerSearchKey | `${PickerSearchKey}To`, string>) => {
setFilterSectionDescription(inputs.sectionDescription || "All");
setFilterStockTakeSession(inputs.stockTakeSession || "");
};

const handleResetSearch = () => {
setFilterSectionDescription("All");
setFilterStockTakeSession("");
};
const fetchStockTakeSessions = useCallback(
async (pageNum: number, size: number) => {
setLoading(true);
@@ -188,8 +241,15 @@ const [total, setTotal] = useState(0);

return (
<Box>
<Box sx={{ width: "100%", mb: 2 }}>
<SearchBox<PickerSearchKey>
criteria={criteria}
onSearch={handleSearch}
onReset={handleResetSearch}
/>
</Box>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>


<Typography variant="body2" color="text.secondary">
{t("Total Sections")}: {stockTakeSessions.length}
@@ -209,7 +269,7 @@ const [total, setTotal] = useState(0);
</Box>

<Grid container spacing={2}>
{stockTakeSessions.map((session: AllPickedStockTakeListReponse) => {
{filteredSessions.map((session: AllPickedStockTakeListReponse) => {
const statusColor = getStatusColor(session.status || "");
const lastStockTakeDate = session.lastStockTakeDate
? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT)
@@ -229,10 +289,11 @@ const [total, setTotal] = useState(0);
>
<CardContent sx={{ pb: 1, flexGrow: 1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
<Typography variant="subtitle1" fontWeight={600}>
{t("Section")}: {session.stockTakeSession}
</Typography>
<Typography variant="subtitle1" fontWeight={600}>
{t("Section")}: {session.stockTakeSession}
{session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : null}
</Typography>
</Stack>

<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>


+ 355
- 0
src/components/Warehouse/TabStockTakeSectionMapping.tsx Visa fil

@@ -0,0 +1,355 @@
"use client";

import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
TextField,
Typography,
CircularProgress,
IconButton,
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from "@mui/material";
import Delete from "@mui/icons-material/Delete";
import Add from "@mui/icons-material/Add";
import { useTranslation } from "react-i18next";
import { Edit } from "@mui/icons-material";
import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
import SearchResults, { Column } from "@/components/SearchResults/SearchResults";
import {
fetchStockTakeSections,
updateSectionDescription,
clearWarehouseSection,
getWarehousesBySection,
searchWarehousesForAddToSection,
editWarehouse,
} from "@/app/api/warehouse/actions";
import { WarehouseResult } from "@/app/api/warehouse";
import { StockTakeSectionInfo } from "@/app/api/warehouse";
import { deleteDialog, successDialog } from "@/components/Swal/CustomAlerts";

type SearchKey = "stockTakeSection" | "stockTakeSectionDescription";

export default function TabStockTakeSectionMapping() {
const { t } = useTranslation(["warehouse", "common"]);
const [sections, setSections] = useState<StockTakeSectionInfo[]>([]);
const [filteredSections, setFilteredSections] = useState<StockTakeSectionInfo[]>([]);
const [selectedSection, setSelectedSection] = useState<StockTakeSectionInfo | null>(null);
const [warehousesInSection, setWarehousesInSection] = useState<WarehouseResult[]>([]);
const [loading, setLoading] = useState(true);
const [openDialog, setOpenDialog] = useState(false);
const [editDesc, setEditDesc] = useState("");
const [savingDesc, setSavingDesc] = useState(false);
const [warehouseList, setWarehouseList] = useState<WarehouseResult[]>([]);
const [openAddDialog, setOpenAddDialog] = useState(false);
const [addStoreId, setAddStoreId] = useState("");
const [addWarehouse, setAddWarehouse] = useState("");
const [addArea, setAddArea] = useState("");
const [addSlot, setAddSlot] = useState("");
const [addSearchResults, setAddSearchResults] = useState<WarehouseResult[]>([]);
const [addSearching, setAddSearching] = useState(false);
const [addingWarehouseId, setAddingWarehouseId] = useState<number | null>(null);
const loadSections = useCallback(async () => {
setLoading(true);
try {
const data = await fetchStockTakeSections();
const withId = (data ?? []).map((s) => ({
...s,
id: s.stockTakeSection,
}));
setSections(withId);
setFilteredSections(withId);
} catch (e) {
console.error(e);
setSections([]);
setFilteredSections([]);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
loadSections();
}, [loadSections]);

const handleViewSection = useCallback(async (section: StockTakeSectionInfo) => {
setSelectedSection(section);
setEditDesc(section.stockTakeSectionDescription ?? "");
setOpenDialog(true);
try {
const list = await getWarehousesBySection(section.stockTakeSection);
setWarehousesInSection(list ?? []);
} catch (e) {
console.error(e);
setWarehousesInSection([]);
}
}, []);

const criteria: Criterion<SearchKey>[] = useMemo(
() => [
{ type: "text", label: "Stock Take Section", paramName: "stockTakeSection", placeholder: "" },
{ type: "text", label: "Stock Take Section Description", paramName: "stockTakeSectionDescription", placeholder: "" },
],
[]
);

const handleSearch = useCallback((inputs: Record<SearchKey | `${SearchKey}To`, string>) => {
const section = (inputs.stockTakeSection ?? "").trim().toLowerCase();
const desc = (inputs.stockTakeSectionDescription ?? "").trim().toLowerCase();
setFilteredSections(
sections.filter(
(s) =>
(!section || (s.stockTakeSection ?? "").toLowerCase().includes(section)) &&
(!desc || (s.stockTakeSectionDescription ?? "").toLowerCase().includes(desc))
)
);
}, [sections]);

const handleReset = useCallback(() => {
setFilteredSections(sections);
}, [sections]);

const handleSaveDescription = useCallback(async () => {
if (!selectedSection) return;
setSavingDesc(true);
try {
await updateSectionDescription(selectedSection.stockTakeSection, editDesc || null);
await loadSections();
if (selectedSection) {
setSelectedSection((prev) => (prev ? { ...prev, stockTakeSectionDescription: editDesc || null } : null));
}
successDialog(t("Saved"), t);
} catch (e) {
console.error(e);
} finally {
setSavingDesc(false);
}
}, [selectedSection, editDesc, loadSections, t]);

const handleRemoveWarehouse = useCallback(
(warehouse: WarehouseResult) => {
deleteDialog(async () => {
try {
await clearWarehouseSection(warehouse.id);
setWarehousesInSection((prev) => prev.filter((w) => w.id !== warehouse.id));
successDialog(t("Delete Success"), t);
} catch (e) {
console.error(e);
}
}, t);
},
[t]
);
const handleOpenAddWarehouse = useCallback(() => {
setAddStoreId("");
setAddWarehouse("");
setAddArea("");
setAddSlot("");
setAddSearchResults([]);
setOpenAddDialog(true);
}, []);

const handleAddSearch = useCallback(async () => {
if (!selectedSection) return;
setAddSearching(true);
try {
const params: { store_id?: string; warehouse?: string; area?: string; slot?: string } = {};
if (addStoreId.trim()) params.store_id = addStoreId.trim();
if (addWarehouse.trim()) params.warehouse = addWarehouse.trim();
if (addArea.trim()) params.area = addArea.trim();
if (addSlot.trim()) params.slot = addSlot.trim();
const list = await searchWarehousesForAddToSection(params, selectedSection.stockTakeSection);
setAddSearchResults(list ?? []);
} catch (e) {
console.error(e);
setAddSearchResults([]);
} finally {
setAddSearching(false);
}
}, [selectedSection, addStoreId, addWarehouse, addArea, addSlot]);

const handleAddWarehouseToSection = useCallback(
async (w: WarehouseResult) => {
if (!selectedSection) return;
setAddingWarehouseId(w.id);
try {
await editWarehouse(w.id, {
stockTakeSection: selectedSection.stockTakeSection,
stockTakeSectionDescription: selectedSection.stockTakeSectionDescription ?? undefined,
});
setWarehousesInSection((prev) => [...prev, w]);
setAddSearchResults((prev) => prev.filter((x) => x.id !== w.id));
successDialog(t("Add Success") ?? t("Saved"), t);
} catch (e) {
console.error(e);
} finally {
setAddingWarehouseId(null);
}
},
[selectedSection, t]
);
const columns = useMemo<Column<StockTakeSectionInfo>[]>(
() => [
{ name: "stockTakeSection", label: t("stockTakeSection"), align: "left", sx: { width: "25%" } },
{ name: "stockTakeSectionDescription", label: t("stockTakeSectionDescription"), align: "left", sx: { width: "35%" } },
{
name: "id",
label: t("Edit"),
onClick: (row) => handleViewSection(row),
buttonIcon: <Edit />,
buttonIcons: {} as Record<keyof StockTakeSectionInfo, React.ReactNode>,
color: "primary",
sx: { width: "20%" },
},
],
[t, handleViewSection]
);

if (loading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", minHeight: 200, alignItems: "center" }}>
<CircularProgress />
</Box>
);
}

return (
<Box>
<SearchBox<SearchKey> criteria={criteria} onSearch={handleSearch} onReset={handleReset} />
<SearchResults<StockTakeSectionInfo> items={filteredSections} columns={columns} />

<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth sx={{ zIndex: 1000 }}>
<DialogTitle>
{t("Mapping Details")} - {selectedSection?.stockTakeSection} ({selectedSection?.stockTakeSectionDescription ?? ""})
</DialogTitle>
<DialogContent>
<Stack direction="row" alignItems="center" spacing={2} sx={{ mb: 1, minHeight: 40 }}>
<Typography variant="body2" sx={{ display: "flex", alignItems: "center" }}>
{t("stockTakeSectionDescription")}
</Typography>
<TextField size="small" value={editDesc} onChange={(e) => setEditDesc(e.target.value)} sx={{ minWidth: 200 }} />
<Button variant="contained" size="small" disabled={savingDesc} onClick={handleSaveDescription}>
{t("Save")}
</Button>
<Box sx={{ flex: 1 }} />
<Button variant="contained" startIcon={<Add />} onClick={handleOpenAddWarehouse}>
{t("Add Warehouse")}
</Button>
</Stack>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>{t("code")}</TableCell>
<TableCell>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{warehousesInSection.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center">{t("No warehouses")}</TableCell></TableRow>
) : (
warehousesInSection.map((w) => (
<TableRow key={w.id}>
<TableCell>{w.code}</TableCell>
<TableCell>
<IconButton color="error" size="small" onClick={() => handleRemoveWarehouse(w)}>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
</DialogActions>
</Dialog>
<Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth sx={{ zIndex: 1000 }}>
<DialogTitle>{t("Add Warehouse")}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ pt: 1 }}>
<TextField
size="small"
label={t("Store ID")}
value={addStoreId}
onChange={(e) => setAddStoreId(e.target.value)}
fullWidth
/>
<TextField
size="small"
label={t("warehouse")}
value={addWarehouse}
onChange={(e) => setAddWarehouse(e.target.value)}
fullWidth
/>
<TextField
size="small"
label={t("area")}
value={addArea}
onChange={(e) => setAddArea(e.target.value)}
fullWidth
/>
<TextField
size="small"
label={t("slot")}
value={addSlot}
onChange={(e) => setAddSlot(e.target.value)}
fullWidth
/>
<Button variant="contained" onClick={handleAddSearch} disabled={addSearching}>
{addSearching ? <CircularProgress size={20} /> : t("Search")}
</Button>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>{t("code")}</TableCell>
<TableCell>{t("name")}</TableCell>
<TableCell>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{addSearchResults
.filter((w) => !warehousesInSection.some((inc) => inc.id === w.id))
.map((w) => (
<TableRow key={w.id}>
<TableCell>{w.code}</TableCell>
<TableCell>{w.name}</TableCell>
<TableCell>
<Button
size="small"
variant="outlined"
disabled={addingWarehouseId === w.id}
onClick={() => handleAddWarehouseToSection(w)}
>
{addingWarehouseId === w.id ? <CircularProgress size={16} /> : t("Add")}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button>
</DialogActions>
</Dialog>
</Box>
);
}

+ 520
- 0
src/components/Warehouse/WarehouseHandle.tsx Visa fil

@@ -0,0 +1,520 @@
"use client";

import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults/SearchResults";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { WarehouseResult } from "@/app/api/warehouse";
import { deleteWarehouse, editWarehouse } 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";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";

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

// State for editing order & stockTakeSection
const [editingWarehouse, setEditingWarehouse] = useState<WarehouseResult | null>(null);
const [editValues, setEditValues] = useState({
order: "",
stockTakeSection: "",
stockTakeSectionDescription: "",
});
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [editError, setEditError] = useState("");

const [searchInputs, setSearchInputs] = useState({
store_id: "",
warehouse: "",
area: "",
slot: "",
stockTakeSection: "",
stockTakeSectionDescription: "",
});

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);
}
}, t);
}, [t, router]);

const handleReset = useCallback(() => {
setSearchInputs({
store_id: "",
warehouse: "",
area: "",
slot: "",
stockTakeSection: "",
stockTakeSectionDescription: "",
});
setFilteredWarehouse(warehouses);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
}, [warehouses, pagingController.pageSize]);

const onEditClick = useCallback((warehouse: WarehouseResult) => {
setEditingWarehouse(warehouse);
setEditValues({
order: warehouse.order ?? "",
stockTakeSection: warehouse.stockTakeSection ?? "",
stockTakeSectionDescription: warehouse.stockTakeSectionDescription ?? "",
});
setEditError("");
}, []);

const handleEditClose = useCallback(() => {
if (isSavingEdit) return;
setEditingWarehouse(null);
setEditError("");
}, [isSavingEdit]);

const handleEditSave = useCallback(async () => {
if (!editingWarehouse) return;

const trimmedOrder = editValues.order.trim();
const trimmedStockTakeSection = editValues.stockTakeSection.trim();
const trimmedStockTakeSectionDescription = editValues.stockTakeSectionDescription.trim();
const orderPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/;
const sectionPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/;

if (trimmedOrder && !orderPattern.test(trimmedOrder)) {
setEditError(`${t("order")} 格式必須為 XF-YYY`);
return;
}

if (trimmedStockTakeSection && !sectionPattern.test(trimmedStockTakeSection)) {
setEditError(`${t("stockTakeSection")} 格式必須為 ST-YYY`);
return;
}

try {
setIsSavingEdit(true);
setEditError("");

await editWarehouse(editingWarehouse.id, {
order: trimmedOrder || undefined,
stockTakeSection: trimmedStockTakeSection || undefined,
stockTakeSectionDescription: trimmedStockTakeSectionDescription || undefined,
});

setFilteredWarehouse((prev) =>
prev.map((w) =>
w.id === editingWarehouse.id
? {
...w,
order: trimmedOrder || undefined,
stockTakeSection: trimmedStockTakeSection || undefined,
stockTakeSectionDescription: trimmedStockTakeSectionDescription || undefined,
}
: w,
),
);

router.refresh();
setEditingWarehouse(null);
} catch (error: unknown) {
console.error("Failed to edit warehouse:", error);
const message = error instanceof Error ? error.message : t("An error has occurred. Please try again later.");
setEditError(message);
} finally {
setIsSavingEdit(false);
}
}, [editValues, editingWarehouse, router, t, setFilteredWarehouse]);

const handleSearch = useCallback(() => {
setIsSearching(true);
try {
let results: WarehouseResult[] = warehouses;

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() || "";
const stockTakeSectionDescription = searchInputs.stockTakeSectionDescription?.trim() || "";
if (storeId || warehouse || area || slot || stockTakeSection || stockTakeSectionDescription) {
results = warehouses.filter((warehouseItem) => {
if (stockTakeSection) {
const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase();
if (!itemStockTakeSection.includes(stockTakeSection.toLowerCase())) {
return false;
}
}
if (stockTakeSectionDescription) {
const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase();
if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription.toLowerCase())) {
return false;
}
}
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 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;
}
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;
}
return true;
});
} else {
results = warehouses;
}

setFilteredWarehouse(results);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
} catch (error) {
console.error("Error searching warehouses:", error);
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() || "";
const stockTakeSectionDescription = searchInputs.stockTakeSectionDescription?.trim().toLowerCase() || "";
setFilteredWarehouse(
warehouses.filter((warehouseItem) => {
if (stockTakeSection) {
const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase();
if (!itemStockTakeSection.includes(stockTakeSection)) {
return false;
}
}
if (stockTakeSectionDescription) {
const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase();
if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription)) {
return false;
}
}
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: "action",
label: t("Edit"),
onClick: onEditClick,
buttonIcon: <EditIcon />,
color: "primary",
sx: { width: "10%", minWidth: "80px" },
},
{
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: "stockTakeSectionDescription",
label: t("stockTakeSectionDescription"),
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",
}}
>
<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>
<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>
<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>
<TextField
label={t("slot")}
value={searchInputs.slot}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, slot: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
<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 sx={{ flex: 1, minWidth: "150px", ml: 2 }}>
<TextField
label={t("stockTakeSectionDescription")}
value={searchInputs.stockTakeSectionDescription}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, stockTakeSectionDescription: 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}
/>
<Dialog
open={Boolean(editingWarehouse)}
onClose={handleEditClose}
fullWidth
maxWidth="sm"
>
<DialogTitle>{t("Edit")}</DialogTitle>
<DialogContent sx={{ pt: 2, display: "flex", flexDirection: "column", gap: 2 }}>
{editError && (
<Typography variant="body2" color="error">
{editError}
</Typography>
)}
<TextField
label={t("order")}
value={editValues.order}
onChange={(e) =>
setEditValues((prev) => ({ ...prev, order: e.target.value }))
}
size="small"
fullWidth
/>
<TextField
label={t("stockTakeSection")}
value={editValues.stockTakeSection}
onChange={(e) =>
setEditValues((prev) => ({ ...prev, stockTakeSection: e.target.value }))
}
size="small"
fullWidth
/>
<TextField
label={t("stockTakeSectionDescription")}
value={editValues.stockTakeSectionDescription}
onChange={(e) =>
setEditValues((prev) => ({ ...prev, stockTakeSectionDescription: e.target.value }))
}
size="small"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleEditClose} disabled={isSavingEdit}>
{t("Cancel")}
</Button>
<Button
onClick={handleEditSave}
disabled={isSavingEdit}
variant="contained"
>
{t("Save", { ns: "common" })}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default WarehouseHandle;

+ 40
- 0
src/components/Warehouse/WarehouseHandleLoading.tsx Visa fil

@@ -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/Warehouse/WarehouseHandleWrapper.tsx Visa fil

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

+ 67
- 0
src/components/Warehouse/WarehouseTabs.tsx Visa fil

@@ -0,0 +1,67 @@
"use client";

import { useState, ReactNode, useEffect } from "react";
import { Box, Tabs, Tab } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useSearchParams, useRouter } from "next/navigation";

interface WarehouseTabsProps {
tab0Content: ReactNode;
tab1Content: ReactNode;
}

function TabPanel({
children,
value,
index,
}: {
children?: ReactNode;
value: number;
index: number;
}) {
return (
<div role="tabpanel" hidden={value !== index}>
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
</div>
);
}

export default function WarehouseTabs({ tab0Content, tab1Content }: WarehouseTabsProps) {
const { t } = useTranslation("warehouse");
const searchParams = useSearchParams();
const router = useRouter();
const [currentTab, setCurrentTab] = useState(() => {
const tab = searchParams.get("tab");
return tab === "1" ? 1 : 0;
});

useEffect(() => {
const tab = searchParams.get("tab");
setCurrentTab(tab === "1" ? 1 : 0);
}, [searchParams]);

const handleTabChange = (_e: React.SyntheticEvent, newValue: number) => {
setCurrentTab(newValue);
const params = new URLSearchParams(searchParams.toString());
if (newValue === 0) params.delete("tab");
else params.set("tab", String(newValue));
router.push(`?${params.toString()}`, { scroll: false });
};

return (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label={t("Warehouse List")} />
<Tab label={t("Stock Take Section & Warehouse Mapping")} />
</Tabs>
</Box>
<TabPanel value={currentTab} index={0}>
{tab0Content}
</TabPanel>
<TabPanel value={currentTab} index={1}>
{tab1Content}
</TabPanel>
</Box>
);
}

+ 1
- 0
src/components/Warehouse/index.ts Visa fil

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

+ 39
- 9
src/components/WarehouseHandle/WarehouseHandle.tsx Visa fil

@@ -46,6 +46,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
const [editValues, setEditValues] = useState({
order: "",
stockTakeSection: "",
});
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [editError, setEditError] = useState("");
@@ -56,6 +57,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
area: "",
slot: "",
stockTakeSection: "",
stockTakeSectionDescription: "",
});

const onDeleteClick = useCallback((warehouse: WarehouseResult) => {
@@ -78,6 +80,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
area: "",
slot: "",
stockTakeSection: "",
stockTakeSectionDescription: "",
});
setFilteredWarehouse(warehouses);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
@@ -103,7 +106,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {

const trimmedOrder = editValues.order.trim();
const trimmedStockTakeSection = editValues.stockTakeSection.trim();

const orderPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/;
const sectionPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/;

@@ -140,9 +142,10 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {

router.refresh();
setEditingWarehouse(null);
} catch (error) {
} catch (error: unknown) {
console.error("Failed to edit warehouse:", error);
setEditError(t("An error has occurred. Please try again later."));
const message = error instanceof Error ? error.message : t("An error has occurred. Please try again later.");
setEditError(message);
} finally {
setIsSavingEdit(false);
}
@@ -158,8 +161,8 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
const area = searchInputs.area?.trim() || "";
const slot = searchInputs.slot?.trim() || "";
const stockTakeSection = searchInputs.stockTakeSection?.trim() || "";
if (storeId || warehouse || area || slot || stockTakeSection) {
const stockTakeSectionDescription = searchInputs.stockTakeSectionDescription?.trim() || "";
if (storeId || warehouse || area || slot || stockTakeSection || stockTakeSectionDescription) {
results = warehouses.filter((warehouseItem) => {
if (stockTakeSection) {
const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase();
@@ -167,7 +170,12 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
return false;
}
}
if (stockTakeSectionDescription) {
const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase();
if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription.toLowerCase())) {
return false;
}
}
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
@@ -214,7 +222,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
const area = searchInputs.area?.trim().toLowerCase() || "";
const slot = searchInputs.slot?.trim().toLowerCase() || "";
const stockTakeSection = searchInputs.stockTakeSection?.trim().toLowerCase() || "";
const stockTakeSectionDescription = searchInputs.stockTakeSectionDescription?.trim().toLowerCase() || "";
setFilteredWarehouse(
warehouses.filter((warehouseItem) => {
if (stockTakeSection) {
@@ -223,7 +231,12 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
return false;
}
}
if (stockTakeSectionDescription) {
const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase();
if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription)) {
return false;
}
}
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
@@ -313,7 +326,13 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
headerAlign: "left",
sx: { width: "15%", minWidth: "120px" },
},

{
name: "stockTakeSectionDescription",
label: t("stockTakeSectionDescription"),
align: "left",
headerAlign: "left",
sx: { width: "15%", minWidth: "120px" },
},
{
name: "action",
label: t("Delete"),
@@ -401,6 +420,17 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
fullWidth
/>
</Box>
<Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}>
<TextField
label={t("stockTakeSectionDescription")}
value={searchInputs.stockTakeSectionDescription}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, stockTakeSectionDescription: e.target.value }))
}
size="small"
fullWidth
/>
</Box>
</Box>
<CardActions sx={{ justifyContent: "flex-start", px: 0, pt: 1 }}>
<Button


+ 2
- 0
src/i18n/zh/common.json Visa fil

@@ -41,6 +41,8 @@
"Sales Qty": "銷售數量",
"Sales UOM": "銷售單位",
"Bom Material" : "BOM 材料",
"Stock Take Section": "盤點區域",
"Stock Take Section Description": "盤點區域描述",

"Depth": "顔色深淺度 深1淺5",
"Search": "搜索",


+ 18
- 0
src/i18n/zh/warehouse.json Visa fil

@@ -8,6 +8,24 @@
"Edit": "編輯",
"Delete": "刪除",
"Delete Success": "刪除成功",
"Actions": "操作",
"Add": "新增",
"Store ID": "樓層",
"Saved": "已儲存",
"Add Success": "新增成功",
"Saved Successfully": "儲存成功",
"Stock Take Section": "盤點區域",
"Add Warehouse": "新增倉庫",
"Save": "儲存",
"Stock Take Section Description": "盤點區域描述",
"Mapping Details": "對應詳細資料",
"Warehouses in this section": "此區域內的倉庫",
"No warehouses": "此區域內沒有倉庫",
"Remove": "移除",
"stockTakeSectionDescription": "盤點區域描述",
"Warehouse List": "倉庫列表",
"Stock Take Section & Warehouse Mapping": "盤點區域 & 倉庫對應",
"Warehouse": "倉庫",
"warehouse": "倉庫",
"Rows per page": "每頁行數",


Laddar…
Avbryt
Spara