Commit Graph

19 Commits

Author SHA1 Message Date
Arnaud f1df7ae1b5 feat(admin): bouton Éditer sur la fiche projet (mode admin)
La fiche projet publique /projects/:slug avait déjà des actions inline
en mode admin (Supprimer, ajouter étape, ajouter ressource, etc.), mais
pas de raccourci pour basculer vers le form d'édition complet où on
peut changer titre, slug, catégorie, status, descriptions, progression.

Ajout d'un bouton "Éditer" à côté de "Supprimer" en haut à droite (admin
only) qui pointe vers /admin/projects/:slug/edit. Style outline blanc
pour ne pas concurrencer visuellement le rouge de Supprimer.

Cohérent avec les autres tableaux admin où le clic sur le titre ouvre
maintenant l'édition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:55:19 +02:00
Arnaud 2b7d55581f feat(admin): clic sur titre de projet -> écran d'édition
Cohérence finale des 3 tableaux admin : le clic sur l'élément principal
de la 1re colonne pointe maintenant sur l'écran d'édition admin
correspondant pour les projets, les membres et les catégories.

Avant : project-detail (fiche publique). Maintenant : admin-project-edit.
Le lien "Éditer" en bout de ligne reste pour la découvrabilité, et la
fiche publique reste accessible depuis la nav globale ou /projects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:53:37 +02:00
Arnaud 8b303ab434 feat(admin): clic sur nom de catégorie -> écran d'édition
Cohérence avec /admin/members et /admin/projects : le nom dans la
1re colonne est maintenant un RouterLink vers `admin-category-edit`.
Le bouton "Éditer" en bout de ligne reste pour la découvrabilité.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:52:31 +02:00
Arnaud 8ab02409f6 fix(admin): clic sur nom de membre -> écran d'édition (était un lien mort)
Le lien sur display_name dans la liste /admin/members pointait sur
`project-detail` avec le slug du MEMBRE comme :slug -> aboutissait sur
une 404 ou la mauvaise fiche projet selon les collisions de slug.

