Kaynağa Gözat

Merge remote-tracking branch 'origin/MergeProblem1' into MergeProblem1

# Conflicts:
#	src/i18n/zh/jo.json
reset-do-picking-order
B.E.N.S.O.N 2 hafta önce
ebeveyn
işleme
5c243b376b
50 değiştirilmiş dosya ile 1790 ekleme ve 652 silme
  1. +4
    -4
      src/app/(main)/ps/page.tsx
  2. +22
    -3
      src/app/api/pdf/actions.ts
  3. +1
    -1
      src/app/api/po/actions.ts
  4. +48
    -0
      src/app/api/stockAdjustment/actions.ts
  5. +2
    -1
      src/app/api/stockIn/actions.ts
  6. +3
    -0
      src/app/api/stockIssue/actions.ts
  7. +11
    -4
      src/app/api/stockTake/actions.ts
  8. +37
    -0
      src/app/global.css
  9. +9
    -1
      src/app/layout.tsx
  10. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  11. +1
    -1
      src/components/General/LoadingComponent.tsx
  12. +1
    -1
      src/components/InputDataGrid/InputDataGrid.tsx
  13. +636
    -8
      src/components/InventorySearch/InventoryLotLineTable.tsx
  14. +7
    -1
      src/components/InventorySearch/InventorySearch.tsx
  15. +6
    -2
      src/components/InventorySearch/InventorySearchWrapper.tsx
  16. +19
    -13
      src/components/JoSearch/JoSearch.tsx
  17. +2
    -0
      src/components/Jodetail/FInishedJobOrderRecord.tsx
  18. +2
    -0
      src/components/Jodetail/FinishedGoodSearchWrapper.tsx
  19. +26
    -21
      src/components/Jodetail/JobPickExecutionForm.tsx
  20. +13
    -42
      src/components/Jodetail/JobPickExecutionsecondscan.tsx
  21. +1
    -1
      src/components/Jodetail/JobmatchForm.tsx
  22. +4
    -2
      src/components/Jodetail/JodetailSearch.tsx
  23. +2
    -4
      src/components/Jodetail/completeJobOrderRecord.tsx
  24. +1
    -1
      src/components/Jodetail/newJobPickExecution.tsx
  25. +29
    -14
      src/components/LoginPage/LoginPage.tsx
  26. +59
    -22
      src/components/Logo/Logo.tsx
  27. +2
    -2
      src/components/NavigationContent/NavigationContent.tsx
  28. +1
    -1
      src/components/PoDetail/PoInputGrid.tsx
  29. +11
    -1
      src/components/ProductionProcess/BagConsumptionForm.tsx
  30. +1
    -1
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  31. +5
    -4
      src/components/ProductionProcess/ProductionProcessList.tsx
  32. +9
    -10
      src/components/PutAwayScan/PutAwayModal.tsx
  33. +12
    -1
      src/components/Qc/QcForm.tsx
  34. +26
    -16
      src/components/Qc/QcStockInModal.tsx
  35. +1
    -1
      src/components/SearchResults/SearchResults.tsx
  36. +13
    -0
      src/components/StockIssue/SearchPage.tsx
  37. +35
    -4
      src/components/StockIssue/SubmitIssueForm.tsx
  38. +65
    -61
      src/components/StockRecord/SearchPage.tsx
  39. +1
    -1
      src/components/StockRecord/index.tsx
  40. +112
    -66
      src/components/StockTakeManagement/ApproverStockTake.tsx
  41. +35
    -2
      src/components/StockTakeManagement/PickerCardList.tsx
  42. +259
    -202
      src/components/StockTakeManagement/PickerReStockTake.tsx
  43. +155
    -123
      src/components/StockTakeManagement/PickerStockTake.tsx
  44. +4
    -0
      src/components/StyledDataGrid/StyledDataGrid.tsx
  45. +32
    -3
      src/i18n/zh/common.json
  46. +38
    -1
      src/i18n/zh/inventory.json
  47. +19
    -4
      src/i18n/zh/jo.json
  48. +1
    -0
      src/i18n/zh/pickOrder.json
  49. +1
    -1
      src/i18n/zh/qcItemAll.json
  50. +5
    -0
      tailwind.config.js

+ 4
- 4
src/app/(main)/ps/page.tsx Dosyayı Görüntüle

