259 lines
11 KiB
Vue
259 lines
11 KiB
Vue
<template>
|
|
<div class="p-4 max-w-3xl mx-auto">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h1 class="text-2xl font-bold text-green">🌱 Plantations</h1>
|
|
<button @click="showCreate = true"
|
|
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
|
|
+ Nouvelle
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filtres statut -->
|
|
<div class="flex gap-2 mb-4 flex-wrap">
|
|
<button v-for="s in statuts" :key="s.val" @click="filterStatut = s.val"
|
|
:class="['px-3 py-1 rounded-full text-xs font-medium transition-colors',
|
|
filterStatut === s.val ? 'bg-blue text-bg' : 'bg-bg-soft text-text-muted hover:text-text']">
|
|
{{ s.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="store.loading" class="text-text-muted text-sm">Chargement...</div>
|
|
<div v-else-if="!filtered.length" class="text-text-muted text-sm text-center py-8">
|
|
Aucune plantation enregistrée.
|
|
</div>
|
|
|
|
<div v-for="p in filtered" :key="p.id"
|
|
class="bg-bg-soft rounded-xl mb-3 border border-bg-hard overflow-hidden">
|
|
<!-- En-tête plantation -->
|
|
<div class="p-4 flex items-start gap-3">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="text-text font-semibold">{{ plantName(p.variety_id) }}</span>
|
|
<span class="text-text-muted text-xs">— {{ gardenName(p.garden_id) }}</span>
|
|
<span :class="['text-xs px-2 py-0.5 rounded-full font-medium', statutClass(p.statut)]">{{ p.statut }}</span>
|
|
</div>
|
|
<div class="text-text-muted text-xs mt-1 flex gap-3 flex-wrap">
|
|
<span>{{ p.quantite }} plant(s)</span>
|
|
<span v-if="p.date_plantation">🌱 {{ fmtDate(p.date_plantation) }}</span>
|
|
<span v-if="p.notes">📝 {{ p.notes }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 items-center">
|
|
<button @click="toggleRecoltes(p.id!)"
|
|
:class="['text-xs px-2 py-1 rounded transition-colors',
|
|
openRecoltes === p.id ? 'bg-aqua/20 text-aqua' : 'bg-bg-hard text-text-muted hover:text-aqua']">
|
|
🍅 Récoltes
|
|
</button>
|
|
<button @click="startEdit(p)" class="text-yellow text-xs hover:underline">Édit.</button>
|
|
<button @click="store.remove(p.id!)" class="text-text-muted hover:text-red text-sm ml-1">✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section récoltes (dépliable) -->
|
|
<div v-if="openRecoltes === p.id" class="border-t border-bg-hard px-4 py-3 bg-bg/50">
|
|
<div v-if="loadingRecoltes" class="text-text-muted text-xs py-2">Chargement...</div>
|
|
<div v-else>
|
|
<div v-if="!recoltesList.length" class="text-text-muted text-xs mb-2">Aucune récolte enregistrée.</div>
|
|
<div v-for="r in recoltesList" :key="r.id"
|
|
class="flex items-center gap-3 text-sm py-1 border-b border-bg-hard last:border-0">
|
|
<span class="text-aqua font-mono">{{ r.quantite }} {{ r.unite }}</span>
|
|
<span class="text-text-muted text-xs">{{ fmtDate(r.date_recolte) }}</span>
|
|
<span v-if="r.notes" class="text-text-muted text-xs flex-1 truncate">{{ r.notes }}</span>
|
|
<button @click="deleteRecolte(r.id!, p.id!)" class="text-text-muted hover:text-red text-xs ml-auto">✕</button>
|
|
</div>
|
|
|
|
<!-- Formulaire ajout récolte -->
|
|
<form @submit.prevent="addRecolte(p.id!)" class="flex gap-2 mt-3 flex-wrap items-end">
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Quantité *</label>
|
|
<input v-model.number="rForm.quantite" type="number" step="0.1" min="0" required
|
|
class="bg-bg border border-bg-hard rounded px-2 py-1 text-text text-xs w-20 focus:border-aqua outline-none" />
|
|
</div>
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Unité</label>
|
|
<select v-model="rForm.unite"
|
|
class="bg-bg border border-bg-hard rounded px-2 py-1 text-text text-xs focus:border-aqua outline-none">
|
|
<option>kg</option><option>g</option><option>unites</option><option>litres</option><option>bottes</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Date *</label>
|
|
<input v-model="rForm.date_recolte" type="date" required
|
|
class="bg-bg border border-bg-hard rounded px-2 py-1 text-text text-xs focus:border-aqua outline-none" />
|
|
</div>
|
|
<button type="submit"
|
|
class="bg-aqua text-bg px-3 py-1 rounded text-xs font-semibold hover:opacity-90 self-end">
|
|
+ Ajouter
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal création / édition plantation -->
|
|
<div v-if="showCreate" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
|
|
@click.self="closeCreate">
|
|
<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 plantation' : 'Nouvelle plantation' }}</h2>
|
|
<form @submit.prevent="createPlanting" class="flex flex-col gap-3">
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Jardin *</label>
|
|
<select v-model.number="cForm.garden_id" required
|
|
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green">
|
|
<option value="">Choisir un jardin</option>
|
|
<option v-for="g in gardensStore.gardens" :key="g.id" :value="g.id">{{ g.nom }}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Plante *</label>
|
|
<select v-model.number="cForm.variety_id" required
|
|
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green">
|
|
<option value="">Choisir une plante</option>
|
|
<option v-for="p in plantsStore.plants" :key="p.id" :value="p.id">
|
|
{{ p.nom_commun }}{{ p.variete ? ' — ' + p.variete : '' }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Quantité</label>
|
|
<input v-model.number="cForm.quantite" type="number" min="1"
|
|
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
|
|
</div>
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Date plantation</label>
|
|
<input v-model="cForm.date_plantation" type="date"
|
|
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="text-text-muted text-xs block mb-1">Statut</label>
|
|
<select v-model="cForm.statut"
|
|
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green">
|
|
<option value="prevu">Prévu</option>
|
|
<option value="en_cours">En cours</option>
|
|
<option value="termine">Terminé</option>
|
|
</select>
|
|
</div>
|
|
<textarea v-model="cForm.notes" placeholder="Notes..."
|
|
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green resize-none h-16" />
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" @click="closeCreate" class="px-4 py-2 text-text-muted hover:text-text text-sm">Annuler</button>
|
|
<button type="submit" class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
|
|
{{ editId ? 'Enregistrer' : 'Créer' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { usePlantingsStore } from '@/stores/plantings'
|
|
import { useGardensStore } from '@/stores/gardens'
|
|
import { usePlantsStore } from '@/stores/plants'
|
|
import { recoltesApi, type Recolte } from '@/api/recoltes'
|
|
|
|
const store = usePlantingsStore()
|
|
const gardensStore = useGardensStore()
|
|
const plantsStore = usePlantsStore()
|
|
|
|
const showCreate = ref(false)
|
|
const editId = ref<number | null>(null)
|
|
const filterStatut = ref('')
|
|
const openRecoltes = ref<number | null>(null)
|
|
const recoltesList = ref<Recolte[]>([])
|
|
const loadingRecoltes = ref(false)
|
|
|
|
const statuts = [
|
|
{ val: '', label: 'Toutes' },
|
|
{ val: 'prevu', label: '📋 Prévu' },
|
|
{ val: 'en_cours', label: '🌿 En cours' },
|
|
{ val: 'termine', label: '✅ Terminé' },
|
|
{ val: 'echoue', label: '❌ Échoué' },
|
|
]
|
|
|
|
const cForm = reactive({
|
|
garden_id: 0, variety_id: 0, quantite: 1,
|
|
date_plantation: '', statut: 'prevu', notes: ''
|
|
})
|
|
|
|
const rForm = reactive({
|
|
quantite: 1, unite: 'kg', date_recolte: new Date().toISOString().slice(0, 10)
|
|
})
|
|
|
|
const filtered = computed(() =>
|
|
filterStatut.value ? store.plantings.filter(p => p.statut === filterStatut.value) : store.plantings
|
|
)
|
|
|
|
function plantName(id: number) {
|
|
const p = plantsStore.plants.find(x => x.id === id)
|
|
return p ? (p.variete ? `${p.nom_commun} (${p.variete})` : p.nom_commun) : `Plante #${id}`
|
|
}
|
|
|
|
function gardenName(id: number) {
|
|
return gardensStore.gardens.find(g => g.id === id)?.nom ?? `Jardin #${id}`
|
|
}
|
|
|
|
function fmtDate(s: string) {
|
|
return new Date(s + (s.length === 10 ? 'T12:00:00' : '')).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })
|
|
}
|
|
|
|
const statutClass = (s: string) => ({
|
|
prevu: 'bg-blue/20 text-blue',
|
|
en_cours: 'bg-green/20 text-green',
|
|
termine: 'bg-text-muted/20 text-text-muted',
|
|
echoue: 'bg-red/20 text-red',
|
|
}[s] || 'bg-bg text-text-muted')
|
|
|
|
async function toggleRecoltes(id: number) {
|
|
if (openRecoltes.value === id) { openRecoltes.value = null; return }
|
|
openRecoltes.value = id
|
|
loadingRecoltes.value = true
|
|
try { recoltesList.value = await recoltesApi.list(id) } catch { recoltesList.value = [] }
|
|
finally { loadingRecoltes.value = false }
|
|
}
|
|
|
|
async function addRecolte(plantingId: number) {
|
|
const created = await recoltesApi.create(plantingId, { ...rForm })
|
|
recoltesList.value.push(created)
|
|
Object.assign(rForm, { quantite: 1, unite: 'kg', date_recolte: new Date().toISOString().slice(0, 10) })
|
|
}
|
|
|
|
async function deleteRecolte(id: number, plantingId: number) {
|
|
if (!confirm('Supprimer cette récolte ?')) return
|
|
await recoltesApi.delete(id)
|
|
recoltesList.value = recoltesList.value.filter(r => r.id !== id)
|
|
}
|
|
|
|
function startEdit(p: typeof store.plantings[0]) {
|
|
editId.value = p.id!
|
|
Object.assign(cForm, {
|
|
garden_id: p.garden_id, variety_id: p.variety_id,
|
|
quantite: p.quantite, date_plantation: p.date_plantation?.slice(0, 10) || '',
|
|
statut: p.statut, notes: p.notes || '',
|
|
})
|
|
showCreate.value = true
|
|
}
|
|
|
|
function closeCreate() { showCreate.value = false; editId.value = null }
|
|
|
|
async function createPlanting() {
|
|
if (editId.value) {
|
|
await store.update(editId.value, { ...cForm })
|
|
} else {
|
|
await store.create({ ...cForm })
|
|
}
|
|
closeCreate()
|
|
Object.assign(cForm, { garden_id: 0, variety_id: 0, quantite: 1, date_plantation: '', statut: 'prevu', notes: '' })
|
|
}
|
|
|
|
onMounted(() => {
|
|
store.fetchAll()
|
|
gardensStore.fetchAll()
|
|
plantsStore.fetchAll()
|
|
})
|
|
</script>
|