Files
gartenmanager/frontend/src/components/BedForm.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

103 lines
3.0 KiB
Vue

<template>
<form @submit.prevent="handleSubmit" class="form">
<div class="field">
<label>Name *</label>
<InputText v-model="form.name" required class="w-full" />
</div>
<div class="field-row">
<div class="field">
<label>Breite (m) *</label>
<InputNumber v-model="form.width_m" :min="0.1" :max="99" :step="0.1" :min-fraction-digits="1" required class="w-full" />
</div>
<div class="field">
<label>Länge (m) *</label>
<InputNumber v-model="form.length_m" :min="0.1" :max="99" :step="0.1" :min-fraction-digits="1" required class="w-full" />
</div>
</div>
<div class="field">
<label>Lage *</label>
<Dropdown
v-model="form.location"
:options="locationOptions"
option-label="label"
option-value="value"
required
class="w-full"
/>
</div>
<div class="field">
<label>Bodentyp</label>
<Dropdown
v-model="form.soil_type"
:options="soilOptions"
option-label="label"
option-value="value"
class="w-full"
/>
</div>
<div class="field">
<label>Notizen</label>
<Textarea v-model="form.notes" rows="2" class="w-full" />
</div>
<div class="dialog-footer">
<Button type="button" label="Abbrechen" severity="secondary" text @click="$emit('cancel')" />
<Button type="submit" label="Speichern" icon="pi pi-check" />
</div>
</form>
</template>
<script setup>
import { reactive, watch } from 'vue'
import InputText from 'primevue/inputtext'
import InputNumber from 'primevue/inputnumber'
import Dropdown from 'primevue/dropdown'
import Textarea from 'primevue/textarea'
import Button from 'primevue/button'
const props = defineProps({ initial: { type: Object, default: null } })
const emit = defineEmits(['save', 'cancel'])
const form = reactive({
name: '',
width_m: 1.0,
length_m: 1.0,
location: 'sonnig',
soil_type: 'normal',
notes: '',
})
watch(() => props.initial, (val) => {
if (val) Object.assign(form, { name: val.name, width_m: Number(val.width_m), length_m: Number(val.length_m), location: val.location, soil_type: val.soil_type, notes: val.notes || '' })
}, { immediate: true })
const locationOptions = [
{ label: 'Sonnig', value: 'sonnig' },
{ label: 'Halbschatten', value: 'halbschatten' },
{ label: 'Schatten', value: 'schatten' },
]
const soilOptions = [
{ label: 'Normal', value: 'normal' },
{ label: 'Sandig', value: 'sandig' },
{ label: 'Lehmig', value: 'lehmig' },
{ label: 'Humusreich', value: 'humusreich' },
]
function handleSubmit() {
emit('save', { ...form, notes: form.notes || null })
}
</script>
<style scoped>
.form { display: flex; flex-direction: column; gap: 0.75rem; }
.field { display: flex; flex-direction: column; gap: 0.3rem; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
label { font-size: 0.875rem; font-weight: 600; }
.w-full { width: 100%; }
.dialog-footer { display: flex; justify-content: flex-end; gap: 0.5rem; padding-top: 0.5rem; }
</style>