This commit is contained in:
2026-03-01 07:21:46 +01:00
parent 9db5cbf236
commit 7967f63fea
39 changed files with 3297 additions and 1646 deletions

View File

@@ -1,51 +1,157 @@
<template>
<div class="p-4 max-w-5xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-green"> Tâches</h1>
<button class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
@click="openCreateTemplate">+ Nouveau template</button>
<div class="p-4 max-w-6xl mx-auto space-y-8">
<!-- En-tête -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-green tracking-tight">Gestion des Tâches</h1>
<p class="text-text-muted text-xs mt-1">Organisez vos travaux au jardin avec des listes et des modèles.</p>
</div>
<button class="btn-primary flex items-center gap-2" @click="openCreateTemplate">
<span class="text-lg leading-none">+</span> Template
</button>
</div>
<div v-for="[groupe, label] in groupes" :key="groupe" class="mb-6">
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-2">{{ label }}</h2>
<div v-if="!byStatut(groupe).length" class="text-text-muted text-xs pl-2 mb-2"></div>
<div v-for="t in byStatut(groupe)" :key="t.id"
class="bg-bg-soft rounded-lg p-3 mb-2 flex items-center gap-3 border border-bg-hard">
<span :class="{
'text-red': t.priorite === 'haute',
'text-yellow': t.priorite === 'normale',
'text-text-muted': t.priorite === 'basse'
}"></span>
<div class="flex-1 min-w-0">
<div class="text-text text-sm">{{ t.titre }}</div>
<div v-if="t.description" class="text-text-muted text-xs">{{ t.description }}</div>
<div v-if="t.echeance && t.statut !== 'template'" class="text-text-muted text-xs">📅 {{ fmtDate(t.echeance) }}</div>
<div v-if="t.frequence_jours != null && t.frequence_jours > 0" class="text-text-muted text-xs">
🔁 Tous les {{ t.frequence_jours }} jours
<!-- Section "Créer rapidement" -->
<section class="bg-bg-soft/20 rounded-xl border border-bg-soft overflow-hidden">
<button
class="w-full px-5 py-3 flex items-center justify-between text-left hover:bg-bg-soft/30 transition-colors"
@click="showQuickSection = !showQuickSection"
>
<span class="text-text font-bold text-sm flex items-center gap-2">
<span class="text-yellow"></span> Créer rapidement
</span>
<span class="text-text-muted text-xs flex items-center gap-2">
<span>{{ totalTemplates }} template{{ totalTemplates > 1 ? 's' : '' }} disponible{{ totalTemplates > 1 ? 's' : '' }}</span>
<span :class="['transition-transform inline-block', showQuickSection ? 'rotate-180' : '']"></span>
</span>
</button>
<div v-show="showQuickSection" class="border-t border-bg-soft divide-y divide-bg-soft/50">
<!-- Mes templates personnalisés -->
<div v-if="byStatut('template').length" class="p-4 space-y-3">
<h3 class="text-text-muted text-[10px] font-bold uppercase tracking-widest">Mes templates</h3>
<div class="flex flex-wrap gap-2">
<button
v-for="t in byStatut('template')" :key="t.id"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm font-medium transition-all hover:scale-[1.02]"
:class="priorityChipClass(t.priorite)"
@click="openSchedule(t)"
>
<span>{{ priorityIcon(t.priorite) }}</span>
<span>{{ t.titre }}</span>
<span v-if="t.frequence_jours" class="text-[10px] opacity-70">🔁 {{ t.frequence_jours }}j</span>
<span class="ml-1 opacity-50 text-[10px]"> programmer</span>
</button>
</div>
<div v-if="t.planting_id && t.statut !== 'template'" class="text-text-muted text-xs">🌱 Plantation #{{ t.planting_id }}</div>
</div>
<div class="flex gap-1 items-center shrink-0">
<button v-if="t.statut === 'a_faire'" class="text-xs text-blue hover:underline"
@click="store.updateStatut(t.id!, 'en_cours')"> En cours</button>
<button v-if="t.statut === 'en_cours'" class="text-xs text-green hover:underline"
@click="store.updateStatut(t.id!, 'fait')"> Fait</button>
<button
v-if="t.statut === 'template'"
@click="startEdit(t)"
class="text-xs text-yellow hover:underline ml-2"
>
Édit.
</button>
<button class="text-xs text-text-muted hover:text-red ml-1" @click="store.remove(t.id!)"></button>
<!-- Tâches courantes prédéfinies -->
<div class="p-4 space-y-3">
<h3 class="text-text-muted text-[10px] font-bold uppercase tracking-widest">Tâches courantes</h3>
<div class="flex flex-wrap gap-2">
<button
v-for="qt in quickTemplatesFiltered" :key="qt.titre"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-bg-soft bg-bg text-text-muted text-sm hover:text-text hover:border-text-muted transition-all hover:scale-[1.02]"
@click="openScheduleQuick(qt)"
>
<span>{{ qt.icone }}</span>
<span>{{ qt.titre }}</span>
</button>
</div>
</div>
</div>
</section>
<!-- Kanban : À faire / En cours / Terminé -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div v-for="[groupe, label] in listGroupes" :key="groupe" class="space-y-3">
<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', groupeColor(groupe)]"></span>
{{ label }}
<span class="ml-auto bg-bg-soft px-2 py-0.5 rounded text-[10px]">{{ byStatut(groupe).length }}</span>
</h2>
<div v-if="!byStatut(groupe).length" class="card-jardin text-center py-8 opacity-30 border-dashed">
<p class="text-text-muted text-xs uppercase tracking-widest font-bold">Aucune tâche</p>
</div>
<div v-for="t in byStatut(groupe)" :key="t.id"
class="card-jardin flex items-center gap-4 group relative overflow-hidden">
<!-- Barre priorité -->
<div :class="[
'absolute left-0 top-0 bottom-0 w-1',
t.priorite === 'haute' ? 'bg-red' : t.priorite === 'normale' ? 'bg-yellow' : 'bg-bg-hard'
]"></div>
<div class="flex-1 min-w-0 pl-1">
<div class="flex items-center gap-2 mb-1">
<span class="text-text font-bold text-sm">{{ t.titre }}</span>
<span v-if="t.frequence_jours" class="badge badge-aqua !text-[8px]">🔁 {{ freqLabel(t.frequence_jours) }}</span>
</div>
<div v-if="t.description" class="text-text-muted text-xs line-clamp-1 mb-1 italic">{{ t.description }}</div>
<div class="flex flex-wrap gap-3 text-[10px] font-bold uppercase tracking-tighter text-text-muted opacity-70">
<span v-if="t.echeance" class="flex items-center gap-1">📅 {{ fmtDate(t.echeance) }}</span>
<span v-if="t.planting_id" class="flex items-center gap-1">🌱 {{ plantingShortLabel(t.planting_id) }}</span>
</div>
</div>
<div class="flex gap-2 items-center shrink-0">
<button v-if="t.statut === 'a_faire'" class="btn-outline !py-1 !px-2 text-blue border-blue/20 hover:bg-blue/10 text-[10px] font-bold uppercase"
@click="updateStatut(t.id!, 'en_cours')">Démarrer</button>
<button v-if="t.statut === 'en_cours'" class="btn-outline !py-1 !px-2 text-green border-green/20 hover:bg-green/10 text-[10px] font-bold uppercase"
@click="updateStatut(t.id!, 'fait')">Terminer</button>
<button @click="removeTask(t.id!)"
class="p-1.5 text-text-muted hover:text-red transition-colors opacity-0 group-hover:opacity-100"></button>
</div>
</div>
</div>
</div>
<!-- Modal création / édition -->
<!-- Bibliothèque de templates -->
<section v-if="byStatut('template').length" class="space-y-3">
<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-aqua"></span>
Bibliothèque de templates
<span class="ml-auto bg-bg-soft px-2 py-0.5 rounded text-[10px]">{{ byStatut('template').length }}</span>
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
<div v-for="t in byStatut('template')" :key="t.id"
class="card-jardin flex items-center gap-3 group relative overflow-hidden">
<div :class="[
'absolute left-0 top-0 bottom-0 w-1',
t.priorite === 'haute' ? 'bg-red' : t.priorite === 'normale' ? 'bg-yellow' : 'bg-bg-hard'
]"></div>
<div class="flex-1 min-w-0 pl-1">
<div class="flex items-center gap-2 mb-0.5">
<span class="text-text font-bold text-sm">{{ t.titre }}</span>
<span v-if="t.frequence_jours" class="badge badge-aqua !text-[8px]">🔁 {{ freqLabel(t.frequence_jours) }}</span>
</div>
<div v-if="t.description" class="text-text-muted text-xs line-clamp-1 italic">{{ t.description }}</div>
</div>
<div class="flex items-center gap-1 shrink-0">
<button
class="btn-outline !py-1 !px-2 text-aqua border-aqua/20 hover:bg-aqua/10 text-[10px] font-bold uppercase"
@click="openSchedule(t)"
>Programmer</button>
<button @click="startEdit(t)"
class="p-1.5 text-text-muted hover:text-yellow transition-colors opacity-0 group-hover:opacity-100"></button>
<button @click="removeTask(t.id!)"
class="p-1.5 text-text-muted hover:text-red transition-colors opacity-0 group-hover:opacity-100"></button>
</div>
</div>
</div>
</section>
<!-- Modal création / édition template -->
<div v-if="showForm" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft">
<h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier la tâche' : 'Nouveau template' }}</h2>
<h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier le template' : 'Nouveau template' }}</h2>
<form @submit.prevent="submit" class="grid gap-3">
<div>
<label class="text-text-muted text-xs block mb-1">Titre *</label>
@@ -54,93 +160,328 @@
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Description</label>
<textarea v-model="form.description" rows="2"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none resize-none" />
<textarea v-model="form.description" rows="1"
@input="autoResize"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none resize-none transition-all overflow-hidden" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-text-muted text-xs block mb-1">Priorité</label>
<select v-model="form.priorite" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
<option value="basse">Basse</option>
<option value="normale">Normale</option>
<option value="haute">Haute</option>
</select>
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Type</label>
<input value="Template" readonly
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text-muted text-sm" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Priorité</label>
<select v-model="form.priorite" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
<option value="basse">Basse</option>
<option value="normale">Normale</option>
<option value="haute">Haute</option>
</select>
</div>
<div class="bg-bg rounded border border-bg-hard p-3">
<label class="inline-flex items-center gap-2 text-sm text-text">
<label class="inline-flex items-center gap-2 text-sm text-text cursor-pointer">
<input v-model="form.repetition" type="checkbox" class="accent-green" />
Répétition
Répétition périodique
</label>
<p class="text-text-muted text-[11px] mt-1">Fréquence proposée quand la tâche est ajoutée depuis une plantation.</p>
<p class="text-text-muted text-[11px] mt-1">Fréquence proposée quand la tâche est programmée depuis un template.</p>
</div>
<div v-if="form.repetition">
<label class="text-text-muted text-xs block mb-1">Fréquence (jours)</label>
<input
v-model.number="form.frequence_jours"
type="number"
min="1"
step="1"
required
placeholder="Ex: 7"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none"
/>
<div v-if="form.repetition" class="flex gap-2 items-center">
<input v-model.number="form.freq_nb" type="number" min="1" max="99" required placeholder="1"
class="w-20 bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none font-mono" />
<select v-model="form.freq_unite" class="flex-1 bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
<option value="jours">Jour(s)</option>
<option value="semaines">Semaine(s)</option>
<option value="mois">Mois</option>
</select>
<span class="text-text-muted text-xs whitespace-nowrap">= {{ formFreqEnJours }} j</span>
</div>
<div class="flex gap-2 mt-2">
<button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold">
{{ editId ? 'Enregistrer' : 'Créer le template' }}
<button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold" :disabled="submitting">
{{ submitting ? 'Enregistrement…' : (editId ? 'Enregistrer' : 'Créer le template') }}
</button>
<button type="button" class="text-text-muted text-sm px-4 py-2 hover:text-text" @click="closeForm">Annuler</button>
</div>
</form>
</div>
</div>
<!-- Modal "Programmer une tâche" -->
<div v-if="showScheduleModal" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeSchedule">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-sm border border-bg-soft max-h-[90vh] overflow-y-auto">
<h2 class="text-text font-bold text-lg mb-1">Programmer une tâche</h2>
<p class="text-text-muted text-xs mb-5 italic">
Crée une tâche <span class="text-blue font-bold">À faire</span> depuis ce template.
</p>
<form @submit.prevent="createScheduled" class="grid gap-3">
<!-- Titre -->
<div>
<label class="text-text-muted text-xs block mb-1">Titre *</label>
<input v-model="scheduleForm.titre" required
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
</div>
<!-- Date de démarrage -->
<div>
<label class="text-text-muted text-xs block mb-1">
Date de démarrage
<span class="opacity-50">(par défaut : aujourd'hui)</span>
</label>
<input v-model="scheduleForm.date_debut" type="date"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
</div>
<!-- Priorité -->
<div>
<label class="text-text-muted text-xs block mb-1">Priorité</label>
<select v-model="scheduleForm.priorite" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
<option value="basse">⚪ Basse</option>
<option value="normale">🟡 Normale</option>
<option value="haute">🔴 Haute</option>
</select>
</div>
<!-- Plantation liée -->
<div>
<label class="text-text-muted text-xs block mb-1">
Plantation liée
<span class="opacity-50">(optionnel)</span>
</label>
<select v-model="scheduleForm.planting_id"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none">
<option :value="null">— Aucune plantation —</option>
<optgroup v-for="g in plantingsByGarden" :key="g.gardenId" :label="g.gardenName">
<option v-for="p in g.plantings" :key="p.id" :value="p.id">
{{ plantingLabel(p) }}
</option>
</optgroup>
</select>
</div>
<!-- Répétition -->
<div class="bg-bg rounded border border-bg-hard p-3 space-y-2">
<label class="inline-flex items-center gap-2 text-sm text-text cursor-pointer">
<input v-model="scheduleForm.repetition" type="checkbox" class="accent-green" />
Répétition périodique
</label>
<div v-if="scheduleForm.repetition" class="flex gap-2 items-center pt-1">
<span class="text-text-muted text-xs shrink-0">Tous les</span>
<input v-model.number="scheduleForm.freq_nb" type="number" min="1" max="99"
class="w-16 bg-bg-soft border border-bg-soft rounded px-2 py-1.5 text-text text-sm focus:border-green outline-none font-mono text-center" />
<select v-model="scheduleForm.freq_unite"
class="flex-1 bg-bg-soft border border-bg-soft rounded px-2 py-1.5 text-text text-sm focus:border-green outline-none">
<option value="jours">Jour(s)</option>
<option value="semaines">Semaine(s)</option>
<option value="mois">Mois</option>
</select>
</div>
<p v-if="scheduleForm.repetition" class="text-text-muted text-[11px]">
→ Récurrence de {{ scheduleFreqEnJours }} jour{{ scheduleFreqEnJours > 1 ? 's' : '' }}
</p>
</div>
<!-- Notes -->
<div>
<label class="text-text-muted text-xs block mb-1">Notes <span class="opacity-50">(optionnel)</span></label>
<textarea v-model="scheduleForm.notes" rows="2" placeholder="Précisions sur cette occurrence…"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none resize-none" />
</div>
<div class="flex gap-2 mt-1">
<button type="submit" class="bg-blue text-bg px-4 py-2 rounded text-sm font-semibold flex-1" :disabled="submitting">
{{ submitting ? 'Création' : 'Créer la tâche' }}
</button>
<button type="button" class="text-text-muted text-sm px-4 py-2 hover:text-text" @click="closeSchedule">Annuler</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { useTasksStore } from '@/stores/tasks'
import { usePlantingsStore } from '@/stores/plantings'
import { usePlantsStore } from '@/stores/plants'
import { useGardensStore } from '@/stores/gardens'
import type { Task } from '@/api/tasks'
import type { Planting } from '@/api/plantings'
import { useToast } from '@/composables/useToast'
const store = useTasksStore()
const showForm = ref(false)
const editId = ref<number | null>(null)
// ── Types ──────────────────────────────────────────────────────────────────────
interface QuickTemplate {
titre: string
icone: string
priorite: string
description?: string
}
// ── Stores & composables ───────────────────────────────────────────────────────
const store = useTasksStore()
const plantingsStore = usePlantingsStore()
const plantsStore = usePlantsStore()
const gardensStore = useGardensStore()
const toast = useToast()
// ── État UI ────────────────────────────────────────────────────────────────────
const showForm = ref(false)
const showScheduleModal = ref(false)
const showQuickSection = ref(true)
const editId = ref<number | null>(null)
const submitting = ref(false)
// ── Formulaire template (création / édition) ───────────────────────────────────
const form = reactive({
titre: '',
description: '',
priorite: 'normale',
statut: 'template',
repetition: false,
frequence_jours: undefined as number | undefined,
freq_nb: 1 as number,
freq_unite: 'semaines' as 'jours' | 'semaines' | 'mois',
})
const groupes: [string, string][] = [
const formFreqEnJours = computed(() => {
const n = form.freq_nb || 1
if (form.freq_unite === 'semaines') return n * 7
if (form.freq_unite === 'mois') return n * 30
return n
})
// ── Formulaire "programmer" (instancier un template) ──────────────────────────
const scheduleForm = reactive({
titre: '',
date_debut: today(),
notes: '',
priorite: 'normale',
planting_id: null as number | null,
repetition: false,
freq_nb: 1 as number,
freq_unite: 'semaines' as 'jours' | 'semaines' | 'mois',
})
const scheduleFreqEnJours = computed(() => {
const n = scheduleForm.freq_nb || 1
if (scheduleForm.freq_unite === 'semaines') return n * 7
if (scheduleForm.freq_unite === 'mois') return n * 30
return n
})
// ── Templates prédéfinis jardinage ─────────────────────────────────────────────
const QUICK_TEMPLATES: QuickTemplate[] = [
{ titre: 'Arrosage', icone: '💧', priorite: 'normale' },
{ titre: 'Semis en intérieur', icone: '🌱', priorite: 'normale' },
{ titre: 'Semis en pleine terre', icone: '🌾', priorite: 'normale' },
{ titre: 'Repiquage / Transplantation',icone: '🪴', priorite: 'normale' },
{ titre: 'Récolte', icone: '🥕', priorite: 'normale' },
{ titre: 'Taille / Ébourgeonnage', icone: '', priorite: 'normale' },
{ titre: 'Désherbage', icone: '🌿', priorite: 'basse' },
{ titre: 'Fertilisation / Amendement', icone: '💊', priorite: 'normale' },
{ titre: 'Traitement phytosanitaire', icone: '🧪', priorite: 'haute' },
{ titre: 'Observation / Relevé', icone: '👁', priorite: 'basse' },
{ titre: 'Paillage', icone: '🍂', priorite: 'basse' },
{ titre: 'Compostage', icone: '', priorite: 'basse' },
{ titre: 'Buttage', icone: '', priorite: 'normale' },
{ titre: 'Protection gel / Voile', icone: '🌡', priorite: 'haute' },
{ titre: 'Tuteurage', icone: '🪵', priorite: 'normale' },
{ titre: 'Éclaircissage', icone: '🌞', priorite: 'normale' },
]
const quickTemplatesFiltered = computed(() => {
const existing = new Set(byStatut('template').map(t => t.titre.toLowerCase().trim()))
return QUICK_TEMPLATES.filter(qt => !existing.has(qt.titre.toLowerCase().trim()))
})
const totalTemplates = computed(
() => byStatut('template').length + quickTemplatesFiltered.value.length
)
// ── Plantations groupées par jardin pour le <optgroup> ─────────────────────────
const plantingsByGarden = computed(() => {
const gardens = gardensStore.gardens
const plantings = plantingsStore.plantings.filter(p => p.statut !== 'termine' && p.statut !== 'echoue')
const groups: { gardenId: number; gardenName: string; plantings: Planting[] }[] = []
for (const g of gardens) {
const gPlantings = plantings.filter(p => p.garden_id === g.id)
if (gPlantings.length) {
groups.push({ gardenId: g.id!, gardenName: g.nom, plantings: gPlantings })
}
}
// Plantations sans jardin reconnu
const knownGardenIds = new Set(gardens.map(g => g.id))
const orphans = plantings.filter(p => !knownGardenIds.has(p.garden_id))
if (orphans.length) {
groups.push({ gardenId: 0, gardenName: 'Autres', plantings: orphans })
}
return groups
})
function plantingLabel(p: Planting): string {
const plant = plantsStore.plants.find(pl => pl.id === p.variety_id)
const nom = plant
? [plant.nom_commun, plant.variete].filter(Boolean).join(' ')
: `Variété #${p.variety_id}`
const date = p.date_plantation ? ` (${fmtDate(p.date_plantation)})` : ''
return `${nom}${date}`
}
function plantingShortLabel(id: number): string {
const p = plantingsStore.plantings.find(x => x.id === id)
if (!p) return `#${id}`
const plant = plantsStore.plants.find(pl => pl.id === p.variety_id)
return plant?.nom_commun ?? `#${id}`
}
// ── Groupes Kanban ─────────────────────────────────────────────────────────────
const listGroupes: [string, string][] = [
['a_faire', 'À faire'],
['en_cours', 'En cours'],
['fait', 'Terminé'],
['template', 'Templates'],
]
function groupeColor(g: string) {
const map: Record<string, string> = { a_faire: 'bg-blue', en_cours: 'bg-yellow', fait: 'bg-green' }
return map[g] ?? 'bg-bg-hard'
}
const byStatut = (s: string) => store.tasks.filter(t => t.statut === s)
// ── Helpers ────────────────────────────────────────────────────────────────────
function today() {
return new Date().toISOString().slice(0, 10)
}
function fmtDate(s: string) {
return new Date(s + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })
}
function freqLabel(jours: number): string {
if (jours % 30 === 0 && jours >= 30) return `${jours / 30}mois`
if (jours % 7 === 0 && jours >= 7) return `${jours / 7}sem`
return `${jours}j`
}
function priorityIcon(p: string) {
return { haute: '🔴', normale: '🟡', basse: '' }[p] ?? ''
}
function priorityChipClass(p: string) {
const map: Record<string, string> = {
haute: 'border-red/30 bg-red/10 text-red hover:bg-red/20',
normale: 'border-yellow/30 bg-yellow/10 text-yellow hover:bg-yellow/20',
basse: 'border-bg-soft bg-bg text-text-muted hover:bg-bg-soft',
}
return map[p] ?? map.basse
}
function autoResize(event: Event) {
const el = event.target as HTMLTextAreaElement
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}
// ── Gestion template (form création/édition) ───────────────────────────────────
function resetForm() {
Object.assign(form, {
titre: '',
description: '',
priorite: 'normale',
statut: 'template',
repetition: false,
frequence_jours: undefined,
titre: '', description: '', priorite: 'normale',
repetition: false, freq_nb: 1, freq_unite: 'semaines',
})
}
@@ -152,13 +493,19 @@ function openCreateTemplate() {
function startEdit(t: Task) {
editId.value = t.id!
const jours = t.frequence_jours ?? 0
let freq_nb = jours
let freq_unite: 'jours' | 'semaines' | 'mois' = 'jours'
if (jours >= 30 && jours % 30 === 0) { freq_nb = jours / 30; freq_unite = 'mois' }
else if (jours >= 7 && jours % 7 === 0) { freq_nb = jours / 7; freq_unite = 'semaines' }
Object.assign(form, {
titre: t.titre,
description: t.description || '',
priorite: t.priorite,
statut: t.statut || 'template',
repetition: Boolean(t.recurrence || t.frequence_jours),
frequence_jours: t.frequence_jours ?? undefined,
freq_nb: freq_nb || 1,
freq_unite,
})
showForm.value = true
}
@@ -168,25 +515,130 @@ function closeForm() {
editId.value = null
}
onMounted(() => store.fetchAll())
async function submit() {
if (submitting.value) return
submitting.value = true
const freqJours = form.repetition ? formFreqEnJours.value : null
const payload = {
titre: form.titre,
description: form.description,
description: form.description || undefined,
priorite: form.priorite,
statut: 'template',
recurrence: form.repetition ? 'jours' : null,
frequence_jours: form.repetition ? (form.frequence_jours ?? 7) : null,
echeance: undefined,
planting_id: undefined,
frequence_jours: freqJours,
}
if (editId.value) {
await store.update(editId.value, payload)
} else {
await store.create(payload)
try {
if (editId.value) {
await store.update(editId.value, payload)
toast.success('Template modifié')
} else {
await store.create(payload)
toast.success('Template créé')
}
closeForm()
resetForm()
} catch {
// L'intercepteur Axios affiche le message
} finally {
submitting.value = false
}
closeForm()
resetForm()
}
// ── Programmer une tâche depuis un template ────────────────────────────────────
function resetScheduleForm() {
Object.assign(scheduleForm, {
titre: '',
date_debut: today(),
notes: '',
priorite: 'normale',
planting_id: null,
repetition: false,
freq_nb: 1,
freq_unite: 'semaines',
})
}
function openSchedule(t: Task) {
resetScheduleForm()
const jours = t.frequence_jours ?? 0
let freq_nb = jours
let freq_unite: 'jours' | 'semaines' | 'mois' = 'jours'
if (jours >= 30 && jours % 30 === 0) { freq_nb = jours / 30; freq_unite = 'mois' }
else if (jours >= 7 && jours % 7 === 0) { freq_nb = jours / 7; freq_unite = 'semaines' }
Object.assign(scheduleForm, {
titre: t.titre,
notes: t.description || '',
priorite: t.priorite,
repetition: Boolean(t.frequence_jours),
freq_nb: freq_nb || 1,
freq_unite,
})
showScheduleModal.value = true
}
function openScheduleQuick(qt: QuickTemplate) {
resetScheduleForm()
Object.assign(scheduleForm, {
titre: `${qt.icone} ${qt.titre}`,
priorite: qt.priorite,
})
showScheduleModal.value = true
}
function closeSchedule() {
showScheduleModal.value = false
}
async function createScheduled() {
if (submitting.value) return
submitting.value = true
const freqJours = scheduleForm.repetition ? scheduleFreqEnJours.value : null
try {
await store.create({
titre: scheduleForm.titre,
description: scheduleForm.notes || undefined,
priorite: scheduleForm.priorite,
statut: 'a_faire',
echeance: scheduleForm.date_debut || today(),
planting_id: scheduleForm.planting_id ?? undefined,
recurrence: scheduleForm.repetition ? 'jours' : null,
frequence_jours: freqJours,
})
toast.success(`"${scheduleForm.titre}" ajoutée aux tâches à faire`)
closeSchedule()
} catch {
// L'intercepteur Axios affiche le message
} finally {
submitting.value = false
}
}
// ── Actions Kanban ─────────────────────────────────────────────────────────────
async function updateStatut(id: number, statut: string) {
try {
await store.updateStatut(id, statut)
} catch { /* L'intercepteur Axios affiche le message */ }
}
async function removeTask(id: number) {
try {
await store.remove(id)
toast.success('Tâche supprimée')
} catch { /* L'intercepteur Axios affiche le message */ }
}
// ── Initialisation ─────────────────────────────────────────────────────────────
onMounted(async () => {
try {
await Promise.all([
store.fetchAll(),
plantingsStore.fetchAll(),
plantsStore.fetchAll(),
gardensStore.fetchAll(),
])
} catch {
toast.error('Impossible de charger les données')
}
})
</script>