Browse Source

update

MergeProblem1
CANCERYS\kw093 5 days ago
parent
commit
bd92bf2492
7 changed files with 257 additions and 53 deletions
  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 View File

@@ -44,6 +44,10 @@ export interface InventoryLotDetailResponse {
stockTakeSection?: string | null; stockTakeSection?: string | null;
stockTakeSectionDescription?: string | null; stockTakeSectionDescription?: string | null;
stockTakerName?: 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 ( export const getInventoryLotDetailsBySection = async (


+ 193
- 10
src/components/StockTakeManagement/ApproverStockTakeAll.tsx View File

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


type QtySelectionType = "first" | "second" | "approver"; 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> = ({ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
selectedSession, selectedSession,
mode, mode,
@@ -66,6 +86,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>(50); const [pageSize, setPageSize] = useState<number | string>(50);
const [total, setTotal] = useState(0); 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; const currentUserId = session?.id ? parseInt(session.id) : undefined;


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


const sortedDetails = useMemo(() => { 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( const handleSaveApproverStockTake = useCallback(
async (detail: InventoryLotDetailResponse) => { async (detail: InventoryLotDetailResponse) => {
@@ -410,6 +482,12 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
[inventoryLotDetails] [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 ( return (
<Box> <Box>
{onBack && ( {onBack && (
@@ -482,23 +560,110 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <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("UOM")}</TableCell>
<TableCell> <TableCell>
{t("Stock Take Qty(include Bad Qty)= Available Qty")} {t("Stock Take Qty(include Bad Qty)= Available Qty")}
</TableCell> </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("Remark")}</TableCell>
<TableCell>{t("Record Status")}</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> <TableCell>{t("Action")}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{sortedDetails.length === 0 ? ( {sortedDetails.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={8} align="center">
<TableCell colSpan={mode === "approved" ? 10 : 8} align="center">
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("No data")} {t("No data")}
</Typography> </Typography>
@@ -515,6 +680,16 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({


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


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

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


+ 44
- 34
src/components/StockTakeManagement/PickerCardList.tsx View File

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


interface PickerCardListProps { interface PickerCardListProps {
/** 由父層保存,從明細返回時仍回到同一頁 */
page: number;
pageSize: number;
onListPageChange: (page: number) => void;
onCardClick: (session: AllPickedStockTakeListReponse) => void; onCardClick: (session: AllPickedStockTakeListReponse) => void;
onReStockTakeClick: (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"]); const { t } = useTranslation(["inventory", "common"]);
dayjs.extend(duration); dayjs.extend(duration);


const PER_PAGE = 6; const PER_PAGE = 6;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]); 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 [creating, setCreating] = useState(false);
const [openConfirmDialog, setOpenConfirmDialog] = useState(false); const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All"); const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All");
@@ -106,41 +116,40 @@ const criteria: Criterion<PickerSearchKey>[] = [
const handleSearch = (inputs: Record<PickerSearchKey | `${PickerSearchKey}To`, string>) => { const handleSearch = (inputs: Record<PickerSearchKey | `${PickerSearchKey}To`, string>) => {
setFilterSectionDescription(inputs.sectionDescription || "All"); setFilterSectionDescription(inputs.sectionDescription || "All");
setFilterStockTakeSession(inputs.stockTakeSession || ""); setFilterStockTakeSession(inputs.stockTakeSession || "");
fetchStockTakeSessions(0, pageSize, {
sectionDescription: inputs.sectionDescription || "All",
stockTakeSections: inputs.stockTakeSession ?? "",
});
onListPageChange(0);
}; };
const handleResetSearch = () => { const handleResetSearch = () => {
setFilterSectionDescription("All"); setFilterSectionDescription("All");
setFilterStockTakeSession(""); 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 : []); setStockTakeSessions(Array.isArray(res.records) ? res.records : []);
setTotal(res.total || 0); setTotal(res.total || 0);
setPage(pageNum);
} catch (e) {
})
.catch((e) => {
console.error(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 startIdx = page * PER_PAGE;
//const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE);
@@ -161,13 +170,14 @@ const handleResetSearch = () => {
console.log(message); console.log(message);
await fetchStockTakeSessions(0, pageSize);
onListPageChange(0);
setListRefreshNonce((n) => n + 1);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
setCreating(false); setCreating(false);
} }
}, [fetchStockTakeSessions, t]);
}, [onListPageChange, t]);
useEffect(() => { useEffect(() => {
fetchStockTakeSections() fetchStockTakeSections()
.then((sections) => { .then((sections) => {
@@ -376,7 +386,7 @@ const handleResetSearch = () => {
page={page} page={page}
rowsPerPage={pageSize} rowsPerPage={pageSize}
onPageChange={(e, newPage) => { onPageChange={(e, newPage) => {
fetchStockTakeSessions(newPage, pageSize);
onListPageChange(newPage);
}} }}
rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死 rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死
/> />


+ 4
- 4
src/components/StockTakeManagement/PickerReStockTake.tsx View File

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


+ 4
- 4
src/components/StockTakeManagement/PickerStockTake.tsx View File

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


+ 7
- 1
src/components/StockTakeManagement/StockTakeTab.tsx View File

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


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


+ 1
- 0
src/i18n/zh/inventory.json View File

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


Loading…
Cancel
Save