chore: build
This commit is contained in:
@@ -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 +
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user