645 lines
28 KiB
Vue
645 lines
28 KiB
Vue
<template>
|
||
<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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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 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>
|
||
<input v-model="form.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>
|
||
<div>
|
||
<label class="text-text-muted text-xs block mb-1">Description</label>
|
||
<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>
|
||
<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 cursor-pointer">
|
||
<input v-model="form.repetition" type="checkbox" class="accent-green" />
|
||
Répétition périodique
|
||
</label>
|
||
<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" 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" :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 { 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'
|
||
|
||
// ── 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',
|
||
repetition: false,
|
||
freq_nb: 1 as number,
|
||
freq_unite: 'semaines' as 'jours' | 'semaines' | 'mois',
|
||
})
|
||
|
||
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é'],
|
||
]
|
||
|
||
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',
|
||
repetition: false, freq_nb: 1, freq_unite: 'semaines',
|
||
})
|
||
}
|
||
|
||
function openCreateTemplate() {
|
||
editId.value = null
|
||
resetForm()
|
||
showForm.value = true
|
||
}
|
||
|
||
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,
|
||
repetition: Boolean(t.recurrence || t.frequence_jours),
|
||
freq_nb: freq_nb || 1,
|
||
freq_unite,
|
||
})
|
||
showForm.value = true
|
||
}
|
||
|
||
function closeForm() {
|
||
showForm.value = false
|
||
editId.value = null
|
||
}
|
||
|
||
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 || undefined,
|
||
priorite: form.priorite,
|
||
statut: 'template',
|
||
recurrence: form.repetition ? 'jours' : null,
|
||
frequence_jours: freqJours,
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
// ── 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>
|