"use client"; import { useCallback, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import SearchBox, { Criterion } from "@/components/SearchBox"; import { ItemsResult, ItemsResultResponse } from "@/app/api/settings/item"; import { Box, Button, Card, CardContent, CircularProgress, Grid, Typography } from "@mui/material"; import Download from "@mui/icons-material/Download"; import Upload from "@mui/icons-material/Upload"; import { priceFormatter, OUTPUT_DATETIME_FORMAT } from "@/app/utils/formatUtil"; import dayjs, { Dayjs } from "dayjs"; import axiosInstance from "@/app/(main)/axios/axiosInstance"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; type SearchQuery = { code: string; name: string; }; type ItemPriceSearchComponent = React.FC & { Loading: React.FC; }; type SearchParamNames = keyof SearchQuery; const ItemPriceSearch: ItemPriceSearchComponent = () => { const { t } = useTranslation(["inventory", "common"]); const [item, setItem] = useState(null); const [isSearching, setIsSearching] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const [isUploading, setIsUploading] = useState(false); const criteria: Criterion[] = useMemo( () => [ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Name"), paramName: "name", type: "text" }, ], [t], ); const fetchExactItem = useCallback(async (query: SearchQuery) => { const trimmedCode = query.code.trim(); const trimmedName = query.name.trim(); if (!trimmedCode && !trimmedName) { setItem(null); return; } setIsSearching(true); try { const params = { code: trimmedCode, name: trimmedName, pageNum: 1, pageSize: 20, }; const res = await axiosInstance.get( `${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { params }, ); if (!res?.data?.records || res.data.records.length === 0) { setItem(null); return; } const records = res.data.records as ItemsResult[]; const exactMatch = records.find((r) => { const codeMatch = !trimmedCode || (r.code && String(r.code).toLowerCase() === trimmedCode.toLowerCase()); const nameMatch = !trimmedName || (r.name && String(r.name).toLowerCase() === trimmedName.toLowerCase()); return codeMatch && nameMatch; }); setItem(exactMatch ?? null); } catch { setItem(null); } finally { setIsSearching(false); } }, []); const handleSearch = useCallback( (inputs: Record) => { const query: SearchQuery = { code: inputs.code ?? "", name: inputs.name ?? "", }; fetchExactItem(query); }, [fetchExactItem], ); const handleReset = useCallback(() => { setItem(null); }, []); const fileInputRef = useRef(null); const handleDownloadTemplate = useCallback(async () => { setIsDownloading(true); try { const res = await axiosInstance.get( `${NEXT_PUBLIC_API_URL}/items/marketUnitPrice/template`, { responseType: "blob" }, ); const url = URL.createObjectURL(res.data as Blob); const a = document.createElement("a"); a.href = url; a.download = "market_unit_price_template.xlsx"; a.click(); URL.revokeObjectURL(url); } catch { alert(t("Download failed", { ns: "common" })); } finally { setIsDownloading(false); } }, [t]); const handleUploadClick = useCallback(() => { fileInputRef.current?.click(); }, []); const handleUploadChange = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setIsUploading(true); const formData = new FormData(); formData.append("file", file); try { const res = await axiosInstance.post<{ success: boolean; updated?: number; errors?: string[]; }>( `${NEXT_PUBLIC_API_URL}/items/marketUnitPrice/import`, formData, { headers: { "Content-Type": "multipart/form-data" } }, ); const data = res.data; if (!data.success) { const errMsg = data.errors?.length > 0 ? `${t("Upload failed", { ns: "common" })}\n\n${data.errors.join("\n")}` : t("Upload failed", { ns: "common" }); alert(errMsg); return; } const updated = data.updated ?? 0; if (data.errors?.length) { const successLabel = t("Upload successful", { ns: "common" }); const countLabel = t("item(s) updated", { ns: "common" }); const rowErrorsLabel = t("Upload row errors", { ns: "common" }); const msg = `${successLabel} ${updated} ${countLabel}\n\n${rowErrorsLabel}\n${data.errors.join("\n")}`; alert(msg); } else { alert(t("Upload successful", { ns: "common" })); } if (item) fetchExactItem({ code: item.code ?? "", name: item.name ?? "" }); } catch { alert(t("Upload failed", { ns: "common" })); } finally { setIsUploading(false); e.target.value = ""; } }, [item, fetchExactItem, t], ); const avgPrice = useMemo(() => { if (item?.averageUnitPrice == null || item.averageUnitPrice === "") return null; const n = Number(item.averageUnitPrice); return Number.isFinite(n) ? n : null; }, [item?.averageUnitPrice]); return ( <> criteria={criteria} onSearch={handleSearch} onReset={handleReset} extraActions={ <> } /> {isSearching && ( {t("Searching")}... )} {!isSearching && !item && ( {t("No item selected")} )} {!isSearching && item && ( {/* Box 1: Item info */} {item.code} {item.name} {[ item.type ? t(item.type.toUpperCase(), { ns: "common" }) : null, item.purchaseUnit ?? null, ] .filter(Boolean) .join(" · ") || "—"} {/* Box 2: Avg unit price (from items table) */} {t("Average unit price", { ns: "inventory" })} {avgPrice != null && avgPrice !== 0 ? `HKD ${priceFormatter.format(avgPrice)}` : t("No Purchase Order After 2026-01-01", { ns: "common" })} {/* Box 3: Latest market unit price & update date (from items table) */} {t("Latest market unit price", { ns: "inventory" })} {item.latestMarketUnitPrice != null && Number(item.latestMarketUnitPrice) !== 0 ? `HKD ${priceFormatter.format(Number(item.latestMarketUnitPrice))}` : t("No Import Record", { ns: "common" })} {item.latestMupUpdatedDate != null && item.latestMupUpdatedDate !== "" ? (() => { const raw = item.latestMupUpdatedDate; let d: Dayjs | null = null; if (Array.isArray(raw) && raw.length >= 5) { const [y, m, day, h, min] = raw as number[]; d = dayjs(new Date(y, (m ?? 1) - 1, day ?? 1, h ?? 0, min ?? 0)); } else if (typeof raw === "string") { d = dayjs(raw); } return d?.isValid() ? d.format(OUTPUT_DATETIME_FORMAT) : String(raw); })() : item.latestMarketUnitPrice != null && Number(item.latestMarketUnitPrice) !== 0 ? "—" : ""} )} ); }; ItemPriceSearch.Loading = function Loading() { return
Loading...
; }; export default ItemPriceSearch;