172 lines
5.0 KiB
TypeScript
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'),
|
|
};
|