Files
siop2/packages/shared/src/schemas/gestion.ts
pr-daaif 460ef4a80e feat(r3.4): corrections de recette (8 écarts) + recherche globale ⌘K
Arbitrage du référent sur la revue pixel : tout corriger, activer la
recherche.

- Stock : filtre fournisseur, sous-seuil en tête, « Entrée de stock »
  depuis la liste ; fiche pièce : fournisseur → lien Tiers.
- Statistiques : période 3/6/12 mois (paramètre months au contrat).
- Tiers : rattachements syndic→site (migration r3_recette_fixes,
  Location.partnerId gardé CLIENT), éditable sur la fiche site, seedé.
- Bibliothèque : filtre « Rattaché à » + glisser-déposer (modale
  préremplie, rattachement toujours requis).
- Recherche globale : GET /search (73 opérations) — familles OT/
  ascenseurs/sites filtrées par la matrice, « voir autre » respecté ;
  topbar ⌘K, debounce, résultats groupés, navigation clavier.

74 tests API (8 nouveaux sur le scoping de la recherche), 14/14
Playwright dont un parcours « recette corrigée », 18/18 contrôles en
navigateur réel, zéro erreur console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:14:43 +01:00

215 lines
6.9 KiB
TypeScript

import { z } from 'zod';
/** Vocabulaires & schémas de la gestion (R3). */
export const PARTNER_KINDS = ['SUPPLIER', 'CLIENT'] as const;
export type PartnerKind = (typeof PARTNER_KINDS)[number];
export const PARTNER_KIND_LABELS: Record<PartnerKind, string> = {
SUPPLIER: 'Fournisseur',
CLIENT: 'Client / syndic',
};
export const PURCHASE_ORDER_STATUSES = ['DRAFT', 'SENT', 'RECEIVED', 'CANCELLED'] as const;
export type PurchaseOrderStatus = (typeof PURCHASE_ORDER_STATUSES)[number];
export const PURCHASE_ORDER_STATUS_LABELS: Record<PurchaseOrderStatus, string> = {
DRAFT: 'Brouillon',
SENT: 'Envoyé',
RECEIVED: 'Reçu',
CANCELLED: 'Annulé',
};
export const STOCK_MOVEMENT_KINDS = ['RECEIPT', 'ENTRY', 'CONSUMPTION', 'ADJUSTMENT'] as const;
export type StockMovementKind = (typeof STOCK_MOVEMENT_KINDS)[number];
export const STOCK_MOVEMENT_KIND_LABELS: Record<StockMovementKind, string> = {
RECEIPT: 'Réception',
ENTRY: 'Entrée',
CONSUMPTION: 'Sortie (OT)',
ADJUSTMENT: 'Ajustement',
};
// ————— Tiers —————
export const PartnerSchema = z.object({
id: z.uuid(),
name: z.string(),
kind: z.enum(PARTNER_KINDS),
contactName: z.string().nullable(),
phone: z.string().nullable(),
email: z.string().nullable(),
city: z.string().nullable(),
isActive: z.boolean(),
openOrders: z.number().int(), // BC non reçus/annulés (fournisseur)
siteNames: z.array(z.string()), // sites gérés (client/syndic) — maquette « Rattachements »
});
export type PartnerDto = z.infer<typeof PartnerSchema>;
export const PartnersResponseSchema = z.object({ partners: z.array(PartnerSchema) });
export type PartnersResponse = z.infer<typeof PartnersResponseSchema>;
export const PartnerCreateSchema = z.object({
name: z.string().min(1).max(120),
kind: z.enum(PARTNER_KINDS),
contactName: z.string().max(120).optional(),
phone: z.string().max(40).optional(),
email: z.email().optional(),
city: z.string().max(80).optional(),
});
export type PartnerCreate = z.infer<typeof PartnerCreateSchema>;
export const PartnerUpdateSchema = PartnerCreateSchema.partial().extend({
isActive: z.boolean().optional(),
});
export type PartnerUpdate = z.infer<typeof PartnerUpdateSchema>;
// ————— Pièces & stock (dérivé des mouvements) —————
export const PartSchema = z.object({
id: z.uuid(),
reference: z.string(),
designation: z.string(),
threshold: z.number().int(),
lastUnitPrice: z.number().nullable(),
compatible: z.string().nullable(),
supplierId: z.uuid().nullable(),
supplierName: z.string().nullable(),
isActive: z.boolean(),
stock: z.number().int(), // Σ mouvements — calculé, jamais stocké
belowThreshold: z.boolean(),
});
export type PartDto = z.infer<typeof PartSchema>;
export const PartsResponseSchema = z.object({ parts: z.array(PartSchema) });
export type PartsResponse = z.infer<typeof PartsResponseSchema>;
export const PartCreateSchema = z.object({
designation: z.string().min(1).max(200),
threshold: z.number().int().min(0).optional(),
supplierId: z.uuid().optional(),
compatible: z.string().max(200).optional(),
initialPrice: z.number().positive().optional(), // premier PU connu
});
export type PartCreate = z.infer<typeof PartCreateSchema>;
export const PartUpdateSchema = z.object({
designation: z.string().min(1).max(200).optional(),
threshold: z.number().int().min(0).optional(),
supplierId: z.uuid().nullable().optional(),
compatible: z.string().max(200).nullable().optional(),
isActive: z.boolean().optional(),
});
export type PartUpdate = z.infer<typeof PartUpdateSchema>;
export const StockMovementSchema = z.object({
id: z.uuid(),
kind: z.enum(STOCK_MOVEMENT_KINDS),
quantity: z.number().int(),
unitPrice: z.number().nullable(),
reason: z.string().nullable(),
workOrderReference: z.string().nullable(),
purchaseOrderReference: z.string().nullable(),
byName: z.string().nullable(),
createdAt: z.iso.datetime(),
});
export type StockMovementDto = z.infer<typeof StockMovementSchema>;
export const PartDetailSchema = PartSchema.extend({
movements: z.array(StockMovementSchema), // récents d'abord — le stock EST cette liste
});
export type PartDetail = z.infer<typeof PartDetailSchema>;
/** Entrée manuelle (+) ou ajustement (±, motif requis). */
export const StockMovementCreateSchema = z.object({
kind: z.enum(['ENTRY', 'ADJUSTMENT']),
quantity: z.number().int().refine((q) => q !== 0, 'Quantité non nulle requise'),
reason: z.string().max(300).optional(),
});
export type StockMovementCreate = z.infer<typeof StockMovementCreateSchema>;
// ————— Bons de commande —————
export const PurchaseOrderLineSchema = z.object({
id: z.uuid(),
partId: z.uuid(),
partReference: z.string(),
designation: z.string(),
quantity: z.number().int(),
unitPrice: z.number(),
});
export const PurchaseOrderSchema = z.object({
id: z.uuid(),
reference: z.string(),
status: z.enum(PURCHASE_ORDER_STATUSES),
supplierId: z.uuid(),
supplierName: z.string(),
lines: z.array(PurchaseOrderLineSchema),
total: z.number(),
sentAt: z.iso.datetime().nullable(),
receivedAt: z.iso.datetime().nullable(),
createdAt: z.iso.datetime(),
});
export type PurchaseOrderDto = z.infer<typeof PurchaseOrderSchema>;
export const PurchaseOrdersResponseSchema = z.object({
purchaseOrders: z.array(PurchaseOrderSchema),
});
export type PurchaseOrdersResponse = z.infer<typeof PurchaseOrdersResponseSchema>;
export const PurchaseOrderCreateSchema = z.object({
supplierId: z.uuid(),
lines: z
.array(
z.object({
partId: z.uuid(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
}),
)
.min(1),
});
export type PurchaseOrderCreate = z.infer<typeof PurchaseOrderCreateSchema>;
export const PurchaseOrderTransitionSchema = z.object({
to: z.enum(['SENT', 'RECEIVED', 'CANCELLED']),
});
export type PurchaseOrderTransition = z.infer<typeof PurchaseOrderTransitionSchema>;
// ————— Coûts sur OT —————
export const ConsumePartSchema = z.object({
partId: z.uuid(),
quantity: z.number().int().positive(),
});
export type ConsumePart = z.infer<typeof ConsumePartSchema>;
export const LaborTimeCreateSchema = z.object({
minutes: z.number().int().positive().max(24 * 60),
userId: z.uuid().optional(), // défaut : la personne connectée
note: z.string().max(200).optional(),
});
export type LaborTimeCreate = z.infer<typeof LaborTimeCreateSchema>;
export const WorkOrderCostsSchema = z.object({
parts: z.array(
z.object({
id: z.uuid(),
designation: z.string(),
quantity: z.number().int(),
unitPrice: z.number(),
total: z.number(),
}),
),
labor: z.array(
z.object({
id: z.uuid(),
displayName: z.string(),
minutes: z.number().int(),
hourlyRate: z.number(),
total: z.number(),
note: z.string().nullable(),
}),
),
total: z.number(),
});
export type WorkOrderCosts = z.infer<typeof WorkOrderCostsSchema>;