Maintenant : pointe sur `admin-member-edit`. Plus cohérent avec le pattern
utilisé pour les projets (clic sur titre -> fiche) et avec ce que l'admin
attend dans un tableau d'admin (clic = édition).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:50:42 +02:00
Arnaud 97c68531ed feat(admin): création de membre via dashboard
Nouvelle page /admin/members/new, jumeau de l'écran d'édition :
  - Form complet : display_name, slug (auto), initials (auto), kind, bio,
    email, website, avatar URL
  - Avatar preview live (initiales / URL d'avatar mis à jour à la saisie)
  - Slug auto-généré depuis display_name (NFD + lowercase + tirets)
    tant que l'utilisateur n'a pas édité manuellement (flag slugDirty)
  - Initiales auto-suggérées (1re lettre de chaque mot, max 3, upper),
    flag initialsDirty distinct -> tu peux laisser le slug auto mais
    forcer des initiales manuelles ou vice-versa
  - Body construit en omettant les champs vides -> l'API génère ses propres
    initiales si vides, slug auto-uniqueifie sur collision (-2/-3...)
  - 201 -> redirige vers /admin/members/:slug/edit pour voir le résultat
  - 422/409/401 gérés comme les autres écrans admin

UI
  - AdminMembersPage : CTA "Nouveau membre" en haut à droite (à côté de
    la recherche, justify-between sur le wrap parent)
  - Router : route nommée admin-member-new avec guard admin

Backend
  - Aucune nouvelle route : POST /api/members existait déjà
  - Le contrôleur valide display_name (required), kind enum, email format,
    URLs ; auto-génère initiales si non fournies (1res lettres de chaque mot)
  - Slug auto avec suffixe en cas de collision (cf. Slugger::unique)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:47:42 +02:00
Arnaud dcf99f011a feat(admin): création de catégorie via dashboard
Nouvelle page /admin/categories/new, jumeau de l'écran d'édition :
  - Form complet : name, slug (auto), description, color_hex, icon,
    dimension, sort_order
  - Preview live identique à l'éditeur (top border + folder tinté + badge)
  - Slug auto-généré depuis le nom (normalisation NFD + lowercase + dashes)
    tant que l'utilisateur n'a pas touché manuellement au champ. Flag
    slugDirty mis à true sur premier @input du slug -> on arrête l'auto.
  - Validation color_hex /^#RRGGBB$/ côté client + normalisation upper-case
  - Body construit en omettant les champs vides -> l'API applique ses
    défauts (color_hex='#888888', dimension='transversal', sort_order=0)
  - Sur 201 : redirige vers /admin/categories/:slug/edit pour que l'admin
    voie tout de suite sa nouvelle catégorie et puisse ajuster
  - 422/409/401 gérés comme sur les autres écrans admin

UI
  - AdminCategoriesPage : CTA "Nouvelle catégorie" en haut à droite
  - Router : route nommée admin-category-new avec guard admin

Backend
  - Aucune nouvelle route : POST /api/categories existait déjà
  - Le contrôleur valide name (required, max 128), dimension enum, color
    hex format, slug auto-unique

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:45:25 +02:00
Arnaud dd7c3910f3 feat(admin): édition de catégorie via dashboard
Nouvelle page /admin/categories/:slug/edit, même pattern que members :
  - Preview live de la carte catégorie (top border colorée + folder icon
    tinté + badge "X projets" + label dimension)
  - Form complet : name, slug, description, color_hex, icon, dimension, sort_order
  - Color picker natif <input type="color"> + champ HEX texte synchronisés,
    validation /^#[0-9A-Fa-f]{6}$/ côté client
  - Diff PATCH (champs vides -> null), upper-case normalisé pour color_hex
  - Si le slug change -> router.replace vers la nouvelle URL
  - Bouton Supprimer en header désactivé si projects_count > 0 (FK RESTRICT)
  - 422 -> fieldErrors, 409 -> slug collision, 401 -> session expirée
  - projects_count récupéré direct depuis GET /api/categories/:slug
    (la réponse contient ce champ pour la route détail, cf. controller PHP)

UI
  - AdminCategoriesPage : nouveau lien "Éditer" par ligne du tableau
  - Router : route nommée admin-category-edit avec guard admin

Backend
  - Aucune nouvelle route : PATCH /api/categories/:slug existait déjà
  - Validation côté serveur : in dimension enum, colorHex format,
    integer sort_order >= 0, slug auto-unique avec suffixe -2/-3...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:43:03 +02:00
Arnaud cb37b461c5 feat(admin): édition de membre via dashboard
Nouvelle page /admin/members/:slug/edit avec :
  - Preview avatar live (initiales/URL mis à jour à la saisie)
  - Form complet : display_name, slug, initials, kind, bio, email, website, avatar
  - Envoie un diff au PATCH (champs vides -> null, pas chaîne vide)
  - Si le slug change, redirige automatiquement vers la nouvelle URL
  - Encart "Projets attachés" en lecture (avec lien vers chaque fiche)
    pour rappeler l'impact d'une suppression ou d'un changement de slug
  - Bouton Supprimer en header avec confirm qui mentionne le nb de projets
  - Banner vert "Modifications enregistrées" sur succès
  - Gestion erreurs : 422 -> fieldErrors, 401, 409 (slug collision), réseau

Backend
  - Aucune nouvelle route : PATCH /api/members/:slug existait déjà
  - Le contrôleur gère déjà slug auto-unique, initials, kind enum, etc.

UI
  - AdminMembersPage : nouveau lien "Éditer" par ligne, à côté de Supprimer
  - Router : route nommée admin-member-edit avec guard admin

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:39:33 +02:00
Arnaud 689dfeee14 feat(team): édition inline du rôle + retrait + sélection de membre existant
Backend
  - ProjectMembersController::update : nouveau PATCH /api/projects/:slug/members/:member_slug
    pour changer le rôle (admin/contributor/observer). 404 si pivot absent,
    no-op idempotent si déjà même rôle, 422 si rôle invalide. Route câblée
    dans public/index.php.

Frontend
  - JoinProjectModal : 2 onglets (Membre existant / Nouveau membre)
      * "existant" : <select> de l'annuaire filtré sur excludeSlugs (membres
        déjà sur le projet), preview avatar+bio du membre sélectionné
      * "nouveau" : flow d'origine (créer le membre puis l'attacher)
      * sélecteur de rôle visible dans les 2 modes
      * onJoined remonte un Member complet (au lieu de {id,slug,name})
        -> ProjectDetailPage peut pousser un ProjectMemberEntry avec ses
           vraies initials/avatar/kind dans la liste optimiste

  - ProjectDetailPage panneau Équipe (mode admin uniquement) :
      * Rôle inline éditable via <select> stylé minimal -> PATCH optimiste
        avec rollback en cas d'échec
      * Bouton retirer-membre par ligne (DELETE) avec confirm
      * Le badge "Vous" / "Admin connecté" ne change pas

