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>
427 lines
12 KiB
TypeScript
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),
|
|
},
|
|
];
|