@@ -287,10 +287,10 @@ export default function ProductionSchedulePage() {
</div>
</div>

{/* Detail Modal */}
{/* Detail Modal – z-index above sidebar drawer (1200) so they don't overlap on small windows */}
{isDetailOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
className="fixed inset-0 z-[1300] flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="detail-title"
@@ -416,7 +416,7 @@ export default function ProductionSchedulePage() {
{/* Forecast Dialog */}
{isForecastDialogOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
className="fixed inset-0 z-[1300] flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
>
@@ -503,7 +503,7 @@ export default function ProductionSchedulePage() {
{/* Export Dialog */}
{isExportDialogOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
className="fixed inset-0 z-[1300] flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
>


+ 22
- 3
src/app/api/pdf/actions.ts Dosyayı Görüntüle

@@ -2,7 +2,7 @@

// import { serverFetchBlob } from "@/app/utils/fetchUtil";
// import { BASE_API_URL } from "@/config/api";
import { serverFetchBlob } from "../../utils/fetchUtil";
import { serverFetchBlob, serverFetchWithNoContent } from "../../utils/fetchUtil";
import { BASE_API_URL } from "../../../config/api";

export interface FileResponse {
@@ -12,7 +12,7 @@ export interface FileResponse {

export const fetchPoQrcode = async (data: any) => {
const reportBlob = await serverFetchBlob<FileResponse>(
`${BASE_API_URL}/stockInLine/print-label`,
`${BASE_API_URL}/stockInLine/download-label`,
{
method: "POST",
body: JSON.stringify(data),
@@ -27,7 +27,7 @@ export interface LotLineToQrcode {
}
export const fetchQrCodeByLotLineId = async (data: LotLineToQrcode) => {
const reportBlob = await serverFetchBlob<FileResponse>(
`${BASE_API_URL}/inventoryLotLine/print-label`,
`${BASE_API_URL}/inventoryLotLine/download-label`,
{
method: "POST",
body: JSON.stringify(data),
@@ -37,3 +37,22 @@ export const fetchQrCodeByLotLineId = async (data: LotLineToQrcode) => {

return reportBlob;
}

export interface PrintLabelForInventoryLotLineRequest {
inventoryLotLineId: number;
printerId: number;
printQty?: number;
}

export async function printLabelForInventoryLotLine(data: PrintLabelForInventoryLotLineRequest) {
const params = new URLSearchParams();
params.append("inventoryLotLineId", data.inventoryLotLineId.toString());
params.append("printerId", data.printerId.toString());
if (data.printQty != null && data.printQty !== undefined) {
params.append("printQty", data.printQty.toString());
}
return serverFetchWithNoContent(
`${BASE_API_URL}/inventoryLotLine/print-label?${params.toString()}`,
{ method: "GET" }
);
}

+ 1
- 1
src/app/api/po/actions.ts Dosyayı Görüntüle

@@ -250,7 +250,7 @@ export const testing = cache(async (queryParams?: Record<string, any>) => {
// DEPRECIATED
export const printQrCodeForSil = cache(async(data: PrintQrCodeForSilRequest) => {
const params = convertObjToURLSearchParams(data)
return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/printQrCode?${params}`,
return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/print-label?${params}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },


+ 48
- 0
src/app/api/stockAdjustment/actions.ts Dosyayı Görüntüle

@@ -0,0 +1,48 @@
"use server";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { serverFetchJson } from "@/app/utils/fetchUtil";

export interface StockAdjustmentLineRequest {
id: number;
lotNo?: string | null;
adjustedQty: number;
productlotNo?: string | null;
dnNo?: string | null;
isOpeningInventory: boolean;
isNew: boolean;
itemId: number;
itemNo: string;
expiryDate: string;
warehouseId: number;
uom?: string | null;
}

export interface StockAdjustmentRequest {
itemId: number;
originalLines: StockAdjustmentLineRequest[];
currentLines: StockAdjustmentLineRequest[];
}

export interface MessageResponse {
id: number | null;
name: string;
code: string;
type: string;
message: string | null;
errorPosition: string | null;
}

export const submitStockAdjustment = async (data: StockAdjustmentRequest) => {
const result = await serverFetchJson<MessageResponse>(
`${BASE_API_URL}/stockAdjustment/submit`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("inventoryLotLines");
revalidateTag("inventories");
return result;
};

+ 2
- 1
src/app/api/stockIn/actions.ts Dosyayı Görüntüle

@@ -13,6 +13,7 @@ import { Uom } from "../settings/uom";
import { convertObjToURLSearchParams } from "@/app/utils/commonUtil";
// import { BASE_API_URL } from "@/config/api";
import { Result } from "../settings/item";

export interface PostStockInLineResponse<T> {
id: number | null;
name: string;
@@ -232,7 +233,7 @@ export const testing = cache(async (queryParams?: Record<string, any>) => {

export const printQrCodeForSil = cache(async(data: PrintQrCodeForSilRequest) => {
const params = convertObjToURLSearchParams(data)
return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/printQrCode?${params}`,
return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/print-label?${params}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },


+ 3
- 0
src/app/api/stockIssue/actions.ts Dosyayı Görüntüle

@@ -25,6 +25,7 @@ export interface StockIssueResult {
handleStatus: string;
handleDate: string | null;
handledBy: number | null;
uomDesc: string | null;
}
export interface ExpiryItemResult {
id: number;
@@ -178,6 +179,8 @@ export async function submitMissItem(issueId: number, handler: number) {
itemDescription: string | null;
storeLocation: string | null;
issues: IssueDetailItem[];
bookQty: number;
uomDesc: string | null;
}
export interface IssueDetailItem {


+ 11
- 4
src/app/api/stockTake/actions.ts Dosyayı Görüntüle

@@ -40,6 +40,7 @@ export interface InventoryLotDetailResponse {
approverQty: number | null;
approverBadQty: number | null;
finalQty: number | null;
bookQty: number | null;
}

export const getInventoryLotDetailsBySection = async (
@@ -207,6 +208,7 @@ export interface BatchSaveApproverStockTakeRecordRequest {
stockTakeId: number;
stockTakeSection: string;
approverId: number;
variancePercentTolerance?: number | null;
}

export interface BatchSaveApproverStockTakeRecordResponse {
@@ -312,7 +314,10 @@ export const getInventoryLotDetailsBySectionNotMatch = async (
);
return response;
}

export interface SearchStockTransactionResult {
records: StockTransactionResponse[];
total: number;
}
export interface SearchStockTransactionRequest {
startDate: string | null;
endDate: string | null;
@@ -345,7 +350,6 @@ export interface StockTransactionListResponse {
}

export const searchStockTransactions = cache(async (request: SearchStockTransactionRequest) => {
// 构建查询字符串
const params = new URLSearchParams();
if (request.itemCode) params.append("itemCode", request.itemCode);
@@ -366,7 +370,10 @@ export const searchStockTransactions = cache(async (request: SearchStockTransact
next: { tags: ["Stock Transaction List"] },
}
);
// 确保返回正确的格式
return response?.records || [];
// 回傳 records 與 total,供分頁正確顯示
return {
records: response?.records || [],
total: response?.total ?? 0,
};
});


+ 37
- 0
src/app/global.css Dosyayı Görüntüle

@@ -29,9 +29,46 @@ body {
overscroll-behavior: none;
}

/* Tablet/mobile: stable layout when virtual keyboard opens */
html {
/* Prefer dynamic viewport height so layout can adapt to keyboard (if browser resizes) */
height: 100%;
/* Base font size: slightly larger for readability */
font-size: 16px;
}
@media (min-width: 640px) {
html {
font-size: 17px;
}
}
@media (min-width: 1024px) {
html {
font-size: 18px;
}
}
body {
min-height: 100%;
min-height: 100dvh;
background-color: var(--background);
color: var(--foreground);
font-size: 1rem;
line-height: 1.6;
}

/* Full-height containers: use dvh so keyboard doesn’t squash the layout when overlay is used */
@media (max-width: 1024px) {
.min-h-screen {
min-height: 100dvh;
}
}

/* Avoid iOS zoom on input focus (keep inputs ≥16px where possible) */
@media (max-width: 1024px) {
input,
select,
textarea {
font-size: max(16px, 1rem);
}
}

.app-search-criteria {


+ 9
- 1
src/app/layout.tsx Dosyayı Görüntüle

@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
// import { detectLanguage } from "@/i18n";
// import ThemeRegistry from "@/theme/ThemeRegistry";
import { detectLanguage } from "../i18n";
@@ -9,6 +9,14 @@ export const metadata: Metadata = {
description: "FPSMS - xxxx Management System",
};

/** Tablet/mobile: virtual keyboard overlays content instead of resizing viewport (avoids "half screen gone"). */
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
interactiveWidget: "overlays-content",
};

export default async function RootLayout({
children,
}: {


+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Dosyayı Görüntüle

@@ -14,6 +14,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/tasks": "Task Template",
"/tasks/create": "Create Task Template",
"/settings/qcItem": "Qc Item",
"/settings/qcItemAll": "QC Item All",
"/settings/qrCodeHandle": "QR Code Handle",
"/settings/rss": "Demand Forecast Setting",
"/settings/equipment": "Equipment",


+ 1
- 1
src/components/General/LoadingComponent.tsx Dosyayı Görüntüle

@@ -8,7 +8,7 @@ export const LoadingComponent: React.FC = () => {
display="flex"
justifyContent="center"
alignItems="center"
// autoheight="true"
>
<CircularProgress />
</Box>


+ 1
- 1
src/components/InputDataGrid/InputDataGrid.tsx Dosyayı Görüntüle

@@ -370,7 +370,7 @@ function InputDataGrid<T, V, E>({
// columns={!checkboxSelection ? _columns : columns}
columns={needActions ? _columns : columns}
editMode="row"
// autoHeight
sx={{
height: "30vh",
"--DataGrid-overlayHeight": "100px",


+ 636
- 8
src/components/InventorySearch/InventoryLotLineTable.tsx Dosyayı Görüntüle

@@ -1,10 +1,13 @@
import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
import SaveIcon from "@mui/icons-material/Save";
import EditIcon from "@mui/icons-material/Edit";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { useTranslation } from "react-i18next";
import { Column } from "../SearchResults";
import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults";
import { arrayToDateString } from "@/app/utils/formatUtil";
import { Box, Card, Grid, IconButton, Modal, TextField, Typography, Button } from "@mui/material";
import { Box, Card, Checkbox, FormControlLabel, Grid, IconButton, Modal, TextField, Typography, Button, Chip } from "@mui/material";
import useUploadContext from "../UploadProvider/useUploadContext";
import { downloadFile } from "@/app/utils/commonUtil";
import { fetchQrCodeByLotLineId, LotLineToQrcode } from "@/app/api/pdf/actions";
@@ -17,6 +20,30 @@ import { WarehouseResult } from "@/app/api/warehouse";
import { fetchWarehouseListClient } from "@/app/api/warehouse/client";
import { createStockTransfer } from "@/app/api/inventory/actions";
import { msg, msgError } from "@/components/Swal/CustomAlerts";
import { PrinterCombo } from "@/app/api/settings/printer";
import { printLabelForInventoryLotLine } from "@/app/api/pdf/actions";
import TuneIcon from "@mui/icons-material/Tune";
import AddIcon from "@mui/icons-material/Add";
import { Table, TableBody, TableCell, TableHead, TableRow } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
import CheckIcon from "@mui/icons-material/Check";
import { submitStockAdjustment, StockAdjustmentLineRequest } from "@/app/api/stockAdjustment/actions";

type AdjustmentEntry = InventoryLotLineResult & {
adjustedQty: number;
originalQty?: number;
productlotNo?: string;
dnNo?: string;
isNew?: boolean;
isOpeningInventory?: boolean;
remarks?: string;
};


interface Props {
inventoryLotLines: InventoryLotLineResult[] | null;
@@ -25,10 +52,17 @@ interface Props {
totalCount: number;
inventory: InventoryResult | null;
onStockTransferSuccess?: () => void | Promise<void>;
printerCombo?: PrinterCombo[];
onStockAdjustmentSuccess?: () => void | Promise<void>;
}

const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory, onStockTransferSuccess }) => {
const InventoryLotLineTable: React.FC<Props> = ({
inventoryLotLines, pagingController, setPagingController, totalCount, inventory,
onStockTransferSuccess, printerCombo = [],
onStockAdjustmentSuccess,
}) => {
const { t } = useTranslation(["inventory"]);
const PRINT_PRINTER_ID_KEY = 'inventoryLotLinePrintPrinterId';
const { setIsUploading } = useUploadContext();
const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false);
const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null);
@@ -37,7 +71,27 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
const [targetLocationInput, setTargetLocationInput] = useState<string>("");
const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0);
const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]);

const [printModalOpen, setPrintModalOpen] = useState(false);
const [lotLineForPrint, setLotLineForPrint] = useState<InventoryLotLineResult | null>(null);
const [printPrinter, setPrintPrinter] = useState<PrinterCombo | null>(null);
const [printQty, setPrintQty] = useState(1);
const [stockAdjustmentModalOpen, setStockAdjustmentModalOpen] = useState(false);
const [pendingRemovalLineId, setPendingRemovalLineId] = useState<number | null>(null);
const [removalReasons, setRemovalReasons] = useState<Record<number, string>>({});
const [addEntryModalOpen, setAddEntryModalOpen] = useState(false);
const [addEntryForm, setAddEntryForm] = useState({
lotNo: '',
qty: 0,
expiryDate: '',
locationId: null as number | null,
locationInput: '',
productlotNo: '',
dnNo: '',
isOpeningInventory: false,
remarks: '',
});
const originalAdjustmentLinesRef = useRef<AdjustmentEntry[]>([]);
const [adjustmentEntries, setAdjustmentEntries] = useState<AdjustmentEntry[]>([]);
useEffect(() => {
if (stockTransferModalOpen) {
fetchWarehouseListClient()
@@ -46,6 +100,14 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
}
}, [stockTransferModalOpen]);

useEffect(() => {
if (addEntryModalOpen) {
fetchWarehouseListClient()
.then(setWarehouses)
.catch(console.error);
}
}, [addEntryModalOpen]);

const availableLotLines = useMemo(
() => (inventoryLotLines ?? []).filter((line) => line.status?.toLowerCase() === "available"),
[inventoryLotLines]
@@ -53,6 +115,182 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
const originalQty = selectedLotLine?.availableQty || 0;
const remainingQty = originalQty - qtyToBeTransferred;

const prevAdjustmentModalOpenRef = useRef(false);

useEffect(() => {
const wasOpen = prevAdjustmentModalOpenRef.current;
prevAdjustmentModalOpenRef.current = stockAdjustmentModalOpen;

if (stockAdjustmentModalOpen && inventory) {
// Only init when we transition to open (modal just opened)
if (!wasOpen) {
const initial = (availableLotLines ?? []).map((line) => ({
...line,
adjustedQty: line.availableQty ?? 0,
originalQty: line.availableQty ?? 0,
remarks: '',
}));
setAdjustmentEntries(initial);
originalAdjustmentLinesRef.current = initial;
}
setPendingRemovalLineId(null);
setRemovalReasons({});
}
}, [stockAdjustmentModalOpen, inventory, availableLotLines]);

const handleAdjustmentReset = useCallback(() => {

setPendingRemovalLineId(null);
setRemovalReasons({});
setAdjustmentEntries(
(availableLotLines ?? []).map((line) => ({
...line,
adjustedQty: line.availableQty ?? 0,
originalQty: line.availableQty ?? 0,
remarks: '',
}))
);
}, [availableLotLines]);

const handleAdjustmentQtyChange = useCallback((lineId: number, value: number) => {
setAdjustmentEntries((prev) =>
prev.map((line) =>
line.id === lineId ? { ...line, adjustedQty: Math.max(0, value) } : line
)
);
}, []);

const handleAdjustmentRemarksChange = useCallback((lineId: number, value: string) => {
setAdjustmentEntries((prev) =>
prev.map((line) =>
line.id === lineId ? { ...line, remarks: value } : line
)
);
}, []);

const handleRemoveAdjustmentLine = useCallback((lineId: number) => {
setAdjustmentEntries((prev) => prev.filter((line) => line.id !== lineId));
}, []);

const handleRemoveClick = useCallback((lineId: number) => {
setPendingRemovalLineId((prev) => (prev === lineId ? null : lineId));
}, []);

const handleRemovalReasonChange = useCallback((lineId: number, value: string) => {
setRemovalReasons((prev) => ({ ...prev, [lineId]: value }));
}, []);

const handleConfirmRemoval = useCallback((lineId: number) => {
setAdjustmentEntries((prev) => prev.filter((line) => line.id !== lineId));
setPendingRemovalLineId(null);
}, []);

const handleCancelRemoval = useCallback(() => {
setPendingRemovalLineId(null);
}, []);
const hasAdjustmentChange = useMemo(() => {
const original = originalAdjustmentLinesRef.current;
const current = adjustmentEntries;
if (original.length !== current.length) return true;
const origById = new Map(original.map((line) => [line.id, { adjustedQty: line.adjustedQty ?? 0, remarks: line.remarks ?? '' }]));
for (const line of current) {
const o = origById.get(line.id);
if (!o) return true;
if (o.adjustedQty !== (line.adjustedQty ?? 0) || (o.remarks ?? '') !== (line.remarks ?? '')) return true;
}
return false;
}, [adjustmentEntries]);

const toApiLine = useCallback((line: AdjustmentEntry, itemCode: string): StockAdjustmentLineRequest => {
const [y, m, d] = Array.isArray(line.expiryDate) ? line.expiryDate : [];
const expiryDate = y != null && m != null && d != null
? `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
: '';
return {
id: line.id,
lotNo: line.lotNo ?? null,
adjustedQty: line.adjustedQty ?? 0,
productlotNo: line.productlotNo ?? null,
dnNo: line.dnNo ?? null,
isOpeningInventory: line.isOpeningInventory ?? false,
isNew: line.isNew ?? false,
itemId: line.item?.id ?? 0,
itemNo: line.item?.code ?? itemCode,
expiryDate,
warehouseId: line.warehouse?.id ?? 0,
uom: line.uom ?? null,
};
}, []);

const handleAdjustmentSave = useCallback(async () => {
if (!inventory) return;
const itemCode = inventory.itemCode;
const originalLines = originalAdjustmentLinesRef.current.map((line) => toApiLine(line, itemCode));
const currentLines = adjustmentEntries.map((line) => toApiLine(line, itemCode));
try {
setIsUploading(true);
await submitStockAdjustment({
itemId: inventory.itemId,
originalLines,
currentLines,
});
msg(t("Saved successfully"));
setStockAdjustmentModalOpen(false);
await onStockAdjustmentSuccess?.();
} catch (e: unknown) {
const message = e instanceof Error ? e.message : String(e);
msgError(message || t("Save failed"));
} finally {
setIsUploading(false);
}
}, [adjustmentEntries, inventory, t, toApiLine, onStockAdjustmentSuccess]);

const handleOpenAddEntry = useCallback(() => {
setAddEntryForm({
lotNo: '',
qty: 0,
expiryDate: '',
locationId: null,
locationInput: '',
productlotNo: '',
dnNo: '',
isOpeningInventory: false,
remarks: '',
});
setAddEntryModalOpen(true);
}, []);

const handleAddEntrySubmit = useCallback(() => {
if (addEntryForm.qty < 0 || !addEntryForm.expiryDate || !addEntryForm.locationId || !inventory) return;
const warehouse = warehouses.find(w => w.id === addEntryForm.locationId);
if (!warehouse) return;
const [y, m, d] = addEntryForm.expiryDate.split('-').map(Number);
const newEntry: AdjustmentEntry = {
id: -Date.now(),
lotNo: addEntryForm.lotNo.trim() || '',
item: { id: inventory.itemId, code: inventory.itemCode, name: inventory.itemName, type: inventory.itemType },
warehouse: { id: warehouse.id, code: warehouse.code, name: warehouse.name },
inQty: 0, outQty: 0, holdQty: 0,
expiryDate: [y, m, d],
status: 'available',
availableQty: addEntryForm.qty,
uom: inventory.uomUdfudesc || inventory.uomShortDesc || inventory.uomCode,
qtyPerSmallestUnit: inventory.qtyPerSmallestUnit ?? 1,
baseUom: inventory.baseUom || '',
stockInLineId: 0,
originalQty: 0,
adjustedQty: addEntryForm.qty,
productlotNo: addEntryForm.productlotNo.trim() || undefined,
dnNo: addEntryForm.dnNo.trim() || undefined,
isNew: true,
isOpeningInventory: addEntryForm.isOpeningInventory,
remarks: addEntryForm.remarks?.trim() ?? '',
};
setAdjustmentEntries(prev => [...prev, newEntry]);
setAddEntryModalOpen(false);
}, [addEntryForm, inventory, warehouses]);

const downloadQrCode = useCallback(async (lotLineId: number) => {
setIsUploading(true);
// const postData = { stockInLineIds: [42,43,44] };
@@ -78,6 +316,34 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
[],
);

const handlePrintClick = useCallback((lotLine: InventoryLotLineResult) => {
setLotLineForPrint(lotLine);
const labelPrinters = (printerCombo || []).filter(p => p.type === 'Label');
const savedId = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(PRINT_PRINTER_ID_KEY) : null;
const savedPrinter = savedId ? labelPrinters.find(p => p.id === Number(savedId)) : null;
setPrintPrinter(savedPrinter ?? labelPrinters[0] ?? null);
setPrintQty(1);
setPrintModalOpen(true);
}, [printerCombo]);

const handlePrintConfirm = useCallback(async () => {
if (!lotLineForPrint || !printPrinter) return;
try {
setIsUploading(true);
await printLabelForInventoryLotLine({
inventoryLotLineId: lotLineForPrint.id,
printerId: printPrinter.id,
printQty,
});
msg(t("Print sent"));
setPrintModalOpen(false);
} catch (e: any) {
msgError(e?.message ?? t("Print failed"));
} finally {
setIsUploading(false);
}
}, [lotLineForPrint, printPrinter, printQty, setIsUploading, t]);

const onDetailClick = useCallback(
(lotLine: InventoryLotLineResult) => {
downloadQrCode(lotLine.id)
@@ -163,7 +429,7 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
{
name: "id",
label: t("Print QR Code"),
onClick: () => {},
onClick: handlePrintClick,
buttonIcon: <PrintIcon />,
align: "center",
headerAlign: "center",
@@ -190,9 +456,11 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
// }
// },
],
[t, onDetailClick, downloadQrCode, handleStockTransfer],
[t, onDetailClick, downloadQrCode, handleStockTransfer, handlePrintClick],
);


const handleCloseStockTransferModal = useCallback(() => {
setStockTransferModalOpen(false);
setSelectedLotLine(null);
@@ -234,7 +502,31 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
}, [selectedLotLine, targetLocation, qtyToBeTransferred, handleCloseStockTransferModal, setIsUploading, t, onStockTransferSuccess]);

return <>
<Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', mb: 2 }}>
<Typography variant="h6">
{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}
</Typography>
{inventory && (
<Chip
icon={<TuneIcon />}
label={t("Stock Adjustment")}
onClick={() => setStockAdjustmentModalOpen(true)}
sx={{
cursor: 'pointer',
height: 30,
fontWeight: 'bold',
'& .MuiChip-label': {
fontSize: '0.875rem',
fontWeight: 'bold',
},
'& .MuiChip-icon': {
fontSize: '1rem',
},
}}
/>
)}
</Box>
<SearchResults<InventoryLotLineResult>
items={availableLotLines}
columns={columns}
@@ -428,7 +720,343 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
</Card>
</Modal>

</>
<Modal
open={printModalOpen}
onClose={() => setPrintModalOpen(false)}
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<Card sx={{ position: 'relative', minWidth: 320, maxWidth: 480, p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">{t("Print QR Code")}</Typography>
<IconButton onClick={() => setPrintModalOpen(false)}><CloseIcon /></IconButton>
</Box>
<Grid container spacing={2}>
<Grid item xs={12}>
<Autocomplete
options={(printerCombo || []).filter(printer => printer.type === 'Label')}
getOptionLabel={(opt) => opt.name ?? opt.label ?? opt.code ?? `Printer ${opt.id}`}
value={printPrinter}
onChange={(_, v) => {
setPrintPrinter(v);
if (typeof sessionStorage !== 'undefined') {
if (v?.id != null) sessionStorage.setItem(PRINT_PRINTER_ID_KEY, String(v.id));
else sessionStorage.removeItem(PRINT_PRINTER_ID_KEY);
}
}}
renderInput={(params) => <TextField {...params} label={t("Printer")} />}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Print Qty")}
type="number"
value={printQty}
onChange={(e) => setPrintQty(Math.max(1, parseInt(e.target.value) || 1))}
inputProps={{ min: 1 }}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<Button variant="contained" fullWidth onClick={handlePrintConfirm} disabled={!printPrinter}>
{t("Print")}
</Button>
</Grid>
</Grid>
</Card>
</Modal>
<Modal
open={stockAdjustmentModalOpen}
onClose={() => setStockAdjustmentModalOpen(false)}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Card
sx={{
position: 'relative',
width: '95%',
maxWidth: '1400px',
maxHeight: '92vh',
overflow: 'auto',
p: 3,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">
{inventory
? `${t("Edit mode")}: ${inventory.itemCode} ${inventory.itemName}`
: t("Stock Adjustment")
}
</Typography>
<IconButton onClick={() => setStockAdjustmentModalOpen(false)}>
<CloseIcon />
</IconButton>
</Box>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={handleOpenAddEntry}
>
{t("Add entry")}
</Button>
<Button
variant="outlined"
startIcon={<RestartAltIcon />}
onClick={handleAdjustmentReset}
>
{t("Reset")}
</Button>
<Button
variant="contained"
color="primary"
startIcon={<SaveIcon />}
onClick={handleAdjustmentSave}
disabled={!hasAdjustmentChange}
>
{t("Save")}
</Button>
</Box>

{/* List view */}
<Box sx={{ overflow: 'auto' }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>{t("Lot No")}</TableCell>
<TableCell align="right">{t("Original Qty")}</TableCell>
<TableCell align="right">{t("Adjusted Qty")}</TableCell>
<TableCell align="right" sx={{ minWidth: 100 }}>{t("Difference")}</TableCell>
<TableCell>{t("Stock UoM")}</TableCell>
<TableCell>{t("Expiry Date")}</TableCell>
<TableCell>{t("Location")}</TableCell>
<TableCell>{t("Remarks")}</TableCell>
<TableCell align="center" sx={{ minWidth: 240 }}>{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{adjustmentEntries.map((line) => (
<TableRow
key={line.id}
sx={{
backgroundColor: pendingRemovalLineId === line.id ? 'action.hover' : undefined,
}}
>
<TableCell>
<Box component="span" sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
<span>
{line.lotNo?.trim() ? line.lotNo : t("No lot no entered, will be generated by system.")}
{line.isOpeningInventory && ` (${t("Opening Inventory")})`}
</span>
{line.productlotNo && <span>{t("productLotNo")}: {line.productlotNo}</span>}
{line.dnNo && <span>{t("dnNo")}: {line.dnNo}</span>}
</Box>
</TableCell>
<TableCell align="right">{line.originalQty ?? 0}</TableCell>
<TableCell align="right">
<TextField
type="text"
inputMode="numeric"
value={String(line.adjustedQty)}
onChange={(e) => {
const raw = e.target.value.replace(/\D/g, '');
if (raw === '') {
handleAdjustmentQtyChange(line.id, 0);
return;
}
const num = parseInt(raw, 10);
if (!Number.isNaN(num) && num >= 0) handleAdjustmentQtyChange(line.id, num);
}}
inputProps={{ style: { textAlign: 'right' } }}
size="small"
sx={{
width: 120,
'& .MuiInputBase-root': {
display: 'flex',
alignItems: 'center',
height: 56,
},
'& .MuiInputBase-input': {
fontSize: 16,
textAlign: 'right',
height: 40,
lineHeight: '40px',
paddingTop: 0,
paddingBottom: 0,
boxSizing: 'border-box',
MozAppearance: 'textfield',
},
'& .MuiInputBase-input::-webkit-outer-spin-button': {
WebkitAppearance: 'none',
margin: 0,
},
'& .MuiInputBase-input::-webkit-inner-spin-button': {
WebkitAppearance: 'none',
margin: 0,
},
}}
/>
</TableCell>
<TableCell align="right" sx={{ minWidth: 100, fontWeight: 700 }}>
{(() => {
const diff = line.adjustedQty - (line.originalQty ?? 0);
const text = diff > 0 ? `+${diff}` : diff < 0 ? `${diff}` : '±0';
const color = diff > 0 ? 'success.main' : diff < 0 ? 'error.main' : 'text.secondary';
return <Box component="span" sx={{ color }}>{text}</Box>;
})()}
</TableCell>
<TableCell>{line.uom}</TableCell>
<TableCell>{arrayToDateString(line.expiryDate)}</TableCell>
<TableCell>{line.warehouse?.code ?? ""}</TableCell>
<TableCell>
{pendingRemovalLineId === line.id ? (
<TextField
size="small"
placeholder={t("Reason for removal")}
value={removalReasons[line.id] ?? ""}
onChange={(e) => handleRemovalReasonChange(line.id, e.target.value)}
sx={{
width: 160,
maxWidth: '100%',
'& .MuiInputBase-root': {
display: 'flex',
alignItems: 'center',
height: 56,
},
'& .MuiInputBase-input': {
fontSize: '1rem',
height: 40,
lineHeight: '40px',
paddingTop: 0,
paddingBottom: 0,
boxSizing: 'border-box',
'&::placeholder': { color: '#9e9e9e', opacity: 1 },
},
}}
/>
) : (line.adjustedQty - (line.originalQty ?? 0)) !== 0 ? (
<TextField
size="small"
placeholder={t("Reason for adjustment")}
value={line.remarks ?? ""}
onChange={(e) => handleAdjustmentRemarksChange(line.id, e.target.value)}
sx={{
width: 160,
maxWidth: '100%',
'& .MuiInputBase-root': {
display: 'flex',
alignItems: 'center',
height: 56,
},
'& .MuiInputBase-input': {
fontSize: '1rem',
height: 40,
lineHeight: '40px',
paddingTop: 0,
paddingBottom: 0,
boxSizing: 'border-box',
'&::placeholder': { color: '#9e9e9e', opacity: 1 },
},
}}
/>
) : null}
</TableCell>
<TableCell align="center">
{pendingRemovalLineId === line.id ? (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}>
<Button size="small" variant="outlined" onClick={handleCancelRemoval}>
{t("Cancel")}
</Button>
<Button
size="small"
variant="contained"
color="error"
startIcon={<CheckIcon />}
onClick={() => handleConfirmRemoval(line.id)}
>
{t("Confirm remove")}
</Button>
</Box>
) : (
<IconButton
size="small"
onClick={() => handleRemoveClick(line.id)}
color="error"
title={t("Remove")}
>
<DeleteIcon fontSize="small" />
</IconButton>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Card>
</Modal>

<Modal
open={addEntryModalOpen}
onClose={() => setAddEntryModalOpen(false)}
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<Card sx={{ position: 'relative', minWidth: 600, maxWidth: 900, p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">{t("Add entry")}</Typography>
<IconButton onClick={() => setAddEntryModalOpen(false)}><CloseIcon /></IconButton>
</Box>
<Grid container spacing={2}>
<Grid item xs={4}>
<TextField label={t("Available Qty")} type="number" fullWidth required value={addEntryForm.qty || ''} onChange={(e) => setAddEntryForm(f => ({ ...f, qty: Math.max(0, parseInt(e.target.value) || 0) }))} inputProps={{ min: 0 }} />
</Grid>
<Grid item xs={4}>
<TextField label={t("Stock UoM")} fullWidth disabled value={inventory?.uomUdfudesc || inventory?.uomShortDesc || inventory?.uomCode || ''} sx={{ '& .MuiInputBase-input': { color: 'text.secondary' } }} InputLabelProps={{ shrink: true }} />
</Grid>
<Grid item xs={4}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker label={t("Expiry Date")} format={INPUT_DATE_FORMAT} value={addEntryForm.expiryDate ? dayjs(addEntryForm.expiryDate) : null} onChange={(value) => setAddEntryForm(f => ({ ...f, expiryDate: value ? dayjs(value).format(INPUT_DATE_FORMAT) : '' }))} slotProps={{ textField: { fullWidth: true, required: true } }} />
</LocalizationProvider>
</Grid>
<Grid item xs={6}>
<Autocomplete options={warehouses} getOptionLabel={(o) => o.code || ''} value={addEntryForm.locationId ? warehouses.find(w => w.id === addEntryForm.locationId) ?? null : null} inputValue={addEntryForm.locationInput} onInputChange={(_, v) => setAddEntryForm(f => ({ ...f, locationInput: v }))} onChange={(_, v) => setAddEntryForm(f => ({ ...f, locationId: v?.id ?? null, locationInput: v?.code ?? '' }))} renderInput={(params) => <TextField {...params} label={t("Location")} required />} />
</Grid>
<Grid item xs={6} sx={{ display: 'flex', alignItems: 'center' }}>
<FormControlLabel control={<Checkbox checked={addEntryForm.isOpeningInventory} onChange={(e) => setAddEntryForm(f => ({ ...f, isOpeningInventory: e.target.checked }))} />} label={t("Opening Inventory")} />
</Grid>
<Grid item xs={4}>
<TextField label={t("productLotNo")} fullWidth placeholder={t("Optional - system will generate")} value={addEntryForm.productlotNo} onChange={(e) => setAddEntryForm(f => ({ ...f, productlotNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} />
</Grid>
<Grid item xs={4}>
<TextField label={t("dnNo")} fullWidth placeholder={t("Optional - system will generate")} value={addEntryForm.dnNo} onChange={(e) => setAddEntryForm(f => ({ ...f, dnNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} />
</Grid>
<Grid item xs={4}>
<TextField label={t("Lot No")} fullWidth placeholder={t("Optional - system will generate")} value={addEntryForm.lotNo} onChange={(e) => setAddEntryForm(f => ({ ...f, lotNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} />
</Grid>
<Grid item xs={12}>
<TextField
label={t("Remarks")}
fullWidth
placeholder={t("Reason for adjustment")}
value={addEntryForm.remarks}
onChange={(e) => setAddEntryForm(f => ({ ...f, remarks: e.target.value }))}
multiline
minRows={2}
sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }}
/>
</Grid>
<Grid item xs={12}>
<Button variant="contained" fullWidth onClick={handleAddEntrySubmit} disabled={addEntryForm.qty < 0 || !addEntryForm.expiryDate || !addEntryForm.locationId}>
{t("Add")}
</Button>
</Grid>
</Grid>
</Card>
</Modal>
</>

}

export default InventoryLotLineTable;

+ 7
- 1
src/components/InventorySearch/InventorySearch.tsx Dosyayı Görüntüle

@@ -10,9 +10,11 @@ import InventoryTable from "./InventoryTable";
import { defaultPagingController } from "../SearchResults/SearchResults";
import InventoryLotLineTable from "./InventoryLotLineTable";
import { SearchInventory, SearchInventoryLotLine, fetchInventories, fetchInventoryLotLines } from "@/app/api/inventory/actions";
import { PrinterCombo } from "@/app/api/settings/printer";

interface Props {
inventories: InventoryResult[];
printerCombo?: PrinterCombo[];
}

type SearchQuery = Partial<
@@ -32,7 +34,7 @@ type SearchQuery = Partial<
>;
type SearchParamNames = keyof SearchQuery;

const InventorySearch: React.FC<Props> = ({ inventories }) => {
const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
const { t } = useTranslation(["inventory", "common"]);

// Inventory
@@ -249,9 +251,13 @@ const InventorySearch: React.FC<Props> = ({ inventories }) => {
setPagingController={setInventoryLotLinesPagingController}
totalCount={inventoryLotLinesTotalCount}
inventory={selectedInventory}
printerCombo={printerCombo ?? []}
onStockTransferSuccess={() =>
refetchInventoryLotLineData(selectedInventory?.itemId ?? null, "search", inventoryLotLinesPagingController)
}
onStockAdjustmentSuccess={() =>
refetchInventoryLotLineData(selectedInventory?.itemId ?? null, "search", inventoryLotLinesPagingController)
}
/>
</>
);


+ 6
- 2
src/components/InventorySearch/InventorySearchWrapper.tsx Dosyayı Görüntüle

@@ -2,15 +2,19 @@ import React from "react";
import GeneralLoading from "../General/GeneralLoading";
import { fetchInventories } from "@/app/api/inventory";
import InventorySearch from "./InventorySearch";
import { fetchPrinterCombo } from "@/app/api/settings/printer";

interface SubComponents {
Loading: typeof GeneralLoading;
}

const InventorySearchWrapper: React.FC & SubComponents = async () => {
const [inventories] = await Promise.all([fetchInventories()]);
const [inventories, printerCombo] = await Promise.all([
fetchInventories(),
fetchPrinterCombo(),
]);

return <InventorySearch inventories={inventories} />;
return <InventorySearch inventories={inventories} printerCombo={printerCombo ?? []} />;
};

InventorySearchWrapper.Loading = GeneralLoading;


+ 19
- 13
src/components/JoSearch/JoSearch.tsx Dosyayı Görüntüle

@@ -23,7 +23,7 @@ import { SessionWithTokens } from "@/config/authConfig";
import { createStockInLine } from "@/app/api/stockIn/actions";
import { msg } from "../Swal/CustomAlerts";
import dayjs from "dayjs";
import { fetchInventories } from "@/app/api/inventory/actions";
//import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { PrinterCombo } from "@/app/api/settings/printer";
import { JobTypeResponse } from "@/app/api/jo/actions";
@@ -76,16 +76,21 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
useEffect(() => {
const fetchDetailedJos = async () => {
const detailedMap = new Map<number, JobOrder>();
for (const jo of filteredJos) {
try {
const detailedJo = await fetchJoDetailClient(jo.id);
detailedMap.set(jo.id, detailedJo);
} catch (error) {
console.error(`Error fetching detail for JO ${jo.id}:`, error);
}
try {
const results = await Promise.all(
filteredJos.map((jo) =>
fetchJoDetailClient(jo.id).then((detail) => ({ id: jo.id, detail })).catch((error) => {
console.error(`Error fetching detail for JO ${jo.id}:`, error);
return null;
})
)
);
results.forEach((r) => {
if (r) detailedMap.set(r.id, r.detail);
});
} catch (error) {
console.error("Error fetching JO details:", error);
}
setDetailedJos(detailedMap);
};

@@ -93,7 +98,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
fetchDetailedJos();
}
}, [filteredJos]);
/*
useEffect(() => {
const fetchInventoryData = async () => {
try {
@@ -102,9 +107,9 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
name: "",
type: "",
pageNum: 0,
pageSize: 1000
pageSize: 200,
});
setInventoryData(inventoryResponse.records);
setInventoryData(inventoryResponse.records ?? []);
} catch (error) {
console.error("Error fetching inventory data:", error);
}
@@ -112,6 +117,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT

fetchInventoryData();
}, []);
*/
const getStockAvailable = (pickLine: JoDetailPickLine) => {
const inventory = inventoryData.find(inventory =>


+ 2
- 0
src/components/Jodetail/FInishedJobOrderRecord.tsx Dosyayı Görüntüle

@@ -509,9 +509,11 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
size="small"
sx={{ mb: 1 }}
/>
{/*
<Typography variant="body2" color="text.secondary">
{jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")}
</Typography>
*/}
<Chip
label={jobOrderPickOrder.secondScanCompleted ? t("Second Scan Completed") : t("Second Scan Pending")}
color={jobOrderPickOrder.secondScanCompleted ? 'success' : 'warning'}


+ 2
- 0
src/components/Jodetail/FinishedGoodSearchWrapper.tsx Dosyayı Görüntüle

@@ -16,6 +16,8 @@ const JodetailSearchWrapper: React.FC & SubComponents = async () => {
type: undefined,
status: undefined,
itemName: undefined,
pageNum: 0,
pageSize: 50,
}),
fetchPrinterCombo(),
]);


+ 26
- 21
src/components/Jodetail/JobPickExecutionForm.tsx Dosyayı Görüntüle

@@ -91,7 +91,9 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
const [verifiedQty, setVerifiedQty] = useState<number>(0);
const { data: session } = useSession() as { data: SessionWithTokens | null };
const missSet = formData.missQty != null;
const badItemSet = formData.badItemQty != null;
const badPackageSet = (formData as any).badPackageQty != null;
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
return lot.availableQty || 0;
}, []);
@@ -162,9 +164,9 @@ useEffect(() => {
storeLocation: selectedLot.location,
requiredQty: selectedLot.requiredQty,
actualPickQty: initialVerifiedQty,
missQty: 0,
badItemQty: 0,
badPackageQty: 0, // Bad Package Qty (frontend only)
missQty: undefined,
badItemQty: undefined,
badPackageQty: undefined,
issueRemark: "",
pickerName: "",
handledBy: undefined,
@@ -195,10 +197,10 @@ useEffect(() => {
const newErrors: FormErrors = {};
const ap = Number(verifiedQty) || 0;
const miss = Number(formData.missQty) || 0;
const badItem = Number(formData.badItemQty) || 0;
const badPackage = Number((formData as any).badPackageQty) || 0;
const totalBad = badItem + badPackage;
const total = ap + miss + totalBad;
const badItem = Number(formData.badItemQty) ?? 0;
const badPackage = Number((formData as any).badPackageQty) ?? 0;
const totalBadQty = badItem + badPackage;
const total = ap + miss + totalBadQty;
const availableQty = selectedLot?.availableQty || 0;

// 1. Check actualPickQty cannot be negative
@@ -231,7 +233,7 @@ useEffect(() => {
}

// 5. At least one field must have a value
if (ap === 0 && miss === 0 && totalBad === 0) {
if (ap === 0 && miss === 0 && totalBadQty === 0) {
newErrors.actualPickQty = t("Enter pick qty or issue qty");
}

@@ -245,10 +247,9 @@ useEffect(() => {
// 增加 badPackageQty 判断,确保有坏包装会走 issue 流程
const badPackageQty = Number((formData as any).badPackageQty) || 0;
const isNormalPick = verifiedQty > 0
&& formData.missQty == 0
&& formData.badItemQty == 0
&& badPackageQty == 0;
const isNormalPick = (formData.missQty == null || formData.missQty === 0)
&& (formData.badItemQty == null || formData.badItemQty === 0)
&& (badPackageQty === 0);
if (isNormalPick) {
if (onNormalPickSubmit) {
@@ -288,11 +289,12 @@ useEffect(() => {
const submissionData: PickExecutionIssueData = {
...(formData as PickExecutionIssueData),
actualPickQty: verifiedQty,
lotId: formData.lotId || selectedLot?.lotId || 0,
lotNo: formData.lotNo || selectedLot?.lotNo || '',
pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '',
pickerName: session?.user?.name || '',
badItemQty: totalBadQty,
lotId: formData.lotId ?? selectedLot?.lotId ?? 0,
lotNo: formData.lotNo ?? selectedLot?.lotNo ?? '',
pickOrderCode: formData.pickOrderCode ?? selectedPickOrderLine?.pickOrderCode ?? '',
pickerName: session?.user?.name ?? '',
missQty: formData.missQty ?? 0, // 这里:null/undefined → 0
badItemQty: totalBadQty, // totalBadQty 下面用 ?? 0 算
badReason,
};
@@ -397,7 +399,8 @@ useEffect(() => {
pattern: "[0-9]*",
min: 0,
}}
value={formData.missQty || 0}
disabled={badItemSet || badPackageSet}
value={formData.missQty || ""}
onChange={(e) => {
handleInputChange(
"missQty",
@@ -421,7 +424,7 @@ useEffect(() => {
pattern: "[0-9]*",
min: 0,
}}
value={formData.badItemQty || 0}
value={formData.badItemQty || ""}
onChange={(e) => {
const newBadItemQty = e.target.value === ""
? undefined
@@ -429,6 +432,7 @@ useEffect(() => {
handleInputChange('badItemQty', newBadItemQty);
}}
error={!!errors.badItemQty}
disabled={missSet || badPackageSet}
helperText={errors.badItemQty}
variant="outlined"
/>
@@ -444,7 +448,7 @@ useEffect(() => {
pattern: "[0-9]*",
min: 0,
}}
value={(formData as any).badPackageQty || 0}
value={(formData as any).badPackageQty || ""}
onChange={(e) => {
handleInputChange(
"badPackageQty",
@@ -453,6 +457,7 @@ useEffect(() => {
: Math.max(0, Number(e.target.value) || 0)
);
}}
disabled={missSet || badItemSet}
error={!!errors.badItemQty}
variant="outlined"
/>


+ 13
- 42
src/components/Jodetail/JobPickExecutionsecondscan.tsx Dosyayı Görüntüle

@@ -868,7 +868,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
qty: submitQty,
isMissing: false,
isBad: false,
reason: undefined
reason: undefined,
userId: currentUserId ?? 0
}
);
@@ -881,7 +882,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
} catch (error) {
console.error("Error submitting second scan quantity:", error);
}
}, [fetchJobOrderData]);
}, [fetchJobOrderData, currentUserId]);

const handlePickExecutionForm = useCallback((lot: any) => {
console.log("=== Pick Execution Form ===");
@@ -1263,55 +1264,24 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
})()}
</TableCell>
{/*
<TableCell align="center">
{lot.matchStatus?.toLowerCase() === 'scanned' ||
lot.matchStatus?.toLowerCase() === 'completed' ? (
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%'
}}>
<Checkbox
checked={true}
disabled={true}
readOnly={true}
size="large"
sx={{
color: 'success.main',
'&.Mui-checked': {
color: 'success.main',
},
transform: 'scale(1.3)',
'& .MuiSvgIcon-root': {
fontSize: '1.5rem',
}
}}
/>
</Box>
) : (
<Typography variant="body2" color="text.secondary">
{t(" ")}
</Typography>
)}
</TableCell>
*/}

<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Stack direction="row" spacing={1} alignItems="center">
<Button
variant="contained"
onClick={() => {
onClick={async () => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
handlePickQtyChange(lotKey, submitQty);
handleSubmitPickQtyWithQty(lot, submitQty);
updateSecondQrScanStatus(lot.pickOrderId, lot.itemId, currentUserId || 0, submitQty);
// 先更新 matching 狀態(可選,依你後端流程)
await updateSecondQrScanStatus(lot.pickOrderId, lot.itemId, currentUserId || 0, submitQty);
// 再提交數量並 await refetch,表格會即時更新提料員
await handleSubmitPickQtyWithQty(lot, submitQty);
}}
disabled={
//lot.matchStatus !== 'scanned' ||
lot.matchStatus === 'completed' ||
lot.matchStatus == 'scanned' ||
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected'
@@ -1331,7 +1301,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={
lot.matchStatus !== 'scanned' ||
lot.matchStatus === 'completed' ||
lot.matchStatus == 'scanned' ||
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected'


+ 1
- 1
src/components/Jodetail/JobmatchForm.tsx Dosyayı Görüntüle

@@ -80,7 +80,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
// onNormalPickSubmit,
// selectedRowId,
}) => {
const { t } = useTranslation();
const { t } = useTranslation('common');
const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
const [errors, setErrors] = useState<FormErrors>({});
const [loading, setLoading] = useState(false);


+ 4
- 2
src/components/Jodetail/JodetailSearch.tsx Dosyayı Görüntüle

@@ -218,8 +218,10 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders, printerCombo }) => {
// 在组件加载时获取未分配订单
useEffect(() => {
loadUnassignedOrders();
}, [loadUnassignedOrders]);
if (tabIndex === 0) {
loadUnassignedOrders();
}
}, [tabIndex, loadUnassignedOrders]);

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {


+ 2
- 4
src/components/Jodetail/completeJobOrderRecord.tsx Dosyayı Görüntüle

@@ -642,23 +642,20 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
</Box>
</Stack>
</CardContent>
<CardActions>
<CardActions sx={{ alignItems: "center", gap: 1 }}>
<Button
variant="outlined"
onClick={() => handleDetailClick(jobOrderPickOrder)}
>
{t("View Details")}
</Button>

<Button
variant="contained"
color="primary"
onClick={() => handlePickRecord(jobOrderPickOrder)}
sx={{ mt: 1 }}
>
{t("Print Pick Record")}
</Button>

</CardActions>
</Card>
))}
@@ -675,6 +672,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[5, 10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
/>
)}
</Box>


+ 1
- 1
src/components/Jodetail/newJobPickExecution.tsx Dosyayı Görüntüle

@@ -1822,7 +1822,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}, [handleSubmitPickQtyWithQty]);
const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot =>
lot.stockOutLineStatus === 'checked'
lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'partially_completed'
);
if (scannedLots.length === 0) {


+ 29
- 14
src/components/LoginPage/LoginPage.tsx Dosyayı Görüntüle

@@ -8,19 +8,33 @@ import { Box } from "@mui/material";
const LoginPage = () => {
return (
<Grid container height="100vh">
<Grid item sm sx={{ backgroundColor: "#c5e58b"}}>
<Box sx={{
backgroundImage: "url('logo/HomepageLogo.png')",
backgroundRepeat: "no-repeat",
backgroundSize: "60% 40%",
width: "100%",
height: "100%",
backgroundPosition: "center",

}}>
</Box>
<Grid
item
xs={0}
sm={4}
md={5}
lg={6}
sx={{
display: { xs: "none", sm: "block" },
backgroundColor: "#c5e58b",
minHeight: { xs: 0, sm: "100vh" },
}}
>
<Box
sx={{
width: "100%",
height: "100%",
minHeight: { sm: "100vh" },
backgroundImage: "url('logo/HomepageLogo.png')",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
backgroundSize: "contain",
maxWidth: 960,
margin: "0 auto",
}}
/>
</Grid>
<Grid item xs={12} sm={8} lg={5}>
<Grid item xs={12} sm={8} md={7} lg={6}>
<Box
sx={{
width: "100%",
@@ -29,10 +43,11 @@ const LoginPage = () => {
display: "flex",
alignItems: "flex-end",
justifyContent: "center",
svg: { maxHeight: 120 },
backgroundImage: "linear-gradient(135deg, rgba(59,130,246,0.15) 0%, #f1f5f9 45%, #f8fafc 100%)",
backgroundColor: "#f8fafc",
}}
>
<Logo />
<Logo height={42} />
</Box>
<Paper square sx={{ height: "100%" }}>
<LoginForm />


+ 59
- 22
src/components/Logo/Logo.tsx Dosyayı Görüntüle

@@ -6,14 +6,18 @@ interface Props {
className?: string;
}

/** Same logo height everywhere so login and main page look identical. */
const DEFAULT_LOGO_HEIGHT = 42;

/**
* Logo: 3D-style badge (FP) + MTMS wordmark.
* Badge uses gradient and highlight for depth; FP = Food Production, MTMS = system name.
* Logo: rounded badge (FP) with links motif inside + FP-MTMS wordmark.
* Uses fixed typography so words look the same on login and main page.
*/
const Logo: React.FC<Props> = ({ height = 44, className = "" }) => {
const Logo: React.FC<Props> = ({ height = DEFAULT_LOGO_HEIGHT, className = "" }) => {
const size = Math.max(28, height);
const badgeSize = Math.round(size * 0.7);
const fontSize = Math.round(size * 0.5);
const titleFontSize = 21;
const subtitleFontSize = 10;
const fpSize = badgeSize <= 22 ? 10 : badgeSize <= 28 ? 12 : 14;

return (
@@ -22,7 +26,7 @@ const Logo: React.FC<Props> = ({ height = 44, className = "" }) => {
style={{ display: "flex", flexShrink: 0 }}
aria-label="FP-MTMS"
>
{/* 3D badge: FP with gradient, top bevel, and soft shadow */}
{/* Badge: rounded square with links motif inside + FP */}
<svg
width={badgeSize}
height={badgeSize}
@@ -32,29 +36,38 @@ const Logo: React.FC<Props> = ({ height = 44, className = "" }) => {
aria-hidden
>
<defs>
{/* Energetic blue gradient: bright top → deep blue bottom */}
<linearGradient id="logo-bg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#60a5fa" />
<stop offset="40%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#1d4ed8" />
</linearGradient>
<linearGradient id="logo-bevel" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="rgba(255,255,255,0.45)" />
<stop offset="0%" stopColor="rgba(255,255,255,0.4)" />
<stop offset="100%" stopColor="rgba(255,255,255,0)" />
</linearGradient>
<filter id="logo-shadow" x="-15%" y="-5%" width="130%" height="120%">
<feDropShadow dx="0" dy="2" stdDeviation="1.5" floodOpacity="0.35" floodColor="#1e40af" />
</filter>
</defs>
{/* Shadow layer - deep blue */}
<rect x="1" y="2" width="36" height="36" rx="8" fill="#1e40af" fillOpacity="0.4" />
{/* Main 3D body */}
<rect x="0" y="0" width="36" height="36" rx="8" fill="url(#logo-bg)" filter="url(#logo-shadow)" />
{/* Top bevel (inner 3D) */}
<rect x="2" y="2" width="32" height="12" rx="6" fill="url(#logo-bevel)" />
{/* FP text */}
{/* Shadow */}
<rect x="3" y="4" width="34" height="34" rx="8" fill="#1e40af" fillOpacity="0.35" />
{/* Body */}
<rect x="2" y="2" width="36" height="36" rx="8" fill="url(#logo-bg)" filter="url(#logo-shadow)" />
<rect x="2" y="2" width="36" height="12" rx="7" fill="url(#logo-bevel)" />
{/* Links motif inside: small chain links in corners, clear center for FP */}
<g fill="none" stroke="rgba(255,255,255,0.55)" strokeWidth="1.4" strokeLinecap="round">
<path d="M 8 10 a 3 3 0 1 1 0 4.5 a 3 3 0 1 1 0 -4.5" />
<path d="M 12 10 a 3 3 0 1 1 0 4.5 a 3 3 0 1 1 0 -4.5" />
<line x1="11" y1="12.2" x2="12" y2="12.2" />
</g>
<g fill="none" stroke="rgba(255,255,255,0.5)" strokeWidth="1.4" strokeLinecap="round">
<path d="M 28 28 a 3 3 0 1 1 0 4.5 a 3 3 0 1 1 0 -4.5" />
<path d="M 32 28 a 3 3 0 1 1 0 4.5 a 3 3 0 1 1 0 -4.5" />
<line x1="31" y1="30.2" x2="32" y2="30.2" />
</g>
{/* FP text – top-right so it doesn’t overlap the links */}
<text
x="18"
x="20"
y="24"
textAnchor="middle"
fill="#f8fafc"
@@ -68,17 +81,41 @@ const Logo: React.FC<Props> = ({ height = 44, className = "" }) => {
FP
</text>
</svg>
{/* Wordmark: MTMS + subtitle — strong, energetic */}
<div className="flex flex-col justify-center leading-tight">
{/* Wordmark: fixed typography so login and main page match */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 4,
fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
}}
>
<span
className="font-bold tracking-tight text-blue-700 dark:text-blue-200"
style={{ fontSize: `${fontSize}px`, letterSpacing: "0.03em" }}
style={{
display: "block",
whiteSpace: "nowrap",
fontSize: titleFontSize,
fontWeight: 700,
letterSpacing: "0.03em",
lineHeight: 1.25,
color: "#1e40af",
}}
className="dark:text-blue-200"
>
MTMS
FP-MTMS
</span>
<span
className="text-[10px] font-semibold uppercase tracking-wider text-blue-600/90 dark:text-blue-300/90"
style={{ letterSpacing: "0.1em" }}
style={{
display: "block",
whiteSpace: "nowrap",
fontSize: subtitleFontSize,
fontWeight: 500,
letterSpacing: "0.12em",
lineHeight: 1.4,
textTransform: "uppercase",
color: "#2563eb",
}}
className="dark:text-blue-300"
>
Food Production
</span>


+ 2
- 2
src/components/NavigationContent/NavigationContent.tsx Dosyayı Görüntüle

@@ -96,7 +96,7 @@ const NavigationContent: React.FC = () => {
{
icon: <AssignmentTurnedIn />,
label: "Stock Take Management",
requiredAbility: [AUTH.STOCK_TAKE, AUTH.ADMIN],
requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
path: "/stocktakemanagement",
},
{
@@ -120,7 +120,7 @@ const NavigationContent: React.FC = () => {
{
icon: <Description />,
label: "Stock Record",
requiredAbility: [AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
path: "/stockRecord",
},
],


+ 1
- 1
src/components/PoDetail/PoInputGrid.tsx Dosyayı Görüntüle

@@ -525,7 +525,7 @@ const closeNewModal = useCallback(() => {
width: 150,
// flex: 0.5,
renderCell: (params) => {
return params.row.uom?.udfudesc;
return itemDetail.uom?.udfudesc;
},
},
{


+ 11
- 1
src/components/ProductionProcess/BagConsumptionForm.tsx Dosyayı Görüntüle

@@ -140,7 +140,17 @@ const BagConsumptionForm: React.FC<BagConsumptionFormProps> = ({
alert(t("Please select at least one bag"));
return;
}

for (const row of validRows) {
const selectedBag = bagList.find((b) => b.id === row.bagLotLineId);
const available = selectedBag?.balanceQty ?? 0;
const requested = row.consumedQty + row.scrapQty;
if (requested > available) {
alert(
`${selectedBag?.bagName ?? "Bag"}: ${t("Insufficient balance")}. ${t("Available")}: ${available}, ${t("Requested")}: ${requested}`
);
return;
}
}
// 提交每个 bag consumption
const promises = validRows.map((row) => {
const selectedBag = bagList.find((b) => b.id === row.bagLotLineId);


+ 1
- 1
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx Dosyayı Görüntüle

@@ -422,7 +422,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
const productionProcessesLineRemarkTableColumns: GridColDef[] = [
{
field: "seqNo",
headerName: t("Seq"),
headerName: t("SEQ"),
flex: 0.2,
align: "left",
headerAlign: "left",


+ 5
- 4
src/components/ProductionProcess/ProductionProcessList.tsx Dosyayı Görüntüle

@@ -220,7 +220,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
display: "flex",
flexDirection: "column",
border: "1px solid",
borderColor: "success.main",
borderColor: "blue",
}}
>
<CardContent
@@ -240,8 +240,9 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
<Chip size="small" label={t(status)} color={statusColor as any} />
</Stack>

<Typography variant="body2" color="text.secondary">
{t("Item Name")}: {process.itemCode} {process.itemName}
<Typography variant="subtitle1" color="blue">
{/* <strong>{t("Item Name")}:</strong> */}
{process.itemCode} {process.itemName}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Production Priority")}: {process.productionPriority}
@@ -306,7 +307,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
</Button>
)}
{statusLower === "completed" && (
<Button onClick={() => handleViewStockIn(process)}>
<Button variant="contained" size="small" onClick={() => handleViewStockIn(process)}>
{t("view stockin")}
</Button>
)}


+ 9
- 10
src/components/PutAwayScan/PutAwayModal.tsx Dosyayı Görüntüle

@@ -57,17 +57,16 @@ const style = {
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
pt: { xs: 0.5, sm: 1, md: 1.5 },
px: { xs: 1, sm: 1.5, md: 2 },
pb: { xs: 0.5, sm: 1, md: 1.5 },
width: { xs: "95%", sm: "85%", md: "75%", lg: "70%" },
maxWidth: "900px",
maxHeight: { xs: "98vh", sm: "95vh", md: "90vh" },
pt: { xs: 0.5, sm: 0.75, md: 1 },
px: { xs: 1, sm: 1, md: 1.5 },
pb: { xs: 0.5, sm: 0.75, md: 1 },
width: { xs: "95%", sm: "72%", md: "60%", lg: "70%" },
maxWidth: "720px",
maxHeight: { xs: "98vh", sm: "92vh", md: "88vh" },
overflow: "hidden",
display: "flex",
flexDirection: "column",
};

const scannerStyle = {
position: "absolute",
top: "50%",
@@ -442,9 +441,9 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
{itemDetail != undefined ? (
<>
<Stack direction="column" justifyContent="flex-end" gap={0.25} sx={{ mb: 0.5 }}>
<Typography variant="h4" sx={{ fontSize: { xs: "0.95rem", sm: "1.1rem", md: "1.3rem" }, mb: 0.25, lineHeight: 1.2 }}>
處理上架
</Typography>
<Typography variant="h4" sx={{ fontSize: { xs: "0.95rem", sm: "0.95rem", md: "1.1rem" }, mb: 0.25, lineHeight: 1.2 }}>
處理上架
</Typography>
<Box sx={{ "& .MuiFormControl-root": { mb: 0.5 }, "& .MuiTextField-root": { mb: 0.5 }, "& .MuiGrid-item": { mb: 0.25 } }}>
<Grid item xs={12}>
{itemDetail.jobOrderId ? (


+ 12
- 1
src/components/Qc/QcForm.tsx Dosyayı Görüntüle

@@ -232,12 +232,23 @@ const QcForm: React.FC<Props> = ({ rows, disabled = false }) => {

return (
<>
<StyledDataGrid
columns={qcColumns}
rows={rows}
// autoHeight
sortModel={[]}
getRowHeight={() => 'auto'}
initialState={{
pagination: { paginationModel: { page: 0, pageSize: 100 } },
}}
pageSizeOptions={[100]}
slotProps={{
pagination: {
sx: {
display: "none",
},
},
}}
/>
</>
);


+ 26
- 16
src/components/Qc/QcStockInModal.tsx Dosyayı Görüntüle

@@ -68,6 +68,7 @@ interface CommonProps extends Omit<ModalProps, "children"> {
interface Props extends CommonProps {
// itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] };
}

const QcStockInModal: React.FC<Props> = ({
open,
onClose,
@@ -94,6 +95,10 @@ const QcStockInModal: React.FC<Props> = ({
() => `qcStockInModal_selectedPrinterId_${session?.id ?? "guest"}`,
[session?.id],
);
const labelPrinterCombo = useMemo(
() => (printerCombo || []).filter((p) => p.type === "Label"),
[printerCombo],
);
const getDefaultPrinter = useMemo(() => {
if (!printerCombo.length) return undefined;
if (typeof window === "undefined") return printerCombo[0];
@@ -102,7 +107,7 @@ const QcStockInModal: React.FC<Props> = ({
const matched = savedId ? printerCombo.find(p => p.id === Number(savedId)) : undefined;
return matched ?? printerCombo[0];
}, [printerCombo, printerStorageKey]);
const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]);
const [selectedPrinter, setSelectedPrinter] = useState(labelPrinterCombo[0]);
const [printQty, setPrintQty] = useState(1);
const [tabIndex, setTabIndex] = useState(0);

@@ -504,6 +509,7 @@ const QcStockInModal: React.FC<Props> = ({
// Put away model
const [pafRowModesModel, setPafRowModesModel] = useState<GridRowModesModel>({})
const [pafRowSelectionModel, setPafRowSelectionModel] = useState<GridRowSelectionModel>([])

const pafSubmitDisable = useMemo(() => {
return Object.entries(pafRowModesModel).length > 0 || Object.entries(pafRowModesModel).some(([key, value], index) => value.mode === GridRowModes.Edit)
}, [pafRowModesModel])
@@ -749,21 +755,25 @@ const printQrcode = useCallback(
{tabIndex == 1 && (
<Stack direction="row" justifyContent="flex-end" gap={1} sx={{m:3, mt:"auto"}}>
<Autocomplete
disableClearable
options={printerCombo}
defaultValue={selectedPrinter}
onChange={(event, value) => {
setSelectedPrinter(value)
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label={t("Printer")}
sx={{ width: 300}}
/>
)}
/>
disableClearable
options={labelPrinterCombo}
getOptionLabel={(option) =>
option.name || option.label || option.code || `Printer ${option.id}`
}
value={selectedPrinter}
onChange={(_, newValue) => {
if (newValue) setSelectedPrinter(newValue);
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label={t("Printer")}
sx={{ width: 300 }}
inputProps={{ ...params.inputProps, readOnly: true }}
/>
)}
/>
<TextField
variant="outlined"
label={t("Print Qty")}


+ 1
- 1
src/components/SearchResults/SearchResults.tsx Dosyayı Görüntüle

@@ -198,7 +198,7 @@ function SearchResults<T extends ResultWithId>({
setCheckboxIds = undefined,
onRowClick = undefined,
}: Props<T>) {
const { t } = useTranslation("dashboard");
const { t } = useTranslation();
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);


+ 13
- 0
src/components/StockIssue/SearchPage.tsx Dosyayı Görüntüle

@@ -169,7 +169,17 @@ const SearchPage: React.FC<Props> = ({ dataList }) => {
{ name: "itemDescription", label: t("Item") },
{ name: "lotNo", label: t("Lot No.") },
{ name: "storeLocation", label: t("Location") },
{
name: "bookQty",
label: t("Book Qty"),
renderCell: (item) => (
<>{item.bookQty?.toFixed(2) ?? "0"} {item.uomDesc ?? ""}</>
),
},
{ name: "issueQty", label: t("Miss Qty") },
{ name: "uomDesc", label: t("UoM"), renderCell: (item) => (
<>{item.uomDesc ?? ""}</>
) },
{
name: "id",
label: t("Action"),
@@ -196,6 +206,9 @@ const SearchPage: React.FC<Props> = ({ dataList }) => {
{ name: "lotNo", label: t("Lot No.") },
{ name: "storeLocation", label: t("Location") },
{ name: "issueQty", label: t("Defective Qty") },
{ name: "uomDesc", label: t("UoM"), renderCell: (item) => (
<>{item.uomDesc ?? ""}</>
) },
{
name: "id",
label: t("Action"),


+ 35
- 4
src/components/StockIssue/SubmitIssueForm.tsx Dosyayı Görüntüle

@@ -49,7 +49,10 @@ const SubmitIssueForm: React.FC<Props> = ({
const [submitting, setSubmitting] = useState(false);
const [details, setDetails] = useState<LotIssueDetailResponse | null>(null);
const [submitQty, setSubmitQty] = useState<string>("");

const bookQty = details?.bookQty ?? 0;
const submitQtyNum = parseFloat(submitQty);
const submitQtyValid = !Number.isNaN(submitQtyNum) && submitQtyNum >= 0;
const remainAvailable = submitQtyValid ? Math.max(0, bookQty - submitQtyNum) : bookQty;
useEffect(() => {
if (open && lotId) {
loadDetails();
@@ -121,9 +124,17 @@ const SubmitIssueForm: React.FC<Props> = ({
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>{t("Lot No.")}:</strong> {details.lotNo}
</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>{t("Location")}:</strong> {details.storeLocation}
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>{t("Book Qty")}:</strong>{" "}
{details.bookQty}
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>{t("UoM")}:</strong>{" "}
{details.uomDesc ?? ""}
</Typography>
</Box>

<TableContainer component={Paper} sx={{ mb: 2 }}>
@@ -146,8 +157,8 @@ const SubmitIssueForm: React.FC<Props> = ({
<TableCell>{issue.pickerName || "-"}</TableCell>
<TableCell align="right">
{issueType === "miss"
? issue.missQty?.toFixed(2) || "0"
: issue.issueQty?.toFixed(2) || "0"}
? issue.missQty?.toFixed(0) || "0"
: issue.issueQty?.toFixed(0) || "0"}
</TableCell>
<TableCell>{issue.pickOrderCode}</TableCell>
<TableCell>{issue.doOrderCode || "-"}</TableCell>
@@ -168,6 +179,26 @@ const SubmitIssueForm: React.FC<Props> = ({
inputProps={{ min: 0, step: 0.01 }}
sx={{ mt: 2 }}
/>
<TextField
fullWidth
label={t("Remain available Quantity")}
type="number"
value={remainAvailable}
onChange={(e) => {
const raw = e.target.value;
if (raw === "") {
setSubmitQty("");
return;
}
const remain = parseFloat(raw);
if (!Number.isNaN(remain) && remain >= 0) {
const newSubmit = Math.max(0, bookQty - remain);
setSubmitQty(newSubmit.toFixed(0));
}
}}
inputProps={{ min: 0, step: 0.01, readOnly: false }}
sx={{ mt: 2 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={submitting}>


+ 65
- 61
src/components/StockRecord/SearchPage.tsx Dosyayı Görüntüle

@@ -41,7 +41,7 @@ const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => {
// 添加分页状态
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>(10);
const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 });
const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 100 });
const [hasSearchQuery, setHasSearchQuery] = useState(false);
const [totalCount, setTotalCount] = useState(initialDataList.length);
@@ -134,7 +134,7 @@ const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => {
// 当 processedData 变化时更新 filteredList(不更新 pagingController,避免循环)
useEffect(() => {
setFilteredList(processedData);
setTotalCount(processedData.length);
// 只在初始加载时设置 pageSize
if (isInitialMount.current && processedData.length > 0) {
setPageSize("all");
@@ -146,55 +146,53 @@ const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => {

// API 调用函数(参考 PoSearch 的实现)
// API 调用函数(参考 PoSearch 的实现)
const newPageFetch = useCallback(
async (
pagingController: Record<string, number>,
filterArgs: Record<string, any>,
) => {
setLoading(true);
try {
// 处理空字符串,转换为 null
const itemCode = filterArgs.itemCode?.trim() || null;
const itemName = filterArgs.itemName?.trim() || null;
// 验证:至少需要 itemCode 或 itemName
if (!itemCode && !itemName) {
console.warn("Search requires at least itemCode or itemName");
const newPageFetch = useCallback(
async (
pagingController: Record<string, number>,
filterArgs: Record<string, any>,
) => {
setLoading(true);
try {
const itemCode = filterArgs.itemCode?.trim() || null;
const itemName = filterArgs.itemName?.trim() || null;
if (!itemCode && !itemName) {
console.warn("Search requires at least itemCode or itemName");
setDataList([]);
setTotalCount(0);
return;
}
const params: SearchStockTransactionRequest = {
itemCode: itemCode,
itemName: itemName,
type: filterArgs.type?.trim() || null,
startDate: filterArgs.startDate || null,
endDate: filterArgs.endDate || null,
pageNum: pagingController.pageNum - 1 || 0,
pageSize: pagingController.pageSize || 100,
};
const res = await searchStockTransactions(params);
if (res && typeof res === 'object' && Array.isArray(res.records)) {
setDataList(res.records);
setTotalCount(res.total ?? res.records.length);
} else {
console.error("Invalid response format:", res);
setDataList([]);
setTotalCount(0);
}
} catch (error) {
console.error("Fetch error:", error);
setDataList([]);
setTotalCount(0);
return;
} finally {
setLoading(false);
}
const params: SearchStockTransactionRequest = {
itemCode: itemCode,
itemName: itemName,
type: filterArgs.type?.trim() || null,
startDate: filterArgs.startDate || null,
endDate: filterArgs.endDate || null,
pageNum: pagingController.pageNum - 1 || 0,
pageSize: pagingController.pageSize || 100,
};
console.log("Search params:", params); // 添加调试日志
const res = await searchStockTransactions(params);
console.log("Search response:", res); // 添加调试日志
if (res && Array.isArray(res)) {
setDataList(res);
} else {
console.error("Invalid response format:", res);
setDataList([]);
}
} catch (error) {
console.error("Fetch error:", error);
setDataList([]);
} finally {
setLoading(false);
}
},
[],
);
},
[],
);

// 使用 useRef 来存储上一次的值,避免不必要的 API 调用
const prevPagingControllerRef = useRef(pagingController);
@@ -240,13 +238,13 @@ const newPageFetch = useCallback(
const newSize = parseInt(event.target.value, 10);
if (newSize === -1) {
setPageSize("all");
setPagingController(prev => ({ ...prev, pageSize: filteredList.length, pageNum: 1 }));
setPagingController(prev => ({ ...prev, pageSize: 100, pageNum: 1 })); // 用 100 觸發後端回傳全部
} else if (!isNaN(newSize)) {
setPageSize(newSize);
setPagingController(prev => ({ ...prev, pageSize: newSize, pageNum: 1 }));
}
setPage(0);
}, [filteredList.length]);
}, []);

const searchCriteria: Criterion<string>[] = useMemo(
() => [
@@ -263,7 +261,16 @@ const newPageFetch = useCallback(
{
label: t("Type"),
paramName: "type",
type: "text",
type: "select-labelled",
options: [
{ value: "tke", label: t("tke") }, // 盤點
{ value: "ADJ", label: t("adj") },
{ value: "Nor", label: t("nor") },
{ value: "TRF", label: t("trf") },
{ value: "OPEN", label: t("open") }, // 開倉
{ value: "miss", label: t("miss") },
{ value: "bad", label: t("bad") },
],
},
{
label: t("Start Date"),
@@ -390,29 +397,26 @@ const newPageFetch = useCallback(
setPagingController(prev => ({ ...prev, pageNum: 1 }));
}, []);

// 计算实际显示的 items(分页)
const paginatedItems = useMemo(() => {
if (pageSize === "all") {
return filteredList;
}
const actualPageSize = typeof pageSize === 'number' ? pageSize : 10;
const startIndex = page * actualPageSize;
const endIndex = startIndex + actualPageSize;
return filteredList.slice(startIndex, endIndex);
}, [filteredList, page, pageSize]);
const size = typeof pageSize === 'number' ? pageSize : 10;
const start = page * size;
return filteredList.slice(start, start + size);
}, [filteredList, pageSize, page]);

// 计算传递给 SearchResults 的 pageSize(确保在选项中)
const actualPageSizeForTable = useMemo(() => {
if (pageSize === "all") {
return filteredList.length;
return totalCount > 0 ? totalCount : filteredList.length;
}
const size = typeof pageSize === 'number' ? pageSize : 10;
// 如果 size 不在标准选项中,使用 "all" 模式
if (![10, 25, 100].includes(size)) {
return filteredList.length;
return size;
}
return size;
}, [pageSize, filteredList.length]);
}, [pageSize, filteredList.length, totalCount]);

return (
<>


+ 1
- 1
src/components/StockRecord/index.tsx Dosyayı Görüntüle

@@ -18,7 +18,7 @@ const Wrapper: React.FC & SubComponents = async () => {
pageSize: 100,
});

return <SearchPage dataList={dataList || []} />;
return <SearchPage dataList={dataList?.records ?? []} />;
};

Wrapper.Loading = GeneralLoading;


+ 112
- 66
src/components/StockTakeManagement/ApproverStockTake.tsx Dosyayı Görüntüle

@@ -56,8 +56,8 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({

const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
const [loadingDetails, setLoadingDetails] = useState(false);
const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(false);
const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(true);
const [variancePercentTolerance, setVariancePercentTolerance] = useState<string>("5");
// 每个记录的选择状态,key 为 detail.id
const [qtySelection, setQtySelection] = useState<Record<number, QtySelectionType>>({});
const [approverQty, setApproverQty] = useState<Record<number, string>>({});
@@ -71,7 +71,17 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const handleBatchSubmitAllRef = useRef<() => Promise<void>>();

const isWithinVarianceTolerance = useCallback((
difference: number,
bookQty: number,
percentStr: string
): boolean => {
const percent = parseFloat(percentStr || "0");
if (isNaN(percent) || percent < 0) return true; // 无效输入时视为全部通过
if (bookQty === 0) return difference === 0;
const threshold = Math.abs(bookQty) * (percent / 100);
return Math.abs(difference) <= threshold;
}, []);
const handleChangePage = useCallback((event: unknown, newPage: number) => {
setPage(newPage);
}, []);
@@ -133,7 +143,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0")) || 0;
}
const bookQty = detail.availableQty || 0;
const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
return selectedQty - bookQty;
}, [approverQty, approverBadQty]);
@@ -159,16 +169,29 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
// 4. 添加过滤逻辑(在渲染表格之前)
const filteredDetails = useMemo(() => {
if (!showOnlyWithDifference) {
return inventoryLotDetails;
let result = inventoryLotDetails;
if (showOnlyWithDifference) {
const percent = parseFloat(variancePercentTolerance || "0");
const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent;
result = result.filter(detail => {
// 已完成項目一律顯示
if (detail.finalQty != null || detail.stockTakeRecordStatus === "completed") {
return true;
}
const selection = qtySelection[detail.id] ??
(detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first");
const difference = calculateDifference(detail, selection);
const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
if (bookQty === 0) return difference !== 0;
const threshold = Math.abs(bookQty) * (thresholdPercent / 100);
return Math.abs(difference) > threshold;
});
}
return inventoryLotDetails.filter(detail => {
const selection = qtySelection[detail.id] || (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first");
const difference = calculateDifference(detail, selection);
return difference !== 0;
});
}, [inventoryLotDetails, showOnlyWithDifference, qtySelection, calculateDifference]);
return result;
}, [inventoryLotDetails, showOnlyWithDifference, variancePercentTolerance, qtySelection, calculateDifference]);
const handleSaveApproverStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
if (!selectedSession || !currentUserId) {
@@ -231,7 +254,22 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
onSnackbar(t("Approver stock take record saved successfully"), "success");

await loadDetails(page, pageSize);
// 計算最終數量(合格數)
const goodQty = finalQty - finalBadQty;

setInventoryLotDetails((prev) =>
prev.map((d) =>
d.id === detail.id
? {
...d,
finalQty: goodQty,
approverQty: selection === "approver" ? finalQty : d.approverQty,
approverBadQty: selection === "approver" ? finalBadQty : d.approverBadQty,
stockTakeRecordStatus: "completed",
}
: d
)
);
} catch (e: any) {
console.error("Save approver stock take record error:", e);
let errorMessage = t("Failed to save approver stock take record");
@@ -264,6 +302,11 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId);
onSnackbar(t("Stock take record status updated to not match"), "success");
setInventoryLotDetails((prev) =>
prev.map((d) =>
d.id === detail.id ? { ...d, stockTakeRecordStatus: "notMatch" } : d
)
);
} catch (e: any) {
console.error("Update stock take record status error:", e);
let errorMessage = t("Failed to update stock take record status");
@@ -284,17 +327,9 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
setUpdatingStatus(false);
// Reload after status update - the useEffect will handle it with current page/pageSize
// Or explicitly reload:
setPage((currentPage) => {
setPageSize((currentPageSize) => {
setTimeout(() => {
loadDetails(currentPage, currentPageSize);
}, 0);
return currentPageSize;
});
return currentPage;
});
}
}, [selectedSession, t, onSnackbar, loadDetails]);
}, [selectedSession, t, onSnackbar, ]);
const handleBatchSubmitAll = useCallback(async () => {
if (!selectedSession || !currentUserId) {
@@ -309,6 +344,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
stockTakeId: selectedSession.stockTakeId,
stockTakeSection: selectedSession.stockTakeSession,
approverId: currentUserId,
variancePercentTolerance: parseFloat(variancePercentTolerance || "0") || undefined,
};

const result = await batchSaveApproverStockTakeRecords(request);
@@ -349,10 +385,10 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
}, [handleBatchSubmitAll]);
const formatNumber = (num: number | null | undefined): string => {
if (num == null) return "0.00";
if (num == null) return "0";
return num.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
minimumFractionDigits: 0,
maximumFractionDigits: 0
});
};
@@ -411,25 +447,30 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
</Typography>

<Stack direction="row" spacing={2} alignItems="center">
<Button
variant={showOnlyWithDifference ? "contained" : "outlined"}
color="primary"
onClick={() => setShowOnlyWithDifference(!showOnlyWithDifference)}
startIcon={
<TextField
size="small"
type="number"
value={variancePercentTolerance}
onChange={(e) => setVariancePercentTolerance(e.target.value)}
label={t("Variance %")}
sx={{ width: 100 }}
inputProps={{ min: 0, max: 100, step: 0.1 }}
/>
{/*
<FormControlLabel
control={
<Checkbox
checked={showOnlyWithDifference}
onChange={(e) => setShowOnlyWithDifference(e.target.checked)}
sx={{ p: 0, pointerEvents: 'none' }}
/>
}
sx={{ textTransform: 'none' }}
>
{t("Only Variance")}
label={t("Only Variance")}
/>
*/}
<Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}>
{t("Batch Save All")}
</Button>
<Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}>
{t("Batch Save All")}
</Button>
</Stack>
</Stack>
</Stack>
{loadingDetails ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
@@ -454,9 +495,10 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
@@ -492,25 +534,27 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
</Stack>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
<TableCell sx={{ minWidth: 300 }}>
{detail.finalQty != null ? (
<Stack spacing={0.5}>
{(() => {
const finalDifference = (detail.finalQty || 0) - (detail.availableQty || 0);
const differenceColor = finalDifference > 0
? 'error.main'
: finalDifference < 0
? 'error.main'
: 'success.main';
return (
<Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}>
{t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber(finalDifference)}
</Typography>
);
})()}
</Stack>
{detail.finalQty != null ? (
<Stack spacing={0.5}>
{(() => {
// 若有 bookQty(盤點當時帳面),用它來算差異;否則用 availableQty
const bookQtyToUse = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
const finalDifference = (detail.finalQty || 0) - bookQtyToUse;
const differenceColor = detail.stockTakeRecordStatus === "completed"
? 'text.secondary'
: finalDifference !== 0
? 'error.main'
: 'success.main';
return (
<Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}>
{t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(bookQtyToUse)} = {formatNumber(finalDifference)}
</Typography>
);
})()}
</Stack>
) : (
<Stack spacing={1}>
{hasFirst && (
@@ -581,7 +625,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
disabled={selection !== "approver"}
/>
<Typography variant="body2">
={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))}
= {formatNumber(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))}
</Typography>
</Stack>
)}
@@ -597,12 +641,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0;
}
const bookQty = detail.availableQty || 0;
const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
const difference = selectedQty - bookQty;
const differenceColor = difference > 0
? 'error.main'
: difference < 0
? 'error.main'
const differenceColor = detail.stockTakeRecordStatus === "completed"
? 'text.secondary'
: difference !== 0
? 'error.main'
: 'success.main';
return (
@@ -621,11 +665,13 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
</Typography>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
{detail.stockTakeRecordStatus === "completed" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
) : detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="default" />
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
) : (


+ 35
- 2
src/components/StockTakeManagement/PickerCardList.tsx Dosyayı Görüntüle

@@ -13,6 +13,11 @@ import {
TablePagination,
Grid,
LinearProgress,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
} from "@mui/material";
import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
@@ -41,7 +46,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]);
const [page, setPage] = useState(0);
const [creating, setCreating] = useState(false);
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const fetchStockTakeSessions = useCallback(async () => {
setLoading(true);
try {
@@ -64,6 +69,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE);
const handleCreateStockTake = useCallback(async () => {
setOpenConfirmDialog(false);
setCreating(true);
try {
const result = await createStockTakeForSections();
@@ -177,7 +183,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
<Button
variant="contained"
color="primary"
onClick={handleCreateStockTake}
onClick={() => setOpenConfirmDialog(true)}
disabled={creating}
>
{creating ? <CircularProgress size={20} /> : t("Create Stock Take for All Sections")}
@@ -263,6 +269,33 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
rowsPerPageOptions={[PER_PAGE]}
/>
)}
{/* Create Stock Take 確認 Dialog */}
<Dialog
open={openConfirmDialog}
onClose={() => setOpenConfirmDialog(false)}
maxWidth="xs"
fullWidth
>
<DialogTitle>{t("Create Stock Take for All Sections")}</DialogTitle>
<DialogContent>
<DialogContentText>
{t("Confirm create stock take for all sections?")}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenConfirmDialog(false)}>
{t("Cancel")}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleCreateStockTake}
disabled={creating}
>
{creating ? <CircularProgress size={20} /> : t("Confirm")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};


+ 259
- 202
src/components/StockTakeManagement/PickerReStockTake.tsx Dosyayı Görüntüle

@@ -21,7 +21,6 @@ import { useState, useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
AllPickedStockTakeListReponse,
getInventoryLotDetailsBySection,
InventoryLotDetailResponse,
saveStockTakeRecord,
SaveStockTakeRecordRequest,
@@ -51,13 +50,13 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
const [loadingDetails, setLoadingDetails] = useState(false);
// 编辑状态
const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null);
const [firstQty, setFirstQty] = useState<string>("");
const [secondQty, setSecondQty] = useState<string>("");
const [firstBadQty, setFirstBadQty] = useState<string>("");
const [secondBadQty, setSecondBadQty] = useState<string>("");
const [remark, setRemark] = useState<string>("");
const [recordInputs, setRecordInputs] = useState<Record<number, {
firstQty: string;
secondQty: string;
firstBadQty: string;
secondBadQty: string;
remark: string;
}>>({});
const [saving, setSaving] = useState(false);
const [batchSaving, setBatchSaving] = useState(false);
const [shortcutInput, setShortcutInput] = useState<string>("");
@@ -115,28 +114,36 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}
}, [selectedSession, total]);
useEffect(() => {
const inputs: Record<number, { firstQty: string; secondQty: string; firstBadQty: string; secondBadQty: string; remark: string }> = {};
inventoryLotDetails.forEach((detail) => {
const firstTotal = detail.firstStockTakeQty != null
? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString()
: "";
const secondTotal = detail.secondStockTakeQty != null
? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString()
: "";
inputs[detail.id] = {
firstQty: firstTotal,
secondQty: secondTotal,
firstBadQty: detail.firstBadQty?.toString() || "",
secondBadQty: detail.secondBadQty?.toString() || "",
remark: detail.remarks || "",
};
});
setRecordInputs(inputs);
}, [inventoryLotDetails]);

useEffect(() => {
loadDetails(page, pageSize);
}, [page, pageSize, loadDetails]);

const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => {
setEditingRecord(detail);
setFirstQty(detail.firstStockTakeQty?.toString() || "");
setSecondQty(detail.secondStockTakeQty?.toString() || "");
setFirstBadQty(detail.firstBadQty?.toString() || "");
setSecondBadQty(detail.secondBadQty?.toString() || "");
setRemark(detail.remarks || "");
}, []);

const handleCancelEdit = useCallback(() => {
setEditingRecord(null);
setFirstQty("");
setSecondQty("");
setFirstBadQty("");
setSecondBadQty("");
setRemark("");
}, []);

const formatNumber = (num: number | null | undefined): string => {
if (num == null || Number.isNaN(num)) return "0";
return num.toLocaleString("en-US", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
};
const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
if (!selectedSession || !currentUserId) {
return;
@@ -145,41 +152,69 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
const qty = isFirstSubmit ? firstQty : secondQty;
const badQty = isFirstSubmit ? firstBadQty : secondBadQty;
if (!qty || !badQty) {
// 用戶輸入為 total 和 bad,需計算 available = total - bad(與 PickerStockTake 一致)
const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty;
const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty;

if (!totalQtyStr) {
onSnackbar(
isFirstSubmit
? t("Please enter QTY and Bad QTY")
: t("Please enter Second QTY and Bad QTY"),
? t("Please enter QTY")
: t("Please enter Second QTY"),
"error"
);
return;
}

const totalQty = parseFloat(totalQtyStr);
const badQty = parseFloat(badQtyStr || "0") || 0;

if (Number.isNaN(totalQty)) {
onSnackbar(t("Invalid QTY"), "error");
return;
}

const availableQty = totalQty - badQty;

if (availableQty < 0) {
onSnackbar(t("Available QTY cannot be negative"), "error");
return;
}

setSaving(true);
try {
const request: SaveStockTakeRecordRequest = {
stockTakeRecordId: detail.stockTakeRecordId || null,
inventoryLotLineId: detail.id,
qty: parseFloat(qty),
badQty: parseFloat(badQty),
remark: isSecondSubmit ? (remark || null) : null,
qty: availableQty,
badQty: badQty,
remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null,
};
console.log('handleSaveStockTake: request:', request);
console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId);
console.log('handleSaveStockTake: currentUserId:', currentUserId);
await saveStockTakeRecord(
const result = await saveStockTakeRecord(
request,
selectedSession.stockTakeId,
currentUserId
);
onSnackbar(t("Stock take record saved successfully"), "success");
handleCancelEdit();
await loadDetails(page, pageSize);

const savedId = result?.id ?? detail.stockTakeRecordId;
setInventoryLotDetails((prev) =>
prev.map((d) =>
d.id === detail.id
? {
...d,
stockTakeRecordId: savedId ?? d.stockTakeRecordId,
firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty,
firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null,
secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty,
secondBadQty: isSecondSubmit ? badQty : d.secondBadQty ?? null,
remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks,
stockTakeRecordStatus: "pass",
}
: d
)
);
} catch (e: any) {
console.error("Save stock take record error:", e);
let errorMessage = t("Failed to save stock take record");
@@ -199,15 +234,13 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
} finally {
setSaving(false);
}
}, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar, page, pageSize, loadDetails]);
}, [selectedSession, recordInputs, t, currentUserId, onSnackbar, page, pageSize, loadDetails]);

const handleBatchSubmitAll = useCallback(async () => {
if (!selectedSession || !currentUserId) {
console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId');
return;
}

console.log('handleBatchSubmitAll: Starting batch save...');
setBatchSaving(true);
try {
const request: BatchSaveStockTakeRecordRequest = {
@@ -217,7 +250,6 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
};

const result = await batchSaveStockTakeRecords(request);
console.log('handleBatchSubmitAll: Result:', result);

onSnackbar(
t("Batch save completed: {{success}} success, {{errors}} errors", {
@@ -273,31 +305,19 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
const newInput = prev + e.key;
if (newInput === '{2fitestall}') {
console.log('✅ Shortcut {2fitestall} detected!');
setTimeout(() => {
if (handleBatchSubmitAllRef.current) {
console.log('Calling handleBatchSubmitAll...');
handleBatchSubmitAllRef.current().catch(err => {
console.error('Error in handleBatchSubmitAll:', err);
});
} else {
console.error('handleBatchSubmitAllRef.current is null');
}
}, 0);
return "";
}
if (newInput.length > 15) {
return "";
}
if (newInput.length > 0 && !newInput.startsWith('{')) {
return "";
}
if (newInput.length > 5 && !newInput.startsWith('{2fi')) {
return "";
}
if (newInput.length > 15) return "";
if (newInput.length > 0 && !newInput.startsWith('{')) return "";
if (newInput.length > 5 && !newInput.startsWith('{2fi')) return "";
return newInput;
});
@@ -315,11 +335,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}, []);

const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
if (detail.stockTakeRecordStatus === "pass") {
if (selectedSession?.status?.toLowerCase() === "completed") {
return true;
}
const recordStatus = detail.stockTakeRecordStatus?.toLowerCase();
if (recordStatus === "pass" || recordStatus === "completed") {
return true;
}
return false;
}, []);
}, [selectedSession?.status]);
const uniqueWarehouses = Array.from(
new Set(
@@ -328,6 +352,9 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
.filter(warehouse => warehouse && warehouse.trim() !== "")
)
).join(", ");

const defaultInputs = { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" };

return (
<Box>
<Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
@@ -339,42 +366,31 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>
{/*
{shortcutInput && (
<Box sx={{ mb: 2, p: 1.5, bgcolor: 'info.light', borderRadius: 1, border: '1px solid', borderColor: 'info.main' }}>
<Typography variant="body2" color="info.dark" fontWeight={500}>
{t("Shortcut Input")}: <strong style={{ fontFamily: 'monospace', fontSize: '1.1em' }}>{shortcutInput}</strong>
</Typography>
</Box>
)}
*/}
{loadingDetails ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Qty")}</TableCell>
<TableCell>{t("Bad Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
@@ -382,7 +398,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<TableBody>
{inventoryLotDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={8} align="center">
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
@@ -390,99 +406,156 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
</TableRow>
) : (
inventoryLotDetails.map((detail) => {
const isEditing = editingRecord?.id === detail.id;
const submitDisabled = isSubmitDisabled(detail);
const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
const inputs = recordInputs[detail.id] ?? defaultInputs;

return (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
<TableCell sx={{
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>
<Stack spacing={0.5}>
<Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstStockTakeQty ? (
<Typography variant="body2">
{t("First")}: {detail.firstStockTakeQty.toFixed(2)}
</Typography>
) : null}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => setSecondQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondStockTakeQty ? (
<TableCell>{detail.uom || "-"}</TableCell>
<TableCell sx={{ minWidth: 300 }}>
<Stack spacing={1}>
{/* First */}
{!submitDisabled && isFirstSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("First")}:</Typography>
<TextField
size="small"
type="number"
value={inputs.firstQty}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({
...prev,
[detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstQty: val }
}));
}}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={inputs.firstBadQty}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({
...prev,
[detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstBadQty: val }
}));
}}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
= {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.firstBadQty || "0"))}
</Typography>
</Stack>
) : detail.firstStockTakeQty != null ? (
<Typography variant="body2">
{t("Second")}: {detail.secondStockTakeQty.toFixed(2)}
{t("First")}:{" "}
{formatNumber((detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0))}{" "}
({formatNumber(detail.firstBadQty ?? 0)}) ={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography>
) : null}
{!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstBadQty != null && detail.firstBadQty > 0 ? (
<Typography variant="body2">
{t("First")}: {detail.firstBadQty.toFixed(2)}
</Typography>
) : (
<Typography variant="body2" sx={{ visibility: 'hidden' }}>
{t("First")}: 0.00
</Typography>
)}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondBadQty != null && detail.secondBadQty > 0 ? (

{/* Second */}
{!submitDisabled && isSecondSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("Second")}:</Typography>
<TextField
size="small"
type="number"
value={inputs.secondQty}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({
...prev,
[detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: val }
}));
}}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={inputs.secondBadQty}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({
...prev,
[detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: val }
}));
}}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
= {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.secondBadQty || "0"))}
</Typography>
</Stack>
) : detail.secondStockTakeQty != null ? (
<Typography variant="body2">
{t("Second")}: {detail.secondBadQty.toFixed(2)}
{t("Second")}:{" "}
{formatNumber((detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0))}{" "}
({formatNumber(detail.secondBadQty ?? 0)}) ={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)}
</Typography>
) : null}
{!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
{!detail.firstStockTakeQty && !detail.secondStockTakeQty && !submitDisabled && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
@@ -490,13 +563,16 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
</Stack>
</TableCell>
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
{!submitDisabled && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
value={inputs.remark}
onChange={(e) => setRecordInputs(prev => ({
...prev,
[detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value }
}))}
sx={{ width: 150 }}
/>
</>
@@ -506,49 +582,30 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
</Typography>
)}
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
) : (
<Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
)}
</TableCell>
{detail.stockTakeRecordStatus === "completed" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
) : detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="default" />
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
) : (
<Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
)}
</TableCell>
<TableCell>
{isEditing ? (
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="contained"
onClick={() => handleSaveStockTake(detail)}
disabled={saving || submitDisabled}
>
{t("Save")}
</Button>
<Button
size="small"
onClick={handleCancelEdit}
>
{t("Cancel")}
</Button>
</Stack>
) : (
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="outlined"
onClick={() => handleStartEdit(detail)}
disabled={submitDisabled}
variant="contained"
onClick={() => handleSaveStockTake(detail)}
disabled={saving || submitDisabled }
>
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
: t("View")}
{t("Save")}
</Button>
)}
</Stack>
</TableCell>
</TableRow>
);


+ 155
- 123
src/components/StockTakeManagement/PickerStockTake.tsx Dosyayı Görüntüle

@@ -55,13 +55,14 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
const [loadingDetails, setLoadingDetails] = useState(false);
// 编辑状态
const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null);
// firstQty / secondQty 保存的是 total = available + bad
const [firstQty, setFirstQty] = useState<string>("");
const [secondQty, setSecondQty] = useState<string>("");
const [firstBadQty, setFirstBadQty] = useState<string>("");
const [secondBadQty, setSecondBadQty] = useState<string>("");
const [recordInputs, setRecordInputs] = useState<Record<number, {
firstQty: string;
secondQty: string;
firstBadQty: string;
secondBadQty: string;
remark: string;
}>>({});
const [savingRecordId, setSavingRecordId] = useState<number | null>(null);
const [remark, setRemark] = useState<string>("");
const [saving, setSaving] = useState(false);
const [batchSaving, setBatchSaving] = useState(false);
@@ -91,7 +92,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}
setPage(0);
}, []);
const loadDetails = useCallback(async (pageNum: number, size: number | string) => {
const loadDetails = useCallback(async (
pageNum: number,
size: number | string,
options?: { silent?: boolean }
) => {
console.log('loadDetails called with:', { pageNum, size, selectedSessionTotal: selectedSession.totalInventoryLotNumber });
setLoadingDetails(true);
try {
@@ -132,44 +137,34 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
setLoadingDetails(false);
}
}, [selectedSession, total]);
useEffect(() => {
loadDetails(page, pageSize);
}, [page, pageSize, loadDetails]);
const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => {
setEditingRecord(detail);

// 编辑时,输入 total = qty + badQty
const firstTotal =
detail.firstStockTakeQty != null
const inputs: Record<number, { firstQty: string; secondQty: string; firstBadQty: string; secondBadQty: string; remark: string }> = {};
inventoryLotDetails.forEach((detail) => {
const firstTotal = detail.firstStockTakeQty != null
? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString()
: "";
const secondTotal =
detail.secondStockTakeQty != null
const secondTotal = detail.secondStockTakeQty != null
? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString()
: "";

setFirstQty(firstTotal);
setSecondQty(secondTotal);
setFirstBadQty(detail.firstBadQty?.toString() || "");
setSecondBadQty(detail.secondBadQty?.toString() || "");
setRemark(detail.remarks || "");
}, []);

const handleCancelEdit = useCallback(() => {
setEditingRecord(null);
setFirstQty("");
setSecondQty("");
setFirstBadQty("");
setSecondBadQty("");
setRemark("");
}, []);
inputs[detail.id] = {
firstQty: firstTotal,
secondQty: secondTotal,
firstBadQty: detail.firstBadQty?.toString() || "",
secondBadQty: detail.secondBadQty?.toString() || "",
remark: detail.remarks || "",
};
});
setRecordInputs(inputs);
}, [inventoryLotDetails]);
useEffect(() => {
loadDetails(page, pageSize);
}, [page, pageSize, loadDetails]);

const formatNumber = (num: number | null | undefined): string => {
if (num == null || Number.isNaN(num)) return "0.00";
if (num == null || Number.isNaN(num)) return "0";
return num.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
};

@@ -184,24 +179,25 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;

// 现在用户输入的是 total 和 bad,需要算 available = total - bad
const totalQtyStr = isFirstSubmit ? firstQty : secondQty;
const badQtyStr = isFirstSubmit ? firstBadQty : secondBadQty;

if (!totalQtyStr || !badQtyStr) {
const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty;
const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty;
// 只檢查 totalQty,Bad Qty 未輸入時預設為 0
if (!totalQtyStr) {
onSnackbar(
isFirstSubmit
? t("Please enter QTY and Bad QTY")
: t("Please enter Second QTY and Bad QTY"),
? t("Please enter QTY")
: t("Please enter Second QTY"),
"error"
);
return;
}
const totalQty = parseFloat(totalQtyStr);
const badQty = parseFloat(badQtyStr);
if (Number.isNaN(totalQty) || Number.isNaN(badQty)) {
onSnackbar(t("Invalid QTY or Bad QTY"), "error");
const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0
if (Number.isNaN(totalQty)) {
onSnackbar(t("Invalid QTY"), "error");
return;
}

@@ -219,7 +215,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
inventoryLotLineId: detail.id,
qty: availableQty, // 保存 available qty
badQty: badQty, // 保存 bad qty
remark: isSecondSubmit ? (remark || null) : null,
remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null,
};
console.log("handleSaveStockTake: request:", request);
console.log("handleSaveStockTake: selectedSession.stockTakeId:", selectedSession.stockTakeId);
@@ -228,10 +224,24 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
await saveStockTakeRecord(request, selectedSession.stockTakeId, currentUserId);

onSnackbar(t("Stock take record saved successfully"), "success");
handleCancelEdit();
await loadDetails(page, pageSize);
//await loadDetails(page, pageSize, { silent: true });
setInventoryLotDetails((prev) =>
prev.map((d) =>
d.id === detail.id
? {
...d,
stockTakeRecordId: d.stockTakeRecordId ?? null, // 首次儲存後可從 response 取得,此處先保留
firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty,
firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null,
secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty,
secondBadQty: isSecondSubmit ? badQty : d.secondBadQty ?? null,
remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks,
stockTakeRecordStatus: "pass",
}
: d
)
);
} catch (e: any) {
console.error("Save stock take record error:", e);
let errorMessage = t("Failed to save stock take record");
@@ -254,18 +264,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
},
[
selectedSession,
firstQty,
secondQty,
firstBadQty,
secondBadQty,
recordInputs,
remark,
handleCancelEdit,
t,
currentUserId,
onSnackbar,
loadDetails,
page,
pageSize,
]
);

@@ -387,11 +390,15 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}, []);

const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
if (detail.stockTakeRecordStatus === "pass") {
if (selectedSession?.status?.toLowerCase() === "completed") {
return true;
}
const recordStatus = detail.stockTakeRecordStatus?.toLowerCase();
if (recordStatus === "pass" || recordStatus === "completed") {
return true;
}
return false;
}, []);
}, [selectedSession?.status]);

const uniqueWarehouses = Array.from(
new Set(
@@ -460,9 +467,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
@@ -478,7 +486,6 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
</TableRow>
) : (
inventoryLotDetails.map((detail) => {
const isEditing = editingRecord?.id === detail.id;
const submitDisabled = isSubmitDisabled(detail);
const isFirstSubmit =
!detail.stockTakeRecordId || !detail.firstStockTakeQty;
@@ -513,19 +520,24 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
</Box>
</Stack>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
{/* Qty + Bad Qty 合并显示/输入 */}
<TableCell sx={{ minWidth: 300 }}>
<Stack spacing={1}>
{/* First */}
{isEditing && isFirstSubmit ? (
{!submitDisabled && isFirstSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("First")}:</Typography>
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
value={recordInputs[detail.id]?.firstQty || ""}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } }));
}}
sx={{
width: 130,
minWidth: 130,
@@ -533,14 +545,23 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
height: "1.4375em",
padding: "4px 8px",
},
"& .MuiInputBase-input::placeholder": {
color: "grey.400", // MUI light grey
opacity: 1,
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
value={recordInputs[detail.id]?.firstBadQty || ""}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } }));
}}
sx={{
width: 130,
minWidth: 130,
@@ -548,14 +569,18 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
height: "1.4375em",
padding: "4px 8px",
},
"& .MuiInputBase-input::placeholder": {
color: "grey.400", // MUI light grey
opacity: 1,
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
=
{formatNumber(
parseFloat(firstQty || "0") -
parseFloat(firstBadQty || "0")
parseFloat(recordInputs[detail.id]?.firstQty || "0") -
parseFloat(recordInputs[detail.id]?.firstBadQty || "0")
)}
</Typography>
</Stack>
@@ -576,14 +601,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
) : null}

{/* Second */}
{isEditing && isSecondSubmit ? (
{!submitDisabled && isSecondSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("Second")}:</Typography>
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => setSecondQty(e.target.value)}
value={recordInputs[detail.id]?.secondQty || ""}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } }));
}}
sx={{
width: 130,
minWidth: 130,
@@ -597,8 +627,13 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
value={recordInputs[detail.id]?.secondBadQty || ""}
inputProps={{ min: 0, step: "any" }}
onChange={(e) => {
const val = e.target.value;
if (val.includes("-")) return;
setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } }));
}}
sx={{
width: 130,
minWidth: 130,
@@ -612,8 +647,8 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<Typography variant="body2">
=
{formatNumber(
parseFloat(secondQty || "0") -
parseFloat(secondBadQty || "0")
parseFloat(recordInputs[detail.id]?.secondQty || "0") -
parseFloat(recordInputs[detail.id]?.secondBadQty || "0")
)}
</Typography>
</Stack>
@@ -635,7 +670,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({

{!detail.firstStockTakeQty &&
!detail.secondStockTakeQty &&
!isEditing && (
!submitDisabled && (
<Typography
variant="body2"
color="text.secondary"
@@ -648,13 +683,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({

{/* Remark */}
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
{!submitDisabled && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
value={recordInputs[detail.id]?.remark || ""}
onChange={(e) => setRecordInputs(prev => ({
...prev,
[detail.id]: {
...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }),
remark: e.target.value
}
}))}
sx={{ width: 150 }}
/>
</>
@@ -665,32 +706,38 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
)}
</TableCell>

<TableCell>{detail.uom || "-"}</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="success"
/>
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="warning"
/>
) : (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus || "")}
color="default"
/>
)}
</TableCell>
{detail.stockTakeRecordStatus === "completed" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="success"
/>
) : detail.stockTakeRecordStatus === "pass" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="default"
/>
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="warning"
/>
) : (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus || "")}
color="default"
/>
)}
</TableCell>

<TableCell>
{isEditing ? (
<Stack direction="row" spacing={1}>
<Button
size="small"
@@ -700,24 +747,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
>
{t("Save")}
</Button>
<Button size="small" onClick={handleCancelEdit}>
{t("Cancel")}
</Button>
</Stack>
) : (
<Button
size="small"
variant="outlined"
onClick={() => handleStartEdit(detail)}
disabled={submitDisabled}
>
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
: t("View")}
</Button>
)}
</TableCell>
</TableRow>
);


+ 4
- 0
src/components/StyledDataGrid/StyledDataGrid.tsx Dosyayı Görüntüle

@@ -1,6 +1,8 @@
import { styled } from "@mui/material";
import { DataGrid ,DataGridProps,zhTW} from "@mui/x-data-grid";
import { forwardRef } from "react";
import { useTranslation } from "react-i18next";

const StyledDataGridBase = styled(DataGrid)(({ theme }) => ({
"--unstable_DataGrid-radius": 0,
"& .MuiDataGrid-columnHeaders": {
@@ -29,12 +31,14 @@ const StyledDataGridBase = styled(DataGrid)(({ theme }) => ({
},
}));
const StyledDataGrid = forwardRef<HTMLDivElement, DataGridProps>((props, ref) => {
const { t } = useTranslation();
return (
<StyledDataGridBase
ref={ref}
{...props}
localeText={{
...zhTW.components.MuiDataGrid.defaultProps.localeText,
labelRowsPerPage: t("Rows per page"),
...props.localeText, // 允许覆盖
}}
/>


+ 32
- 3
src/i18n/zh/common.json Dosyayı Görüntüle

@@ -13,7 +13,7 @@
"Overall Time Remaining": "總剩餘時間",
"Reset": "重置",
"Search": "搜索",
"This lot is rejected, please scan another lot.": "此批次已封存,請掃描另一個批號。",
"This lot is rejected, please scan another lot.": "此批次發現問題,請掃描另一個批號。",
"Process Start Time": "工序開始時間",
"Stock Req. Qty": "需求數",
"Staff No Required": "員工編號必填",
@@ -124,6 +124,7 @@

"Today": "今天",
"Yesterday": "昨天",
"Two Days Ago": "前天",
"Input Equipment is not match with process": "輸入的設備與流程不匹配",
"Staff No is required": "員工編號必填",
@@ -132,6 +133,8 @@
"Production Date": "生產日期",
"QC Check Item": "QC品檢項目",
"QC Category": "QC品檢模板",
"QC Item All": "QC 綜合管理",
"qcItemAll": "QC 綜合管理",
"qcCategory": "品檢模板",
"QC Check Template": "QC檢查模板",
"Mail": "郵件",
@@ -148,6 +151,7 @@
"Production Date":"生產日期",
"QC Check Item":"QC品檢項目",
"QC Category":"QC品檢模板",
"QC Item All":"QC 綜合管理",
"qcCategory":"品檢模板",
"QC Check Template":"QC檢查模板",
"QR Code Handle":"二維碼列印及下載",
@@ -284,7 +288,8 @@
"Please scan equipment code": "請掃描設備編號",
"Equipment Code": "設備編號",
"Seq": "步驟",
"Item Name": "物料名稱",
"SEQ": "步驟",
"Item Name": "產品名稱",
"Job Order Info": "工單信息",
"Matching Stock": "工單對料",
"No data found": "沒有找到資料",
@@ -469,5 +474,29 @@
"Delete Success": "刪除成功",
"Delete Failed": "刪除失敗",
"Create Printer": "新增列印機",
"Report": "報告"
"Report": "報告",
"Issue": "問題",
"Note:": "注意:",
"Required Qty": "需求數量",
"Verified Qty": "確認數量",
"Max": "最大值",
"Min": "最小值",
"Max": "最大值",
"This form is for reporting issues only. You must report either missing items or bad items.": "此表單僅用於報告問題。您必須報告缺少的物品或不良物品。",
"Pick Execution Issue Form": "提料問題表單",
"Missing items": "缺少物品",
"Total (Verified + Bad + Missing) must equal Required quantity": "總數必須等於需求數量",
"Missing item Qty": "缺少物品數量",
"seq": "序號",
"Job Order Pick Execution": "工單提料",
"Bad Item Qty": "不良物品數量",
"Issue Remark": "問題備註",
"At least one issue must be reported": "至少需要報告一個問題",
"Qty is required": "數量是必填項",
"Verified quantity cannot exceed received quantity": "確認數量不能超過接收數量",
"Handled By": "處理者",
"submit": "提交",
"Received Qty": "接收數量"

}

+ 38
- 1
src/i18n/zh/inventory.json Dosyayı Görüntüle

@@ -7,14 +7,26 @@
"Qty": "盤點數量",
"UoM": "單位",
"mat": "物料",
"variance": "差異",
"Variance %": "差異百分比",
"fg": "成品",
"Back to List": "返回列表",
"Record Status": "記錄狀態",
"Stock take record status updated to not match": "盤點記錄狀態更新為數值不符",
"available": "可用",
"Issue Qty": "問題數量",
"tke": "盤點",
"Submit Bad Item": "提交不良品",
"Remain available Quantity": "剩餘可用數量",
"Submitting...": "提交中...",
"Item-lotNo-ExpiryDate": "貨品-批號-到期日",
"Submit Miss Item": "提交缺貨",
"Confirm": "確認",
"Confirm create stock take for all sections?": "確認為所有區域創建盤點?",
"Item-lotNo-ExpiryDate": "貨品-批號-到期日",
"not available": "不可用",
"Book Qty": "帳面庫存",
"Submit Quantity": "實際問題數量",
"Batch Submit All": "批量提交所有",
"Batch Save All": "批量保存所有",
"Batch Submit All": "批量提交所有",
@@ -39,6 +51,7 @@
"DO Order Code": "送貨單編號",
"JO Order Code": "工單編號",
"Picker Name": "提料員",
"Rows per page": "每頁行數",

"rejected": "已拒絕",
"miss": "缺貨",
@@ -206,6 +219,9 @@
"Loading": "加載中",
"adj": "調整",
"nor": "正常",
"trf": "轉倉",


"Stock transfer successful": "轉倉成功",
"Failed to transfer stock": "轉倉失敗",
@@ -218,6 +234,27 @@
"Target Location": "目標倉位",
"Original Qty": "原有數量",
"Qty To Be Transferred": "待轉數量",
"Submit": "提交"
"Submit": "提交",

"Printer": "列印機",
"Print Qty": "列印數量",
"Print": "列印",
"Print sent": "已送出列印",
"Print failed": "列印失敗",

"Stock Adjustment": "庫存調整",
"Edit mode": "編輯模式",
"Add entry": "新增倉存",

"productLotNo": "產品批號",
"dnNo": "送貨單編號",
"Optional - system will generate": "選填,系統將自動生成",
"Add": "新增",
"Opening Inventory": "開倉",
"Reason for adjustment": "調整原因",
"No lot no entered, will be generated by system.": "未輸入批號,將由系統生成。",
"Reason for removal": "移除原因",
"Confirm remove": "確認移除",
"Adjusted Qty": "調整後倉存"

}

+ 19
- 4
src/i18n/zh/jo.json Dosyayı Görüntüle

@@ -93,6 +93,11 @@
"Bag Code": "包裝袋編號",

"Sequence": "序",
"Seq": "步驟",
"SEQ": "步驟",
"Today": "今天",
"Yesterday": "昨天",
"Two Days Ago": "前天",
"Item Code": "成品/半成品編號",
"Paused": "已暫停",
"paused": "已暫停",
@@ -115,7 +120,7 @@
"Pick Order Detail": "提料單細節",
"Finished Job Order Record": "已完成工單記錄",
"No. of Items to be Picked": "需提料數量",
"No. of Items with Issue During Pick": "提料過程中出現問題數量",
"No. of Items with Issue During Pick": "問題數量",
"Pick Start Time": "提料開始時間",
"Pick End Time": "提料結束時間",
"FG / WIP Item": "成品/半成品",
@@ -141,7 +146,7 @@
"Start QR Scan": "開始QR掃碼",
"Stop QR Scan": "停止QR掃碼",
"Rows per page": "每頁行數",
"Job Order Item Name": "工單物料名稱",
"Job Order Item Name": "工單產品名稱",
"Job Order Code": "工單編號",
"View Details": "查看詳情",
"Skip": "跳過",
@@ -322,7 +327,7 @@
"acceptedQty": "接受數量",
"bind": "綁定",
"expiryDate": "有效期",
"itemName": "物料名稱",
"itemName": "產品名稱",
"itemNo": "成品編號",
"not default warehosue": "不是默認倉庫",
"printQty": "打印數量",
@@ -347,7 +352,7 @@
"receivedQty": "接收數量",
"stock in information": "庫存信息",
"No Uom": "沒有單位",
"Print Pick Record": "打印頭紙",
"Print Pick Record": "打印頭紙",
"Printed Successfully.": "成功列印",
"Submit All Scanned": "提交所有已掃描項目",
"Submitting...": "提交中...",
@@ -557,5 +562,15 @@
"Production Time Remaining": "生產剩餘時間",
"Process": "工序",
"Start": "開始",
"This form is for reporting issues only. You must report either missing items or bad items.": "此表單僅用於報告問題。您必須報告缺少的物品或不良物品。",
"Pick Execution Issue Form": "提料問題表單",
"Missing items": "缺少物品",
"Total (Verified + Bad + Missing) must equal Required quantity": "總數必須等於需求數量",
"Missing item Qty": "缺少物品數量",
"Bad Item Qty": "不良物品數量",
"Issue Remark": "問題備註",
"seq": "序號",
"Handled By": "處理者",
"Job Order Pick Execution": "工單提料",
"Finish": "完成"
}

+ 1
- 0
src/i18n/zh/pickOrder.json Dosyayı Görüntüle

@@ -367,6 +367,7 @@
"View Details": "查看詳情",
"No Item": "沒有貨品",
"None": "沒有",
"This form is for reporting issues only. You must report either missing items or bad items.": "此表單僅用於報告問題。您必須報告缺少的物品或不良物品。",
"Add Selected Items to Created Items": "將已選擇的貨品添加到已建立的貨品中",
"All pick orders created successfully": "所有提料單建立成功",
"Failed to create group": "建立分組失敗",


+ 1
- 1
src/i18n/zh/qcItemAll.json Dosyayı Görüntüle

@@ -42,7 +42,7 @@
"Select Qc Item": "選擇品檢項目",
"Select Type": "選擇類型",
"Item Code": "物料編號",
"Item Name": "物料名稱",
"Item Name": "產品名稱",
"Qc Category Code": "品檢模板編號",
"Qc Category Name": "品檢模板名稱",
"Qc Item Code": "品檢項目編號",


+ 5
- 0
tailwind.config.js Dosyayı Görüntüle

@@ -18,6 +18,11 @@ module.exports = {
border: "var(--border)",
muted: "var(--muted)",
},
fontSize: {
xs: ["0.8125rem", { lineHeight: "1.25rem" }],
sm: ["0.9375rem", { lineHeight: "1.375rem" }],
base: ["1.0625rem", { lineHeight: "1.625rem" }],
},
},
},
plugins: [],


Yükleniyor…
İptal
Kaydet