浏览代码

update

master
CANCERYS\kw093 2 个月前
父节点
当前提交
538dae013a
共有 13 个文件被更改,包括 1498 次插入92 次删除
  1. +19
    -0
      src/app/(main)/stocktakemanagement/page.tsx
  2. +24
    -0
      src/app/api/do/actions.tsx
  3. +4
    -1
      src/app/api/pickOrder/actions.ts
  4. +2
    -1
      src/components/DoDetail/DoDetail.tsx
  5. +127
    -90
      src/components/DoSearch/DoSearch.tsx
  6. +4
    -0
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  7. +5
    -0
      src/components/NavigationContent/NavigationContent.tsx
  8. +339
    -0
      src/components/StockTakeManagement/InventoryAdjustmentsTab.tsx
  9. +457
    -0
      src/components/StockTakeManagement/PickExecutionIssuesTab.tsx
  10. +69
    -0
      src/components/StockTakeManagement/StockTakeManagement.tsx
  11. +15
    -0
      src/components/StockTakeManagement/StockTakeManagementWrapper.tsx
  12. +432
    -0
      src/components/StockTakeManagement/StockTakeTab.tsx
  13. +1
    -0
      src/components/StockTakeManagement/index.ts

+ 19
- 0
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 (
<I18nProvider namespaces={["inventory"]}>
<Suspense fallback={<StockTakeManagementWrapper.Loading />}>
<StockTakeManagementWrapper />
</Suspense>
</I18nProvider>
);
}

