feat(plantes): popup variété + bouton ➕ Variété + temp_germination/temps_levee_j
- Ajoute detailPlantObj (ref<Plant>) synchronisé dans openDetails/prevVariety/nextVariety/closeDetail - Renomme detailVarieties (ref<Plant[]>) en detailPlantGroup pour la navigation par groupe de nom_commun - Ajoute detailVarieties comme computed<PlantVariety[]> depuis detailPlantObj.value.varieties - Ajoute refs/fonctions formulaire variété : showFormVariety, editVariety, formVariety, openAddVariety, openEditVariety, closeFormVariety, submitVariety, deleteVariety - Bouton ➕ Variété dans le footer du popup détail - Liste des PlantVariety dans le popup détail (avec édition/suppression et alerte DLUO) - Champs temp_germination et temps_levee_j dans la section caractéristiques - Popup formulaire variété (z-[70]) avec tous les champs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -85,7 +85,7 @@
|
|||||||
|
|
||||||
<!-- Navigation variétés -->
|
<!-- Navigation variétés -->
|
||||||
<div class="flex items-center gap-3 mt-3">
|
<div class="flex items-center gap-3 mt-3">
|
||||||
<div v-if="detailVarieties.length > 1" class="flex items-center gap-2">
|
<div v-if="detailPlantGroup.length > 1" class="flex items-center gap-2">
|
||||||
<button @click="prevVariety"
|
<button @click="prevVariety"
|
||||||
:disabled="detailVarietyIdx === 0"
|
:disabled="detailVarietyIdx === 0"
|
||||||
:class="['w-7 h-7 rounded-full border flex items-center justify-center text-sm font-bold transition-all',
|
:class="['w-7 h-7 rounded-full border flex items-center justify-center text-sm font-bold transition-all',
|
||||||
@@ -93,12 +93,12 @@
|
|||||||
‹
|
‹
|
||||||
</button>
|
</button>
|
||||||
<span class="text-text-muted text-xs font-bold">
|
<span class="text-text-muted text-xs font-bold">
|
||||||
Variété {{ detailVarietyIdx + 1 }} / {{ detailVarieties.length }}
|
Variété {{ detailVarietyIdx + 1 }} / {{ detailPlantGroup.length }}
|
||||||
</span>
|
</span>
|
||||||
<button @click="nextVariety"
|
<button @click="nextVariety"
|
||||||
:disabled="detailVarietyIdx === detailVarieties.length - 1"
|
:disabled="detailVarietyIdx === detailPlantGroup.length - 1"
|
||||||
:class="['w-7 h-7 rounded-full border flex items-center justify-center text-sm font-bold transition-all',
|
:class="['w-7 h-7 rounded-full border flex items-center justify-center text-sm font-bold transition-all',
|
||||||
detailVarietyIdx === detailVarieties.length - 1 ? 'border-bg-soft text-text-muted/30 cursor-not-allowed' : 'border-yellow/50 text-yellow hover:bg-yellow/10']">
|
detailVarietyIdx === detailPlantGroup.length - 1 ? 'border-bg-soft text-text-muted/30 cursor-not-allowed' : 'border-yellow/50 text-yellow hover:bg-yellow/10']">
|
||||||
›
|
›
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,6 +134,14 @@
|
|||||||
<span class="font-bold">Mois: {{ detailPlant.plantation_mois }}</span>
|
<span class="font-bold">Mois: {{ detailPlant.plantation_mois }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="detailPlantObj?.temp_germination" 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">T° germination</span>
|
||||||
|
<span class="text-text text-sm">{{ detailPlantObj.temp_germination }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="detailPlantObj?.temps_levee_j" 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">Temps de levée</span>
|
||||||
|
<span class="text-text text-sm">{{ detailPlantObj.temps_levee_j }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Boutique -->
|
<!-- Boutique -->
|
||||||
@@ -203,6 +211,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Variétés (PlantVariety) -->
|
||||||
|
<div v-if="detailVarieties.length" class="space-y-2 mt-4">
|
||||||
|
<h3 class="text-[9px] font-black text-text-muted uppercase tracking-widest">
|
||||||
|
🌿 Variétés ({{ detailVarieties.length }})
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div v-for="v in detailVarieties" :key="v.id"
|
||||||
|
class="flex items-center justify-between bg-bg/30 px-3 py-2 rounded-lg border border-bg-soft">
|
||||||
|
<div>
|
||||||
|
<span class="text-text text-sm font-bold">{{ v.variete || '(sans nom)' }}</span>
|
||||||
|
<span v-if="v.boutique_nom" class="text-text-muted text-xs ml-2">🛒 {{ v.boutique_nom }}</span>
|
||||||
|
<span v-if="v.prix_achat" class="text-yellow text-xs ml-2">{{ v.prix_achat.toFixed(2) }}€</span>
|
||||||
|
<span v-if="v.dluo && isDluoExpired(v.dluo)" class="text-red text-xs ml-1">⚠️ DLUO</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button @click="openEditVariety(v)" class="text-text-muted hover:text-yellow text-xs px-2">✏️</button>
|
||||||
|
<button @click="deleteVariety(v.id!)" class="text-text-muted hover:text-red text-xs px-2">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Galerie Photos -->
|
<!-- Galerie Photos -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
@@ -228,6 +258,10 @@
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="p-4 bg-bg-hard border-t border-bg-soft flex gap-3">
|
<div class="p-4 bg-bg-hard border-t border-bg-soft flex gap-3">
|
||||||
|
<button @click="openAddVariety"
|
||||||
|
class="btn-primary !bg-green !text-bg py-2 px-4 font-black uppercase text-xs flex items-center gap-1">
|
||||||
|
➕ Variété
|
||||||
|
</button>
|
||||||
<button @click="startEdit(detailPlant)" class="btn-primary !bg-yellow !text-bg flex-1 py-3 font-black uppercase text-xs tracking-widest">Modifier la fiche</button>
|
<button @click="startEdit(detailPlant)" class="btn-primary !bg-yellow !text-bg flex-1 py-3 font-black uppercase text-xs tracking-widest">Modifier la fiche</button>
|
||||||
<button @click="removePlant(detailPlant.id!)" class="btn-outline !border-red/20 !text-red hover:bg-red/10 px-6 py-3 font-black uppercase text-xs tracking-widest">Supprimer</button>
|
<button @click="removePlant(detailPlant.id!)" class="btn-outline !border-red/20 !text-red hover:bg-red/10 px-6 py-3 font-black uppercase text-xs tracking-widest">Supprimer</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -462,6 +496,81 @@
|
|||||||
|
|
||||||
<!-- Upload Photo -->
|
<!-- Upload Photo -->
|
||||||
<input type="file" ref="fileInput" accept="image/*" class="hidden" @change="handleFileUpload" />
|
<input type="file" ref="fileInput" accept="image/*" class="hidden" @change="handleFileUpload" />
|
||||||
|
|
||||||
|
<!-- ====== POPUP FORMULAIRE VARIÉTÉ ====== -->
|
||||||
|
<div v-if="showFormVariety" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[70] flex items-center justify-center p-4" @click.self="closeFormVariety">
|
||||||
|
<div class="bg-bg-hard rounded-3xl p-6 w-full max-w-xl border border-bg-soft shadow-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="flex items-center justify-between mb-5 border-b border-bg-soft pb-4">
|
||||||
|
<h2 class="text-text font-black text-xl uppercase">
|
||||||
|
{{ editVariety ? 'Modifier la variété' : '➕ Nouvelle variété' }}
|
||||||
|
</h2>
|
||||||
|
<button @click="closeFormVariety" class="text-text-muted hover:text-red text-2xl">✕</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-text-muted text-xs mb-4 italic">
|
||||||
|
Plante : <span class="text-yellow font-bold">{{ detailPlantObj?.nom_commun }}</span>
|
||||||
|
</p>
|
||||||
|
<form @submit.prevent="submitVariety" 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">Nom de la variété *</label>
|
||||||
|
<input v-model="formVariety.variete" 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: Nantaise, Cornue des Andes, Moneymaker…" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Tags</label>
|
||||||
|
<input v-model="formVariety.tags"
|
||||||
|
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="bio, f1, ancien, résistant…" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-1">Enseigne</label>
|
||||||
|
<select v-model="formVariety.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="formVariety.prix_achat" type="number" step="0.01" min="0"
|
||||||
|
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 / Qté sachet</label>
|
||||||
|
<input v-model="formVariety.poids" placeholder="ex: 5g, 100 graines"
|
||||||
|
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="formVariety.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="formVariety.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="formVariety.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 spécifiques à cette variété</label>
|
||||||
|
<textarea v-model="formVariety.notes_variete" 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" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2 flex justify-between pt-4 border-t border-bg-soft">
|
||||||
|
<button type="button" @click="closeFormVariety"
|
||||||
|
class="text-text-muted hover:text-red uppercase text-xs font-bold px-6">Annuler</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="btn-primary px-8 py-3 !bg-green !text-bg font-black">
|
||||||
|
{{ editVariety ? 'Sauvegarder' : 'Ajouter' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -469,7 +578,7 @@
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { usePlantsStore } from '@/stores/plants'
|
import { usePlantsStore } from '@/stores/plants'
|
||||||
import type { Plant } from '@/api/plants'
|
import type { Plant, PlantVariety } from '@/api/plants'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
|
||||||
const plantsStore = usePlantsStore()
|
const plantsStore = usePlantsStore()
|
||||||
@@ -485,17 +594,35 @@ const lightbox = ref<Media | null>(null)
|
|||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
const uploadTarget = ref<Plant | null>(null)
|
const uploadTarget = ref<Plant | null>(null)
|
||||||
|
|
||||||
// Navigation variétés
|
// Navigation variétés (groupe de Plant par nom_commun)
|
||||||
const detailVarieties = ref<Plant[]>([])
|
const detailPlantGroup = ref<Plant[]>([])
|
||||||
const detailVarietyIdx = ref(0)
|
const detailVarietyIdx = ref(0)
|
||||||
const detailPlant = computed(() => detailVarieties.value[detailVarietyIdx.value] ?? null)
|
const detailPlant = computed(() => detailPlantGroup.value[detailVarietyIdx.value] ?? null)
|
||||||
|
|
||||||
|
// detailPlantObj : ref sur la plante ouverte en détail (Plant complet)
|
||||||
|
const detailPlantObj = ref<Plant | null>(null)
|
||||||
|
|
||||||
|
// detailVarieties : variétés (PlantVariety) de la plante affichée
|
||||||
|
const detailVarieties = computed<PlantVariety[]>(() => {
|
||||||
|
if (!detailPlantObj.value) return []
|
||||||
|
return detailPlantObj.value.varieties ?? []
|
||||||
|
})
|
||||||
|
|
||||||
// Associations au niveau nom_commun (première variété ayant des données)
|
// Associations au niveau nom_commun (première variété ayant des données)
|
||||||
const detailAssociations = computed(() =>
|
const detailAssociations = computed(() =>
|
||||||
detailVarieties.value.find(v => v.associations_favorables?.length || v.associations_defavorables?.length)
|
detailPlantGroup.value.find(v => v.associations_favorables?.length || v.associations_defavorables?.length)
|
||||||
?? detailPlant.value
|
?? detailPlant.value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Refs pour le formulaire variété
|
||||||
|
const showFormVariety = ref(false)
|
||||||
|
const editVariety = ref<PlantVariety | null>(null)
|
||||||
|
const formVariety = reactive<Partial<PlantVariety>>({
|
||||||
|
variete: '', tags: '', notes_variete: '',
|
||||||
|
boutique_nom: '', boutique_url: '', prix_achat: undefined,
|
||||||
|
date_achat: '', poids: '', dluo: '',
|
||||||
|
})
|
||||||
|
|
||||||
interface Media {
|
interface Media {
|
||||||
id: number; entity_type: string; entity_id: number
|
id: number; entity_type: string; entity_id: number
|
||||||
url: string; thumbnail_url?: string; titre?: string
|
url: string; thumbnail_url?: string; titre?: string
|
||||||
@@ -637,7 +764,8 @@ function catTextClass(cat: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
detailVarieties.value = []
|
detailPlantGroup.value = []
|
||||||
|
detailPlantObj.value = null
|
||||||
detailVarietyIdx.value = 0
|
detailVarietyIdx.value = 0
|
||||||
plantPhotos.value = []
|
plantPhotos.value = []
|
||||||
}
|
}
|
||||||
@@ -645,21 +773,24 @@ function closeDetail() {
|
|||||||
async function openDetails(p: Plant) {
|
async function openDetails(p: Plant) {
|
||||||
const key = (p.nom_commun || '').toLowerCase()
|
const key = (p.nom_commun || '').toLowerCase()
|
||||||
const group = plantGroups.value.get(key) || [p]
|
const group = plantGroups.value.get(key) || [p]
|
||||||
detailVarieties.value = [...group].sort((a, b) => (a.id || 0) - (b.id || 0))
|
detailPlantGroup.value = [...group].sort((a, b) => (a.id || 0) - (b.id || 0))
|
||||||
detailVarietyIdx.value = 0
|
detailVarietyIdx.value = 0
|
||||||
await fetchPhotos(detailVarieties.value[0].id!)
|
detailPlantObj.value = detailPlantGroup.value[0]
|
||||||
|
await fetchPhotos(detailPlantGroup.value[0].id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prevVariety() {
|
async function prevVariety() {
|
||||||
if (detailVarietyIdx.value > 0) {
|
if (detailVarietyIdx.value > 0) {
|
||||||
detailVarietyIdx.value--
|
detailVarietyIdx.value--
|
||||||
|
detailPlantObj.value = detailPlant.value
|
||||||
await fetchPhotos(detailPlant.value!.id!)
|
await fetchPhotos(detailPlant.value!.id!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function nextVariety() {
|
async function nextVariety() {
|
||||||
if (detailVarietyIdx.value < detailVarieties.value.length - 1) {
|
if (detailVarietyIdx.value < detailPlantGroup.value.length - 1) {
|
||||||
detailVarietyIdx.value++
|
detailVarietyIdx.value++
|
||||||
|
detailPlantObj.value = detailPlant.value
|
||||||
await fetchPhotos(detailPlant.value!.id!)
|
await fetchPhotos(detailPlant.value!.id!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,6 +843,52 @@ function closeForm() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openAddVariety() {
|
||||||
|
if (!detailPlantObj.value) return
|
||||||
|
editVariety.value = null
|
||||||
|
Object.assign(formVariety, {
|
||||||
|
variete: '', tags: '', notes_variete: '',
|
||||||
|
boutique_nom: '', boutique_url: '', prix_achat: undefined,
|
||||||
|
date_achat: '', poids: '', dluo: '',
|
||||||
|
})
|
||||||
|
showFormVariety.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditVariety(v: PlantVariety) {
|
||||||
|
editVariety.value = v
|
||||||
|
Object.assign(formVariety, { ...v })
|
||||||
|
showFormVariety.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFormVariety() {
|
||||||
|
showFormVariety.value = false
|
||||||
|
editVariety.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitVariety() {
|
||||||
|
if (!detailPlantObj.value?.id) return
|
||||||
|
const payload = { ...formVariety, prix_achat: formVariety.prix_achat ?? undefined }
|
||||||
|
if (editVariety.value?.id) {
|
||||||
|
await plantsStore.updateVariety(detailPlantObj.value.id, editVariety.value.id, payload)
|
||||||
|
toast.success('Variété modifiée')
|
||||||
|
} else {
|
||||||
|
await plantsStore.createVariety(detailPlantObj.value.id, payload)
|
||||||
|
toast.success('Variété ajoutée')
|
||||||
|
}
|
||||||
|
// Refresh plant data so detailPlantObj reflects updated varieties
|
||||||
|
await plantsStore.fetchAll()
|
||||||
|
const updatedPlant = plantsStore.plants.find(p => p.id === detailPlantObj.value?.id)
|
||||||
|
if (updatedPlant) detailPlantObj.value = updatedPlant
|
||||||
|
closeFormVariety()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteVariety(vid: number) {
|
||||||
|
if (!detailPlantObj.value?.id) return
|
||||||
|
if (!confirm('Supprimer cette variété ?')) return
|
||||||
|
await plantsStore.removeVariety(detailPlantObj.value.id, vid)
|
||||||
|
toast.success('Variété supprimée')
|
||||||
|
}
|
||||||
|
|
||||||
async function submitPlant() {
|
async function submitPlant() {
|
||||||
if (submitting.value) return
|
if (submitting.value) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user