Explorar el Código

update muti round & create round by item

stockTakeUpdate
CANCERYS\kw093 hace 3 semanas
padre
commit
791f726363
Se han modificado 9 ficheros con 1253 adiciones y 259 borrados
  1. +114
    -5
      src/app/api/stockTake/actions.ts
  2. +21
    -12
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  3. +408
    -72
      src/components/StockTakeManagement/PickerCardList.tsx
  4. +42
    -18
      src/components/StockTakeManagement/PickerReStockTake.tsx
  5. +34
    -25
      src/components/StockTakeManagement/PickerStockTake.tsx
  6. +293
    -0
      src/components/StockTakeManagement/StockTakeRoundCardList.tsx
  7. +293
    -127
      src/components/StockTakeManagement/StockTakeTab.tsx
  8. +24
    -0
      src/i18n/en/stockTake.json
  9. +24
    -0
      src/i18n/zh/stockTake.json

+ 114
- 5
src/app/api/stockTake/actions.ts Ver fichero

@@ -117,6 +117,27 @@ export const getInventoryLotDetailsBySection = async (
//console.log('[API] Response received:', response);
return response;
}

export const getInventoryLotDetailsByItem = async (
itemId: number,
stockTakeRoundId: number,
pageNum: number = 0,
pageSize: number = 10,
notMatchOnly: boolean = false,
) => {
const params = new URLSearchParams({
itemId: String(itemId),
stockTakeRoundId: String(stockTakeRoundId),
pageNum: String(pageNum),
pageSize: String(pageSize),
notMatchOnly: String(notMatchOnly),
});
return serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(
`${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsByItem?${params.toString()}`,
{ method: "GET" },
);
};

export interface SaveStockTakeRecordRequest {
stockTakeRecordId?: number | null;
inventoryLotLineId: number;
@@ -144,6 +165,8 @@ export interface AllPickedStockTakeListReponse {
warehouseArea: string | null;
storeId: string | null;
reStockTakeTrueFalse: boolean;
listMode?: string;
itemId?: number | null;
}

/** 與 Picker 列表一致:區域描述、盤點區域、貨品編號/名稱(逗號多關鍵字由後端解析) */
@@ -279,6 +302,7 @@ export const getStockTakeRecordsPaged = async (
area?: string;
storeId?: string;
onlyLatestRound?: boolean;
stockTakeRoundId?: number | null;
}
) => {
const searchParams = new URLSearchParams();
@@ -302,6 +326,9 @@ export const getStockTakeRecordsPaged = async (
if (params?.onlyLatestRound != null) {
searchParams.set("onlyLatestRound", String(params.onlyLatestRound));
}
if (params?.stockTakeRoundId != null && params.stockTakeRoundId > 0) {
searchParams.set("stockTakeRoundId", String(params.stockTakeRoundId));
}
const url = `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList?${searchParams.toString()}`;
const res = await serverFetchJson<RecordsRes<AllPickedStockTakeListReponse>>(url, { method: "GET" });
return res;
@@ -315,13 +342,72 @@ export const getApproverStockTakeRecords = async () => {
);
return stockTakeRecords;
}
export const getLatestApproverStockTakeHeader = async () => {
export const getLatestApproverStockTakeHeader = async (
stockTakeRoundId?: number | null
) => {
const params = new URLSearchParams();
if (stockTakeRoundId != null && stockTakeRoundId > 0) {
params.set("stockTakeRoundId", String(stockTakeRoundId));
}
const qs = params.toString();
return serverFetchJson<AllPickedStockTakeListReponse>(
`${BASE_API_URL}/stockTakeRecord/LatestApproverStockTakeHeader`,
`${BASE_API_URL}/stockTakeRecord/LatestApproverStockTakeHeader${qs ? `?${qs}` : ""}`,
{ method: "GET" }
);
}

export type StockTakeRoundCard = {
stockTakeRoundId: number;
stockTakeRoundName: string | null;
stockTakeId: number;
planStartDate: string | null;
yearMonth: string;
open: boolean;
status: string;
sectionCount: number;
totalItemNumber: number;
totalInventoryLotNumber: number;
stockTakenCount: number;
approvedCount: number;
mainFloor: string | null;
reStockTakeTrueFalse: boolean;
stockTakeRoundMode?: string | null;
};

export type StockTakeRoundCardsRes = {
hasOpenRounds: boolean;
rounds: StockTakeRoundCard[];
completedYearMonths: string[];
};

export const getStockTakeRoundCards = async (yearMonth?: string | null) => {
const params = new URLSearchParams();
if (yearMonth?.trim()) params.set("yearMonth", yearMonth.trim());
const qs = params.toString();
return serverFetchJson<StockTakeRoundCardsRes>(
`${BASE_API_URL}/stockTakeRecord/stockTakeRoundCards${qs ? `?${qs}` : ""}`,
{ method: "GET" },
);
};

export const getBusyStockTakeSections = async () => {
return serverFetchJson<string[]>(
`${BASE_API_URL}/stockTakeRecord/busyStockTakeSections`,
{ method: "GET" },
);
};

export const closeStockTakeRound = async (stockTakeRoundId: number) => {
return serverFetchJson<{ success: boolean; message?: string; closedCount?: number }>(
`${BASE_API_URL}/stockTakeRecord/closeRound`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ stockTakeRoundId }),
},
);
};

export type LatestStockTakeRoundMeta = {
stockTakeRoundId: number | null;
planStartDate: string | null;
@@ -333,6 +419,29 @@ export const getLatestStockTakeRoundMeta = async () => {
{ method: "GET" },
);
};
export const createStockTakeForItems = async (
itemIds: number[],
stockTakeRoundName?: string | null,
planStart?: string | null,
) => {
const trimmedName = stockTakeRoundName?.trim();
const trimmedPlanStart = planStart?.trim();
return serverFetchJson<Map<string, string>>(
`${BASE_API_URL}/stockTake/createForItems`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
itemIds,
...(trimmedName ? { stockTakeRoundName: trimmedName } : {}),
...(trimmedPlanStart ? { planStart: trimmedPlanStart } : {}),
}),
},
);
};

export const createStockTakeForSections = async (
sections: string[],
stockTakeRoundName?: string | null,
@@ -373,9 +482,9 @@ export const saveStockTakeRecord = async (
body: JSON.stringify(request),
},
);
console.log('saveStockTakeRecord: request:', request);
console.log('saveStockTakeRecord: stockTakeId:', stockTakeId);
console.log('saveStockTakeRecord: stockTakerId:', stockTakerId);
// console.log('saveStockTakeRecord: request:', request);
//console.log('saveStockTakeRecord: stockTakeId:', stockTakeId);
//console.log('saveStockTakeRecord: stockTakerId:', stockTakerId);

return result;
} catch (error: any) {


+ 21
- 12
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Ver fichero

@@ -37,6 +37,7 @@ import {
DialogActions,
} from "@mui/material";
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { createPortal } from "react-dom";
import { Collapse } from "@mui/material";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
@@ -75,6 +76,8 @@ interface ApproverStockTakeAllProps {
mode: "pending" | "approved";
onBack?: () => void;
onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
/** 審核待處理時,把批量保存按鈕掛到分頁列右側 */
batchButtonHost?: HTMLElement | null;
}

type QtySelectionType = "first" | "second" | "approver";
@@ -365,6 +368,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
mode,
onBack,
onSnackbar,
batchButtonHost = null,
}) => {
const { t } = useTranslation(["stockTake", "common"]);
const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent();
@@ -1623,8 +1627,21 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
const effectivePageSize =
pageSize === "all" ? Math.max(total || 0, 0) : (pageSize as number);

const batchSaveButton = mode === "pending" ? (
<Button
variant="contained"
color="primary"
onClick={handleOpenBatchSaveConfirm}
disabled={batchSaving}
>
{t("Batch Save All")}
</Button>
) : null;
const batchButtonInTabBar = Boolean(batchButtonHost && batchSaveButton);

return (
<Box>
{batchButtonInTabBar ? createPortal(batchSaveButton, batchButtonHost as HTMLElement) : null}
{onBack && (
<Button
onClick={onBack}
@@ -1633,32 +1650,24 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
{t("Back to List")}
</Button>
)}
{(uniqueWarehouses || (batchSaveButton && !batchButtonInTabBar)) && (
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: 2 }}
>
<Typography variant="h6" sx={{ mb: 2 }}>
<Typography variant="h6">
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>

<Stack direction="row" spacing={2} alignItems="center">
{mode === "pending" && (
<Button
variant="contained"
color="primary"
onClick={handleOpenBatchSaveConfirm}
disabled={batchSaving}
>
{t("Batch Save All")}
</Button>
)}
{!batchButtonInTabBar ? batchSaveButton : null}
</Stack>
</Stack>
)}
<Accordion
expanded={showFilters}
onChange={(_, expanded) => setShowFilters(expanded)}


