FPSMS-frontend
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

169 行
4.9 KiB

  1. "use client";
  2. import { NEXT_PUBLIC_API_URL } from "@/config/api";
  3. import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
  4. export interface JobOrderListItem {
  5. id: number;
  6. code: string | null;
  7. planStart: string | null;
  8. itemCode: string | null;
  9. itemName: string | null;
  10. reqQty: number | null;
  11. stockInLineId: number | null;
  12. itemId: number | null;
  13. lotNo: string | null;
  14. /** 打袋機 DataFlex cumulative printed qty */
  15. bagPrintedQty?: number;
  16. /** 標簽機 cumulative printed qty */
  17. labelPrintedQty?: number;
  18. /** 激光機 cumulative printed qty */
  19. laserPrintedQty?: number;
  20. }
  21. export interface PrinterStatusRequest {
  22. printerType: "dataflex" | "laser";
  23. printerIp?: string;
  24. printerPort?: number;
  25. }
  26. export interface PrinterStatusResponse {
  27. connected: boolean;
  28. message: string;
  29. }
  30. export interface OnPackQrDownloadRequest {
  31. jobOrders: {
  32. jobOrderId: number;
  33. itemCode: string;
  34. }[];
  35. }
  36. /** Same mapping as Bag Print download buttons: one entry per row with a non-empty item code. */
  37. export function buildOnPackJobOrdersPayload(jobOrders: JobOrderListItem[]): {
  38. jobOrderId: number;
  39. itemCode: string;
  40. }[] {
  41. return jobOrders
  42. .map((jobOrder) => ({
  43. jobOrderId: jobOrder.id,
  44. itemCode: jobOrder.itemCode?.trim() || "",
  45. }))
  46. .filter((jobOrder) => jobOrder.itemCode.length > 0);
  47. }
  48. export interface NgpclPushResponse {
  49. pushed: boolean;
  50. message: string;
  51. }
  52. /**
  53. * POST the same lemon OnPack ZIP bytes as download-onpack-qr-text to the server-configured NGPCL HTTP endpoint (ngpcl.push-url).
  54. * When the URL is not configured, response has pushed=false — use download ZIP instead.
  55. */
  56. export async function pushOnPackTextQrZipToNgpcl(request: OnPackQrDownloadRequest): Promise<NgpclPushResponse> {
  57. const url = `${NEXT_PUBLIC_API_URL}/plastic/ngpcl/push-onpack-qr-text`;
  58. const res = await clientAuthFetch(url, {
  59. method: "POST",
  60. headers: { "Content-Type": "application/json" },
  61. body: JSON.stringify(request),
  62. });
  63. if (res.status === 401 || res.status === 403) {
  64. return { pushed: false, message: "Session expired or unauthorized." };
  65. }
  66. const data = (await res.json()) as NgpclPushResponse;
  67. if (!res.ok) {
  68. throw new Error(data.message || `HTTP ${res.status}`);
  69. }
  70. return data;
  71. }
  72. /** Readable message when ZIP download returns non-OK (plain text, JSON error body, or generic). */
  73. async function zipDownloadError(res: Response): Promise<Error> {
  74. const text = await res.text();
  75. const ct = res.headers.get("content-type") ?? "";
  76. if (ct.includes("application/json")) {
  77. try {
  78. const j = JSON.parse(text) as { message?: string; error?: string };
  79. if (typeof j.message === "string" && j.message.length > 0) {
  80. return new Error(j.message);
  81. }
  82. if (typeof j.error === "string" && j.error.length > 0) {
  83. return new Error(j.error);
  84. }
  85. } catch {
  86. /* ignore parse */
  87. }
  88. }
  89. if (text && text.length > 0 && text.length < 800 && !text.trim().startsWith("{")) {
  90. return new Error(text);
  91. }
  92. return new Error(`下載失敗(HTTP ${res.status})。請查看後端日誌或確認資料庫已執行 Liquibase 更新。`);
  93. }
  94. /**
  95. * Fetch job orders by plan date from GET /py/job-orders.
  96. * Client-side only; uses auth token from localStorage.
  97. */
  98. export async function fetchJobOrders(planStart: string): Promise<JobOrderListItem[]> {
  99. const url = `${NEXT_PUBLIC_API_URL}/py/job-orders?planStart=${encodeURIComponent(planStart)}`;
  100. const res = await clientAuthFetch(url, { method: "GET" });
  101. if (!res.ok) {
  102. throw new Error(`Failed to fetch job orders: ${res.status}`);
  103. }
  104. return res.json();
  105. }
  106. export async function checkPrinterStatus(
  107. request: PrinterStatusRequest,
  108. ): Promise<PrinterStatusResponse> {
  109. const url = `${NEXT_PUBLIC_API_URL}/plastic/check-printer`;
  110. const res = await clientAuthFetch(url, {
  111. method: "POST",
  112. headers: { "Content-Type": "application/json" },
  113. body: JSON.stringify(request),
  114. });
  115. const data = (await res.json()) as PrinterStatusResponse;
  116. if (!res.ok) {
  117. return data;
  118. }
  119. return data;
  120. }
  121. export async function downloadOnPackQrZip(
  122. request: OnPackQrDownloadRequest,
  123. ): Promise<Blob> {
  124. const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr`;
  125. const res = await clientAuthFetch(url, {
  126. method: "POST",
  127. headers: { "Content-Type": "application/json" },
  128. body: JSON.stringify(request),
  129. });
  130. if (!res.ok) {
  131. throw await zipDownloadError(res);
  132. }
  133. return res.blob();
  134. }
  135. /** OnPack2023 檸檬機 — text QR template (`onpack2030_2`), no separate .bmp */
  136. export async function downloadOnPackTextQrZip(
  137. request: OnPackQrDownloadRequest,
  138. ): Promise<Blob> {
  139. const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-text`;
  140. const res = await clientAuthFetch(url, {
  141. method: "POST",
  142. headers: { "Content-Type": "application/json" },
  143. body: JSON.stringify(request),
  144. });
  145. if (!res.ok) {
  146. throw await zipDownloadError(res);
  147. }
  148. return res.blob();
  149. }