243 lines
9.7 KiB
Vue
243 lines
9.7 KiB
Vue
<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>
|