+ 408
- 72
src/components/StockTakeManagement/PickerCardList.tsx Ver fichero

@@ -31,6 +31,7 @@ import {
Tabs,
ToggleButton,
ToggleButtonGroup,
Checkbox,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
@@ -42,10 +43,14 @@ import { useTranslation } from "react-i18next";
import duration from "dayjs/plugin/duration";
import {
AllPickedStockTakeListReponse,
createStockTakeForItems,
createStockTakeForSections,
getStockTakeRecordsPaged,
getLatestStockTakeRoundMeta,
getBusyStockTakeSections,
getStockTakeRoundCards,
} from "@/app/api/stockTake/actions";
import { fetchItemsByPage } from "@/app/api/settings/item/actions";
import { fetchStockTakeSections } from "@/app/api/warehouse/actions";
import { fetchMissingStockTakeSectionIssues, fetchWarehouseListClient } from "@/app/api/warehouse/client";
import type { MissingStockTakeSectionIssueItem, StockTakeSectionInfo, WarehouseResult } from "@/app/api/warehouse";
@@ -74,6 +79,15 @@ function chunkList<T>(items: T[], size: number): T[][] {
return rows;
}

type CreateDialogItemHit = { id: number; code: string; name: string };

function itemRecordsFromPage(res: unknown): { id: string | number; code?: string; name?: string }[] {
const records = (res as { records?: unknown[] })?.records;
return Array.isArray(records)
? (records as { id: string | number; code?: string; name?: string }[])
: [];
}

function withAllOption(options: string[]): string[] {
return options.length > 1 ? [LOCATION_ALL, ...options] : options;
}
@@ -101,10 +115,14 @@ function matchesPickerCardFilters(
}
if (filters.status && filters.status !== "All") {
const normalized = filters.status.trim().toLowerCase();
const accepted = normalized === "stocktaking"
? ["stocktaking", "processing", "in_progress"]
: [normalized];
if (!accepted.includes((session.status || "").trim().toLowerCase())) return false;
if (normalized === "notmatch") {
if (!session.reStockTakeTrueFalse) return false;
} else {
const accepted = normalized === "stocktaking"
? ["stocktaking", "processing", "in_progress"]
: [normalized];
if (!accepted.includes((session.status || "").trim().toLowerCase())) return false;
}
}
const area = filters.area?.trim() ?? "";
if (area && !(session.warehouseArea || "").toLowerCase().includes(area.toLowerCase())) return false;
@@ -200,6 +218,11 @@ interface PickerCardListProps {
/** 樓層/倉庫/倉位,由父層保存,從明細返回時仍顯示 */
locationPick: PickerLocationPick;
onLocationPickChange: (next: PickerLocationPick) => void;
stockTakeRoundId?: number | null;
hideSectionList?: boolean;
autoOpenCreate?: boolean;
onCreateDialogClose?: () => void;
onRoundsChanged?: () => void;
}

export type PickerSearchKind = "location" | "section";
@@ -284,6 +307,11 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
onAppliedFiltersChange,
locationPick,
onLocationPickChange,
stockTakeRoundId = null,
hideSectionList = false,
autoOpenCreate = false,
onCreateDialogClose,
onRoundsChanged,
}) => {
const { t } = useTranslation(["stockTake", "common"]);
const router = useRouter();
@@ -300,6 +328,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
const [globalRoundPlanStartDate, setGlobalRoundPlanStartDate] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const [busySections, setBusySections] = useState<Set<string>>(() => new Set());
const [openCreateStockTakeSummaryConfirm, setOpenCreateStockTakeSummaryConfirm] = useState(false);
const [missingSectionWarnDrawerOpen, setMissingSectionWarnDrawerOpen] = useState(false);
const [missingSectionCount, setMissingSectionCount] = useState(0);
@@ -312,6 +341,13 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
/** 建立盤點對話框:目前選中的樓層分頁(對應 sortedFloorKeys 索引) */
const [createFloorTabIndex, setCreateFloorTabIndex] = useState(0);
const [createDialogSearchQuery, setCreateDialogSearchQuery] = useState("");
const [createMode, setCreateMode] = useState<"section" | "item">("section");
const [blockSectionCreate, setBlockSectionCreate] = useState(false);
const [createDialogItemCode, setCreateDialogItemCode] = useState("");
const [createDialogItemName, setCreateDialogItemName] = useState("");
const [createDialogItemHits, setCreateDialogItemHits] = useState<CreateDialogItemHit[]>([]);
const [createDialogSelectedItems, setCreateDialogSelectedItems] = useState<CreateDialogItemHit[]>([]);
const [createDialogItemSearching, setCreateDialogItemSearching] = useState(false);
const [sectionsLoading, setSectionsLoading] = useState(false);
const [stockTakeSectionRows, setStockTakeSectionRows] = useState<StockTakeSectionInfo[]>([]);
const createStockTakeInFlightRef = useRef(false);
@@ -341,7 +377,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
: null,
);

const statusOptions = ["pending", "stockTaking", "approving", "completed"];
const statusOptions = ["All", "pending", "stockTaking", "approving", "completed", "notMatch"];

const searchKind = searchFilters.searchKind ?? "location";

@@ -377,14 +413,24 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}, []);

useEffect(() => {
if (autoOpenCreate) setOpenConfirmDialog(true);
}, [autoOpenCreate]);

useEffect(() => {
if (hideSectionList) {
setLoading(false);
return;
}
let cancelled = false;
setLoading(true);
getStockTakeRecordsPaged(0, 10000, {
sectionDescription: "All",
stockTakeSections: "",
status: "All",
area: "",
storeId: "All",
onlyLatestRound: true,
onlyLatestRound: !(stockTakeRoundId != null && stockTakeRoundId > 0),
stockTakeRoundId: stockTakeRoundId != null && stockTakeRoundId > 0 ? stockTakeRoundId : undefined,
})
.then((res) => {
if (cancelled) return;
@@ -400,7 +446,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
return () => {
cancelled = true;
};
}, [listRefreshNonce]);
}, [hideSectionList, listRefreshNonce, stockTakeRoundId]);

const roundSectionCodes = useMemo(
() =>
@@ -649,6 +695,44 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
void loadMissingStockTakeSectionIssues();
}, [openConfirmDialog, loadMissingStockTakeSectionIssues]);

useEffect(() => {
if (!openConfirmDialog) return;
let cancelled = false;
getBusyStockTakeSections()
.then((list) => {
if (cancelled) return;
setBusySections(new Set((Array.isArray(list) ? list : []).map((s) => s.trim()).filter(Boolean)));
})
.catch((e) => {
console.error("Failed to load busy stock take sections:", e);
if (!cancelled) setBusySections(new Set());
});
return () => {
cancelled = true;
};
}, [openConfirmDialog]);

useEffect(() => {
if (!openConfirmDialog) return;
let cancelled = false;
getStockTakeRoundCards()
.then((res) => {
if (cancelled) return;
const blocked = (res.rounds || []).some(
(round) => round.open && round.stockTakeRoundMode === "item",
);
setBlockSectionCreate(blocked);
if (blocked) setCreateMode("item");
})
.catch((e) => {
console.error("Failed to check open item stock take rounds:", e);
if (!cancelled) setBlockSectionCreate(false);
});
return () => {
cancelled = true;
};
}, [openConfirmDialog]);

const handleGoWarehouseSettings = useCallback(() => {
setMissingSectionWarnDrawerOpen(false);
setOpenConfirmDialog(false);
@@ -660,10 +744,19 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}, [createDialogSelectedSections]);

const handleOpenCreateStockTakeSummaryConfirm = useCallback(() => {
if (createDialogSelectedSections.length === 0) return;
if (createDialogPlanStart == null || !createDialogPlanStart.isValid()) return;
if (createMode === "item") {
if (createDialogSelectedItems.length === 0) return;
} else if (createDialogSelectedSections.length === 0) {
return;
}
setOpenCreateStockTakeSummaryConfirm(true);
}, [createDialogSelectedSections.length, createDialogPlanStart]);
}, [
createDialogPlanStart,
createDialogSelectedItems.length,
createDialogSelectedSections.length,
createMode,
]);

/** 盤點區域 → 樓層:優先 API(warehouse 帶 storeId),否則用列表卡片上的 storeId */
const sectionsByStore = useMemo(() => {
@@ -715,11 +808,18 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
return keys;
}, [sectionsByStore]);

