avant 50
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user