|
- "use client";
-
- import { NEXT_PUBLIC_API_URL } from "@/config/api";
- import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
-
- export interface JobOrderListItem {
- id: number;
- code: string | null;
- planStart: string | null;
- itemCode: string | null;
- itemName: string | null;
- reqQty: number | null;
- stockInLineId: number | null;
- itemId: number | null;
- lotNo: string | null;
- /** Effective shelf life days used for print (chilled or -18, according to useMinus18). */
- defaultShelfLifeDays?: number | null;
- /** True when expiry is computed from -18 warehouse days. */
- useMinus18?: boolean | null;
- /** Print date + effective shelf life days (yyyy-MM-dd). */
- expiryDate?: string | null;
- /** 打袋機 DataFlex cumulative printed qty */
- bagPrintedQty?: number;
- /** 標簽機 cumulative printed qty */
- labelPrintedQty?: number;
- /** 激光機 cumulative printed qty */
- laserPrintedQty?: number;
- }
-
- export interface PrinterStatusRequest {
- printerType: "dataflex" | "laser";
- printerIp?: string;
- printerPort?: number;
- }
-
- export interface PrinterStatusResponse {
- connected: boolean;
- message: string;
- }
-
- export interface OnPackQrDownloadRequest {
- jobOrders: {
- jobOrderId: number;
- itemCode: string;
- }[];
- /** /bagPrint filter date (YYYY-MM-DD). Used by expiry ZIP for production date. */
- planDate?: string;
- }
-
- /** Same mapping as Bag Print download buttons: one entry per row with a non-empty item code. */
- export function buildOnPackJobOrdersPayload(jobOrders: JobOrderListItem[]): {
- jobOrderId: number;
- itemCode: string;
- }[] {
- return jobOrders
- .map((jobOrder) => ({
- jobOrderId: jobOrder.id,
- itemCode: jobOrder.itemCode?.trim() || "",
- }))
- .filter((jobOrder) => jobOrder.itemCode.length > 0);
- }
-
- export interface NgpclPushResponse {
- pushed: boolean;
- message: string;
- }
-
- /**
- * POST the same lemon OnPack ZIP bytes as download-onpack-qr-text to the server-configured NGPCL HTTP endpoint (ngpcl.push-url).
- * When the URL is not configured, response has pushed=false — use download ZIP instead.
- */
- export async function pushOnPackTextQrZipToNgpcl(request: OnPackQrDownloadRequest): Promise<NgpclPushResponse> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/ngpcl/push-onpack-qr-text`;
- const res = await clientAuthFetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(request),
- });
- if (res.status === 401 || res.status === 403) {
- return { pushed: false, message: "Session expired or unauthorized." };
- }
- const data = (await res.json()) as NgpclPushResponse;
- if (!res.ok) {
- throw new Error(data.message || `HTTP ${res.status}`);
- }
- return data;
- }
-
- /** Readable message when ZIP download returns non-OK (plain text, JSON error body, or generic). */
- async function zipDownloadError(res: Response): Promise<Error> {
- return parseBagPrintApiError(res, "下載");
- }
-
- /** Backend ErrorRes is `{ timestamp, traceId }` with no message; avoid calling that a ZIP download failure. */
- async function parseBagPrintApiError(res: Response, action: string): Promise<Error> {
- const text = await res.text();
- const ct = res.headers.get("content-type") ?? "";
- if (ct.includes("application/json")) {
- try {
- const j = JSON.parse(text) as { message?: string; error?: string; traceId?: string };
- if (typeof j.message === "string" && j.message.length > 0) {
- return new Error(j.message);
- }
- if (typeof j.error === "string" && j.error.length > 0) {
- return new Error(j.error);
- }
- if (typeof j.traceId === "string" && j.traceId.length > 0) {
- return new Error(
- `${action}失敗(HTTP ${res.status})。請重啟後端以執行 Liquibase,或查看日誌 traceId ${j.traceId}。`,
- );
- }
- } catch {
- /* ignore parse */
- }
- }
- if (text && text.length > 0 && text.length < 800 && !text.trim().startsWith("{")) {
- return new Error(text);
- }
- return new Error(`${action}失敗(HTTP ${res.status})。請查看後端日誌或確認資料庫已執行 Liquibase 更新。`);
- }
-
- /**
- * Fetch job orders by plan date from GET /py/job-orders.
- * Client-side only; uses auth token from localStorage.
- */
- export async function fetchJobOrders(planStart: string): Promise<JobOrderListItem[]> {
- const url = `${NEXT_PUBLIC_API_URL}/py/job-orders?planStart=${encodeURIComponent(planStart)}`;
- const res = await clientAuthFetch(url, { method: "GET" });
- if (!res.ok) {
- throw new Error(`Failed to fetch job orders: ${res.status}`);
- }
- return res.json();
- }
-
- export async function checkPrinterStatus(
- request: PrinterStatusRequest,
- ): Promise<PrinterStatusResponse> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/check-printer`;
- const res = await clientAuthFetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(request),
- });
-
- const data = (await res.json()) as PrinterStatusResponse;
- if (!res.ok) {
- return data;
- }
-
- return data;
- }
-
- export async function downloadOnPackQrZip(
- request: OnPackQrDownloadRequest,
- ): Promise<Blob> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr`;
- const res = await clientAuthFetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(request),
- });
-
- if (!res.ok) {
- throw await zipDownloadError(res);
- }
-
- return res.blob();
- }
-
- /** 汁水機 OnPack — same as QR ZIP, plus LOGO_EXP BMP from item_default_shelf_life. */
- export async function downloadOnPackQrZipWithExpiry(
- request: OnPackQrDownloadRequest,
- ): Promise<Blob> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-with-expiry`;
- const res = await clientAuthFetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(request),
- });
-
- if (!res.ok) {
- throw await zipDownloadError(res);
- }
-
- return res.blob();
- }
-
- /** OnPack2023 檸檬機 — text QR template (`onpack2030_2`), no separate .bmp */
- export async function downloadOnPackTextQrZip(
- request: OnPackQrDownloadRequest,
- ): Promise<Blob> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-text`;
- const res = await clientAuthFetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(request),
- });
-
- if (!res.ok) {
- throw await zipDownloadError(res);
- }
-
- return res.blob();
- }
-
- /** OnPack2023 檸檬機 — same as text ZIP, plus TEXT_EXP from item_default_shelf_life. */
- export async function downloadOnPackTextQrZipWithExpiry(
- request: OnPackQrDownloadRequest,
- ): Promise<Blob> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-text-with-expiry`;
- const res = await clientAuthFetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(request),
- });
-
- if (!res.ok) {
- throw await zipDownloadError(res);
- }
-
- return res.blob();
- }
-
- export type OnPackMachine = "juice" | "lemon";
-
- export interface OnPackTemplateFileDto {
- id: number;
- machine: OnPackMachine | string;
- itemCode: string;
- fileName: string;
- byteSize: number;
- modified?: string | null;
- }
-
- export interface OnPackTemplateUploadResponse {
- machine: string;
- itemCode: string;
- saved: string[];
- }
-
- export interface OnPackSupportedItemDto {
- itemCode: string;
- printable: boolean;
- inDatabase: boolean;
- builtin: boolean;
- registered: boolean;
- }
-
- export interface OnPackSupportedCatalogDto {
- juice: OnPackSupportedItemDto[];
- lemon: OnPackSupportedItemDto[];
- }
-
- export async function fetchOnPackSupportedCatalog(): Promise<OnPackSupportedCatalogDto> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/supported`;
- const res = await clientAuthFetch(url, { method: "GET" });
- if (!res.ok) {
- throw await parseBagPrintApiError(res, "讀取 OnPack 支援清單");
- }
- return (await res.json()) as OnPackSupportedCatalogDto;
- }
-
- export async function listOnPackTemplates(machine?: OnPackMachine): Promise<OnPackTemplateFileDto[]> {
- const q = machine ? `?machine=${encodeURIComponent(machine)}` : "";
- const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates${q}`;
- const res = await clientAuthFetch(url, { method: "GET" });
- if (!res.ok) {
- throw await parseBagPrintApiError(res, "讀取 OnPack 模板");
- }
- return (await res.json()) as OnPackTemplateFileDto[];
- }
-
- export async function uploadOnPackTemplates(
- machine: OnPackMachine,
- itemCode: string,
- files: File[],
- ): Promise<OnPackTemplateUploadResponse> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates`;
- const body = new FormData();
- body.append("machine", machine);
- body.append("itemCode", itemCode);
- files.forEach((f) => body.append("files", f));
- const res = await clientAuthFetch(url, { method: "POST", body });
- if (!res.ok) {
- throw await parseBagPrintApiError(res, "上傳 OnPack 模板");
- }
- return (await res.json()) as OnPackTemplateUploadResponse;
- }
-
- export async function deleteOnPackTemplate(id: number): Promise<void> {
- const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/${id}`;
- const res = await clientAuthFetch(url, { method: "DELETE" });
- if (!res.ok && res.status !== 204) {
- throw await parseBagPrintApiError(res, "刪除 OnPack 模板");
- }
- }
|