| @@ -3,6 +3,7 @@ | |||||
| import { cache } from 'react'; | import { cache } from 'react'; | ||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; // 改为 serverFetchJson | import { serverFetchJson } from "@/app/utils/fetchUtil"; // 改为 serverFetchJson | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { stockTakeDebugLog } from "@/components/StockTakeManagement/stockTakeDebugLog"; | |||||
| export interface RecordsRes<T> { | export interface RecordsRes<T> { | ||||
| records: T[]; | records: T[]; | ||||
| @@ -51,6 +52,31 @@ export interface InventoryLotDetailResponse { | |||||
| approverTime?: string | string[] | null; | approverTime?: string | string[] | null; | ||||
| } | } | ||||
| /** | |||||
| * `approverInventoryLotDetailsAll*`: | |||||
| * - `total` = 全域 `inventory_lot_line` 中 `status = available` 筆數(與 DB COUNT 一致) | |||||
| * - `filteredRecordCount` = 目前 tab/篩選後筆數(分頁用) | |||||
| */ | |||||
| export interface ApproverInventoryLotDetailsRecordsRes extends RecordsRes<InventoryLotDetailResponse> { | |||||
| filteredRecordCount?: number; | |||||
| totalWaitingForApprover?: number; | |||||
| totalApproved?: number; | |||||
| } | |||||
| function normalizeApproverInventoryLotDetailsRes( | |||||
| raw: ApproverInventoryLotDetailsRecordsRes | |||||
| ): ApproverInventoryLotDetailsRecordsRes { | |||||
| const waiting = Number(raw.totalWaitingForApprover ?? 0) || 0; | |||||
| const approved = Number(raw.totalApproved ?? 0) || 0; | |||||
| return { | |||||
| records: Array.isArray(raw.records) ? raw.records : [], | |||||
| total: Number(raw.total ?? 0) || 0, | |||||
| filteredRecordCount: Number(raw.filteredRecordCount ?? 0) || 0, | |||||
| totalWaitingForApprover: waiting, | |||||
| totalApproved: approved, | |||||
| }; | |||||
| } | |||||
| export const getInventoryLotDetailsBySection = async ( | export const getInventoryLotDetailsBySection = async ( | ||||
| stockTakeSection: string, | stockTakeSection: string, | ||||
| stockTakeId?: number | null, | stockTakeId?: number | null, | ||||
| @@ -122,13 +148,13 @@ export const getApproverInventoryLotDetailsAll = async ( | |||||
| } | } | ||||
| const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAll?${params.toString()}`; | const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAll?${params.toString()}`; | ||||
| const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>( | |||||
| const response = await serverFetchJson<ApproverInventoryLotDetailsRecordsRes>( | |||||
| url, | url, | ||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| }, | }, | ||||
| ); | ); | ||||
| return response; | |||||
| return normalizeApproverInventoryLotDetailsRes(response); | |||||
| } | } | ||||
| export const getApproverInventoryLotDetailsAllPending = async ( | export const getApproverInventoryLotDetailsAllPending = async ( | ||||
| stockTakeId?: number | null, | stockTakeId?: number | null, | ||||
| @@ -142,7 +168,8 @@ export const getApproverInventoryLotDetailsAllPending = async ( | |||||
| params.append("stockTakeId", String(stockTakeId)); | params.append("stockTakeId", String(stockTakeId)); | ||||
| } | } | ||||
| const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllPending?${params.toString()}`; | const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllPending?${params.toString()}`; | ||||
| return serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(url, { method: "GET" }); | |||||
| const response = await serverFetchJson<ApproverInventoryLotDetailsRecordsRes>(url, { method: "GET" }); | |||||
| return normalizeApproverInventoryLotDetailsRes(response); | |||||
| } | } | ||||
| export const getApproverInventoryLotDetailsAllApproved = async ( | export const getApproverInventoryLotDetailsAllApproved = async ( | ||||
| stockTakeId?: number | null, | stockTakeId?: number | null, | ||||
| @@ -156,7 +183,8 @@ export const getApproverInventoryLotDetailsAllApproved = async ( | |||||
| params.append("stockTakeId", String(stockTakeId)); | params.append("stockTakeId", String(stockTakeId)); | ||||
| } | } | ||||
| const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllApproved?${params.toString()}`; | const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllApproved?${params.toString()}`; | ||||
| return serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(url, { method: "GET" }); | |||||
| const response = await serverFetchJson<ApproverInventoryLotDetailsRecordsRes>(url, { method: "GET" }); | |||||
| return normalizeApproverInventoryLotDetailsRes(response); | |||||
| } | } | ||||
| export const importStockTake = async (data: FormData) => { | export const importStockTake = async (data: FormData) => { | ||||
| @@ -242,6 +270,20 @@ export const saveStockTakeRecord = async ( | |||||
| console.log('saveStockTakeRecord: request:', request); | console.log('saveStockTakeRecord: request:', request); | ||||
| console.log('saveStockTakeRecord: stockTakeId:', stockTakeId); | console.log('saveStockTakeRecord: stockTakeId:', stockTakeId); | ||||
| console.log('saveStockTakeRecord: stockTakerId:', stockTakerId); | console.log('saveStockTakeRecord: stockTakerId:', stockTakerId); | ||||
| // #region agent log | |||||
| stockTakeDebugLog( | |||||
| "actions.ts:saveStockTakeRecord", | |||||
| "server action saveStockTakeRecord ok", | |||||
| "H3", | |||||
| { | |||||
| stockTakeId, | |||||
| stockTakerId, | |||||
| inventoryLotLineId: request.inventoryLotLineId, | |||||
| hasRecordId: request.stockTakeRecordId != null, | |||||
| resultId: result?.id ?? null, | |||||
| } | |||||
| ); | |||||
| // #endregion | |||||
| return result; | return result; | ||||
| } catch (error: any) { | } catch (error: any) { | ||||
| // 尝试从错误响应中提取消息 | // 尝试从错误响应中提取消息 | ||||
| @@ -271,12 +313,26 @@ export interface BatchSaveStockTakeRecordResponse { | |||||
| errors: string[]; | errors: string[]; | ||||
| } | } | ||||
| export const batchSaveStockTakeRecords = cache(async (data: BatchSaveStockTakeRecordRequest) => { | export const batchSaveStockTakeRecords = cache(async (data: BatchSaveStockTakeRecordRequest) => { | ||||
| return serverFetchJson<BatchSaveStockTakeRecordResponse>(`${BASE_API_URL}/stockTakeRecord/batchSaveStockTakeRecords`, | |||||
| const r = await serverFetchJson<BatchSaveStockTakeRecordResponse>(`${BASE_API_URL}/stockTakeRecord/batchSaveStockTakeRecords`, | |||||
| { | { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| }) | }) | ||||
| // #region agent log | |||||
| stockTakeDebugLog( | |||||
| "actions.ts:batchSaveStockTakeRecords", | |||||
| "server batch picker result", | |||||
| "H4", | |||||
| { | |||||
| stockTakeId: data.stockTakeId, | |||||
| stockTakeSection: data.stockTakeSection, | |||||
| successCount: r.successCount, | |||||
| errorCount: r.errorCount, | |||||
| } | |||||
| ); | |||||
| // #endregion | |||||
| return r | |||||
| }) | }) | ||||
| // Add these interfaces and functions | // Add these interfaces and functions | ||||
| @@ -325,6 +381,19 @@ export const saveApproverStockTakeRecord = async ( | |||||
| body: JSON.stringify(request), | body: JSON.stringify(request), | ||||
| }, | }, | ||||
| ); | ); | ||||
| // #region agent log | |||||
| stockTakeDebugLog( | |||||
| "actions.ts:saveApproverStockTakeRecord", | |||||
| "server action saveApproverStockTakeRecord ok", | |||||
| "H3", | |||||
| { | |||||
| stockTakeId, | |||||
| stockTakeRecordId: request.stockTakeRecordId ?? null, | |||||
| lastSelect: request.lastSelect ?? null, | |||||
| hasApproverQty: request.approverQty != null, | |||||
| } | |||||
| ); | |||||
| // #endregion | |||||
| return result; | return result; | ||||
| } catch (error: any) { | } catch (error: any) { | ||||
| if (error?.response) { | if (error?.response) { | ||||
| @@ -354,7 +423,7 @@ export const batchSaveApproverStockTakeRecords = cache(async (data: BatchSaveApp | |||||
| ) | ) | ||||
| export const batchSaveApproverStockTakeRecordsAll = cache(async (data: BatchSaveApproverStockTakeAllRequest) => { | export const batchSaveApproverStockTakeRecordsAll = cache(async (data: BatchSaveApproverStockTakeAllRequest) => { | ||||
| return serverFetchJson<BatchSaveApproverStockTakeRecordResponse>( | |||||
| const r = await serverFetchJson<BatchSaveApproverStockTakeRecordResponse>( | |||||
| `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecordsAll`, | `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecordsAll`, | ||||
| { | { | ||||
| method: "POST", | method: "POST", | ||||
| @@ -362,6 +431,20 @@ export const batchSaveApproverStockTakeRecordsAll = cache(async (data: BatchSave | |||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| } | } | ||||
| ) | ) | ||||
| // #region agent log | |||||
| stockTakeDebugLog( | |||||
| "actions.ts:batchSaveApproverStockTakeRecordsAll", | |||||
| "server batch approver-all result", | |||||
| "H4", | |||||
| { | |||||
| stockTakeId: data.stockTakeId, | |||||
| approverId: data.approverId, | |||||
| successCount: r.successCount, | |||||
| errorCount: r.errorCount, | |||||
| } | |||||
| ); | |||||
| // #endregion | |||||
| return r | |||||
| }) | }) | ||||
| export const updateStockTakeRecordStatusToNotMatch = async ( | export const updateStockTakeRecordStatusToNotMatch = async ( | ||||
| @@ -65,7 +65,9 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||||
| const { t } = useTranslation("ticketReleaseTable"); | const { t } = useTranslation("ticketReleaseTable"); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | const abilities = session?.abilities ?? session?.user?.abilities ?? []; | ||||
| const canManageDoPickOps = abilities.includes(AUTH.ADMIN); | |||||
| // 依照 DB `authority.authority = 'ADMIN'` 的逻辑:仅 abilities 明確包含 ADMIN 才允許操作 | |||||
| // (避免 abilities 裡出現前後空白導致 includes 判斷失效) | |||||
| const canManageDoPickOps = abilities.some((a) => a.trim() === AUTH.ADMIN); | |||||
| const [queryDate, setQueryDate] = useState<Dayjs>(() => dayjs()); | const [queryDate, setQueryDate] = useState<Dayjs>(() => dayjs()); | ||||
| const [selectedFloor, setSelectedFloor] = useState<string>(""); | const [selectedFloor, setSelectedFloor] = useState<string>(""); | ||||
| @@ -2763,6 +2763,7 @@ const sortedData = [...sourceData].sort((a, b) => { | |||||
| disabled={ | disabled={ | ||||
| (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) || | (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) || | ||||
| lot.stockOutLineStatus === 'completed' || | lot.stockOutLineStatus === 'completed' || | ||||
| lot.stockOutLineStatus === 'checked' || | |||||
| lot.noLot === true || | lot.noLot === true || | ||||
| !lot.lotId || | !lot.lotId || | ||||
| (Number(lot.stockOutLineId) > 0 && | (Number(lot.stockOutLineId) > 0 && | ||||
| @@ -397,8 +397,8 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => { | const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => { | ||||
| const requiredQty = lot.requiredQty || 0; | const requiredQty = lot.requiredQty || 0; | ||||
| const stockOutLineQty = lot.stockOutLineQty || 0; | |||||
| return Math.max(0, requiredQty - stockOutLineQty); | |||||
| const availableQty = lot.availableQty || 0; | |||||
| return Math.max(0, requiredQty + availableQty); | |||||
| }, []); | }, []); | ||||
| // Add QR scanner context | // Add QR scanner context | ||||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| @@ -506,7 +506,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| const stockOutLineUpdate = await updateStockOutLineStatus({ | const stockOutLineUpdate = await updateStockOutLineStatus({ | ||||
| id: selectedLotForQr.stockOutLineId, | id: selectedLotForQr.stockOutLineId, | ||||
| status: 'checked', | status: 'checked', | ||||
| qty: selectedLotForQr.stockOutLineQty || 0 | |||||
| qty: 0 | |||||
| }); | }); | ||||
| console.log(" Stock out line updated to 'checked':", stockOutLineUpdate); | console.log(" Stock out line updated to 'checked':", stockOutLineUpdate); | ||||
| @@ -361,13 +361,9 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| try { | try { | ||||
| // FIXED: 计算累计拣货数量 | // FIXED: 计算累计拣货数量 | ||||
| const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty; | const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty; | ||||
| console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0); | |||||
| console.log(" DEBUG - Current submit:", qty); | |||||
| console.log(" DEBUG - Total picked:", totalPickedForThisLot); | |||||
| console.log("�� DEBUG - Required qty:", selectedLot.requiredQty); | |||||
| // FIXED: 状态应该基于累计拣货数量 | // FIXED: 状态应该基于累计拣货数量 | ||||
| let newStatus = 'partially_completed'; | |||||
| let newStatus = 'completed'; | |||||
| if (totalPickedForThisLot >= selectedLot.requiredQty) { | if (totalPickedForThisLot >= selectedLot.requiredQty) { | ||||
| newStatus = 'completed'; | newStatus = 'completed'; | ||||
| } | } | ||||
| @@ -388,16 +384,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| return; | return; | ||||
| } | } | ||||
| if (qty > 0) { | |||||
| const inventoryLotLineUpdate = await updateInventoryLotLineQuantities({ | |||||
| inventoryLotLineId: lotId, | |||||
| qty: qty, | |||||
| status: 'available', | |||||
| operation: 'pick' | |||||
| }); | |||||
| console.log("Inventory lot line updated:", inventoryLotLineUpdate); | |||||
| } | |||||
| // RE-ENABLE: Check if pick order should be completed | // RE-ENABLE: Check if pick order should be completed | ||||
| if (newStatus === 'completed') { | if (newStatus === 'completed') { | ||||
| @@ -32,6 +32,7 @@ import { SessionWithTokens } from "@/config/authConfig"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | ||||
| import { AUTH } from "@/authorities"; | |||||
| import { | import { | ||||
| @@ -103,6 +104,9 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| const [openModal, setOpenModal] = useState<boolean>(false); | const [openModal, setOpenModal] = useState<boolean>(false); | ||||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | |||||
| // 依照 DB `authority.authority = 'ADMIN'` 的逻辑:僅 abilities 明確包含 ADMIN 才能操作 | |||||
| const canManageUpdateJo = abilities.some((a) => a.trim() === AUTH.ADMIN); | |||||
| type ProcessFilter = "all" | "drink" | "other"; | type ProcessFilter = "all" | "drink" | "other"; | ||||
| const [suggestedLocationCode, setSuggestedLocationCode] = useState<string | null>(null); | const [suggestedLocationCode, setSuggestedLocationCode] = useState<string | null>(null); | ||||
| @@ -275,6 +279,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| fetchProcesses(); | fetchProcesses(); | ||||
| }, [fetchProcesses]); | }, [fetchProcesses]); | ||||
| const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => { | const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => { | ||||
| if (!canManageUpdateJo) return; | |||||
| if (!process.jobOrderId) { | if (!process.jobOrderId) { | ||||
| alert(t("Invalid Job Order Id")); | alert(t("Invalid Job Order Id")); | ||||
| return; | return; | ||||
| @@ -308,7 +313,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, [t, fetchProcesses]); | |||||
| }, [t, fetchProcesses, canManageUpdateJo]); | |||||
| const openConfirm = useCallback((message: string, action: () => Promise<void>) => { | const openConfirm = useCallback((message: string, action: () => Promise<void>) => { | ||||
| setConfirmMessage(message); | setConfirmMessage(message); | ||||
| @@ -590,13 +595,16 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="small" | size="small" | ||||
| disabled={!canManageUpdateJo} | |||||
| onClick={() => | onClick={() => | ||||
| openConfirm( | |||||
| t("Confirm to update this Job Order?"), | |||||
| async () => { | |||||
| await handleUpdateJo(process); | |||||
| } | |||||
| ) | |||||
| canManageUpdateJo | |||||
| ? openConfirm( | |||||
| t("Confirm to update this Job Order?"), | |||||
| async () => { | |||||
| await handleUpdateJo(process); | |||||
| } | |||||
| ) | |||||
| : undefined | |||||
| } | } | ||||
| > | > | ||||
| {t("Update Job Order")} | {t("Update Job Order")} | ||||
| @@ -317,12 +317,14 @@ useEffect(() => { | |||||
| try { | try { | ||||
| const parseStartTime = performance.now(); | const parseStartTime = performance.now(); | ||||
| const data: QrCodeInfo = JSON.parse(scannedValues); | |||||
| const normalizedScannedValues = scannedValues.replace(/\\"/g, '"'); | |||||
| const data: QrCodeInfo = JSON.parse(normalizedScannedValues); | |||||
| const parseTime = performance.now() - parseStartTime; | const parseTime = performance.now() - parseStartTime; | ||||
| // console.log(`%c Parsed scan data`, "color:green", data); | // console.log(`%c Parsed scan data`, "color:green", data); | ||||
| //console.log(`⏱️ [QR SCANNER PROCESS] JSON parse time: ${parseTime.toFixed(2)}ms`); | //console.log(`⏱️ [QR SCANNER PROCESS] JSON parse time: ${parseTime.toFixed(2)}ms`); | ||||
| const content = scannedValues.substring(1, scannedValues.length - 1); | |||||
| const content = normalizedScannedValues.substring(1, normalizedScannedValues.length - 1); | |||||
| data.value = content; | data.value = content; | ||||
| const setResultStartTime = performance.now(); | const setResultStartTime = performance.now(); | ||||
| @@ -9,6 +9,9 @@ | |||||
| "Approver Pending": "審核待處理", | "Approver Pending": "審核待處理", | ||||
| "Approver Approved": "審核通過", | "Approver Approved": "審核通過", | ||||
| "Approver Time": "審核時間", | "Approver Time": "審核時間", | ||||
| "Total need stock take": "總需盤點數量", | |||||
| "Waiting for Approver": "待審核數量", | |||||
| "Total Approved": "已審核數量", | |||||
| "mat": "物料", | "mat": "物料", | ||||
| "variance": "差異", | "variance": "差異", | ||||
| "Plan Start Date": "計劃開始日期", | "Plan Start Date": "計劃開始日期", | ||||