Code-review (high effort, --fix appliqué) :
  - loadDirectory affiche maintenant une vraie erreur réseau au lieu de
    silencieusement présenter "tous les membres déjà sur ce projet"
  - loadDirectory est lazy : on ne fetche l'annuaire que si l'onglet
    "existant" est actif (économise 1 requête si admin saute direct à "nouveau")
  - Aperçu du membre sélectionné : computed `selectedMember` au lieu de
    <template v-for><template v-if>
  - Bouton retirer-membre : opacity-60 sur mobile (sm: et plus garde le
    behavior hover-to-reveal) -> reste atteignable sur touch
  - + label `aria-label` pour l'accessibilité

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:31:34 +02:00
Arnaud adb2bd3fc1 feat: galerie d'images projet (upload disque) + dashboard /admin
==== Galerie d'images ====

Backend
  - Migration 003 : table project_images (FK CASCADE sur projects)
  - ImagesController : CRUD complet, deux modes de création
      * URL externe via JSON body
      * Upload disque via multipart/form-data avec :
          - validation MIME RÉELLE via finfo (jpeg/png/webp/gif)
          - taille max 5 Mo, vérifiée explicitement (au-delà du php.ini)
          - vérification UPLOAD_ERR_* avec messages clairs
          - stockage dans public/uploads/projects/:slug/<hash>.<ext>
          - dimensions image extraites via getimagesize (best-effort)
          - row DB avec mime_type / size_bytes / width / height
  - Request : ajout isMultipart(), formField(), file()
  - public/index.php : 3 routes (index public, store/update/destroy admin)

