import { ApiError, createApiClient } from "../lib/apiClient"; import { authorizedPostBlobDownload } from "../lib/batchFileHttp"; import type { LabelReportData, LabelReportQueryInput, ReportsPrintLogListItem, ReportsPrintLogListResult, ReportsPrintLogQueryInput, TemplatePrintStatListItem, TemplatePrintStatListResult, TemplatePrintStatQueryInput, } from "../types/reports"; const api = createApiClient({ getToken: () => { try { return localStorage.getItem("access_token") ?? localStorage.getItem("token") ?? null; } catch { return null; } }, }); const REPORTS_PREFIX = "/reports"; function str(raw: unknown, c: string, p: string, fallback = ""): string { if (!raw || typeof raw !== "object") return fallback; const o = raw as Record; const v = o[c] ?? o[p]; if (v === null || v === undefined) return fallback; return String(v); } function num(raw: unknown, c: string, p: string, fallback = 0): number { if (!raw || typeof raw !== "object") return fallback; const o = raw as Record; const v = o[c] ?? o[p]; const n = typeof v === "number" ? v : Number(v); return Number.isFinite(n) ? n : fallback; } function normPrintItem(raw: unknown): ReportsPrintLogListItem { if (!raw || typeof raw !== "object") { return { taskId: "", labelCode: "", productName: "None", productCategoryName: "None", labelCategoryName: "None", categoryName: "None", templateText: "", printedAt: "", printedByName: "None", locationText: "None", locationId: "", expiryDateText: "None", }; } const o = raw as Record; return { taskId: str(o, "taskId", "TaskId"), labelCode: str(o, "labelCode", "LabelCode") || "None", productName: str(o, "productName", "ProductName", "None") || "None", productCategoryName: str(o, "productCategoryName", "ProductCategoryName", "") || str(o, "categoryName", "CategoryName", "None") || "None", labelCategoryName: str(o, "labelCategoryName", "LabelCategoryName", "None") || "None", categoryName: str(o, "categoryName", "CategoryName", "None") || "None", templateText: str(o, "templateText", "TemplateText", ""), printedAt: str(o, "printedAt", "PrintedAt", ""), printedByName: str(o, "printedByName", "PrintedByName", "None") || "None", locationText: str(o, "locationText", "LocationText", "None") || "None", locationId: str(o, "locationId", "LocationId", ""), expiryDateText: str(o, "expiryDateText", "ExpiryDateText", "None") || "None", }; } function buildPrintLogQuery( input: ReportsPrintLogQueryInput, opts: { includePaging: boolean } = { includePaging: true } ) { const q: Record = { Sorting: input.sorting, PartnerId: input.partnerId, GroupId: input.groupId, LocationId: input.locationId, StartDate: input.startDate, EndDate: input.endDate, Keyword: input.keyword, }; if (opts.includePaging) { q.SkipCount = input.skipCount; q.MaxResultCount = input.maxResultCount; } return q; } export async function getReportsPrintLogList( input: ReportsPrintLogQueryInput, signal?: AbortSignal ): Promise { const raw = await api.requestJson({ path: `${REPORTS_PREFIX}/print-log-list`, method: "GET", query: buildPrintLogQuery(input, { includePaging: true }), signal, }); const o = (raw && typeof raw === "object" ? raw : {}) as Record; const itemsRaw = o.items ?? o.Items; const list = Array.isArray(itemsRaw) ? itemsRaw.map(normPrintItem) : []; const total = num(o, "totalCount", "TotalCount", list.length); return { items: list, totalCount: total }; } function normLabelSlice(x: unknown): { name: string; count: number } | null { if (!x || typeof x !== "object") return null; const o = x as Record; const name = str( o, "categoryName", "CategoryName", str(o, "name", "Name", str(o, "labelCategoryName", "LabelCategoryName", "Category")) ); if (!name.trim()) return null; const count = num( o, "count", "Count", num(o, "printCount", "PrintCount", num(o, "totalPrinted", "TotalPrinted", 0)) ); return { name: name.trim(), count }; } function normTrendPoint(x: unknown): { date: string; count: number } | null { if (!x || typeof x !== "object") return null; const o = x as Record; const date = str( o, "date", "Date", str(o, "day", "Day", str(o, "printDate", "PrintDate", "")) ); if (!date.trim()) return null; const count = num(o, "count", "Count", num(o, "volume", "Volume", 0)); return { date: date.trim(), count }; } function normTopProduct(x: unknown) { if (!x || typeof x !== "object") return null; const o = x as Record; const productName = str(o, "productName", "ProductName", ""); if (!productName.trim()) return null; return { productName: productName.trim(), categoryName: str(o, "categoryName", "CategoryName", "None") || "None", totalPrinted: num( o, "totalPrinted", "TotalPrinted", num(o, "printCount", "PrintCount", 0) ), usagePercent: num(o, "usagePercent", "UsagePercent", 0), }; } function normLabelReport(raw: unknown): LabelReportData { if (!raw || typeof raw !== "object") { return { summary: { totalLabelsPrinted: 0, totalLabelsPrintedPrevPeriod: 0, totalLabelsPrintedChangeRate: 0, hottestCategoryName: "None", hottestCategoryCount: 0, topProductName: "None", topProductCount: 0, avgDailyPrints: 0, avgDailyPrintsChangeRate: 0, }, labelsByCategory: [], printVolumeTrend: [], mostUsedProducts: [], }; } const root = raw as Record; const sumRaw = root.summary ?? root.Summary; const s = (sumRaw && typeof sumRaw === "object" ? sumRaw : {}) as Record; const labelsRaw = root.labelsByCategory ?? root.LabelsByCategory; const trendRaw = root.printVolumeTrend ?? root.PrintVolumeTrend; const productsRaw = root.mostUsedProducts ?? root.MostUsedProducts; const labelsByCategory: { name: string; count: number }[] = []; if (Array.isArray(labelsRaw)) { for (const x of labelsRaw) { const n = normLabelSlice(x); if (n) labelsByCategory.push(n); } } const printVolumeTrend: { date: string; count: number }[] = []; if (Array.isArray(trendRaw)) { for (const x of trendRaw) { const p = normTrendPoint(x); if (p) printVolumeTrend.push(p); } } const mostUsedProducts: { productName: string; categoryName: string; totalPrinted: number; usagePercent: number; }[] = []; if (Array.isArray(productsRaw)) { for (const x of productsRaw) { const p = normTopProduct(x); if (p) mostUsedProducts.push(p); } } return { summary: { totalLabelsPrinted: num(s, "totalLabelsPrinted", "TotalLabelsPrinted", 0), totalLabelsPrintedPrevPeriod: num( s, "totalLabelsPrintedPrevPeriod", "TotalLabelsPrintedPrevPeriod", 0 ), totalLabelsPrintedChangeRate: num( s, "totalLabelsPrintedChangeRate", "TotalLabelsPrintedChangeRate", 0 ), hottestCategoryName: str( s, "hottestLabelCategoryName", "HottestLabelCategoryName", str( s, "mostPopularLabelCategoryName", "MostPopularLabelCategoryName", str(s, "hottestCategoryName", "HottestCategoryName", "None") ) ) || "None", hottestCategoryCount: num( s, "hottestLabelCategoryCount", "HottestLabelCategoryCount", num(s, "hottestCategoryCount", "HottestCategoryCount", 0) ), topProductName: str( s, "topProductName", "TopProductName", str(s, "mostUsedProductName", "MostUsedProductName", "None") ) || "None", topProductCount: num(s, "topProductCount", "TopProductCount", 0), avgDailyPrints: num(s, "avgDailyPrints", "AvgDailyPrints", 0), avgDailyPrintsChangeRate: num( s, "avgDailyPrintsChangeRate", "AvgDailyPrintsChangeRate", 0 ), }, labelsByCategory: labelsByCategory, printVolumeTrend: printVolumeTrend, mostUsedProducts: mostUsedProducts, }; } function buildLabelReportQuery(input: LabelReportQueryInput) { return { PartnerId: input.partnerId, GroupId: input.groupId, LocationId: input.locationId, StartDate: input.startDate, EndDate: input.endDate, Keyword: input.keyword, }; } export async function getLabelReport( input: LabelReportQueryInput, signal?: AbortSignal ): Promise { const raw = await api.requestJson({ path: `${REPORTS_PREFIX}/label-report`, method: "GET", query: buildLabelReportQuery(input), signal, }); return normLabelReport(raw); } function normTemplatePrintStatItem(raw: unknown): TemplatePrintStatListItem { if (!raw || typeof raw !== "object") { return { templateId: null, templateName: "None", printedCount: 0 }; } const o = raw as Record; const tid = str(o, "templateId", "TemplateId", ""); return { templateId: tid.trim() ? tid : null, templateName: str(o, "templateName", "TemplateName", "None") || "None", printedCount: num(o, "printedCount", "PrintedCount", 0), }; } function buildTemplatePrintStatQuery(input: TemplatePrintStatQueryInput) { return { SkipCount: input.skipCount, MaxResultCount: input.maxResultCount, Sorting: input.sorting ?? "PrintedCount desc", PartnerId: input.partnerId, GroupId: input.groupId, LocationId: input.locationId, StartDate: input.startDate, EndDate: input.endDate, Keyword: input.keyword, }; } /** 按模板汇总打印数量(`GET /api/app/reports/template-print-stat-list`) */ export async function getTemplatePrintStatList( input: TemplatePrintStatQueryInput, signal?: AbortSignal, ): Promise { const raw = await api.requestJson({ path: `${REPORTS_PREFIX}/template-print-stat-list`, method: "GET", query: buildTemplatePrintStatQuery(input), signal, }); const o = (raw && typeof raw === "object" ? raw : {}) as Record; const itemsRaw = o.items ?? o.Items; const list = Array.isArray(itemsRaw) ? itemsRaw.map(normTemplatePrintStatItem) : []; const total = num(o, "totalCount", "TotalCount", list.length); return { items: list, totalCount: total, pageIndex: num(o, "pageIndex", "PageIndex", input.skipCount), pageSize: num(o, "pageSize", "PageSize", input.maxResultCount), totalPages: num(o, "totalPages", "TotalPages", 0), }; } // --- 下载、重打:不走 JSON 解包,单独 fetch --- function joinUrl(baseUrl: string, path: string): string { if (!baseUrl) return path; const b = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; const p = path.startsWith("/") ? path : `/${path}`; return `${b}${p}`; } function toQueryString(params: Record): string { const qs = new URLSearchParams(); for (const [k, v] of Object.entries(params)) { if (v === undefined || v === null || v === "") continue; if (typeof v === "boolean") { qs.set(k, v ? "true" : "false"); continue; } qs.set(k, String(v)); } const s = qs.toString(); return s ? `?${s}` : ""; } function getTokenForFetch(): string | null { try { return localStorage.getItem("access_token") ?? localStorage.getItem("token") ?? null; } catch { return null; } } function parseFileNameFromContentDisposition(h: string | null): string | null { if (!h) return null; const m = /filename\*?=(?:UTF-8''|)([^;]+)/i.exec(h); if (m?.[1]) return decodeURIComponent(m[1].trim().replace(/^["']|["']$/g, "")); return null; } function buildLabelReportExportQuery(input: LabelReportQueryInput) { return toQueryString({ PartnerId: input.partnerId, GroupId: input.groupId, LocationId: input.locationId, StartDate: input.startDate, EndDate: input.endDate, Keyword: input.keyword, } as Record); } function buildPrintLogExportQuery(input: ReportsPrintLogQueryInput) { return toQueryString({ Sorting: input.sorting ?? "PrintedAt desc", PartnerId: input.partnerId, GroupId: input.groupId, LocationId: input.locationId, StartDate: input.startDate, EndDate: input.endDate, Keyword: input.keyword, } as Record); } export async function exportPrintLogPdf(input: ReportsPrintLogQueryInput): Promise { const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:19001"; const path = joinUrl( baseUrl, `/api/app${REPORTS_PREFIX}/export-print-log-pdf${buildPrintLogExportQuery(input)}` ); const token = getTokenForFetch(); const res = await fetch(path, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {} }); const ct = res.headers.get("content-type") ?? ""; if (!res.ok) { if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const p = payload as { error?: { message?: string } }; const msg = p?.error?.message?.trim() || "Export failed."; throw new ApiError(msg, res.status, payload); } const t = await res.text().catch(() => ""); throw new ApiError(t || "Export failed.", res.status, t); } if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const p = payload as { error?: { message?: string } }; const msg = p?.error?.message?.trim() || "Export failed."; throw new ApiError(msg, res.status, payload); } const blob = await res.blob(); const name = parseFileNameFromContentDisposition(res.headers.get("content-disposition")) || "print-log-export.pdf"; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; a.click(); URL.revokeObjectURL(url); } export async function exportPrintLogExcel(input: ReportsPrintLogQueryInput, signal?: AbortSignal): Promise { await authorizedPostBlobDownload({ path: `${REPORTS_PREFIX}/export-print-log-excel`, query: { Sorting: input.sorting ?? "PrintedAt desc", PartnerId: input.partnerId, GroupId: input.groupId, LocationId: input.locationId, StartDate: input.startDate, EndDate: input.endDate, Keyword: input.keyword, }, defaultFileName: "print-log-export.xlsx", signal, }); } export async function exportLabelReportPdf(input: LabelReportQueryInput): Promise { const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:19001"; const path = joinUrl( baseUrl, `/api/app${REPORTS_PREFIX}/export-label-report-pdf${buildLabelReportExportQuery(input)}` ); const token = getTokenForFetch(); const res = await fetch(path, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {} }); const ct = res.headers.get("content-type") ?? ""; if (!res.ok) { if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const p = payload as { error?: { message?: string } }; const msg = p?.error?.message?.trim() || "Export failed."; throw new ApiError(msg, res.status, payload); } const t = await res.text().catch(() => ""); throw new ApiError(t || "Export failed.", res.status, t); } if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const p = payload as { error?: { message?: string } }; const msg = p?.error?.message?.trim() || "Export failed."; throw new ApiError(msg, res.status, payload); } const blob = await res.blob(); const name = parseFileNameFromContentDisposition(res.headers.get("content-disposition")) || "label-report-export.pdf"; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; a.click(); URL.revokeObjectURL(url); } export type ReprintPrintLogInput = { locationId: string; taskId: string; printQuantity?: number; clientRequestId?: string; printerId?: string; printerMac?: string; printerAddress?: string; }; export async function reprintPrintLog(input: ReprintPrintLogInput, signal?: AbortSignal): Promise { return api.requestJson({ path: `${REPORTS_PREFIX}/reprint-print-log`, method: "POST", body: { locationId: input.locationId, taskId: input.taskId, printQuantity: input.printQuantity ?? 1, clientRequestId: input.clientRequestId, printerId: input.printerId, printerMac: input.printerMac, printerAddress: input.printerAddress, }, signal, }); }