CANCERYS\kw093 5 дней назад
Родитель
Сommit
bd92bf2492
7 измененных файлов: 257 добавлений и 53 удалений
  1. +4
    -0
      src/app/api/stockTake/actions.ts
  2. +193
    -10
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  3. +44
    -34
      src/components/StockTakeManagement/PickerCardList.tsx
  4. +4
    -4
      src/components/StockTakeManagement/PickerReStockTake.tsx
  5. +4
    -4
      src/components/StockTakeManagement/PickerStockTake.tsx
  6. +7
    -1
      src/components/StockTakeManagement/StockTakeTab.tsx
  7. +1
    -0
      src/i18n/zh/inventory.json

+ 4
- 0
src/app/api/stockTake/actions.ts Просмотреть файл

@@ -44,6 +44,10 @@ export interface InventoryLotDetailResponse {
stockTakeSection?: string | null;
stockTakeSectionDescription?: string | null;
stockTakerName?: string | null;
/** ISO string or backend LocalDateTime array */
stockTakeEndTime?: string | string[] | null;
/** ISO string or backend LocalDateTime array */
approverTime?: string | string[] | null;
}

export const getInventoryLotDetailsBySection = async (


+ 193
- 10
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Просмотреть файл

@@ -17,6 +17,7 @@ import {
TextField,
Radio,
TablePagination,
TableSortLabel,
} from "@mui/material";
import { useState, useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -45,6 +46,25 @@ interface ApproverStockTakeAllProps {

type QtySelectionType = "first" | "second" | "approver";

type ApprovedSortKey =
| "stockTakeEndTime"
| "stockTakeSection"
| "item"
| "stockTakerName"
| "variance";

function parseDateTimeMs(
v: string | string[] | null | undefined
): number {
if (v == null) return 0;
if (Array.isArray(v)) {
const arr = v as unknown as number[];
const [y, m, d, h = 0, min = 0, s = 0] = arr;
return dayjs(`${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")} ${h}:${min}:${s}`).valueOf();
}
return dayjs(v as string).valueOf();
}

const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
selectedSession,
mode,
@@ -66,6 +86,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>(50);
const [total, setTotal] = useState(0);
const [approvedSortKey, setApprovedSortKey] = useState<ApprovedSortKey | null>(null);
const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc");

const currentUserId = session?.id ? parseInt(session.id) : undefined;

@@ -200,12 +222,62 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
]);

const sortedDetails = useMemo(() => {
return [...filteredDetails].sort((a, b) => {
const sectionA = (a.stockTakeSection || "").trim();
const sectionB = (b.stockTakeSection || "").trim();
return sectionA.localeCompare(sectionB, undefined, { numeric: true, sensitivity: "base" });
const list = [...filteredDetails];
if (mode !== "approved") {
return list.sort((a, b) =>
(a.stockTakeSection || "").localeCompare(b.stockTakeSection || "", undefined, {
numeric: true,
sensitivity: "base",
})
);
}
const key = approvedSortKey ?? "stockTakeSection";
const mul = approvedSortDir === "asc" ? 1 : -1;
return list.sort((a, b) => {
let cmp = 0;
switch (key) {
case "stockTakeEndTime":
cmp =
parseDateTimeMs(a.approverTime ?? a.stockTakeEndTime) -
parseDateTimeMs(b.approverTime ?? b.stockTakeEndTime);
break;
case "stockTakeSection":
cmp = (a.stockTakeSection || "").localeCompare(b.stockTakeSection || "", undefined, {
numeric: true,
sensitivity: "base",
});
break;
case "item":
cmp = `${a.itemCode || ""} ${a.itemName || ""}`.localeCompare(
`${b.itemCode || ""} ${b.itemName || ""}`,
undefined,
{ numeric: true, sensitivity: "base" }
);
break;
case "stockTakerName":
cmp = (a.stockTakerName || "").localeCompare(b.stockTakerName || "", undefined, {
numeric: true,
sensitivity: "base",
});
break;
case "variance":
cmp = Number(a.varianceQty ?? 0) - Number(b.varianceQty ?? 0);
break;
default:
cmp = 0;
}
return cmp * mul;
});
}, [filteredDetails]);
}, [filteredDetails, mode, approvedSortKey, approvedSortDir]);

const handleApprovedSort = useCallback((property: ApprovedSortKey) => {
if (approvedSortKey === property) {
setApprovedSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setApprovedSortKey(property);
setApprovedSortDir("asc");
}
}, [approvedSortKey]);

const handleSaveApproverStockTake = useCallback(
async (detail: InventoryLotDetailResponse) => {
@@ -410,6 +482,12 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
[inventoryLotDetails]
);

const formatRecordEndTime = (detail: InventoryLotDetailResponse) => {
const ms = parseDateTimeMs(detail.approverTime ?? detail.stockTakeEndTime);
if (!ms) return "-";
return dayjs(ms).format("YYYY-MM-DD HH:mm");
};

return (
<Box>
{onBack && (
@@ -482,23 +560,110 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
{mode === "approved" && (
<TableCell
sortDirection={
approvedSortKey === "stockTakeEndTime" ? approvedSortDir : false
}
>
<TableSortLabel
active={approvedSortKey === "stockTakeEndTime"}
direction={
approvedSortKey === "stockTakeEndTime" ? approvedSortDir : "asc"
}
onClick={() => handleApprovedSort("stockTakeEndTime")}
>
{t("Approver Time")}
</TableSortLabel>
</TableCell>
)}
<TableCell
sortDirection={
mode === "approved" && (approvedSortKey === "stockTakeSection" || approvedSortKey === null)
? approvedSortDir
: false
}
>
{mode === "approved" ? (
<TableSortLabel
active={
approvedSortKey === "stockTakeSection" || approvedSortKey === null
}
direction={approvedSortDir}
onClick={() => handleApprovedSort("stockTakeSection")}
>
{t("Warehouse Location")}
</TableSortLabel>
) : (
t("Warehouse Location")
)}
</TableCell>
<TableCell
sortDirection={
mode === "approved" && approvedSortKey === "item" ? approvedSortDir : false
}
>
{mode === "approved" ? (
<TableSortLabel
active={approvedSortKey === "item"}
direction={approvedSortKey === "item" ? approvedSortDir : "asc"}
onClick={() => handleApprovedSort("item")}
>
{t("Item-lotNo-ExpiryDate")}
</TableSortLabel>
) : (
t("Item-lotNo-ExpiryDate")
)}
</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>
{t("Stock Take Qty(include Bad Qty)= Available Qty")}
</TableCell>
{mode === "approved" && (
<TableCell
sortDirection={
approvedSortKey === "variance" ? approvedSortDir : false
}
>
<TableSortLabel
active={approvedSortKey === "variance"}
direction={approvedSortKey === "variance" ? approvedSortDir : "asc"}
onClick={() => handleApprovedSort("variance")}
>
{t("Variance")}
</TableSortLabel>
</TableCell>
)}
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Picker")}</TableCell>
<TableCell
sortDirection={
mode === "approved" && approvedSortKey === "stockTakerName"
? approvedSortDir
: false
}
>
{mode === "approved" ? (
<TableSortLabel
active={approvedSortKey === "stockTakerName"}
direction={
approvedSortKey === "stockTakerName" ? approvedSortDir : "asc"
}
onClick={() => handleApprovedSort("stockTakerName")}
>
{t("Picker")}
</TableSortLabel>
) : (
t("Picker")
)}
</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={8} align="center">
<TableCell colSpan={mode === "approved" ? 10 : 8} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
@@ -515,6 +680,16 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({

return (
<TableRow key={detail.id}>
{mode === "approved" && (
<TableCell>
<Stack spacing={0.5}>
<Typography variant="caption" color="text.secondary">
{formatRecordEndTime(detail)}
</Typography>
</Stack>
</TableCell>
)}
<TableCell>
<Stack spacing={0.5}>
<Typography variant="body2"><strong>{detail.stockTakeSection || "-"} {detail.stockTakeSectionDescription || "-"}</strong></Typography>
@@ -759,6 +934,14 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
)}
</TableCell>

{mode === "approved" && (
<TableCell>
<Typography variant="body2">
{formatNumber(detail.varianceQty)}
</Typography>
</TableCell>
)}

<TableCell>
<Typography variant="body2">
{detail.remarks || "-"}


+ 44
- 34
src/components/StockTakeManagement/PickerCardList.tsx Просмотреть файл

@@ -37,20 +37,30 @@ import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
const PER_PAGE = 6;

interface PickerCardListProps {
/** 由父層保存,從明細返回時仍回到同一頁 */
page: number;
pageSize: number;
onListPageChange: (page: number) => void;
onCardClick: (session: AllPickedStockTakeListReponse) => void;
onReStockTakeClick: (session: AllPickedStockTakeListReponse) => void;
}

const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockTakeClick }) => {
const PickerCardList: React.FC<PickerCardListProps> = ({
page,
pageSize,
onListPageChange,
onCardClick,
onReStockTakeClick,
}) => {
const { t } = useTranslation(["inventory", "common"]);
dayjs.extend(duration);

const PER_PAGE = 6;
const [loading, setLoading] = useState(false);
const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(6); // 每页 6 条
const [total, setTotal] = useState(0);
const [total, setTotal] = useState(0);
/** 建立盤點後若仍在 page 0,仍強制重新載入 */
const [listRefreshNonce, setListRefreshNonce] = useState(0);
const [creating, setCreating] = useState(false);
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All");
@@ -106,41 +116,40 @@ const criteria: Criterion<PickerSearchKey>[] = [
const handleSearch = (inputs: Record<PickerSearchKey | `${PickerSearchKey}To`, string>) => {
setFilterSectionDescription(inputs.sectionDescription || "All");
setFilterStockTakeSession(inputs.stockTakeSession || "");
fetchStockTakeSessions(0, pageSize, {
sectionDescription: inputs.sectionDescription || "All",
stockTakeSections: inputs.stockTakeSession ?? "",
});
onListPageChange(0);
};
const handleResetSearch = () => {
setFilterSectionDescription("All");
setFilterStockTakeSession("");
fetchStockTakeSessions(0, pageSize, {
sectionDescription: "All",
stockTakeSections: "",
});
onListPageChange(0);
};
const fetchStockTakeSessions = useCallback(
async (pageNum: number, size: number, filterOverrides?: { sectionDescription: string; stockTakeSections: string }) => {
setLoading(true);
try {
const res = await getStockTakeRecordsPaged(pageNum, size, filterOverrides);

useEffect(() => {
let cancelled = false;
setLoading(true);
getStockTakeRecordsPaged(page, pageSize, {
sectionDescription: filterSectionDescription,
stockTakeSections: filterStockTakeSession,
})
.then((res) => {
if (cancelled) return;
setStockTakeSessions(Array.isArray(res.records) ? res.records : []);
setTotal(res.total || 0);
setPage(pageNum);
} catch (e) {
})
.catch((e) => {
console.error(e);
setStockTakeSessions([]);
setTotal(0);
} finally {
setLoading(false);
}
},
[]
);
useEffect(() => {
fetchStockTakeSessions(0, pageSize);
}, [fetchStockTakeSessions, pageSize]);
if (!cancelled) {
setStockTakeSessions([]);
setTotal(0);
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [page, pageSize, filterSectionDescription, filterStockTakeSession, listRefreshNonce]);

//const startIdx = page * PER_PAGE;
//const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE);
@@ -161,13 +170,14 @@ const handleResetSearch = () => {
console.log(message);
await fetchStockTakeSessions(0, pageSize);
onListPageChange(0);
setListRefreshNonce((n) => n + 1);
} catch (e) {
console.error(e);
} finally {
setCreating(false);
}
}, [fetchStockTakeSessions, t]);
}, [onListPageChange, t]);
useEffect(() => {
fetchStockTakeSections()
.then((sections) => {
@@ -376,7 +386,7 @@ const handleResetSearch = () => {
page={page}
rowsPerPage={pageSize}
onPageChange={(e, newPage) => {
fetchStockTakeSessions(newPage, pageSize);
onListPageChange(newPage);
}}
rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死
/>


+ 4
- 4
src/components/StockTakeManagement/PickerReStockTake.tsx Просмотреть файл

@@ -599,13 +599,13 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<TextField
size="small"
value={inputs.remark}
onKeyDown={blockNonIntegerKeys}
inputProps={{ inputMode: "text", pattern: "[0-9]*" }}
// onKeyDown={blockNonIntegerKeys}
//inputProps={{ inputMode: "text", pattern: "[0-9]*" }}
onChange={(e) => {
const clean = sanitizeIntegerInput(e.target.value);
// const clean = sanitizeIntegerInput(e.target.value);
setRecordInputs(prev => ({
...prev,
[detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: clean }
[detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value }
}));
}}
sx={{ width: 150 }}


+ 4
- 4
src/components/StockTakeManagement/PickerStockTake.tsx Просмотреть файл

@@ -771,15 +771,15 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<TextField
size="small"
value={recordInputs[detail.id]?.remark || ""}
onKeyDown={blockNonIntegerKeys}
inputProps={{ inputMode: "text", pattern: "[0-9]*" }}
// onKeyDown={blockNonIntegerKeys}
//inputProps={{ inputMode: "text", pattern: "[0-9]*" }}
onChange={(e) => {
const clean = sanitizeIntegerInput(e.target.value);
// const clean = sanitizeIntegerInput(e.target.value);
setRecordInputs(prev => ({
...prev,
[detail.id]: {
...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }),
remark: clean
remark: e.target.value
}
}));
}}


+ 7
- 1
src/components/StockTakeManagement/StockTakeTab.tsx Просмотреть файл

@@ -19,6 +19,9 @@ const StockTakeTab: React.FC = () => {
const [viewScope, setViewScope] = useState<ViewScope>("picker");
const [approverSession, setApproverSession] = useState<AllPickedStockTakeListReponse | null>(null);
const [approverLoading, setApproverLoading] = useState(false);
/** 從卡片列表進入明細後返回時保留分頁 */
const [pickerListPage, setPickerListPage] = useState(0);
const [pickerListPageSize] = useState(6);
const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
@@ -120,7 +123,10 @@ const StockTakeTab: React.FC = () => {
</Tabs>

{tabValue === 0 && (
<PickerCardList
<PickerCardList
page={pickerListPage}
pageSize={pickerListPageSize}
onListPageChange={setPickerListPage}
onCardClick={(session) => {
setViewScope("picker");
handleCardClick(session);


+ 1
- 0
src/i18n/zh/inventory.json Просмотреть файл

@@ -8,6 +8,7 @@
"UoM": "單位",
"Approver Pending": "審核待處理",
"Approver Approved": "審核通過",
"Approver Time": "審核時間",
"mat": "物料",
"variance": "差異",
"Plan Start Date": "計劃開始日期",


Загрузка…
Отмена
Сохранить