Files
gartenmanager/frontend/src/views/BedsView.vue
Faultier314 834a3bf4d5 feat: Phase 1 complete – full working application
Backend (FastAPI):
- REST API: auth, plants, beds, plantings
- CRUD layer with CRUDBase
- Pydantic v2 schemas for all entities
- Alembic migration: complete schema + all enums
- Seed data: 28 global plants + 15 compatibilities

Frontend (Vue 3 + PrimeVue):
- Axios client with JWT interceptor + auto-refresh
- Pinia stores: auth, beds, plants
- Views: Login, Beds, BedDetail, PlantLibrary
- Components: AppLayout, BedForm, PlantingForm, PlantForm

Docker:
- docker-compose.yml (production)
- docker-compose.dev.yml (development with hot-reload)
- Nginx config with SPA fallback + API proxy
- Multi-stage frontend Dockerfile
- .env.example, .gitignore

Version: 1.0.0-alpha
2026-04-06 07:45:00 +02:00

137 lines
4.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>
<div class="page-header">
<div>
<h2>Beete</h2>
<p class="subtitle">Übersicht aller Beete in diesem Tenant</p>
</div>
<Button label="Neues Beet" icon="pi pi-plus" @click="openCreateDialog" />
</div>
<DataTable
:value="bedsStore.beds"
:loading="bedsStore.loading"
striped-rows
hover
responsive-layout="scroll"
class="mt-3"
>
<template #empty>Noch keine Beete vorhanden.</template>
<Column field="name" header="Name" sortable>
<template #body="{ data }">
<router-link :to="`/beete/${data.id}`" class="bed-link">{{ data.name }}</router-link>
</template>
</Column>
<Column header="Größe (m²)" sortable sort-field="area_m2">
<template #body="{ data }">
{{ data.area_m2 }}
<span class="dim">({{ data.width_m }} × {{ data.length_m }} m)</span>
</template>
</Column>
<Column field="location" header="Lage" sortable>
<template #body="{ data }">
<Tag :value="locationLabel(data.location)" :severity="locationSeverity(data.location)" />
</template>
</Column>
<Column field="soil_type" header="Bodentyp" sortable>
<template #body="{ data }">{{ soilLabel(data.soil_type) }}</template>
</Column>
<Column header="Aktionen" style="width: 8rem">
<template #body="{ data }">
<div class="actions">
<Button icon="pi pi-pencil" text severity="secondary" @click="openEditDialog(data)" />
<Button icon="pi pi-trash" text severity="danger" @click="confirmDelete(data)" />
</div>
</template>
</Column>
</DataTable>
<!-- Create/Edit Dialog -->
<Dialog
v-model:visible="dialogVisible"
:header="editingBed ? 'Beet bearbeiten' : 'Neues Beet'"
modal
style="width: 480px"
>
<BedForm :initial="editingBed" @save="handleSave" @cancel="dialogVisible = false" />
</Dialog>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useBedsStore } from '@/stores/beds'
import { useConfirm } from 'primevue/useconfirm'
import { useToast } from 'primevue/usetoast'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import Button from 'primevue/button'
import Tag from 'primevue/tag'
import Dialog from 'primevue/dialog'
import BedForm from '@/components/BedForm.vue'
const bedsStore = useBedsStore()
const confirm = useConfirm()
const toast = useToast()
const dialogVisible = ref(false)
const editingBed = ref(null)
onMounted(() => bedsStore.fetchBeds())
function openCreateDialog() {
editingBed.value = null
dialogVisible.value = true
}
function openEditDialog(bed) {
editingBed.value = bed
dialogVisible.value = true
}
async function handleSave(payload) {
try {
if (editingBed.value) {
await bedsStore.updateBed(editingBed.value.id, payload)
toast.add({ severity: 'success', summary: 'Gespeichert', detail: 'Beet aktualisiert.', life: 3000 })
} else {
await bedsStore.createBed(payload)
toast.add({ severity: 'success', summary: 'Erstellt', detail: 'Neues Beet angelegt.', life: 3000 })
}
dialogVisible.value = false
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.detail || 'Speichern fehlgeschlagen.', life: 5000 })
}
}
function confirmDelete(bed) {
confirm.require({
message: `Beet „${bed.name}" wirklich löschen?`,
header: 'Bestätigung',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Löschen',
rejectLabel: 'Abbrechen',
acceptClass: 'p-button-danger',
accept: async () => {
await bedsStore.deleteBed(bed.id)
toast.add({ severity: 'info', summary: 'Gelöscht', detail: 'Beet wurde entfernt.', life: 3000 })
},
})
}
const locationLabel = (v) => ({ sonnig: 'Sonnig', halbschatten: 'Halbschatten', schatten: 'Schatten' }[v] || v)
const locationSeverity = (v) => ({ sonnig: 'warning', halbschatten: 'info', schatten: 'secondary' }[v] || 'secondary')
const soilLabel = (v) => ({ normal: 'Normal', sandig: 'Sandig', lehmig: 'Lehmig', humusreich: 'Humusreich' }[v] || v)
</script>
<style scoped>
.page-header { display: flex; align-items: flex-start; justify-content: space-between; }
.page-header h2 { font-size: 1.4rem; font-weight: 700; margin-bottom: 0.15rem; }
.subtitle { color: var(--text-color-secondary); font-size: 0.875rem; }
.bed-link { color: var(--green-700); text-decoration: none; font-weight: 600; }
.bed-link:hover { text-decoration: underline; }
.dim { color: var(--text-color-secondary); font-size: 0.8rem; margin-left: 0.3rem; }
.actions { display: flex; gap: 0.25rem; }
.mt-3 { margin-top: 1rem; }
</style>