import { redirectToLogin } from './auth.js'; export class ApiError extends Error { constructor(public status: number, public code: string, public details?: unknown) { super(`${status} ${code}`); } /** Удобочитаемое сообщение для UI: разворачивает zod issues если есть. */ prettyMessage(): string { if (this.code === 'validation_error' && this.details && typeof this.details === 'object') { const d = this.details as { issues?: { fieldErrors?: Record } }; const fields = d.issues?.fieldErrors; if (fields) { const parts = Object.entries(fields) .filter(([, msgs]) => msgs && msgs.length > 0) .map(([k, msgs]) => `${k}: ${msgs!.join(', ')}`); if (parts.length) return `Ошибка в полях: ${parts.join('; ')}`; } } return `${this.code} (HTTP ${this.status})`; } } async function request(method: string, path: string, body?: unknown): Promise { const init: RequestInit = { method, credentials: 'include' }; if (body !== undefined) { init.headers = { 'Content-Type': 'application/json' }; init.body = JSON.stringify(body); } const res = await fetch(path, init); if (res.status === 401) { redirectToLogin(); } if (res.status === 204) return undefined as T; const text = await res.text(); const data = text ? JSON.parse(text) : undefined; if (!res.ok) { throw new ApiError(res.status, (data as { error?: string })?.error ?? 'http_error', data); } return data as T; } export const api = { get: (p: string) => request('GET', p), post: (p: string, body: unknown) => request('POST', p, body), put: (p: string, body: unknown) => request('PUT', p, body), del: (p: string) => request('DELETE', p), }; export type Organization = { id: string; name: string; inn: string; kpp: string | null; ogrn: string | null; legalAddress: string | null; bankName: string | null; bankBik: string | null; bankAccount: string | null; signatoryName: string | null; signatoryPosition: string | null; }; export type Client = { id: string; organizationId: string; kind: 'ul' | 'ip' | 'fl'; name: string; inn: string | null; kpp: string | null; address: string | null; email: string | null; phone: string | null; contactPerson: string | null; requisitesJson: Record | null; createdAt: string; updatedAt: string; }; export type Service = { id: string; organizationId: string; name: string; unit: string; defaultPriceCents: number; // BigInt сериализуется в number (см. apps/api/src/lib/bigint.ts) defaultVat: VatRate; notes: string | null; archivedAt: string | null; }; export type VatRate = 'none' | 'vat_0' | 'vat_5' | 'vat_7' | 'vat_10' | 'vat_20'; export type DocType = 'contract' | 'invoice' | 'act' | 'upd'; export type DocStatus = 'draft' | 'issued' | 'sent' | 'partially_paid' | 'paid' | 'cancelled' | 'signed'; export type DocumentLine = { id: string; documentId: string; position: number; serviceId: string | null; name: string; qtyMilli: number; unit: string; priceCents: number; vat: VatRate; sumCents: number; }; export type DocumentSummary = { id: string; organizationId: string; docType: DocType; number: string; issuedAt: string | null; status: DocStatus; clientId: string | null; client: { id: string; name: string; kind: Client['kind'] } | null; parentDocumentId: string | null; totalCents: number; vatCents: number; currency: string; tochkaDocumentId: string | null; pdfPath: string | null; createdAt: string; updatedAt: string; }; export type Document = DocumentSummary & { body: DocBody; client: Client | null; lines: DocumentLine[]; }; export type DocumentTemplate = { id: string; organizationId: string; docType: DocType; name: string; body: DocBody; createdAt: string; updatedAt: string; }; // === Block schema (mirrors packages/shared) === export type RichText = { type: string; content?: RichText[]; text?: string; marks?: { type: string }[]; }; export type Block = | { id: string; type: 'heading'; level: 1 | 2 | 3; text: RichText } | { id: string; type: 'paragraph'; text: RichText } | { id: string; type: 'party'; role: 'executor' | 'customer'; bind: { kind: 'self' } | { kind: 'client'; clientId?: string }; } | { id: string; type: 'services_table'; columns: Array<'name' | 'qty' | 'unit' | 'price' | 'vat' | 'sum'>; lines: { lineId: string }[]; } | { id: string; type: 'totals'; showVat: boolean; showInWords: boolean } | { id: string; type: 'terms'; text: RichText } | { id: string; type: 'signatures'; sides: ('executor' | 'customer')[] } | { id: string; type: 'custom_text'; text: RichText } | { id: string; type: 'page_break' }; export type DocBody = { version: 1; blocks: Block[]; vars: Record; }; export type LineHistoryItem = { serviceId: string | null; name: string; unit: string; priceCents: number; vat: VatRate; lastUsed: string; useCount: number; };