Commit Graph

12 Commits

Author SHA1 Message Date
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