Explorar el Código

update stock take and stock take report

production
CANCERYS\kw093 hace 1 semana
padre
commit
16d1d0f193
Se han modificado 12 ficheros con 442 adiciones y 299 borrados
  1. +8
    -0
      src/app/(main)/report/page.tsx
  2. +13
    -1
      src/app/api/stockTake/actions.ts
  3. +1
    -0
      src/app/api/warehouse/index.ts
  4. +0
    -94
      src/components/JoWorkbench/newJobPickExecution.tsx
  5. +1
    -1
      src/components/StockTakeManagement/ApproverStockTake.tsx
  6. +184
    -96
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  7. +178
    -82
      src/components/StockTakeManagement/PickerCardList.tsx
  8. +8
    -2
      src/components/StockTakeManagement/PickerReStockTake.tsx
  9. +12
    -13
      src/components/StockTakeManagement/PickerStockTake.tsx
  10. +26
    -2
      src/config/reportConfig.ts
  11. +3
    -5
      src/i18n/zh/common.json
  12. +8
    -3
      src/i18n/zh/inventory.json

+ 8
- 0
src/app/(main)/report/page.tsx Ver fichero

@@ -141,6 +141,14 @@ export default function ReportPage() {
} }
// Clear dynamic options when report changes // Clear dynamic options when report changes
setDynamicOptions({}); setDynamicOptions({});

// Default "All" (no filter) for stock take variance report conditions.
if (selectedReportId === 'rep-012') {
setCriteria({
store_id: 'All',
status: 'All',
});
}
}, [selectedReportId]); }, [selectedReportId]);


// React 18 Strict Mode (dev) mounts → unmounts → remounts, so effects with [] run twice. // React 18 Strict Mode (dev) mounts → unmounts → remounts, so effects with [] run twice.


+ 13
- 1
src/app/api/stockTake/actions.ts Ver fichero

@@ -29,6 +29,7 @@ export interface InventoryLotDetailResponse {
warehouseSlot: string; warehouseSlot: string;
warehouseArea: string; warehouseArea: string;
warehouse: string; warehouse: string;
storeId?: string | null;
varianceQty: number | null; varianceQty: number | null;
status: string; status: string;
remarks: string | null; remarks: string | null;
@@ -137,6 +138,8 @@ export interface AllPickedStockTakeListReponse {
endTime: string | null; endTime: string | null;
planStartDate: string | null; planStartDate: string | null;
stockTakeSectionDescription: string | null; stockTakeSectionDescription: string | null;
warehouseArea: string | null;
storeId: string | null;
reStockTakeTrueFalse: boolean; reStockTakeTrueFalse: boolean;
} }


@@ -254,7 +257,7 @@ export const getStockTakeRecords = async () => {
export const getStockTakeRecordsPaged = async ( export const getStockTakeRecordsPaged = async (
pageNum: number, pageNum: number,
pageSize: number, pageSize: number,
params?: { sectionDescription?: string; stockTakeSections?: string }
params?: { sectionDescription?: string; stockTakeSections?: string; status?: string; area?: string; storeId?: string }
) => { ) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.set("pageNum", String(pageNum)); searchParams.set("pageNum", String(pageNum));
@@ -265,6 +268,15 @@ export const getStockTakeRecordsPaged = async (
if (params?.stockTakeSections?.trim()) { if (params?.stockTakeSections?.trim()) {
searchParams.set("stockTakeSections", params.stockTakeSections.trim()); searchParams.set("stockTakeSections", params.stockTakeSections.trim());
} }
if (params?.status && params.status !== "All") {
searchParams.set("status", params.status);
}
if (params?.area?.trim()) {
searchParams.set("area", params.area.trim());
}
if (params?.storeId && params.storeId !== "All") {
searchParams.set("storeId", params.storeId);
}
const url = `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList?${searchParams.toString()}`; const url = `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList?${searchParams.toString()}`;
const res = await serverFetchJson<RecordsRes<AllPickedStockTakeListReponse>>(url, { method: "GET" }); const res = await serverFetchJson<RecordsRes<AllPickedStockTakeListReponse>>(url, { method: "GET" });
return res; return res;


+ 1
- 0
src/app/api/warehouse/index.ts Ver fichero

@@ -39,5 +39,6 @@ export interface StockTakeSectionInfo {
id: string; id: string;
stockTakeSection: string; stockTakeSection: string;
stockTakeSectionDescription: string | null; stockTakeSectionDescription: string | null;
storeId?: string | null;
warehouseCount: number; warehouseCount: number;
} }

+ 0
- 94
src/components/JoWorkbench/newJobPickExecution.tsx Ver fichero

@@ -44,9 +44,6 @@ import {
checkAndCompletePickOrderByConsoCode, checkAndCompletePickOrderByConsoCode,
confirmLotSubstitution, confirmLotSubstitution,
updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加 updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加
batchSubmitList, // ✅ 添加
batchSubmitListRequest, // ✅ 添加
batchSubmitListLineRequest,
} from "@/app/api/pickOrder/actions"; } from "@/app/api/pickOrder/actions";
// 修改:使用 Job Order API // 修改:使用 Job Order API
import { import {
@@ -3783,92 +3780,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}); });
} }


// Hold-only:與整批相同規則,只送一筆 batch,把實揈/完成狀態寫回 DB
if (useHoldOnlyApi && pickOrderIdEarly && solId > 0) {
const freshData =
await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderIdEarly);
const flatLots = getAllLotsFromHierarchical(freshData);
const lotRow = flatLots.find(
(l: any) => Number(l.stockOutLineId) === solId,
);
if (!lotRow) {
throw new Error(
"Could not find lot row after refresh for batch submit",
);
}

