feat(api,web): c2 consulta de clientes — list + search + auth flow
prisma: modelo Client + migração 20260527225728_add_client + seed dev (10 clientes)
api: GET /clients (list, busca, filtro atividade/financeiro, paginação) + GET /clients/:id
rep vê carteira própria; supervisor/admin vê tudo; activityStatus calculado de lastOrderAt
@sar/api-interface: ClientSummarySchema, ClientDetailSchema, ClientListResponseSchema
web: ClientsPage (tabela AntD, busca, filtro), DevLogin (token dev), authStore, Bearer no apiFetch
oq-4 resolvida: creditLimit gerenciado no SAR
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
export * from './lib/ping.contract';
|
||||
export * from './lib/auth.contract';
|
||||
export * from './lib/client.contract';
|
||||
|
||||
74
libs/shared/api-interface/src/lib/client.contract.ts
Normal file
74
libs/shared/api-interface/src/lib/client.contract.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Contratos canônicos de C2 — Consulta de Clientes.
|
||||
// Consumidos pela API (output tipado) e pela Web (TanStack Query + parse).
|
||||
|
||||
// ─── Enums ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const FinancialStatusSchema = z.enum(['regular', 'attention', 'blocked']);
|
||||
export type FinancialStatus = z.infer<typeof FinancialStatusSchema>;
|
||||
|
||||
// Calculado em runtime a partir de lastOrderAt (não persiste no banco).
|
||||
export const ActivityStatusSchema = z.enum(['active', 'alert', 'inactive']);
|
||||
export type ActivityStatus = z.infer<typeof ActivityStatusSchema>;
|
||||
|
||||
// ─── Address ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const AddressSchema = z.object({
|
||||
street: z.string().min(1),
|
||||
number: z.string().min(1),
|
||||
complement: z.string().optional(),
|
||||
district: z.string().min(1),
|
||||
city: z.string().min(1),
|
||||
state: z.string().length(2), // UF
|
||||
zip: z.string().regex(/^\d{8}$/), // sem máscara
|
||||
});
|
||||
export type Address = z.infer<typeof AddressSchema>;
|
||||
|
||||
// ─── Client Summary (lista) ───────────────────────────────────────────────────
|
||||
|
||||
export const ClientSummarySchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
tradeName: z.string().nullable(),
|
||||
taxId: z.string(),
|
||||
financialStatus: FinancialStatusSchema,
|
||||
activityStatus: ActivityStatusSchema,
|
||||
lastOrderAt: z.iso.datetime().nullable(),
|
||||
lastOrderValue: z.string().nullable(), // Decimal serializado como string
|
||||
openOrdersCount: z.number().int().nonnegative(),
|
||||
});
|
||||
export type ClientSummary = z.infer<typeof ClientSummarySchema>;
|
||||
|
||||
// ─── Client Detail (ficha) ───────────────────────────────────────────────────
|
||||
|
||||
export const ClientDetailSchema = ClientSummarySchema.extend({
|
||||
email: z.string().email().nullable(),
|
||||
phone: z.string().nullable(),
|
||||
address: AddressSchema.nullable(),
|
||||
creditLimit: z.string().nullable(), // Decimal serializado como string; null = não definido
|
||||
erpCode: z.string().nullable(),
|
||||
syncedAt: z.iso.datetime().nullable(),
|
||||
createdAt: z.iso.datetime(),
|
||||
updatedAt: z.iso.datetime(),
|
||||
});
|
||||
export type ClientDetail = z.infer<typeof ClientDetailSchema>;
|
||||
|
||||
// ─── List query + response ────────────────────────────────────────────────────
|
||||
|
||||
export const ClientListQuerySchema = z.object({
|
||||
q: z.string().optional(), // busca nome/taxId
|
||||
status: ActivityStatusSchema.optional(), // filtro de atividade
|
||||
financialStatus: FinancialStatusSchema.optional(),
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||
});
|
||||
export type ClientListQuery = z.infer<typeof ClientListQuerySchema>;
|
||||
|
||||
export const ClientListResponseSchema = z.object({
|
||||
data: z.array(ClientSummarySchema),
|
||||
total: z.number().int().nonnegative(),
|
||||
page: z.number().int().positive(),
|
||||
limit: z.number().int().positive(),
|
||||
});
|
||||
export type ClientListResponse = z.infer<typeof ClientListResponseSchema>;
|
||||
Reference in New Issue
Block a user