const isSectionBusy = useCallback(
(section: string) => busySections.has(section.trim()),
[busySections],
);

const allSectionsForCreateDialog = useMemo(() => {
const s = new Set<string>();
sectionsByStore.forEach((arr) => arr.forEach((x) => s.add(x)));
return Array.from(s).sort((a, b) => a.localeCompare(b));
}, [sectionsByStore]);
return Array.from(s)
.filter((sec) => !busySections.has(sec.trim()))
.sort((a, b) => a.localeCompare(b));
}, [busySections, sectionsByStore]);

useEffect(() => {
if (createFloorTabIndex >= sortedFloorKeys.length && sortedFloorKeys.length > 0) {
@@ -734,7 +834,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({

const toggleFloorSelectAll = useCallback(
(storeKey: string) => {
const list = sectionsByStore.get(storeKey) ?? [];
const list = (sectionsByStore.get(storeKey) ?? []).filter((s) => !busySections.has(s.trim()));
setCreateDialogSelectedSections((prev) => {
const allIn = list.length > 0 && list.every((s) => prev.includes(s));
if (allIn) {
@@ -745,7 +845,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
return Array.from(next).sort((a, b) => a.localeCompare(b));
});
},
[sectionsByStore],
[busySections, sectionsByStore],
);

/** 與卡片標題括號內相同:描述 / 區域 / 樓層 */
@@ -842,21 +942,27 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
drag.sectionIds,
);
const merged = new Set(drag.baseSelection);
hit.forEach((id) => merged.add(id));
hit.forEach((id) => {
if (!busySections.has(id.trim())) merged.add(id);
});
setCreateDialogSelectedSections(
Array.from(merged).sort((a, b) => a.localeCompare(b)),
);
} else if (drag?.pointerDownSection) {
const sec = drag.pointerDownSection;
setCreateDialogSelectedSections((prev) => {
if (prev.includes(sec)) return prev.filter((s) => s !== sec);
return [...prev, sec].sort((a, b) => a.localeCompare(b));
});
if (busySections.has(sec.trim())) {
/* in progress */
} else {
setCreateDialogSelectedSections((prev) => {
if (prev.includes(sec)) return prev.filter((s) => s !== sec);
return [...prev, sec].sort((a, b) => a.localeCompare(b));
});
}
}
marqueeDragRef.current = null;
setMarqueeRect(null);
setMarqueePreviewIds(new Set());
}, [getSectionIdsInMarquee]);
}, [busySections, getSectionIdsInMarquee]);

const handleCreateSectionGridPointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
@@ -920,7 +1026,11 @@ const PickerCardList: React.FC<PickerCardListProps> = ({

const handleCreateStockTake = useCallback(async () => {
if (createStockTakeInFlightRef.current) return;
if (createDialogSelectedSections.length === 0) return;
if (createMode === "item") {
if (createDialogSelectedItems.length === 0) return;
} else if (createDialogSelectedSections.length === 0) {
return;
}
createStockTakeInFlightRef.current = true;
setOpenCreateStockTakeSummaryConfirm(false);
setOpenConfirmDialog(false);
@@ -930,11 +1040,18 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
createDialogPlanStart != null
? dayjsToDateTimeString(createDialogPlanStart.startOf("day"))
: dayjsToDateTimeString(dayjs().startOf("day"));
const result = await createStockTakeForSections(
createDialogSelectedSections,
createDialogRoundName,
planStart,
);
const result =
createMode === "item"
? await createStockTakeForItems(
createDialogSelectedItems.map((it) => it.id),
createDialogRoundName,
planStart,
)
: await createStockTakeForSections(
createDialogSelectedSections,
createDialogRoundName,
planStart,
);
const createdCount = Object.values(result).filter((msg) => msg.startsWith("Created:")).length;
const skippedCount = Object.values(result).filter((msg) => msg.startsWith("Skipped:")).length;
const errorCount = Object.values(result).filter((msg) => msg.startsWith("Error:")).length;
@@ -948,15 +1065,68 @@ const PickerCardList: React.FC<PickerCardListProps> = ({

onListPageChange(0);
setListRefreshNonce((n) => n + 1);
onRoundsChanged?.();
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
setCreateDialogSelectedItems([]);
setCreateMode("section");
} catch (e) {
console.error(e);
} finally {
setCreating(false);
createStockTakeInFlightRef.current = false;
}
}, [createDialogPlanStart, createDialogRoundName, createDialogSelectedSections, onListPageChange, t]);
}, [
createDialogPlanStart,
createDialogRoundName,
createDialogSelectedItems,
createDialogSelectedSections,
createMode,
onListPageChange,
onRoundsChanged,
t,
]);
useEffect(() => {
if (!openConfirmDialog || createMode !== "item") return;
const code = createDialogItemCode.trim();
const name = createDialogItemName.trim();
if (!code && !name) {
setCreateDialogItemHits([]);
return;
}
let cancelled = false;
const timer = window.setTimeout(() => {
setCreateDialogItemSearching(true);
const params: Record<string, string | number> = { pageSize: 300, pageNum: 1 };
if (code) params.code = code;
if (name) params.name = name;
fetchItemsByPage(params)
.then((page) => {
if (cancelled) return;
const uniq: CreateDialogItemHit[] = [];
itemRecordsFromPage(page).forEach((row) => {
const id = Number(row.id);
if (!id) return;
uniq.push({ id, code: row.code || "", name: row.name || "" });
});
uniq.sort((a, b) =>
a.code.localeCompare(b.code, undefined, { numeric: true, sensitivity: "base" }),
);
setCreateDialogItemHits(uniq);
})
.catch((e) => {
console.error("Failed to search items for stock take create:", e);
if (!cancelled) setCreateDialogItemHits([]);
})
.finally(() => {
if (!cancelled) setCreateDialogItemSearching(false);
});
}, 300);
return () => {
cancelled = true;
window.clearTimeout(timer);
};
}, [openConfirmDialog, createMode, createDialogItemCode, createDialogItemName]);
useEffect(() => {
setSectionsLoading(true);
fetchStockTakeSections()
@@ -1060,8 +1230,21 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100);
};
const planStartDate = globalRoundPlanStartDate;
const closeCreateDialog = () => {
setOpenConfirmDialog(false);
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
setCreateMode(blockSectionCreate ? "item" : "section");
setCreateDialogItemCode("");
setCreateDialogItemName("");
setCreateDialogItemHits([]);
setCreateDialogSelectedItems([]);
onCreateDialogClose?.();
};
return (
<Box>
{!hideSectionList && (
<>
<Card elevation={0} sx={{ mb: 2 }}>
<CardContent>
<Typography variant="overline" sx={{ display: "block", mb: 1 }}>
@@ -1173,27 +1356,26 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
</FormControl>
</Grid>
)}
<Grid item xs={12} md={4} sx={{ order: 2 }}>
<FormControl fullWidth size="small" sx={searchFieldSx}>
<InputLabel>{t("Status")}</InputLabel>
<Select
value={searchFilters.status}
label={t("Status")}
onChange={(e) =>
applyFilters({
...searchFilters,
status: e.target.value,
})
}
<Grid item xs={12} sx={{ order: searchKind === "location" ? 2 : undefined }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}>
<Typography variant="body2" sx={{ minWidth: 72, fontWeight: 600 }}>
{t("Status")}
</Typography>
<ToggleButtonGroup
exclusive
value={searchFilters.status || "All"}
onChange={(_, next: string | null) => {
if (next == null) return;
applyFilters({ ...searchFilters, status: next });
}}
>
<MenuItem value="All">{t("All")}</MenuItem>
{statusOptions.map((status) => (
<MenuItem key={status} value={status}>
{t(status)}
</MenuItem>
<ToggleButton key={status} value={status} sx={{ minHeight: 48, px: 2, textTransform: "none" }}>
{status === "All" ? t("All") : t(status)}
</ToggleButton>
))}
</Select>
</FormControl>
</ToggleButtonGroup>
</Box>
</Grid>
{searchKind === "location" && (
<Grid item xs={12} sx={{ order: 1 }}>
@@ -1367,12 +1549,13 @@ const PickerCardList: React.FC<PickerCardListProps> = ({

<Typography variant="body2" color="text.secondary">
{t("Total Sections")}: {total}
{roundRecords.some((s) => s.listMode === "item") ? t("Total Items") : t("Total Sections")}: {total}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Start Stock Take Date")}: {planStartDate || "-"}
</Typography>

{stockTakeRoundId != null && stockTakeRoundId > 0 ? null : (
<Button
variant="contained"
color="primary"
@@ -1381,6 +1564,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
>
{creating ? <CircularProgress size={20} /> : t("Create Stock Take (Select Sections)")}
</Button>
)}
</Box>

{loading ? (
@@ -1415,8 +1599,11 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
<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}
{sectionMeta ? ` (${sectionMeta})` : null}
{session.listMode === "item"
? `${t("Item")}: ${session.stockTakeSession}${
session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : ""
}`
: `${t("Section")}: ${session.stockTakeSession}${sectionMeta ? ` (${sectionMeta})` : ""}`}
</Typography>
</Stack>
@@ -1455,7 +1642,12 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
{t("View ReStockTake")}
</Button>
</Stack>
<Chip size="small" label={t(session.status || "")} color={statusColor as any} />
<Stack direction="row" spacing={0.5} flexWrap="wrap" justifyContent="flex-end">
{session.reStockTakeTrueFalse ? (
<Chip size="small" label={t("notMatch")} color="warning" />
) : null}
<Chip size="small" label={t(session.status || "")} color={statusColor as any} />
</Stack>
</CardActions>
</Card>
</Grid>
@@ -1475,15 +1667,13 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}}
rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死
/>
)}
</>
)}
{/* Create Stock Take 確認 Dialog */}
<Dialog
open={openConfirmDialog}
onClose={() => {
setOpenConfirmDialog(false);
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
}}
onClose={closeCreateDialog}
maxWidth={false}
fullWidth
PaperProps={{
@@ -1536,6 +1726,20 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
</Box>
</Tooltip>
</DialogTitle>
<Box sx={{ px: 2, flexShrink: 0 }}>
<Tabs
value={createMode}
onChange={(_, v: "section" | "item") => {
if (!v) return;
setCreateMode(v);
}}
>
{blockSectionCreate ? null : (
<Tab value="section" label={t("Create by section")} />
)}
<Tab value="item" label={t("Create by item")} />
</Tabs>
</Box>
<DialogContent
dividers
sx={{
@@ -1611,7 +1815,11 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
/>
</LocalizationProvider>
</Box>
{sortedFloorKeys.length === 0 && !sectionsLoading ? (
{createMode === "item" ? (
<Typography variant="body2" color="text.secondary">
{t("Create by item hint")}
</Typography>
) : sortedFloorKeys.length === 0 && !sectionsLoading ? (
<Typography variant="body2" color="text.secondary">
{t("No stock take sections from warehouse")}
</Typography>
@@ -1671,6 +1879,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
})}
</Stack>
)}
{createMode === "item" ? null : (
<Stack spacing={1}>
<Button
fullWidth
@@ -1690,12 +1899,15 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
{t("Clear selection all floors")}
</Button>
</Stack>
)}
<Typography variant="body2" color="text.secondary">
{t("Total selected sections label")}{" "}
{createMode === "item" ? t("Total selected items label") : t("Total selected sections label")}{" "}
<Typography component="span" variant="body2" color="primary.main" fontWeight={700}>
{createDialogSelectedSections.length}
{createMode === "item"
? createDialogSelectedItems.length
: createDialogSelectedSections.length}
</Typography>{" "}
{t("sections unit")}
{createMode === "item" ? t("items unit") : t("sections unit")}
</Typography>
</Box>

@@ -1712,7 +1924,108 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
overflow: "hidden",
}}
>
{activeCreateFloorKey != null ? (
{createMode === "item" ? (
<Stack spacing={1.5} sx={{ flex: 1, minHeight: 0 }}>
<Stack direction={{ xs: "column", sm: "row" }} spacing={1}>
<TextField
fullWidth
size="small"
hiddenLabel
placeholder={t("Item Code")}
value={createDialogItemCode}
onChange={(e) => setCreateDialogItemCode(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" color="action" />
</InputAdornment>
),
}}
sx={createDialogCompactInputSx}
/>
<TextField
fullWidth
size="small"
hiddenLabel
placeholder={t("Item Name")}
value={createDialogItemName}
onChange={(e) => setCreateDialogItemName(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" color="action" />
</InputAdornment>
),
}}
sx={createDialogCompactInputSx}
/>
</Stack>
<Typography variant="caption" color="text.secondary">
{t("Create by item selected count", { count: createDialogSelectedItems.length })}
</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.75 }}>
{createDialogSelectedItems.map((it) => (
<Chip
key={it.id}
size="small"
label={`${it.code} ${it.name}`.trim()}
onDelete={() =>
setCreateDialogSelectedItems((prev) => prev.filter((x) => x.id !== it.id))
}
/>
))}
</Box>
<Box sx={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
{createDialogItemSearching ? (
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
<CircularProgress size={24} />
</Box>
) : createDialogItemHits.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{createDialogItemCode.trim() || createDialogItemName.trim()
? t("No data")
: t("Create by item search hint")}
</Typography>
) : (
<Stack>
{createDialogItemHits.map((it) => {
const checked = createDialogSelectedItems.some((x) => x.id === it.id);
return (
<Box
key={it.id}
sx={{
display: "flex",
alignItems: "center",
gap: 1,
py: 0.5,
borderBottom: 1,
borderColor: "divider",
}}
>
<Checkbox
checked={checked}
onChange={() => {
setCreateDialogSelectedItems((prev) =>
checked ? prev.filter((x) => x.id !== it.id) : [...prev, it],
);
}}
/>
<Box>
<Typography variant="body2" fontWeight={600}>
{it.code}
</Typography>
<Typography variant="caption" color="text.secondary">
{it.name}
</Typography>
</Box>
</Box>
);
})}
</Stack>
)}
</Box>
</Stack>
) : activeCreateFloorKey != null ? (
<>
<Stack
direction="row"
@@ -1829,9 +2142,10 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
>
{createDialogFilteredSections.map((section) => {
const meta = getCreateDialogSectionMeta(section);
const busy = isSectionBusy(section);
const checked = createDialogSelectedSections.includes(section);
const previewSelected = marqueePreviewIds.has(section);
const visuallySelected = checked || previewSelected;
const previewSelected = !busy && marqueePreviewIds.has(section);
const visuallySelected = !busy && (checked || previewSelected);
return (
<Card
key={section}
@@ -1846,7 +2160,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}
}}
sx={{
cursor: "default",
cursor: busy ? "not-allowed" : "default",
opacity: busy ? 0.55 : 1,
borderWidth: visuallySelected ? 2 : 1,
borderColor: visuallySelected ? "primary.main" : "divider",
bgcolor: visuallySelected
@@ -1864,6 +2179,11 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
<Typography variant="body2" fontWeight={700} noWrap title={section}>
{section}
</Typography>
{busy ? (
<Typography variant="caption" color="warning.main" display="block">
{t("In progress")}
</Typography>
) : null}
{meta ? (
<Typography
variant="caption"
@@ -1905,11 +2225,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
</DialogContent>
<DialogActions sx={{ px: 3, py: 2, flexShrink: 0 }}>
<Button
onClick={() => {
setOpenConfirmDialog(false);
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
}}
onClick={closeCreateDialog}
>
{t("Cancel")}
</Button>
@@ -1919,7 +2235,9 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
onClick={handleOpenCreateStockTakeSummaryConfirm}
disabled={
creating ||
createDialogSelectedSections.length === 0 ||
(createMode === "item"
? createDialogSelectedItems.length === 0
: createDialogSelectedSections.length === 0) ||
createDialogPlanStart == null ||
!createDialogPlanStart.isValid()
}
@@ -1955,11 +2273,13 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
: "-"}
</Typography>
<Typography variant="body2" fontWeight={600}>
{t("Total selected sections label")}{" "}
{createMode === "item" ? t("Total selected items label") : t("Total selected sections label")}{" "}
<Typography component="span"fontWeight={600} >
{createStockTakeSummarySections.length}
{createMode === "item"
? createDialogSelectedItems.length
: createStockTakeSummarySections.length}
</Typography>{" "}
{t("sections unit")}
{createMode === "item" ? t("items unit") : t("sections unit")}
</Typography>
</Stack>
<Box
@@ -1973,7 +2293,18 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}}
>
<Stack spacing={1}>
{createStockTakeSummarySections.map((section) => {
{createMode === "item"
? createDialogSelectedItems.map((it) => (
<Box key={it.id}>
<Typography variant="body2" fontWeight={600}>
{it.code}
</Typography>
<Typography variant="caption" color="text.secondary">
{it.name}
</Typography>
</Box>
))
: createStockTakeSummarySections.map((section) => {
const meta = getCreateDialogSectionMeta(section);
return (
<Box key={section}>
@@ -2002,7 +2333,12 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
variant="contained"
color="primary"
onClick={handleCreateStockTake}
disabled={creating || createDialogSelectedSections.length === 0}
disabled={
creating ||
(createMode === "item"
? createDialogSelectedItems.length === 0
: createDialogSelectedSections.length === 0)
}
>
{creating ? <CircularProgress size={20} /> : t("Confirm")}
</Button>


+ 42
- 18
src/components/StockTakeManagement/PickerReStockTake.tsx Ver fichero

@@ -27,6 +27,7 @@ import {
BatchSaveStockTakeRecordRequest,
batchSaveStockTakeRecords,
batchSavePickerStockTakeInputs,
getInventoryLotDetailsByItem,
getInventoryLotDetailsBySectionNotMatch
} from "@/app/api/stockTake/actions";
import { buildPickerBatchSaveRequests } from "./buildPickerBatchSaveRequests";
@@ -117,15 +118,26 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
actualSize = typeof size === 'string' ? parseInt(size, 10) : size;
}
const response = await getInventoryLotDetailsBySectionNotMatch(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize,
const roundId =
selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0
? selectedSession.stockTakeRoundId
: null
);
: null;
const response =
selectedSession.listMode === "item" && selectedSession.itemId && roundId
? await getInventoryLotDetailsByItem(
selectedSession.itemId,
roundId,
pageNum,
actualSize,
true,
)
: await getInventoryLotDetailsBySectionNotMatch(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize,
roundId,
);
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
setTotal(response.total || 0);
} catch (e) {
@@ -482,7 +494,11 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
{t("Back to List")}
</Button>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{selectedSession.listMode === "item"
? `${t("Item")}: ${selectedSession.stockTakeSession}${
selectedSession.stockTakeSectionDescription ? ` (${selectedSession.stockTakeSectionDescription})` : ""
}`
: `${t("Stock Take Section")}: ${selectedSession.stockTakeSession}`}
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
@@ -507,7 +523,9 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>
{selectedSession.listMode === "item" ? t("Warehouse code") : t("Warehouse Location")}
</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell sx={{ width: 250, minWidth: 250 }}>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
@@ -536,7 +554,21 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({

return (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
<TableCell>
{selectedSession.listMode === "item" ? (
<Stack spacing={0.25}>
<Typography fontWeight={700}>{detail.warehouseCode || "-"}</Typography>
<Typography variant="body2" color="text.secondary">
{[detail.warehouseArea, detail.warehouseSlot].filter((v) => v?.trim()).join(" ") || "-"}
</Typography>
</Stack>
) : (
<>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</>
)}
</TableCell>
<TableCell sx={{
maxWidth: 280,
wordBreak: 'break-word',
@@ -561,7 +593,6 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
{!submitDisabled && isFirstSubmit ? (
<Stack spacing={0.5} alignItems="flex-start">
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("First")}:</Typography>
<TextField
size="small"
type="number"
@@ -645,7 +676,6 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
{!submitDisabled && isSecondSubmit ? (
<Stack spacing={0.5} alignItems="flex-start">
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("Second")}:</Typography>
<TextField
size="small"
type="number"
@@ -724,12 +754,6 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
/>
</Typography>
) : null}

{!detail.firstStockTakeQty && !detail.secondStockTakeQty && !submitDisabled && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell>


+ 34
- 25
src/components/StockTakeManagement/PickerStockTake.tsx Ver fichero

@@ -26,6 +26,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
AllPickedStockTakeListReponse,
getInventoryLotDetailsByItem,
getInventoryLotDetailsBySection,
InventoryLotDetailResponse,
saveStockTakeRecord,
@@ -149,16 +150,20 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
console.log('Using specific size - actualSize set to:', actualSize);
}
console.log('Calling getInventoryLotDetailsBySection with actualSize:', actualSize);
const response = await getInventoryLotDetailsBySection(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize,
const roundId =
selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0
? selectedSession.stockTakeRoundId
: null
);
: null;
const response =
selectedSession.listMode === "item" && selectedSession.itemId && roundId
? await getInventoryLotDetailsByItem(selectedSession.itemId, roundId, pageNum, actualSize)
: await getInventoryLotDetailsBySection(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize,
roundId,
);
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
setTotal(response.total || 0);
} catch (e) {
@@ -547,7 +552,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
{t("Back to List")}
</Button>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{selectedSession.listMode === "item"
? `${t("Item")}: ${selectedSession.stockTakeSession}${
selectedSession.stockTakeSectionDescription ? ` (${selectedSession.stockTakeSectionDescription})` : ""
}`
: `${t("Stock Take Section")}: ${selectedSession.stockTakeSession}`}
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
@@ -604,7 +613,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>
{selectedSession.listMode === "item" ? t("Warehouse code") : t("Warehouse Location")}
</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell sx={{ width: 250, minWidth: 250 }}>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
@@ -644,8 +655,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}}
>
<TableCell>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
{selectedSession.listMode === "item" ? (
<Stack spacing={0.25}>
<Typography fontWeight={700}>{detail.warehouseCode || "-"}</Typography>
<Typography variant="body2" color="text.secondary">
{[detail.warehouseArea, detail.warehouseSlot].filter((v) => v?.trim()).join(" ") || "-"}
</Typography>
</Stack>
) : (
<>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</>
)}
</TableCell>
<TableCell
sx={{
@@ -678,7 +700,6 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
{!submitDisabled && isFirstSubmit ? (
<Stack spacing={0.5} alignItems="flex-start">
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("First")}:</Typography>
<TextField
size="small"
type="text"
@@ -764,7 +785,6 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
{!submitDisabled && isSecondSubmit ? (
<Stack spacing={0.5} alignItems="flex-start">
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("Second")}:</Typography>
<TextField
size="small"
type="text"
@@ -841,17 +861,6 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
/>
</Typography>
) : null}

{!detail.firstStockTakeQty &&
!detail.secondStockTakeQty &&
!submitDisabled && (
<Typography
variant="body2"
color="text.secondary"
>
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell>


+ 293
- 0
src/components/StockTakeManagement/StockTakeRoundCardList.tsx Ver fichero

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

import {
Box,
Button,
Card,
CardActions,
CardContent,
Chip,
CircularProgress,
Grid,
LinearProgress,
Stack,
Typography,
} from "@mui/material";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
closeStockTakeRound,
getStockTakeRoundCards,
type AllPickedStockTakeListReponse,
type StockTakeRoundCard,
} from "@/app/api/stockTake/actions";

export function roundCardToSession(card: StockTakeRoundCard): AllPickedStockTakeListReponse {
return {
id: card.stockTakeRoundId,
stockTakeSession: card.stockTakeRoundName || "",
lastStockTakeDate: null,
status: card.status,
approverName: null,
currentStockTakeItemNumber: 0,
totalInventoryLotNumber: card.totalInventoryLotNumber || 0,
stockTakeId: card.stockTakeId,
stockTakeRoundId: card.stockTakeRoundId,
stockTakerName: null,
totalItemNumber: card.totalItemNumber || 0,
startTime: null,
endTime: null,
planStartDate: card.planStartDate,
stockTakeSectionDescription: null,
warehouseArea: null,
storeId: card.mainFloor,
reStockTakeTrueFalse: card.reStockTakeTrueFalse,
listMode: card.stockTakeRoundMode === "item" ? "item" : "section",
};
}

function progressPercent(done: number, total: number): number {
if (total <= 0) return 0;
return Math.min(100, Math.round((done / total) * 100));
}

interface StockTakeRoundCardListProps {
isAdmin: boolean;
onEnterRound: (card: StockTakeRoundCard, rounds: StockTakeRoundCard[]) => void;
onCreateClick: () => void;
creating?: boolean;
refreshNonce?: number;
yearMonth: string;
onYearMonthChange: (yearMonth: string) => void;
}

const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({
isAdmin,
onEnterRound,
onCreateClick,
creating = false,
refreshNonce = 0,
yearMonth,
onYearMonthChange,
}) => {
const { t } = useTranslation(["stockTake", "common"]);
const [loading, setLoading] = useState(true);
const [hasOpenRounds, setHasOpenRounds] = useState(false);
const [rounds, setRounds] = useState<StockTakeRoundCard[]>([]);
const [completedYearMonths, setCompletedYearMonths] = useState<string[]>([]);
const [closingId, setClosingId] = useState<number | null>(null);
const closeLockRef = useRef(false);
const thisMonth = dayjs().format("YYYY-MM");

const load = useCallback(async (ym: string) => {
setLoading(true);
try {
const res = await getStockTakeRoundCards(ym);
setHasOpenRounds(Boolean(res.hasOpenRounds));
setRounds(Array.isArray(res.rounds) ? res.rounds : []);
const months = Array.isArray(res.completedYearMonths) ? res.completedYearMonths : [];
setCompletedYearMonths(months);
} catch (e) {
console.error("Failed to load stock take rounds:", e);
setHasOpenRounds(false);
setRounds([]);
setCompletedYearMonths([]);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
void load(yearMonth);
}, [load, yearMonth, refreshNonce]);

// Completed months plus current month (even if no round created yet). No future months.
const monthOptions = useMemo(() => {
const set = new Set(completedYearMonths.filter((m) => m && m <= thisMonth));
set.add(thisMonth);
return Array.from(set).sort((a, b) => b.localeCompare(a));
}, [completedYearMonths, thisMonth]);

const monthIndex = useMemo(() => {
const i = monthOptions.indexOf(yearMonth);
return i >= 0 ? i : 0;
}, [monthOptions, yearMonth]);

const goMonth = (delta: number) => {
if (hasOpenRounds || monthOptions.length === 0) return;
const next = monthIndex + delta;
if (next < 0 || next >= monthOptions.length) return;
onYearMonthChange(monthOptions[next]);
};

const handleCloseRound = async (card: StockTakeRoundCard) => {
if (!isAdmin || closeLockRef.current) return;
closeLockRef.current = true;
setClosingId(card.stockTakeRoundId);
try {
await closeStockTakeRound(card.stockTakeRoundId);
await load(yearMonth);
} catch (e) {
console.error("Failed to close stock take round:", e);
} finally {
setClosingId(null);
closeLockRef.current = false;
}
};

const roundLabel = (card: StockTakeRoundCard) => {
const name = card.stockTakeRoundName?.trim();
if (name) return name;
return `${t("Stock Take Round")} #${card.stockTakeRoundId}`;
};

const statusColor = (status: string) => {
const s = (status || "").toLowerCase();
if (s === "completed") return "success";
if (s === "approving") return "info";
if (s === "stocktaking") return "primary";
return "warning";
};

return (
<Box>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, gap: 1, flexWrap: "wrap" }}>
<Typography variant="body2" color="text.secondary">
{hasOpenRounds
? t("Open rounds this month")
: t("Completed rounds for month", { month: yearMonth })}
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
{!hasOpenRounds && monthOptions.length > 0 ? (
<>
<Button
size="small"
variant="outlined"
disabled={monthIndex >= monthOptions.length - 1}
onClick={() => goMonth(1)}
>
{t("Previous month")}
</Button>
<Typography variant="body2" sx={{ minWidth: 80, textAlign: "center" }}>
{yearMonth}
</Typography>
<Button size="small" variant="outlined" disabled={monthIndex <= 0} onClick={() => goMonth(-1)}>
{t("Next month")}
</Button>
</>
) : null}
<Button variant="contained" onClick={onCreateClick} disabled={creating || !isAdmin}>
{creating ? <CircularProgress size={20} /> : t("Create Stock Take (Select Sections)")}
</Button>
</Stack>
</Box>

{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : rounds.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 4, textAlign: "center" }}>
{t("No data")}
</Typography>
) : (
<Grid container spacing={2}>
{rounds.map((card) => {
const isItemRound = card.stockTakeRoundMode === "item";
const total = isItemRound ? (card.totalInventoryLotNumber || 0) : (card.sectionCount || 0);
const taken = card.stockTakenCount || 0;
const approved = card.approvedCount || 0;
return (
<Grid key={card.stockTakeRoundId} item xs={12} sm={6} md={4}>
<Card
sx={{
minHeight: 180,
display: "flex",
flexDirection: "column",
border: "1px solid",
borderColor: card.open ? "primary.main" : "success.main",
}}
>
<CardContent sx={{ pb: 1, flexGrow: 1 }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}>
<Typography variant="subtitle1" fontWeight={600}>
{roundLabel(card)}
</Typography>
{card.stockTakeRoundMode === "item" ? (
<Chip size="small" label={t("Create by item")} color="info" />
) : null}
</Stack>
<Typography variant="body2" color="text.secondary">
{t("Plan Start Date")}:{" "}
{card.planStartDate ? dayjs(card.planStartDate).format(OUTPUT_DATE_FORMAT) : "-"}
</Typography>
<Typography variant="body2" color="text.secondary">
{card.stockTakeRoundMode === "item"
? `${t("Total Items")}: ${card.totalItemNumber || 0}`
: `${t("Total Sections")}: ${card.sectionCount}`}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Main floor")}: {card.mainFloor?.trim() || "-"}
</Typography>
<Box sx={{ mt: 1.5 }}>
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">
{isItemRound ? t("Stock taken lots") : t("Stock taken")}
</Typography>
<Typography variant="caption" color="text.secondary">
{taken}/{total}
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={progressPercent(taken, total)}
sx={{ height: 8, borderRadius: 1, mb: 1 }}
/>
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">
{isItemRound ? t("Approved lots") : t("Approved progress")}
</Typography>
<Typography variant="caption" color="text.secondary">
{approved}/{total}
</Typography>
</Stack>
<LinearProgress
variant="determinate"
color="success"
value={progressPercent(approved, total)}
sx={{ height: 8, borderRadius: 1 }}
/>
</Box>
</CardContent>
<CardActions sx={{ pt: 0.5, justifyContent: "space-between", flexWrap: "wrap", gap: 1 }}>
<Stack direction="row" spacing={1}>
<Button size="small" variant="contained" onClick={() => onEnterRound(card, rounds)}>
{t("View Details")}
</Button>
{isAdmin && card.open ? (
<Button
size="small"
variant="outlined"
color="warning"
disabled={closingId === card.stockTakeRoundId}
onClick={() => handleCloseRound(card)}
>
{closingId === card.stockTakeRoundId ? <CircularProgress size={16} /> : t("Close round")}
</Button>
) : null}
</Stack>
<Chip size="small" label={t(card.status || " ")} color={statusColor(card.status) as "success" | "info" | "primary" | "warning"} />
</CardActions>
</Card>
</Grid>
);
})}
</Grid>
)}
</Box>
);
};

export default StockTakeRoundCardList;

+ 293
- 127
src/components/StockTakeManagement/StockTakeTab.tsx Ver fichero

@@ -1,18 +1,40 @@
"use client";

import { Box, Tab, Tabs, Snackbar, Alert, CircularProgress, Typography, TextField, Button, Stack } from "@mui/material";
import {
Alert,
Box,
Button,
CircularProgress,
FormControl,
InputLabel,
MenuItem,
Select,
Snackbar,
Stack,
Tab,
Tabs,
TextField,
Typography,
} from "@mui/material";
import { useState, useCallback, useEffect, useRef } from "react";
import dayjs from "dayjs";
import { useSession } from "next-auth/react";
import { useTranslation } from "react-i18next";
import { AUTH } from "@/authorities";
import { SessionWithTokens } from "@/config/authConfig";
import { AllPickedStockTakeListReponse, getLatestApproverStockTakeHeader } from "@/app/api/stockTake/actions";
import {
AllPickedStockTakeListReponse,
closeStockTakeRound,
getLatestApproverStockTakeHeader,
type StockTakeRoundCard,
} from "@/app/api/stockTake/actions";
import { fetchWarehouseListClient } from "@/app/api/warehouse/client";
import PickerCardList from "./PickerCardList";
import { EMPTY_PICKER_LOCATION, type PickerCardListFilters, type PickerLocationPick } from "./PickerCardList";
import PickerStockTake from "./PickerStockTake";
import PickerReStockTake from "./PickerReStockTake";
import ApproverStockTakeAll from "./ApproverStockTakeAll";
import StockTakeRoundCardList, { roundCardToSession } from "./StockTakeRoundCardList";
import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent";
import {
parseQtyGapWarnPercent,
@@ -46,17 +68,23 @@ const StockTakeTab: React.FC = () => {
const [viewScope, setViewScope] = useState<ViewScope>("picker");
const [approverSession, setApproverSession] = useState<AllPickedStockTakeListReponse | null>(null);
const [approverLoading, setApproverLoading] = useState(false);
const approverHeaderLoadedRef = useRef(false);
/** 從卡片列表進入明細後返回時保留分頁 */
const [pickerListPage, setPickerListPage] = useState(0);
const [pickerListPageSize] = useState(6);
const [pickerSearchFilters, setPickerSearchFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS);
const [pickerAppliedFilters, setPickerAppliedFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS);
const [pickerLocationPick, setPickerLocationPick] = useState<PickerLocationPick>(EMPTY_PICKER_LOCATION);
const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
severity: "success" | "error" | "warning"
const [selectedRound, setSelectedRound] = useState<StockTakeRoundCard | null>(null);
const [switchableRounds, setSwitchableRounds] = useState<StockTakeRoundCard[]>([]);
const [roundListNonce, setRoundListNonce] = useState(0);
const [roundListYearMonth, setRoundListYearMonth] = useState(() => dayjs().format("YYYY-MM"));
const [batchButtonHost, setBatchButtonHost] = useState<HTMLDivElement | null>(null);
const [showCreateOnRoundPage, setShowCreateOnRoundPage] = useState(false);
const [closingRound, setClosingRound] = useState(false);
const closeRoundLock = useRef(false);
const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
severity: "success" | "error" | "warning";
}>({
open: false,
message: "",
@@ -116,23 +144,72 @@ const StockTakeTab: React.FC = () => {
fetchWarehouseListClient().catch((e) => console.error("Failed to prefetch warehouses:", e));
}, []);

useEffect(() => {
if (tabValue !== 1 && tabValue !== 2) return;
if (approverHeaderLoadedRef.current) return;
approverHeaderLoadedRef.current = true;
setApproverLoading(true);
getLatestApproverStockTakeHeader()
.then((header) => {
setApproverSession(header ?? null);
})
.catch((e) => {
const enterRound = useCallback(
async (card: StockTakeRoundCard, roundsForSwitcher?: StockTakeRoundCard[]) => {
setSelectedRound(card);
if (card.yearMonth) setRoundListYearMonth(card.yearMonth);
if (roundsForSwitcher) setSwitchableRounds(roundsForSwitcher);
setPickerListPage(0);
setPickerSearchFilters(DEFAULT_PICKER_CARD_LIST_FILTERS);
setPickerAppliedFilters(DEFAULT_PICKER_CARD_LIST_FILTERS);
setPickerLocationPick(EMPTY_PICKER_LOCATION);
setApproverLoading(true);
try {
const header = await getLatestApproverStockTakeHeader(card.stockTakeRoundId);
setApproverSession(header ?? roundCardToSession(card));
} catch (e) {
console.error(e);
setApproverSession(null);
})
.finally(() => setApproverLoading(false));
}, [tabValue]);
setApproverSession(roundCardToSession(card));
} finally {
setApproverLoading(false);
}
},
[],
);

const handleBackToRounds = useCallback(() => {
setSelectedRound(null);
setSelectedSession(null);
setApproverSession(null);
setShowCreateOnRoundPage(false);
setRoundListNonce((n) => n + 1);
}, []);

const handleSwitchRound = useCallback(
(roundId: number) => {
const card = switchableRounds.find((r) => r.stockTakeRoundId === roundId);
if (!card) return;
setSelectedSession(null);
void enterRound(card);
},
[enterRound, switchableRounds],
);

const handleCloseSelectedRound = useCallback(async () => {
if (!isAdmin || !selectedRound?.open || closeRoundLock.current) return;
closeRoundLock.current = true;
setClosingRound(true);
try {
await closeStockTakeRound(selectedRound.stockTakeRoundId);
handleSnackbar(t("Round closed"), "success");
handleBackToRounds();
} catch (e) {
handleSnackbar(e instanceof Error ? e.message : t("Failed to close round"), "error");
} finally {
closeRoundLock.current = false;
setClosingRound(false);
}
}, [handleBackToRounds, handleSnackbar, isAdmin, selectedRound, t]);

const showingPickerDetail = Boolean(selectedSession) && viewScope === "picker" && tabValue === 0;
const roundId = selectedRound?.stockTakeRoundId ?? 0;
const roundLabel = (card: StockTakeRoundCard) => {
const name = card.stockTakeRoundName?.trim();
const title = name || `${t("Stock Take Round")} #${card.stockTakeRoundId}`;
const mode = card.stockTakeRoundMode === "item" ? t("Create by item") : t("Create by section");
const floor = card.mainFloor?.trim();
return floor ? `${title} · ${mode} · ${floor}` : `${title} · ${mode}`;
};

return (
<Box>
@@ -152,115 +229,204 @@ const StockTakeTab: React.FC = () => {
)
) : null}
<Box sx={{ display: showingPickerDetail ? "none" : "block" }}>
{isAdmin && (
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
<Typography
component="label"
htmlFor="stock-take-qty-gap-warn"
sx={{ m: 0, height: 40, fontSize: 18, fontWeight: 500, lineHeight: "40px" }}
>
{t("qtyGapWarnPercent")}
</Typography>
<TextField
id="stock-take-qty-gap-warn"
size="small"
type="number"
value={qtyGapDraft}
onChange={(e) => setQtyGapDraft(e.target.value.replace(/[^\d]/g, ""))}
inputProps={{ min: 0, max: 1000, inputMode: "numeric" }}
sx={{
width: 88,
m: 0,
"& .MuiFilledInput-root": { height: 40 },
"& .MuiFilledInput-input.MuiInputBase-inputSizeSmall": {
height: 40,
boxSizing: "border-box",
paddingTop: 0,
paddingBottom: 0,
lineHeight: "40px",
},
}}
/>
<Button
size="small"
variant="outlined"
disabled={qtyGapSaving}
onClick={saveQtyGapWarnPercent}
sx={{ height: 40 }}
>
{t("Save")}
</Button>
</Stack>
)}
<Tabs
value={tabValue}
onChange={(e, newValue) => {
setTabValue(newValue);
if (newValue === 0) {
setViewScope("picker");
} else {
setViewScope("approver-all");
}
}}
sx={{ mb: 2 }}
>
<Tab label={t("Picker")} />
<Tab label={t("Approver Pending")} />
<Tab label={t("Approver Approved")} />
</Tabs>
{isAdmin && (
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
<Typography
component="label"
htmlFor="stock-take-qty-gap-warn"
sx={{ m: 0, height: 40, fontSize: 18, fontWeight: 500, lineHeight: "40px" }}
>
{t("qtyGapWarnPercent")}
</Typography>
<TextField
id="stock-take-qty-gap-warn"
size="small"
type="number"
value={qtyGapDraft}
onChange={(e) => setQtyGapDraft(e.target.value.replace(/[^\d]/g, ""))}
inputProps={{ min: 0, max: 1000, inputMode: "numeric" }}
sx={{
width: 88,
m: 0,
"& .MuiFilledInput-root": { height: 40 },
"& .MuiFilledInput-input.MuiInputBase-inputSizeSmall": {
height: 40,
boxSizing: "border-box",
paddingTop: 0,
paddingBottom: 0,
lineHeight: "40px",
},
}}
/>
<Button
size="small"
variant="outlined"
disabled={qtyGapSaving}
onClick={saveQtyGapWarnPercent}
sx={{ height: 40 }}
>
{t("Save")}
</Button>
</Stack>
)}

{tabValue === 0 && (
<PickerCardList
page={pickerListPage}
pageSize={pickerListPageSize}
onListPageChange={setPickerListPage}
searchFilters={pickerSearchFilters}
appliedFilters={pickerAppliedFilters}
onSearchFiltersChange={setPickerSearchFilters}
onAppliedFiltersChange={setPickerAppliedFilters}
locationPick={pickerLocationPick}
onLocationPickChange={setPickerLocationPick}
onCardClick={(session) => {
setViewScope("picker");
handleCardClick(session);
}}
onReStockTakeClick={handleReStockTakeClick}
/>
)}
{approverHeaderLoadedRef.current || tabValue === 1 || tabValue === 2 ? (
<>
<Box sx={{ display: tabValue === 1 ? "block" : "none" }}>
{approverLoading && !approverSession ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : approverSession ? (
<ApproverStockTakeAll
selectedSession={approverSession}
mode="pending"
onSnackbar={handleSnackbar}
{!selectedRound ? (
<>
<StockTakeRoundCardList
isAdmin={isAdmin}
refreshNonce={roundListNonce}
yearMonth={roundListYearMonth}
onYearMonthChange={setRoundListYearMonth}
creating={showCreateOnRoundPage}
onEnterRound={(card, listRounds) => {
setSwitchableRounds(listRounds);
void enterRound(card, listRounds);
}}
onCreateClick={() => setShowCreateOnRoundPage(true)}
/>
{showCreateOnRoundPage ? (
<PickerCardList
page={0}
pageSize={pickerListPageSize}
onListPageChange={() => undefined}
searchFilters={DEFAULT_PICKER_CARD_LIST_FILTERS}
appliedFilters={DEFAULT_PICKER_CARD_LIST_FILTERS}
onSearchFiltersChange={() => undefined}
onAppliedFiltersChange={() => undefined}
locationPick={EMPTY_PICKER_LOCATION}
onLocationPickChange={() => undefined}
onCardClick={() => undefined}
onReStockTakeClick={() => undefined}
hideSectionList
autoOpenCreate
onCreateDialogClose={() => setShowCreateOnRoundPage(false)}
onRoundsChanged={() => {
setShowCreateOnRoundPage(false);
setRoundListNonce((n) => n + 1);
}}
/>
) : approverHeaderLoadedRef.current ? (
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
) : null}
</Box>
<Box sx={{ display: tabValue === 2 ? "block" : "none" }}>
{approverSession ? (
<ApproverStockTakeAll
selectedSession={approverSession}
mode="approved"
onSnackbar={handleSnackbar}
</>
) : (
<>
<Typography variant="h6" fontWeight={700} sx={{ mb: 1 }}>
{switchableRounds.length > 1 ? (
<FormControl size="small" sx={{ minWidth: 220 }}>
<InputLabel>{t("Stock Take Round")}</InputLabel>
<Select
label={t("Stock Take Round")}
value={roundId}
onChange={(e) => handleSwitchRound(Number(e.target.value))}
>
{switchableRounds.map((card) => (
<MenuItem key={card.stockTakeRoundId} value={card.stockTakeRoundId}>
{roundLabel(card)}
</MenuItem>
))}
</Select>
</FormControl>
) : (
roundLabel(selectedRound)
)}
</Typography>
<Box sx={{ mb: 1 }}>
<Button variant="outlined" onClick={handleBackToRounds}>
{t("Back to rounds")}
</Button>
</Box>

<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
<Tabs
value={tabValue}
onChange={(e, newValue) => {
setTabValue(newValue);
if (newValue === 0) {
setViewScope("picker");
} else {
setViewScope("approver-all");
}
}}
sx={{ minHeight: 48 }}
>
<Tab label={t("Picker")} />
<Tab label={t("Approver Pending")} />
<Tab label={t("Approver Approved")} />
</Tabs>
<Stack direction="row" spacing={1} alignItems="center">
{isAdmin && selectedRound.open ? (
<Button
variant="outlined"
color="warning"
disabled={closingRound}
onClick={handleCloseSelectedRound}
>
{closingRound ? <CircularProgress size={18} /> : t("Close round")}
</Button>
) : null}
<Box ref={setBatchButtonHost} />
</Stack>
</Stack>

{tabValue === 0 && (
<PickerCardList
page={pickerListPage}
pageSize={pickerListPageSize}
onListPageChange={setPickerListPage}
searchFilters={pickerSearchFilters}
appliedFilters={pickerAppliedFilters}
onSearchFiltersChange={setPickerSearchFilters}
onAppliedFiltersChange={setPickerAppliedFilters}
locationPick={pickerLocationPick}
onLocationPickChange={setPickerLocationPick}
stockTakeRoundId={roundId}
onCardClick={(session) => {
setViewScope("picker");
handleCardClick(session);
}}
onReStockTakeClick={handleReStockTakeClick}
onRoundsChanged={() => {
setRoundListNonce((n) => n + 1);
handleBackToRounds();
}}
/>
) : approverHeaderLoadedRef.current && !approverLoading ? (
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
)}
{tabValue === 1 || tabValue === 2 ? (
<>
<Box sx={{ display: tabValue === 1 ? "block" : "none" }}>
{approverLoading && !approverSession ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : approverSession ? (
<ApproverStockTakeAll
selectedSession={approverSession}
mode="pending"
onSnackbar={handleSnackbar}
batchButtonHost={tabValue === 1 ? batchButtonHost : null}
/>
) : (
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
)}
</Box>
<Box sx={{ display: tabValue === 2 ? "block" : "none" }}>
{approverSession ? (
<ApproverStockTakeAll
selectedSession={approverSession}
mode="approved"
onSnackbar={handleSnackbar}
/>
) : !approverLoading ? (
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
) : null}
</Box>
</>
) : null}
</Box>
</>
) : null}
</>
)}
</Box>

<Snackbar
@@ -276,4 +442,4 @@ const StockTakeTab: React.FC = () => {
);
};

export default StockTakeTab;
export default StockTakeTab;

+ 24
- 0
src/i18n/en/stockTake.json Ver fichero

@@ -18,6 +18,20 @@
"Area": "Area",
"Available QTY cannot be negative": "Available QTY cannot be negative",
"Back to List": "Back to List",
"Back to rounds": "Back to rounds",
"Close round": "Close round",
"Completed rounds for month": "Completed rounds ({{month}})",
"Failed to close round": "Failed to close round",
"In progress": "In progress",
"Next month": "Next month",
"Open rounds this month": "Open rounds (this month)",
"Previous month": "Previous month",
"Round closed": "Round closed",
"Main floor": "Main floor",
"Stock taken": "Stock taken",
"Approved progress": "Approved",
"Stock taken lots": "Stock taken lots",
"Approved lots": "Approved lots",
"Bad Qty": "Bad Qty",
"Batch Save All": "Batch Save All",
"Batch approver save completed: {{success}} success, {{errors}} errors": "Batch approver save completed: {{success}} success, {{errors}} errors",
@@ -33,6 +47,14 @@
"Confirm batch save approver": "Confirm batch save",
"Confirm create stock take": "Confirm create stock take",
"Control Time": "Control Time",
"Create by item": "By item",
"Create by section": "By section",
"Create by item hint": "Search and select items. This round includes available lots of these items in every floor / warehouse.",
"Create by item search hint": "Enter an item code, an item name, or both. Both must match when filled.",
"Create by item selected count": "{{count}} item(s) selected",
"Search item code or name": "Search item code or name",
"Total selected items label": "Selected items",
"items unit": "item(s)",
"Create Stock Take (Select Sections)": "Create stock take (select sections)",
"Created": "Created",
"Creation date": "Creation date",
@@ -59,6 +81,7 @@
"Issue No": "Issue No",
"Issue Remark": "Issue Remark",
"Item": "Item",
"Item Name": "Item Name",
"Item Code": "Item Code",
"Item-lotNo-ExpiryDate": "Item-lotNo-ExpiryDate",
"Last Stock Take Date": "Last Stock Take Date",
@@ -151,6 +174,7 @@
"View Details": "View Details",
"View ReStockTake": "View ReStockTake",
"Warehouse": "Warehouse",
"Warehouse code": "Warehouse code",
"Warehouse Location": "Warehouse Location",
"Warehouse missing stock take section drawer hint": "These locations have no stock take section (ST-xxx) and cannot be included in stock take. Fix in warehouse settings.",
"Warehouse missing stock take section empty": "No warehouses missing stock take section",


+ 24
- 0
src/i18n/zh/stockTake.json Ver fichero

@@ -1,8 +1,23 @@
{
"View Details": "查看詳細",
"Back to List": "返回列表",
"Back to rounds": "返回輪次列表",
"Close round": "關閉此輪",
"Completed rounds for month": "已完成輪次({{month}})",
"Failed to close round": "關閉輪次失敗",
"In progress": "進行中",
"Next month": "下一個月",
"Open rounds this month": "進行中的輪次(本月)",
"Previous month": "上一個月",
"Round closed": "已關閉此輪",
"Main floor": "主要樓層",
"Stock taken": "已盤點",
"Approved progress": "已審核",
"Stock taken lots": "已盤點批次",
"Approved lots": "已審核批次",
"Stock Take Section": "盤點區域類型",
"Warehouse": "倉庫",
"Warehouse code": "倉庫代碼",
"All": "全部",
"Rows per page": "每頁行數",
"Remark": "備註",
@@ -17,6 +32,7 @@
"Stock Take Section code": "盤點區域",
"Stock Take Section code placeholder": "可使用逗號搜索多個區域",
"Item": "貨品",
"Item Name": "貨品名稱",
"Store ID": "樓層",
"Pending": "待處理",
"Pass": "已盤點",
@@ -112,6 +128,14 @@
"Select area first": "請先選擇倉位",
"Slot": "位置",
"Start Stock Take Date": "盤點日期",
"Create by item": "依貨品",
"Create by section": "依區域",
"Create by item hint": "搜尋並勾選貨品。此輪會納入這些貨在所有樓層/倉的可用庫存。",
"Create by item search hint": "可分別輸入貨品編號、貨品名稱;兩個都填時要同時符合",
"Create by item selected count": "已選 {{count}} 項貨品",
"Search item code or name": "搜尋貨品編號或名稱",
"Total selected items label": "已選貨品",
"items unit": "項",
"Create Stock Take (Select Sections)": "建立盤點(選擇區域)",
"Total Item Kind Number": "貨品種類數量",
"View ReStockTake": "查看重新盤點",


Cargando…
Cancelar
Guardar