const requiredQty = Number(
lotRow.requiredQty || lotRow.pickOrderLineRequiredQty || 0,
);
const issuePickedVal = picked;
const currentActualPickQty = Number(
issuePickedVal ?? lotRow.actualPickQty ?? 0,
);
const onlyComplete =
lotRow.stockOutLineStatus === "partially_completed" ||
lotRow.stockOutLineStatus === "PARTIALLY_COMPLETE" ||
issuePickedVal !== undefined;
const expired = isLotAvailabilityExpired(lotRow);
const unavailable = isInventoryLotLineUnavailable(lotRow);

let targetActual: number;
let newStatus: string;

if (unavailable) {
targetActual = currentActualPickQty;
newStatus = "completed";
} else if (expired && issuePickedVal === undefined) {
targetActual = 0;
newStatus = "completed";
} else if (onlyComplete) {
targetActual = currentActualPickQty;
newStatus = "completed";
} else {
const remainingQty = Math.max(
0,
requiredQty - currentActualPickQty,
);
const cumulativeQty = currentActualPickQty + remainingQty;
targetActual = cumulativeQty;
newStatus = "partially_completed";
if (requiredQty > 0 && cumulativeQty >= requiredQty) {
newStatus = "completed";
}
}

const line: batchSubmitListLineRequest = {
stockOutLineId: solId,
pickOrderLineId: Number(lotRow.pickOrderLineId),
inventoryLotLineId: lotRow.lotId ? Number(lotRow.lotId) : null,
requiredQty,
actualPickQty: targetActual,
stockOutLineStatus: newStatus,
pickOrderConsoCode: String(lotRow.pickOrderConsoCode || ""),
noLot: Boolean(lotRow.noLot === true),
};

const batchResult = await batchSubmitList({
userId: currentUserId || 0,
lines: [line],
});

if (!batchResult || batchResult.code !== "SUCCESS") {
throw new Error(
batchResult?.message ||
"Batch submit failed after hold adjustment",
);
}

const conso = String(lotRow.pickOrderConsoCode || "").trim();
if (conso) {
try {
await checkAndCompletePickOrderByConsoCode(conso);
} catch (e) {
console.error("❌ completion check after single batch:", e);
}
}
}

setPickExecutionFormOpen(false); setPickExecutionFormOpen(false);
setSelectedLotForExecutionForm(null); setSelectedLotForExecutionForm(null);


@@ -3880,16 +3791,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}, },
[ [
fetchJobOrderData, fetchJobOrderData,
getAllLotsFromHierarchical,
currentUserId, currentUserId,
selectedLotForExecutionForm, selectedLotForExecutionForm,
updateHandledBy, updateHandledBy,
filterArgs, filterArgs,
session?.user?.name, session?.user?.name,
batchSubmitList,
checkAndCompletePickOrderByConsoCode,
isLotAvailabilityExpired,
isInventoryLotLineUnavailable,
], ],
); );
// Calculate remaining required quantity // Calculate remaining required quantity


+ 1
- 1
src/components/StockTakeManagement/ApproverStockTake.tsx Ver fichero

@@ -689,7 +689,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
variant="outlined" variant="outlined"
color="warning" color="warning"
onClick={() => handleUpdateStatusToNotMatch(detail)} onClick={() => handleUpdateStatusToNotMatch(detail)}
disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"}
disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"||hasSecond}
> >
{t("ReStockTake")} {t("ReStockTake")}
</Button> </Button>


+ 184
- 96
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Ver fichero

@@ -18,6 +18,15 @@ import {
Radio, Radio,
TablePagination, TablePagination,
TableSortLabel, TableSortLabel,
Card,
CardContent,
CardActions,
Grid,
FormControl,
InputLabel,
Select,
MenuItem,
Autocomplete,
} from "@mui/material"; } from "@mui/material";
import { useState, useCallback, useEffect, useMemo } from "react"; import { useState, useCallback, useEffect, useMemo } from "react";
import { Collapse } from "@mui/material"; import { Collapse } from "@mui/material";
@@ -37,7 +46,6 @@ import {
updateStockTakeRecordStatusToNotMatch, updateStockTakeRecordStatusToNotMatch,
type ApproverInventoryLotDetailsQuery, type ApproverInventoryLotDetailsQuery,
} from "@/app/api/stockTake/actions"; } from "@/app/api/stockTake/actions";
import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; import { fetchStockTakeSections } from "@/app/api/warehouse/actions";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig"; import { SessionWithTokens } from "@/config/authConfig";
@@ -66,13 +74,12 @@ type ApprovedSortKey =
| "stockTakerName" | "stockTakerName"
| "variance"; | "variance";


type ApproverSearchKey = "sectionDescription" | "stockTakeSession" | "itemKeyword" | "warehouseKeyword"|"status";

type ApproverSearchFilters = { type ApproverSearchFilters = {
sectionDescription: string; sectionDescription: string;
stockTakeSession: string; stockTakeSession: string;
itemKeyword: string; itemKeyword: string;
warehouseKeyword: string; warehouseKeyword: string;
storeId: string;
status: string; status: string;
}; };


@@ -85,15 +92,6 @@ function buildApproverInventoryQuery(filters: ApproverSearchFilters): ApproverIn
}; };
} }


function hasAnyApproverSearchCriterion(f: ApproverSearchFilters): boolean {
return (
(f.sectionDescription && f.sectionDescription !== "All") ||
f.stockTakeSession.trim() !== "" ||
f.itemKeyword.trim() !== "" ||
f.warehouseKeyword.trim() !== ""
);
}

