chore: build
ci-web / test (push) Canceled after 0s
ci-api / test (push) Canceled after 24s

This commit is contained in:
Xor290
2026-08-22 21:58:39 +02:00
parent f407bc3dd0
commit 0f28d4757e
9 changed files with 164 additions and 11 deletions
@@ -11,9 +11,11 @@ interface Alert {
active: boolean
}
// Les deux seuls types d'alerte que peut envoyer un livreur (voir
// DeliveryAlertsSection) : contrôle de police / guet-apens.
const INITIAL: Alert[] = [
{ id: '1', driver: 'lucas_d', message: 'Accès bloqué, portail fermé', time: '03/08/2026, 14:32', active: true },
{ id: '2', driver: 'emma_l', message: 'Client injoignable', time: '02/08/2026, 19:05', active: false },
{ id: '1', driver: 'lucas_d', message: 'Guet-apens', time: '03/08/2026, 14:32', active: true },
{ id: '2', driver: 'emma_l', message: 'Contrôle de police', time: '02/08/2026, 19:05', active: false },
]
// Le bouton "Résoudre" change réellement l'état de l'alerte (badge +
+7
View File
@@ -97,6 +97,12 @@ export interface CodeBuySub {
created_at: string
}
export interface PremiumUser {
username: string
activated_at: string
expired_at: string
}
export interface Contact {
id: string
username: string
@@ -223,6 +229,7 @@ export const api = {
transferDemoToPremium: (id: string) => request<Demo>('POST', `/demos/${id}/premium`),
listCodes: () => request<{ items: CodeBuySub[] }>('GET', '/codes'),
listPremiumUsers: () => request<{ items: PremiumUser[] }>('GET', '/premium'),
createCode: (username: string) =>
request<{ code: string }>('POST', '/codes', { username }),
addCode: (code: string) =>
+8 -4
View File
@@ -262,8 +262,8 @@ export function Documentation() {
{/* Sélecteur de rôle + sommaire */}
<Box borderTopWidth="1px" borderBottomWidth="1px" bg="chakra-subtle-bg" position="sticky" top={0} zIndex={2}>
<Container maxW="container.lg" py={3}>
<HStack spacing={2} mb={2} overflowX="auto">
<Container maxW="container.lg" py={{ base: 2, md: 3 }}>
<HStack spacing={2} mb={{ base: 0, md: 2 }} overflowX="auto">
{ROLES.map((r) => (
<Button
key={r.key}
@@ -278,10 +278,14 @@ export function Documentation() {
</Button>
))}
</HStack>
<Wrap spacing={5} shouldWrapChildren>
{/* Sommaire des sections : utile en desktop (nav rapide), mais sur
mobile ça se réplique sur plusieurs lignes et reste collé en
haut de l'écran en permanence (sticky) masqué plutôt que de
manger l'espace, le scroll normal suffit sur ce format. */}
<Wrap spacing={4} shouldWrapChildren display={{ base: 'none', md: 'flex' }}>
{activeRole.features.map((f) => (
<WrapItem key={f.id}>
<Text as="a" href={`#${f.id}`} fontSize="sm" fontWeight="semibold" color="primary.500" whiteSpace="nowrap" _hover={{ textDecoration: 'underline' }}>
<Text as="a" href={`#${f.id}`} fontSize="xs" fontWeight="medium" color="gray.500" whiteSpace="nowrap" _hover={{ color: 'primary.500', textDecoration: 'underline' }}>
{f.kicker}
</Text>
</WrapItem>
-1
View File
@@ -53,7 +53,6 @@ const plans: Plan[] = [
description: 'Passez en production : stockage persistant, votre démo n\'expire plus.',
features: [
'Tout ce qui est inclus dans Démo',
'Votre nom de domaine',
'Support prioritaire',
],
cta: 'Nous contacter',
+78 -3
View File
@@ -24,15 +24,32 @@ import {
VStack,
} from '@chakra-ui/react'
import { useNavigate } from 'react-router-dom'
import { api, ApiError, type CodeBuySub } from '../../lib/api'
import { api, ApiError, type CodeBuySub, type PremiumUser } from '../../lib/api'
// Polling interval pour rafraîchir la liste
const POLL_MS = 10000
// Formatage date FR court (ex. "22 août 2026"). Renvoie "—" si vide/invalide.
function formatDate(iso: string) {
if (!iso) return '—'
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return '—'
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: 'long', year: 'numeric' })
}
// Nombre de jours restants avant l'échéance (négatif = déjà expiré).
function daysLeft(iso: string): number | null {
if (!iso) return null
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return null
return Math.ceil((d.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
}
export function Codes() {
const toast = useToast()
const navigate = useNavigate()
const [codes, setCodes] = useState<CodeBuySub[]>([])
const [premiumUsers, setPremiumUsers] = useState<PremiumUser[]>([])
const [loading, setLoading] = useState(true)
const [busy, setBusy] = useState<string | null>(null)
const [username, setUsername] = useState('')
@@ -40,8 +57,9 @@ export function Codes() {
const load = useCallback(async () => {
try {
const res = await api.listCodes()
setCodes(res.items ?? [])
const [codesRes, premiumRes] = await Promise.all([api.listCodes(), api.listPremiumUsers()])
setCodes(codesRes.items ?? [])
setPremiumUsers(premiumRes.items ?? [])
} catch (err) {
if (err instanceof ApiError && err.status === 401) navigate('/admin/login')
else toast({ status: 'error', title: 'Chargement des codes impossible' })
@@ -179,6 +197,9 @@ export function Codes() {
</Box>
{/* Liste des codes */}
<Heading size="sm" mb={3}>
Codes générés (non encore utilisés)
</Heading>
{loading ? (
<Spinner />
) : codes.length === 0 ? (
@@ -225,6 +246,60 @@ export function Codes() {
</Table>
</TableContainer>
)}
{/* Clients premium (codes déjà activés) */}
<Heading size="sm" mt={10} mb={3}>
Clients premium
</Heading>
{loading ? (
<Spinner />
) : premiumUsers.length === 0 ? (
<Text color="gray.500">Aucun client premium pour le moment.</Text>
) : (
<TableContainer borderWidth="1px" borderRadius="lg">
<Table>
<Thead>
<Tr>
<Th>Client</Th>
<Th>Code activé le</Th>
<Th>Abonnement expire le</Th>
<Th>Statut</Th>
</Tr>
</Thead>
<Tbody>
{premiumUsers.map((u) => {
const left = daysLeft(u.expired_at)
return (
<Tr key={u.username}>
<Td>
<Badge colorScheme="purple" px={2} py={1}>
{u.username}
</Badge>
</Td>
<Td fontSize="sm" color="gray.400">
{formatDate(u.activated_at)}
</Td>
<Td fontSize="sm" color="gray.400">
{formatDate(u.expired_at)}
</Td>
<Td>
{left === null ? (
<Text color="gray.400">—</Text>
) : left < 0 ? (
<Badge colorScheme="red">Expiré</Badge>
) : (
<Badge colorScheme={left <= 7 ? 'orange' : 'green'}>
{left} j restant{left > 1 ? 's' : ''}
</Badge>
)}
</Td>
</Tr>
)
})}
</Tbody>
</Table>
</TableContainer>
)}
</>
)
}