feat(planning): vue Gantt + toggle calendrier/gantt

This commit is contained in:
2026-03-22 12:51:16 +01:00
parent 4fca4b9278
commit 76d0984b06

View File

@@ -1,126 +1,267 @@
<template>
<div class="p-4 max-w-6xl mx-auto space-y-8">
<div class="flex items-center justify-between">
<div class="p-4 max-w-6xl mx-auto space-y-6">
<!-- ====== En-tête ====== -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-green tracking-tight">Planning</h1>
<p class="text-text-muted text-xs mt-1">Visualisez et planifiez vos interventions sur 4 semaines.</p>
</div>
<!-- Navigateur -->
<div class="flex items-center gap-2 bg-bg-soft/30 p-1 rounded-xl border border-bg-soft">
<button @click="prevPeriod" class="btn-outline !py-1.5 !px-3 text-xs font-bold uppercase tracking-widest border-transparent hover:bg-bg-soft transition-all">Préc.</button>
<button @click="goToday" class="btn-primary !py-1.5 !px-4 text-xs !rounded-lg">Aujourd'hui</button>
<button @click="nextPeriod" class="btn-outline !py-1.5 !px-3 text-xs font-bold uppercase tracking-widest border-transparent hover:bg-bg-soft transition-all">Suiv.</button>
</div>
</div>
<div class="flex items-center gap-3">
<div class="text-yellow font-bold text-sm tracking-widest uppercase bg-yellow/5 px-4 py-1 rounded-full border border-yellow/10">
{{ periodLabel }}
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
<!-- Toggle Calendrier / Gantt -->
<div class="flex bg-bg-soft/30 border border-bg-soft rounded-xl p-1 gap-1">
<button
@click="viewMode = 'calendar'"
:class="['py-1.5 px-3 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all',
viewMode === 'calendar' ? 'bg-green/20 text-green border border-green/30' : 'text-text-muted hover:text-text']"
>📅 Calendrier</button>
<button
@click="viewMode = 'gantt'"
:class="['py-1.5 px-3 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all',
viewMode === 'gantt' ? 'bg-aqua/20 text-aqua border border-aqua/30' : 'text-text-muted hover:text-text']"
>📊 Gantt</button>
</div>
<!-- Calendrier Grid -->
<div class="space-y-4">
<!-- En-tête jours -->
<div class="grid grid-cols-7 gap-3">
<div v-for="dayName in dayHeaders" :key="dayName"
class="text-center text-[10px] font-black uppercase tracking-[0.2em] text-text-muted/60 pb-2">
{{ dayName }}
<!-- Navigateur -->
<div class="flex items-center gap-2 bg-bg-soft/30 p-1 rounded-xl border border-bg-soft">
<button @click="prevPeriod" class="btn-outline !py-1.5 !px-3 text-xs font-bold uppercase tracking-widest border-transparent hover:bg-bg-soft transition-all">Préc.</button>
<button @click="goToday" class="btn-primary !py-1.5 !px-4 text-xs !rounded-lg">Aujourd'hui</button>
<button @click="nextPeriod" class="btn-outline !py-1.5 !px-3 text-xs font-bold uppercase tracking-widest border-transparent hover:bg-bg-soft transition-all">Suiv.</button>
</div>
</div>
</div>
<!-- Grille 4 semaines -->
<div class="grid grid-cols-7 gap-3">
<div v-for="d in periodDays" :key="d.iso"
@click="selectDay(d.iso)"
:class="['min-h-32 card-jardin !p-2 flex flex-col group relative cursor-pointer border-2 transition-all',
d.isToday ? 'border-green/40 bg-green/5' : 'border-bg-hard/50',
selectedIso === d.iso ? '!border-yellow bg-yellow/5 scale-[1.02] z-10 shadow-xl' : 'hover:border-bg-soft']">
<div class="flex items-center justify-between mb-2">
<span :class="['text-xs font-bold font-mono', d.isToday ? 'text-green' : 'text-text-muted']">{{ d.dayNum }}</span>
<span v-if="d.showMonth" class="text-[9px] font-black uppercase tracking-tighter text-aqua">{{ d.monthShort }}</span>
<div class="text-yellow font-bold text-sm tracking-widest uppercase bg-yellow/5 px-4 py-1 rounded-full border border-yellow/10 self-start">
{{ periodLabel }}
</div>
<!-- ====== VUE CALENDRIER ====== -->
<template v-if="viewMode === 'calendar'">
<div class="space-y-4">
<div class="grid grid-cols-7 gap-3">
<div v-for="dayName in dayHeaders" :key="dayName"
class="text-center text-[10px] font-black uppercase tracking-[0.2em] text-text-muted/60 pb-2">
{{ dayName }}
</div>
</div>
<div class="flex-1 space-y-1 overflow-hidden">
<div v-for="t in tasksByDay[d.iso]?.slice(0, 3)" :key="t.id"
:class="['text-[9px] font-bold uppercase tracking-tighter rounded px-1.5 py-0.5 truncate border', priorityBorderClass(t.priorite)]">
{{ t.titre }}
<div class="grid grid-cols-7 gap-3">
<div v-for="d in periodDays" :key="d.iso"
@click="selectDay(d.iso)"
:class="['min-h-32 card-jardin !p-2 flex flex-col group relative cursor-pointer border-2 transition-all',
d.isToday ? 'border-green/40 bg-green/5' : 'border-bg-hard/50',
selectedIso === d.iso ? '!border-yellow bg-yellow/5 scale-[1.02] z-10 shadow-xl' : 'hover:border-bg-soft']">
<div class="flex items-center justify-between mb-2">
<span :class="['text-xs font-bold font-mono', d.isToday ? 'text-green' : 'text-text-muted']">{{ d.dayNum }}</span>
<span v-if="d.showMonth" class="text-[9px] font-black uppercase tracking-tighter text-aqua">{{ d.monthShort }}</span>
</div>
<div v-if="tasksByDay[d.iso]?.length > 3" class="text-[9px] text-text-muted font-bold text-center pt-1">
+ {{ tasksByDay[d.iso].length - 3 }} autres
</div>
</div>
<!-- Indicateurs points si plein -->
<div v-if="todoTasksByDay[d.iso]?.length" class="absolute bottom-2 right-2 flex gap-0.5">
<span v-for="(t, i) in todoTasksByDay[d.iso].slice(0, 3)" :key="i"
:class="['w-1 h-1 rounded-full', dotClass(t.priorite)]"></span>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 pt-4">
<!-- Détail jour sélectionné -->
<section class="lg:col-span-2 space-y-4">
<h2 class="text-text-muted text-xs font-bold uppercase tracking-widest flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-yellow"></span>
Détails du {{ selectedLabel }}
</h2>
<div v-if="!selectedTasks.length" class="card-jardin text-center py-12 opacity-40 border-dashed">
<p class="text-text-muted text-sm italic">Libre comme l'air ! Aucune tâche pour cette date. 🍃</p>
</div>
<div v-else class="space-y-3">
<div v-for="t in selectedTasks" :key="t.id"
class="card-jardin flex items-center gap-4 group">
<div :class="['w-1.5 h-10 rounded-full shrink-0', dotClass(t.priorite)]"></div>
<div class="flex-1 min-w-0">
<div class="text-text font-bold text-sm">{{ t.titre }}</div>
<div class="flex items-center gap-3 mt-1">
<span :class="['badge !text-[9px]', statutClass(t.statut)]">{{ t.statut?.replace('_', ' ') }}</span>
<span class="text-[10px] text-text-muted uppercase font-bold tracking-tighter opacity-60">Priorité {{ t.priorite }}</span>
<div class="flex-1 space-y-1 overflow-hidden">
<div v-for="t in tasksByDay[d.iso]?.slice(0, 3)" :key="t.id"
:class="['text-[9px] font-bold uppercase tracking-tighter rounded px-1.5 py-0.5 truncate border', priorityBorderClass(t.priorite)]">
{{ t.titre }}
</div>
<div v-if="tasksByDay[d.iso]?.length > 3" class="text-[9px] text-text-muted font-bold text-center pt-1">
+ {{ tasksByDay[d.iso].length - 3 }} autres
</div>
</div>
<div class="text-green text-xl opacity-0 group-hover:opacity-100 transition-all"></div>
</div>
</div>
</section>
<!-- Tâches sans date -->
<section class="space-y-4">
<h2 class="text-text-muted text-xs font-bold uppercase tracking-widest flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-orange"></span>
À planifier
</h2>
<div v-if="!unscheduled.length" class="card-jardin text-center py-12 opacity-30 border-dashed">
<p class="text-[10px] font-bold uppercase">Tout est en ordre</p>
</div>
<div class="space-y-2">
<div v-for="t in unscheduled" :key="t.id"
class="card-jardin !p-3 flex flex-col gap-2 hover:border-orange/30 transition-colors">
<div class="flex items-start justify-between gap-2">
<span class="text-text font-bold text-xs flex-1 line-clamp-2">{{ t.titre }}</span>
<div :class="['w-2 h-2 rounded-full shrink-0 mt-1', dotClass(t.priorite)]"></div>
</div>
<div class="flex justify-between items-center">
<span :class="['badge !text-[8px]', statutClass(t.statut)]">{{ t.statut }}</span>
<button class="text-[10px] text-orange font-bold uppercase hover:underline">Planifier</button>
<div v-if="todoTasksByDay[d.iso]?.length" class="absolute bottom-2 right-2 flex gap-0.5">
<span v-for="(t, i) in todoTasksByDay[d.iso].slice(0, 3)" :key="i"
:class="['w-1 h-1 rounded-full', dotClass(t.priorite)]"></span>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 pt-4">
<!-- Détail jour sélectionné -->
<section class="lg:col-span-2 space-y-4">
<h2 class="text-text-muted text-xs font-bold uppercase tracking-widest flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-yellow"></span>
Détails du {{ selectedLabel }}
</h2>
<div v-if="!selectedTasks.length" class="card-jardin text-center py-12 opacity-40 border-dashed">
<p class="text-text-muted text-sm italic">Libre comme l'air ! Aucune tâche pour cette date. 🍃</p>
</div>
<div v-else class="space-y-3">
<div v-for="t in selectedTasks" :key="t.id" class="card-jardin flex items-center gap-4 group">
<div :class="['w-1.5 h-10 rounded-full shrink-0', dotClass(t.priorite)]"></div>
<div class="flex-1 min-w-0">
<div class="text-text font-bold text-sm">{{ t.titre }}</div>
<div class="flex items-center gap-3 mt-1">
<span :class="['badge !text-[9px]', statutClass(t.statut)]">{{ t.statut?.replace('_', ' ') }}</span>
<span class="text-[10px] text-text-muted uppercase font-bold tracking-tighter opacity-60">Priorité {{ t.priorite }}</span>
</div>
</div>
<div class="text-green text-xl opacity-0 group-hover:opacity-100 transition-all"></div>
</div>
</div>
</section>
<!-- Tâches sans date -->
<section class="space-y-4">
<h2 class="text-text-muted text-xs font-bold uppercase tracking-widest flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-orange"></span>
À planifier
</h2>
<div v-if="!unscheduled.length" class="card-jardin text-center py-12 opacity-30 border-dashed">
<p class="text-[10px] font-bold uppercase">Tout est en ordre</p>
</div>
<div class="space-y-2">
<div v-for="t in unscheduled" :key="t.id"
class="card-jardin !p-3 flex flex-col gap-2 hover:border-orange/30 transition-colors">
<div class="flex items-start justify-between gap-2">
<span class="text-text font-bold text-xs flex-1 line-clamp-2">{{ t.titre }}</span>
<div :class="['w-2 h-2 rounded-full shrink-0 mt-1', dotClass(t.priorite)]"></div>
</div>
<div class="flex justify-between items-center">
<span :class="['badge !text-[8px]', statutClass(t.statut)]">{{ t.statut }}</span>
<button class="text-[10px] text-orange font-bold uppercase hover:underline">Planifier</button>
</div>
</div>
</div>
</section>
</div>
</template>
<!-- ====== VUE GANTT ====== -->
<template v-else>
<div class="card-jardin !p-0 overflow-hidden border-aqua/10">
<!-- En-tête Gantt (sticky) -->
<div class="bg-bg-hard border-b border-bg-soft">
<!-- Libellés mois -->
<div class="flex border-b border-bg-soft/40">
<div class="w-48 shrink-0 border-r border-bg-soft"></div>
<div class="flex-1 flex overflow-hidden">
<div
v-for="mg in ganttMonthGroups" :key="mg.label"
class="text-[8px] font-black uppercase tracking-widest text-text-muted/50 px-2 py-1 border-r border-bg-soft/40 truncate overflow-hidden"
:style="`flex: ${mg.span} ${mg.span} 0%`"
>{{ mg.label }}</div>
</div>
</div>
<!-- Numéros de jours -->
<div class="flex">
<div class="w-48 shrink-0 border-r border-bg-soft px-2 py-1">
<span class="text-[9px] font-black uppercase tracking-widest text-text-muted/40">Tâche</span>
</div>
<div class="flex-1 grid" style="grid-template-columns: repeat(28, 1fr)">
<div
v-for="d in periodDays" :key="d.iso"
:class="['text-center py-1 border-r border-bg-soft/30 last:border-r-0', d.isToday ? 'bg-green/10' : '']"
>
<span :class="['text-[8px] font-mono', d.isToday ? 'text-green font-bold' : 'text-text-muted/40']">
{{ d.dayNum }}
</span>
</div>
</div>
</div>
</div>
<!-- Lignes de tâches groupées par statut -->
<div class="divide-y divide-bg-hard/60">
<template v-for="group in ganttGroups" :key="group.statut">
<!-- Titre du groupe -->
<div class="flex bg-bg-soft/30">
<div class="w-48 shrink-0 px-3 py-1.5 border-r border-bg-soft">
<span :class="['text-[9px] font-black uppercase tracking-widest', group.color]">
{{ group.label }} · {{ group.tasks.length }}
</span>
</div>
<div class="flex-1 grid" style="grid-template-columns: repeat(28, 1fr)">
<div v-for="d in periodDays" :key="d.iso"
:class="['border-r border-bg-soft/20 last:border-r-0 h-full min-h-[1.75rem]', d.isToday ? 'bg-green/5' : '']">
</div>
</div>
</div>
<!-- Une ligne par tâche -->
<div
v-for="t in group.tasks" :key="t.id"
class="flex h-8 hover:bg-bg-soft/20 transition-colors group/row"
>
<!-- Libellé -->
<div class="w-48 shrink-0 px-2 flex items-center gap-2 border-r border-bg-hard/50">
<span :class="['w-1.5 h-1.5 rounded-full shrink-0', dotClass(t.priorite)]"></span>
<span class="text-[10px] font-bold truncate text-text/70 group-hover/row:text-text transition-colors">
{{ t.titre }}
</span>
</div>
<!-- Zone barres -->
<div class="flex-1 relative">
<!-- Colonnes jours (guides visuels) -->
<div class="absolute inset-0 grid pointer-events-none" style="grid-template-columns: repeat(28, 1fr)">
<div
v-for="d in periodDays" :key="d.iso"
:class="['border-r border-bg-hard/20 last:border-r-0 h-full', d.isToday ? 'bg-green/5' : '']">
</div>
</div>
<!-- Barre Gantt -->
<div
v-if="ganttBarVisible(t)"
class="absolute top-[5px] h-[18px] rounded flex items-center overflow-hidden transition-all duration-200"
:class="ganttBarClass(t)"
:style="ganttBarStyle(t)"
:title="`${t.titre} — échéance ${t.echeance?.slice(0, 10)}`"
>
<span class="text-[8px] font-bold px-1.5 truncate whitespace-nowrap leading-none">
{{ t.titre }}
</span>
</div>
<!-- Marqueur hors période -->
<div v-else-if="t.echeance" class="absolute inset-y-0 flex items-center pl-1">
<span class="text-[8px] text-text-muted/40 italic">hors période</span>
</div>
</div>
</div>
</template>
<!-- Vide -->
<div v-if="!ganttTasks.length" class="py-16 text-center">
<p class="text-text-muted text-sm italic opacity-40">Aucune tâche planifiée. 🌱</p>
</div>
</div>
</div>
<!-- Légende + stats -->
<div class="flex flex-wrap items-center gap-6 text-[10px] font-bold uppercase tracking-widest">
<div class="flex items-center gap-2 text-text-muted">
<span class="w-4 h-3 rounded-sm bg-red/60 border border-red/40"></span> Haute
</div>
<div class="flex items-center gap-2 text-text-muted">
<span class="w-4 h-3 rounded-sm bg-yellow/60 border border-yellow/40"></span> Normale
</div>
<div class="flex items-center gap-2 text-text-muted">
<span class="w-4 h-3 rounded-sm bg-blue/50 border border-blue/30"></span> Basse
</div>
<div class="flex items-center gap-2 text-text-muted">
<span class="w-4 h-3 rounded-sm bg-text-muted/30 border border-text-muted/20"></span> Terminée
</div>
<span class="text-text-muted/50">·</span>
<span class="text-aqua">{{ ganttTasks.length }} tâche(s) affichée(s)</span>
<span v-if="unscheduled.length" class="text-orange">{{ unscheduled.length }} non planifiée(s)</span>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { computed, ref } from 'vue'
import { useTasksStore } from '@/stores/tasks'
const store = useTasksStore()
@@ -129,10 +270,13 @@ const today = new Date()
const weekStart = ref(getMonday(today))
const selectedIso = ref(toIso(today))
const dayHeaders = ['lun', 'mar', 'mer', 'jeu', 'ven', 'sam', 'dim']
const viewMode = ref<'calendar' | 'gantt'>('calendar')
// ── Utilitaires date ──────────────────────────────────────────────────────────
function getMonday(d: Date) {
const day = d.getDay()
const diff = (day === 0 ? -6 : 1 - day)
const diff = day === 0 ? -6 : 1 - day
const m = new Date(d)
m.setDate(d.getDate() + diff)
m.setHours(0, 0, 0, 0)
@@ -143,6 +287,8 @@ function toIso(d: Date) {
return d.toISOString().slice(0, 10)
}
// ── Période 4 semaines ────────────────────────────────────────────────────────
const periodDays = computed(() => {
const todayIso = toIso(today)
return Array.from({ length: 28 }, (_, i) => {
@@ -161,46 +307,12 @@ const periodDays = computed(() => {
})
const periodLabel = computed(() => {
const start = periodDays.value[0]
const end = periodDays.value[27]
const s = new Date(start.iso + 'T12:00:00')
const e = new Date(end.iso + 'T12:00:00')
const s = new Date(periodDays.value[0].iso + 'T12:00:00')
const e = new Date(periodDays.value[27].iso + 'T12:00:00')
return `${s.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })} ${e.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })}`
})
const tasksByDay = computed(() => {
const map: Record<string, typeof store.tasks> = {}
for (const t of store.tasks) {
if (t.statut === 'template') continue
if (!t.echeance) continue
const key = t.echeance.slice(0, 10)
if (!map[key]) map[key] = []
map[key].push(t)
}
return map
})
const todoTasksByDay = computed(() => {
const map: Record<string, typeof store.tasks> = {}
for (const [iso, tasks] of Object.entries(tasksByDay.value)) {
map[iso] = tasks.filter(t => t.statut !== 'fait')
}
return map
})
const selectedTasks = computed(() => tasksByDay.value[selectedIso.value] || [])
const selectedLabel = computed(() => {
if (!selectedIso.value) return 'Détail du jour'
const d = new Date(selectedIso.value + 'T12:00:00')
return d.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})
})
const unscheduled = computed(() => store.tasks.filter(t => !t.echeance && t.statut !== 'fait' && t.statut !== 'template'))
// ── Navigation ────────────────────────────────────────────────────────────────
function prevPeriod() {
const d = new Date(weekStart.value)
@@ -222,23 +334,149 @@ function selectDay(iso: string) {
selectedIso.value = iso
}
// ── Données calendrier ────────────────────────────────────────────────────────
const tasksByDay = computed(() => {
const map: Record<string, typeof store.tasks> = {}
for (const t of store.tasks) {
if (t.statut === 'template' || !t.echeance) continue
const key = t.echeance.slice(0, 10)
if (!map[key]) map[key] = []
map[key].push(t)
}
return map
})
const todoTasksByDay = computed(() => {
const map: Record<string, typeof store.tasks> = {}
for (const [iso, tasks] of Object.entries(tasksByDay.value)) {
map[iso] = tasks.filter(t => t.statut !== 'fait')
}
return map
})
const selectedTasks = computed(() => tasksByDay.value[selectedIso.value] || [])
const selectedLabel = computed(() => {
if (!selectedIso.value) return 'Détail du jour'
const d = new Date(selectedIso.value + 'T12:00:00')
return d.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })
})
const unscheduled = computed(() =>
store.tasks.filter(t => !t.echeance && t.statut !== 'fait' && t.statut !== 'template')
)
// ── Données Gantt ─────────────────────────────────────────────────────────────
/** Décalage en jours depuis le début de la période (peut être négatif ou > 27) */
function dayOffset(iso: string): number {
const ps = new Date(periodDays.value[0].iso + 'T12:00:00')
const d = new Date(iso + 'T12:00:00')
return Math.round((d.getTime() - ps.getTime()) / 86400000)
}
const ganttGroups = computed(() => {
const all = store.tasks.filter(t => t.statut !== 'template' && t.echeance)
const sort = (arr: typeof store.tasks) =>
[...arr].sort((a, b) => (a.echeance ?? '').localeCompare(b.echeance ?? ''))
return [
{ statut: 'en_cours', label: 'En cours', color: 'text-green', tasks: sort(all.filter(t => t.statut === 'en_cours')) },
{ statut: 'a_faire', label: 'À faire', color: 'text-aqua', tasks: sort(all.filter(t => t.statut === 'a_faire')) },
{ statut: 'fait', label: 'Terminées', color: 'text-text-muted', tasks: sort(all.filter(t => t.statut === 'fait')) },
].filter(g => g.tasks.length > 0)
})
const ganttTasks = computed(() => ganttGroups.value.flatMap(g => g.tasks))
/** Groupes de mois pour l'en-tête du Gantt */
const ganttMonthGroups = computed(() => {
const groups: { label: string; span: number }[] = []
let current = ''
let count = 0
for (const d of periodDays.value) {
const month = new Date(d.iso + 'T12:00:00').toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })
if (month !== current) {
if (current) groups.push({ label: current, span: count })
current = month
count = 1
} else {
count++
}
}
if (current) groups.push({ label: current, span: count })
return groups
})
function ganttBarVisible(t: (typeof store.tasks)[0]): boolean {
if (!t.echeance) return false
const end = dayOffset(t.echeance.slice(0, 10))
// Pour les tâches en cours, la barre commence aujourd'hui (ou au début de la période)
const start = t.statut === 'en_cours'
? Math.max(0, dayOffset(toIso(today)))
: end
return end >= 0 && start <= 27
}
function ganttBarStyle(t: (typeof store.tasks)[0]): Record<string, string> {
if (!t.echeance) return {}
const endOff = dayOffset(t.echeance.slice(0, 10))
let startOff: number
if (t.statut === 'en_cours') {
// Barre de aujourd'hui → échéance
startOff = Math.max(0, dayOffset(toIso(today)))
} else {
// Barre ponctuelle à l'échéance (largeur min 1 jour)
startOff = endOff
}
const clampStart = Math.max(0, startOff)
const clampEnd = Math.min(27, endOff)
if (clampStart > 27 || clampEnd < 0) return { display: 'none' }
const leftPct = (clampStart / 28) * 100
// Au moins 1 colonne de large (minimum visuel)
const spanDays = Math.max(1, clampEnd - clampStart + 1)
const widthPct = (spanDays / 28) * 100
return {
left: `${leftPct.toFixed(3)}%`,
width: `${widthPct.toFixed(3)}%`,
}
}
function ganttBarClass(t: (typeof store.tasks)[0]): string {
if (t.statut === 'fait') return 'bg-text-muted/30 border border-text-muted/20 text-text-muted/60'
const colors: Record<string, string> = {
haute: 'bg-red/60 border border-red/40 text-bg',
normale: 'bg-yellow/60 border border-yellow/40 text-bg',
basse: 'bg-blue/50 border border-blue/30 text-bg',
}
return colors[t.priorite as string] ?? 'bg-text-muted/40 border border-text-muted/30 text-bg'
}
// ── Helpers CSS ───────────────────────────────────────────────────────────────
const priorityBorderClass = (p: string) => ({
haute: 'border-red/30 text-red bg-red/5',
haute: 'border-red/30 text-red bg-red/5',
normale: 'border-yellow/30 text-yellow bg-yellow/5',
basse: 'border-bg-soft text-text-muted bg-bg-hard/50',
}[p] || 'border-bg-soft text-text-muted')
basse: 'border-bg-soft text-text-muted bg-bg-hard/50',
}[p] ?? 'border-bg-soft text-text-muted')
const dotClass = (p: string) => ({
haute: 'bg-red shadow-[0_0_5px_rgba(251,73,52,0.5)]',
normale: 'bg-yellow shadow-[0_0_5px_rgba(250,189,47,0.5)]',
basse: 'bg-text-muted',
}[p] || 'bg-text-muted')
haute: 'bg-red shadow-[0_0_5px_rgba(251,73,52,0.5)]',
normale: 'bg-yellow shadow-[0_0_5px_rgba(250,189,47,0.5)]',
basse: 'bg-text-muted',
}[p] ?? 'bg-text-muted')
const statutClass = (s: string) => ({
a_faire: 'badge-blue',
a_faire: 'badge-blue',
en_cours: 'badge-green',
fait: 'badge-text-muted',
}[s] || '')
fait: 'badge-text-muted',
}[s] ?? '')
// ── Init ──────────────────────────────────────────────────────────────────────
import { onMounted } from 'vue'
onMounted(() => store.fetchAll())
</script>