function isBlankApproverField(value: string | undefined): boolean { function isBlankApproverField(value: string | undefined): boolean {
return value == null || String(value).trim() === ""; return value == null || String(value).trim() === "";
} }
@@ -202,9 +200,15 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [approvedSortKey, setApprovedSortKey] = useState<ApprovedSortKey | null>(null); const [approvedSortKey, setApprovedSortKey] = useState<ApprovedSortKey | null>(null);
const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc");
const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState<
{ value: string; label: string }[]
>([]);
const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]);
const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]);
const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]);
const [searchSectionDescription, setSearchSectionDescription] = useState<string>("All");
const [searchStockTakeSession, setSearchStockTakeSession] = useState<string>("");
const [searchItemKeyword, setSearchItemKeyword] = useState<string>("");
const [searchWarehouseKeyword, setSearchWarehouseKeyword] = useState<string>("");
const [searchStoreId, setSearchStoreId] = useState<string>("All");
const [searchStatus, setSearchStatus] = useState<string>(mode === "pending" ? "pass" : "All");
const [showFilters, setShowFilters] = useState(true) const [showFilters, setShowFilters] = useState(true)
const [appliedFilters, setAppliedFilters] = useState<ApproverSearchFilters | null>(null); const [appliedFilters, setAppliedFilters] = useState<ApproverSearchFilters | null>(null);


@@ -227,76 +231,32 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
[] []
); );


const handleApproverSearchBoxSearch = useCallback(
(inputs: Record<ApproverSearchKey | `${ApproverSearchKey}To`, string>) => {
const next: ApproverSearchFilters = {
sectionDescription: inputs.sectionDescription || "All",
stockTakeSession: inputs.stockTakeSession || "",
itemKeyword: inputs.itemKeyword || "",
warehouseKeyword: inputs.warehouseKeyword || "",
status: inputs.status || "All",
};
/*
if (!hasAnyApproverSearchCriterion(next)) {
onSnackbar(t("Please set at least one search criterion"), "warning");
return;
}
*/
setAppliedFilters(next);
setPage(0);
},
[onSnackbar, t]
);

const handleApproverSearchBoxReset = useCallback(() => {
const handleSearch = useCallback(() => {
const next: ApproverSearchFilters = {
sectionDescription: searchSectionDescription || "All",
stockTakeSession: searchStockTakeSession || "",
itemKeyword: searchItemKeyword || "",
warehouseKeyword: searchWarehouseKeyword || "",
storeId: searchStoreId || "All",
status: mode === "pending" ? (searchStatus || "pass") : "All",
};
setAppliedFilters(next);
setPage(0);
}, [searchSectionDescription, searchStockTakeSession, searchItemKeyword, searchWarehouseKeyword, searchStoreId, searchStatus, mode]);

const handleResetSearch = useCallback(() => {
const defaultStatus = mode === "pending" ? "pass" : "All";
setSearchSectionDescription("All");
setSearchStockTakeSession("");
setSearchItemKeyword("");
setSearchWarehouseKeyword("");
setSearchStoreId("All");
setSearchStatus(defaultStatus);
setAppliedFilters(null); setAppliedFilters(null);
setPage(0); setPage(0);
setInventoryLotDetails([]); setInventoryLotDetails([]);
setTotal(0); setTotal(0);
}, []);

const approverSearchCriteria: Criterion<ApproverSearchKey>[] = useMemo(
() => [
{
type: "autocomplete",
label: t("Stock Take Section Description"),
paramName: "sectionDescription",
options: sectionDescriptionAutocompleteOptions,
needAll: true,
},
{
type: "text",
label: t("Stock Take Section (can use , to search multiple sections)"),
paramName: "stockTakeSession",
placeholder: "",
},
{
type: "text",
label: t("Item"),
paramName: "itemKeyword",
placeholder: "",
},

{
type: "text",
label: t("Warehouse"),
paramName: "warehouseKeyword",
placeholder: "",
},
{
type: "select-labelled",
label: t("Record Status"),
paramName: "status",
options: [
{ label: t("All"), value: "All" },
{ label: t("Pending"), value: "pending" },
{ label: t("Not Match"), value: "notMatch" },
{ label: t("Pass"), value: "pass" }, // UI=Pass,值=completed
],
}
],
[t, sectionDescriptionAutocompleteOptions]
);
}, [mode]);


