| @@ -200,6 +200,21 @@ export const fetchPoInClient = cache(async (id: number) => { | |||
| }); | |||
| }); | |||
| export interface PurchaseOrderSummary { | |||
| id: number; | |||
| code: string; | |||
| status: string; | |||
| orderDate: string; | |||
| estimatedArrivalDate: string; | |||
| supplierName: string; | |||
| escalated: boolean; | |||
| } | |||
| export const fetchPoSummariesClient = cache(async (ids: number[]) => { | |||
| return serverFetchJson<PurchaseOrderSummary[]>(`${BASE_API_URL}/po/summary`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| export const fetchPoListClient = cache( | |||
| async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| @@ -43,6 +43,7 @@ import { | |||
| checkPolAndCompletePo, | |||
| fetchPoInClient, | |||
| fetchPoListClient, | |||
| fetchPoSummariesClient, | |||
| startPo, | |||
| } from "@/app/api/po/actions"; | |||
| import { | |||
| @@ -201,6 +202,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| purchaseOrder.pol || [], | |||
| ); | |||
| const [polInputList, setPolInputList] = useState<PolInputResult[]>([]) | |||
| const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; | |||
| useEffect(() => { | |||
| setPolInputList( | |||
| (purchaseOrder.pol ?? []).map(() => ({ | |||
| @@ -209,7 +211,21 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| } as PolInputResult)) | |||
| ); | |||
| }, [purchaseOrder.pol]); | |||
| useEffect(() => { | |||
| try { | |||
| const raw = sessionStorage.getItem("po-detail-selection"); | |||
| if (raw) { | |||
| const parsed = JSON.parse(raw) as { id: number; code: string; status: string; supplier: string | null }[]; | |||
| if (Array.isArray(parsed) && parsed.length > 0) { | |||
| setPoList(parsed as PoResult[]); | |||
| sessionStorage.removeItem("po-detail-selection"); // 可选:用一次就删,避免下次从别处进还看到旧数据 | |||
| } | |||
| } | |||
| } catch (e) { | |||
| console.warn("sessionStorage getItem/parse failed", e); | |||
| } | |||
| }, []); | |||
| const pathname = usePathname() | |||
| const searchParams = useSearchParams(); | |||
| @@ -227,6 +243,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| const router = useRouter(); | |||
| const [poList, setPoList] = useState<PoResult[]>([]); | |||
| const [isPoListLoading, setIsPoListLoading] = useState(false); | |||
| const [selectedPoId, setSelectedPoId] = useState(po.id); | |||
| const [focusField, setFocusField] = useState<HTMLInputElement>(); | |||
| @@ -240,15 +257,26 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| } | |||
| }) | |||
| const fetchPoList = useCallback(async () => { | |||
| setIsPoListLoading(true); | |||
| try { | |||
| if (selectedIdsParam) { | |||
| const selectedIds = selectedIdsParam.split(',').map(id => parseInt(id)); | |||
| const promises = selectedIds.map(id => fetchPoInClient(id)); | |||
| const results = await Promise.all(promises); | |||
| setPoList(results.filter(Boolean)); | |||
| const MAX_IDS = 20; // 一次最多加载 20 个,防止卡死 | |||
| const allIds = selectedIdsParam | |||
| .split(',') | |||
| .map(id => parseInt(id)) | |||
| .filter(id => !Number.isNaN(id)); | |||
| const limitedIds = allIds.slice(0, MAX_IDS); | |||
| if (allIds.length > MAX_IDS) { | |||
| console.warn( | |||
| `selectedIds too many (${allIds.length}), only loading first ${MAX_IDS}.` | |||
| ); | |||
| } | |||
| const result = await fetchPoSummariesClient(limitedIds); | |||
| setPoList(result as any); | |||
| } else { | |||
| const result = await fetchPoListClient({ limit: 20, offset: 0 }); | |||
| if (result && result.records) { | |||
| setPoList(result.records); | |||
| @@ -256,6 +284,8 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| } | |||
| } catch (error) { | |||
| console.error("Failed to fetch PO list:", error); | |||
| } finally { | |||
| setIsPoListLoading(false); | |||
| } | |||
| }, [selectedIdsParam]); | |||
| @@ -311,11 +341,11 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| fetchPoDetail(currentPoId); | |||
| } | |||
| }, [currentPoId, fetchPoDetail]); | |||
| /* | |||
| useEffect(() => { | |||
| fetchPoList(); | |||
| }, [fetchPoList]); | |||
| */ | |||
| useEffect(() => { | |||
| if (currentPoId) { | |||
| setSelectedPoId(parseInt(currentPoId)); | |||
| @@ -490,12 +520,15 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| const highlightColor = (Number(receivedTotal.replace(/,/g, '')) <= 0) ? "red" : "inherit"; | |||
| return ( | |||
| <> | |||
| <TableRow | |||
| sx={{ "& > *": { borderBottom: "unset" }, | |||
| color: "black" | |||
| }} | |||
| onClick={() => changeStockInLines(row.id)} | |||
| > | |||
| {/* <TableCell> | |||
| <IconButton | |||
| disabled={purchaseOrder.status.toLowerCase() === "pending"} | |||
| @@ -729,13 +762,15 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}> | |||
| {/* left side select po */} | |||
| <Grid item xs={4}> | |||
| <PoSearchList | |||
| poList={poList} | |||
| selectedPoId={selectedPoId} | |||
| onSelect={handlePoSelect} | |||
| /> | |||
| </Grid> | |||
| <Stack spacing={1}> | |||
| <PoSearchList | |||
| poList={poList} | |||
| selectedPoId={selectedPoId} | |||
| onSelect={handlePoSelect} | |||
| /> | |||
| </Stack> | |||
| </Grid> | |||
| {/* right side po info */} | |||
| <Grid item xs={8}> | |||
| @@ -41,6 +41,7 @@ const PoSearch: React.FC<Props> = ({ | |||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({estimatedArrivalDate : dayjsToDateString(dayjs(), "input")}); | |||
| const { t } = useTranslation(["purchaseOrder", "dashboard"]); | |||
| const router = useRouter(); | |||
| const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; | |||
| const [pagingController, setPagingController] = useState( | |||
| defaultPagingController, | |||
| ); | |||
| @@ -83,7 +84,18 @@ const PoSearch: React.FC<Props> = ({ | |||
| (po: PoResult) => { | |||
| setSelectedPoIds([]); | |||
| setSelectAll(false); | |||
| router.push(`/po/edit?id=${po.id}&start=true&selectedIds=${po.id}`); | |||
| const listForDetail = [ | |||
| { id: po.id, code: po.code, status: po.status, supplier: po.supplier ?? null }, | |||
| ]; | |||
| try { | |||
| sessionStorage.setItem( | |||
| PO_DETAIL_SELECTION_KEY, | |||
| JSON.stringify(listForDetail), | |||
| ); | |||
| } catch (e) { | |||
| console.warn("sessionStorage setItem failed", e); | |||
| } | |||
| router.push(`/po/edit?id=${po.id}&start=true`); | |||
| }, | |||
| [router], | |||
| ); | |||
| @@ -111,12 +123,26 @@ const PoSearch: React.FC<Props> = ({ | |||
| // navigate to PoDetail page | |||
| const handleGoToPoDetail = useCallback(() => { | |||
| if (selectedPoIds.length > 0) { | |||
| const selectedIdsParam = selectedPoIds.join(','); | |||
| const firstPoId = selectedPoIds[0]; | |||
| router.push(`/po/edit?id=${firstPoId}&start=true&selectedIds=${selectedIdsParam}`); | |||
| if (selectedPoIds.length === 0) return; | |||
| const selectedList = filteredPo.filter((p) => selectedPoIds.includes(p.id)); | |||
| const listForDetail = selectedList.map((p) => ({ | |||
| id: p.id, | |||
| code: p.code, | |||
| status: p.status, | |||
| supplier: p.supplier ?? null, | |||
| })); | |||
| try { | |||
| sessionStorage.setItem("po-detail-selection", JSON.stringify(listForDetail)); | |||
| } catch (e) { | |||
| console.warn("sessionStorage setItem failed", e); | |||
| } | |||
| }, [selectedPoIds, router]); | |||
| const selectedIdsParam = selectedPoIds.join(","); | |||
| const firstPoId = selectedPoIds[0]; | |||
| router.push(`/po/edit?id=${firstPoId}&start=true&selectedIds=${selectedIdsParam}`); | |||
| }, [selectedPoIds, filteredPo, router]); | |||
| const itemColumn = useCallback((value: string | undefined) => { | |||
| if (!value) { | |||
| @@ -11,6 +11,7 @@ | |||
| "Variance %": "差異百分比", | |||
| "fg": "成品", | |||
| "Back to List": "返回列表", | |||
| "Start Stock Take Date": "盤點日期", | |||
| "Record Status": "記錄狀態", | |||
| "Stock take record status updated to not match": "盤點記錄狀態更新為數值不符", | |||
| "available": "可用", | |||
| @@ -9,6 +9,7 @@ | |||
| "Edit Product / Material": "編輯產品 / 材料", | |||
| "Product / Material": "產品 / 材料", | |||
| "Product / Material Details": "產品 / 材料詳情", | |||
| "Qc items": "QC 項目", | |||
| "Qc Category": "質檢模板", | |||
| "Name": "名稱", | |||
| @@ -29,12 +29,12 @@ | |||
| "Do you want to start?": "確定開始嗎?", | |||
| "Start": "開始", | |||
| "Pick Order Code(s)": "提料單編號", | |||
| "Delivery Order Code(s)": "送貨單編號", | |||
| "Delivery Order Code(s)": "提料單編號", | |||
| "Start Success": "開始成功", | |||
| "Truck Lance Code": "車牌號碼", | |||
| "Pick Order Codes": "提料單編號", | |||
| "Pick Order Lines": "提料單行數", | |||
| "Delivery Order Codes": "送貨單編號", | |||
| "Delivery Order Codes": "提料單編號", | |||
| "Delivery Order Lines": "送貨單行數", | |||
| "Lines Per Pick Order": "每提料單行數", | |||
| "Pick Orders Details": "提料單詳情", | |||
| @@ -394,9 +394,15 @@ | |||
| "submitStockIn": "提交入庫", | |||
| "not default warehosue": "不是默認倉庫", | |||
| "printQty": "打印數量", | |||
| "Shop": "商店名稱", | |||
| "warehouse": "倉庫", | |||
| "Add Record": "添加記錄", | |||
| "Clean Record": "清空記錄", | |||
| "Select": "選擇", | |||
| "Close": "關閉", | |||
| "Truck": "車輛", | |||
| "Date": "日期", | |||
| "Delivery Order Code": "送貨單編號", | |||
| "Escalation Info": "升級信息", | |||
| "Escalation Result": "升級結果", | |||
| "acceptQty must not greater than": "接受數量不能大於", | |||
| @@ -426,10 +432,17 @@ | |||
| "No entries available": "該樓層未有需處理訂單", | |||
| "Today": "是日", | |||
| "Tomorrow": "翌日", | |||
| "No Stock Available": "沒有庫存可用", | |||
| "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", | |||
| "Day After Tomorrow": "後日", | |||
| "Select Date": "請選擇日期", | |||
| "Search by Shop": "搜尋商店", | |||
| "Search by Truck": "搜尋貨車", | |||
| "Print DN & Label": "列印提料單和送貨單標籤", | |||
| "Print Label": "列印送貨單標籤", | |||
| "Not Yet Finished Released Do Pick Orders": "未完成提料單", | |||
| "Not yet finished released do pick orders": "未完成提料單", | |||
| "Released orders not yet completed - click lane to select and assign": "未完成提料單- 點擊貨車班次選擇並分配", | |||
| "Ticket Release Table": "查看提貨情況", | |||
| "Please take one pick order before printing the draft.": "請先從「撳單/提料單詳情」頁面下方選取提料單,再列印草稿。", | |||
| "No released pick order records found.": "目前沒有可用的提料單。", | |||