| @@ -5,8 +5,10 @@ import { Suspense } from "react"; | |||||
| import { Stack } from "@mui/material"; | import { Stack } from "@mui/material"; | ||||
| import { Button } from "@mui/material"; | import { Button } from "@mui/material"; | ||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import WarehouseHandle from "@/components/WarehouseHandle"; | |||||
| import Add from "@mui/icons-material/Add"; | 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 = { | export const metadata: Metadata = { | ||||
| title: "Warehouse Management", | title: "Warehouse Management", | ||||
| @@ -16,12 +18,7 @@ const Warehouse: React.FC = async () => { | |||||
| const { t } = await getServerI18n("warehouse"); | const { t } = await getServerI18n("warehouse"); | ||||
| return ( | 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}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| {t("Warehouse")} | {t("Warehouse")} | ||||
| </Typography> | </Typography> | ||||
| @@ -35,11 +32,14 @@ const Warehouse: React.FC = async () => { | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| <I18nProvider namespaces={["warehouse", "common", "dashboard"]}> | <I18nProvider namespaces={["warehouse", "common", "dashboard"]}> | ||||
| <Suspense fallback={<WarehouseHandle.Loading />}> | |||||
| <WarehouseHandle /> | |||||
| <Suspense fallback={null}> | |||||
| <WarehouseTabs | |||||
| tab0Content={<WarehouseHandleWrapper />} | |||||
| tab1Content={<TabStockTakeSectionMapping />} | |||||
| /> | |||||
| </Suspense> | </Suspense> | ||||
| </I18nProvider> | </I18nProvider> | ||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default Warehouse; | |||||
| export default Warehouse; | |||||
| @@ -349,6 +349,7 @@ export interface AllJoborderProductProcessInfoResponse { | |||||
| jobOrderId: number; | jobOrderId: number; | ||||
| timeNeedToComplete: number; | timeNeedToComplete: number; | ||||
| uom: string; | uom: string; | ||||
| isDrink?: boolean | null; | |||||
| stockInLineId: number; | stockInLineId: number; | ||||
| jobOrderCode: string; | jobOrderCode: string; | ||||
| productProcessLineCount: number; | 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[]>( | return serverFetchJson<AllJoborderProductProcessInfoResponse[]>( | ||||
| `${BASE_API_URL}/product-process/Demo/Process/all`, | |||||
| `${BASE_API_URL}/product-process/Demo/Process/all${query}`, | |||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| next: { tags: ["productProcess"] }, | next: { tags: ["productProcess"] }, | ||||
| @@ -3,7 +3,7 @@ | |||||
| import { serverFetchString, serverFetchWithNoContent, serverFetchJson } 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 { revalidateTag } from "next/cache"; | ||||
| import { WarehouseResult } from "./index"; | |||||
| import { WarehouseResult, StockTakeSectionInfo } from "./index"; | |||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| export interface WarehouseInputs { | export interface WarehouseInputs { | ||||
| @@ -17,6 +17,7 @@ export interface WarehouseInputs { | |||||
| slot?: string; | slot?: string; | ||||
| order?: string; | order?: string; | ||||
| stockTakeSection?: string; | stockTakeSection?: string; | ||||
| stockTakeSectionDescription?: string; | |||||
| } | } | ||||
| export const fetchWarehouseDetail = cache(async (id: number) => { | export const fetchWarehouseDetail = cache(async (id: number) => { | ||||
| @@ -81,4 +82,62 @@ export const importNewWarehouse = async (data: FormData) => { | |||||
| }, | }, | ||||
| ); | ); | ||||
| return importWarehouse; | 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; | |||||
| }); | |||||
| }); | |||||
| @@ -15,6 +15,7 @@ export interface WarehouseResult { | |||||
| slot?: string; | slot?: string; | ||||
| order?: string; | order?: string; | ||||
| stockTakeSection?: string; | stockTakeSection?: string; | ||||
| stockTakeSectionDescription?: string; | |||||
| } | } | ||||
| export interface WarehouseCombo { | export interface WarehouseCombo { | ||||
| @@ -34,3 +35,9 @@ export const fetchWarehouseCombo = cache(async () => { | |||||
| next: { tags: ["warehouseCombo"] }, | next: { tags: ["warehouseCombo"] }, | ||||
| }); | }); | ||||
| }); | }); | ||||
| export interface StockTakeSectionInfo { | |||||
| id: string; | |||||
| stockTakeSection: string; | |||||
| stockTakeSectionDescription: string | null; | |||||
| warehouseCount: number; | |||||
| } | |||||
| @@ -41,6 +41,7 @@ const CreateWarehouse: React.FC = () => { | |||||
| slot: "", | slot: "", | ||||
| order: "", | order: "", | ||||
| stockTakeSection: "", | stockTakeSection: "", | ||||
| stockTakeSectionDescription: "", | |||||
| }); | }); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.log(error); | console.log(error); | ||||
| @@ -89,7 +90,8 @@ const CreateWarehouse: React.FC = () => { | |||||
| router.replace("/settings/warehouse"); | router.replace("/settings/warehouse"); | ||||
| } catch (e) { | } catch (e) { | ||||
| console.log(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], | [router, t], | ||||
| @@ -153,6 +153,14 @@ const WarehouseDetail: React.FC = () => { | |||||
| helperText={errors.stockTakeSection?.message} | helperText={errors.stockTakeSection?.message} | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| <Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}> | |||||
| <TextField | |||||
| label={t("stockTakeSectionDescription")} | |||||
| fullWidth | |||||
| size="small" | |||||
| {...register("stockTakeSectionDescription")} | |||||
| /> | |||||
| </Box> | |||||
| </Box> | </Box> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -52,7 +52,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| const [openModal, setOpenModal] = useState<boolean>(false); | const [openModal, setOpenModal] = useState<boolean>(false); | ||||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | 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 [suggestedLocationCode, setSuggestedLocationCode] = useState<string | null>(null); | ||||
| const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => { | const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => { | ||||
| if (!currentUserId) { | if (!currentUserId) { | ||||
| @@ -108,7 +109,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| const fetchProcesses = useCallback(async () => { | const fetchProcesses = useCallback(async () => { | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const data = await fetchAllJoborderProductProcessInfo(); | |||||
| const isDrinkParam = | |||||
| filter === "all" ? undefined : filter === "drink" ? true : false; | |||||
| const data = await fetchAllJoborderProductProcessInfo(isDrinkParam); | |||||
| setProcesses(data || []); | setProcesses(data || []); | ||||
| setPage(0); | setPage(0); | ||||
| } catch (e) { | } catch (e) { | ||||
| @@ -117,7 +121,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, []); | |||||
| }, [filter]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchProcesses(); | fetchProcesses(); | ||||
| @@ -176,6 +180,29 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| </Box> | </Box> | ||||
| ) : ( | ) : ( | ||||
| <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 }}> | <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||||
| {t("Total processes")}: {processes.length} | {t("Total processes")}: {processes.length} | ||||
| </Typography> | </Typography> | ||||
| @@ -98,6 +98,23 @@ const SearchPage: React.FC<Props> = ({ dataList }) => { | |||||
| lotId = item.lotId; | lotId = item.lotId; | ||||
| itemId = item.itemId; | 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) { | if (lotId && itemId) { | ||||
| @@ -109,7 +126,7 @@ const SearchPage: React.FC<Props> = ({ dataList }) => { | |||||
| alert(t("Item not found")); | alert(t("Item not found")); | ||||
| } | } | ||||
| }, | }, | ||||
| [tab, currentUserId, t, missItems, badItems] | |||||
| [tab, currentUserId, t, missItems, badItems, expiryItems] | |||||
| ); | ); | ||||
| const handleFormSuccess = useCallback(() => { | const handleFormSuccess = useCallback(() => { | ||||
| @@ -19,6 +19,7 @@ import { | |||||
| DialogContentText, | DialogContentText, | ||||
| DialogActions, | DialogActions, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | |||||
| import { useState, useCallback, useEffect } from "react"; | import { useState, useCallback, useEffect } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import duration from "dayjs/plugin/duration"; | import duration from "dayjs/plugin/duration"; | ||||
| @@ -50,6 +51,58 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| const [creating, setCreating] = useState(false); | const [creating, setCreating] = useState(false); | ||||
| const [openConfirmDialog, setOpenConfirmDialog] = 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( | const fetchStockTakeSessions = useCallback( | ||||
| async (pageNum: number, size: number) => { | async (pageNum: number, size: number) => { | ||||
| setLoading(true); | setLoading(true); | ||||
| @@ -188,8 +241,15 @@ const [total, setTotal] = useState(0); | |||||
| return ( | return ( | ||||
| <Box> | <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 }}> | <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Total Sections")}: {stockTakeSessions.length} | {t("Total Sections")}: {stockTakeSessions.length} | ||||
| @@ -209,7 +269,7 @@ const [total, setTotal] = useState(0); | |||||
| </Box> | </Box> | ||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| {stockTakeSessions.map((session: AllPickedStockTakeListReponse) => { | |||||
| {filteredSessions.map((session: AllPickedStockTakeListReponse) => { | |||||
| const statusColor = getStatusColor(session.status || ""); | const statusColor = getStatusColor(session.status || ""); | ||||
| const lastStockTakeDate = session.lastStockTakeDate | const lastStockTakeDate = session.lastStockTakeDate | ||||
| ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT) | ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT) | ||||
| @@ -229,10 +289,11 @@ const [total, setTotal] = useState(0); | |||||
| > | > | ||||
| <CardContent sx={{ pb: 1, flexGrow: 1 }}> | <CardContent sx={{ pb: 1, flexGrow: 1 }}> | ||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 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> | </Stack> | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | ||||
| @@ -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> | |||||
| ); | |||||
| } | |||||
| @@ -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; | |||||
| @@ -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,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> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./WarehouseHandleWrapper"; | |||||
| @@ -46,6 +46,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const [editValues, setEditValues] = useState({ | const [editValues, setEditValues] = useState({ | ||||
| order: "", | order: "", | ||||
| stockTakeSection: "", | stockTakeSection: "", | ||||
| }); | }); | ||||
| const [isSavingEdit, setIsSavingEdit] = useState(false); | const [isSavingEdit, setIsSavingEdit] = useState(false); | ||||
| const [editError, setEditError] = useState(""); | const [editError, setEditError] = useState(""); | ||||
| @@ -56,6 +57,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| area: "", | area: "", | ||||
| slot: "", | slot: "", | ||||
| stockTakeSection: "", | stockTakeSection: "", | ||||
| stockTakeSectionDescription: "", | |||||
| }); | }); | ||||
| const onDeleteClick = useCallback((warehouse: WarehouseResult) => { | const onDeleteClick = useCallback((warehouse: WarehouseResult) => { | ||||
| @@ -78,6 +80,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| area: "", | area: "", | ||||
| slot: "", | slot: "", | ||||
| stockTakeSection: "", | stockTakeSection: "", | ||||
| stockTakeSectionDescription: "", | |||||
| }); | }); | ||||
| setFilteredWarehouse(warehouses); | setFilteredWarehouse(warehouses); | ||||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | ||||
| @@ -103,7 +106,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const trimmedOrder = editValues.order.trim(); | const trimmedOrder = editValues.order.trim(); | ||||
| const trimmedStockTakeSection = editValues.stockTakeSection.trim(); | const trimmedStockTakeSection = editValues.stockTakeSection.trim(); | ||||
| const orderPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/; | const orderPattern = /^[A-Za-z0-9]{2}-[A-Za-z0-9]{3}$/; | ||||
| const sectionPattern = /^[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(); | router.refresh(); | ||||
| setEditingWarehouse(null); | setEditingWarehouse(null); | ||||
| } catch (error) { | |||||
| } catch (error: unknown) { | |||||
| console.error("Failed to edit warehouse:", error); | 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 { | } finally { | ||||
| setIsSavingEdit(false); | setIsSavingEdit(false); | ||||
| } | } | ||||
| @@ -158,8 +161,8 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const area = searchInputs.area?.trim() || ""; | const area = searchInputs.area?.trim() || ""; | ||||
| const slot = searchInputs.slot?.trim() || ""; | const slot = searchInputs.slot?.trim() || ""; | ||||
| const stockTakeSection = searchInputs.stockTakeSection?.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) => { | results = warehouses.filter((warehouseItem) => { | ||||
| if (stockTakeSection) { | if (stockTakeSection) { | ||||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | ||||
| @@ -167,7 +170,12 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| return false; | return false; | ||||
| } | } | ||||
| } | } | ||||
| if (stockTakeSectionDescription) { | |||||
| const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase(); | |||||
| if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription.toLowerCase())) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (storeId || warehouse || area || slot) { | if (storeId || warehouse || area || slot) { | ||||
| if (!warehouseItem.code) { | if (!warehouseItem.code) { | ||||
| return false; | return false; | ||||
| @@ -214,7 +222,7 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const area = searchInputs.area?.trim().toLowerCase() || ""; | const area = searchInputs.area?.trim().toLowerCase() || ""; | ||||
| const slot = searchInputs.slot?.trim().toLowerCase() || ""; | const slot = searchInputs.slot?.trim().toLowerCase() || ""; | ||||
| const stockTakeSection = searchInputs.stockTakeSection?.trim().toLowerCase() || ""; | const stockTakeSection = searchInputs.stockTakeSection?.trim().toLowerCase() || ""; | ||||
| const stockTakeSectionDescription = searchInputs.stockTakeSectionDescription?.trim().toLowerCase() || ""; | |||||
| setFilteredWarehouse( | setFilteredWarehouse( | ||||
| warehouses.filter((warehouseItem) => { | warehouses.filter((warehouseItem) => { | ||||
| if (stockTakeSection) { | if (stockTakeSection) { | ||||
| @@ -223,7 +231,12 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| return false; | return false; | ||||
| } | } | ||||
| } | } | ||||
| if (stockTakeSectionDescription) { | |||||
| const itemStockTakeSectionDescription = String(warehouseItem.stockTakeSectionDescription || "").toLowerCase(); | |||||
| if (!itemStockTakeSectionDescription.includes(stockTakeSectionDescription)) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (storeId || warehouse || area || slot) { | if (storeId || warehouse || area || slot) { | ||||
| if (!warehouseItem.code) { | if (!warehouseItem.code) { | ||||
| return false; | return false; | ||||
| @@ -313,7 +326,13 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| headerAlign: "left", | headerAlign: "left", | ||||
| sx: { width: "15%", minWidth: "120px" }, | sx: { width: "15%", minWidth: "120px" }, | ||||
| }, | }, | ||||
| { | |||||
| name: "stockTakeSectionDescription", | |||||
| label: t("stockTakeSectionDescription"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "120px" }, | |||||
| }, | |||||
| { | { | ||||
| name: "action", | name: "action", | ||||
| label: t("Delete"), | label: t("Delete"), | ||||
| @@ -401,6 +420,17 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| fullWidth | fullWidth | ||||
| /> | /> | ||||
| </Box> | </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> | </Box> | ||||
| <CardActions sx={{ justifyContent: "flex-start", px: 0, pt: 1 }}> | <CardActions sx={{ justifyContent: "flex-start", px: 0, pt: 1 }}> | ||||
| <Button | <Button | ||||
| @@ -41,6 +41,8 @@ | |||||
| "Sales Qty": "銷售數量", | "Sales Qty": "銷售數量", | ||||
| "Sales UOM": "銷售單位", | "Sales UOM": "銷售單位", | ||||
| "Bom Material" : "BOM 材料", | "Bom Material" : "BOM 材料", | ||||
| "Stock Take Section": "盤點區域", | |||||
| "Stock Take Section Description": "盤點區域描述", | |||||
| "Depth": "顔色深淺度 深1淺5", | "Depth": "顔色深淺度 深1淺5", | ||||
| "Search": "搜索", | "Search": "搜索", | ||||
| @@ -8,6 +8,24 @@ | |||||
| "Edit": "編輯", | "Edit": "編輯", | ||||
| "Delete": "刪除", | "Delete": "刪除", | ||||
| "Delete Success": "刪除成功", | "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": "倉庫", | ||||
| "warehouse": "倉庫", | "warehouse": "倉庫", | ||||
| "Rows per page": "每頁行數", | "Rows per page": "每頁行數", | ||||