const loadDetails = useCallback(async (filters: ApproverSearchFilters) => { const loadDetails = useCallback(async (filters: ApproverSearchFilters) => {
setLoadingDetails(true); setLoadingDetails(true);
@@ -342,13 +302,19 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
fetchStockTakeSections() fetchStockTakeSections()
.then((sections) => { .then((sections) => {
const descSet = new Set<string>(); const descSet = new Set<string>();
const sectionSet = new Set<string>();
const storeSet = new Set<string>(["2F", "4F"]);
sections.forEach((s) => { sections.forEach((s) => {
const section = s.stockTakeSection?.trim();
if (section) sectionSet.add(section);
const desc = s.stockTakeSectionDescription?.trim(); const desc = s.stockTakeSectionDescription?.trim();
if (desc) descSet.add(desc); if (desc) descSet.add(desc);
const storeId = s.storeId?.trim();
if (storeId) storeSet.add(storeId);
}); });
setSectionDescriptionAutocompleteOptions(
Array.from(descSet).map((desc) => ({ value: desc, label: desc }))
);
setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b)));
setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b)));
setStoreIdOptions(Array.from(storeSet).sort((a, b) => a.localeCompare(b)));
}) })
.catch((e) => { .catch((e) => {
console.error("Failed to load section descriptions for approver search:", e); console.error("Failed to load section descriptions for approver search:", e);
@@ -361,6 +327,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
setApproverQty({}); setApproverQty({});
setApproverBadQty({}); setApproverBadQty({});
setAppliedFilters(null); setAppliedFilters(null);
setSearchStatus(mode === "pending" ? "pass" : "All");
setPage(0); setPage(0);
setInventoryLotDetails([]); setInventoryLotDetails([]);
setTotal(0); setTotal(0);
@@ -491,8 +458,14 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
const percent = parseFloat(variancePercentTolerance || "0"); const percent = parseFloat(variancePercentTolerance || "0");
const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent; const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent;
const statusFilter = appliedFilters?.status ?? "All";
const statusFilter = mode === "pending" ? (appliedFilters?.status ?? "pass") : "All";
const storeIdFilter = appliedFilters?.storeId ?? "All";
return inventoryLotDetails.filter((detail) => { return inventoryLotDetails.filter((detail) => {
if (storeIdFilter !== "All") {
if ((detail.storeId || "").trim().toLowerCase() !== storeIdFilter.trim().toLowerCase()) {
return false;
}
}
if (statusFilter !== "All") { if (statusFilter !== "All") {
const rowStatus = normalizeStatus(detail.stockTakeRecordStatus); const rowStatus = normalizeStatus(detail.stockTakeRecordStatus);
const wanted = normalizeStatus(statusFilter); const wanted = normalizeStatus(statusFilter);
@@ -526,6 +499,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
variancePercentTolerance, variancePercentTolerance,
qtySelection, qtySelection,
calculateDifference, calculateDifference,
appliedFilters,
mode,
]); ]);


const sortedDetails = useMemo(() => { const sortedDetails = useMemo(() => {
@@ -890,7 +865,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
{ {
field: "qtyBlock", field: "qtyBlock",
headerName: t("Stock Take Qty(include Bad Qty)= Available Qty"), headerName: t("Stock Take Qty(include Bad Qty)= Available Qty"),
minWidth: 420,
minWidth: 320,
flex: 3, flex: 3,
sortable: false, sortable: false,
renderCell: (params: GridRenderCellParams<InventoryLotDetailResponse>) => { renderCell: (params: GridRenderCellParams<InventoryLotDetailResponse>) => {
@@ -949,7 +924,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
{formatNumber( {formatNumber(
(detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0) (detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0)
)}{" "} )}{" "}
({detail.firstBadQty ?? 0}) ={" "}
{/* ({detail.firstBadQty ?? 0}) */}
={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)} {formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography> </Typography>
</Stack> </Stack>
@@ -974,7 +950,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
(detail.secondStockTakeQty ?? 0) + (detail.secondStockTakeQty ?? 0) +
(detail.secondBadQty ?? 0) (detail.secondBadQty ?? 0)
)}{" "} )}{" "}
({detail.secondBadQty ?? 0}) ={" "}
{/* ({detail.secondBadQty ?? 0}) */}
={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)} {formatNumber(detail.secondStockTakeQty ?? 0)}
</Typography> </Typography>
</Stack> </Stack>
@@ -1013,6 +990,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
/> />
{/*
<TextField <TextField
size="small" size="small"
type="number" type="number"
@@ -1030,6 +1008,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
disabled={mode === "approved" || selection !== "approver"} disabled={mode === "approved" || selection !== "approver"}
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
/> />
*/
}
<Typography variant="body2" sx={{ minWidth: 90 }}> <Typography variant="body2" sx={{ minWidth: 90 }}>
= {formatNumber(approverGoodQty)} = {formatNumber(approverGoodQty)}
</Typography> </Typography>
@@ -1124,8 +1104,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
sortable: false, sortable: false,
renderCell: (params) => { renderCell: (params) => {
const detail = params.row; const detail = params.row;
const hasSecond =
detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0;
const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0;
const selection = const selection =
qtySelection[detail.id] || (hasSecond ? "second" : "first"); qtySelection[detail.id] || (hasSecond ? "second" : "first");
@@ -1149,7 +1128,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
variant="outlined" variant="outlined"
color="warning" color="warning"
onClick={() => handleUpdateStatusToNotMatch(detail)} onClick={() => handleUpdateStatusToNotMatch(detail)}
disabled={updatingStatus}
disabled={updatingStatus||hasSecond}
> >
{t("Not Match")} {t("Not Match")}
</Button> </Button>
@@ -1189,6 +1168,25 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
]); ]);
const effectivePageSize = const effectivePageSize =
pageSize === "all" ? Math.max(total || 0, 0) : (pageSize as number); pageSize === "all" ? Math.max(total || 0, 0) : (pageSize as number);
useEffect(() => {
setStockTakeSectionOptions((prev) => {
const sectionSet = new Set<string>(prev);
inventoryLotDetails.forEach((item) => {
const section = item.stockTakeSection?.trim();
if (section) sectionSet.add(section);
});
return Array.from(sectionSet).sort((a, b) => a.localeCompare(b));
});
setStoreIdOptions((prev) => {
const storeSet = new Set<string>([...prev, "2F", "4F"]);
inventoryLotDetails.forEach((item) => {
const storeId = item.storeId?.trim();
if (storeId) storeSet.add(storeId);
});
return Array.from(storeSet).sort((a, b) => a.localeCompare(b));
});
}, [inventoryLotDetails]);

return ( return (
<Box> <Box>
{onBack && ( {onBack && (
@@ -1260,11 +1258,101 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({


<AccordionDetails> <AccordionDetails>
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%" }}>
<SearchBox<ApproverSearchKey>
criteria={approverSearchCriteria}
onSearch={handleApproverSearchBoxSearch}
onReset={handleApproverSearchBoxReset}
/>
<Card elevation={0}>
<CardContent>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<FormControl fullWidth>
<InputLabel>{t("Stock Take Section")}</InputLabel>
<Select
value={searchSectionDescription}
label={t("Stock Take Section")}
onChange={(e) => setSearchSectionDescription(e.target.value)}
>
<MenuItem value="All">{t("All")}</MenuItem>
{sectionDescriptionOptions.map((desc) => (
<MenuItem key={desc} value={desc}>
{desc}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={4}>
<Autocomplete
freeSolo
options={stockTakeSectionOptions}
value={searchStockTakeSession}
onInputChange={(_, newValue) => setSearchStockTakeSession(newValue)}
renderInput={(params) => (
<TextField
{...params}
fullWidth
label={t("Stock Take Section (can use , to search multiple sections)")}
/>
)}
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label={t("Item")}
value={searchItemKeyword}
onChange={(e) => setSearchItemKeyword(e.target.value)}
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label={t("Warehouse")}
value={searchWarehouseKeyword}
onChange={(e) => setSearchWarehouseKeyword(e.target.value)}
/>
</Grid>
<Grid item xs={12} md={4}>
<FormControl fullWidth>
<InputLabel>{t("Store ID")}</InputLabel>
<Select
value={searchStoreId}
label={t("Store ID")}
onChange={(e) => setSearchStoreId(e.target.value)}
>
<MenuItem value="All">{t("All")}</MenuItem>
{storeIdOptions.map((storeId) => (
<MenuItem key={storeId} value={storeId}>
{storeId}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
{mode === "pending" && (
<Grid item xs={12} md={4}>
<FormControl fullWidth>
<InputLabel>{t("Record Status")}</InputLabel>
<Select
value={searchStatus}
label={t("Record Status")}
onChange={(e) => setSearchStatus(e.target.value)}
>
<MenuItem value="pending">{t("Pending")}</MenuItem>
<MenuItem value="notMatch">{t("Not Match")}</MenuItem>
<MenuItem value="pass">{t("Pass")}</MenuItem>
</Select>
</FormControl>
</Grid>
)}
</Grid>
<CardActions sx={{ px: 0, pt: 2, gap: 1 }}>
<Button variant="outlined" onClick={handleResetSearch}>
{t("Reset")}
</Button>
<Button variant="contained" onClick={handleSearch}>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
</Box> </Box>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>


+ 178
- 82
src/components/StockTakeManagement/PickerCardList.tsx Ver fichero

@@ -12,21 +12,24 @@ import {
CircularProgress, CircularProgress,
TablePagination, TablePagination,
Grid, Grid,
LinearProgress,
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogActions, DialogActions,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Autocomplete,
} from "@mui/material"; } from "@mui/material";
import { SessionWithTokens } from "@/config/authConfig"; import { SessionWithTokens } from "@/config/authConfig";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import duration from "dayjs/plugin/duration"; import duration from "dayjs/plugin/duration";
import { import {
getStockTakeRecords,
AllPickedStockTakeListReponse, AllPickedStockTakeListReponse,
createStockTakeForSections, createStockTakeForSections,
getStockTakeRecordsPaged, getStockTakeRecordsPaged,
@@ -36,7 +39,6 @@ import { fetchStockTakeSections } from "@/app/api/warehouse/actions";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { AUTH } from "@/authorities"; import { AUTH } from "@/authorities";
const PER_PAGE = 6;


interface PickerCardListProps { interface PickerCardListProps {
/** 由父層保存,從明細返回時仍回到同一頁 */ /** 由父層保存,從明細返回時仍回到同一頁 */
@@ -57,7 +59,6 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
const { t } = useTranslation(["inventory", "common"]); const { t } = useTranslation(["inventory", "common"]);
dayjs.extend(duration); dayjs.extend(duration);


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 [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -68,66 +69,45 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
const [listRefreshNonce, setListRefreshNonce] = useState(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 [filterStockTakeSession, setFilterStockTakeSession] = useState<string>("");
const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState<{ value: string; label: string }[]>([]);
type PickerSearchKey = "sectionDescription" | "stockTakeSession";
const sectionDescriptionOptions = Array.from(
new Set(
stockTakeSessions
.map((s) => s.stockTakeSectionDescription)
.filter((v): v is string => !!v)
)
);
/*
// 按 description + section 双条件过滤
const filteredSessions = stockTakeSessions.filter((s) => {
const matchDesc =
filterSectionDescription === "All" ||
s.stockTakeSectionDescription === filterSectionDescription;
const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]);
const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]);
const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]);
const [searchSectionDescription, setSearchSectionDescription] = useState<string>("All");
const [searchStockTakeSession, setSearchStockTakeSession] = useState<string>("");
const [searchStatus, setSearchStatus] = useState<string>("All");
const [searchArea, setSearchArea] = useState<string>("");
const [searchStoreId, setSearchStoreId] = useState<string>("All");


const sessionParts = (filterStockTakeSession ?? "")
.split(",")
.map((p) => p.trim().toLowerCase())
.filter(Boolean);
const matchSession =
sessionParts.length === 0 ||
sessionParts.some((part) =>
(s.stockTakeSession ?? "").toString().toLowerCase().includes(part)
);
const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All");
const [filterStockTakeSession, setFilterStockTakeSession] = useState<string>("");
const [filterStatus, setFilterStatus] = useState<string>("All");
const [filterArea, setFilterArea] = useState<string>("");
const [filterStoreId, setFilterStoreId] = useState<string>("All");


return matchDesc && matchSession;
});
*/
const statusOptions = ["pending", "stockTaking", "approving", "completed"];


// SearchBox 的条件配置
const criteria: Criterion<PickerSearchKey>[] = [
{
type: "autocomplete",
label: t("Stock Take Section Description"),
paramName: "sectionDescription",
options: sectionDescriptionAutocompleteOptions,
needAll: true,
},
{
type: "text",
label: t("Stock Take Section (can use , to search multiple sections)"),
paramName: "stockTakeSession",
placeholder: "",
},
];
const handleSearch = () => {
setFilterSectionDescription(searchSectionDescription || "All");
setFilterStockTakeSession(searchStockTakeSession || "");
setFilterStatus(searchStatus || "All");
setFilterArea(searchArea || "");
setFilterStoreId(searchStoreId || "All");
onListPageChange(0);
};


const handleSearch = (inputs: Record<PickerSearchKey | `${PickerSearchKey}To`, string>) => {
setFilterSectionDescription(inputs.sectionDescription || "All");
setFilterStockTakeSession(inputs.stockTakeSession || "");
onListPageChange(0);
};
const handleResetSearch = () => {
setFilterSectionDescription("All");
setFilterStockTakeSession("");
onListPageChange(0);
};
const handleResetSearch = () => {
setSearchSectionDescription("All");
setSearchStockTakeSession("");
setSearchStatus("All");
setSearchArea("");
setSearchStoreId("All");
setFilterSectionDescription("All");
setFilterStockTakeSession("");
setFilterStatus("All");
setFilterArea("");
setFilterStoreId("All");
onListPageChange(0);
};


useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -135,6 +115,9 @@ const handleResetSearch = () => {
getStockTakeRecordsPaged(page, pageSize, { getStockTakeRecordsPaged(page, pageSize, {
sectionDescription: filterSectionDescription, sectionDescription: filterSectionDescription,
stockTakeSections: filterStockTakeSession, stockTakeSections: filterStockTakeSession,
status: filterStatus,
area: filterArea,
storeId: filterStoreId,
}) })
.then((res) => { .then((res) => {
if (cancelled) return; if (cancelled) return;
@@ -154,7 +137,7 @@ const handleResetSearch = () => {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [page, pageSize, filterSectionDescription, filterStockTakeSession, listRefreshNonce]);
}, [page, pageSize, filterSectionDescription, filterStockTakeSession, filterStatus, filterArea, filterStoreId, 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);
@@ -187,18 +170,44 @@ const handleResetSearch = () => {
fetchStockTakeSections() fetchStockTakeSections()
.then((sections) => { .then((sections) => {
const descSet = new Set<string>(); const descSet = new Set<string>();
const sectionSet = new Set<string>();
const storeIdSet = new Set<string>(["2F", "4F"]);
sections.forEach((s) => { sections.forEach((s) => {
const section = s.stockTakeSection?.trim();
if (section) sectionSet.add(section);
const desc = s.stockTakeSectionDescription?.trim(); const desc = s.stockTakeSectionDescription?.trim();
if (desc) descSet.add(desc); if (desc) descSet.add(desc);
const storeId = s.storeId?.trim();
if (storeId) storeIdSet.add(storeId);
}); });
setSectionDescriptionAutocompleteOptions(
Array.from(descSet).map((desc) => ({ value: desc, label: desc }))
);
setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b)));
setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b)));
setStoreIdOptions(Array.from(storeIdSet).sort((a, b) => a.localeCompare(b)));
}) })
.catch((e) => { .catch((e) => {
console.error("Failed to load section descriptions for filter:", e); console.error("Failed to load section descriptions for filter:", e);
}); });
}, []); }, []);
useEffect(() => {
setStockTakeSectionOptions((prev) => {
const sectionSet = new Set<string>(prev);
stockTakeSessions.forEach((item) => {
const section = item.stockTakeSession?.trim();
if (section) sectionSet.add(section);
});
return Array.from(sectionSet).sort((a, b) => a.localeCompare(b));
});
}, [stockTakeSessions]);
useEffect(() => {
setStoreIdOptions((prev) => {
const storeIdSet = new Set<string>([...prev, "2F", "4F"]);
stockTakeSessions.forEach((item) => {
const storeId = item.storeId?.trim();
if (storeId) storeIdSet.add(storeId);
});
return Array.from(storeIdSet).sort((a, b) => a.localeCompare(b));
});
}, [stockTakeSessions]);
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const statusLower = status.toLowerCase(); const statusLower = status.toLowerCase();
if (statusLower === "completed") return "success"; if (statusLower === "completed") return "success";
@@ -277,23 +286,99 @@ const handleResetSearch = () => {
if (!first?.planStartDate) return null; if (!first?.planStartDate) return null;
return dayjs(first.planStartDate).format(OUTPUT_DATE_FORMAT); return dayjs(first.planStartDate).format(OUTPUT_DATE_FORMAT);
})(); })();
if (loading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
);
}

return ( return (
<Box> <Box>
<Box sx={{ width: "100%", mb: 2 }}>
<SearchBox<PickerSearchKey>
criteria={criteria}
onSearch={handleSearch}
onReset={handleResetSearch}
/>
</Box>
<Card elevation={0} sx={{ mb: 2 }}>
<CardContent>
<Typography variant="overline" sx={{ display: "block", mb: 1 }}>
{t("Search Criteria")}
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<FormControl fullWidth>
<InputLabel>{t("Stock Take Section")}</InputLabel>
<Select
value={searchSectionDescription}
label={t("Stock Take Section")}
onChange={(e) => setSearchSectionDescription(e.target.value)}
>
<MenuItem value="All">{t("All")}</MenuItem>
{sectionDescriptionOptions.map((desc) => (
<MenuItem key={desc} value={desc}>
{desc}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={4}>
<Autocomplete
freeSolo
options={stockTakeSectionOptions}
value={searchStockTakeSession}
onInputChange={(_, newValue) => setSearchStockTakeSession(newValue)}
renderInput={(params) => (
<TextField
{...params}
fullWidth
label={t("Stock Take Section (can use , to search multiple sections)")}
/>
)}
/>
</Grid>
<Grid item xs={12} md={4}>
<FormControl fullWidth>
<InputLabel>{t("Status")}</InputLabel>
<Select
value={searchStatus}
label={t("Status")}
onChange={(e) => setSearchStatus(e.target.value)}
>
<MenuItem value="All">{t("All")}</MenuItem>
{statusOptions.map((status) => (
<MenuItem key={status} value={status}>
{t(status)}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label={t("Area")}
value={searchArea}
onChange={(e) => setSearchArea(e.target.value)}
/>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>{t("Store ID")}</InputLabel>
<Select
value={searchStoreId}
label={t("Store ID")}
onChange={(e) => setSearchStoreId(e.target.value)}
>
<MenuItem value="All">{t("All")}</MenuItem>
{storeIdOptions.map((storeId) => (
<MenuItem key={storeId} value={storeId}>
{storeId}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
<CardActions sx={{ px: 0, pt: 2, gap: 1 }}>
<Button variant="outlined" onClick={handleResetSearch}>
{t("Reset")}
</Button>
<Button variant="contained" onClick={handleSearch}>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>


@@ -314,13 +399,23 @@ const handleResetSearch = () => {
</Button> </Button>
</Box> </Box>


{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={2}> <Grid container spacing={2}>
{stockTakeSessions.map((session: AllPickedStockTakeListReponse) => {
{stockTakeSessions.map((session: AllPickedStockTakeListReponse) => {
const statusColor = getStatusColor(session.status || ""); const statusColor = getStatusColor(session.status || "");
const lastStockTakeDate = session.lastStockTakeDate const lastStockTakeDate = session.lastStockTakeDate
? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT) ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT)
: "-"; : "-";
const completionRate = getCompletionRate(session); const completionRate = getCompletionRate(session);
const sectionMeta = [
session.stockTakeSectionDescription,
session.warehouseArea,
session.storeId,
].filter((v): v is string => Boolean(v && v.trim())).join(" / ");


return ( return (
<Grid key={session.id} item xs={12} sm={6} md={4}> <Grid key={session.id} item xs={12} sm={6} md={4}>
@@ -337,7 +432,7 @@ const handleResetSearch = () => {
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
<Typography variant="subtitle1" fontWeight={600}> <Typography variant="subtitle1" fontWeight={600}>
{t("Section")}: {session.stockTakeSession} {t("Section")}: {session.stockTakeSession}
{session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : null}
{sectionMeta ? ` (${sectionMeta})` : null}
</Typography> </Typography>
</Stack> </Stack>
@@ -383,6 +478,7 @@ const handleResetSearch = () => {
); );
})} })}
</Grid> </Grid>
)}


{total > 0 && ( {total > 0 && (
<TablePagination <TablePagination


+ 8
- 2
src/components/StockTakeManagement/PickerReStockTake.tsx Ver fichero

@@ -495,6 +495,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}} }}
placeholder={t("Stock Take Qty")} placeholder={t("Stock Take Qty")}
/> />
{/*
<TextField <TextField
size="small" size="small"
type="number" type="number"
@@ -520,6 +521,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}} }}
placeholder={t("Bad Qty")} placeholder={t("Bad Qty")}
/> />
*/}
<Typography variant="body2"> <Typography variant="body2">
= {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.firstBadQty || "0"))} = {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.firstBadQty || "0"))}
</Typography> </Typography>
@@ -528,7 +530,8 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<Typography variant="body2"> <Typography variant="body2">
{t("First")}:{" "} {t("First")}:{" "}
{formatNumber((detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0))}{" "} {formatNumber((detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0))}{" "}
({formatNumber(detail.firstBadQty ?? 0)}) ={" "}
{/* ({formatNumber(detail.firstBadQty ?? 0)}) */}
={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)} {formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography> </Typography>
) : null} ) : null}
@@ -562,6 +565,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}} }}
placeholder={t("Stock Take Qty")} placeholder={t("Stock Take Qty")}
/> />
{/*
<TextField <TextField
size="small" size="small"
type="number" type="number"
@@ -587,6 +591,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}} }}
placeholder={t("Bad Qty")} placeholder={t("Bad Qty")}
/> />
*/}
<Typography variant="body2"> <Typography variant="body2">
= {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.secondBadQty || "0"))} = {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.secondBadQty || "0"))}
</Typography> </Typography>
@@ -595,7 +600,8 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<Typography variant="body2"> <Typography variant="body2">
{t("Second")}:{" "} {t("Second")}:{" "}
{formatNumber((detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0))}{" "} {formatNumber((detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0))}{" "}
({formatNumber(detail.secondBadQty ?? 0)}) ={" "}
{/* ({formatNumber(detail.secondBadQty ?? 0)}) */}
={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)} {formatNumber(detail.secondStockTakeQty ?? 0)}
</Typography> </Typography>
) : null} ) : null}


+ 12
- 13
src/components/StockTakeManagement/PickerStockTake.tsx Ver fichero

@@ -191,18 +191,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty;
const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty; const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty;
// 只檢查 totalQty,Bad Qty 未輸入時預設為 0
if (!totalQtyStr) {
onSnackbar(
isFirstSubmit
? t("Please enter QTY")
: t("Please enter Second QTY"),
"error"
);
return;
}

const totalQty = parseFloat(totalQtyStr);
const totalQty = parseFloat(totalQtyStr || "0") || 0;
const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0 const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0
if (Number.isNaN(totalQty)) { if (Number.isNaN(totalQty)) {
@@ -617,6 +608,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}} }}
placeholder={t("Stock Take Qty")} placeholder={t("Stock Take Qty")}
/> />
{/*
<TextField <TextField
size="small" size="small"
type="number" type="number"
@@ -643,6 +635,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}} }}
placeholder={t("Bad Qty")} placeholder={t("Bad Qty")}
/> />
*/}
<Typography variant="body2"> <Typography variant="body2">
= =
{formatNumber( {formatNumber(
@@ -658,11 +651,13 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
(detail.firstStockTakeQty ?? 0) + (detail.firstStockTakeQty ?? 0) +
(detail.firstBadQty ?? 0) (detail.firstBadQty ?? 0)
)}{" "} )}{" "}
{/*
( (
{formatNumber( {formatNumber(
detail.firstBadQty ?? 0 detail.firstBadQty ?? 0
)} )}
) ={" "}
*/}
={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)} {formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography> </Typography>
) : null} ) : null}
@@ -693,6 +688,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}} }}
placeholder={t("Stock Take Qty")} placeholder={t("Stock Take Qty")}
/> />
{/*
<TextField <TextField
size="small" size="small"
type="number" type="number"
@@ -715,6 +711,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}} }}
placeholder={t("Bad Qty")} placeholder={t("Bad Qty")}
/> />
*/}
<Typography variant="body2"> <Typography variant="body2">
= =
{formatNumber( {formatNumber(
@@ -730,11 +727,13 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
(detail.secondStockTakeQty ?? 0) + (detail.secondStockTakeQty ?? 0) +
(detail.secondBadQty ?? 0) (detail.secondBadQty ?? 0)
)}{" "} )}{" "}
{/*
( (
{formatNumber( {formatNumber(
detail.secondBadQty ?? 0 detail.secondBadQty ?? 0
)} )}
) ={" "}
*/}
={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)} {formatNumber(detail.secondStockTakeQty ?? 0)}
</Typography> </Typography>
) : null} ) : null}


