feat(planning): vue Gantt + toggle calendrier/gantt
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user