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>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import { Gem } from "@/components/brand/Gem";
|
|
import { Card } from "@/components/ui/Card";
|
|
import { formatRubisToHours } from "@/lib/format";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* RubisHero — la pièce centrale du dashboard.
|
|
* Cf. wireframe 4.1 : ◆ géant + "X rubis gagnés ce mois" + verbalisation
|
|
* de la conversion en heures + barre de progression vers l'objectif mensuel.
|
|
*
|
|
* Layout asymétrique :
|
|
* - Mobile : centré, gem en haut
|
|
* - Desktop : gem à gauche (XL), texte au milieu, progression à droite
|
|
*
|
|
* Pas une carte plate "X stat" : c'est l'identité émotionnelle de l'app.
|
|
*/
|
|
type RubisHeroProps = {
|
|
rubisThisMonth: number;
|
|
/** Pourcentage 0-100 vers l'objectif. */
|
|
monthlyGoalProgress: number;
|
|
/** Top X% des utilisateurs sur le mois (si disponible). */
|
|
percentile?: number;
|
|
className?: string;
|
|
};
|
|
|
|
export function RubisHero({
|
|
rubisThisMonth,
|
|
monthlyGoalProgress,
|
|
percentile,
|
|
className,
|
|
}: RubisHeroProps) {
|
|
const progress = Math.min(100, Math.max(0, monthlyGoalProgress));
|
|
const hoursLabel = formatRubisToHours(rubisThisMonth);
|
|
|
|
return (
|
|
<Card variant="hero" padding="md" className={cn("relative overflow-hidden", className)}>
|
|
{/* Halo rubis-glow en arrière-plan, très discret */}
|
|
<div
|
|
aria-hidden="true"
|
|
className="absolute -top-20 -right-16 size-64 rounded-full pointer-events-none"
|
|
style={{
|
|
background:
|
|
"radial-gradient(circle, rgba(251,228,234,0.7), transparent 65%)",
|
|
}}
|
|
/>
|
|
|
|
<div className="relative z-10 flex flex-col gap-6 sm:flex-row sm:items-center sm:gap-8">
|
|
<div className="flex shrink-0 items-center gap-4 sm:flex-col sm:items-center sm:gap-1">
|
|
<Gem size={56} glow aria-label="Compteur de rubis" />
|
|
<p className="text-[10.5px] font-semibold uppercase tracking-[0.16em] text-ink-3 sm:mt-1">
|
|
Rubis
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-display text-[28px] font-bold leading-[1.1] tracking-[-0.022em] text-ink sm:text-[26px]">
|
|
<span className="tabular-nums">{rubisThisMonth}</span> rubis{" "}
|
|
<em className="not-italic text-rubis">gagnés</em> ce mois
|
|
</p>
|
|
<p className="mt-1.5 text-[13.5px] leading-relaxed text-ink-2">
|
|
≈{" "}
|
|
<span className="font-semibold text-ink tabular-nums">{hoursLabel}</span>{" "}
|
|
que vous n'avez pas passées à relancer.
|
|
{percentile !== undefined && (
|
|
<>
|
|
{" "}
|
|
Vous êtes dans le <strong>top {percentile} %</strong> ce mois-ci.
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="shrink-0 sm:w-[160px]">
|
|
<div
|
|
className="h-1.5 w-full overflow-hidden rounded-full bg-cream-2"
|
|
role="progressbar"
|
|
aria-valuenow={progress}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
>
|
|
<div
|
|
className="h-full bg-rubis transition-[width] duration-500"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
<p className="mt-2 text-[10.5px] font-semibold uppercase tracking-[0.1em] text-ink-3">
|
|
Objectif mensuel · <span className="tabular-nums">{progress} %</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|