Proxy
  - caddy.conf : /uploads/* ajouté au reverse_proxy comme /api/*
  - vite.config.ts : pareil pour le dev local

Frontend
  - Type ProjectImage (api.ts) avec url + storage_path + dimensions
  - uploadFile() helper dans client.ts (FormData + Bearer)
  - ImageGallery.vue : grille thumbnails + lightbox plein-écran avec
    nav clavier (← → Esc), bouton supprimer par image en mode admin
  - ImageUploader.vue : drag-drop + file picker, multi-fichiers en série,
    validation taille côté client, légende commune optionnelle
  - ProjectDetailPage : nouvel onglet Images entre Roadmap et Discussions

==== Dashboard /admin ====

5 nouvelles pages sous /admin :
  - /admin                           : KPI + répartition catégorie + flux activité
  - /admin/projects                  : table CRUD (recherche + filtres status/categorie),
                                       actions Éditer / Archiver / Supprimer par ligne
  - /admin/projects/:slug/edit       : form complet d'édition (envoie un diff au PATCH)
  - /admin/members                   : annuaire avec recherche + Supprimer
  - /admin/categories                : table avec compte de projets ;
                                       Supprimer désactivé si la catégorie est utilisée
  - AdminSubNav.vue                  : 4 onglets en haut de chaque page admin

Garde d'auth :
  - beforeEnter sur les routes /admin/* (sauf /admin/login)
  - Redirige vers /admin/login?next=<url> si pas de token
  - Après login, retour automatique vers next= (sinon /admin)

Sidebar :
  - Nouvelle section "Administration" visible UNIQUEMENT quand le token
    est en localStorage, avec un lien vers le dashboard

Vérifié localement
  - Build vue-tsc : 91 modules, OK
  - Upload disque : fichier PNG stocké dans public/uploads/, accessible via /uploads/
  - GET /api/projects/nos-ecoles/images : 2 entrées renvoyées (1 URL + 1 disque)
  - Toutes les routes /admin/* répondent 200 via le proxy Vite

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:07:59 +02:00
Arnaud 034cb8ba99 feat(donut): noms de catégories sur l'anneau intermédiaire (ceiling ring)
Les noms étaient tout à l'extérieur (R=275), au-delà des limites planétaires.
L'utilisateur veut "sur le cercle intermédiaire entre les camemberts des
catégories et les limites planétaires" — c'est-à-dire SUR l'anneau gris du
plafond écologique qui sépare la zone sûre (où vivent les wedges colorés)
des limites planétaires.

  - R_CAT_LABEL : 275 → 212 (milieu de l'anneau gris 200..225)
  - viewBox : retour à 560×560 (plus besoin d'étendre à R=275)
  - Cream background : 295 → 270 (idem)
  - Font-size : 11 → 10 (plus discret sur ring)
  - paint-order + stroke #FAFAF6 sur le texte : "halo" cream autour des
    lettres pour rester lisibles malgré le pattern strié du ring gris.
    Plus simple qu'un mask et fonctionne dans tous les browsers modernes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:42:25 +02:00
Arnaud 14c8d87ad8 feat(donut): noms de catégories en arc tout à l'extérieur du donut
Les libellés des 6 catégories thématiques (Data, Démocratie, Habitat,
Territoire, Écologie, Éducation) étaient posés HORIZONTAUX au milieu
de leur wedge dans la zone sûre. Ils chevauchaient les points-projets
quand on en avait beaucoup et compliquaient la lecture.

Déplacement à R=275, ARC autour du donut, dans la couleur de la catégorie :

  - Nouvelle constante R_CAT_LABEL (la plus extérieure : > planétaires à 252)
  - viewBox 600x600 (vs 560x560) pour caser la nouvelle frontière
  - Fond cream étendu à R=295 pour soutenir les labels jusqu'au bord
  - Font-size 11 + weight 700 + lettre-espacement 0.5px pour lisibilité
  - Réutilisation du helper arcLabelPaths (halfSpan 28° = quasi-pleine
    largeur du secteur de 60°)
  - Couleur récupérée par index depuis la liste categories réactive

Suppression du rendu in-wedge des labels catégorie : redondance avec les
arcs extérieurs + meilleure lisibilité des points-projets dans la zone sûre.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:38:41 +02:00
Arnaud 8fc8c21b7e feat(donut): libellés du plancher social sur 2 lignes (arc multi-niveaux)
Beaucoup de dimensions Raworth dépassent l'arc disponible à R=88 :
« Revenu & travail », « Liens sociaux », « Égalité de genre »,
« Équité sociale », « Voix politique », « Paix & justice ». Posées en
une seule ligne, le texte débordait ou était illisible.

Solution : permettre 2 lignes par libellé, chaque ligne ayant son propre
arc à un rayon décalé. Du coup ces 6 dimensions sont séparées en 2 mots
empilés radialement. Les 6 autres (Alimentation, Santé, Éducation, Eau,
Énergie, Logement) restent sur une seule ligne.

Refactor :
  - Type `Label = readonly string[]` (1..N lignes)
  - PLANETARY_BOUNDARIES et SOCIAL_FOUNDATIONS unifiés au format tableau
    de tableaux (planétaires restent toutes à 1 ligne, sociales mixtes)
  - arcLabelPaths() gère le multi-lignes : une ArcLine par ligne, avec
    décalage radial de ±lineSpacing/2
  - lineSpacing = 9u (légèrement > font-size 8 pour éviter le chevauchement)
  - Inversion du décalage radial selon la moitié du donut pour que la
    ligne "primaire" reste toujours du côté lecteur (haut de l'écran) :
        top : ligne 0 = R+offset (extérieur)
        bot : ligne 0 = R-offset (intérieur)
  - Aplatissement via flatMap pour rendre comme une flat-list dans le
    SVG (pas de `<template>` imbriqué)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:34:20 +02:00
Arnaud 1d4910a940 feat(donut): agrandissement de l'anneau intérieur pour la lisibilité
Le plancher social et ses 12 dimensions étaient à l'étroit autour de
R=67 (anneau intérieur 78..100). Maintenant :

  - R_SOCIAL_LABEL passe de 67 à 88
    -> circonférence du cercle où sont posés les labels = 2π·88 ≈ 553u
    -> chaque label dispose de ~46u d'arc (contre ~35 avant)
  - R_FLOOR_INNER passe de 78 à 102
  - R_FLOOR_OUTER passe de 100 à 125
    -> l'anneau orange du plancher social est visuellement plus présent
  - R_CENTER passe de 50 à 55 (pour ne pas écraser le "LE DONUT")
  - Font-size des labels sociaux : 6 → 8 (plus lisible avec l'arc élargi)

L'espace sûr (R_FLOOR_OUTER..R_CEILING_INNER) passe de 100u à 75u : les
trois bandes de positionnement des projets (social / transversal /
écologique) sont recalibrées en conséquence sans chevauchement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:29:17 +02:00
Arnaud 986241dc85 feat(donut): texte en arc de cercle + 12 dimensions du plancher social
Limites planétaires + dimensions sociales : labels qui épousent vraiment la
courbure de l'anneau au lieu d'être posés en ligne droite tangente. Implémenté
avec SVG <textPath> + un <path> caché par label.

Petit piège : pour qu'un label en bas du donut reste lisible (pas écrit à
l'envers ou de droite à gauche), il faut INVERSER la direction du path selon
la moitié.

  Helper arcLabelPaths() :
    - Moitié haute (angles ~0°)   : path va de θ-δ vers θ+δ, sweep=1
    - Moitié basse (angles ~180°) : path va de θ+δ vers θ-δ, sweep=0
  → la direction du chemin au point central du label est toujours "horizontale
    à gauche-droite" du point de vue d'un lecteur fixe.

Ajouts :
  - 12 dimensions du plancher social Raworth (Alimentation, Santé, Éducation,
    Revenu & travail, Eau, Énergie, Liens sociaux, Logement, Égalité de genre,
    Équité sociale, Voix politique, Paix & justice) placées en arc entre le
    centre et l'anneau intérieur (rayon 67)
  - R_CENTER passé de 56 à 50 pour libérer 6 unités au profit des labels
    sociaux sans toucher au "LE DONUT / X projets" au centre
  - Suppression des textes centrés "PLAFOND ÉCOLOGIQUE" et "PLANCHER SOCIAL"
    sur les anneaux (redondants avec les labels qui les détaillent)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:26:00 +02:00
Arnaud 362b3c5712 fix(sidebar): liens de nav ne naviguaient pas (RouterLink v-slot sans custom)
Le pattern <RouterLink v-slot="..."><a @click.prevent>...</a></RouterLink>
sans le prop `custom` rend DEUX <a> imbriqués : celui de RouterLink (qui gère
la navigation) + celui du slot. Notre @click.prevent sur le <a> interne
bloquait le clic avant qu'il remonte au <a> externe. Résultat : clic mort.

Fix : ajout du prop `custom` (RouterLink ne rend plus son <a>) +
@click="navigate" (fonction fournie par le v-slot qui fait le router.push
en gérant les modifier keys, le ctrl-clic, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:14:09 +02:00
Arnaud 8dd9cffaed fix(donut): hover anti-jitter + couleurs catégories + limites planétaires
1. Jitter au survol (bug visible) :
   `transform: scale(1.5)` en SVG prend pour origine le (0,0) du viewBox,
   pas le centre du <circle>. Le scale "déplaçait" donc le cercle vers le centre
   de la viewport → la souris n'était plus dessus → mouseleave → retour →
   mouseenter → re-déplacé. Boucle infinie de tremblement.

   Fix : classe `.dot-circle` avec
       transform-box: fill-box;
       transform-origin: center;
   Le scale prend désormais pour pivot le centre de la forme. Stable.

2. Couleurs des secteurs thématiques (par la maquette d'origine) :
   Réintroduction des wedges colorés dans la zone sûre, opacité 10 % sur
   le fill + 20 % sur le stroke. Reste sobre, garde la lisibilité.

3. Limites planétaires (de Kate Raworth) :
   Les 9 limites planétaires (Changement climatique, Acidification des
   océans, Biodiversité, Eau douce, Cycles N&P, Pollution chimique,
   Pollution de l'air, Couche d'ozone, Usage des sols) sont placées en
   étiquettes tangentes autour de l'anneau extérieur, avec flip 180° en
   moitié basse pour rester lisibles.

Cosmétique :
   - viewBox 560×560 (au lieu de 520) pour caser les labels extérieurs
   - Labels catégorie déplacés AU MILIEU de leur wedge (sur la zone sûre)
     plutôt qu'à l'extérieur (où ils chevauchaient les limites planétaires)
   - Séparateurs radiaux passés en blanc pour mieux contraster avec les
     wedges colorés

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:10:37 +02:00
Arnaud 80193e5bc0 feat(viz): refonte du donut + jeu d'essai démo de 20 projets
Donut (DonutPage.vue) — alignement avec la maquette "Capture 2026-05-28 205310" :
  - Suppression des secteurs en couleur vive : 6 secteurs séparés par des
    lignes radiales en pointillé gris léger
  - 2 anneaux gris texturés (pattern diagonal) : "Plafond écologique" (extérieur)
    et "Plancher social" (intérieur), avec leurs labels
  - Espace sûr cream entre les deux où vivent les projets
  - Positionnement des projets :
      angle  : secteur de la catégorie + jitter stable (hash slug)
      rayon  : bande définie par la dimension de la catégorie
               (social -> 108..140, ecologique -> 160..192, transversal -> 142..158)
               puis modulé par progress_percent (plus avancé = plus éloigné du centre)
  - Projets archivés rendus en POINTS CREUX (stroke only, sans fill)
  - Tooltip survol -> RouterLink cliquable vers la fiche projet
  - Légende droite réécrite avec les phrases exactes de la maquette
    ("L'anneau intérieur représente le plancher social, ..." etc.)
  - Liste des catégories par dimension avec compte de projets cliquables

Jeu d'essai (tests/seed-demo.sh) :
  - 10 actifs / 6 réalisés / 4 archivés (= 20 projets), réalistes Marseille
  - 10 membres (4 collectives/orgs, 4 personnes, 2 collectives + AMU)
  - 26 liens project_members, 27 étapes de roadmap (mix done/à faire)
  - 6 discussions, 8 ressources, 8 events d'activité antérieurs
  - Lit DB_* depuis .env, force --default-character-set=utf8mb4
  - TRUNCATE+INSERT idempotent (la table categories est préservée)
  - Hors /migrations/ pour ne PAS être appliqué automatiquement par apply.sh

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:17:47 +02:00
Arnaud 7fb22bcb29 feat(frontend): SPA Vue 3 + Vite + Tailwind v4 consommant l'API
Nouveau dossier /frontend avec un SPA complet qui couvre les écrans de la
maquette (cf. docs/Capture*.png) : public + admin token. Backend inchangé.

Stack
  - Vue 3.5 + Vite 8 + TypeScript 6 + Tailwind v4 (via @tailwindcss/vite)
  - Vue Router 4 en mode history, code-split par page (lazy import)
  - Aucune lib UI, aucun store (pas de Pinia) — état local + fetch direct
  - Bundle initial 50 kB gzip, total dist 240 kB

Pages
  - /                 redirige vers /donut
  - /dashboard        4 KPIs + mini donut + flux activité (via /api/stats + /api/activity)
  - /donut            viz SVG custom : 6 secteurs catégories, projets en points
                      colorés, anneaux plancher social / plafond écologique,
                      tooltip + click vers la fiche projet
  - /projects         grille de cartes paginée + filtres (catégorie, statut, recherche
                      avec debounce, query string comme source de vérité)
  - /projects/new     formulaire complet de création (token admin requis)
  - /projects/:slug   fiche : header méta + tabs Roadmap/Discussions/Ressources
                      + panneau droit Progression + Équipe, modal "Rejoindre"
  - /archives         projets status=archived
  - /categories       6 cartes coloured-top-border + count via /api/stats
  - /admin/login      saisie du token Bearer, validé via POST /api/members (vide)
  - 404               fallback

Structure
  - src/api/client.ts        : wrapper fetch typé + token Bearer auto + ApiError
  - src/composables/useAuth  : token localStorage + reactivité Vue
  - src/types/api.ts         : types miroirs des réponses PHP
  - src/utils/formatDate.ts  : ISO/MySQL/DATE -> JJ mois AAAA, relatif, etc.
  - src/components/          : Avatar, ProjectCard, StatusBadge, CategoryBadge,
                               ProgressBar, LoadingState, ErrorState, JoinProjectModal
  - src/layouts/AppLayout    : sidebar nav + main scrollable
  - src/components/AppSidebar: logo donut SVG, CTA, nav, statut admin en bas
  - src/pages/               : 10 pages ci-dessus

Intégration & déploiement
  - vite.config.ts proxy /api/* vers http://donut-gestprojet.test (Laragon)
  - caddy.conf prod : /api/* -> reverse_proxy PHP, le reste -> static dist/
    avec fallback /index.html (history mode), cache long sur /assets/*
  - DEPLOY.md mis à jour avec build local + rsync dist/ vers
    /srv/donut/web/infolab-front/dist
  - README.md mis à jour avec section "Frontend (SPA Vue 3)"

Build & types
  - npm run build : vue-tsc strict (TS 6 + erasableSyntaxOnly) -> OK
  - paths alias @/* configuré dans tsconfig.app.json

Vérifié localement
  - npm run dev sur :5173, proxy /api -> Apache Laragon -> PHP
  - Toutes les routes répondent 200 (curl http://localhost:5173/...)
  - Routes API consommées correctement (categories, stats, projects, ...)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:40:39 +02:00