import { useCallback, useEffect, useState } from 'react' import { Badge, Box, Button, Flex, Heading, Link, Spacer, Spinner, Table, TableContainer, Tbody, Td, Text, Th, Thead, Tr, useToast, Wrap, } from '@chakra-ui/react' import { useNavigate } from 'react-router-dom' import { api, ApiError, type Project } from '../../lib/api' import { formatDate, monthsLabel, projectStatusColor, projectStatusLabel, timeRemaining } from '../../lib/format' import { ConfirmDialog } from '../../components/ConfirmDialog' import { CreateProjectModal } from '../../components/CreateProjectModal' import { ExtendProjectModal } from '../../components/ExtendProjectModal' // Un déploiement en cours => on rafraîchit régulièrement. const POLL_MS = 5000 // Projets vitrine déployés pour des clients sur abonnement de 1 à 12 mois. export function Projects() { const toast = useToast() const navigate = useNavigate() const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(true) const [busy, setBusy] = useState(null) const [createOpen, setCreateOpen] = useState(false) const [toExtend, setToExtend] = useState(null) const [toDelete, setToDelete] = useState(null) const load = useCallback(async () => { try { const res = await api.listProjects() setProjects((res.items ?? []).filter((p) => p.status !== 'deleted')) } catch (err) { if (err instanceof ApiError && err.status === 401) navigate('/admin/login') else toast({ status: 'error', title: 'Chargement des projets impossible' }) } finally { setLoading(false) } }, [navigate, toast]) useEffect(() => { void load() const id = setInterval(() => void load(), POLL_MS) return () => clearInterval(id) }, [load]) const confirmDelete = async () => { if (!toDelete) return const p = toDelete setBusy(p.id) try { await api.deleteProject(p.id) toast({ status: 'success', title: 'Projet supprimé' }) setToDelete(null) await load() } catch (err) { const msg = err instanceof ApiError ? err.message : 'Erreur' toast({ status: 'error', title: 'Suppression impossible', description: msg }) } finally { setBusy(null) } } // Échéance affichée : date de suppression et temps restant. const deadline = (p: Project) => ( {formatDate(p.expires_at)} · {timeRemaining(p.expires_at)} ) return ( Projets {loading ? ( ) : projects.length === 0 ? ( Aucun projet déployé. ) : ( {projects.map((p) => { const locked = busy === p.id const canExtend = p.status === 'ready' || p.status === 'provisioning' return ( ) })}
Client Statut Adresse Abonnement Échéance Admins
{p.client_name} {projectStatusLabel(p.status)} {p.host} {monthsLabel(p.months_purchased)} au total {deadline(p)} {p.admin_username} · max {p.admin_number}
)} setCreateOpen(false)} onCreated={() => void load()} /> setToExtend(null)} onExtended={() => void load()} /> void confirmDelete()} onClose={() => setToDelete(null)} > Le projet de {toDelete?.client_name} sera supprimé immédiatement, avec ses données et ses sauvegardes. Cette action est irréversible.
) }