|
- "use client";
-
- import {
- Box,
- Button,
- Stack,
- Typography,
- Chip,
- CircularProgress,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- TextField,
- } from "@mui/material";
- import { useState, useCallback, useEffect, useRef } from "react";
- import { useTranslation } from "react-i18next";
- import {
- AllPickedStockTakeListReponse,
- getInventoryLotDetailsBySection,
- InventoryLotDetailResponse,
- saveStockTakeRecord,
- SaveStockTakeRecordRequest,
- BatchSaveStockTakeRecordRequest,
- batchSaveStockTakeRecords,
- } from "@/app/api/stockTake/actions";
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- import dayjs from "dayjs";
- import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
-
- interface PickerStockTakeProps {
- selectedSession: AllPickedStockTakeListReponse;
- onBack: () => void;
- onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
- }
-
- const PickerStockTake: React.FC<PickerStockTakeProps> = ({
- selectedSession,
- onBack,
- onSnackbar,
- }) => {
- const { t } = useTranslation(["inventory", "common"]);
- const { data: session } = useSession() as { data: SessionWithTokens | null };
-
- 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 [saving, setSaving] = useState(false);
- const [batchSaving, setBatchSaving] = useState(false);
- const [shortcutInput, setShortcutInput] = useState<string>("");
-
- const currentUserId = session?.id ? parseInt(session.id) : undefined;
- const handleBatchSubmitAllRef = useRef<() => Promise<void>>();
-
- useEffect(() => {
- const loadDetails = async () => {
- setLoadingDetails(true);
- try {
- const details = await getInventoryLotDetailsBySection(
- selectedSession.stockTakeSession,
- selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
- );
- setInventoryLotDetails(Array.isArray(details) ? details : []);
- } catch (e) {
- console.error(e);
- setInventoryLotDetails([]);
- } finally {
- setLoadingDetails(false);
- }
- };
- loadDetails();
- }, [selectedSession]);
-
- 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 handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
- if (!selectedSession || !currentUserId) {
- return;
- }
-
- 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) {
- onSnackbar(
- isFirstSubmit
- ? t("Please enter QTY and Bad QTY")
- : t("Please enter Second QTY and Bad QTY"),
- "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,
- };
- console.log('handleSaveStockTake: request:', request);
- console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId);
- console.log('handleSaveStockTake: currentUserId:', currentUserId);
- await saveStockTakeRecord(
- request,
- selectedSession.stockTakeId,
- currentUserId
- );
-
- onSnackbar(t("Stock take record saved successfully"), "success");
- handleCancelEdit();
-
- const details = await getInventoryLotDetailsBySection(
- selectedSession.stockTakeSession,
- selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
- );
- setInventoryLotDetails(Array.isArray(details) ? details : []);
- } catch (e: any) {
- console.error("Save stock take record error:", e);
- let errorMessage = t("Failed to save stock take record");
-
- if (e?.message) {
- errorMessage = e.message;
- } else if (e?.response) {
- try {
- const errorData = await e.response.json();
- errorMessage = errorData.message || errorData.error || errorMessage;
- } catch {
- // ignore
- }
- }
-
- onSnackbar(errorMessage, "error");
- } finally {
- setSaving(false);
- }
- }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]);
-
- 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 = {
- stockTakeId: selectedSession.stockTakeId,
- stockTakeSection: selectedSession.stockTakeSession,
- stockTakerId: currentUserId,
- };
-
- const result = await batchSaveStockTakeRecords(request);
- console.log('handleBatchSubmitAll: Result:', result);
-
- onSnackbar(
- t("Batch save completed: {{success}} success, {{errors}} errors", {
- success: result.successCount,
- errors: result.errorCount,
- }),
- result.errorCount > 0 ? "warning" : "success"
- );
-
- const details = await getInventoryLotDetailsBySection(
- selectedSession.stockTakeSession,
- selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
- );
- setInventoryLotDetails(Array.isArray(details) ? details : []);
- } catch (e: any) {
- console.error("handleBatchSubmitAll: Error:", e);
- let errorMessage = t("Failed to batch save stock take records");
-
- if (e?.message) {
- errorMessage = e.message;
- } else if (e?.response) {
- try {
- const errorData = await e.response.json();
- errorMessage = errorData.message || errorData.error || errorMessage;
- } catch {
- // ignore
- }
- }
-
- onSnackbar(errorMessage, "error");
- } finally {
- setBatchSaving(false);
- }
- }, [selectedSession, t, currentUserId, onSnackbar]);
-
- useEffect(() => {
- handleBatchSubmitAllRef.current = handleBatchSubmitAll;
- }, [handleBatchSubmitAll]);
-
- useEffect(() => {
- const handleKeyPress = (e: KeyboardEvent) => {
- const target = e.target as HTMLElement;
- if (target && (
- target.tagName === 'INPUT' ||
- target.tagName === 'TEXTAREA' ||
- target.isContentEditable
- )) {
- return;
- }
-
- if (e.ctrlKey || e.metaKey || e.altKey) {
- return;
- }
-
- if (e.key.length === 1) {
- setShortcutInput(prev => {
- 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 "";
- }
-
- return newInput;
- });
- } else if (e.key === 'Backspace') {
- setShortcutInput(prev => prev.slice(0, -1));
- } else if (e.key === 'Escape') {
- setShortcutInput("");
- }
- };
-
- window.addEventListener('keydown', handleKeyPress);
- return () => {
- window.removeEventListener('keydown', handleKeyPress);
- };
- }, []);
-
- const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
- if (detail.stockTakeRecordStatus === "pass") {
- return true;
- }
- return false;
- }, []);
-
- return (
- <Box>
- <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
- {t("Back to List")}
- </Button>
- <Typography variant="h6" sx={{ mb: 2 }}>
- {t("Stock Take Section")}: {selectedSession.stockTakeSession}
- </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>
- ) : (
- <TableContainer component={Paper}>
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>{t("Warehouse Location")}</TableCell>
- <TableCell>{t("Item")}</TableCell>
- {/*<TableCell>{t("Item Name")}</TableCell>*/}
- {/*<TableCell>{t("Lot No")}</TableCell>*/}
- <TableCell>{t("Expiry Date")}</TableCell>
- <TableCell>{t("Qty")}</TableCell>
- <TableCell>{t("Bad Qty")}</TableCell>
- {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/}
- <TableCell>{t("Remark")}</TableCell>
-
- <TableCell>{t("UOM")}</TableCell>
- <TableCell>{t("Status")}</TableCell>
- <TableCell>{t("Record Status")}</TableCell>
- <TableCell>{t("Action")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {inventoryLotDetails.length === 0 ? (
- <TableRow>
- <TableCell colSpan={12} align="center">
- <Typography variant="body2" color="text.secondary">
- {t("No data")}
- </Typography>
- </TableCell>
- </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;
-
- return (
- <TableRow key={detail.id}>
- <TableCell>{detail.warehouseCode || "-"}</TableCell>
- <TableCell sx={{
- maxWidth: 100,
- wordBreak: 'break-word',
- whiteSpace: 'normal',
- lineHeight: 1.5
- }}>{detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""}</TableCell>
- {/*
- <TableCell
- sx={{
- maxWidth: 200,
- wordBreak: 'break-word',
- whiteSpace: 'normal',
- lineHeight: 1.5
- }}
- >
- {detail.itemName || "-"}
- </TableCell>*/}
- {/*<TableCell>{detail.lotNo || "-"}</TableCell>*/}
- <TableCell>
- {detail.expiryDate
- ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
- : "-"}
- </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 ? (
- <Typography variant="body2">
- {t("Second")}: {detail.secondStockTakeQty.toFixed(2)}
- </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 ? (
- <Typography variant="body2">
- {t("First")}: {detail.firstBadQty.toFixed(2)}
- </Typography>
- ) : null}
-
- {isEditing && isSecondSubmit ? (
- <TextField
- size="small"
- type="number"
- value={secondBadQty}
- onChange={(e) => setSecondBadQty(e.target.value)}
- sx={{ width: 100 }}
-
- />
- ) : detail.secondBadQty ? (
- <Typography variant="body2">
- {t("Second")}: {detail.secondBadQty.toFixed(2)}
- </Typography>
- ) : null}
-
- {!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
- <Typography variant="body2" color="text.secondary">
- -
- </Typography>
- )}
-
- </Stack>
- </TableCell>
- <TableCell sx={{ width: 180 }}>
- {isEditing && isSecondSubmit ? (
- <>
- <Typography variant="body2">{t("Remark")}</Typography>
- <TextField
- size="small"
- value={remark}
- onChange={(e) => setRemark(e.target.value)}
- sx={{ width: 150 }}
- // If you want a single-line input, remove multiline/rows:
- // multiline
- // rows={2}
- />
- </>
- ) : (
- <Typography variant="body2">
- {detail.remarks || "-"}
- </Typography>
- )}
- </TableCell>
- <TableCell>{detail.uom || "-"}</TableCell>
- <TableCell>
- {detail.status ? (
- <Chip size="small" label={t(detail.status)} color="default" />
- ) : (
- "-"
- )}
- </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>
- <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>
- ) : (
- <Button
- size="small"
- variant="outlined"
- onClick={() => handleStartEdit(detail)}
- disabled={submitDisabled}
- >
- {!detail.stockTakeRecordId
- ? t("Input")
- : detail.stockTakeRecordStatus === "notMatch"
- ? t("Input")
- : t("View")}
- </Button>
- )}
- </TableCell>
- </TableRow>
- );
- })
- )}
- </TableBody>
- </Table>
- </TableContainer>
- )}
- </Box>
- );
- };
-
- export default PickerStockTake;
|