feat(plantes): API plants.ts — Plant + PlantVariety + endpoints varieties

Remplace Plant (variete/boutique/tags inline) par Plant + PlantVariety séparés.
Ajoute temp_germination, temps_levee_j, varieties[]. Ajoute CRUD variétés dans plantsApi.
Corrige PlantesView et TachesView pour lire boutique/variete via varieties?.[0].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 19:28:19 +01:00
parent d4d104b2c2
commit 32c7781d14
3 changed files with 66 additions and 43 deletions

View File

@@ -1,29 +1,12 @@
// frontend/src/api/plants.ts
import client from './client'
export interface Plant {
export interface PlantVariety {
id?: number
nom_commun: string
nom_botanique?: string
plant_id?: number
variete?: string
famille?: string
categorie?: string // potager|fleur|arbre|arbuste
tags?: string
type_plante?: string
besoin_eau?: string
besoin_soleil?: string
espacement_cm?: number
temp_min_c?: number
hauteur_cm?: number
plantation_mois?: string
recolte_mois?: string
semis_interieur_mois?: string
semis_exterieur_mois?: string
maladies_courantes?: string
astuces_culture?: string
url_reference?: string
notes?: string
associations_favorables?: string[]
associations_defavorables?: string[]
notes_variete?: string
boutique_nom?: string
boutique_url?: string
prix_achat?: number
@@ -32,10 +15,49 @@ export interface Plant {
dluo?: string
}
export interface Plant {
id?: number
nom_commun: string
nom_botanique?: string
famille?: string
categorie?: string // potager|fleur|arbre|arbuste
type_plante?: string
besoin_eau?: string
besoin_soleil?: string
espacement_cm?: number
temp_min_c?: number
hauteur_cm?: number
temp_germination?: string // ex: "8-10°C"
temps_levee_j?: string // ex: "15-20 jours"
plantation_mois?: string
recolte_mois?: string
semis_interieur_mois?: string
semis_exterieur_mois?: string
repiquage_mois?: string
profondeur_semis_cm?: number
duree_culture_j?: number
sol_conseille?: string
maladies_courantes?: string
astuces_culture?: string
url_reference?: string
notes?: string
associations_favorables?: string[]
associations_defavorables?: string[]
varieties?: PlantVariety[]
}
export const plantsApi = {
list: (categorie?: string) => client.get<Plant[]>('/api/plants', { params: categorie ? { categorie } : {} }).then(r => r.data),
list: (categorie?: string) =>
client.get<Plant[]>('/api/plants', { params: categorie ? { categorie } : {} }).then(r => r.data),
get: (id: number) => client.get<Plant>(`/api/plants/${id}`).then(r => r.data),
create: (p: Partial<Plant>) => client.post<Plant>('/api/plants', p).then(r => r.data),
update: (id: number, p: Partial<Plant>) => client.put<Plant>(`/api/plants/${id}`, p).then(r => r.data),
delete: (id: number) => client.delete(`/api/plants/${id}`),
// Variétés
createVariety: (plantId: number, v: Partial<PlantVariety>) =>
client.post<PlantVariety>(`/api/plants/${plantId}/varieties`, v).then(r => r.data),
updateVariety: (plantId: number, vid: number, v: Partial<PlantVariety>) =>
client.put<PlantVariety>(`/api/plants/${plantId}/varieties/${vid}`, v).then(r => r.data),
deleteVariety: (plantId: number, vid: number) =>
client.delete(`/api/plants/${plantId}/varieties/${vid}`),
}

View File

@@ -102,7 +102,7 @@
</button>
</div>
<p v-if="detailPlant.variete" class="text-yellow font-bold uppercase tracking-widest text-sm">{{ detailPlant.variete }}</p>
<p v-if="detailPlant.varieties?.[0]?.variete" class="text-yellow font-bold uppercase tracking-widest text-sm">{{ detailPlant.varieties?.[0]?.variete }}</p>
</div>
</div>
<button @click="closeDetail" class="text-text-muted hover:text-red transition-colors text-2xl ml-4"></button>
@@ -137,34 +137,34 @@
</div>
<!-- Boutique -->
<div v-if="detailPlant.boutique_nom || detailPlant.prix_achat || detailPlant.poids || detailPlant.dluo" class="space-y-2">
<div v-if="detailPlant.varieties?.[0]?.boutique_nom || detailPlant.varieties?.[0]?.prix_achat || detailPlant.varieties?.[0]?.poids || detailPlant.varieties?.[0]?.dluo" class="space-y-2">
<h3 class="text-[10px] font-black text-text-muted uppercase tracking-widest">🛒 Approvisionnement</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div v-if="detailPlant.boutique_nom" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<div v-if="detailPlant.varieties?.[0]?.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 text-sm font-bold">{{ detailPlant.boutique_nom }}</span>
<span class="text-text text-sm font-bold">{{ detailPlant.varieties?.[0]?.boutique_nom }}</span>
</div>
<div v-if="detailPlant.prix_achat" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<div v-if="detailPlant.varieties?.[0]?.prix_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">Prix</span>
<span class="text-yellow font-bold">{{ detailPlant.prix_achat.toFixed(2) }} </span>
<span class="text-yellow font-bold">{{ detailPlant.varieties?.[0]?.prix_achat?.toFixed(2) }} </span>
</div>
<div v-if="detailPlant.date_achat" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<div v-if="detailPlant.varieties?.[0]?.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">{{ detailPlant.date_achat }}</span>
<span class="text-text text-sm">{{ detailPlant.varieties?.[0]?.date_achat }}</span>
</div>
<div v-if="detailPlant.poids" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<div v-if="detailPlant.varieties?.[0]?.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 text-sm font-bold">{{ detailPlant.poids }}</span>
<span class="text-text text-sm font-bold">{{ detailPlant.varieties?.[0]?.poids }}</span>
</div>
<div v-if="detailPlant.dluo" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<div v-if="detailPlant.varieties?.[0]?.dluo" 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">DLUO</span>
<span :class="['text-sm font-bold', isDluoExpired(detailPlant.dluo) ? 'text-red' : 'text-green']">
{{ detailPlant.dluo }}{{ isDluoExpired(detailPlant.dluo) ? ' ' : '' }}
<span :class="['text-sm font-bold', isDluoExpired(detailPlant.varieties?.[0]?.dluo ?? '') ? 'text-red' : 'text-green']">
{{ detailPlant.varieties?.[0]?.dluo }}{{ isDluoExpired(detailPlant.varieties?.[0]?.dluo ?? '') ? ' ' : '' }}
</span>
</div>
<div v-if="detailPlant.boutique_url" class="bg-bg/30 p-3 rounded-xl border border-bg-soft">
<div v-if="detailPlant.varieties?.[0]?.boutique_url" 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">Lien</span>
<a :href="detailPlant.boutique_url" target="_blank" rel="noopener"
<a :href="detailPlant.varieties?.[0]?.boutique_url" target="_blank" rel="noopener"
class="text-blue text-xs hover:underline truncate block">🔗 Voir le produit</a>
</div>
</div>
@@ -572,7 +572,7 @@ const filteredPlants = computed(() => {
result = result.filter(p => {
const group = plantGroups.value.get((p.nom_commun || '').toLowerCase()) || []
return group.some(v =>
v.nom_commun?.toLowerCase().includes(q) || v.variete?.toLowerCase().includes(q)
v.nom_commun?.toLowerCase().includes(q) || v.varieties?.[0]?.variete?.toLowerCase().includes(q)
)
})
}
@@ -684,15 +684,16 @@ function startEdit(p: Plant) {
closeDetail()
editPlant.value = p
const v0 = p.varieties?.[0]
Object.assign(form, {
nom_commun: p.nom_commun || '', variete: p.variete || '', famille: p.famille || '',
nom_commun: p.nom_commun || '', variete: v0?.variete || '', famille: p.famille || '',
categorie: p.categorie || 'potager', besoin_eau: p.besoin_eau || 'moyen', besoin_soleil: p.besoin_soleil || 'plein soleil',
plantation_mois: p.plantation_mois || '', notes: p.notes || '',
associations_favorables: [...(withAssoc.associations_favorables ?? [])],
associations_defavorables: [...(withAssoc.associations_defavorables ?? [])],
boutique_nom: p.boutique_nom || '', boutique_url: p.boutique_url || '',
prix_achat: p.prix_achat ?? null, date_achat: p.date_achat || '',
poids: p.poids || '', dluo: p.dluo || '',
boutique_nom: v0?.boutique_nom || '', boutique_url: v0?.boutique_url || '',
prix_achat: v0?.prix_achat ?? null, date_achat: v0?.date_achat || '',
poids: v0?.poids || '', dluo: v0?.dluo || '',
})
assocFilter.value = ''
showAssocModal.value = false

View File

@@ -416,7 +416,7 @@ const plantingsByGarden = computed(() => {
function plantingLabel(p: Planting): string {
const plant = plantsStore.plants.find(pl => pl.id === p.variety_id)
const nom = plant
? [plant.nom_commun, plant.variete].filter(Boolean).join(' ')
? [plant.nom_commun, plant.varieties?.[0]?.variete].filter(Boolean).join(' ')
: `Variété #${p.variety_id}`
const date = p.date_plantation ? ` (${fmtDate(p.date_plantation)})` : ''
return `${nom}${date}`