Files
tehran_tala/apps/user-app/src/api/client.ts
T
2026-09-21 17:37:26 +03:30

172 lines
5.0 KiB
TypeScript

import type { CreateOrderDto, CustomerBalanceDto } from '@gold-bank/shared-types';
const BASE = '/api';
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) {
const body = await res.text();
throw new Error(`API ${path} failed (${res.status}): ${body}`);
}
return res.json();
}
export interface ProductImageSummary {
id: string;
url: string;
sortOrder: number;
role: 'FEATURED' | 'FEATURED_MOBILE' | 'GALLERY' | 'GALLERY_MOBILE';
}
export interface ProductVariantSummary {
id: string;
title: string;
rashWeightGrams: string;
defaultWagePercent: string;
/// On-hand stock and the "orderable at zero stock" flag. Both exist on
/// ProductVariant and are already returned by GET /catalog/products (the
/// catalog service includes every variant field) — declared here so the UI
/// can read them.
qty?: number;
isPreOrder?: boolean;
/// The gallery image chosen for this variant in the admin panel. It is one of
/// the product's OWN images — the same ProductImage row, not a copy — so
/// several variants (e.g. different weights of one design) may share it.
/// Null when no picture was picked; callers fall back to the product image.
imageId?: string | null;
image?: ProductImageSummary | null;
}
export interface CategorySummary {
id: string;
title: string;
parentId: string | null;
}
export interface BrandSummary {
id: string;
title: string;
}
export interface CollectionSummary {
id: string;
title: string;
description: string | null;
isActive: boolean;
}
export interface ProductSummary {
id: string;
title: string;
description: string | null;
/// Legacy Product.product_code — the human-facing code. Already returned by
/// GET /catalog/products.
productCode?: string | null;
variants: ProductVariantSummary[];
/// Returned sorted by sortOrder, so images[0] really is the product's main
/// photo rather than whichever row the database happened to hand back.
images: ProductImageSummary[];
category: CategorySummary | null;
brand: BrandSummary | null;
collections: Array<{ collectionId: string; collection: CollectionSummary }>;
}
export interface LedgerEntrySummary {
id: string;
currency: string;
direction: string;
sourceType: string;
amount: string;
confirmed: boolean;
createdAt: string;
note: string | null;
}
export interface CustomerSummary {
id: string;
fullName: string;
mobile: string;
goldDebtGrams: string;
fiatDebtRial: string;
goldCreditLimitGrams: string;
isActive: boolean;
isArchived: boolean;
}
export interface OrderSummary {
id: string;
orderNumber: string;
status: string;
totalGoldWeightGrams: string;
isSuspendedOverCreditLimit: boolean;
}
export interface OrderItemDetail {
id: string;
productVariantId: string;
quantity: number;
rashWeightGramsSnapshot: string;
wagePercentSnapshot: string;
discountPercentSnapshot: string;
computedWeightGrams: string;
flatRialCharge: string;
}
export interface OrderDetail {
id: string;
orderNumber: string;
status: string;
totalGoldWeightGrams: string;
wageWeightGrams: string;
flatRialCharge: string;
isSuspendedOverCreditLimit: boolean;
deliveredAt: string | null;
createdAt: string;
items: OrderItemDetail[];
}
export interface CustomerDetail {
id: string;
fullName: string;
mobile: string;
nationalCode: string;
cityId: string | null;
goldCreditLimitGrams: string;
goldDebtGrams: string;
fiatDebtRial: string;
verificationStep: string;
createdAt: string;
}
export interface GoldPriceQuote {
id: string;
karatPerMille: number;
pricePerGramRial: string;
source: string;
quotedAt: string;
}
/** One admin-uploaded home banner slide — raw shape as saved by the admin
* panel's manage/Const.vue "اسلایدر موبایل" tab (image + optional link). */
export interface AdminSlide {
image: string;
link: string;
}
export const api = {
listProducts: () => request<ProductSummary[]>('/catalog/products'),
listCustomers: () => request<CustomerSummary[]>('/customers'),
getCustomer: (id: string) => request<CustomerDetail>(`/customers/${id}`),
getBalance: (customerId: string) => request<CustomerBalanceDto>(`/accounting/customers/${customerId}/balance`),
getLedgerEntries: (customerId: string) => request<LedgerEntrySummary[]>(`/customers/${customerId}/ledger-entries`),
createOrder: (dto: CreateOrderDto) => request<OrderSummary>('/orders', { method: 'POST', body: JSON.stringify(dto) }),
getCustomerOrders: (customerId: string) => request<OrderDetail[]>(`/customers/${customerId}/orders`),
getLatestGoldPrice: (karatPerMille = 750) => request<GoldPriceQuote>(`/pricing/latest?karatPerMille=${karatPerMille}`),
/** GET /home/slider — public, no auth. Empty array when the admin hasn't
* configured any slides (the caller falls back to a derived hero). */
getHomeSlider: () => request<AdminSlide[]>('/home/slider'),
};