diff --git a/src/app/(main)/stocktakemanagement/page.tsx b/src/app/(main)/stocktakemanagement/page.tsx
new file mode 100644
index 0000000..252bdf4
--- /dev/null
+++ b/src/app/(main)/stocktakemanagement/page.tsx
@@ -0,0 +1,19 @@
+import { Suspense } from "react";
+import StockTakeManagementWrapper from "@/components/StockTakeManagement";
+import { I18nProvider, getServerI18n } from "@/i18n";
+import { Typography } from "@mui/material";
+import { isArray } from "lodash";
+import { Metadata } from "next";
+import { notFound } from "next/navigation";
+
+
+export default async function InventoryManagementPage() {
+ const { t } = await getServerI18n("inventory");
+ return (
+
+ }>
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx
index 4be8783..aabc77b 100644
--- a/src/app/api/do/actions.tsx
+++ b/src/app/api/do/actions.tsx
@@ -101,7 +101,31 @@ export interface PrintDNLabelsRespone{
success: boolean;
message?: string
}
+export interface BatchReleaseRequest {
+ ids: number[];
+}
+export interface BatchReleaseResponse {
+ success: boolean;
+ message?: string
+}
+export const startBatchReleaseAsync = cache(async (data: { ids: number[]; userId: number }) => {
+ const { ids, userId } = data;
+ return await serverFetchJson<{ id: number|null; code: string; entity?: any }>(
+ `${BASE_API_URL}/doPickOrder/batch-release/async?userId=${userId}`,
+ {
+ method: "POST",
+ body: JSON.stringify(ids),
+ headers: { "Content-Type": "application/json" },
+ }
+ );
+});
+export const getBatchReleaseProgress = cache(async (jobId: string) => {
+ return await serverFetchJson<{ id: number|null; code: string; entity?: any }>(
+ `${BASE_API_URL}/doPickOrder/batch-release/progress/${jobId}`,
+ { method: "GET" }
+ );
+});
export const assignPickOrderByStore = cache(async (data: AssignByStoreRequest) => {
return await serverFetchJson(`${BASE_API_URL}/doPickOrder/assign-by-store`,
{
diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts
index ead62f8..8e885a9 100644
--- a/src/app/api/pickOrder/actions.ts
+++ b/src/app/api/pickOrder/actions.ts
@@ -400,8 +400,11 @@ export const updatePickExecutionIssueStatus = async (
return result;
};
export async function fetchStoreLaneSummary(storeId: string): Promise {
+ // ✅ 硬编码测试日期 - 改成你想测试的日期
+ const testDate = "2025-10-16"; // 或者 "2025-10-16", "2025-10-17" 等
+
const response = await serverFetchJson(
- `${BASE_API_URL}/doPickOrder/summary-by-store?storeId=${encodeURIComponent(storeId)}`,
+ `${BASE_API_URL}/doPickOrder/summary-by-store?storeId=${encodeURIComponent(storeId)}&requiredDate=${testDate}`,
{
method: "GET",
}
diff --git a/src/components/DoDetail/DoDetail.tsx b/src/components/DoDetail/DoDetail.tsx
index e7d70c1..9f3bd0b 100644
--- a/src/components/DoDetail/DoDetail.tsx
+++ b/src/components/DoDetail/DoDetail.tsx
@@ -33,7 +33,8 @@ const DoDetail: React.FC = ({
const { data: session } = useSession() as { data: SessionWithTokens | null }; // ✅ Use correct session type
const currentUserId = session?.id ? parseInt(session.id) : undefined; // ✅ Get user ID from session.id
-
+ console.log("🔍 DoSearch - session:", session);
+console.log("🔍 DoSearch - currentUserId:", currentUserId);
const formProps = useForm({
defaultValues: defaultValues
})
diff --git a/src/components/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx
index 520d5a6..b2acb47 100644
--- a/src/components/DoSearch/DoSearch.tsx
+++ b/src/components/DoSearch/DoSearch.tsx
@@ -1,7 +1,7 @@
"use client";
import { DoResult } from "@/app/api/do";
-import { DoSearchAll, fetchDoSearch, releaseDo } from "@/app/api/do/actions";
+import { DoSearchAll, fetchDoSearch, releaseDo ,startBatchReleaseAsync, getBatchReleaseProgress} from "@/app/api/do/actions";
import { useRouter } from "next/navigation";
import React, { ForwardedRef, useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -30,7 +30,8 @@ import { Box, Button, Grid, Stack, Typography, TablePagination } from "@mui/mate
import StyledDataGrid from "../StyledDataGrid";
import { GridRowSelectionModel } from "@mui/x-data-grid";
import Swal from "sweetalert2";
-
+import { useSession } from "next-auth/react";
+import { SessionWithTokens } from "@/config/authConfig";
type Props = {
filterArgs?: Record;
searchQuery?: Record;
@@ -60,6 +61,11 @@ const DoSearch: React.FC = ({filterArgs, searchQuery, onDeliveryOrderSear
const { t } = useTranslation("do");
const router = useRouter();
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+ console.log("🔍 DoSearch - session:", session);
+console.log("🔍 DoSearch - currentUserId:", currentUserId);
+ const [searchTimeout, setSearchTimeout] = useState(null);
const [rowSelectionModel, setRowSelectionModel] =
useState([]);
@@ -293,15 +299,14 @@ const DoSearch: React.FC = ({filterArgs, searchQuery, onDeliveryOrderSear
//SEARCH FUNCTION
const handleSearch = useCallback(async (query: SearchBoxInputs) => {
try {
-
setCurrentSearchParams(query);
-
+
let orderStartDate = query.orderDate;
let orderEndDate = query.orderDateTo;
let estArrStartDate = query.estimatedArrivalDate;
let estArrEndDate = query.estimatedArrivalDateTo;
const time = "T00:00:00";
-
+
if(orderStartDate != ""){
orderStartDate = query.orderDate + time;
}
@@ -332,7 +337,7 @@ const DoSearch: React.FC = ({filterArgs, searchQuery, onDeliveryOrderSear
estArrStartDate,
estArrEndDate
);
-
+
setSearchAllDos(data);
setHasSearched(true);
setHasResults(data.length > 0);
@@ -340,94 +345,126 @@ const DoSearch: React.FC = ({filterArgs, searchQuery, onDeliveryOrderSear
console.error("Error: ", error);
}
}, []);
-
- const handleBatchRelease = useCallback(async () => {
- const query = currentSearchParams;
-
- let orderStartDate = query.orderDate;
- let orderEndDate = query.orderDateTo;
- let estArrStartDate = query.estimatedArrivalDate;
- let estArrEndDate = query.estimatedArrivalDateTo;
- const time = "T00:00:00";
-
-if(orderStartDate != ""){
- orderStartDate = query.orderDate + time;
- }
- if(orderEndDate != ""){
- orderEndDate = query.orderDateTo + time;
- }
- if(estArrStartDate != ""){
- estArrStartDate = query.estimatedArrivalDate + time;
- }
- if(estArrEndDate != ""){
- estArrEndDate = query.estimatedArrivalDateTo + time;
+ const debouncedSearch = useCallback((query: SearchBoxInputs) => {
+ if (searchTimeout) {
+ clearTimeout(searchTimeout);
}
- let status = "";
- if(query.status == "All"){
- status = "";
- }
- else{
- status = query.status;
+ const timeout = setTimeout(() => {
+ handleSearch(query);
+ }, 300);
+
+ setSearchTimeout(timeout);
+ }, [handleSearch, searchTimeout]);
+ const handleBatchRelease = useCallback(async () => {
+ const selectedIds = rowSelectionModel as number[];
+ if (!selectedIds.length) return;
+
+ console.log("🔍 handleBatchRelease - currentUserId:", currentUserId);
+ console.log("🔍 handleBatchRelease - selectedIds:", selectedIds);
+
+ const result = await Swal.fire({
+ icon: "question",
+ title: t("Batch Release"),
+ html: `
+
+
${t("Selected items on current page")}: ${selectedIds.length}
+
${t("Total search results")}: ${searchAllDos.length}
+
+
${t("Choose release option")}:
+
+ `,
+ showCancelButton: true,
+ confirmButtonText: t("Release All Search Results"),
+ cancelButtonText: t("Release Selected Only"),
+ denyButtonText: t("Cancel"),
+ showDenyButton: true,
+ confirmButtonColor: "#8dba00",
+ cancelButtonColor: "#2196f3",
+ denyButtonColor: "#F04438"
+ });
+
+ if (result.isDenied) return;
+
+ let idsToRelease: number[];
+ if (result.isConfirmed) {
+ idsToRelease = searchAllDos.map(d => d.id);
+ } else {
+ idsToRelease = selectedIds;
}
-
- const batchReleaseData = await fetchDoSearch(
- query.code || "",
- query.shopName || "",
- status,
- orderStartDate,
- orderEndDate,
- estArrStartDate,
- estArrEndDate
- );
-
- const extractedIds = batchReleaseData.map(item => item.id);
- const extractedIdsCount = batchReleaseData.map(item => item.id).length;
- const extractedItemsCount = batchReleaseData.flatMap(item => item.deliveryOrderLines).length;
-
- console.log("Batch Release Data:", batchReleaseData);
- console.log("Query:", query);
- console.log("IDs: " + extractedIds);
- console.log("Total Shops: " + extractedIdsCount);
- console.log("Total Items: " + extractedItemsCount);
-
- const result = await Swal.fire(
- {
- icon: "question",
- title: t("Batch Release"),
- html: t("Selected Shop(s): ") + extractedIdsCount.toString() + `
`+
- t("Selected Item(s): ") + extractedItemsCount.toString() + ``,
- showCancelButton: true,
- confirmButtonText: t("Confirm"),
- cancelButtonText: t("Cancel"),
- confirmButtonColor: "#8dba00",
- cancelButtonColor: "#F04438"
- });
- if (result.isConfirmed) {
- Swal.fire({
- title: t("Releasing"),
- text: t("Please wait"),
- allowOutsideClick: false,
- allowEscapeKey: false,
- showConfirmButton: false,
- didOpen: () => {
- Swal.showLoading();
- }
- });
-
- await Promise.all(extractedIds.map((id) => releaseDo({ id: id })));
-
- Swal.fire({
- position: "bottom-end",
- icon: "success",
- text: t("Batch release completed successfully."),
- showConfirmButton: false,
- timer: 1500
- });
+
+ try {
+ const startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 });
+ const jobId = startRes?.entity?.jobId;
+
+ if (!jobId) {
+ await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to start batch release") });
+ return;
}
-
-
- }, [currentSearchParams]);
+
+ await Swal.fire({
+ title: t("Releasing"),
+ html: `
+
+
${t("Total")}: 0
+
${t("Finished")}: 0
+
+
+ `,
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ showConfirmButton: false,
+ didOpen: async () => {
+ const update = (total:number, finished:number, success:number, failed:number) => {
+ const bar = document.getElementById("br-bar") as HTMLElement;
+ const pct = total > 0 ? Math.floor((finished / total) * 100) : 0;
+ (document.getElementById("br-total") as HTMLElement).innerText = `${t("Total")}: ${total}`;
+ (document.getElementById("br-finished") as HTMLElement).innerText = `${t("Finished")}: ${finished}`;
+
+ if (bar) bar.style.width = `${pct}%`;
+ };
+
+ const timer = setInterval(async () => {
+ try {
+ const p = await getBatchReleaseProgress(jobId);
+ const e = p?.entity || {};
+ update(e.total ?? 0, e.finished ?? 0, e.success ?? 0, e.failedCount ?? 0);
+
+ if (p.code === "FINISHED" || e.running === false) {
+ clearInterval(timer);
+ Swal.close();
+
+ // 简化完成提示 - 只显示完成,不显示成功/失败统计
+ await Swal.fire({
+ icon: "success",
+ title: t("Completed"),
+ text: t("Batch release completed"),
+ confirmButtonText: t("OK")
+ });
+
+ if (currentSearchParams && Object.keys(currentSearchParams).length > 0) {
+ await handleSearch(currentSearchParams);
+ }
+ setRowSelectionModel([]);
+ }
+ } catch (err) {
+ console.error("progress poll error:", err);
+ }
+ }, 800);
+ }
+ });
+ } catch (error) {
+ console.error("Batch release error:", error);
+ await Swal.fire({
+ icon: "error",
+ title: t("Error"),
+ text: t("An error occurred during batch release"),
+ confirmButtonText: t("OK")
+ });
+ }
+ }, [rowSelectionModel, t, currentUserId, searchAllDos, currentSearchParams, handleSearch]);
return (
diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
index 9f9c4e2..b7f38aa 100644
--- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
+++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
@@ -34,6 +34,10 @@ import Swal from "sweetalert2";
import { printDN, printDNLabels } from "@/app/api/do/actions";
import { FGPickOrderResponse, fetchStoreLaneSummary, assignByLane,type StoreLaneSummary } from "@/app/api/pickOrder/actions";
import FGPickOrderCard from "./FGPickOrderCard";
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import dayjs, { Dayjs } from 'dayjs';
interface Props {
pickOrders: PickOrderResult[];
diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx
index aa5b1df..04e099b 100644
--- a/src/components/NavigationContent/NavigationContent.tsx
+++ b/src/components/NavigationContent/NavigationContent.tsx
@@ -79,6 +79,11 @@ const NavigationContent: React.FC = () => {
label: "View item In-out And inventory Ledger",
path: "/inventory",
},
+ {
+ icon: ,
+ label: "Stock Take Management",
+ path: "/stocktakemanagement",
+ },
{
icon: ,
label: "Put Away Scan",
diff --git a/src/components/StockTakeManagement/InventoryAdjustmentsTab.tsx b/src/components/StockTakeManagement/InventoryAdjustmentsTab.tsx
new file mode 100644
index 0000000..42ae4cb
--- /dev/null
+++ b/src/components/StockTakeManagement/InventoryAdjustmentsTab.tsx
@@ -0,0 +1,339 @@
+"use client";
+
+import {
+ Box,
+ Card,
+ CardContent,
+ Chip,
+ Paper,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TablePagination,
+ TableRow,
+ TextField,
+ Typography,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ Alert,
+} from "@mui/material";
+import { useState, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import dayjs from "dayjs";
+import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
+
+// Fake data types
+interface InventoryAdjustment {
+ id: number;
+ adjustmentNo: string;
+ type: "stock_take" | "manual" | "damage" | "return";
+ itemCode: string;
+ itemName: string;
+ lotNo: string;
+ location: string;
+ beforeQty: number;
+ afterQty: number;
+ adjustmentQty: number;
+ reason: string;
+ adjustedBy: string;
+ adjustedDate: string;
+ status: "approved" | "pending" | "rejected";
+}
+
+// Fake data
+const fakeAdjustments: InventoryAdjustment[] = [
+ {
+ id: 1,
+ adjustmentNo: "ADJ-2024-001",
+ type: "stock_take",
+ itemCode: "M001",
+ itemName: "Material A",
+ lotNo: "LOT-2024-001",
+ location: "A-01-01",
+ beforeQty: 100,
+ afterQty: 98,
+ adjustmentQty: -2,
+ reason: "Stock take variance",
+ adjustedBy: "John Doe",
+ adjustedDate: "2024-10-10T10:30:00",
+ status: "approved",
+ },
+ {
+ id: 2,
+ adjustmentNo: "ADJ-2024-002",
+ type: "damage",
+ itemCode: "M003",
+ itemName: "Material C",
+ lotNo: "LOT-2024-003",
+ location: "A-01-03",
+ beforeQty: 75,
+ afterQty: 70,
+ adjustmentQty: -5,
+ reason: "Damaged during handling",
+ adjustedBy: "Jane Smith",
+ adjustedDate: "2024-10-11T14:20:00",
+ status: "approved",
+ },
+ {
+ id: 3,
+ adjustmentNo: "ADJ-2024-003",
+ type: "manual",
+ itemCode: "M002",
+ itemName: "Material B",
+ lotNo: "LOT-2024-002",
+ location: "A-01-02",
+ beforeQty: 50,
+ afterQty: 55,
+ adjustmentQty: 5,
+ reason: "Found extra stock",
+ adjustedBy: "John Doe",
+ adjustedDate: "2024-10-12T09:15:00",
+ status: "pending",
+ },
+ {
+ id: 4,
+ adjustmentNo: "ADJ-2024-004",
+ type: "return",
+ itemCode: "M001",
+ itemName: "Material A",
+ lotNo: "LOT-2024-004",
+ location: "A-02-01",
+ beforeQty: 30,
+ afterQty: 35,
+ adjustmentQty: 5,
+ reason: "Return from production",
+ adjustedBy: "Mike Johnson",
+ adjustedDate: "2024-10-13T11:45:00",
+ status: "approved",
+ },
+];
+
+const InventoryAdjustmentsTab: React.FC = () => {
+ const { t } = useTranslation(["inventory"]);
+
+ // States
+ const [adjustments] = useState(fakeAdjustments);
+ const [typeFilter, setTypeFilter] = useState("all");
+ const [statusFilter, setStatusFilter] = useState("all");
+ const [searchText, setSearchText] = useState("");
+
+ // Pagination
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+
+ // Filter adjustments
+ const filteredAdjustments = useMemo(() => {
+ return adjustments.filter((adj) => {
+ // Type filter
+ if (typeFilter !== "all" && adj.type !== typeFilter) {
+ return false;
+ }
+
+ // Status filter
+ if (statusFilter !== "all" && adj.status !== statusFilter) {
+ return false;
+ }
+
+ // Search filter
+ if (searchText) {
+ const search = searchText.toLowerCase();
+ return (
+ adj.adjustmentNo.toLowerCase().includes(search) ||
+ adj.itemCode.toLowerCase().includes(search) ||
+ adj.itemName.toLowerCase().includes(search) ||
+ adj.lotNo.toLowerCase().includes(search)
+ );
+ }
+
+ return true;
+ });
+ }, [adjustments, typeFilter, statusFilter, searchText]);
+
+ // Pagination
+ const paginatedAdjustments = useMemo(() => {
+ const startIndex = page * rowsPerPage;
+ return filteredAdjustments.slice(startIndex, startIndex + rowsPerPage);
+ }, [filteredAdjustments, page, rowsPerPage]);
+
+ const handlePageChange = (event: unknown, newPage: number) => {
+ setPage(newPage);
+ };
+
+ const handleRowsPerPageChange = (event: React.ChangeEvent) => {
+ setRowsPerPage(parseInt(event.target.value, 10));
+ setPage(0);
+ };
+
+ const getAdjustmentTypeColor = (type: string) => {
+ switch (type) {
+ case "stock_take":
+ return "primary";
+ case "manual":
+ return "info";
+ case "damage":
+ return "error";
+ case "return":
+ return "success";
+ default:
+ return "default";
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case "approved":
+ return "success";
+ case "pending":
+ return "warning";
+ case "rejected":
+ return "error";
+ default:
+ return "default";
+ }
+ };
+
+ return (
+
+
+ {t("This is a demo with fake data. API integration pending.")}
+
+
+ {/* Filters */}
+
+
+
+
+ {t("Type")}
+
+
+
+
+ {t("Status")}
+
+
+
+ setSearchText(e.target.value)}
+ placeholder={t("Adjustment No, Item, Lot...")}
+ sx={{ flexGrow: 1 }}
+ />
+
+
+
+ {t("Total Adjustments")}: {filteredAdjustments.length}
+
+
+
+
+ {/* Table */}
+
+
+
+
+ {t("Adjustment No")}
+ {t("Type")}
+ {t("Item")}
+ {t("Lot No")}
+ {t("Location")}
+ {t("Before Qty")}
+ {t("After Qty")}
+ {t("Adjustment")}
+ {t("Reason")}
+ {t("Adjusted By")}
+ {t("Date")}
+ {t("Status")}
+
+
+
+ {paginatedAdjustments.length === 0 ? (
+
+
+
+ {t("No adjustments found")}
+
+
+
+ ) : (
+ paginatedAdjustments.map((adj) => (
+
+ {adj.adjustmentNo}
+
+
+
+
+ {adj.itemCode}
+
+ {adj.itemName}
+
+
+ {adj.lotNo}
+ {adj.location}
+ {adj.beforeQty}
+ {adj.afterQty}
+ = 0 ? "success.main" : "error.main",
+ fontWeight: "bold",
+ }}
+ >
+ {adj.adjustmentQty > 0 ? `+${adj.adjustmentQty}` : adj.adjustmentQty}
+
+ {adj.reason}
+ {adj.adjustedBy}
+ {dayjs(adj.adjustedDate).format(OUTPUT_DATE_FORMAT)}
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default InventoryAdjustmentsTab;
\ No newline at end of file
diff --git a/src/components/StockTakeManagement/PickExecutionIssuesTab.tsx b/src/components/StockTakeManagement/PickExecutionIssuesTab.tsx
new file mode 100644
index 0000000..d98efba
--- /dev/null
+++ b/src/components/StockTakeManagement/PickExecutionIssuesTab.tsx
@@ -0,0 +1,457 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Paper,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TablePagination,
+ TableRow,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { useCallback, useEffect, useState, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import SearchBox, { Criterion } from "../SearchBox";
+import SearchResults, { Column } from "../SearchResults";
+import { defaultPagingController } from "../SearchResults/SearchResults";
+import {
+ fetchAllPickExecutionIssues,
+ PickExecutionIssue,
+ updatePickExecutionIssueStatus,
+ FetchPickExecutionIssuesParams,
+} from "@/app/api/pickOrder/actions";
+import dayjs from "dayjs";
+import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
+
+type SearchQuery = {
+ issueNo: string;
+ pickOrderCode: string;
+ itemCode: string;
+ lotNo: string;
+ type: string;
+ status: string;
+};
+
+type SearchParamNames = keyof SearchQuery;
+
+const PickExecutionIssuesTab: React.FC = () => {
+ const { t } = useTranslation(["inventory", "pickOrder"]);
+
+ // States
+ const [issues, setIssues] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [pagingController, setPagingController] = useState(defaultPagingController);
+ const [totalCount, setTotalCount] = useState(0);
+
+ // Search inputs
+ const defaultInputs = useMemo(() => ({
+ issueNo: "",
+ pickOrderCode: "",
+ itemCode: "",
+ lotNo: "",
+ type: "",
+ status: "",
+ }), []);
+ const [inputs, setInputs] = useState>(defaultInputs);
+
+ // Handle dialog
+ const [selectedIssue, setSelectedIssue] = useState(null);
+ const [handleDialogOpen, setHandleDialogOpen] = useState(false);
+ const [handleRemark, setHandleRemark] = useState("");
+
+ // Search criteria
+ const searchCriteria: Criterion[] = useMemo(
+ () => [
+ { label: t("Issue No"), paramName: "issueNo", type: "text" },
+ { label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text" },
+ { label: t("Item Code"), paramName: "itemCode", type: "text" },
+ { label: t("Lot No"), paramName: "lotNo", type: "text" },
+ {
+ label: t("Type"),
+ paramName: "type",
+ type: "select",
+ options: ["all", "jo", "do", "material"],
+ },
+ {
+ label: t("Status"),
+ paramName: "status",
+ type: "select",
+ options: ["all", "pending", "resolved"],
+ },
+ ],
+ [t],
+ );
+
+ // Fetch issues
+ const refetchIssuesData = useCallback(async (
+ query: Record,
+ actionType: "reset" | "search" | "paging" | "init",
+ pagingController: typeof defaultPagingController,
+ ) => {
+ console.log("%c Action Type.", "color:red", actionType);
+
+ setLoading(true);
+ try {
+ // Build API parameters with proper type casting
+ let apiParams: FetchPickExecutionIssuesParams | undefined = undefined;
+
+ if (query.type && query.type !== "all") {
+ // Type assertion to ensure the string is one of the valid types
+ const validType = query.type as "jo" | "do" | "material";
+ apiParams = { type: validType };
+ }
+
+ // Call API with type parameter if specified
+ const result = await fetchAllPickExecutionIssues(apiParams);
+
+ // Apply client-side filtering for other parameters
+ let filteredResult = result;
+
+ if (query.issueNo) {
+ filteredResult = filteredResult.filter(issue =>
+ issue.issueNo.toLowerCase().includes(query.issueNo.toLowerCase())
+ );
+ }
+
+ if (query.pickOrderCode) {
+ filteredResult = filteredResult.filter(issue =>
+ issue.pickOrderCode.toLowerCase().includes(query.pickOrderCode.toLowerCase())
+ );
+ }
+
+ if (query.itemCode) {
+ filteredResult = filteredResult.filter(issue =>
+ issue.itemCode.toLowerCase().includes(query.itemCode.toLowerCase())
+ );
+ }
+
+ if (query.lotNo) {
+ filteredResult = filteredResult.filter(issue =>
+ issue.lotNo && issue.lotNo.toLowerCase().includes(query.lotNo.toLowerCase())
+ );
+ }
+
+ if (query.status && query.status !== "all") {
+ filteredResult = filteredResult.filter(issue => {
+ if (query.status === "pending") {
+ return issue.handleStatus.toLowerCase() === "pending";
+ }
+ if (query.status === "resolved") {
+ return issue.handleStatus.toLowerCase() !== "pending";
+ }
+ return true;
+ });
+ }
+
+ setIssues(filteredResult);
+ setTotalCount(filteredResult.length);
+
+ } catch (error) {
+ console.error("Error fetching pick execution issues:", error);
+ setIssues([]);
+ setTotalCount(0);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ refetchIssuesData(defaultInputs, "init", defaultPagingController);
+ }, [refetchIssuesData, defaultInputs]);
+
+ useEffect(() => {
+ refetchIssuesData(inputs, "paging", pagingController);
+ }, [pagingController, refetchIssuesData, inputs]);
+
+ // Handlers
+ const onReset = useCallback(() => {
+ refetchIssuesData(defaultInputs, "reset", defaultPagingController);
+ setInputs(() => defaultInputs);
+ setPagingController(() => defaultPagingController);
+ }, [refetchIssuesData, defaultInputs]);
+
+ const onSearch = useCallback((query: Record) => {
+ refetchIssuesData(query, "search", defaultPagingController);
+ setInputs(() => query);
+ setPagingController(() => defaultPagingController);
+ }, [refetchIssuesData]);
+
+ const handleViewDetail = (issue: PickExecutionIssue) => {
+ setSelectedIssue(issue);
+ setHandleDialogOpen(true);
+ setHandleRemark("");
+ };
+
+ const handleCloseDialog = () => {
+ setHandleDialogOpen(false);
+ setSelectedIssue(null);
+ setHandleRemark("");
+ };
+
+ const handleMarkAsResolved = async () => {
+ if (!selectedIssue) return;
+
+ try {
+ await updatePickExecutionIssueStatus({
+ issueId: selectedIssue.id,
+ handleStatus: "resolved",
+ handleDate: new Date().toISOString(),
+ handleRemark: handleRemark,
+ });
+
+ // Refresh data
+ await refetchIssuesData(inputs, "search", pagingController);
+ handleCloseDialog();
+ } catch (error) {
+ console.error("Error updating issue status:", error);
+ }
+ };
+
+ const getIssueCategoryColor = (category: string) => {
+ switch (category.toLowerCase()) {
+ case "lot_issue":
+ return "error";
+ case "quality_issue":
+ return "warning";
+ case "quantity_mismatch":
+ return "info";
+ default:
+ return "default";
+ }
+ };
+
+ // Table columns
+ const columns: Column[] = useMemo(
+ () => [
+ { name: "issueNo", label: t("Issue No") },
+ { name: "pickOrderCode", label: t("Pick Order") },
+ {
+ name: "itemCode",
+ label: t("Item"),
+ renderCell: (params) => (
+
+ {params.itemCode}
+
+ {params.itemDescription}
+
+
+ ),
+ },
+ {
+ name: "lotNo",
+ label: t("Lot No"),
+ renderCell: (params) => params.lotNo || "-"
+ },
+ { name: "requiredQty", label: t("Required Qty"), align: "right", type: "integer" },
+ {
+ name: "actualPickQty",
+ label: t("Actual Qty"),
+ align: "right",
+ type: "integer",
+ renderCell: (params) => params.actualPickQty || 0
+ },
+ {
+ name: "missQty",
+ label: t("Miss Qty"),
+ align: "right",
+ type: "integer",
+ renderCell: (params) => params.missQty || 0
+ },
+ {
+ name: "issueCategory",
+ label: t("Category"),
+ renderCell: (params) => (
+
+ ),
+ },
+ {
+ name: "handleStatus",
+ label: t("Status"),
+ renderCell: (params) => (
+
+ ),
+ },
+ {
+ name: "pickExecutionDate",
+ label: t("Date"),
+ renderCell: (params) => dayjs(params.pickExecutionDate).format(OUTPUT_DATE_FORMAT),
+ },
+ {
+ name: "id",
+ label: t("Action"),
+ renderCell: (params) => (
+
+ ),
+ },
+ ],
+ [t, handleViewDetail],
+ );
+
+ return (
+
+
+
+
+ items={issues}
+ columns={columns}
+ pagingController={pagingController}
+ setPagingController={setPagingController}
+ totalCount={totalCount}
+ />
+
+ {/* Handle Issue Dialog */}
+
+
+ );
+};
+
+export default PickExecutionIssuesTab;
\ No newline at end of file
diff --git a/src/components/StockTakeManagement/StockTakeManagement.tsx b/src/components/StockTakeManagement/StockTakeManagement.tsx
new file mode 100644
index 0000000..20f59e9
--- /dev/null
+++ b/src/components/StockTakeManagement/StockTakeManagement.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import { Box, Tabs, Tab, Typography } from "@mui/material";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import PickExecutionIssuesTab from "./PickExecutionIssuesTab";
+import StockTakeTab from "./StockTakeTab";
+import InventoryAdjustmentsTab from "./InventoryAdjustmentsTab";
+
+interface TabPanelProps {
+ children?: React.ReactNode;
+ index: number;
+ value: number;
+}
+
+function TabPanel(props: TabPanelProps) {
+ const { children, value, index, ...other } = props;
+
+ return (
+
+ {value === index && {children}}
+
+ );
+}
+
+const StockTakeManagement: React.FC = () => {
+ const { t } = useTranslation(["inventory", "common"]);
+ const [currentTab, setCurrentTab] = useState(0);
+
+ const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
+ setCurrentTab(newValue);
+ };
+
+ return (
+
+
+ {t("Inventory Exception Management")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default StockTakeManagement;
\ No newline at end of file
diff --git a/src/components/StockTakeManagement/StockTakeManagementWrapper.tsx b/src/components/StockTakeManagement/StockTakeManagementWrapper.tsx
new file mode 100644
index 0000000..d306255
--- /dev/null
+++ b/src/components/StockTakeManagement/StockTakeManagementWrapper.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import GeneralLoading from "../General/GeneralLoading";
+import StockTakeManagement from "./StockTakeManagement";
+
+interface SubComponents {
+ Loading: typeof GeneralLoading;
+}
+
+const StockTakeManagementWrapper: React.FC & SubComponents = async () => {
+ return ;
+};
+
+StockTakeManagementWrapper.Loading = GeneralLoading;
+
+export default StockTakeManagementWrapper;
\ No newline at end of file
diff --git a/src/components/StockTakeManagement/StockTakeTab.tsx b/src/components/StockTakeManagement/StockTakeTab.tsx
new file mode 100644
index 0000000..55ec814
--- /dev/null
+++ b/src/components/StockTakeManagement/StockTakeTab.tsx
@@ -0,0 +1,432 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography,
+ Paper,
+ Alert,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+} from "@mui/material";
+import { useState, useMemo, useCallback } from "react";
+import { useTranslation } from "react-i18next";
+import SearchBox, { Criterion } from "../SearchBox";
+import SearchResults, { Column } from "../SearchResults";
+import { defaultPagingController } from "../SearchResults/SearchResults";
+
+// Fake data types
+interface Floor {
+ id: number;
+ code: string;
+ name: string;
+ warehouseCode: string;
+ warehouseName: string;
+}
+
+interface Zone {
+ id: number;
+ floorId: number;
+ code: string;
+ name: string;
+ description: string;
+}
+
+interface InventoryLotLineForStockTake {
+ id: number;
+ zoneId: number;
+ itemCode: string;
+ itemName: string;
+ lotNo: string;
+ location: string;
+ systemQty: number;
+ countedQty?: number;
+ variance?: number;
+ uom: string;
+}
+
+// Fake data
+const fakeFloors: Floor[] = [
+ { id: 1, code: "F1", name: "1st Floor", warehouseCode: "WH001", warehouseName: "Main Warehouse" },
+ { id: 2, code: "F2", name: "2nd Floor", warehouseCode: "WH001", warehouseName: "Main Warehouse" },
+ { id: 3, code: "F3", name: "3rd Floor", warehouseCode: "WH001", warehouseName: "Main Warehouse" },
+];
+
+const fakeZones: Zone[] = [
+ { id: 1, floorId: 1, code: "Z-A", name: "Zone A", description: "Row 1-5" },
+ { id: 2, floorId: 1, code: "Z-B", name: "Zone B", description: "Row 6-10" },
+ { id: 3, floorId: 2, code: "Z-C", name: "Zone C", description: "Row 1-5" },
+ { id: 4, floorId: 2, code: "Z-D", name: "Zone D", description: "Row 6-10" },
+ { id: 5, floorId: 3, code: "Z-E", name: "Zone E", description: "Row 1-5" },
+];
+
+const fakeLots: InventoryLotLineForStockTake[] = [
+ { id: 1, zoneId: 1, itemCode: "M001", itemName: "Material A", lotNo: "LOT-2024-001", location: "A-01-01", systemQty: 100, uom: "PCS" },
+ { id: 2, zoneId: 1, itemCode: "M002", itemName: "Material B", lotNo: "LOT-2024-002", location: "A-01-02", systemQty: 50, uom: "PCS" },
+ { id: 3, zoneId: 1, itemCode: "M003", itemName: "Material C", lotNo: "LOT-2024-003", location: "A-01-03", systemQty: 75, uom: "KG" },
+ { id: 4, zoneId: 2, itemCode: "M004", itemName: "Material D", lotNo: "LOT-2024-004", location: "B-01-01", systemQty: 200, uom: "PCS" },
+ { id: 5, zoneId: 2, itemCode: "M005", itemName: "Material E", lotNo: "LOT-2024-005", location: "B-01-02", systemQty: 150, uom: "KG" },
+];
+
+type FloorSearchQuery = {
+ floorCode: string;
+ floorName: string;
+ warehouseCode: string;
+};
+
+type FloorSearchParamNames = keyof FloorSearchQuery;
+
+const StockTakeTab: React.FC = () => {
+ const { t } = useTranslation(["inventory"]);
+
+ // Search states for floors
+ const defaultFloorInputs = useMemo(() => ({
+ floorCode: "",
+ floorName: "",
+ warehouseCode: "",
+ }), []);
+ const [floorInputs, setFloorInputs] = useState>(defaultFloorInputs);
+
+ // Selection states
+ const [selectedFloor, setSelectedFloor] = useState(null);
+ const [selectedZone, setSelectedZone] = useState(null);
+
+ // Paging controllers
+ const [floorsPagingController, setFloorsPagingController] = useState(defaultPagingController);
+ const [zonesPagingController, setZonesPagingController] = useState(defaultPagingController);
+ const [lotsPagingController, setLotsPagingController] = useState(defaultPagingController);
+
+ // Stock take dialog
+ const [stockTakeDialogOpen, setStockTakeDialogOpen] = useState(false);
+ const [selectedLot, setSelectedLot] = useState(null);
+ const [countedQty, setCountedQty] = useState(0);
+ const [remark, setRemark] = useState("");
+
+ // Filtered data
+ const filteredFloors = useMemo(() => {
+ return fakeFloors.filter(floor => {
+ if (floorInputs.floorCode && !floor.code.toLowerCase().includes(floorInputs.floorCode.toLowerCase())) {
+ return false;
+ }
+ if (floorInputs.floorName && !floor.name.toLowerCase().includes(floorInputs.floorName.toLowerCase())) {
+ return false;
+ }
+ if (floorInputs.warehouseCode && !floor.warehouseCode.toLowerCase().includes(floorInputs.warehouseCode.toLowerCase())) {
+ return false;
+ }
+ return true;
+ });
+ }, [floorInputs]);
+
+ const filteredZones = useMemo(() => {
+ if (!selectedFloor) return [];
+ return fakeZones.filter(zone => zone.floorId === selectedFloor.id);
+ }, [selectedFloor]);
+
+ const filteredLots = useMemo(() => {
+ if (!selectedZone) return [];
+ return fakeLots.filter(lot => lot.zoneId === selectedZone.id);
+ }, [selectedZone]);
+
+ // Search criteria
+ const floorSearchCriteria: Criterion[] = useMemo(
+ () => [
+ { label: t("Floor Code"), paramName: "floorCode", type: "text" },
+ { label: t("Floor Name"), paramName: "floorName", type: "text" },
+ { label: t("Warehouse Code"), paramName: "warehouseCode", type: "text" },
+ ],
+ [t],
+ );
+
+ // Handlers
+ const handleFloorSearch = useCallback((query: Record) => {
+ setFloorInputs(() => query);
+ setFloorsPagingController(() => defaultPagingController);
+ }, []);
+
+ const handleFloorReset = useCallback(() => {
+ setFloorInputs(() => defaultFloorInputs);
+ setFloorsPagingController(() => defaultPagingController);
+ setSelectedFloor(null);
+ setSelectedZone(null);
+ }, [defaultFloorInputs]);
+
+ const handleFloorClick = useCallback((floor: Floor) => {
+ setSelectedFloor(floor);
+ setSelectedZone(null);
+ setZonesPagingController(() => defaultPagingController);
+ setLotsPagingController(() => defaultPagingController);
+ }, []);
+
+ const handleZoneClick = useCallback((zone: Zone) => {
+ setSelectedZone(zone);
+ setLotsPagingController(() => defaultPagingController);
+ }, []);
+
+ const handleStockTakeClick = useCallback((lot: InventoryLotLineForStockTake) => {
+ setSelectedLot(lot);
+ setCountedQty(lot.countedQty || lot.systemQty);
+ setRemark("");
+ setStockTakeDialogOpen(true);
+ }, []);
+
+ const handleStockTakeSubmit = useCallback(() => {
+ if (!selectedLot) return;
+
+ const variance = countedQty - selectedLot.systemQty;
+
+ // Here you would call the API to submit stock take
+ console.log("Stock Take Submitted:", {
+ lotId: selectedLot.id,
+ systemQty: selectedLot.systemQty,
+ countedQty: countedQty,
+ variance: variance,
+ remark: remark,
+ });
+
+ alert(`${t("Stock take recorded successfully!")}\n${t("Variance")}: ${variance > 0 ? '+' : ''}${variance}`);
+
+ // Update the lot with counted qty (in real app, this would come from backend)
+ selectedLot.countedQty = countedQty;
+ selectedLot.variance = variance;
+
+ setStockTakeDialogOpen(false);
+ setSelectedLot(null);
+ }, [selectedLot, countedQty, remark, t]);
+
+ const handleDialogClose = useCallback(() => {
+ setStockTakeDialogOpen(false);
+ setSelectedLot(null);
+ }, []);
+
+ // Floor columns
+ const floorColumns: Column[] = useMemo(
+ () => [
+ { name: "code", label: t("Floor Code") },
+ { name: "name", label: t("Floor Name") },
+ {
+ name: "warehouseCode",
+ label: t("Warehouse"),
+ renderCell: (params) => `${params.warehouseCode} - ${params.warehouseName}`,
+ },
+ ],
+ [t],
+ );
+
+ // Zone columns
+ const zoneColumns: Column[] = useMemo(
+ () => [
+ { name: "code", label: t("Zone Code") },
+ { name: "name", label: t("Zone Name") },
+ { name: "description", label: t("Description") },
+ ],
+ [t],
+ );
+
+ // Lot columns
+ const lotColumns: Column[] = useMemo(
+ () => [
+ { name: "itemCode", label: t("Item Code") },
+ { name: "itemName", label: t("Item Name") },
+ { name: "lotNo", label: t("Lot No") },
+ { name: "location", label: t("Location") },
+ { name: "systemQty", label: t("System Qty"), align: "right", type: "integer" },
+ {
+ name: "countedQty",
+ label: t("Counted Qty"),
+ align: "right",
+ renderCell: (params) => params.countedQty || "-",
+ },
+ {
+ name: "variance",
+ label: t("Variance"),
+ align: "right",
+ renderCell: (params) => {
+ if (params.variance === undefined) return "-";
+ const variance = params.variance;
+ return (
+ 0 ? "success.main" : "error.main",
+ fontWeight: variance !== 0 ? "bold" : "normal",
+ }}
+ >
+ {variance > 0 ? `+${variance}` : variance}
+
+ );
+ },
+ },
+ { name: "uom", label: t("UOM") },
+ {
+ name: "id",
+ label: t("Action"),
+ renderCell: (params) => (
+
+ ),
+ },
+ ],
+ [t, handleStockTakeClick],
+ );
+
+ return (
+
+
+ {t("This is a demo with fake data. API integration pending.")}
+
+
+ {/* Step 1: Select Floor */}
+
+ {t("Step 1: Select Floor")}
+
+
+
+ items={filteredFloors}
+ columns={floorColumns}
+ pagingController={floorsPagingController}
+ setPagingController={setFloorsPagingController}
+ totalCount={filteredFloors.length}
+ onRowClick={handleFloorClick}
+ />
+
+ {/* Step 2: Select Zone */}
+ {selectedFloor && (
+ <>
+
+ {t("Step 2: Select Zone")} - {selectedFloor.name}
+
+
+ items={filteredZones}
+ columns={zoneColumns}
+ pagingController={zonesPagingController}
+ setPagingController={setZonesPagingController}
+ totalCount={filteredZones.length}
+ onRowClick={handleZoneClick}
+ />
+ >
+ )}
+
+ {/* Step 3: Stock Take */}
+ {selectedZone && (
+ <>
+
+ {t("Step 3: Perform Stock Take")} - {selectedZone.name}
+
+
+ items={filteredLots}
+ columns={lotColumns}
+ pagingController={lotsPagingController}
+ setPagingController={setLotsPagingController}
+ totalCount={filteredLots.length}
+ />
+ >
+ )}
+
+ {/* Stock Take Dialog */}
+
+
+ );
+};
+
+export default StockTakeTab;
\ No newline at end of file
diff --git a/src/components/StockTakeManagement/index.ts b/src/components/StockTakeManagement/index.ts
new file mode 100644
index 0000000..bcdb90f
--- /dev/null
+++ b/src/components/StockTakeManagement/index.ts
@@ -0,0 +1 @@
+export { default } from "./StockTakeManagementWrapper";