+ 26
- 2
src/config/reportConfig.ts Ver fichero

@@ -110,7 +110,7 @@ export const REPORTS: ReportDefinition[] = [
apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance-v2`, apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance-v2`,
fields: [ fields: [
{ {
label: "盤點輪次 Stock Take Round",
label: "盤點輪次",
name: "stockTakeRoundId", name: "stockTakeRoundId",
type: "select", type: "select",
required: true, required: true,
@@ -118,7 +118,31 @@ export const REPORTS: ReportDefinition[] = [
dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-take-rounds`, dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-take-rounds`,
options: [] options: []
}, },
{ label: "物料編號 Item Code", name: "itemCode", type: "text", required: false},
{ label: "物料編號", name: "itemCode", type: "text", required: false},
{
label: "倉庫樓層",
name: "store_id",
type: "select",
required: false,
options: [
{ label: "全部", value: "All" },
{ label: "1F", value: "1F" },
{ label: "2F", value: "2F" },
{ label: "3F", value: "3F" },
{ label: "4F", value: "4F" }
],
},
{
label: "狀態",
name: "status",
type: "select",
required: false,
options: [
{ label: "全部", value: "All" },
{ label: "待盤點", value: "pending" },
{ label: "已審核", value: "completed" }
],
},
] ]
}, },
{ id: "rep-011", { id: "rep-011",


+ 3
- 5
src/i18n/zh/common.json Ver fichero

@@ -15,8 +15,6 @@
"Floor": "樓層", "Floor": "樓層",
"Job Order Type": "工單類型", "Job Order Type": "工單類型",


"FG": "成品",
"WIP": "半成品",
"BOM Type": "BOM 類型", "BOM Type": "BOM 類型",
"No Lot": "沒有批號", "No Lot": "沒有批號",
"Select All": "全選", "Select All": "全選",
@@ -110,7 +108,7 @@
"code": "編號", "code": "編號",
"Name": "名稱", "Name": "名稱",
"Assignment successful": "分配成功", "Assignment successful": "分配成功",
"Pass": "通過",
"Unable to get user ID": "無法獲取用戶ID", "Unable to get user ID": "無法獲取用戶ID",
"Unknown error: ": "未知錯誤: ", "Unknown error: ": "未知錯誤: ",
"Please try again later.": "請稍後重試。", "Please try again later.": "請稍後重試。",
@@ -468,8 +466,8 @@
"Powder_Mixture": "箱料粉", "Powder_Mixture": "箱料粉",
"Powder Mixture": "箱料粉", "Powder Mixture": "箱料粉",
"Not Match": "數值不符", "Not Match": "數值不符",
"Pass": "通過",
"pass": "通過",
"Pass": "已盤點",
"pass": "已盤點",
"Actions": "操作", "Actions": "操作",
"Insert": "插入", "Insert": "插入",
"Move to order": "移動到指定順序", "Move to order": "移動到指定順序",


+ 8
- 3
src/i18n/zh/inventory.json Ver fichero

@@ -50,11 +50,13 @@
"Batch Save All": "批量保存所有", "Batch Save All": "批量保存所有",
"not match": "數值不符", "not match": "數值不符",
"Not Match": "數值不符", "Not Match": "數值不符",
"Pass": "通過",
"Pass": "已盤點",
"Area": "區域",
"Selected Qty": "選擇數量", "Selected Qty": "選擇數量",
"Show Search Filters": "顯示搜索器", "Show Search Filters": "顯示搜索器",
"Hide Search Filters": "隱藏搜索器", "Hide Search Filters": "隱藏搜索器",
"Stock Take Qty(include Bad Qty)= Available Qty": "盤點數(含壞品)= 可用數",
"Stock Take Qty(include Bad Qty)= Available Qty": "盤點數= 可用數",
"View ReStockTake": "查看重新盤點", "View ReStockTake": "查看重新盤點",
"Stock Take Qty": "盤點數", "Stock Take Qty": "盤點數",
"variance Percentage": "差異百分比", "variance Percentage": "差異百分比",
@@ -97,9 +99,11 @@
"book qty": "帳面庫存", "book qty": "帳面庫存",
"start time": "開始時間", "start time": "開始時間",
"end time": "結束時間", "end time": "結束時間",
"notmatch": "數值不符",
"Only Variance": "僅差異", "Only Variance": "僅差異",
"Control Time": "操作時間", "Control Time": "操作時間",
"pass": "通過",
"Batch approver save completed: {{success}} success, {{errors}} errors": "批次審核儲存完成:成功 {{success}} 筆,錯誤 {{errors}} 筆",
"pass": "已盤點",
"not pass": "不通過", "not pass": "不通過",
"Available": "可用", "Available": "可用",
"approving": "審核中", "approving": "審核中",
@@ -126,6 +130,7 @@
"Create Stock Take for All Sections": "為所有區域創建盤點", "Create Stock Take for All Sections": "為所有區域創建盤點",
"section": "區域", "section": "區域",
"Stock Take Section": "盤點區域", "Stock Take Section": "盤點區域",
"Store ID":"樓層",
"Warehouse Location": "倉庫位置", "Warehouse Location": "倉庫位置",
"UOM": "單位", "UOM": "單位",
"First Qty": "第一次盤點數量", "First Qty": "第一次盤點數量",


Cargando…
Cancelar
Guardar