rubis/apps/web/src/mocks/seed.ts
ordinarthur 16120ed3e0 feat(web): création client (modale) + email required + SIRET optionnel
Réflexion produit : email required vs optionnel.

Le coeur de Rubis = relances email automatiques. Sans email client →
aucune relance ne peut partir → la fiche client est inutilisable pour
le coeur du produit. Décision : email REQUIRED partout, plutôt que
laisser créer des fiches mortes.

Type Client (packages/shared) :
- email: string (était string | null)
- siret: string | null ajouté (optionnel mais recommandé pour mises
  en demeure formelles + intégrations comptables V2 type Pennylane)

ClientCreateDialog (modale "+ Nouveau client" sur /clients) :
- Email required avec validator Zod min(1).email()
- SIRET ajouté côte-à-côte avec Téléphone (validator 14 chiffres
  ou vide, inputMode='numeric', espaces tolérés à la frappe)
- Adresse postale déplacée full-width (lisibilité)
- Hints éducatifs : 'Préférez compta@/facturation@ à une nominative',
  'Recommandé pour les mises en demeure', 'Requise pour les mises en
  demeure formelles'

Field component aligned :
- Label/hint en haut, input en bas (mt-auto sur le wrapper input)
- Quand 2 Fields sont côte-à-côte avec hints de longueur différente,
  les inputs restent alignés au bas — le hint plus long étire le haut
- Erreur reste collée sous l'input (pas en bas de la cellule)

MSW :
- POST /clients schema strict : email required, siret 14 chiffres si fourni
- Détection doublon par nom (409) conservée
- Handlers création de client implicites (saisie facture, OCR review)
  refusent maintenant la création quand email manquant : 422 ciblé
  'Email du client requis — Rubis en a besoin pour envoyer les relances.'
  Si l'user pick un client existant via le combobox → email déjà en
  DB, pas demandé.

Migration mockDb :
- Anciens clients sans siret → null
- Anciens clients avec email null (cas test) → placeholder dérivé du
  slug du nom (contact@boulangerie-martin.fr) — éditable, juste évite
  un crash au load. slugifyClientName() supprime SARL/SAS/EURL et accents.

Détail /clients/$id :
- SIRET ajouté dans la barre meta du header (Hash icon Lucide +
  tabular-nums) — affiché seulement si rempli
- Email plus conditionnel (toujours présent maintenant)

Seeds :
- Boulangerie Martin SARL : SIRET 82345678900012
- Cabinet Rousseau : SIRET 53412987600028
- Atelier Durand, Garage Lemoine, Studio Lefèvre : siret null
  (pour tester les deux cas dans la liste)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 12:25:37 +02:00

427 lines
12 KiB
TypeScript

