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>
95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
import { forwardRef } from "react";
|
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
import { Check, ChevronDown } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* Select stylé maison à partir des primitives Radix.
|
|
* Style aligné sur Input (1px line, focus ring rubis-glow, radius default 6px).
|
|
*
|
|
* Usage minimaliste :
|
|
* <Select value={planId} onValueChange={setPlanId}>
|
|
* <SelectTrigger>
|
|
* <SelectValue placeholder="Choisir un plan…" />
|
|
* </SelectTrigger>
|
|
* <SelectContent>
|
|
* <SelectItem value="plan_x">Standard B2B</SelectItem>
|
|
* </SelectContent>
|
|
* </Select>
|
|
*/
|
|
export const Select = SelectPrimitive.Root;
|
|
export const SelectGroup = SelectPrimitive.Group;
|
|
export const SelectValue = SelectPrimitive.Value;
|
|
|
|
export const SelectTrigger = forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<SelectPrimitive.Trigger
|
|
ref={ref}
|
|
className={cn(
|
|
"flex w-full items-center justify-between gap-2 rounded-default border border-line bg-white",
|
|
"px-3.5 py-3 font-sans text-[15px] text-ink",
|
|
"transition-[border-color,box-shadow] duration-150",
|
|
"focus:outline-none focus:border-rubis focus:ring-4 focus:ring-rubis-glow",
|
|
"disabled:cursor-not-allowed disabled:bg-cream-2 disabled:text-ink-3",
|
|
"data-[placeholder]:text-ink-3",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<SelectPrimitive.Icon asChild>
|
|
<ChevronDown size={16} className="text-ink-3 shrink-0" aria-hidden="true" />
|
|
</SelectPrimitive.Icon>
|
|
</SelectPrimitive.Trigger>
|
|
));
|
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
|
|
export const SelectContent = forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
<SelectPrimitive.Portal>
|
|
<SelectPrimitive.Content
|
|
ref={ref}
|
|
position={position}
|
|
sideOffset={6}
|
|
className={cn(
|
|
"relative z-50 min-w-[var(--radix-select-trigger-width)] overflow-hidden",
|
|
"rounded-card border border-line bg-white shadow-card",
|
|
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport>
|
|
</SelectPrimitive.Content>
|
|
</SelectPrimitive.Portal>
|
|
));
|
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
|
|
export const SelectItem = forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<SelectPrimitive.Item
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex cursor-default select-none items-center gap-2 rounded-default",
|
|
"px-3 py-2 pr-8 text-[14px] text-ink outline-none",
|
|
"data-[highlighted]:bg-cream-2 data-[highlighted]:text-ink",
|
|
"data-[state=checked]:text-rubis data-[state=checked]:font-medium",
|
|
"data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
<SelectPrimitive.ItemIndicator className="absolute right-2.5 inline-flex">
|
|
<Check size={14} className="text-rubis" aria-hidden="true" />
|
|
</SelectPrimitive.ItemIndicator>
|
|
</SelectPrimitive.Item>
|
|
));
|
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|