feat(frontend): vues MVP — dashboard, jardins, grille, variétés, tâches, plantations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,54 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Dashboard</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold text-green mb-6">Tableau de bord</h1>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">Tâches à faire</h2>
|
||||
<div v-if="!pendingTasks.length" class="text-text-muted text-sm py-2">Aucune tâche en attente.</div>
|
||||
<div
|
||||
v-for="t in pendingTasks"
|
||||
:key="t.id"
|
||||
class="bg-bg-soft rounded-lg p-3 mb-2 flex items-center gap-3 border border-bg-hard"
|
||||
>
|
||||
<span :class="{
|
||||
'text-red': t.priorite === 'haute',
|
||||
'text-yellow': t.priorite === 'normale',
|
||||
'text-text-muted': t.priorite === 'basse'
|
||||
}">●</span>
|
||||
<span class="text-text text-sm flex-1">{{ t.titre }}</span>
|
||||
<button
|
||||
class="text-xs text-green hover:underline px-2"
|
||||
@click="tasksStore.updateStatut(t.id!, 'fait')"
|
||||
>✓ Fait</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">Jardins</h2>
|
||||
<div v-if="gardensStore.loading" class="text-text-muted text-sm">Chargement...</div>
|
||||
<div
|
||||
v-for="g in gardensStore.gardens"
|
||||
:key="g.id"
|
||||
class="bg-bg-soft rounded-lg p-4 mb-2 border border-bg-hard cursor-pointer hover:border-green transition-colors"
|
||||
@click="router.push(`/jardins/${g.id}`)"
|
||||
>
|
||||
<span class="text-text font-medium">{{ g.nom }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted px-2 py-0.5 bg-bg rounded">{{ g.type }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGardensStore } from '@/stores/gardens'
|
||||
import { useTasksStore } from '@/stores/tasks'
|
||||
|
||||
const router = useRouter()
|
||||
const gardensStore = useGardensStore()
|
||||
const tasksStore = useTasksStore()
|
||||
const pendingTasks = computed(() => tasksStore.tasks.filter(t => t.statut === 'a_faire').slice(0, 5))
|
||||
|
||||
onMounted(() => { gardensStore.fetchAll(); tasksStore.fetchAll() })
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Détail jardin</h1>
|
||||
<div class="p-4 max-w-3xl mx-auto">
|
||||
<button class="text-text-muted text-sm mb-4 hover:text-text" @click="router.back()">← Retour</button>
|
||||
|
||||
<div v-if="garden">
|
||||
<h1 class="text-2xl font-bold text-green mb-1">{{ garden.nom }}</h1>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">
|
||||
Grille {{ garden.grille_largeur }}×{{ garden.grille_hauteur }}
|
||||
</h2>
|
||||
<div class="overflow-x-auto pb-2">
|
||||
<div
|
||||
class="grid gap-1 w-max"
|
||||
:style="`grid-template-columns: repeat(${garden.grille_largeur}, 52px)`"
|
||||
>
|
||||
<div
|
||||
v-for="cell in displayCells" :key="`${cell.row}-${cell.col}`"
|
||||
class="w-[52px] h-[52px] bg-bg-soft border border-bg-hard rounded-md flex items-center justify-center text-xs text-text-muted cursor-pointer hover:border-green transition-colors select-none"
|
||||
:class="{ 'border-orange/60 bg-orange/10 text-orange': cell.etat === 'occupe' }"
|
||||
:title="cell.libelle"
|
||||
>
|
||||
{{ cell.libelle }}
|
||||
</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'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const garden = ref<Garden | null>(null)
|
||||
const cells = ref<GardenCell[]>([])
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const id = Number(route.params.id)
|
||||
garden.value = await gardensApi.get(id)
|
||||
cells.value = await gardensApi.cells(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,84 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Jardins</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-green">Jardins</h1>
|
||||
<button
|
||||
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90 transition-opacity"
|
||||
@click="showForm = !showForm"
|
||||
>+ Nouveau</button>
|
||||
</div>
|
||||
|
||||
<form v-if="showForm" class="bg-bg-soft rounded-lg p-4 mb-6 border border-green/30" @submit.prevent="submit">
|
||||
<div class="grid gap-3">
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Nom *</label>
|
||||
<input v-model="form.nom" required
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Type</label>
|
||||
<select v-model="form.type" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
|
||||
<option value="plein_air">Plein air</option>
|
||||
<option value="serre">Serre</option>
|
||||
<option value="tunnel">Tunnel</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Largeur grille</label>
|
||||
<input v-model.number="form.grille_largeur" type="number" min="1" max="20"
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Hauteur grille</label>
|
||||
<input v-model.number="form.grille_hauteur" type="number" min="1" max="20"
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold">Créer</button>
|
||||
<button type="button" class="text-text-muted text-sm px-4 py-2 hover:text-text" @click="showForm = false">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="store.loading" class="text-text-muted text-sm">Chargement...</div>
|
||||
<div
|
||||
v-for="g in store.gardens" :key="g.id"
|
||||
class="bg-bg-soft rounded-lg p-4 mb-3 border border-bg-hard flex items-center gap-3 cursor-pointer hover:border-green transition-colors group"
|
||||
@click="router.push(`/jardins/${g.id}`)"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="text-text font-medium group-hover:text-green transition-colors">{{ g.nom }}</div>
|
||||
<div class="text-text-muted text-xs mt-1">{{ g.type }} · {{ g.grille_largeur }}×{{ g.grille_hauteur }} cases</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-text-muted hover:text-red text-sm px-2 py-1 rounded hover:bg-bg transition-colors"
|
||||
@click.stop="store.remove(g.id!)"
|
||||
>✕</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!store.loading && !store.gardens.length" class="text-text-muted text-sm text-center py-8">
|
||||
Aucun jardin. Créez-en un !
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGardensStore } from '@/stores/gardens'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useGardensStore()
|
||||
const showForm = ref(false)
|
||||
const form = reactive({ nom: '', type: 'plein_air', grille_largeur: 6, grille_hauteur: 4 })
|
||||
|
||||
onMounted(() => store.fetchAll())
|
||||
|
||||
async function submit() {
|
||||
await store.create({ ...form })
|
||||
showForm.value = false
|
||||
Object.assign(form, { nom: '', type: 'plein_air', grille_largeur: 6, grille_hauteur: 4 })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Calendrier lunaire</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold text-green mb-4">Calendrier lunaire</h1>
|
||||
<p class="text-text-muted text-sm">Calendrier lunaire — prochaine étape.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Planning</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold text-green mb-4">Planning</h1>
|
||||
<p class="text-text-muted text-sm">Vue calendrier — prochaine étape.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Plantations</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold text-green mb-6">Plantations</h1>
|
||||
<div v-if="store.loading" class="text-text-muted text-sm">Chargement...</div>
|
||||
<div v-for="p in store.plantings" :key="p.id"
|
||||
class="bg-bg-soft rounded-lg p-4 mb-3 border border-bg-hard">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<div class="text-text font-medium">Plantation #{{ p.id }}</div>
|
||||
<div class="text-text-muted text-xs mt-1">
|
||||
Jardin {{ p.garden_id }} · Variété {{ p.variety_id }} · {{ p.quantite }} plant(s)
|
||||
</div>
|
||||
<span class="inline-block mt-2 text-xs px-2 py-0.5 rounded" :class="{
|
||||
'bg-blue/20 text-blue': p.statut === 'prevu',
|
||||
'bg-green/20 text-green': p.statut === 'en_cours',
|
||||
'bg-text-muted/20 text-text-muted': p.statut === 'termine',
|
||||
'bg-red/20 text-red': p.statut === 'echoue',
|
||||
}">{{ p.statut }}</span>
|
||||
</div>
|
||||
<button class="text-text-muted hover:text-red text-sm" @click="store.remove(p.id!)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!store.loading && !store.plantings.length" class="text-text-muted text-sm text-center py-8">
|
||||
Aucune plantation enregistrée.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { usePlantingsStore } from '@/stores/plantings'
|
||||
|
||||
const store = usePlantingsStore()
|
||||
onMounted(() => store.fetchAll())
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Réglages</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold text-green mb-4">Réglages</h1>
|
||||
<p class="text-text-muted text-sm">Paramètres et export/import — prochaine étape.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,86 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Tâches</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-green">Tâches</h1>
|
||||
<button
|
||||
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
|
||||
@click="showForm = !showForm"
|
||||
>+ Nouvelle</button>
|
||||
</div>
|
||||
|
||||
<form v-if="showForm" class="bg-bg-soft rounded-lg p-4 mb-6 border border-green/30" @submit.prevent="submit">
|
||||
<div class="grid gap-3">
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Titre *</label>
|
||||
<input v-model="form.titre" required
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Priorité</label>
|
||||
<select v-model="form.priorite" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
|
||||
<option value="basse">Basse</option>
|
||||
<option value="normale">Normale</option>
|
||||
<option value="haute">Haute</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Échéance</label>
|
||||
<input v-model="form.echeance" type="date"
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold">Créer</button>
|
||||
<button type="button" class="text-text-muted text-sm px-4 py-2 hover:text-text" @click="showForm = false">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-for="[groupe, label] in groupes" :key="groupe" class="mb-6">
|
||||
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-2">{{ label }}</h2>
|
||||
<div v-if="!byStatut(groupe).length" class="text-text-muted text-xs pl-2 mb-2">—</div>
|
||||
<div v-for="t in byStatut(groupe)" :key="t.id"
|
||||
class="bg-bg-soft rounded-lg p-3 mb-2 flex items-center gap-3 border border-bg-hard">
|
||||
<span :class="{
|
||||
'text-red': t.priorite === 'haute',
|
||||
'text-yellow': t.priorite === 'normale',
|
||||
'text-text-muted': t.priorite === 'basse'
|
||||
}">●</span>
|
||||
<span class="text-text text-sm flex-1">{{ t.titre }}</span>
|
||||
<div class="flex gap-1 items-center">
|
||||
<button v-if="t.statut === 'a_faire'" class="text-xs text-blue hover:underline"
|
||||
@click="store.updateStatut(t.id!, 'en_cours')">→ En cours</button>
|
||||
<button v-if="t.statut === 'en_cours'" class="text-xs text-green hover:underline"
|
||||
@click="store.updateStatut(t.id!, 'fait')">✓ Fait</button>
|
||||
<button class="text-xs text-text-muted hover:text-red ml-2" @click="store.remove(t.id!)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useTasksStore } from '@/stores/tasks'
|
||||
|
||||
const store = useTasksStore()
|
||||
const showForm = ref(false)
|
||||
const form = reactive({ titre: '', priorite: 'normale', statut: 'a_faire', echeance: '' })
|
||||
|
||||
const groupes: [string, string][] = [
|
||||
['a_faire', 'À faire'],
|
||||
['en_cours', 'En cours'],
|
||||
['fait', 'Terminé'],
|
||||
]
|
||||
|
||||
const byStatut = (s: string) => store.tasks.filter(t => t.statut === s)
|
||||
|
||||
onMounted(() => store.fetchAll())
|
||||
|
||||
async function submit() {
|
||||
await store.create({ ...form })
|
||||
showForm.value = false
|
||||
Object.assign(form, { titre: '', priorite: 'normale', statut: 'a_faire', echeance: '' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,82 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold text-green">Variétés</h1>
|
||||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-green">Variétés</h1>
|
||||
<button
|
||||
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
|
||||
@click="showForm = !showForm"
|
||||
>+ Nouvelle</button>
|
||||
</div>
|
||||
|
||||
<form v-if="showForm" class="bg-bg-soft rounded-lg p-4 mb-6 border border-green/30" @submit.prevent="submit">
|
||||
<div class="grid gap-3">
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Nom commun *</label>
|
||||
<input v-model="form.nom_commun" required
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Variété</label>
|
||||
<input v-model="form.variete"
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Famille</label>
|
||||
<input v-model="form.famille"
|
||||
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-text-muted text-xs block mb-1">Besoin en eau</label>
|
||||
<select v-model="form.besoin_eau" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
|
||||
<option value="">—</option>
|
||||
<option value="faible">Faible</option>
|
||||
<option value="moyen">Moyen</option>
|
||||
<option value="fort">Fort</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold">Créer</button>
|
||||
<button type="button" class="text-text-muted text-sm px-4 py-2 hover:text-text" @click="showForm = false">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="store.loading" class="text-text-muted text-sm">Chargement...</div>
|
||||
<div v-for="v in store.varieties" :key="v.id"
|
||||
class="bg-bg-soft rounded-lg p-4 mb-2 border border-bg-hard flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<div class="text-text font-medium">
|
||||
{{ v.nom_commun }}
|
||||
<span v-if="v.variete" class="text-text-muted text-xs"> — {{ v.variete }}</span>
|
||||
</div>
|
||||
<div class="text-text-muted text-xs mt-1">{{ v.famille }}</div>
|
||||
<div class="flex gap-2 mt-2 flex-wrap">
|
||||
<span v-if="v.besoin_eau" class="text-xs px-2 py-0.5 bg-bg rounded text-blue">💧 {{ v.besoin_eau }}</span>
|
||||
<span v-if="v.espacement_cm" class="text-xs px-2 py-0.5 bg-bg rounded text-text-muted">↔ {{ v.espacement_cm }}cm</span>
|
||||
<span v-if="v.plantation_mois" class="text-xs px-2 py-0.5 bg-bg rounded text-green">🌱 mois {{ v.plantation_mois }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="text-text-muted hover:text-red text-sm px-2 py-1 rounded hover:bg-bg transition-colors"
|
||||
@click="store.remove(v.id!)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useVarietiesStore } from '@/stores/varieties'
|
||||
|
||||
const store = useVarietiesStore()
|
||||
const showForm = ref(false)
|
||||
const form = reactive({ nom_commun: '', variete: '', famille: '', besoin_eau: '' })
|
||||
|
||||
onMounted(() => store.fetchAll())
|
||||
|
||||
async function submit() {
|
||||
await store.create({ ...form })
|
||||
showForm.value = false
|
||||
Object.assign(form, { nom_commun: '', variete: '', famille: '', besoin_eau: '' })
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user