137 lines
4.7 KiB
Vue
137 lines
4.7 KiB
Vue
|
|
<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 }} m²
|
|||
|
|
<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>
|