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>
This commit is contained in:
2026-05-28 21:17:47 +02:00
parent 80f96efb56
commit 80193e5bc0
3 changed files with 610 additions and 138 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

+183 -138
View File
@@ -1,20 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* Visualisation Donut — SVG custom. * Visualisation Donut — SVG custom, version maquette « Capture d'écran 2026-05-28 205310 ».
* *
* Modèle Donut Kate Raworth adapté : * Anneaux concentriques sobres (pas de couleurs vives sur les secteurs) :
* - Anneau extérieur (vert pâle) = "Plafond écologique" qu'on ne doit pas dépasser * - Anneau extérieur gris = "Plafond écologique" (limites planétaires)
* - Anneau intérieur (orange pâle) = "Plancher social" qu'on ne doit pas franchir vers le bas * - Espace sûr cream = zone où l'on veut que vivent les projets
* - Zone centrale entre les deux = "espace sûr" où vivent les projets citoyens * - Anneau intérieur gris = "Plancher social" (besoins fondamentaux)
* *
* 6 secteurs colorés (60° chacun) = les 6 catégories thématiques. * Positionnement des projets :
* Chaque projet = 1 point coloré (par catégorie) placé dans son secteur, * - Angle : par catégorie (6 secteurs de 60°), jitter angulaire
* à un rayon qui varie légèrement (hash stable de l'id) pour éviter les chevauchements. * - Rayon : déterminé par la DIMENSION
* Survol → tooltip avec titre du projet et lien vers sa fiche. * social -> bande intérieure (proche du plancher)
* ecologique -> bande extérieure (proche du plafond)
* transversal -> milieu de la zone sûre
* puis modulé par le PROGRESS (plus progressé -> plus éloigné du centre,
* dans les limites de la bande de la dimension)
* - Projets archivés rendus en POINTS CREUX (stroke only).
*/ */
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { RouterLink, useRouter } from 'vue-router'
import { api, ApiError } from '@/api/client' import { api, ApiError } from '@/api/client'
import type { Category, Dimension, Paginated, ProjectListRow, Wrapped } from '@/types/api' import type { Category, Dimension, Paginated, ProjectListRow, Wrapped } from '@/types/api'
import LoadingState from '@/components/LoadingState.vue' import LoadingState from '@/components/LoadingState.vue'
@@ -31,6 +36,8 @@ async function load() {
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
// /api/projects sans filtre renvoie déjà tous les non soft-deletés
// (active + completed + archived) — utile pour les points creux des archives.
const [cats, projs] = await Promise.all([ const [cats, projs] = await Promise.all([
api.get<Wrapped<Category>>('/api/categories'), api.get<Wrapped<Category>>('/api/categories'),
api.get<Paginated<ProjectListRow>>('/api/projects', { query: { per_page: 100 } }), api.get<Paginated<ProjectListRow>>('/api/projects', { query: { per_page: 100 } }),
@@ -45,14 +52,14 @@ async function load() {
} }
onMounted(load) onMounted(load)
// --- Géométrie (rayons en unités du viewBox -250..250) --- // --- Géométrie (en unités du viewBox -260..260) ---
const R_PROJECT_OUTER = 200 const R_CEILING_OUTER = 230
const R_PROJECT_INNER = 110 const R_CEILING_INNER = 200
const R_FLOOR_OUTER = 95 const R_FLOOR_OUTER = 100
const R_FLOOR_INNER = 70 const R_FLOOR_INNER = 78
const R_CENTER = 60 const R_CENTER = 56
/** degrad avec offset -90° pour que 0° soit en haut (12 h). */ /** degrad avec offset -90° (0° en haut). */
function rad(deg: number): number { function rad(deg: number): number {
return (deg - 90) * Math.PI / 180 return (deg - 90) * Math.PI / 180
} }
@@ -60,45 +67,34 @@ function pt(angleDeg: number, r: number): [number, number] {
const a = rad(angleDeg) const a = rad(angleDeg)
return [Math.cos(a) * r, Math.sin(a) * r] return [Math.cos(a) * r, Math.sin(a) * r]
} }
function wedgePath(startDeg: number, endDeg: number, rInner: number, rOuter: number): string {
const [x1, y1] = pt(startDeg, rOuter) /** Hash 32-bit basique pour stabiliser le jitter par projet. */
const [x2, y2] = pt(endDeg, rOuter) function strHash(s: string): number {
const [x3, y3] = pt(endDeg, rInner) let h = 0
const [x4, y4] = pt(startDeg, rInner) for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0
const largeArc = endDeg - startDeg > 180 ? 1 : 0 return Math.abs(h)
return [
`M ${x1.toFixed(2)} ${y1.toFixed(2)}`,
`A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`,
`L ${x3.toFixed(2)} ${y3.toFixed(2)}`,
`A ${rInner} ${rInner} 0 ${largeArc} 0 ${x4.toFixed(2)} ${y4.toFixed(2)}`,
'Z',
].join(' ')
} }
interface WedgeView { /** Lignes radiales de séparation des secteurs (entre plancher et plafond). */
cat: Category const wedgeDividers = computed(() => {
startDeg: number const n = categories.value.length || 6
endDeg: number const step = 360 / n
centerDeg: number return Array.from({ length: n }, (_, i) => {
path: string const angle = i * step
labelPos: [number, number] const [x1, y1] = pt(angle, R_FLOOR_OUTER)
} const [x2, y2] = pt(angle, R_CEILING_INNER)
return { x1, y1, x2, y2, key: i }
})
})
const wedges = computed<WedgeView[]>(() => { /** Position du label catégorie (à l'extérieur du plafond). */
const categoryLabels = computed(() => {
const n = categories.value.length || 6 const n = categories.value.length || 6
const step = 360 / n const step = 360 / n
return categories.value.map((cat, i) => { return categories.value.map((cat, i) => {
const startDeg = i * step const center = i * step + step / 2
const endDeg = (i + 1) * step const [x, y] = pt(center, R_CEILING_OUTER + 18)
const centerDeg = (startDeg + endDeg) / 2 return { cat, x, y }
return {
cat,
startDeg,
endDeg,
centerDeg,
path: wedgePath(startDeg, endDeg, R_PROJECT_INNER, R_PROJECT_OUTER),
labelPos: pt(centerDeg, (R_PROJECT_INNER + R_PROJECT_OUTER) / 2 + 25),
}
}) })
}) })
@@ -106,51 +102,80 @@ interface DotView {
project: ProjectListRow project: ProjectListRow
x: number x: number
y: number y: number
r: number isHollow: boolean
color: string color: string
} }
/** Hash 32-bit basique sur une chaîne (stabilité de positionnement entre rendus). */
function strHash(s: string): number {
let h = 0
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0
return Math.abs(h)
}
const dots = computed<DotView[]>(() => { const dots = computed<DotView[]>(() => {
const byCat = new Map<number, ProjectListRow[]>() if (categories.value.length === 0) return []
for (const p of projects.value) { // index catégorie par slug -> position du secteur (0..5)
const arr = byCat.get(p.category.id) ?? [] const catIndex = new Map<string, number>()
arr.push(p) categories.value.forEach((c, i) => catIndex.set(c.slug, i))
byCat.set(p.category.id, arr) const n = categories.value.length
const step = 360 / n
// Bands selon la dimension (intérieur/extérieur/milieu).
const BANDS: Record<Dimension, [number, number]> = {
social: [R_FLOOR_OUTER + 8, R_FLOOR_OUTER + 40], // 108..140
ecologique: [R_CEILING_INNER - 40, R_CEILING_INNER - 8], // 160..192
transversal: [R_FLOOR_OUTER + 42, R_CEILING_INNER - 42], // 142..158
} }
// Groupage par catégorie pour répartir angulairement.
const byCat = new Map<string, ProjectListRow[]>()
for (const p of projects.value) {
const list = byCat.get(p.category.slug) ?? []
list.push(p)
byCat.set(p.category.slug, list)
}
const out: DotView[] = [] const out: DotView[] = []
for (const w of wedges.value) { for (const [slug, list] of byCat.entries()) {
const ps = byCat.get(w.cat.id) ?? [] const i = catIndex.get(slug) ?? 0
const span = w.endDeg - w.startDeg const wedgeStart = i * step + 3
ps.forEach((p, idx) => { const wedgeEnd = (i + 1) * step - 3
// Répartit angulairement les projets sur la largeur du secteur (avec marge), const span = wedgeEnd - wedgeStart
// et ajoute un petit jitter de rayon pour ne pas tous les aligner.
const t = (idx + 1) / (ps.length + 1) list.forEach((p, idx) => {
const angleDeg = w.startDeg + span * t // Angle : répartition + jitter stable
const jitter = ((strHash(p.slug) % 100) / 100 - 0.5) * (R_PROJECT_OUTER - R_PROJECT_INNER - 30) const t = (idx + 1) / (list.length + 1)
const r = (R_PROJECT_INNER + R_PROJECT_OUTER) / 2 + jitter const angleJitter = ((strHash(p.slug + 'a') % 100) / 100 - 0.5) * Math.min(6, span / Math.max(2, list.length))
const [x, y] = pt(angleDeg, r) const angle = wedgeStart + span * t + angleJitter
// Diamètre du point en fonction de la progression (un peu plus gros si avancé).
const radius = 4 + (p.progress_percent / 100) * 4 // Rayon : bande de dimension + progress
out.push({ project: p, x, y, r: radius, color: p.category.color_hex }) const [rMin, rMax] = BANDS[p.category.dimension] ?? BANDS.transversal
const progressR = rMin + (rMax - rMin) * (p.progress_percent / 100)
const rJitter = ((strHash(p.slug + 'r') % 100) / 100 - 0.5) * 4
const r = Math.max(rMin, Math.min(rMax, progressR + rJitter))
const [x, y] = pt(angle, r)
out.push({
project: p,
x, y,
isHollow: p.status === 'archived',
color: p.category.color_hex,
})
}) })
} }
return out return out
}) })
// Légende groupée par dimension // Légende — catégories regroupées par dimension avec leur count.
const projectsByCategorySlug = computed(() => {
const m: Record<string, number> = {}
for (const p of projects.value) {
m[p.category.slug] = (m[p.category.slug] ?? 0) + 1
}
return m
})
const DIMENSION_ORDER: Dimension[] = ['social', 'ecologique', 'transversal'] const DIMENSION_ORDER: Dimension[] = ['social', 'ecologique', 'transversal']
const DIMENSION_META: Record<Dimension, { label: string; color: string }> = { const DIMENSION_META: Record<Dimension, { label: string; color: string }> = {
social: { label: 'Plancher social', color: '#F97316' }, social: { label: 'Plancher social', color: '#F97316' },
ecologique: { label: 'Plafond écologique', color: '#22C55E' }, ecologique: { label: 'Plafond écologique', color: '#22C55E' },
transversal: { label: 'Transversal', color: '#6B7280' }, transversal: { label: 'Transversal', color: '#6B7280' },
} }
const categoriesByDimension = computed(() => { const categoriesByDimension = computed(() => {
const map: Record<Dimension, Category[]> = { social: [], ecologique: [], transversal: [] } const map: Record<Dimension, Category[]> = { social: [], ecologique: [], transversal: [] }
for (const c of categories.value) map[c.dimension].push(c) for (const c of categories.value) map[c.dimension].push(c)
@@ -168,7 +193,7 @@ function gotoProject(p: ProjectListRow) {
<h1 class="text-3xl font-semibold text-donut-text">Le Donut des Projets Citoyens</h1> <h1 class="text-3xl font-semibold text-donut-text">Le Donut des Projets Citoyens</h1>
<p class="text-donut-muted mt-2"> <p class="text-donut-muted mt-2">
Inspiré du modèle de l'économie du Donut de Kate Raworth. L'Infolab des projets citoyens Inspiré du modèle de l'économie du Donut de Kate Raworth. L'Infolab des projets citoyens
explore les initiatives qui poussent une vie marseillaise dans l'espace sûr : entre explore les initiatives qui poussent une vie marseillaise dans l'espace sûr et juste : entre
plancher social et plafond écologique. plancher social et plafond écologique.
</p> </p>
</header> </header>
@@ -176,103 +201,119 @@ function gotoProject(p: ProjectListRow) {
<LoadingState v-if="loading" class="mt-12 justify-center" /> <LoadingState v-if="loading" class="mt-12 justify-center" />
<ErrorState v-else-if="error" :message="error" :retry="load" class="mt-12" /> <ErrorState v-else-if="error" :message="error" :retry="load" class="mt-12" />
<div v-else class="mt-10 grid grid-cols-1 lg:grid-cols-3 gap-10 items-start"> <div v-else class="mt-10 grid grid-cols-1 lg:grid-cols-5 gap-10 items-start">
<!-- Donut SVG (2/3 sur grands écrans) --> <!-- Donut SVG (3/5 sur grands écrans) -->
<div class="lg:col-span-2 flex flex-col items-center"> <div class="lg:col-span-3 flex flex-col items-center">
<svg viewBox="-250 -250 500 500" class="w-full max-w-[560px] aspect-square"> <svg viewBox="-260 -260 520 520" class="w-full max-w-[600px] aspect-square">
<!-- Plafond écologique (anneau externe) --> <defs>
<circle r="240" fill="#22C55E14" /> <pattern id="ring-stripes" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
<circle r="215" fill="#FAFAF6" /> <rect width="6" height="6" fill="#E5E7EB" />
<text <line x1="0" y1="0" x2="0" y2="6" stroke="#D1D5DB" stroke-width="1.2" />
v-for="i in 1" :key="`ceil-${i}`" </pattern>
x="0" y="-228" text-anchor="middle" font-size="9" </defs>
fill="#22C55E" font-weight="600" style="letter-spacing:1px"
>PLAFOND ÉCOLOGIQUE</text>
<!-- Plancher social (anneau interne) --> <!-- 1) Anneau extérieur PLAFOND ÉCOLOGIQUE (gris) -->
<circle :r="R_FLOOR_OUTER" fill="#F9731614" /> <circle :r="R_CEILING_OUTER" fill="url(#ring-stripes)" />
<circle :r="R_CEILING_INNER" fill="#FAFAF6" />
<!-- 2) Anneau intérieur PLANCHER SOCIAL (gris) -->
<circle :r="R_FLOOR_OUTER" fill="url(#ring-stripes)" />
<circle :r="R_FLOOR_INNER" fill="#FAFAF6" /> <circle :r="R_FLOOR_INNER" fill="#FAFAF6" />
<!-- 6 secteurs catégories --> <!-- 3) Séparateurs radiaux entre secteurs (dotted) -->
<g> <g stroke="#D1D5DB" stroke-width="0.8" stroke-dasharray="2 3">
<path <line
v-for="w in wedges" :key="w.cat.id" v-for="d in wedgeDividers" :key="d.key"
:d="w.path" :x1="d.x1" :y1="d.y1" :x2="d.x2" :y2="d.y2"
:fill="w.cat.color_hex + '14'"
:stroke="w.cat.color_hex + '40'"
stroke-width="0.5"
/> />
</g> </g>
<!-- Étiquettes catégories à l'extérieur des secteurs --> <!-- 4) Étiquettes catégories autour du plafond -->
<g> <g>
<text <text
v-for="w in wedges" :key="`label-${w.cat.id}`" v-for="l in categoryLabels" :key="`label-${l.cat.id}`"
:x="w.labelPos[0]" :y="w.labelPos[1]" :x="l.x" :y="l.y"
text-anchor="middle" text-anchor="middle"
font-size="10" font-size="10"
:fill="w.cat.color_hex" :fill="l.cat.color_hex"
font-weight="600" font-weight="600"
>{{ w.cat.name }}</text> >{{ l.cat.name }}</text>
</g> </g>
<!-- Points projets --> <!-- 5) Labels des anneaux -->
<text x="0" :y="-(R_CEILING_OUTER + 4)" text-anchor="middle" font-size="9" fill="#22C55E" font-weight="700"
style="letter-spacing:1px">PLAFOND ÉCOLOGIQUE</text>
<text x="0" :y="-(R_FLOOR_INNER - 5)" text-anchor="middle" font-size="8" fill="#F97316" font-weight="700"
style="letter-spacing:1px">PLANCHER SOCIAL</text>
<!-- 6) Points-projets -->
<g> <g>
<circle <circle
v-for="d in dots" :key="d.project.id" v-for="d in dots" :key="d.project.id"
:cx="d.x" :cy="d.y" :r="d.r" :cx="d.x" :cy="d.y" r="6"
:fill="d.color" :fill="d.isHollow ? '#FAFAF6' : d.color"
stroke="white" stroke-width="1.5" :stroke="d.color"
:stroke-width="d.isHollow ? 2 : 1.5"
class="cursor-pointer transition-transform hover:scale-150 origin-center" class="cursor-pointer transition-transform hover:scale-150 origin-center"
:class="hoveredProject?.id === d.project.id ? 'scale-150' : ''"
@mouseenter="hoveredProject = d.project" @mouseenter="hoveredProject = d.project"
@mouseleave="hoveredProject = null" @mouseleave="hoveredProject = null"
@click="gotoProject(d.project)" @click="gotoProject(d.project)"
> >
<title>{{ d.project.title }}</title> <title>{{ d.project.title }} — {{ d.project.progress_percent }}%</title>
</circle> </circle>
</g> </g>
<!-- Centre --> <!-- 7) Centre -->
<circle :r="R_CENTER" fill="#FFFFFF" stroke="#E5E7EB" /> <circle :r="R_CENTER" fill="#FFFFFF" stroke="#E5E7EB" />
<text x="0" y="-4" text-anchor="middle" font-size="14" fill="#111827" font-weight="700" <text x="0" y="-4" text-anchor="middle" font-size="13" fill="#111827" font-weight="700"
style="letter-spacing:2px">LEDONUT</text> style="letter-spacing:2px">LE DONUT</text>
<text x="0" y="14" text-anchor="middle" font-size="9" fill="#6B7280"> <text x="0" y="12" text-anchor="middle" font-size="9" fill="#6B7280">
{{ projects.length }} projet{{ projects.length > 1 ? 's' : '' }} {{ projects.length }} projet{{ projects.length > 1 ? 's' : '' }}
</text> </text>
</svg> </svg>
<!-- Tooltip projet survolé --> <!-- Tooltip projet survolé -->
<div class="mt-4 h-12 flex items-center justify-center"> <div class="mt-2 h-12 flex items-center justify-center">
<transition name="fade"> <transition name="fade">
<div <RouterLink
v-if="hoveredProject" v-if="hoveredProject"
class="px-3 py-1.5 rounded-full bg-white border border-donut-border shadow-sm text-sm flex items-center gap-2" :to="{ name: 'project-detail', params: { slug: hoveredProject.slug } }"
class="px-3 py-1.5 rounded-full bg-white border border-donut-border shadow-sm text-sm flex items-center gap-2 hover:shadow-md transition-shadow"
> >
<span <span
class="w-2 h-2 rounded-full" class="w-2 h-2 rounded-full"
:style="{ background: hoveredProject.category.color_hex }" :style="{ background: hoveredProject.category.color_hex }"
/> />
<span class="font-medium">{{ hoveredProject.title }}</span> <span class="font-medium">{{ hoveredProject.title }}</span>
<span class="text-donut-muted text-xs"> {{ hoveredProject.progress_percent }}%</span> <span class="text-donut-muted text-xs">
</div> — {{ hoveredProject.progress_percent }}%
<template v-if="hoveredProject.status === 'archived'"> · archivé</template>
<template v-else-if="hoveredProject.status === 'completed'"> · réalisé</template>
</span>
</RouterLink>
<div v-else class="text-xs text-donut-muted"> <div v-else class="text-xs text-donut-muted">
Survolez un point pour voir le détail d'un projet Survolez un point pour voir le projet · cliquez pour ouvrir
</div> </div>
</transition> </transition>
</div> </div>
</div> </div>
<!-- Légende --> <!-- Légende -->
<aside class="space-y-6"> <aside class="lg:col-span-2 space-y-6">
<div class="rounded-xl border border-donut-border bg-donut-surface p-5"> <div class="rounded-xl border border-donut-border bg-donut-surface p-5">
<h2 class="text-base font-semibold text-donut-text">Comment lire le donut ?</h2> <h2 class="text-base font-semibold text-donut-text">Comment lire ce donut ?</h2>
<p class="text-sm text-donut-muted mt-2 leading-relaxed"> <ul class="mt-3 space-y-2.5 text-sm text-donut-muted leading-relaxed">
Chaque <strong class="text-donut-text">point</strong> est un projet citoyen, placé dans son <li>L'<strong class="text-donut-text">anneau intérieur</strong> représente le
secteur de catégorie. L'anneau extérieur représente le <span class="font-medium" style="color:#F97316">plancher social</span>, qui définit les besoins fondamentaux.</li>
<span class="font-medium" style="color:#22C55E">plafond écologique</span> et l'anneau <li>L'<strong class="text-donut-text">anneau extérieur</strong> représente le
intérieur le <span class="font-medium" style="color:#F97316">plancher social</span>. <span class="font-medium" style="color:#22C55E">plafond écologique</span>, qui définit les limites planétaires.</li>
L'objectif : que tous les projets contribuent à rester dans l'espace sûr entre les deux. <li>L'espace entre les deux est l'<strong class="text-donut-text">espace sûr et juste</strong>.</li>
</p> <li>Les <strong class="text-donut-text">projets sociaux</strong> sont positionnés vers l'intérieur.</li>
<li>Les <strong class="text-donut-text">projets écologiques</strong> sont positionnés vers l'extérieur.</li>
<li>Plus un point est <strong class="text-donut-text">éloigné du centre</strong>, plus le projet est progressé.</li>
<li>Les <strong class="text-donut-text">points creux</strong> sont les projets historiques.</li>
</ul>
</div> </div>
<div <div
@@ -281,7 +322,7 @@ function gotoProject(p: ProjectListRow) {
class="rounded-xl border border-donut-border bg-donut-surface p-5" class="rounded-xl border border-donut-border bg-donut-surface p-5"
> >
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
<span class="w-2 h-2 rounded-full" :style="{ background: DIMENSION_META[dim].color }" /> <span class="w-2.5 h-2.5 rounded-full" :style="{ background: DIMENSION_META[dim].color }" />
<h3 class="text-xs font-semibold uppercase tracking-wider" :style="{ color: DIMENSION_META[dim].color }"> <h3 class="text-xs font-semibold uppercase tracking-wider" :style="{ color: DIMENSION_META[dim].color }">
{{ DIMENSION_META[dim].label }} {{ DIMENSION_META[dim].label }}
</h3> </h3>
@@ -290,14 +331,18 @@ function gotoProject(p: ProjectListRow) {
<li <li
v-for="c in categoriesByDimension[dim]" v-for="c in categoriesByDimension[dim]"
:key="c.id" :key="c.id"
class="flex items-center gap-2 text-sm"
> >
<span class="w-2.5 h-2.5 rounded-full shrink-0" :style="{ background: c.color_hex }" />
<RouterLink <RouterLink
:to="{ name: 'projects', query: { category: c.slug } }" :to="{ name: 'projects', query: { category: c.slug } }"
class="text-donut-text hover:underline" class="flex items-center justify-between gap-2 text-sm group"
> >
{{ c.name }} <span class="flex items-center gap-2 min-w-0">
<span class="w-2.5 h-2.5 rounded-full shrink-0" :style="{ background: c.color_hex }" />
<span class="text-donut-text truncate group-hover:underline">{{ c.name }}</span>
</span>
<span class="text-donut-muted tabular-nums shrink-0">
{{ projectsByCategorySlug[c.slug] ?? 0 }}
</span>
</RouterLink> </RouterLink>
</li> </li>
</ul> </ul>
+427
View File
@@ -0,0 +1,427 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# tests/seed-demo.sh
#
# Populates the local infolab DB with a rich demo dataset :
# - 10 membres (person / collective / org)
# - 20 projets répartis sur les 6 catégories, mix actif/completed/archived
# - 30+ étapes de roadmap (certaines cochées)
# - 10+ discussions & ressources
#
# Sources DB_HOST/DB_USER/DB_PASS/DB_NAME depuis ../.env (à défaut, valeurs Laragon).
# Force --default-character-set=utf8mb4 (cf. README).
#
# Idempotent : TRUNCATE tout sauf categories, puis insert.
# Usage : ./tests/seed-demo.sh
# -----------------------------------------------------------------------------
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$DIR/../.env"
# Defaults Laragon dev
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_USER="infolab"
DB_PASS="dev-pass-local-2026"
DB_NAME="infolab"
if [[ -f "$ENV_FILE" ]]; then
while IFS='=' read -r key val; do
case "$key" in
DB_HOST) DB_HOST="$val" ;;
DB_PORT) DB_PORT="$val" ;;
DB_USER) DB_USER="$val" ;;
DB_PASS) DB_PASS="$val" ;;
DB_NAME) DB_NAME="$val" ;;
esac
done < <(grep -E '^(DB_[A-Z]+)=' "$ENV_FILE" | sed 's/\r$//')
fi
echo ">> Seed demo @ ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
mysql --default-character-set=utf8mb4 \
-h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" 2>&1 <<'SQL' | grep -v -E "^mysql:|password on the command line"
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- Wipe (on conserve la table categories)
TRUNCATE TABLE activity_log;
TRUNCATE TABLE project_members;
TRUNCATE TABLE discussions;
TRUNCATE TABLE resources;
TRUNCATE TABLE roadmap_steps;
TRUNCATE TABLE projects;
TRUNCATE TABLE members;
SET FOREIGN_KEY_CHECKS = 1;
-- ============================================================================
-- MEMBRES
-- ============================================================================
INSERT INTO members (slug, display_name, initials, kind, bio, website_url, contact_email) VALUES
('collectif-ecoles-marseille', 'Collectif des écoles de Marseille', 'CEM', 'collective',
'Réseau de parents d''élèves, enseignants et habitants mobilisés pour l''école publique marseillaise.',
'https://collectif-ecoles-marseille.fr', NULL),
('atd-quart-monde-13', 'ATD Quart Monde 13', 'ATD', 'org',
'Mouvement de lutte contre la grande pauvreté présent dans les quartiers populaires de Marseille.',
'https://www.atd-quartmonde.fr', NULL),
('wildproject', 'Wildproject éditions', 'WP', 'org',
'Maison d''édition marseillaise de référence sur la pensée écologique et la philosophie de la nature.',
'https://wildproject.org', NULL),
('tela-botanica-paca', 'Tela Botanica PACA', 'TB', 'org',
'Réseau de la botanique francophone, antenne régionale active sur la biodiversité méditerranéenne.',
'https://www.tela-botanica.org', NULL),
('camille-dupont', 'Camille Dupont', 'CD', 'person',
'Chercheuse en sciences politiques, intéressée par la démocratie participative locale.', NULL, NULL),
('karim-belhadj', 'Karim Belhadj', 'KB', 'person',
'Développeur indépendant, contributeur Open Street Map et bénévole en éducation populaire.', NULL, NULL),
('ville-marseille-innov', 'Ville de Marseille — Mission Innovation', 'VMI', 'org',
'Service municipal qui pilote les démarches d''innovation citoyenne et de transition.',
'https://marseille.fr', NULL),
('donut-marseille', 'Le Donut Marseille', 'DM', 'collective',
'Collectif qui anime la communauté Donut et son infolab à Marseille.',
'https://ledonut-marseille.org', 'hello@ledonut-marseille.org'),
('univ-amu-data-lab', 'AMU — Data Lab des humanités', 'AMU', 'org',
'Laboratoire d''Aix-Marseille Université dédié à l''analyse de données pour les sciences humaines.', NULL, NULL),
('nadia-ben-salah', 'Nadia Ben Salah', 'NBS', 'person',
'Architecte urbaniste spécialisée dans la rénovation participative des centres anciens.', NULL, NULL);
-- ============================================================================
-- PROJETS
-- IDs catégories résolues via subquery sur le slug (donc ordre indépendant).
-- created_at étalés dans le temps pour rendre les archives crédibles.
-- ============================================================================
-- ----- ACTIFS -----
INSERT INTO projects (slug, title, category_id, status, territory, short_description, context_description, objectives, progress_percent, started_at, created_at) VALUES
('nos-ecoles', 'Nos Écoles',
(SELECT id FROM categories WHERE slug='education-ecoles'),
'active', 'Marseille',
'Plateforme citoyenne pour partager l''état des écoles publiques marseillaises.',
'Issu de la mobilisation citoyenne autour de l''école publique, ce projet cartographie l''état des bâtiments, mutualise les ressources entre établissements et donne une voix aux parents et enseignants.',
'Rendre visible la situation école par école, déclencher l''action publique, fédérer la communauté éducative.',
25, '2024-09-01', '2024-09-01 10:00:00'),
('allo-marie-archipel', 'ALLO MARIE / Archipel',
(SELECT id FROM categories WHERE slug='democratie-citoyennete'),
'active', 'Marseille',
'Outil d''interpellation citoyenne pour saisir directement la mairie et suivre la réponse publique.',
'Inspiré des civic-tech britanniques, ALLO MARIE permet à chacun de signaler un problème de proximité, de l''adresser au service compétent et de tracer le traitement.',
'Réduire le délai de prise en charge, transparence sur les non-réponses, agréger les besoins par quartier.',
60, '2024-03-15', '2024-03-15 09:30:00'),
('crowdcity', 'CrowdCity',
(SELECT id FROM categories WHERE slug='habitat-solidarite'),
'active', 'Marseille — Quartiers Nord',
'Plateforme d''entraide entre habitants pour partager outils, services et savoir-faire.',
'Né d''une expérimentation à la Belle de Mai en 2023, CrowdCity met en relation des habitants qui ont besoin (déménagement, garde, courses) et ceux qui peuvent aider, à l''échelle de l''immeuble ou du quartier.',
'Densifier les liens de voisinage, lutter contre l''isolement, formaliser une économie du don.',
35, '2024-05-10', '2024-05-10 14:00:00'),
('ilots-de-chaleur', 'Cartographie citoyenne des îlots de chaleur',
(SELECT id FROM categories WHERE slug='territoire-urbanisme'),
'active', 'Marseille — Tout territoire',
'Capteurs citoyens distribués pour mesurer la chaleur urbaine et identifier les priorités de renaturation.',
'Marseille est l''une des villes les plus exposées aux vagues de chaleur. Faute de données fines, les choix d''aménagement reposent sur des modèles théoriques. Ce projet outille des habitants pour mesurer.',
'Produire un corpus open data, alimenter le dialogue avec la métropole sur les priorités de renaturation.',
45, '2024-06-20', '2024-06-20 11:00:00'),
('hackathon-nature-ville', 'Hackathon de la nature en ville',
(SELECT id FROM categories WHERE slug='ecologie-biodiversite'),
'active', 'Marseille',
'Événement annuel : développeurs et naturalistes co-construisent des outils pour la biodiversité locale.',
'Édition 2025 organisée avec Tela Botanica et le Muséum. 48h pour produire des prototypes (apps de signalement, dashboards, cartes), suivis sur 6 mois pour les plus prometteurs.',
'Accélérer l''émergence de civic tech écologiques marseillaises, croiser les communautés tech et biodiversité.',
80, '2024-09-15', '2024-09-15 15:00:00'),
('marches-locaux', 'Observatoire citoyen des marchés locaux',
(SELECT id FROM categories WHERE slug='data-open-data'),
'active', 'Marseille — Tous arrondissements',
'Recensement participatif de l''offre alimentaire dans les marchés (origine, prix, saisonnalité).',
'Manque criant de données ouvertes sur la souveraineté alimentaire. Une vingtaine de marchés du territoire suivis chaque semaine par des observateurs bénévoles.',
'Constituer un référentiel open data, outiller le débat sur les circuits courts, informer les consommateurs.',
30, '2024-08-01', '2024-08-01 08:30:00'),
('atelier-numerique-ecoles', 'Atelier numérique des écoles publiques',
(SELECT id FROM categories WHERE slug='education-ecoles'),
'active', 'Marseille — 13013 / 13014',
'Initiation à la programmation et à la culture numérique critique pour les CM1-CM2.',
'Partenariat entre 8 écoles, deux assos d''éducation populaire et une école d''ingénieurs. Cycle de 12 ateliers par classe avec du matériel récupéré et reconditionné.',
'Acculturer les élèves au numérique sans dépendre des géants, former des animateurs locaux.',
50, '2024-10-01', '2024-10-01 10:00:00'),
('reseau-entraide-sud', 'Réseau d''entraide quartiers Sud',
(SELECT id FROM categories WHERE slug='habitat-solidarite'),
'active', 'Marseille — 13008 / 13009',
'Cellule d''écoute et d''orientation pour les habitants isolés, animée par des bénévoles.',
'Lancé après le confinement, le réseau prolonge le travail de voisinage initié pendant la crise sanitaire avec un dispositif structuré.',
'Maintenir le lien avec les personnes vulnérables, faciliter l''accès aux droits et services.',
15, '2024-11-15', '2024-11-15 13:00:00'),
('budgets-participatifs-nord', 'Budgets participatifs Quartiers Nord',
(SELECT id FROM categories WHERE slug='democratie-citoyennete'),
'active', 'Marseille — 13014 / 13015 / 13016',
'Co-construction de l''allocation d''une enveloppe métropolitaine dédiée aux Quartiers Nord.',
'Première édition pilote sur 250 k€ : ateliers de remontée des besoins, vote pondéré, suivi citoyen de l''exécution.',
'Démontrer la faisabilité d''un budget participatif sérieux à l''échelle d''un secteur populaire, essaimer.',
70, '2024-04-01', '2024-04-01 09:00:00'),
('reboisement-collines', 'Reboisement participatif des collines',
(SELECT id FROM categories WHERE slug='ecologie-biodiversite'),
'active', 'Calanques / Étoile / Garlaban',
'Chantiers citoyens de plantation et entretien sur les massifs périurbains.',
'Après les incendies de 2020 et 2022, plusieurs collectifs se sont structurés pour des chantiers de restauration. Le projet mutualise leur calendrier, leur logistique et leur suivi botanique.',
'Renforcer la résilience des massifs, créer un savoir-faire citoyen sur la reforestation méditerranéenne.',
55, '2023-11-01', '2023-11-01 10:00:00');
-- ----- TERMINÉS -----
INSERT INTO projects (slug, title, category_id, status, territory, short_description, context_description, objectives, progress_percent, started_at, completed_at, created_at) VALUES
('trame-verte-caillols', 'Une trame verte pour les Caillols',
(SELECT id FROM categories WHERE slug='ecologie-biodiversite'),
'completed', 'Marseille — 13012',
'Continuité écologique entre les jardins privés et les espaces publics du quartier.',
'Co-construit avec 200 habitants des Caillols sur 18 mois. Identification de corridors potentiels, accord de gestion avec la métropole et plantations.',
'Restaurer une trame verte fonctionnelle, créer un commun de quartier autour de la biodiversité.',
100, '2022-09-01', '2024-06-30', '2022-09-01 10:00:00'),
('opendata-scot', 'Open Data du SCoT métropolitain',
(SELECT id FROM categories WHERE slug='data-open-data'),
'completed', 'Aix-Marseille-Provence',
'Mise en données ouvertes du Schéma de cohérence territoriale métropolitain.',
'Travail conjoint de la métropole et d''un collectif de civic-tech : nettoyage, sémantique, format INSPIRE, accès permanent via geo.amp.fr.',
'Permettre la critique et la réappropriation citoyenne du document de planification stratégique.',
100, '2023-01-15', '2024-09-15', '2023-01-15 09:00:00'),
('forum-democratique', 'Forum permanent de la démocratie citoyenne',
(SELECT id FROM categories WHERE slug='democratie-citoyennete'),
'completed', 'Marseille',
'Trois années de rencontres mensuelles entre acteurs de la démocratie locale.',
'Cycle clôturé après production d''un livre blanc et création d''une nouvelle assemblée citoyenne portée par la métropole.',
'Mettre en réseau les acteurs, formaliser des propositions reprises dans les politiques publiques.',
100, '2021-09-01', '2024-12-15', '2021-09-01 09:30:00'),
('mooc-citoyen', 'MOOC citoyen métropolitain',
(SELECT id FROM categories WHERE slug='education-ecoles'),
'completed', 'Aix-Marseille-Provence',
'Cours en ligne ouvert sur les enjeux du territoire (urbanisme, eau, mobilité, énergie).',
'Six modules co-produits avec AMU, le CEREMA et trois collectifs citoyens. 4 200 inscrits sur la première session.',
'Outiller la participation citoyenne en alimentant une culture commune des enjeux du territoire.',
100, '2023-03-01', '2024-11-30', '2023-03-01 10:00:00'),
('logement-digne-v1', 'Logement digne pour tous — V1',
(SELECT id FROM categories WHERE slug='habitat-solidarite'),
'completed', 'Marseille — Centre-ville',
'Plateforme de signalement de l''habitat indigne, abouti à un protocole municipal.',
'Lancé suite à la rue d''Aubagne. Plus de 600 signalements traités, débouchant sur une procédure d''urgence métropolitaine adoptée fin 2024.',
'Faire entrer la voix des habitants dans la lutte contre l''insalubrité.',
100, '2019-11-05', '2024-12-31', '2019-11-05 11:00:00'),
('renaturation-republique', 'Renaturation de la rue de la République',
(SELECT id FROM categories WHERE slug='territoire-urbanisme'),
'completed', 'Marseille — 13001 / 13002',
'Co-conception puis suivi citoyen de la végétalisation du tronçon nord de la rue de la République.',
'Démarche de co-design (5 ateliers), arbres adaptés au climat 2050, suivi botanique annuel par des bénévoles.',
'Démontrer qu''une renaturation lourde peut être co-conçue avec les habitants en milieu très dense.',
100, '2022-04-01', '2024-10-15', '2022-04-01 09:00:00');
-- ----- ARCHIVÉS -----
INSERT INTO projects (slug, title, category_id, status, territory, short_description, context_description, objectives, progress_percent, started_at, completed_at, created_at) VALUES
('cafe-citoyen-2018', 'Café citoyen 2018-2022',
(SELECT id FROM categories WHERE slug='democratie-citoyennete'),
'archived', 'Marseille — Cours Julien',
'Rendez-vous mensuel d''échanges politiques ouverts ; mis en sommeil en 2022.',
'Pendant cinq ans, un samedi matin par mois au Café des Mauvais Garçons. Archivé après essoufflement des animateurs et reprise du flambeau par d''autres collectifs.',
'Créer un espace de débat horizontal sur la vie politique locale.',
100, '2018-01-01', '2022-06-30', '2018-01-01 09:00:00'),
('velib-marseille-2015', 'Vélib Marseille 2015',
(SELECT id FROM categories WHERE slug='territoire-urbanisme'),
'archived', 'Marseille',
'Prototype citoyen de vélos en libre-service avant l''arrivée des opérateurs privés.',
'Six stations DIY, gestion communautaire. Démantelé en 2017 après l''arrivée d''opérateurs commerciaux et faute de modèle économique.',
'Démontrer la possibilité d''un service de mobilité douce porté par la société civile.',
50, '2015-05-01', '2017-12-31', '2015-05-01 10:00:00'),
('opendata-marseille-2014', 'Première expérimentation Open Data 2014',
(SELECT id FROM categories WHERE slug='data-open-data'),
'archived', 'Marseille',
'Portail open data citoyen lancé avant la plateforme officielle.',
'Premier inventaire des jeux de données publics réutilisables. Repris en 2019 par les institutions, le projet a été archivé après transfert.',
'Ouvrir la voie à une politique open data publique.',
100, '2014-06-01', '2019-09-30', '2014-06-01 10:00:00'),
('ecole-numerique-2017', 'École numérique pilote 2017',
(SELECT id FROM categories WHERE slug='education-ecoles'),
'archived', 'Marseille — Belle de Mai',
'Expérimentation d''une école élémentaire entièrement équipée en tablettes.',
'Évaluation conduite sur 3 ans avec AMU. Conclusions ambivalentes : engagement élevé mais creusement des inégalités. Projet archivé après transfert des enseignements.',
'Documenter l''impact réel de l''équipement numérique massif en école élémentaire.',
100, '2017-09-01', '2020-06-30', '2017-09-01 10:00:00');
-- ============================================================================
-- ÉQUIPES (project_members)
-- ============================================================================
INSERT INTO project_members (project_id, member_id, role) VALUES
((SELECT id FROM projects WHERE slug='nos-ecoles'), (SELECT id FROM members WHERE slug='collectif-ecoles-marseille'), 'admin'),
((SELECT id FROM projects WHERE slug='nos-ecoles'), (SELECT id FROM members WHERE slug='donut-marseille'), 'contributor'),
((SELECT id FROM projects WHERE slug='nos-ecoles'), (SELECT id FROM members WHERE slug='camille-dupont'), 'contributor'),
((SELECT id FROM projects WHERE slug='allo-marie-archipel'), (SELECT id FROM members WHERE slug='karim-belhadj'), 'admin'),
((SELECT id FROM projects WHERE slug='allo-marie-archipel'), (SELECT id FROM members WHERE slug='ville-marseille-innov'), 'contributor'),
((SELECT id FROM projects WHERE slug='crowdcity'), (SELECT id FROM members WHERE slug='atd-quart-monde-13'), 'admin'),
((SELECT id FROM projects WHERE slug='crowdcity'), (SELECT id FROM members WHERE slug='nadia-ben-salah'), 'contributor'),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'), (SELECT id FROM members WHERE slug='univ-amu-data-lab'), 'admin'),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'), (SELECT id FROM members WHERE slug='karim-belhadj'), 'contributor'),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), (SELECT id FROM members WHERE slug='tela-botanica-paca'), 'admin'),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), (SELECT id FROM members WHERE slug='wildproject'), 'contributor'),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), (SELECT id FROM members WHERE slug='donut-marseille'), 'contributor'),
((SELECT id FROM projects WHERE slug='marches-locaux'), (SELECT id FROM members WHERE slug='univ-amu-data-lab'), 'admin'),
((SELECT id FROM projects WHERE slug='atelier-numerique-ecoles'), (SELECT id FROM members WHERE slug='karim-belhadj'), 'admin'),
((SELECT id FROM projects WHERE slug='atelier-numerique-ecoles'), (SELECT id FROM members WHERE slug='collectif-ecoles-marseille'), 'contributor'),
((SELECT id FROM projects WHERE slug='reseau-entraide-sud'), (SELECT id FROM members WHERE slug='atd-quart-monde-13'), 'admin'),
((SELECT id FROM projects WHERE slug='budgets-participatifs-nord'),(SELECT id FROM members WHERE slug='ville-marseille-innov'), 'admin'),
((SELECT id FROM projects WHERE slug='budgets-participatifs-nord'),(SELECT id FROM members WHERE slug='camille-dupont'), 'contributor'),
((SELECT id FROM projects WHERE slug='reboisement-collines'), (SELECT id FROM members WHERE slug='tela-botanica-paca'), 'admin'),
((SELECT id FROM projects WHERE slug='reboisement-collines'), (SELECT id FROM members WHERE slug='wildproject'), 'contributor'),
((SELECT id FROM projects WHERE slug='trame-verte-caillols'), (SELECT id FROM members WHERE slug='tela-botanica-paca'), 'admin'),
((SELECT id FROM projects WHERE slug='opendata-scot'), (SELECT id FROM members WHERE slug='univ-amu-data-lab'), 'admin'),
((SELECT id FROM projects WHERE slug='forum-democratique'), (SELECT id FROM members WHERE slug='camille-dupont'), 'admin'),
((SELECT id FROM projects WHERE slug='mooc-citoyen'), (SELECT id FROM members WHERE slug='univ-amu-data-lab'), 'admin'),
((SELECT id FROM projects WHERE slug='logement-digne-v1'), (SELECT id FROM members WHERE slug='atd-quart-monde-13'), 'admin'),
((SELECT id FROM projects WHERE slug='renaturation-republique'), (SELECT id FROM members WHERE slug='nadia-ben-salah'), 'admin');
-- ============================================================================
-- ROADMAP — quelques étapes par projet actif/terminé
-- ============================================================================
INSERT INTO roadmap_steps (project_id, title, description, step_date, is_done, sort_order) VALUES
((SELECT id FROM projects WHERE slug='nos-ecoles'), 'Création de l''outil 15min', 'MVP avec une dizaine d''écoles pilotes', '2022-06-01', 1, 10),
((SELECT id FROM projects WHERE slug='nos-ecoles'), 'Hackathon des écoles', 'Week-end de design avec parents et enseignants', '2024-03-01', 1, 20),
((SELECT id FROM projects WHERE slug='nos-ecoles'), 'Extension des données', 'Couverture étendue à 150 établissements', '2025-06-01', 0, 30),
((SELECT id FROM projects WHERE slug='nos-ecoles'), 'Tableau de bord public', 'KPIs accessibles sans inscription', NULL, 0, 40),
((SELECT id FROM projects WHERE slug='allo-marie-archipel'), 'Prototype interne', 'Test sur 50 agents municipaux', '2024-04-01', 1, 10),
((SELECT id FROM projects WHERE slug='allo-marie-archipel'), 'Pilote 13001', 'Déploiement sur l''arrondissement Belle de Mai', '2024-07-01', 1, 20),
((SELECT id FROM projects WHERE slug='allo-marie-archipel'), 'Généralisation', 'Toute la ville', '2025-09-01', 0, 30),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'), 'Achat des capteurs', NULL, '2024-06-25', 1, 10),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'), 'Formation des bénévoles', '20 personnes formées en juin 2024', '2024-06-30', 1, 20),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'), 'Première saison de mesure', 'Été 2024 — terminé', '2024-09-15', 1, 30),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'), 'Atlas open data', 'Publication des données', '2025-04-01', 0, 40),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), 'Édition 2024', '12 prototypes produits', '2024-09-22', 1, 10),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), 'Suivi des 3 lauréats', 'Accompagnement 6 mois', '2025-03-22', 1, 20),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), 'Édition 2025', NULL, '2025-09-15', 0, 30),
((SELECT id FROM projects WHERE slug='trame-verte-caillols'), 'Diagnostic écologique', NULL, '2022-12-01', 1, 10),
((SELECT id FROM projects WHERE slug='trame-verte-caillols'), 'Conception participative', '8 ateliers de quartier', '2023-06-01', 1, 20),
((SELECT id FROM projects WHERE slug='trame-verte-caillols'), 'Plantations', '450 arbres et arbustes', '2024-03-01', 1, 30),
((SELECT id FROM projects WHERE slug='trame-verte-caillols'), 'Convention de gestion', 'Validée par la métropole', '2024-06-30', 1, 40),
((SELECT id FROM projects WHERE slug='opendata-scot'), 'Inventaire des jeux', NULL, '2023-04-01', 1, 10),
((SELECT id FROM projects WHERE slug='opendata-scot'), 'Mise au format INSPIRE', NULL, '2023-12-01', 1, 20),
((SELECT id FROM projects WHERE slug='opendata-scot'), 'Publication geo.amp.fr', NULL, '2024-09-15', 1, 30),
((SELECT id FROM projects WHERE slug='crowdcity'), 'Expérimentation Belle de Mai', NULL, '2023-04-01', 1, 10),
((SELECT id FROM projects WHERE slug='crowdcity'), 'V1 application', '50 utilisateurs actifs', '2024-09-01', 1, 20),
((SELECT id FROM projects WHERE slug='crowdcity'), 'Extension 2 quartiers', NULL, '2025-06-01', 0, 30),
((SELECT id FROM projects WHERE slug='reboisement-collines'), 'Cartographie post-incendie 2023', NULL, '2023-11-15', 1, 10),
((SELECT id FROM projects WHERE slug='reboisement-collines'), 'Premier chantier', '120 bénévoles', '2024-02-10', 1, 20),
((SELECT id FROM projects WHERE slug='reboisement-collines'), 'Suivi 2025', 'Mesure de la reprise', '2025-04-01', 0, 30);
-- ============================================================================
-- DISCUSSIONS — quelques fils par projet emblématique
-- ============================================================================
INSERT INTO discussions (project_id, author_name, body) VALUES
((SELECT id FROM projects WHERE slug='nos-ecoles'),
'Camille D.',
'Super initiative — est-ce qu''on peut intégrer l''école Ruffi qui n''est pas encore référencée ?'),
((SELECT id FROM projects WHERE slug='nos-ecoles'),
'Karim B.',
'On a une carto OSM des écoles publiques marseillaises qui pourrait servir de base, je la pousse demain.'),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'),
'Tela Botanica PACA',
'Pour 2025 on pousse pour intégrer un volet hortillonnages — qui est partant ?'),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'),
'Nadia BS',
'Très intéressée par les corrélations entre îlots et taux de minéralisation, dispo pour co-écrire la méthodo.'),
((SELECT id FROM projects WHERE slug='crowdcity'),
'ATD Quart Monde 13',
'Attention à ne pas reproduire les biais des plateformes : les premiers servis sont les plus connectés. Comment on touche les invisibles ?'),
((SELECT id FROM projects WHERE slug='budgets-participatifs-nord'),
'Camille D.',
'Le vote pondéré marche bien dans nos ateliers — je partage la grille de pondération qu''on utilise.');
-- ============================================================================
-- RESSOURCES — quelques liens par projet
-- ============================================================================
INSERT INTO resources (project_id, title, description, url, kind, sort_order) VALUES
((SELECT id FROM projects WHERE slug='nos-ecoles'),
'Site officiel Nos Écoles', NULL,
'https://nos-ecoles.fr', 'link', 10),
((SELECT id FROM projects WHERE slug='nos-ecoles'),
'Méthodologie de relevé',
'PDF, 18 pages, version 2024',
'https://nos-ecoles.fr/docs/methode-2024.pdf', 'document', 20),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'),
'Programme édition 2024', NULL,
'https://hackathon-nature.org/2024', 'link', 10),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'),
'Captation des restitutions',
'Vidéo 38 minutes',
'https://vimeo.com/example/hackathon-nature-2024', 'video', 20),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'),
'Premier jeu de données — été 2024',
'CSV horaire, 12 capteurs sur 3 mois',
'https://data.amp.fr/datasets/ilots-2024.csv', 'dataset', 10),
((SELECT id FROM projects WHERE slug='opendata-scot'),
'Plateforme geo.amp.fr', NULL,
'https://geo.amp.fr', 'link', 10),
((SELECT id FROM projects WHERE slug='trame-verte-caillols'),
'Carte interactive des plantations', NULL,
'https://trame-caillols.org/carte', 'link', 10),
((SELECT id FROM projects WHERE slug='mooc-citoyen'),
'MOOC complet (FUN)', NULL,
'https://www.fun-mooc.fr/mooc-citoyen-amp', 'link', 10);
-- ============================================================================
-- ACTIVITÉ — quelques événements antérieurs pour peupler le flux
-- ============================================================================
INSERT INTO activity_log (project_id, member_id, actor_label, verb, summary, occurred_at) VALUES
((SELECT id FROM projects WHERE slug='nos-ecoles'), NULL, 'Vous', 'created_project',
'a créé le projet « Nos Écoles »', '2024-09-01 10:05:00'),
((SELECT id FROM projects WHERE slug='nos-ecoles'),
(SELECT id FROM members WHERE slug='donut-marseille'),
'Le Donut Marseille', 'joined',
'a rejoint le projet « Nos Écoles »', '2024-09-02 14:22:00'),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), NULL, 'Vous', 'created_project',
'a créé le projet « Hackathon de la nature en ville »', '2024-09-15 15:10:00'),
((SELECT id FROM projects WHERE slug='hackathon-nature-ville'), NULL, 'Vous', 'completed_step',
'a terminé l''étape « Édition 2024 » du projet « Hackathon de la nature en ville »', '2024-09-23 18:30:00'),
((SELECT id FROM projects WHERE slug='trame-verte-caillols'), NULL, 'Vous', 'completed_project',
'a terminé le projet « Une trame verte pour les Caillols »', '2024-06-30 16:00:00'),
((SELECT id FROM projects WHERE slug='allo-marie-archipel'), NULL, 'Vous', 'updated_project',
'a mis à jour le projet « ALLO MARIE / Archipel »', '2025-01-15 11:30:00'),
((SELECT id FROM projects WHERE slug='ilots-de-chaleur'), NULL, 'Vous', 'added_resource',
'a ajouté la ressource « Premier jeu de données — été 2024 » au projet « Cartographie citoyenne des îlots de chaleur »', '2024-10-05 09:15:00'),
((SELECT id FROM projects WHERE slug='budgets-participatifs-nord'),NULL, 'Vous', 'posted_discussion',
'a publié un message dans « Budgets participatifs Quartiers Nord »', '2025-02-10 17:45:00');
-- Résumé
SELECT
(SELECT COUNT(*) FROM projects WHERE deleted_at IS NULL AND status='active') AS active,
(SELECT COUNT(*) FROM projects WHERE deleted_at IS NULL AND status='completed') AS completed,
(SELECT COUNT(*) FROM projects WHERE deleted_at IS NULL AND status='archived') AS archived,
(SELECT COUNT(*) FROM members) AS members,
(SELECT COUNT(*) FROM project_members) AS team_links,
(SELECT COUNT(*) FROM roadmap_steps) AS roadmap_steps,
(SELECT COUNT(*) FROM discussions) AS discussions,
(SELECT COUNT(*) FROM resources) AS resources,
(SELECT COUNT(*) FROM activity_log) AS activity_events;
SQL
echo "OK"