16 Commits

Author SHA1 Message Date
ordinarthur
cfd3680bb4 feat(web): saisie manuelle de facture (modale Radix Dialog)
Modale 'Nouvelle facture' (cf. wireframe 2.3) accessible depuis 4 points :
- Topbar '+ Saisir' (était disabled)
- /factures/import bouton 'Saisir manuellement' (header)
- Dropzone empty state sur /factures (variant full)
- (Reachable de partout dans _app/* via le topbar)

Composants ajoutés :
- Dialog : wrapper Radix Dialog stylé (overlay ink/35 + blur, content
  bg-cream + border-line + shadow-card, close button discret, animations
  fade+zoom). Header / Title / Description / Footer / Close.
- ClientCombobox : autocomplete maison (pas Radix Combobox qui n'existe
  pas, pas cmdk overkill). Input + dropdown filtré, click-outside ferme,
  Escape ferme, option 'Créer le client « X »' quand pas de match exact.
  Border rubis quand un client existant est sélectionné.
- ManualInvoiceDialog : form complet (TanStack Form + validateurs Zod
  par champ). Client (combobox), N° + date émission (côte-à-côte), montant
  + échéance relative 15/30/45/60/90j (Select Radix), plan de relance.

Architecture clean :
- ManualInvoiceProvider au sommet d'AppLayout rend la modale une seule
  fois (un seul réseau de portals Radix)
- Hook useManualInvoice() expose open()/close()/isOpen, accessible
  depuis n'importe quelle route enfant sans plumber des callbacks
- État local de la modale (pas dans l'URL — propre pour V1)

Logique métier MSW :
- GET /api/v1/clients (autocomplete)
- POST /api/v1/invoices : résolution client (clientId fourni → utilise,
  sinon match par nom case-insensitive, sinon création à la volée).
  +1 rubis bonus saisie.
- Conversion relativeDueDays (15/30/45/60/90) → dueDate absolue à la
  soumission

Bug fix montant TTC :
- L'input était contrôlé avec value={(cents/100).toFixed(2)} → reformat
  à chaque keystroke écrasait '10000' en '1.00' (impossible de taper
  des gros montants)
- Passé en defaultValue (uncontrolled) avec step='any' + inputMode='decimal'
- Accepte virgule FR (1240,50) et point (1240.50)
- DialogContent unmount à la fermeture → defaultValue ré-évalué à
  chaque réouverture (reset OK)

Bouton '+ Saisir' du topbar plus disabled, bouton 'Saisir manuellement'
de /factures/import plus disabled. Le bouton dans la dropzone (variant
full) reçoit un onManualEntry prop optionnel.

Bundle prod : 117.62 KB gzip core (+0.06 KB), useManualInvoiceDialog
chunk 6.68 KB gzip, Select chunk 25.14 KB gzip (partagé OCR + plan
editor + manual entry).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 11:50:46 +02:00
ordinarthur
965a92da8f feat(web): /factures/import — page focused d'import via bouton topbar
Le bouton '+ Importer factures' du topbar avait un Button inerte. Il
ouvre maintenant une vraie page focused dédiée :

- Route /factures/import (factures_.import.tsx) avec breadcrumb,
  eyebrow, H1 'Importer *plusieurs* factures.', lede explicatif,
  dropzone full-page avec mutation upload câblée
- Drop-catcher de page comme sur /factures (drop n'importe où marche)
- 3 hints discrets en bas (Formats / Confidentiel / Reprenable) pour
  rassurer le user au moment décisif de l'upload

Routing nesting fix :
- Renommé factures_.import.\$batchId.tsx → factures_.import_.\$batchId.tsx
- Trailing underscore sur 'import_' escape la nouvelle landing parent
- Les 2 routes sont maintenant siblings sous _app :
  · /factures/import → factures_.import.tsx
  · /factures/import/\$batchId → factures_.import_.\$batchId.tsx

Topbar AppLayout :
- '+ Importer factures' = Button asChild + Link to /factures/import
  (middle-click / cmd-click / right-click ouvrent un nouvel onglet)
- '+ Saisir' reste disabled (placeholder modale 2.3, prochaine étape)

Bundle prod : 117.56 KB gzip core (stable, +0.06 vs avant).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 11:35:59 +02:00
ordinarthur
86dae64eb4 feat(web): import OCR — drop fichier → review batch → factures créées
Boucle import complète (cf. wireframe 2.2) :
1. Drop PDF/PNG/JPG sur /factures (dropzone full-page si vide, compact en
   bas si populée, OU drop n'importe où sur la page grâce au drop-catcher
   de route — évite que le browser ouvre le fichier dans un onglet)
2. POST /invoices/upload → MSW génère un batch avec drafts pré-remplis
   (OCR simulé : nom client aléatoire depuis 7 entreprises plausibles,
   montant random, dates calibrées, confidences variables) + délai 800ms
3. Toast "X factures extraites. Vérifions ensemble." + navigate vers
   /factures/import/$batchId
4. Page review step-by-step : PDF preview à gauche + form à droite,
   champs douteux (confidence < 0.7) surlignés border-rubis + hint
   inline, bandeau warning rubis-glow si plusieurs champs incertains
5. Valider & suivante → POST validate → crée la facture en mockDb
   (nouveau client si nom inconnu) + 1 rubis bonus → la suivante
   apparaît automatiquement
6. Skip ou Annuler le batch entier disponibles à tout moment
7. Fin de batch → toast bilan ("X validées · Y ignorées") → /factures

Composants ajoutés :
- PdfPreview : placeholder anti-générique (pas un viewer gris) — header
  mono filename + "page A4" simulée avec barres bg-ink/15 et bg-rubis-glow
- Select : wrapper Radix Select stylé (Trigger / Content / Item) cohérent
  avec Input (1px line, focus rubis-glow ring, item sélectionné rubis + ✓)

Dropzone amélioré :
- Filtre fichier plus tolérant : MIME OU extension (Finder/Explorer
  envoient parfois type === ""), erreur dédiée taille vs format
- Mode isUploading : titre devient "On lit vos factures…", spinner
  sur le bouton Parcourir

MSW handlers (invoices.ts) :
- POST /invoices/upload : crée batch + drafts avec OCR simulé
- GET /invoices/import-batch/:id
- POST /invoices/import-batch/:id/drafts/:draftId/validate
- POST /invoices/import-batch/:id/drafts/:draftId/skip
- DELETE /invoices/import-batch/:id

mockDb étendu :
- importBatches store + StoredImportDraft type
- createImportBatch / findImportBatch / updateImportDraft / deleteImportBatch
- createInvoice / createClient / listClientsForOrg

Bug fix migration :
- Le sessionStorage stockait des snapshots d'avant l'ajout du champ
  importBatches → db.importBatches undefined → push() crashait. Ajout
  d'une migration douce dans load() qui patche les champs manquants
  avec les défauts du seed (pas de perte de données existantes).

Bundle : 117.50 KB gzip core. Route factures_.import._batchId 10.26 KB
gzip — la plus grosse à cause de Radix Select + state form complexe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 11:26:31 +02:00
ordinarthur
b5b67056aa feat(web): plans bibliothèque + éditeur
Bibliothèque /plans (cf. wireframe 3.1) :
- Grid responsive 1/2/3 cols avec PlanCard + CreatePlanCard placeholder
- PlanCard : titre, chip meta (un seul à la fois), aperçu 3 étapes avec
  ◆ rotated comme bullet, footer usage + lien "Modifier →"
- Le plan le plus utilisé reçoit le badge "✦ Le plus utilisé" (rubis-glow
  + Sparkles), les autres gardent leur label de tonalité (Doux / Standard
  / Ferme / Strict). Pas de "PLAN PAR DÉFAUT" partout — info tautologique
  vu que les 4 plans seedés sont des défauts.
- Chips de tonalité adoucis (bg-cream-2 ou rubis-glow, plus de fills lourds)
- Skeleton pulsé pendant le fetch

Éditeur /plans/$slug (cf. wireframe 3.2, route _app/plans_.$slug pour
escape la layout parent) :
- Header : eyebrow humeur + nom + compteur d'usage + boutons Dupliquer
  (V2) / Enregistrer (fonctionnel, désactivé tant que pas de changements)
- Layout 2-col responsive (1fr / 1.4fr) :
  · Gauche : cadence — list de StepCard cliquables, état sélectionné avec
    border-rubis + shadow-rubis, "+ Ajouter une étape" disabled (V2)
  · Droite : éditeur d'email — Card avec subject (Input), body (Textarea
    mono 10 rows), grille de variables-chips
- Variable insertion fonctionnelle : clic = insertion au curseur via
  selectionStart/End du textarea, label FR + token mono ({{numero}})
- Bandeau warning rubis-glow quand l'étape est requiresManualValidation :
  "Validation manuelle obligatoire. L'email est généré en brouillon"
- Save fonctionnel : isDirty calculé via JSON.stringify, mutation PATCH
  /plans/:slug, invalidate cache plans.all + setQueryData detail, toast
- Sync state local ↔ serveur via useEffect sur plan.id+updatedAt

MSW :
- handlers/plans.ts : GET /plans (avec usageCount), GET /plans/:slug,
  PATCH /plans/:slug (validation Zod, recompose ids manquants)
- mockDb : findPlanBySlug, listPlansForOrg, updatePlan
- Calcul usageCount : factures du plan en statut != paid && != cancelled

Lib /plans.ts :
- TONE_LABELS : Amical / Standard / Ferme / Mise en demeure (FR)
- planMoodLabel + planOverallTone (humeur globale = ton de la dernière étape)
- TEMPLATE_VARIABLES : 5 variables avec token + label FR + preview

Bundle prod : 117.31 KB gzip core (stable). plans 2.06 KB gzip,
plans_._slug 3.28 KB gzip — la plus grosse route chunk vu sa complexité
(form + variables + state local).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 11:05:36 +02:00
ordinarthur
14d0e982e9 feat(web): _app shell + dashboard + factures liste & détail
Layout shell pour l'app authentifiée :
- routes/_app.tsx : pathless layout avec auth-guard + onboarding-guard
  (signature null → redirect onboarding/compte)
- AppLayout : grid sidebar + topbar + main + tab bar mobile
- AppSidebar (lg+) : nav verticale + mini compteur rubis en bas
- MobileTabBar : 4 onglets fixed bottom (Accueil, Factures, Plans, Réglages)
- AppTopbar : sticky bg-cream/85 + backdrop-blur, greeting + date sur desktop,
  brand sur mobile
- UserMenu : Radix Popover, avatar initiales rubis, logout mutation

Dashboard / (cf. wireframe 4.1) :
- RubisHero : ◆ 56px + drop-shadow rubis-tinted, "X rubis gagnés" en italic
  rubis sur "gagnés", verbalisation conversion en heures, progression mensuelle
- 4 KpiCard scannables : À relancer, En cours, Encaissé, DSO
  (delta en rubis-deep si intent positif, jamais de vert succès)
- ActivityFeed : journal du jour avec icônes Lucide tonalisées
- TopLatePayers : "Retards récurrents" (pas "mauvais payeurs", cf. marque)
- Quick actions mobile (+ Photo de facture / + Saisir)

Factures liste /factures (wireframe 2.4 + 2.1) :
- 3 états : 0 facture → dropzone full-page · filtre vide → mini-empty
  · populated → filter chips + table desktop / cards mobile
- FilterChips : sync URL (validateSearch zod), counts entre parenthèses
- InvoiceTable : ligne entière cliquable (onClick + role=link + onKeyDown),
  chevron Link séparé pour right-click "ouvrir nouvel onglet"
- InvoiceCardList : version mobile aérée
- StatusBadge : 6 statuts mappés palette marque (rubis solide pour "À valider",
  ink pour "En relance", crème+✓ pour "Encaissée")
- Skeleton pulsé pendant le fetch

Détail facture /factures/$id (wireframe 4.2) :
- Header : eyebrow client + numéro + montant + échéance + délai (J−4 rouge)
  + StatusBadge inline
- Actions : Marquer encaissée (mutation + bonus rubis + invalidate)
- Layout 2-col : Timeline (1.4fr) + sidepanel client/notes (1fr)
- Timeline primitive : pastilles passé/présent/futur (rubis-glow ✓ /
  rubis solide + Clock + ring glow / cercle vide)

Bug fix routing :
- factures.$id.tsx était nesté sous factures.tsx (flat naming TanStack Router)
  → la liste s'affichait à la place de la détail. Renommé factures_.$id.tsx
  pour escape le layout parent. URL inchangée (/factures/$id).

Placeholders soignés : /plans, /clients, /parametres avec EmptyState draft
(bordure pointillée + message qui assume "ça arrive").

MSW étendu :
- mocks/seed.ts : 5 clients, 4 plans avec étapes complètes (Standard B2B,
  Rapide, Patient, Ferme), 10 invoices avec statuses variés calibrés
  sur le wireframe
- handlers/dashboard.ts : GET /dashboard/{kpis,activity,top-late}
- handlers/invoices.ts : GET /invoices (filtres + tri par priorité statut),
  GET /invoices/counts, GET /invoices/:id (timeline calculée depuis le plan),
  POST /invoices/:id/mark-paid (passe en paid + bonus rubis)

Lib étendue :
- format : formatDueDelta (J+10, J−4 avec − typographique), isOverdue
- routes/index.tsx supprimé (remplacé par _app/index.tsx)

Bundle prod : 117 KB gzip core, chaque route en chunk dédié (dashboard
inline dans _app, factures 3.69 KB gzip, factures._id 2.22 KB gzip).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 10:49:06 +02:00
ordinarthur
332bf0bcda feat(web): /signup + 3-step onboarding flow
Nouvelles routes :
- /signup : inscription (fullName + email + password) → /onboarding/compte
- /onboarding : layout avec brand + stepper, auth-guard
- /onboarding/compte : étape 1 (nom + email, prefilled depuis la session)
- /onboarding/entreprise : étape 2 (nom, SIRET optionnel, chips volume)
- /onboarding/signature : étape 3 (signature email + aperçu live)

Nouvelles primitives UI :
- <Card variant="default|flat|hero" padding="sm|md|lg">
- <Stepper> wizard horizontal (current rubis, done rubis-glow + ✓, todo line)
- <Chip selected> : pastille pill, glow + deep quand sélectionnée (le rubis
  plein reste réservé aux CTA, cf. règle "le rubis est rare")
- <Textarea> : mêmes règles a11y/focus que <Input>

MSW handlers étendus :
- PATCH /api/v1/account/profile (fullName, email, signature)
- PATCH /api/v1/organizations/me (name, siret, monthlyVolumeBucket)
- mockDb : ajout des organizations, méthodes updateUser/updateOrg

Wiring :
- /login → "Créer un compte" pointe vers /signup (avant : loop)
- /login succès → /  (au lieu de /login)
- /  → /onboarding/compte si auth, /login sinon (placeholder dashboard)
- /onboarding/signature succès → /

Bundle prod : 113.87 KB gzip core (-2 KB grâce à MSW exclu en prod via
import.meta.env.DEV). Chaque route en chunk dédié (1-2 KB gzip).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 10:22:53 +02:00
ordinarthur
8d3bab6a89 feat: scaffold frontend monorepo + first /login screen
Monorepo Turborepo (pnpm workspaces) avec 3 packages :

- apps/web : SPA React 19 + Vite 8 + Tailwind v4 (CSS-first)
  • TanStack Router (file-based, auto code-splitting), Query, Form
  • Radix primitives bruts + CVA + clsx + tailwind-merge
  • MSW pour mocker l'API tant qu'Adonis n'est pas branché
  • Polices Bricolage Grotesque + Inter self-hostées via fontsource
  • Tokens marque (rubis, cream, ink) exposés via @theme
  • Primitives maison : Gem, Brand, Eyebrow, Button, Input, Field
  • Route /login full flow : TanStack Form + Zod + mutation Query

- apps/api : Adonis 7 (kit api, scaffold via create-adonisjs)
  • Auth access tokens (Bearer) — cf. ADR-017
  • Tuyau core déjà câblé pour la génération de types
  • Routes /api/v1/auth/{signup,login} + /api/v1/account/{profile,logout}
  • Minimal — uniquement le pont front ↔ back

- packages/shared : types TS + schemas Zod + constantes
  • Source unique de vérité partagée api ↔ web
  • Domaines : User, Org, Auth, Client, Invoice, Plan

Tooling racine : Turbo, ESLint v9 flat, Prettier, husky, lint-staged.

CLAUDE.md et docs/decisions.md mis à jour avec ADR-014 à ADR-018
(stack, monorepo, PG existant, Bearer tokens, MinIO existant)
et le pointeur vers docs/tech/architecture.md.

Logo Rubis déplacé de landing/assets/ vers /assets/ (source unique
réutilisée par la landing et l'app).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 10:10:48 +02:00
ordinarthur
7c80c391f1 update wording pricing
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 14s
2026-05-05 19:08:11 +02:00
ordinarthur
42434b962a fix: typos \n\x dans le Dockerfile cassaient le build nginx
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 25s
2026-05-05 19:04:48 +02:00
ordinarthur
269c67ace2 add landing v3
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 8s
2026-05-05 18:52:34 +02:00
ordinarthur
3c1d7fbd71 add landing
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m13s
2026-05-05 16:52:10 +02:00
ordinarthur
acfa608782 landing v3 trop stylé 2026-05-05 16:32:57 +02:00
ordinarthur
43eaa31c83 update landing v2 2026-05-05 16:18:52 +02:00
ordinarthur
9a95107cc0 betterm landigng 2026-05-05 15:53:57 +02:00
ordinarthur
0bda0d6d69 init landing 2026-05-05 15:36:21 +02:00
ordinarthur
89ca81d839 init doc rubis 2026-05-05 15:29:20 +02:00