feat(intrants): IntratsView with Achats + Fabrications tabs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
643
frontend/src/views/IntratsView.vue
Normal file
643
frontend/src/views/IntratsView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user