export interface SubmitNewProductsArgs { products: Array>; environment: "dev" | "prod"; useTestDataSource: boolean; employeeId?: number; } export interface SubmitNewProductsResponse { success: boolean; message?: string; data?: unknown; error?: unknown; } export interface PoLineItemSubmit { pid: number; qty: number; } export interface SubmitNewPurchaseOrderArgs { supplierId: number | string; items: PoLineItemSubmit[]; } export interface SubmitNewPurchaseOrderResponse { success: boolean; poId?: number; message?: string; error?: unknown; raw?: unknown; } export interface CreateProductCategoryArgs { masterCatId: string | number; name: string; environment?: "dev" | "prod"; image?: string; nameForCustoms?: string; taxCodeId?: string | number; } export interface CreateProductCategoryResponse { success: boolean; message?: string; data?: unknown; error?: unknown; category?: unknown; } // Always use relative URLs - proxied by Vite in dev and Caddy in production // Frontend calls /apiv2/* -> Caddy transforms to /api/* -> proxies to www.acherryontop.com const DEV_ENDPOINT = "/apiv2-test/product/setup_new"; const DEV_CREATE_CATEGORY_ENDPOINT = "/apiv2-test/prod_cat/new"; const PROD_ENDPOINT = "/apiv2/product/setup_new"; const PROD_CREATE_CATEGORY_ENDPOINT = "/apiv2/prod_cat/new"; const isHtmlResponse = (payload: string) => { const trimmed = payload.trim(); return trimmed.startsWith(" { const baseUrl = environment === "dev" ? DEV_ENDPOINT : PROD_ENDPOINT; const targetUrl = useTestDataSource ? `${baseUrl}?use_test_data_source=1` : baseUrl; const payload = new URLSearchParams(); const serializedProducts = JSON.stringify(products); payload.append("products", serializedProducts); let response: Response; const fetchOptions: RequestInit = { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", }, body: payload, }; // Authentication strategy depends on endpoint if (environment === "dev") { // Test endpoint: Use auth token in request body const authToken = import.meta.env.VITE_APIV2_AUTH_TOKEN; if (authToken) { payload.append("auth", authToken); fetchOptions.body = payload; } } else { // Prod endpoint: Use cookies (proxied in both dev and production) fetchOptions.credentials = "include"; } try { response = await fetch(targetUrl, fetchOptions); } catch (networkError) { throw new Error(networkError instanceof Error ? networkError.message : "Network request failed"); } const rawBody = await response.text(); if (isHtmlResponse(rawBody)) { throw new Error("Backend authentication required. Please ensure you are logged into the backend system."); } let parsed: unknown; try { parsed = JSON.parse(rawBody); } catch { const message = `Unexpected response from backend (${response.status}).`; throw new Error(message); } if (!parsed || typeof parsed !== "object") { throw new Error("Empty response from backend"); } const parsedResponse = parsed as SubmitNewProductsResponse & Record; const extraFields = parsedResponse as Record; const normalizedResponse: SubmitNewProductsResponse = { success: Boolean(parsedResponse.success), message: typeof parsedResponse.message === "string" ? parsedResponse.message : undefined, data: parsedResponse.data, error: parsedResponse.error ?? extraFields.errors ?? extraFields.error_msg, }; if (!response.ok || !normalizedResponse.success) { return normalizedResponse; } return normalizedResponse; } export async function createProductCategory({ masterCatId, name, environment = "prod", image, nameForCustoms, taxCodeId, }: CreateProductCategoryArgs): Promise { const targetUrl = environment === "dev" ? DEV_CREATE_CATEGORY_ENDPOINT : PROD_CREATE_CATEGORY_ENDPOINT; const payload = new URLSearchParams(); payload.append("master_cat_id", masterCatId.toString()); payload.append("name", name); if (nameForCustoms) { payload.append("name_for_customs", nameForCustoms); } if (image) { payload.append("image", image); } if (typeof taxCodeId !== "undefined" && taxCodeId !== null) { payload.append("tax_code_id", taxCodeId.toString()); } let response: Response; const fetchOptions: RequestInit = { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", }, body: payload, }; // Authentication strategy depends on endpoint if (environment === "dev") { // Test endpoint: Use auth token in request body const authToken = import.meta.env.VITE_APIV2_AUTH_TOKEN; if (authToken) { payload.append("auth", authToken); fetchOptions.body = payload; } } else { // Prod endpoint: Use cookies (proxied in both dev and production) fetchOptions.credentials = "include"; } try { response = await fetch(targetUrl, fetchOptions); } catch (networkError) { throw new Error(networkError instanceof Error ? networkError.message : "Network request failed"); } const rawBody = await response.text(); if (isHtmlResponse(rawBody)) { throw new Error("Backend authentication required. Please ensure you are logged into the backend system."); } let parsed: unknown; try { parsed = JSON.parse(rawBody); } catch { const message = `Unexpected response from backend (${response.status}).`; throw new Error(message); } if (!parsed || typeof parsed !== "object") { throw new Error("Empty response from backend"); } const parsedRecord = parsed as Record; const normalizedResponse: CreateProductCategoryResponse = { success: Boolean(parsedRecord.success ?? parsedRecord.status ?? parsedRecord.result), message: typeof parsedRecord.message === "string" ? parsedRecord.message : undefined, data: parsedRecord.data ?? parsedRecord.category ?? parsedRecord.result, error: parsedRecord.error ?? parsedRecord.errors ?? parsedRecord.error_msg, category: parsedRecord.category, }; if (!response.ok || !normalizedResponse.success) { return normalizedResponse; } return normalizedResponse; } /** * Creates a new purchase order on the legacy PHP backend. * * Mirrors the auth/serialization pattern of `submitNewProducts`: * - URL-encoded body * - cookie-based auth (`credentials: 'include'`) * - HTML response → "Backend authentication required" guard * * The endpoint accepts a JSON array of `{pid, qty}` items in the `items` body * field, with the supplier ID as a path parameter. Returns `{po_id: number}` * on success. Designed to be called from anywhere in the app — not just the * Create PO page. */ export async function submitNewPurchaseOrder({ supplierId, items, }: SubmitNewPurchaseOrderArgs): Promise { if (!supplierId) { throw new Error("supplierId is required"); } if (!Array.isArray(items) || items.length === 0) { throw new Error("At least one item is required"); } const cleanItems = items .map((i) => ({ pid: Number(i.pid), qty: Number(i.qty) })) .filter((i) => Number.isInteger(i.pid) && i.pid > 0 && Number.isFinite(i.qty) && i.qty > 0); if (cleanItems.length === 0) { throw new Error("No valid items to submit"); } const targetUrl = `/apiv2/po/new/${encodeURIComponent(String(supplierId))}`; const payload = new URLSearchParams(); payload.append("items", JSON.stringify(cleanItems)); let response: Response; try { response = await fetch(targetUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", }, body: payload, credentials: "include", }); } catch (networkError) { throw new Error(networkError instanceof Error ? networkError.message : "Network request failed"); } const rawBody = await response.text(); if (isHtmlResponse(rawBody)) { throw new Error("Backend authentication required. Please ensure you are logged into the backend system."); } let parsed: unknown; try { parsed = JSON.parse(rawBody); } catch { throw new Error(`Unexpected response from backend (${response.status}).`); } if (!parsed || typeof parsed !== "object") { throw new Error("Empty response from backend"); } const record = parsed as Record; // The PHP /apiv2 backend wraps successful responses in an envelope: // {"success": true, "data": { "po_id": "32705" }} // po_id can appear either at the top level (legacy shape) OR nested // inside `data` (current shape), and the value is sometimes a string // (e.g. "32705") rather than a number. We look in both locations and // coerce to Number to handle both cases. const data = record.data && typeof record.data === "object" ? (record.data as Record) : {}; const rawPoId = record.po_id ?? record.poId ?? data.po_id ?? data.poId; const poIdNum = typeof rawPoId === "number" ? rawPoId : Number(rawPoId); const hasValidPoId = Number.isFinite(poIdNum) && poIdNum > 0; // Trust the backend's own `success` flag when it's present — it's the // authoritative signal. Fall back to "HTTP OK + valid po_id" for older // response shapes that don't include the flag. const backendSuccess = record.success === true || record.success === "true" || record.success === 1; const success = response.ok && hasValidPoId && (backendSuccess || record.success === undefined); return { success, poId: success ? poIdNum : undefined, message: typeof record.message === "string" ? record.message : undefined, error: record.error ?? record.errors ?? record.error_msg, raw: parsed, }; }