+ 24
- 0
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<AssignByStoreResponse>(`${BASE_API_URL}/doPickOrder/assign-by-store`,
{


+ 4
- 1
src/app/api/pickOrder/actions.ts 查看文件

@@ -400,8 +400,11 @@ export const updatePickExecutionIssueStatus = async (
return result;
};
export async function fetchStoreLaneSummary(storeId: string): Promise<StoreLaneSummary> {
// ✅ 硬编码测试日期 - 改成你想测试的日期
const testDate = "2025-10-16"; // 或者 "2025-10-16", "2025-10-17" 等
const response = await serverFetchJson<StoreLaneSummary>(
`${BASE_API_URL}/doPickOrder/summary-by-store?storeId=${encodeURIComponent(storeId)}`,
`${BASE_API_URL}/doPickOrder/summary-by-store?storeId=${encodeURIComponent(storeId)}&requiredDate=${testDate}`,
{
method: "GET",
}


+ 2
- 1
src/components/DoDetail/DoDetail.tsx 查看文件

@@ -33,7 +33,8 @@ const DoDetail: React.FC<Props> = ({
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<DoDetailType>({
defaultValues: defaultValues
})


+ 127
- 90
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<string, any>;
searchQuery?: Record<string, any>;
@@ -60,6 +61,11 @@ const DoSearch: React.FC<Props> = ({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<NodeJS.Timeout | null>(null);
const [rowSelectionModel, setRowSelectionModel] =
useState<GridRowSelectionModel>([]);

@@ -293,15 +299,14 @@ const DoSearch: React.FC<Props> = ({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<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear
estArrStartDate,
estArrEndDate
);
setSearchAllDos(data);
setHasSearched(true);
setHasResults(data.length > 0);
@@ -340,94 +345,126 @@ const DoSearch: React.FC<Props> = ({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: `
<div>
<p>${t("Selected items on current page")}: ${selectedIds.length}</p>
<p>${t("Total search results")}: ${searchAllDos.length}</p>
<hr>
<p><strong>${t("Choose release option")}:</strong></p>
</div>
`,
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() + `</p>`+
t("Selected Item(s): ") + extractedItemsCount.toString() + `</p>`,
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: `
<div style="text-align:left">
<div id="br-total">${t("Total")}: 0</div>
<div id="br-finished">${t("Finished")}: 0</div>
<div style="margin-top:8px;height:8px;background:#eee;border-radius:4px;">
<div id="br-bar" style="height:8px;width:0%;background:#8dba00;border-radius:4px;"></div>
</div>
</div>
`,
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 (


+ 4
- 0
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[];


+ 5
- 0
src/components/NavigationContent/NavigationContent.tsx 查看文件

@@ -79,6 +79,11 @@ const NavigationContent: React.FC = () => {
label: "View item In-out And inventory Ledger",
path: "/inventory",
},
{
icon: <RequestQuote />,
label: "Stock Take Management",
path: "/stocktakemanagement",
},
{
icon: <RequestQuote />,
label: "Put Away Scan",


+ 339
- 0
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<InventoryAdjustment[]>(fakeAdjustments);
const [typeFilter, setTypeFilter] = useState<string>("all");
const [statusFilter, setStatusFilter] = useState<string>("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<HTMLInputElement>) => {
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 (
<Box>
<Alert severity="info" sx={{ mb: 3 }}>
{t("This is a demo with fake data. API integration pending.")}
</Alert>

{/* Filters */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Stack direction="row" spacing={2} alignItems="center">
<FormControl sx={{ minWidth: 150 }}>
<InputLabel>{t("Type")}</InputLabel>
<Select
value={typeFilter}
label={t("Type")}
onChange={(e) => setTypeFilter(e.target.value)}
>
<MenuItem value="all">{t("All")}</MenuItem>
<MenuItem value="stock_take">{t("Stock Take")}</MenuItem>
<MenuItem value="manual">{t("Manual")}</MenuItem>
<MenuItem value="damage">{t("Damage")}</MenuItem>
<MenuItem value="return">{t("Return")}</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 150 }}>
<InputLabel>{t("Status")}</InputLabel>
<Select
value={statusFilter}
label={t("Status")}
onChange={(e) => setStatusFilter(e.target.value)}
>
<MenuItem value="all">{t("All")}</MenuItem>
<MenuItem value="approved">{t("Approved")}</MenuItem>
<MenuItem value="pending">{t("Pending")}</MenuItem>
<MenuItem value="rejected">{t("Rejected")}</MenuItem>
</Select>
</FormControl>

<TextField
label={t("Search")}
variant="outlined"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder={t("Adjustment No, Item, Lot...")}
sx={{ flexGrow: 1 }}
/>
</Stack>

<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
{t("Total Adjustments")}: {filteredAdjustments.length}
</Typography>
</CardContent>
</Card>

{/* Table */}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Adjustment No")}</TableCell>
<TableCell>{t("Type")}</TableCell>
<TableCell>{t("Item")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
<TableCell>{t("Location")}</TableCell>
<TableCell align="right">{t("Before Qty")}</TableCell>
<TableCell align="right">{t("After Qty")}</TableCell>
<TableCell align="right">{t("Adjustment")}</TableCell>
<TableCell>{t("Reason")}</TableCell>
<TableCell>{t("Adjusted By")}</TableCell>
<TableCell>{t("Date")}</TableCell>
<TableCell>{t("Status")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedAdjustments.length === 0 ? (
<TableRow>
<TableCell colSpan={12} align="center">
<Typography variant="body2" color="text.secondary" sx={{ py: 3 }}>
{t("No adjustments found")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedAdjustments.map((adj) => (
<TableRow key={adj.id} hover>
<TableCell>{adj.adjustmentNo}</TableCell>
<TableCell>
<Chip
label={t(adj.type)}
size="small"
color={getAdjustmentTypeColor(adj.type)}
/>
</TableCell>
<TableCell>
<Typography variant="body2">{adj.itemCode}</Typography>
<Typography variant="caption" color="text.secondary">
{adj.itemName}
</Typography>
</TableCell>
<TableCell>{adj.lotNo}</TableCell>
<TableCell>{adj.location}</TableCell>
<TableCell align="right">{adj.beforeQty}</TableCell>
<TableCell align="right">{adj.afterQty}</TableCell>
<TableCell
align="right"
sx={{
color: adj.adjustmentQty >= 0 ? "success.main" : "error.main",
fontWeight: "bold",
}}
>
{adj.adjustmentQty > 0 ? `+${adj.adjustmentQty}` : adj.adjustmentQty}
</TableCell>
<TableCell>{adj.reason}</TableCell>
<TableCell>{adj.adjustedBy}</TableCell>
<TableCell>{dayjs(adj.adjustedDate).format(OUTPUT_DATE_FORMAT)}</TableCell>
<TableCell>
<Chip label={t(adj.status)} size="small" color={getStatusColor(adj.status)} />
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>

<TablePagination
component="div"
count={filteredAdjustments.length}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
rowsPerPageOptions={[5, 10, 25, 50]}
/>
</Box>
);
};

export default InventoryAdjustmentsTab;

+ 457
- 0
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<PickExecutionIssue[]>([]);
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<Record<SearchParamNames, string>>(defaultInputs);

// Handle dialog
const [selectedIssue, setSelectedIssue] = useState<PickExecutionIssue | null>(null);
const [handleDialogOpen, setHandleDialogOpen] = useState(false);
const [handleRemark, setHandleRemark] = useState("");

// Search criteria
const searchCriteria: Criterion<SearchParamNames>[] = 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<SearchParamNames, string>,
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<SearchParamNames, string>) => {
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<PickExecutionIssue>[] = useMemo(
() => [
{ name: "issueNo", label: t("Issue No") },
{ name: "pickOrderCode", label: t("Pick Order") },
{
name: "itemCode",
label: t("Item"),
renderCell: (params) => (
<Box>
<Typography variant="body2">{params.itemCode}</Typography>
<Typography variant="caption" color="text.secondary">
{params.itemDescription}
</Typography>
</Box>
),
},
{
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) => (
<Chip
label={t(params.issueCategory)}
size="small"
color={getIssueCategoryColor(params.issueCategory)}
/>
),
},
{
name: "handleStatus",
label: t("Status"),
renderCell: (params) => (
<Chip
label={t(params.handleStatus)}
size="small"
color={
params.handleStatus.toLowerCase() === "pending"
? "warning"
: "success"
}
/>
),
},
{
name: "pickExecutionDate",
label: t("Date"),
renderCell: (params) => dayjs(params.pickExecutionDate).format(OUTPUT_DATE_FORMAT),
},
{
name: "id",
label: t("Action"),
renderCell: (params) => (
<Button size="small" onClick={() => handleViewDetail(params)}>
{t("Detail")}
</Button>
),
},
],
[t, handleViewDetail],
);

return (
<Box>
<SearchBox
criteria={searchCriteria}
onSearch={onSearch}
onReset={onReset}
/>

<SearchResults<PickExecutionIssue>
items={issues}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
totalCount={totalCount}
/>

{/* Handle Issue Dialog */}
<Dialog open={handleDialogOpen} onClose={handleCloseDialog} maxWidth="md" fullWidth>
<DialogTitle>{t("Issue Detail")}</DialogTitle>
<DialogContent>
{selectedIssue && (
<Stack spacing={2} sx={{ mt: 2 }}>
<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Issue No")}
</Typography>
<Typography variant="body1">{selectedIssue.issueNo}</Typography>
</Box>

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Pick Order")}
</Typography>
<Typography variant="body1">{selectedIssue.pickOrderCode}</Typography>
</Box>

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Item")}
</Typography>
<Typography variant="body1">
{selectedIssue.itemCode} - {selectedIssue.itemDescription}
</Typography>
</Box>

{selectedIssue.lotNo && (
<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Lot No")}
</Typography>
<Typography variant="body1">{selectedIssue.lotNo}</Typography>
</Box>
)}

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Location")}
</Typography>
<Typography variant="body1">{selectedIssue.storeLocation || "-"}</Typography>
</Box>

<Stack direction="row" spacing={3}>
<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Required Qty")}
</Typography>
<Typography variant="body1">{selectedIssue.requiredQty}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Actual Qty")}
</Typography>
<Typography variant="body1">{selectedIssue.actualPickQty || 0}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Miss Qty")}
</Typography>
<Typography variant="body1" color="error">
{selectedIssue.missQty || 0}
</Typography>
</Box>
</Stack>

{selectedIssue.issueRemark && (
<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Issue Remark")}
</Typography>
<Typography variant="body1">{selectedIssue.issueRemark}</Typography>
</Box>
)}

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Picker")}
</Typography>
<Typography variant="body1">{selectedIssue.pickerName || "-"}</Typography>
</Box>

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Status")}
</Typography>
<Chip
label={t(selectedIssue.handleStatus)}
size="small"
color={
selectedIssue.handleStatus.toLowerCase() === "pending"
? "warning"
: "success"
}
/>
</Box>

{selectedIssue.handleStatus.toLowerCase() === "pending" && (
<TextField
label={t("Handle Remark")}
multiline
rows={3}
value={handleRemark}
onChange={(e) => setHandleRemark(e.target.value)}
fullWidth
/>
)}
</Stack>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>{t("Close")}</Button>
{selectedIssue && selectedIssue.handleStatus.toLowerCase() === "pending" && (
<Button onClick={handleMarkAsResolved} variant="contained" color="primary">
{t("Mark as Resolved")}
</Button>
)}
</DialogActions>
</Dialog>
</Box>
);
};

export default PickExecutionIssuesTab;

+ 69
- 0
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 (
<div
role="tabpanel"
hidden={value !== index}
id={`inventory-management-tabpanel-${index}`}
aria-labelledby={`inventory-management-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
</div>
);
}

const StockTakeManagement: React.FC = () => {
const { t } = useTranslation(["inventory", "common"]);
const [currentTab, setCurrentTab] = useState(0);

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setCurrentTab(newValue);
};

return (
<Box sx={{ width: "100%" }}>
<Typography variant="h4" sx={{ mb: 3 }}>
{t("Inventory Exception Management")}
</Typography>

<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label={t("Pick Execution Issues")} />
<Tab label={t("Stock Take")} />
<Tab label={t("Inventory Adjustments")} />
</Tabs>
</Box>

<TabPanel value={currentTab} index={0}>
<PickExecutionIssuesTab />
</TabPanel>

<TabPanel value={currentTab} index={1}>
<StockTakeTab />
</TabPanel>

<TabPanel value={currentTab} index={2}>
<InventoryAdjustmentsTab />
</TabPanel>
</Box>
);
};

export default StockTakeManagement;

+ 15
- 0
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 <StockTakeManagement />;
};

StockTakeManagementWrapper.Loading = GeneralLoading;

export default StockTakeManagementWrapper;

+ 432
- 0
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<Record<FloorSearchParamNames, string>>(defaultFloorInputs);

// Selection states
const [selectedFloor, setSelectedFloor] = useState<Floor | null>(null);
const [selectedZone, setSelectedZone] = useState<Zone | null>(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<InventoryLotLineForStockTake | null>(null);
const [countedQty, setCountedQty] = useState<number>(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<FloorSearchParamNames>[] = 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<FloorSearchParamNames, string>) => {
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<Floor>[] = 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<Zone>[] = useMemo(
() => [
{ name: "code", label: t("Zone Code") },
{ name: "name", label: t("Zone Name") },
{ name: "description", label: t("Description") },
],
[t],
);

// Lot columns
const lotColumns: Column<InventoryLotLineForStockTake>[] = 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 (
<Typography
variant="body2"
sx={{
color: variance === 0 ? "inherit" : variance > 0 ? "success.main" : "error.main",
fontWeight: variance !== 0 ? "bold" : "normal",
}}
>
{variance > 0 ? `+${variance}` : variance}
</Typography>
);
},
},
{ name: "uom", label: t("UOM") },
{
name: "id",
label: t("Action"),
renderCell: (params) => (
<Button size="small" variant="outlined" onClick={() => handleStockTakeClick(params)}>
{t("Stock Take")}
</Button>
),
},
],
[t, handleStockTakeClick],
);

return (
<Box>
<Alert severity="info" sx={{ mb: 3 }}>
{t("This is a demo with fake data. API integration pending.")}
</Alert>

{/* Step 1: Select Floor */}
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Step 1: Select Floor")}
</Typography>
<SearchBox
criteria={floorSearchCriteria}
onSearch={handleFloorSearch}
onReset={handleFloorReset}
/>
<SearchResults<Floor>
items={filteredFloors}
columns={floorColumns}
pagingController={floorsPagingController}
setPagingController={setFloorsPagingController}
totalCount={filteredFloors.length}
onRowClick={handleFloorClick}
/>

{/* Step 2: Select Zone */}
{selectedFloor && (
<>
<Typography variant="h6" sx={{ mt: 4, mb: 2 }}>
{t("Step 2: Select Zone")} - {selectedFloor.name}
</Typography>
<SearchResults<Zone>
items={filteredZones}
columns={zoneColumns}
pagingController={zonesPagingController}
setPagingController={setZonesPagingController}
totalCount={filteredZones.length}
onRowClick={handleZoneClick}
/>
</>
)}

{/* Step 3: Stock Take */}
{selectedZone && (
<>
<Typography variant="h6" sx={{ mt: 4, mb: 2 }}>
{t("Step 3: Perform Stock Take")} - {selectedZone.name}
</Typography>
<SearchResults<InventoryLotLineForStockTake>
items={filteredLots}
columns={lotColumns}
pagingController={lotsPagingController}
setPagingController={setLotsPagingController}
totalCount={filteredLots.length}
/>
</>
)}

{/* Stock Take Dialog */}
<Dialog open={stockTakeDialogOpen} onClose={handleDialogClose} maxWidth="sm" fullWidth>
<DialogTitle>{t("Stock Take")}</DialogTitle>
<DialogContent>
{selectedLot && (
<Stack spacing={3} sx={{ mt: 2 }}>
<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Item")}
</Typography>
<Typography variant="body1">
{selectedLot.itemCode} - {selectedLot.itemName}
</Typography>
</Box>

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Lot No")}
</Typography>
<Typography variant="body1">{selectedLot.lotNo}</Typography>
</Box>

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Location")}
</Typography>
<Typography variant="body1">{selectedLot.location}</Typography>
</Box>

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("System Qty")}
</Typography>
<Typography variant="body1">
{selectedLot.systemQty} {selectedLot.uom}
</Typography>
</Box>

<TextField
label={t("Counted Qty")}
type="number"
value={countedQty}
onChange={(e) => setCountedQty(parseInt(e.target.value) || 0)}
fullWidth
autoFocus
/>

<Box>
<Typography variant="subtitle2" color="text.secondary">
{t("Variance")}
</Typography>
<Typography
variant="h6"
sx={{
color:
countedQty - selectedLot.systemQty === 0
? "inherit"
: countedQty - selectedLot.systemQty > 0
? "success.main"
: "error.main",
}}
>
{countedQty - selectedLot.systemQty > 0 ? "+" : ""}
{countedQty - selectedLot.systemQty}
</Typography>
</Box>

<TextField
label={t("Remark")}
multiline
rows={3}
value={remark}
onChange={(e) => setRemark(e.target.value)}
fullWidth
/>
</Stack>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleDialogClose}>{t("Cancel")}</Button>
<Button onClick={handleStockTakeSubmit} variant="contained" color="primary">
{t("Submit Stock Take")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};

export default StockTakeTab;

+ 1
- 0
src/components/StockTakeManagement/index.ts 查看文件

@@ -0,0 +1 @@
export { default } from "./StockTakeManagementWrapper";

正在加载...
取消
保存