Files
jardin/frontend/src/views/JardinDetailView.vue
2026-03-01 07:21:46 +01:00

243 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="p-4 max-w-5xl mx-auto">
<button class="text-text-muted text-sm mb-4 hover:text-text" @click="router.back()"> Retour</button>
<div v-if="garden">
<div class="flex items-start justify-between mb-1">
<h1 class="text-2xl font-bold text-green">{{ garden.nom }}</h1>
</div>
<p class="text-text-muted text-sm mb-6">
{{ garden.type }} · {{ garden.exposition ?? 'exposition non définie' }}
<span v-if="garden.sol_type"> · Sol : {{ garden.sol_type }}</span>
<span v-if="garden.surface_m2 != null"> · {{ garden.surface_m2 }} m²</span>
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
<div class="bg-bg-soft border border-bg-hard rounded-lg p-3 text-sm">
<div class="text-text-muted text-xs mb-1">Dimensions</div>
<div class="text-text">
<span v-if="garden.longueur_m != null && garden.largeur_m != null">
{{ garden.longueur_m }} m × {{ garden.largeur_m }} m
</span>
<span v-else>Non renseignées</span>
</div>
</div>
<div class="bg-bg-soft border border-bg-hard rounded-lg p-3 text-sm">
<div class="text-text-muted text-xs mb-1">Géolocalisation</div>
<div class="text-text">
<span v-if="garden.latitude != null && garden.longitude != null">
{{ garden.latitude }}, {{ garden.longitude }}
<span v-if="garden.altitude != null"> · {{ garden.altitude }} m</span>
</span>
<span v-else>Non renseignée</span>
</div>
<div v-if="garden.adresse" class="text-text-muted text-xs mt-1">{{ garden.adresse }}</div>
</div>
</div>
<div v-if="garden.photo_parcelle" class="mb-6">
<div class="text-text-muted text-xs uppercase tracking-widest mb-2">Photo parcelle</div>
<img :src="garden.photo_parcelle" alt="Photo parcelle"
class="w-full max-h-72 object-cover rounded-lg border border-bg-hard bg-bg-soft" />
</div>
<!-- En-tête grille -->
<div class="flex items-center justify-between mb-3">
<h2 class="text-text-muted text-xs uppercase tracking-widest">
Grille {{ garden.grille_largeur }}×{{ garden.grille_hauteur }}
</h2>
<div class="flex items-center gap-2">
<span v-if="editMode" class="text-xs text-orange">Cliquez pour activer/désactiver une zone</span>
<button @click="editMode = !editMode"
:class="['px-3 py-1 rounded-full text-xs font-bold border transition-all',
editMode ? 'bg-orange text-bg border-orange' : 'border-bg-soft text-text-muted hover:border-orange hover:text-orange']">
{{ editMode ? '✓ Terminer' : '✏️ Éditer zones' }}
</button>
</div>
</div>
<!-- Légende -->
<div class="flex flex-wrap gap-4 mb-3 text-[10px] text-text-muted font-bold uppercase tracking-wider">
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded bg-bg border border-bg-soft inline-block"></span>Libre
</span>
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded bg-green/20 border border-green/60 inline-block"></span>Planté
</span>
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded bg-red/20 border border-red/40 inline-block"></span>Non cultivable
</span>
</div>
<div class="overflow-x-auto pb-2">
<div
class="grid gap-1 w-max"
:style="`grid-template-columns: repeat(${garden.grille_largeur}, 56px)`"
>
<div
v-for="cell in displayCells" :key="`${cell.row}-${cell.col}`"
:class="[
'w-[56px] h-[56px] border rounded-md flex flex-col items-center justify-center text-[10px] select-none transition-all overflow-hidden',
editMode && cell.etat !== 'occupe' && !getCellPlanting(cell) ? 'cursor-pointer' : '',
getCellPlanting(cell)
? 'bg-green/10 border-green/60 text-green'
: cell.etat === 'non_cultivable'
? 'bg-red/10 border-red/30 text-red/50'
: 'bg-bg-soft border-bg-hard text-text-muted',
editMode && !getCellPlanting(cell) && cell.etat !== 'occupe'
? (cell.etat === 'non_cultivable' ? 'hover:bg-red/20 hover:border-red/60' : 'hover:border-orange hover:bg-orange/10')
: ''
]"
:title="getCellTitle(cell)"
@click="editMode && !getCellPlanting(cell) && cell.etat !== 'occupe' ? toggleNonCultivable(cell) : undefined"
>
<span :class="['font-mono leading-none', getCellPlanting(cell) ? 'text-[9px] font-bold' : '']">
{{ cell.libelle }}
</span>
<span v-if="getCellPlanting(cell)" class="text-[8px] text-green/80 leading-none mt-0.5 px-0.5 text-center truncate w-full">
{{ plantShortName(getCellPlanting(cell)!) }}
</span>
<span v-else-if="cell.etat === 'non_cultivable'" class="text-[9px] leading-none"></span>
</div>
</div>
</div>
<!-- Saving indicator -->
<div v-if="saving" class="mt-2 text-xs text-text-muted animate-pulse">Enregistrement</div>
<!-- Résumé plantations actives -->
<div v-if="activePlantings.length" class="mt-6">
<h3 class="text-text-muted text-xs uppercase tracking-widest mb-2">Plantations actives ({{ activePlantings.length }})</h3>
<div class="flex flex-wrap gap-2">
<div v-for="p in activePlantings" :key="p.id"
class="bg-bg-soft border border-green/30 rounded px-2 py-1 text-xs text-green flex items-center gap-1.5">
<span class="text-text-muted font-mono text-[10px]">
{{ plantCellLabel(p) }}
</span>
{{ plantName(p) }}
<span class="text-text-muted">· {{ p.statut }}</span>
</div>
</div>
</div>
</div>
<div v-else class="text-text-muted text-sm">Chargement...</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { gardensApi, type Garden, type GardenCell } from '@/api/gardens'
import { plantingsApi, type Planting } from '@/api/plantings'
import { plantsApi, type Plant } from '@/api/plants'
const route = useRoute()
const router = useRouter()
const garden = ref<Garden | null>(null)
const cells = ref<GardenCell[]>([])
const plantings = ref<Planting[]>([])
const plants = ref<Plant[]>([])
const editMode = ref(false)
const saving = ref(false)
const displayCells = computed(() => {
if (!garden.value) return []
const map = new Map(cells.value.map(c => [`${c.row}-${c.col}`, c]))
const result: GardenCell[] = []
for (let row = 0; row < garden.value.grille_hauteur; row++) {
for (let col = 0; col < garden.value.grille_largeur; col++) {
result.push(map.get(`${row}-${col}`) ?? {
col, row,
libelle: `${String.fromCharCode(65 + row)}${col + 1}`,
etat: 'libre',
})
}
}
return result
})
// Plantations actives (ni terminées ni échouées) pour ce jardin
const activePlantings = computed(() =>
plantings.value.filter(p =>
p.garden_id === garden.value?.id &&
p.statut !== 'termine' &&
p.statut !== 'echoue'
)
)
// Map cellId → Planting active
const activePlantingsByCellId = computed(() => {
const map = new Map<number, Planting>()
for (const p of activePlantings.value) {
if (p.cell_ids?.length) {
p.cell_ids.forEach(cid => map.set(cid, p))
} else if (p.cell_id) {
map.set(p.cell_id, p)
}
}
return map
})
function getCellPlanting(cell: GardenCell): Planting | undefined {
if (cell.id == null) return undefined
return activePlantingsByCellId.value.get(cell.id)
}
function plantName(p: Planting): string {
const plant = plants.value.find(pl => pl.id === p.variety_id)
return plant?.nom_commun ?? `Plante #${p.variety_id}`
}
function plantShortName(p: Planting): string {
return plantName(p).slice(0, 8)
}
function plantCellLabel(p: Planting): string {
const ids = p.cell_ids?.length ? p.cell_ids : (p.cell_id ? [p.cell_id] : [])
if (!ids.length) return '—'
return ids
.map(cid => cells.value.find(c => c.id === cid)?.libelle ?? `#${cid}`)
.join(', ')
}
function getCellTitle(cell: GardenCell): string {
const planting = getCellPlanting(cell)
if (planting) return `Planté : ${plantName(planting)} (${planting.statut})`
if (cell.etat === 'non_cultivable') return 'Non cultivable — cliquer pour rendre cultivable'
if (editMode.value) return 'Marquer comme non cultivable'
return cell.libelle ?? ''
}
async function toggleNonCultivable(cell: GardenCell) {
if (!garden.value?.id || saving.value) return
const newEtat = cell.etat === 'non_cultivable' ? 'libre' : 'non_cultivable'
saving.value = true
try {
if (cell.id) {
const updated = await gardensApi.updateCell(garden.value.id, cell.id, { ...cell, etat: newEtat })
const idx = cells.value.findIndex(c => c.id === cell.id)
if (idx !== -1) cells.value[idx] = updated
} else {
const created = await gardensApi.createCell(garden.value.id, {
col: cell.col, row: cell.row,
libelle: cell.libelle,
etat: newEtat,
garden_id: garden.value.id,
})
cells.value.push(created)
}
} catch { /* L'intercepteur affiche l'erreur */ }
finally { saving.value = false }
}
onMounted(async () => {
const id = Number(route.params.id)
;[garden.value, cells.value, plantings.value, plants.value] = await Promise.all([
gardensApi.get(id),
gardensApi.cells(id),
plantingsApi.list(),
plantsApi.list(),
])
})
</script>