/**
* Seed des données pour l'utilisateur démo.
*
* On factorise le seed dans son propre fichier pour ne pas alourdir db.ts
* avec 200 lignes de données. Calibré sur les wireframes pour donner un
* dashboard / liste réalistes au premier login.
*/
import type { Client, Invoice, Plan } from "@rubis/shared";
const NOW = new Date("2026-05-06T08:30:00.000Z");
function isoFromOffset(daysOffset: number, hour = 9): string {
const d = new Date(NOW);
d.setDate(d.getDate() + daysOffset);
d.setHours(hour, 0, 0, 0);
return d.toISOString();
}
const ORG = "org_demo";
export const SEED_CLIENTS: Client[] = [
{
id: "cli_martin",
organizationId: ORG,
name: "Boulangerie Martin SARL",
email: "compta@boulangerie-martin.fr",
phone: "+33 1 23 45 67 89",
address: "12 rue du Pain, 75011 Paris",
siret: "82345678900012",
notes: null,
createdAt: isoFromOffset(-90),
updatedAt: isoFromOffset(-2),
},
{
id: "cli_durand",
organizationId: ORG,
name: "Atelier Durand",
email: "contact@atelier-durand.fr",
phone: null,
address: null,
siret: null,
notes: "Le client a confirmé la réception le 14/04 par téléphone — relance ferme inutile.",
createdAt: isoFromOffset(-120),
updatedAt: isoFromOffset(-3),
},
{
id: "cli_rousseau",
organizationId: ORG,
name: "Cabinet Rousseau",
email: "facturation@cabinet-rousseau.fr",
phone: "+33 4 56 78 90 12",
address: "8 place de la République, 69002 Lyon",
siret: "53412987600028",
notes: null,
createdAt: isoFromOffset(-200),
updatedAt: isoFromOffset(-1),
},
{
id: "cli_lemoine",
organizationId: ORG,
name: "Garage Lemoine",
email: "admin@garage-lemoine.fr",
phone: null,
address: null,
siret: null,
notes: null,
createdAt: isoFromOffset(-60),
updatedAt: isoFromOffset(-5),
},
{
id: "cli_lefevre",
organizationId: ORG,
name: "Studio Lefèvre",
email: "hello@studio-lefevre.com",
phone: null,
address: null,
siret: null,
notes: null,
createdAt: isoFromOffset(-30),
updatedAt: isoFromOffset(-1),
},
];
export const SEED_PLANS: Plan[] = [
{
id: "plan_standard",
organizationId: ORG,
slug: "standard-30j",
name: "Standard B2B",
description: "Cadence sobre, ton qui monte progressivement. Pour la majorité des clients sérieux.",
isDefault: true,
steps: [
{
id: "step_std_1",
order: 0,
offsetDays: 3,
tone: "amical",
subject: "Petit rappel — facture {{numero}}",
body: "Bonjour {{client.name}},\n\nNous espérons que tout va bien. Un petit rappel concernant la facture {{numero}} d'un montant de {{amount}}, échue le {{dueDate}}.\n\nMerci d'avance,\n{{signature}}",
requiresManualValidation: false,
},
{
id: "step_std_2",
order: 1,
offsetDays: 10,
tone: "courtois",
subject: "Relance — facture {{numero}} en retard",
body: "Bonjour {{client.name}},\n\nSauf erreur de notre part, la facture {{numero}} d'un montant de {{amount}} reste impayée.\n\nMerci de procéder au règlement dans les meilleurs délais.\n\n{{signature}}",
requiresManualValidation: false,
},
{
id: "step_std_3",
order: 2,
offsetDays: 25,
tone: "ferme",
subject: "Mise en demeure — facture {{numero}}",
body: "Bonjour {{client.name}},\n\nMalgré nos relances, la facture {{numero}} d'un montant de {{amount}} reste impayée. Nous vous mettons en demeure de régler sous 8 jours.\n\n{{signature}}",
requiresManualValidation: true,
},
],
createdAt: isoFromOffset(-365),
updatedAt: isoFromOffset(-30),
},
{
id: "plan_rapide",
organizationId: ORG,
slug: "rapide-15j",
name: "Rapide",
description: "Cadence resserrée pour les factures récurrentes ou les délais courts.",
isDefault: true,
steps: [
{
id: "step_rap_1",
order: 0,
offsetDays: 1,
tone: "amical",
subject: "Facture {{numero}} échue",
body: "Bonjour, petit rappel pour la facture {{numero}}.\n\n{{signature}}",
requiresManualValidation: false,
},
{
id: "step_rap_2",
order: 1,
offsetDays: 7,
tone: "courtois",
subject: "Relance facture {{numero}}",
body: "La facture {{numero}} reste impayée à ce jour. Merci de régulariser.\n\n{{signature}}",
requiresManualValidation: false,
},
{
id: "step_rap_3",
order: 2,
offsetDays: 15,
tone: "ferme",
subject: "Mise en demeure {{numero}}",
body: "Mise en demeure formelle de payer sous 8 jours.\n\n{{signature}}",
requiresManualValidation: true,
},
],
createdAt: isoFromOffset(-365),
updatedAt: isoFromOffset(-365),
},
{
id: "plan_patient",
organizationId: ORG,
slug: "patient-60j",
name: "Patient",
description: "Pour les clients de longue date. On laisse respirer avant de relancer.",
isDefault: true,
steps: [
{
id: "step_pat_1",
order: 0,
offsetDays: 15,
tone: "amical",
subject: "Facture {{numero}}",
body: "Bonjour, simple rappel.\n\n{{signature}}",
requiresManualValidation: false,
},
{
id: "step_pat_2",
order: 1,
offsetDays: 30,
tone: "courtois",
subject: "Relance facture {{numero}}",
body: "Merci de régulariser dans les meilleurs délais.\n\n{{signature}}",
requiresManualValidation: false,
},
],
createdAt: isoFromOffset(-365),
updatedAt: isoFromOffset(-365),
},
{
id: "plan_ferme",
organizationId: ORG,
slug: "ferme-7j",
name: "Ferme",
description: "Cadence stricte pour les clients à risque ou les retards récurrents.",
isDefault: true,
steps: [
{
id: "step_fer_1",
order: 0,
offsetDays: 1,
tone: "courtois",
subject: "Facture {{numero}}",
body: "Premier rappel.\n\n{{signature}}",
requiresManualValidation: false,
},
{
id: "step_fer_2",
order: 1,
offsetDays: 5,
tone: "ferme",
subject: "Relance ferme {{numero}}",
body: "Le règlement est attendu sous 48h.\n\n{{signature}}",
requiresManualValidation: false,
},
{
id: "step_fer_3",
order: 2,
offsetDays: 10,
tone: "mise_en_demeure",
subject: "Mise en demeure {{numero}}",
body: "Mise en demeure formelle.\n\n{{signature}}",
requiresManualValidation: true,
},
],
createdAt: isoFromOffset(-365),
updatedAt: isoFromOffset(-365),
},
];
type SeedInvoice = Invoice & { clientName: string; planName: string | null; statusLabel?: string };
export const SEED_INVOICES: SeedInvoice[] = [
// À relancer (échéance future)
{
id: "inv_001",
organizationId: ORG,
clientId: "cli_martin",
clientName: "Boulangerie Martin SARL",
numero: "F-2026-0042",
amountTtcCents: 124_000,
issueDate: isoFromOffset(-15),
dueDate: isoFromOffset(10),
status: "pending",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: null,
rubisEarned: 0,
createdAt: isoFromOffset(-15),
updatedAt: isoFromOffset(-15),
},
{
id: "inv_002",
organizationId: ORG,
clientId: "cli_lefevre",
clientName: "Studio Lefèvre",
numero: "F-2026-0044",
amountTtcCents: 245_000,
issueDate: isoFromOffset(-10),
dueDate: isoFromOffset(20),
status: "pending",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: null,
rubisEarned: 0,
createdAt: isoFromOffset(-10),
updatedAt: isoFromOffset(-10),
},
{
id: "inv_003",
organizationId: ORG,
clientId: "cli_lemoine",
clientName: "Garage Lemoine",
numero: "F-2026-0045",
amountTtcCents: 89_000,
issueDate: isoFromOffset(-5),
dueDate: isoFromOffset(25),
status: "pending",
planId: "plan_patient",
planName: "Patient",
pdfStorageKey: null,
notes: null,
rubisEarned: 0,
createdAt: isoFromOffset(-5),
updatedAt: isoFromOffset(-5),
},
// En relance (échéance passée, étape envoyée)
{
id: "inv_004",
organizationId: ORG,
clientId: "cli_durand",
clientName: "Atelier Durand",
numero: "F-2026-0039",
amountTtcCents: 360_000,
issueDate: isoFromOffset(-34),
dueDate: isoFromOffset(-4),
status: "in_relance",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: "Client a confirmé la réception le 14/04 par téléphone.",
rubisEarned: 1,
statusLabel: "Relance J+3 envoyée",
createdAt: isoFromOffset(-34),
updatedAt: isoFromOffset(-1),
},
{
id: "inv_005",
organizationId: ORG,
clientId: "cli_durand",
clientName: "Atelier Durand",
numero: "F-2026-0036",
amountTtcCents: 180_000,
issueDate: isoFromOffset(-50),
dueDate: isoFromOffset(-20),
status: "in_relance",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: null,
rubisEarned: 2,
statusLabel: "Relance J+10 envoyée",
createdAt: isoFromOffset(-50),
updatedAt: isoFromOffset(-10),
},
{
id: "inv_006",
organizationId: ORG,
clientId: "cli_martin",
clientName: "Boulangerie Martin SARL",
numero: "F-2026-0033",
amountTtcCents: 95_500,
issueDate: isoFromOffset(-40),
dueDate: isoFromOffset(-10),
status: "in_relance",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: null,
rubisEarned: 1,
statusLabel: "Relance J+10 demain",
createdAt: isoFromOffset(-40),
updatedAt: isoFromOffset(-3),
},
// À valider (mise en demeure)
{
id: "inv_007",
organizationId: ORG,
clientId: "cli_rousseau",
clientName: "Cabinet Rousseau",
numero: "F-2026-0028",
amountTtcCents: 85_000,
issueDate: isoFromOffset(-65),
dueDate: isoFromOffset(-35),
status: "awaiting_user_confirmation",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: null,
rubisEarned: 2,
statusLabel: "Mise en demeure prête",
createdAt: isoFromOffset(-65),
updatedAt: isoFromOffset(0),
},
// Encaissées
{
id: "inv_008",
organizationId: ORG,
clientId: "cli_lemoine",
clientName: "Garage Lemoine",
numero: "F-2026-0035",
amountTtcCents: 420_000,
issueDate: isoFromOffset(-45),
dueDate: isoFromOffset(-15),
status: "paid",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: null,
rubisEarned: 2,
createdAt: isoFromOffset(-45),
updatedAt: isoFromOffset(0),
},
{
id: "inv_009",
organizationId: ORG,
clientId: "cli_lefevre",
clientName: "Studio Lefèvre",
numero: "F-2026-0030",
amountTtcCents: 156_000,
issueDate: isoFromOffset(-60),
dueDate: isoFromOffset(-30),
status: "paid",
planId: "plan_standard",
planName: "Standard B2B",
pdfStorageKey: null,
notes: null,
rubisEarned: 1,
createdAt: isoFromOffset(-60),
updatedAt: isoFromOffset(-25),
},
// Litige
{
id: "inv_010",
organizationId: ORG,
clientId: "cli_rousseau",
clientName: "Cabinet Rousseau",
numero: "F-2026-0019",
amountTtcCents: 1_200_000,
issueDate: isoFromOffset(-100),
dueDate: isoFromOffset(-70),
status: "litigation",
planId: null,
planName: null,
pdfStorageKey: null,
notes: "Litige en cours — passé en contentieux.",
rubisEarned: 3,
createdAt: isoFromOffset(-100),
updatedAt: isoFromOffset(-15),
},
];