init: M1 scaffolding + M2 organization/clients/services CRUD
- monorepo (npm workspaces): apps/api (Fastify+Prisma+TS), apps/web (Vite+React+TS), packages/shared (zod schemas) - SSO via auth.queo.ru: jose+JWKS plugin, requireDocPermission(viewer|user|admin) - DEV_BYPASS_AUTH for local development (hard-checked off in production) - M2: organization upsert, clients CRUD with search, services catalog with soft-delete - BigInt -> Number serializer for Prisma money columns - Embedded Postgres + npm run dev:demo for one-command local boot - Docker compose for queoserver: postgres + api + web (nginx as ingress proxying /api -> api:3030) - First migration 0_init committed (prisma migrate diff) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { redirectToLogin } from './auth.js';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public status: number, public code: string, public details?: unknown) {
|
||||
super(`${status} ${code}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
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: <T>(p: string) => request<T>('GET', p),
|
||||
post: <T>(p: string, body: unknown) => request<T>('POST', p, body),
|
||||
put: <T>(p: string, body: unknown) => request<T>('PUT', p, body),
|
||||
del: <T = void>(p: string) => request<T>('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<string, unknown> | 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: 'none' | 'vat_0' | 'vat_5' | 'vat_7' | 'vat_10' | 'vat_20';
|
||||
notes: string | null;
|
||||
archivedAt: string | null;
|
||||
};
|
||||
Reference in New Issue
Block a user