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
103 lines
3.0 KiB
Vue
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>
|