325 lines
9.9 KiB
TypeScript
325 lines
9.9 KiB
TypeScript
export interface SubmitNewProductsArgs {
|
|
products: Array<Record<string, unknown>>;
|
|
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("<!DOCTYPE html") || trimmed.startsWith("<html");
|
|
};
|
|
|
|
export async function submitNewProducts({
|
|
products,
|
|
environment,
|
|
useTestDataSource,
|
|
}: SubmitNewProductsArgs): Promise<SubmitNewProductsResponse> {
|
|
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<string, unknown>;
|
|
const extraFields = parsedResponse as Record<string, unknown>;
|
|
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<CreateProductCategoryResponse> {
|
|
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<string, unknown>;
|
|
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<SubmitNewPurchaseOrderResponse> {
|
|
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<string, unknown>;
|
|
// 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<string, unknown>)
|
|
: {};
|
|
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,
|
|
};
|
|
}
|