feat(intrants): IntratsView with Achats + Fabrications tabs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 13:24:26 +01:00
parent 80173171b3
commit f8e64d6a2c

View File

@@ -0,0 +1,643 @@
<!-- frontend/src/views/IntratsView.vue -->
<template>
<div class="p-4 max-w-[1800px] mx-auto space-y-6">
<!-- En-tête -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<h1 class="text-3xl font-bold text-yellow tracking-tight">🧪 Intrants</h1>
<!-- Onglets -->
<div class="flex items-center gap-2 bg-bg-hard rounded-xl p-1 border border-bg-soft">
<button v-for="tab in tabs" :key="tab.key"
@click="activeTab = tab.key"
:class="['px-4 py-2 rounded-lg text-sm font-bold transition-all',
activeTab === tab.key ? 'bg-yellow text-bg' : 'text-text-muted hover:text-text']">
{{ tab.label }}
</button>
</div>
<button @click="openCreateForm"
class="btn-primary !bg-yellow !text-bg flex items-center gap-2 rounded-lg py-2 px-4 shadow-lg hover:scale-105 transition-all font-bold">
<span class="text-lg">+</span> {{ activeTab === 'achats' ? 'Ajouter un achat' : 'Nouvelle fabrication' }}
</button>
</div>
<!-- ====== ONGLET ACHATS ====== -->
<div v-if="activeTab === 'achats'">
<!-- Filtres -->
<div class="flex flex-wrap gap-2 mb-4">
<button v-for="cat in categoriesAchat" :key="cat.val"
@click="filterCat = cat.val"
:class="['px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all border',
filterCat === cat.val ? 'bg-yellow text-bg border-yellow' : 'bg-bg-hard text-text-muted border-bg-soft hover:text-text']">
{{ cat.label }}
</button>
</div>
<!-- Grille achats -->
<div v-if="achatsStore.loading" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div v-for="i in 6" :key="i" class="card-jardin h-32 animate-pulse opacity-20"></div>
</div>
<div v-else-if="filteredAchats.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div v-for="a in filteredAchats" :key="a.id"
class="card-jardin !p-0 overflow-hidden flex flex-col hover:border-yellow/40 transition-all border-l-[6px] cursor-pointer"
:style="{ borderLeftColor: catAchatColor(a.categorie) }"
@click="openDetailAchat(a)">
<div class="p-4 flex-1">
<div class="flex items-start justify-between gap-2 mb-2">
<div>
<span :class="['text-[8px] font-black uppercase tracking-widest px-1.5 py-0.5 rounded', catAchatTextClass(a.categorie)]">
{{ a.categorie }}
</span>
<h3 class="text-text font-bold text-lg mt-1 leading-tight">{{ a.nom }}</h3>
<p v-if="a.marque" class="text-text-muted text-xs">{{ a.marque }}</p>
</div>
<span v-if="a.prix" class="text-yellow font-black text-lg shrink-0">{{ a.prix.toFixed(2) }}</span>
</div>
<div class="flex flex-wrap gap-2 mt-3">
<span v-if="a.boutique_nom" class="text-[10px] bg-bg/40 px-2 py-0.5 rounded border border-bg-soft text-text-muted">
🛒 {{ a.boutique_nom }}
</span>
<span v-if="a.poids" class="text-[10px] bg-bg/40 px-2 py-0.5 rounded border border-bg-soft text-text-muted">
{{ a.poids }}
</span>
<span v-if="a.dluo" :class="['text-[10px] px-2 py-0.5 rounded border',
isDluoExpired(a.dluo) ? 'bg-red/10 border-red/40 text-red' : 'bg-bg/40 border-bg-soft text-text-muted']">
📅 DLUO: {{ a.dluo }}{{ isDluoExpired(a.dluo) ? ' ⚠️' : '' }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-16 text-text-muted/40 italic">
Aucun achat enregistré. Commencez par ajouter un terreau, engrais ou traitement.
</div>
</div>
<!-- ====== ONGLET FABRICATIONS ====== -->
<div v-if="activeTab === 'fabrications'">
<!-- Filtres -->
<div class="flex flex-wrap gap-2 mb-4">
<button v-for="t in typesFabrication" :key="t.val"
@click="filterType = t.val"
:class="['px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all border',
filterType === t.val ? 'bg-yellow text-bg border-yellow' : 'bg-bg-hard text-text-muted border-bg-soft hover:text-text']">
{{ t.label }}
</button>
<div class="w-px bg-bg-soft mx-1"></div>
<button v-for="s in statutsFabrication" :key="s.val"
@click="filterStatut = s.val"
:class="['px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all border',
filterStatut === s.val ? `${s.bgClass} text-bg border-transparent` : 'bg-bg-hard text-text-muted border-bg-soft hover:text-text']">
{{ s.label }}
</button>
</div>
<!-- Grille fabrications -->
<div v-if="fabricationsStore.loading" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div v-for="i in 4" :key="i" class="card-jardin h-40 animate-pulse opacity-20"></div>
</div>
<div v-else-if="filteredFabricatons.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div v-for="f in filteredFabricatons" :key="f.id"
class="card-jardin !p-0 overflow-hidden flex flex-col hover:border-yellow/40 transition-all border-l-[6px] cursor-pointer"
:style="{ borderLeftColor: statutColor(f.statut || 'en_cours') }"
@click="openDetailFabrication(f)">
<div class="p-4 flex-1">
<div class="flex items-start justify-between gap-2 mb-2">
<div class="flex-1 min-w-0">
<span :class="['text-[8px] font-black uppercase tracking-widest px-1.5 py-0.5 rounded', typeFabTextClass(f.type)]">
{{ f.type }}
</span>
<h3 class="text-text font-bold text-lg mt-1 leading-tight">{{ f.nom }}</h3>
</div>
<span :class="['text-[9px] font-black px-2 py-0.5 rounded-full border shrink-0', statutBadgeClass(f.statut || 'en_cours')]">
{{ statutLabel(f.statut || 'en_cours') }}
</span>
</div>
<!-- Ingrédients résumé -->
<p v-if="f.ingredients?.length" class="text-text-muted text-[10px] mb-2 truncate">
🌿 {{ f.ingredients.map(i => i.nom).join(', ') }}
</p>
<div class="flex flex-wrap gap-2">
<span v-if="f.date_fin_prevue" class="text-[10px] bg-bg/40 px-2 py-0.5 rounded border border-bg-soft text-text-muted">
📅 Prêt le {{ f.date_fin_prevue }}
</span>
<span v-if="f.quantite_produite" class="text-[10px] bg-bg/40 px-2 py-0.5 rounded border border-bg-soft text-text-muted">
{{ f.quantite_produite }}
</span>
</div>
</div>
<!-- Boutons rapides statut -->
<div v-if="f.statut === 'en_cours'" class="flex border-t border-bg-soft">
<button @click.stop="quickStatut(f, 'pret')"
class="flex-1 py-2 text-[10px] font-black text-green hover:bg-green/10 transition-colors">
Prêt
</button>
<div class="w-px bg-bg-soft"></div>
<button @click.stop="quickStatut(f, 'echec')"
class="flex-1 py-2 text-[10px] font-black text-red hover:bg-red/10 transition-colors">
Échec
</button>
</div>
</div>
</div>
<div v-else class="text-center py-16 text-text-muted/40 italic">
Aucune fabrication. Commencez par créer un compost ou une décoction.
</div>
</div>
<!-- ====== POPUP DÉTAIL ACHAT ====== -->
<div v-if="detailAchat" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="detailAchat = null">
<div class="bg-bg-hard rounded-3xl w-full max-w-lg border border-bg-soft shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
<div class="p-5 border-b border-bg-soft flex justify-between items-start" :style="{ borderLeft: `8px solid ${catAchatColor(detailAchat.categorie)}` }">
<div>
<span :class="['text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded bg-bg/50', catAchatTextClass(detailAchat.categorie)]">
{{ detailAchat.categorie }}
</span>
<h2 class="text-text font-black text-2xl mt-1">{{ detailAchat.nom }}</h2>
<p v-if="detailAchat.marque" class="text-text-muted text-sm">{{ detailAchat.marque }}</p>
</div>
<button @click="detailAchat = null" class="text-text-muted hover:text-red text-2xl"></button>
</div>
<div class="p-5 overflow-y-auto space-y-4">
<div class="grid grid-cols-2 gap-3">
<div v-if="detailAchat.prix" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">Prix</span>
<span class="text-yellow font-black text-lg">{{ detailAchat.prix.toFixed(2) }} </span>
</div>
<div v-if="detailAchat.poids" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">Poids / Qté</span>
<span class="text-text font-bold">{{ detailAchat.poids }}</span>
</div>
<div v-if="detailAchat.boutique_nom" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">Enseigne</span>
<span class="text-text font-bold">{{ detailAchat.boutique_nom }}</span>
</div>
<div v-if="detailAchat.date_achat" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">Date d'achat</span>
<span class="text-text text-sm">{{ detailAchat.date_achat }}</span>
</div>
<div v-if="detailAchat.dluo" class="bg-bg/30 p-3 rounded-xl border border-bg-soft col-span-2">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">DLUO</span>
<span :class="['font-bold', isDluoExpired(detailAchat.dluo) ? 'text-red' : 'text-green']">
{{ detailAchat.dluo }}{{ isDluoExpired(detailAchat.dluo) ? ' Dépassée' : ' Valide' }}
</span>
</div>
</div>
<div v-if="detailAchat.boutique_url">
<a :href="detailAchat.boutique_url" target="_blank" rel="noopener"
class="text-blue text-sm hover:underline">🔗 Voir le produit en ligne</a>
</div>
<div v-if="detailAchat.notes" class="bg-bg/40 p-4 rounded-2xl border-l-4 border-yellow/30 text-text/90 text-sm italic whitespace-pre-line">
{{ detailAchat.notes }}
</div>
</div>
<div class="p-4 border-t border-bg-soft flex gap-3">
<button @click="startEditAchat(detailAchat)" class="btn-primary !bg-yellow !text-bg flex-1 py-2 font-black uppercase text-xs">Modifier</button>
<button @click="deleteAchat(detailAchat.id!)" class="btn-outline !border-red/20 !text-red hover:bg-red/10 px-4 py-2 font-black uppercase text-xs">Supprimer</button>
</div>
</div>
</div>
<!-- ====== POPUP DÉTAIL FABRICATION ====== -->
<div v-if="detailFabrication" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="detailFabrication = null">
<div class="bg-bg-hard rounded-3xl w-full max-w-lg border border-bg-soft shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
<div class="p-5 border-b border-bg-soft flex justify-between items-start" :style="{ borderLeft: `8px solid ${statutColor(detailFabrication.statut || 'en_cours')}` }">
<div>
<div class="flex items-center gap-2 mb-1">
<span :class="['text-[9px] font-black uppercase px-1.5 py-0.5 rounded', typeFabTextClass(detailFabrication.type)]">
{{ detailFabrication.type }}
</span>
<span :class="['text-[9px] font-black px-2 py-0.5 rounded-full border', statutBadgeClass(detailFabrication.statut || 'en_cours')]">
{{ statutLabel(detailFabrication.statut || 'en_cours') }}
</span>
</div>
<h2 class="text-text font-black text-2xl">{{ detailFabrication.nom }}</h2>
</div>
<button @click="detailFabrication = null" class="text-text-muted hover:text-red text-2xl">✕</button>
</div>
<div class="p-5 overflow-y-auto space-y-4">
<!-- Dates -->
<div class="grid grid-cols-2 gap-3">
<div v-if="detailFabrication.date_debut" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">Début</span>
<span class="text-text text-sm">{{ detailFabrication.date_debut }}</span>
</div>
<div v-if="detailFabrication.date_fin_prevue" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">Prêt le</span>
<span class="text-text text-sm">{{ detailFabrication.date_fin_prevue }}</span>
</div>
<div v-if="detailFabrication.quantite_produite" class="bg-bg/30 p-3 rounded-xl border border-bg-soft col-span-2">
<span class="text-[9px] font-black text-text-muted uppercase block mb-0.5">Quantité produite</span>
<span class="text-text font-bold">{{ detailFabrication.quantite_produite }}</span>
</div>
</div>
<!-- Ingrédients -->
<div v-if="detailFabrication.ingredients?.length" class="space-y-2">
<h3 class="text-[9px] font-black text-text-muted uppercase tracking-widest">🌿 Ingrédients</h3>
<div class="space-y-1">
<div v-for="(ing, i) in detailFabrication.ingredients" :key="i"
class="flex justify-between items-center bg-bg/30 px-3 py-2 rounded-lg border border-bg-soft">
<span class="text-text text-sm">{{ ing.nom }}</span>
<span class="text-yellow font-bold text-sm">{{ ing.quantite }}</span>
</div>
</div>
</div>
<!-- Notes -->
<div v-if="detailFabrication.notes" class="bg-bg/40 p-4 rounded-2xl border-l-4 border-yellow/30 text-text/90 text-sm italic whitespace-pre-line">
{{ detailFabrication.notes }}
</div>
<!-- Changement de statut -->
<div class="space-y-2">
<h3 class="text-[9px] font-black text-text-muted uppercase tracking-widest">Changer le statut</h3>
<div class="flex gap-2 flex-wrap">
<button v-for="s in statutsFabrication.slice(1)" :key="s.val"
@click="changeStatut(detailFabrication, s.val)"
:disabled="detailFabrication.statut === s.val"
:class="['px-3 py-1 rounded-lg text-[10px] font-black uppercase border transition-all',
detailFabrication.statut === s.val
? `${s.bgClass} text-bg border-transparent cursor-default`
: 'border-bg-soft text-text-muted hover:text-text']">
{{ s.label }}
</button>
</div>
</div>
</div>
<div class="p-4 border-t border-bg-soft flex gap-3">
<button @click="startEditFabrication(detailFabrication)" class="btn-primary !bg-yellow !text-bg flex-1 py-2 font-black uppercase text-xs">Modifier</button>
<button @click="deleteFabrication(detailFabrication.id!)" class="btn-outline !border-red/20 !text-red hover:bg-red/10 px-4 py-2 font-black uppercase text-xs">Supprimer</button>
</div>
</div>
</div>
<!-- ====== FORMULAIRE ACHAT ====== -->
<div v-if="showFormAchat" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[60] flex items-center justify-center p-4" @click.self="closeFormAchat">
<div class="bg-bg-hard rounded-3xl p-6 w-full max-w-2xl border border-bg-soft shadow-2xl max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6 border-b border-bg-soft pb-4">
<h2 class="text-text font-black text-xl uppercase">{{ editAchat ? 'Modifier l\'achat' : 'Nouvel achat' }}</h2>
<button @click="closeFormAchat" class="text-text-muted hover:text-red text-2xl"></button>
</div>
<form @submit.prevent="submitAchat" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Catégorie *</label>
<div class="flex gap-2 flex-wrap">
<button v-for="cat in categoriesAchat.slice(1)" :key="cat.val" type="button"
@click="formAchat.categorie = cat.val"
:class="['px-3 py-1 rounded-full text-[10px] font-black uppercase border transition-all',
formAchat.categorie === cat.val ? 'border-transparent text-bg' : 'border-bg-soft text-text-muted hover:text-text']"
:style="formAchat.categorie === cat.val ? { background: catAchatColor(cat.val) } : {}">
{{ cat.label }}
</button>
</div>
</div>
<div class="md:col-span-2">
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Nom *</label>
<input v-model="formAchat.nom" required class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" placeholder="Ex: Terreau universel Floragard" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Marque</label>
<input v-model="formAchat.marque" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Enseigne</label>
<select v-model="formAchat.boutique_nom" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm outline-none focus:border-yellow appearance-none">
<option value=""> Non renseigné </option>
<option v-for="b in BOUTIQUES" :key="b" :value="b">{{ b }}</option>
</select>
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Prix ()</label>
<input v-model.number="formAchat.prix" type="number" step="0.01" min="0" placeholder="0.00" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Poids / Quantité</label>
<input v-model="formAchat.poids" placeholder="ex: 20L, 1kg, 500ml" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Date d'achat</label>
<input v-model="formAchat.date_achat" type="date" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">DLUO</label>
<input v-model="formAchat.dluo" type="date" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div class="md:col-span-2">
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">URL produit</label>
<input v-model="formAchat.boutique_url" type="url" placeholder="https://..." class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div class="md:col-span-2">
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Notes</label>
<textarea v-model="formAchat.notes" rows="2" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none resize-none" />
</div>
<div class="md:col-span-2 flex justify-between pt-4 border-t border-bg-soft">
<button type="button" @click="closeFormAchat" class="text-text-muted hover:text-red uppercase text-xs font-bold px-6">Annuler</button>
<button type="submit" :disabled="submitting" class="btn-primary px-8 py-3 !bg-yellow !text-bg font-black">
{{ editAchat ? 'Sauvegarder' : 'Enregistrer' }}
</button>
</div>
</form>
</div>
</div>
<!-- ====== FORMULAIRE FABRICATION ====== -->
<div v-if="showFormFab" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[60] flex items-center justify-center p-4" @click.self="closeFormFab">
<div class="bg-bg-hard rounded-3xl p-6 w-full max-w-2xl border border-bg-soft shadow-2xl max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6 border-b border-bg-soft pb-4">
<h2 class="text-text font-black text-xl uppercase">{{ editFab ? 'Modifier' : 'Nouvelle fabrication' }}</h2>
<button @click="closeFormFab" class="text-text-muted hover:text-red text-2xl">✕</button>
</div>
<form @submit.prevent="submitFab" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Type *</label>
<div class="flex gap-2 flex-wrap">
<button v-for="t in typesFabrication.slice(1)" :key="t.val" type="button"
@click="formFab.type = t.val"
:class="['px-3 py-1 rounded-full text-[10px] font-black uppercase border transition-all',
formFab.type === t.val ? `${t.bgClass} text-bg border-transparent` : 'border-bg-soft text-text-muted hover:text-text']">
{{ t.label }}
</button>
</div>
</div>
<div class="md:col-span-2">
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Nom *</label>
<input v-model="formFab.nom" required class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" placeholder="Ex: Purin d'ortie mai 2026" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Date de début</label>
<input v-model="formFab.date_debut" type="date" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Date prévue prête</label>
<input v-model="formFab.date_fin_prevue" type="date" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Statut</label>
<select v-model="formFab.statut" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm outline-none focus:border-yellow appearance-none">
<option v-for="s in statutsFabrication.slice(1)" :key="s.val" :value="s.val">{{ s.label }}</option>
</select>
</div>
<div>
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Quantité produite</label>
<input v-model="formFab.quantite_produite" placeholder="ex: 8L, 50kg" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none" />
</div>
<!-- Ingrédients -->
<div class="md:col-span-2 bg-bg/40 border border-bg-soft rounded-2xl p-4">
<div class="flex items-center justify-between mb-3">
<span class="text-[10px] font-black text-text-muted uppercase tracking-widest">🌿 Ingrédients</span>
<button type="button" @click="addIngredient"
class="px-2 py-0.5 rounded-full text-[10px] font-bold border border-green/40 text-green hover:bg-green/10 transition-all">
+ Ajouter
</button>
</div>
<div class="space-y-2">
<div v-for="(ing, i) in formFab.ingredients" :key="i" class="flex gap-2 items-center">
<input v-model="ing.nom" placeholder="Ingrédient" class="flex-1 bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm focus:border-yellow outline-none" />
<input v-model="ing.quantite" placeholder="Qté" class="w-24 bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm focus:border-yellow outline-none" />
<button type="button" @click="removeIngredient(i)" class="text-red/60 hover:text-red text-lg leading-none">✕</button>
</div>
<p v-if="!formFab.ingredients.length" class="text-text-muted/40 text-xs italic">Aucun ingrédient ajouté</p>
</div>
</div>
<div class="md:col-span-2">
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Notes / Recette</label>
<textarea v-model="formFab.notes" rows="3" class="w-full bg-bg border border-bg-soft rounded-xl px-4 py-3 text-text text-sm focus:border-yellow outline-none resize-none" placeholder="Instructions, observations, recette..." />
</div>
<div class="md:col-span-2 flex justify-between pt-4 border-t border-bg-soft">
<button type="button" @click="closeFormFab" class="text-text-muted hover:text-red uppercase text-xs font-bold px-6">Annuler</button>
<button type="submit" :disabled="submitting" class="btn-primary px-8 py-3 !bg-yellow !text-bg font-black">
{{ editFab ? 'Sauvegarder' : 'Créer' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import axios from 'axios'
import { useAchatsStore } from '@/stores/achats'
import { useFabricationsStore } from '@/stores/fabrications'
import type { AchatIntrant } from '@/api/achats'
import type { Fabrication } from '@/api/fabrications'
import { useToast } from '@/composables/useToast'
const achatsStore = useAchatsStore()
const fabricationsStore = useFabricationsStore()
const toast = useToast()
const activeTab = ref<'achats' | 'fabrications'>('achats')
const filterCat = ref('')
const filterType = ref('')
const filterStatut = ref('')
const submitting = ref(false)
// Détails
const detailAchat = ref<AchatIntrant | null>(null)
const detailFabrication = ref<Fabrication | null>(null)
// Formulaire achat
const showFormAchat = ref(false)
const editAchat = ref<AchatIntrant | null>(null)
const formAchat = reactive<Partial<AchatIntrant>>({
categorie: 'terreau', nom: '', marque: '', boutique_nom: '',
boutique_url: '', prix: undefined, poids: '', date_achat: '', dluo: '', notes: '',
})
// Formulaire fabrication
const showFormFab = ref(false)
const editFab = ref<Fabrication | null>(null)
const formFab = reactive({
type: 'purin', nom: '', date_debut: '', date_fin_prevue: '',
statut: 'en_cours', quantite_produite: '', notes: '',
ingredients: [] as { nom: string; quantite: string }[],
})
const tabs: { key: 'achats' | 'fabrications'; label: string }[] = [
{ key: 'achats', label: '🛒 Achats' },
{ key: 'fabrications', label: '🌿 Fabrications' },
]
const BOUTIQUES = [
'Gamm Vert', 'Lidl', 'Super U', 'Intermarché', 'Truffaut', 'Botanic',
'Amazon', 'Graines Baumaux', 'Vilmorin', 'Germinance', 'Direct producteur',
'Marché local', 'Autre',
]
const categoriesAchat = [
{ val: '', label: 'Tous' },
{ val: 'terreau', label: '🪨 Terreau' },
{ val: 'engrais', label: '🌿 Engrais' },
{ val: 'traitement', label: '💊 Traitement' },
{ val: 'autre', label: 'Autre' },
]
const typesFabrication = [
{ val: '', label: 'Tous', bgClass: 'bg-yellow' },
{ val: 'compost', label: '♻️ Compost', bgClass: 'bg-orange' },
{ val: 'decoction', label: '🫖 Décoction', bgClass: 'bg-blue' },
{ val: 'purin', label: '🌱 Purin', bgClass: 'bg-green' },
{ val: 'autre', label: 'Autre', bgClass: 'bg-text-muted' },
]
const statutsFabrication = [
{ val: '', label: 'Tous', bgClass: 'bg-yellow' },
{ val: 'en_cours', label: '⏳ En cours', bgClass: 'bg-orange' },
{ val: 'pret', label: '✓ Prêt', bgClass: 'bg-green' },
{ val: 'utilise', label: '✓ Utilisé', bgClass: 'bg-bg-soft' },
{ val: 'echec', label: '✗ Échec', bgClass: 'bg-red' },
]
const filteredAchats = computed(() => {
let list = achatsStore.achats
if (filterCat.value) list = list.filter(a => a.categorie === filterCat.value)
return list
})
const filteredFabricatons = computed(() => {
let list = fabricationsStore.fabrications
if (filterType.value) list = list.filter(f => f.type === filterType.value)
if (filterStatut.value) list = list.filter(f => f.statut === filterStatut.value)
return list
})
function isDluoExpired(dluo: string) {
return !!dluo && new Date(dluo) < new Date()
}
function catAchatColor(cat: string) {
return ({ terreau: '#fe8019', engrais: '#b8bb26', traitement: '#83a598', autre: '#928374' } as any)[cat] || '#928374'
}
function catAchatTextClass(cat: string) {
return ({ terreau: 'text-orange', engrais: 'text-green', traitement: 'text-blue', autre: 'text-text-muted' } as any)[cat] || 'text-text-muted'
}
function statutColor(statut: string) {
return ({ en_cours: '#fe8019', pret: '#b8bb26', utilise: '#928374', echec: '#fb4934' } as any)[statut] || '#928374'
}
function statutBadgeClass(statut: string) {
return ({
en_cours: 'bg-orange/10 border-orange/40 text-orange',
pret: 'bg-green/10 border-green/40 text-green',
utilise: 'bg-bg-soft border-bg-soft text-text-muted',
echec: 'bg-red/10 border-red/40 text-red',
} as any)[statut] || ''
}
function statutLabel(statut: string) {
return ({ en_cours: '⏳ En cours', pret: '✓ Prêt', utilise: '✓ Utilisé', echec: '✗ Échec' } as any)[statut] || statut
}
function typeFabTextClass(type: string) {
return ({ compost: 'text-orange', decoction: 'text-blue', purin: 'text-green', autre: 'text-text-muted' } as any)[type] || 'text-text-muted'
}
// ---- Achats ----
function openCreateForm() {
if (activeTab.value === 'achats') {
editAchat.value = null
Object.assign(formAchat, { categorie: 'terreau', nom: '', marque: '', boutique_nom: '', boutique_url: '', prix: undefined, poids: '', date_achat: '', dluo: '', notes: '' })
showFormAchat.value = true
} else {
editFab.value = null
Object.assign(formFab, { type: 'purin', nom: '', date_debut: '', date_fin_prevue: '', statut: 'en_cours', quantite_produite: '', notes: '', ingredients: [] })
showFormFab.value = true
}
}
function openDetailAchat(a: AchatIntrant) { detailAchat.value = a }
function startEditAchat(a: AchatIntrant) {
detailAchat.value = null
editAchat.value = a
Object.assign(formAchat, { ...a })
showFormAchat.value = true
}
function closeFormAchat() { showFormAchat.value = false; editAchat.value = null }
async function submitAchat() {
if (submitting.value) return
submitting.value = true
try {
const payload = { ...formAchat, prix: formAchat.prix ?? undefined }
if (editAchat.value) {
await axios.put(`/api/achats/${editAchat.value.id}`, payload)
await achatsStore.fetchAll()
toast.success('Achat modifié')
} else {
await achatsStore.create(payload)
toast.success('Achat enregistré')
}
closeFormAchat()
} catch { /* intercepteur */ } finally { submitting.value = false }
}
async function deleteAchat(id: number) {
if (!confirm('Supprimer cet achat ?')) return
await achatsStore.remove(id)
detailAchat.value = null
toast.success('Achat supprimé')
}
// ---- Fabrications ----
function openDetailFabrication(f: Fabrication) { detailFabrication.value = f }
function startEditFabrication(f: Fabrication) {
detailFabrication.value = null
editFab.value = f
Object.assign(formFab, {
...f,
ingredients: f.ingredients ? [...f.ingredients.map(i => ({ ...i }))] : [],
})
showFormFab.value = true
}
function closeFormFab() { showFormFab.value = false; editFab.value = null }
function addIngredient() { formFab.ingredients.push({ nom: '', quantite: '' }) }
function removeIngredient(i: number) { formFab.ingredients.splice(i, 1) }
async function submitFab() {
if (submitting.value) return
submitting.value = true
try {
const payload = { ...formFab, ingredients: formFab.ingredients.filter(i => i.nom) }
if (editFab.value) {
await axios.put(`/api/fabrications/${editFab.value.id}`, payload)
await fabricationsStore.fetchAll()
toast.success('Fabrication modifiée')
} else {
await fabricationsStore.create(payload)
toast.success('Fabrication créée')
}
closeFormFab()
} catch { /* intercepteur */ } finally { submitting.value = false }
}
async function quickStatut(f: Fabrication, statut: string) {
await fabricationsStore.updateStatut(f.id!, statut)
toast.success(`Statut → ${statutLabel(statut)}`)
}
async function changeStatut(f: Fabrication, statut: string) {
await fabricationsStore.updateStatut(f.id!, statut)
detailFabrication.value = fabricationsStore.fabrications.find(x => x.id === f.id) ?? detailFabrication.value
toast.success(`Statut → ${statutLabel(statut)}`)
}
async function deleteFabrication(id: number) {
if (!confirm('Supprimer cette fabrication ?')) return
await fabricationsStore.remove(id)
detailFabrication.value = null
toast.success('Fabrication supprimée')
}
onMounted(async () => {
await Promise.all([achatsStore.fetchAll(), fabricationsStore.fetchAll()])
})
</script>