chore: add documentation
ci-web / test (push) Successful in 14m34s

This commit is contained in:
Xor290
2026-08-03 15:53:25 +02:00
parent f4b6ef7563
commit fb274b3c35
6 changed files with 1934 additions and 1645 deletions
+1510
View File
File diff suppressed because one or more lines are too long
-1510
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Omnex — Plateforme de gestion de commandes & livraison</title>
<meta name="description" content="Déployez en un clic une démo complète de la plateforme de gestion de commandes et de livraison." />
<script type="module" crossorigin src="/assets/index-CbtPOOTw.js"></script>
<script type="module" crossorigin src="/assets/index-B44IZlvi.js"></script>
</head>
<body>
<div id="root"></div>
+39 -1
View File
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import type { ChangeEvent, ReactNode } from 'react'
import { Badge, Box, Flex, HStack, Progress, Stack, Text } from '@chakra-ui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
// Petits composants de maquette pour la page Documentation — reconstituent
@@ -298,6 +299,43 @@ export function MockStack({ children, spacing = 1.5 }: { children: ReactNode; sp
return <Stack spacing={spacing}>{children}</Stack>
}
// Champ de recherche fonctionnel (filtrage en direct côté appelant).
export function MockSearchInput({
value,
onChange,
placeholder,
}: {
value: string
onChange: (v: string) => void
placeholder: string
}) {
return (
<Box position="relative" my={2}>
<FontAwesomeIcon
icon={faMagnifyingGlass}
style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: COLORS.text3, fontSize: '0.65rem' }}
/>
<Box
as="input"
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
placeholder={placeholder}
w="100%"
bg={COLORS.bg}
border="1px solid"
borderColor={COLORS.border}
borderRadius="8px"
color={COLORS.text}
fontSize="0.7rem"
py={1.5}
pl="26px"
pr={2}
outline="none"
/>
</Box>
)
}
// Encart "accès restreint" — met en avant qu'un rôle (cabine, livreur) ne
// voit qu'un sous-ensemble volontairement limité des actions admin.
export function RestrictedNote({ children }: { children: ReactNode }) {
@@ -1,80 +1,59 @@
import { useState } from 'react'
import { faArrowRotateRight, faFileExport, faTruck, faUserCheck } from '@fortawesome/free-solid-svg-icons'
import { useMemo, useState } from 'react'
import { faArrowRotateRight, faFileExport, faSync, faTruck, faUserCheck } from '@fortawesome/free-solid-svg-icons'
import {
COLORS,
MockButton,
MockCard,
MockRow,
MockTabs,
MockSearchInput,
MockText,
MockupDevice,
} from '../AdminMockup'
type Tab = 'actives' | 'approuvees' | 'annulees'
interface Order {
id: string
client: string
extra: string
tone: 'warning' | 'info' | 'accent'
label: string
amount: string
}
const ORDERS: Record<Tab, { id: string; client: string; extra: string; tone: 'warning' | 'info' | 'accent'; label: string; amount: string }[]> = {
actives: [
const ACTIVE: Order[] = [
{ id: '#1042', client: 'client_marie · 12 rue des Lilas', extra: 'il y a 4 min', tone: 'warning', label: 'En attente', amount: '46,00 €' },
{ id: '#1041', client: 'client_paul · Livreur: karim_d', extra: 'Net après parrainage', tone: 'info', label: 'En route', amount: '33,00 €' },
{ id: '#1040', client: 'client_lea · 8 avenue Foch', extra: 'en attente dassignation', tone: 'accent', label: 'Livreur arrivé', amount: '58,50 €' },
],
approuvees: [
{ id: '#1038', client: 'client_sam · 3 rue Victor Hugo', extra: 'Terminée il y a 1 h', tone: 'info', label: 'Livrée', amount: '27,00 €' },
{ id: '#1036', client: 'client_ana · 21 bd Voltaire', extra: 'Terminée hier', tone: 'info', label: 'Livrée', amount: '41,00 €' },
],
annulees: [
{ id: '#1029', client: 'client_theo · adresse introuvable', extra: 'Annulée par le livreur', tone: 'warning', label: 'Annulée', amount: '19,00 €' },
],
}
const TABS: { key: Tab; label: string }[] = [
{ key: 'actives', label: 'Actives' },
{ key: 'approuvees', label: 'Approuvées' },
{ key: 'annulees', label: 'Annulées' },
]
export function CommandesSection() {
const [tab, setTab] = useState<Tab>('actives')
const [expandedId, setExpandedId] = useState<string | null>('#1040')
const APPROVED: Order[] = [
{ id: '#1038', client: 'client_sam · 3 rue Victor Hugo', extra: 'Terminée il y a 1 h', tone: 'info', label: 'Livrée', amount: '27,00 €' },
{ id: '#1036', client: 'client_ana · 21 bd Voltaire', extra: 'Terminée hier', tone: 'info', label: 'Livrée', amount: '41,00 €' },
{ id: '#1030', client: 'client_marie · 5 rue de Rivoli', extra: 'Terminée il y a 3 j', tone: 'info', label: 'Livrée', amount: '22,00 €' },
]
const orders = ORDERS[tab]
const CANCELLED: Order[] = [
{ id: '#1029', client: 'client_theo · adresse introuvable', extra: 'Annulée par le livreur', tone: 'warning', label: 'Annulée', amount: '19,00 €' },
]
const TONE_COLOR = { warning: COLORS.warning, info: COLORS.info, accent: COLORS.accentLight }
function OrderCard({ o, isOpen, onClick, children }: { o: Order; isOpen: boolean; onClick: () => void; children?: React.ReactNode }) {
return (
<MockupDevice>
<MockRow>
<MockText size="0.85rem" variant="primary">
Commandes
</MockText>
<MockButton tone="outline" icon={faFileExport}>
Export CSV
</MockButton>
</MockRow>
<MockText variant="muted" size="0.66rem">
Cliquez un onglet, puis une commande, pour explorer
</MockText>
<div style={{ marginTop: 10 }}>
<MockTabs tabs={TABS} active={tab} onChange={setTab} />
</div>
{orders.map((o) => {
const isOpen = expandedId === o.id
return (
<MockCard key={o.id} active={isOpen} onClick={() => setExpandedId(isOpen ? null : o.id)}>
<MockCard active={isOpen} onClick={onClick}>
<MockRow>
<MockText mono>{o.id}</MockText>
<MockText variant={o.tone === 'warning' ? 'danger' : 'primary'} size="0.62rem">
<span
style={{
background: `${{ warning: COLORS.warning, info: COLORS.info, accent: COLORS.accentLight }[o.tone]}26`,
color: { warning: COLORS.warning, info: COLORS.info, accent: COLORS.accentLight }[o.tone],
background: `${TONE_COLOR[o.tone]}26`,
color: TONE_COLOR[o.tone],
padding: '2px 9px',
borderRadius: 999,
fontWeight: 800,
fontSize: '0.62rem',
}}
>
{o.label}
</span>
</MockText>
</MockRow>
<MockText variant="secondary" size="0.7rem">
{o.client}
@@ -87,7 +66,100 @@ export function CommandesSection() {
{o.amount}
</MockText>
</MockRow>
{children}
</MockCard>
)
}
// Reproduit la vraie structure de l'écran Commandes : les commandes
// actives sont toujours visibles ; "Approuvées" et "Annulées" sont des
// archives dépliables indépendantes, chacune avec sa propre recherche par
// nom de client (filtrage en direct, comme dans l'app).
export function CommandesSection() {
const [expandedId, setExpandedId] = useState<string | null>('#1040')
const [archiveOpen, setArchiveOpen] = useState<'approuvees' | 'annulees' | null>(null)
const [search, setSearch] = useState('')
const archiveData = archiveOpen === 'approuvees' ? APPROVED : archiveOpen === 'annulees' ? CANCELLED : []
const filtered = useMemo(
() => archiveData.filter((o) => o.client.toLowerCase().includes(search.toLowerCase())),
[archiveData, search],
)
const toggleArchive = (key: 'approuvees' | 'annulees') => {
setArchiveOpen((v) => (v === key ? null : key))
setSearch('')
}
return (
<MockupDevice>
<MockRow>
<MockButton tone="outline" icon={faSync}>
Actualiser
</MockButton>
<MockButton tone="outline" icon={faFileExport}>
Export CSV
</MockButton>
</MockRow>
<div style={{ display: 'flex', gap: 6, marginTop: 10, marginBottom: 6 }}>
<div
onClick={() => toggleArchive('approuvees')}
style={{
cursor: 'pointer',
flex: 1,
textAlign: 'center',
padding: '6px 4px',
borderRadius: 999,
fontSize: '0.66rem',
fontWeight: 700,
border: `1px solid ${archiveOpen === 'approuvees' ? COLORS.success : COLORS.border}`,
background: archiveOpen === 'approuvees' ? `${COLORS.success}22` : 'transparent',
color: archiveOpen === 'approuvees' ? COLORS.success : COLORS.text3,
}}
>
Approuvées ({APPROVED.length})
</div>
<div
onClick={() => toggleArchive('annulees')}
style={{
cursor: 'pointer',
flex: 1,
textAlign: 'center',
padding: '6px 4px',
borderRadius: 999,
fontSize: '0.66rem',
fontWeight: 700,
border: `1px solid ${archiveOpen === 'annulees' ? COLORS.danger : COLORS.border}`,
background: archiveOpen === 'annulees' ? `${COLORS.danger}22` : 'transparent',
color: archiveOpen === 'annulees' ? COLORS.danger : COLORS.text3,
}}
>
Annulées ({CANCELLED.length})
</div>
</div>
{archiveOpen && (
<div style={{ marginBottom: 10 }}>
<MockSearchInput value={search} onChange={setSearch} placeholder="Rechercher par username..." />
<MockText variant="muted" size="0.62rem">
{filtered.length} résultat{filtered.length > 1 ? 's' : ''}
</MockText>
{filtered.map((o) => (
<OrderCard key={o.id} o={o} isOpen={false} onClick={() => {}} />
))}
<div style={{ borderTop: `1px solid ${COLORS.border}`, margin: '10px 0' }} />
</div>
)}
<MockText size="0.78rem" variant="primary">
Commandes actives ({ACTIVE.length})
</MockText>
<div style={{ marginTop: 8 }}>
{ACTIVE.map((o) => {
const isOpen = expandedId === o.id
return (
<OrderCard key={o.id} o={o} isOpen={isOpen} onClick={() => setExpandedId(isOpen ? null : o.id)}>
{isOpen && (
<div style={{ marginTop: 10, paddingTop: 10, borderTop: `1px solid ${COLORS.border}`, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<MockButton tone="accent" icon={faUserCheck}>
@@ -101,9 +173,10 @@ export function CommandesSection() {
</MockButton>
</div>
)}
</MockCard>
</OrderCard>
)
})}
</div>
</MockupDevice>
)
}
@@ -1,79 +1,257 @@
import { useMemo, useState } from 'react'
import { faFire } from '@fortawesome/free-solid-svg-icons'
import {
faArrowRotateLeft,
faCashRegister,
faChevronLeft,
faChevronRight,
faClock,
faFire,
faReceipt,
faTrophy,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { SimpleGrid } from '@chakra-ui/react'
import { COLORS, MockBar, MockCard, MockRow, MockTabs, MockText, MockupDevice, ScreenTitle, StatTile } from '../AdminMockup'
import { faCashRegister, faChartLine, faReceipt, faTrophy } from '@fortawesome/free-solid-svg-icons'
import { COLORS, MockBar, MockButton, MockCard, MockRow, MockTabs, MockText, MockupDevice, ScreenTitle, StatTile } from '../AdminMockup'
// --- petites primitives locales, propres à cet écran très dense ---
function SectionCard({
title,
onReset,
since,
children,
}: {
title: string
onReset?: () => void
since?: string
children: React.ReactNode
}) {
return (
<MockCard>
<MockRow>
<MockText variant="secondary" size="0.72rem">
{title}
</MockText>
{onReset && (
<MockButton tone="outlineDanger" icon={faArrowRotateLeft} onClick={onReset}>
Réinitialiser
</MockButton>
)}
</MockRow>
{since && (
<MockText variant="muted" size="0.58rem">
Depuis le {since}
</MockText>
)}
<div style={{ marginTop: 8 }}>{children}</div>
</MockCard>
)
}
function HRow({ label, value, pct, color, highlight }: { label: string; value: string; pct: number; color: string; highlight?: boolean }) {
return (
<div style={{ marginBottom: 6 }}>
<MockRow>
<MockText variant={highlight ? 'primary' : 'muted'} size="0.64rem">
{highlight && <FontAwesomeIcon icon={faFire} style={{ color: COLORS.warning, marginRight: 4 }} />}
{label}
</MockText>
<MockText variant={highlight ? 'primary' : 'muted'} size="0.64rem">
{value}
</MockText>
</MockRow>
<MockBar value={pct} color={highlight ? COLORS.warning : color} />
</div>
)
}
const MONTHS = ['Mai 2026', 'Juin 2026', 'Juillet 2026', 'Août 2026']
const MONTH_DAYS = [
{ day: '02/08', orders: 14, revenue: '312€', best: false },
{ day: '01/08', orders: 22, revenue: '498€', best: true },
{ day: '31/07', orders: 9, revenue: '201€', best: false },
]
type SortKey = 'quantite' | 'commandes' | 'revenus'
const PRODUCTS = [
{ name: 'Pack Découverte', quantite: 210, commandes: 86, revenus: 1032 },
{ name: 'Édition Standard', quantite: 140, commandes: 54, revenus: 648 },
{ name: 'Format Duo', quantite: 96, commandes: 41, revenus: 492 },
]
const SORT_TABS: { key: SortKey; label: string }[] = [
{ key: 'quantite', label: 'Quantité' },
{ key: 'commandes', label: 'Commandes' },
{ key: 'revenus', label: 'Revenus' },
]
const BAR_COLOR: Record<SortKey, string> = {
quantite: COLORS.accent,
commandes: COLORS.info,
revenus: COLORS.success,
const PRODUCTS: Record<SortKey, { name: string; value: number; display: string }[]> = {
quantite: [
{ name: 'Pack Découverte', value: 210, display: '210' },
{ name: 'Édition Standard', value: 140, display: '140' },
{ name: 'Format Duo', value: 96, display: '96' },
],
commandes: [
{ name: 'Pack Découverte', value: 86, display: '86' },
{ name: 'Édition Standard', value: 54, display: '54' },
{ name: 'Format Duo', value: 41, display: '41' },
],
revenus: [
{ name: 'Pack Découverte', value: 1032, display: '1032€' },
{ name: 'Édition Standard', value: 648, display: '648€' },
{ name: 'Format Duo', value: 492, display: '492€' },
],
}
// Le tri "Top produits" est un vrai tri (state) qui recalcule et réanime
// les barres — pas 3 captures figées.
const HOURS = [
{ label: '10h', value: 4 },
{ label: '12h', value: 18 },
{ label: '14h', value: 9 },
{ label: '18h', value: 22 },
{ label: '20h', value: 31 },
{ label: '22h', value: 12 },
]
const WEEKDAYS = [
{ label: 'Lun', value: 32 },
{ label: 'Mar', value: 28 },
{ label: 'Mer', value: 35 },
{ label: 'Jeu', value: 41 },
{ label: 'Ven', value: 58 },
{ label: 'Sam', value: 71 },
{ label: 'Dim', value: 47 },
]
const DOSES = [
{ label: '1g', value: 40 },
{ label: '3.5g', value: 86 },
{ label: '5g', value: 22 },
{ label: '10g', value: 12 },
]
// Écran très dense dans l'app réelle : reproduit ici l'intégralité des
// sections (KPIs, activité du jour, historique mensuel navigable, 4
// graphiques 30 jours/heures/jours/doses, top produits triable), dans un
// cadre scrollable comme le serait un vrai écran de téléphone.
export function StatistiquesSection() {
const [monthIdx, setMonthIdx] = useState(MONTHS.length - 1)
const [sort, setSort] = useState<SortKey>('revenus')
const sorted = useMemo(() => [...PRODUCTS].sort((a, b) => b[sort] - a[sort]), [sort])
const max = sorted[0][sort]
const sortedProducts = useMemo(() => [...PRODUCTS[sort]].sort((a, b) => b.value - a.value), [sort])
const maxProduct = sortedProducts[0].value
const maxHour = Math.max(...HOURS.map((h) => h.value))
const peakHour = HOURS.reduce((a, b) => (b.value > a.value ? b : a))
const maxWeekday = Math.max(...WEEKDAYS.map((w) => w.value))
const peakWeekday = WEEKDAYS.reduce((a, b) => (b.value > a.value ? b : a))
const maxDose = Math.max(...DOSES.map((d) => d.value))
return (
<MockupDevice>
<ScreenTitle title="Statistiques" subtitle="Activité globale & produits" />
<div style={{ maxHeight: 560, overflowY: 'auto', paddingRight: 2 }}>
<SimpleGrid columns={2} spacing={2} mb={2.5}>
<StatTile icon={faReceipt} value={312} label="Commandes totales" color={COLORS.accentLight} />
<StatTile icon={faCashRegister} value="6,4k€" label="Revenus" color={COLORS.success} />
<StatTile icon={faCashRegister} value="6,4k€" label="Revenus (terminées)" color={COLORS.success} />
<StatTile icon={faClock} value="10,4" label="Moy. commandes/jour" color={COLORS.info} />
<StatTile icon={faTrophy} value="Samedi" label="Jour de pointe" color={COLORS.warning} />
</SimpleGrid>
<MockCard>
<MockText variant="secondary" size="0.72rem">
Activité du jour
</MockText>
<div style={{ display: 'flex', gap: 10, margin: '6px 0 8px' }}>
<MockText variant="muted" size="0.6rem">18 commandes</MockText>
<MockText variant="muted" size="0.6rem">420g vendus</MockText>
<MockText variant="muted" size="0.6rem">312</MockText>
</div>
<HRow label="Fleurs — Pack Découverte" value="86 · 1032€" pct={90} color={COLORS.accent} />
<HRow label="Résines — Format Duo" value="41 · 492€" pct={45} color={COLORS.info} />
</MockCard>
<MockCard>
<MockRow>
<button
onClick={() => setMonthIdx((i) => Math.max(0, i - 1))}
disabled={monthIdx === 0}
style={{ background: 'none', border: 'none', cursor: monthIdx === 0 ? 'default' : 'pointer', color: monthIdx === 0 ? COLORS.border : COLORS.text2 }}
>
<FontAwesomeIcon icon={faChevronLeft} />
</button>
<MockText variant="primary" size="0.72rem">
{MONTHS[monthIdx]}
</MockText>
<button
onClick={() => setMonthIdx((i) => Math.min(MONTHS.length - 1, i + 1))}
disabled={monthIdx === MONTHS.length - 1}
style={{ background: 'none', border: 'none', cursor: monthIdx === MONTHS.length - 1 ? 'default' : 'pointer', color: monthIdx === MONTHS.length - 1 ? COLORS.border : COLORS.text2 }}
>
<FontAwesomeIcon icon={faChevronRight} />
</button>
</MockRow>
<MockText variant="muted" size="0.6rem">Historique mensuel cliquez les flèches</MockText>
<div style={{ marginTop: 8 }}>
{MONTH_DAYS.map((d) => (
<HRow key={d.day} label={d.day} value={`${d.orders} cmd · ${d.revenue}`} pct={(d.orders / 22) * 100} color={COLORS.accent} highlight={d.best} />
))}
</div>
</MockCard>
<SectionCard title="30 derniers jours — commandes" onReset={() => {}} since="15/07/2026">
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 50 }}>
{[4, 8, 6, 10, 7, 12, 9, 14, 6, 11].map((v, i) => (
<div key={i} style={{ flex: 1, height: `${(v / 14) * 100}%`, background: COLORS.accent, borderRadius: 2 }} />
))}
</div>
</SectionCard>
<SectionCard title="Revenus par jour (30j)" onReset={() => {}} since="15/07/2026">
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 50, marginBottom: 6 }}>
{[120, 240, 180, 300, 210, 360, 260, 410, 190, 330].map((v, i) => (
<div key={i} style={{ flex: 1, height: `${(v / 410) * 100}%`, background: COLORS.success, borderRadius: 2 }} />
))}
</div>
<MockText variant="muted" size="0.6rem">
<FontAwesomeIcon icon={faTrophy} style={{ color: COLORS.warning, marginRight: 4 }} />
Meilleure journée : 01/08 498
</MockText>
</SectionCard>
<SectionCard title="Heures d'affluence" onReset={() => {}} since="15/07/2026">
{HOURS.map((h) => (
<HRow key={h.label} label={h.label} value={String(h.value)} pct={(h.value / maxHour) * 100} color={COLORS.info} highlight={h.label === peakHour.label} />
))}
<MockText variant="muted" size="0.6rem">Heure de pointe : {peakHour.label} ({peakHour.value} commandes)</MockText>
</SectionCard>
<SectionCard title="Jours d'affluence" onReset={() => {}} since="15/07/2026">
{WEEKDAYS.map((w) => (
<HRow key={w.label} label={w.label} value={String(w.value)} pct={(w.value / maxWeekday) * 100} color={COLORS.accent} highlight={w.label === peakWeekday.label} />
))}
<MockText variant="muted" size="0.6rem">Pic d'activité : {peakWeekday.label}</MockText>
</SectionCard>
<SectionCard title="Doses populaires — Pack Découverte" onReset={() => {}} since="15/07/2026">
{DOSES.map((d) => (
<HRow key={d.label} label={d.label} value={String(d.value)} pct={(d.value / maxDose) * 100} color={COLORS.secondary} highlight={d.value === maxDose} />
))}
</SectionCard>
<MockCard mb={0}>
<MockRow>
<MockText variant="secondary" size="0.72rem">
Top produits
</MockText>
<FontAwesomeIcon icon={faChartLine} style={{ color: COLORS.text3, fontSize: '0.7rem' }} />
<MockButton tone="outlineDanger" icon={faArrowRotateLeft} onClick={() => {}}>
Réinitialiser
</MockButton>
</MockRow>
<div style={{ marginTop: 8 }}>
<MockTabs tabs={SORT_TABS} active={sort} onChange={setSort} />
</div>
{sorted.map((p, i) => {
const value = p[sort]
const display = sort === 'revenus' ? `${value}` : value
return (
<div key={p.name} style={{ marginBottom: 8 }}>
<MockRow>
<MockText variant="muted" size="0.68rem">
{i === 0 && <FontAwesomeIcon icon={faFire} style={{ color: COLORS.warning, marginRight: 4 }} />}
{p.name}
</MockText>
<MockText variant="muted" size="0.68rem">
{display}
</MockText>
</MockRow>
<MockBar value={(value / max) * 100} color={BAR_COLOR[sort]} />
</div>
)
})}
<MockText variant="muted" size="0.62rem">
<FontAwesomeIcon icon={faTrophy} style={{ marginRight: 4 }} />
Classement recalculé automatiquement selon le tri choisi
{sortedProducts.map((p, i) => (
<HRow key={p.name} label={p.name} value={p.display} pct={(p.value / maxProduct) * 100} color={COLORS.accent} highlight={i === 0} />
))}
<MockText variant="muted" size="0.6rem">
Moins vendu : {sortedProducts[sortedProducts.length - 1].name}
</MockText>
</MockCard>
</div>
</MockupDevice>
)
}