feat: add parentId dropdown and structured traits form mask to UI
Deploy API / deploy (push) Failing after 16s
Deploy API / deploy (push) Failing after 16s
This commit is contained in:
+132
-33
@@ -15,48 +15,115 @@
|
||||
<span class="block sm:inline" v-text="errorMsg"></span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
|
||||
<!-- UNITS SECTION -->
|
||||
<div class="bg-white p-6 rounded-lg shadow">
|
||||
<h2 class="text-2xl font-bold mb-4">Units (Räume/Objekte)</h2>
|
||||
<form @submit.prevent="createUnit" class="mb-4 flex gap-2">
|
||||
<input v-model="newUnit.name" placeholder="Name" class="border p-2 rounded flex-1" required>
|
||||
<input v-model="newUnit.traits" placeholder="Traits (JSON)" class="border p-2 rounded flex-1">
|
||||
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700">Neu</button>
|
||||
</form>
|
||||
<ul class="space-y-2">
|
||||
<li v-for="unit in units" :key="unit.id" class="border p-4 rounded flex flex-col gap-2 bg-gray-50">
|
||||
<div class="flex justify-between items-center">
|
||||
<strong v-text="unit.name"></strong>
|
||||
<button @click="deleteUnit(unit.id)" class="text-red-500 hover:underline">Löschen</button>
|
||||
<h2 class="text-2xl font-bold mb-4 flex justify-between items-center">
|
||||
Units (Räume/Objekte)
|
||||
<span class="text-sm font-normal text-gray-500 bg-gray-100 px-2 py-1 rounded" v-text="units.length + ' Einträge'"></span>
|
||||
</h2>
|
||||
|
||||
<form @submit.prevent="createUnit" class="mb-6 bg-indigo-50 p-4 rounded border border-indigo-100">
|
||||
<h3 class="font-bold mb-3 text-indigo-800">Neue Unit anlegen</h3>
|
||||
|
||||
<div class="flex gap-2 mb-3">
|
||||
<input v-model="newUnit.name" placeholder="Name (z.B. Appartement 1)" class="border border-indigo-200 p-2 rounded flex-1 focus:ring-2 focus:ring-indigo-400 outline-none" required>
|
||||
<select v-model="newUnit.parentId" class="border border-indigo-200 p-2 rounded flex-1 focus:ring-2 focus:ring-indigo-400 outline-none">
|
||||
<option value="">-- Kein Parent (Top-Level) --</option>
|
||||
<option v-for="u in units" :value="u.id" v-text="u.name"></option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea v-model="unit.traitsRaw" class="w-full border p-2 text-sm font-mono rounded" rows="3"></textarea>
|
||||
<button @click="updateUnit(unit)" class="bg-green-500 text-white px-2 py-1 rounded text-sm self-end hover:bg-green-600">Speichern</button>
|
||||
|
||||
<h4 class="text-xs font-semibold text-gray-500 mb-1 uppercase tracking-wider">Eigenschaften (Traits)</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 mb-3 text-sm">
|
||||
<label class="flex items-center gap-2 bg-white p-2 rounded border border-indigo-100 cursor-pointer hover:bg-indigo-100 transition">
|
||||
<input type="checkbox" v-model="newUnit.traitsObj.is_rentable" class="w-4 h-4 text-indigo-600"> Vermietbar
|
||||
</label>
|
||||
<input type="number" v-model.number="newUnit.traitsObj.base_price" placeholder="Basispreis (€)" class="border border-indigo-100 p-2 rounded focus:ring-2 outline-none">
|
||||
<input type="number" v-model.number="newUnit.traitsObj.area_sqm" placeholder="Fläche (m²)" class="border border-indigo-100 p-2 rounded focus:ring-2 outline-none">
|
||||
<input type="number" v-model.number="newUnit.traitsObj.sleep_capacity" placeholder="Schlafplätze" class="border border-indigo-100 p-2 rounded focus:ring-2 outline-none">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 w-full transition shadow-md font-semibold">Unit erstellen</button>
|
||||
</form>
|
||||
|
||||
<ul class="space-y-4">
|
||||
<li v-for="unit in units" :key="unit.id" class="border border-gray-200 p-4 rounded flex flex-col gap-3 bg-white shadow-sm hover:shadow transition">
|
||||
|
||||
<div class="flex justify-between items-start border-b border-gray-100 pb-2">
|
||||
<div>
|
||||
<strong class="text-lg text-gray-800" v-text="unit.name"></strong>
|
||||
<div v-if="unit.parentId" class="text-xs text-gray-500 mt-1">
|
||||
↳ Befindet sich in: <strong class="text-indigo-600" v-text="getUnitName(unit.parentId)"></strong>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="deleteUnit(unit.id)" class="text-red-400 hover:text-red-600 hover:bg-red-50 px-2 py-1 rounded transition text-sm font-bold">✖ Löschen</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input v-model="unit.name" class="border p-2 rounded text-sm flex-1 bg-gray-50 focus:bg-white focus:ring-2 outline-none" placeholder="Name">
|
||||
<select v-model="unit.parentId" class="border p-2 rounded text-sm flex-1 bg-gray-50 focus:bg-white focus:ring-2 outline-none">
|
||||
<option :value="null">-- Kein Parent (Top-Level) --</option>
|
||||
<option v-for="u in units" :value="u.id" :disabled="u.id === unit.id" v-text="u.name"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm bg-gray-50 p-2 rounded border border-gray-100">
|
||||
<label class="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" v-model="unit.traitsObj.is_rentable"> Vermietbar
|
||||
</label>
|
||||
<div class="flex items-center bg-white border rounded px-2">
|
||||
<span class="text-gray-400 mr-1">€</span>
|
||||
<input type="number" v-model.number="unit.traitsObj.base_price" class="p-1 w-full outline-none" placeholder="Preis">
|
||||
</div>
|
||||
<div class="flex items-center bg-white border rounded px-2">
|
||||
<input type="number" v-model.number="unit.traitsObj.area_sqm" class="p-1 w-full outline-none" placeholder="Fläche">
|
||||
<span class="text-gray-400 ml-1">m²</span>
|
||||
</div>
|
||||
<div class="flex items-center bg-white border rounded px-2">
|
||||
<span class="text-gray-400 mr-1">🛏</span>
|
||||
<input type="number" v-model.number="unit.traitsObj.sleep_capacity" class="p-1 w-full outline-none" placeholder="Betten">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="updateUnit(unit)" class="bg-green-500 text-white px-4 py-2 rounded text-sm self-end hover:bg-green-600 transition shadow-sm font-semibold">Änderungen Speichern</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- EVENTS SECTION -->
|
||||
<div class="bg-white p-6 rounded-lg shadow">
|
||||
<h2 class="text-2xl font-bold mb-4">Events (Buchungen/Aufgaben)</h2>
|
||||
<form @submit.prevent="createEvent" class="mb-4 flex flex-col gap-2">
|
||||
<select v-model="newEvent.unitId" class="border p-2 rounded" required>
|
||||
<option disabled value="">Unit auswählen...</option>
|
||||
<h2 class="text-2xl font-bold mb-4 flex justify-between items-center">
|
||||
Events (Buchungen)
|
||||
<span class="text-sm font-normal text-gray-500 bg-gray-100 px-2 py-1 rounded" v-text="events.length + ' Einträge'"></span>
|
||||
</h2>
|
||||
|
||||
<form @submit.prevent="createEvent" class="mb-6 bg-emerald-50 p-4 rounded border border-emerald-100">
|
||||
<h3 class="font-bold mb-3 text-emerald-800">Neues Event anlegen</h3>
|
||||
<select v-model="newEvent.unitId" class="border border-emerald-200 p-2 rounded w-full mb-2 focus:ring-2 focus:ring-emerald-400 outline-none" required>
|
||||
<option disabled value="">Zugehörige Unit auswählen...</option>
|
||||
<option v-for="u in units" :value="u.id" v-text="u.name"></option>
|
||||
</select>
|
||||
<div class="flex gap-2">
|
||||
<input v-model="newEvent.type" placeholder="Type (z.B. booking)" class="border p-2 rounded flex-1" required>
|
||||
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700">Neu</button>
|
||||
<input v-model="newEvent.type" placeholder="Type (z.B. booking)" class="border border-emerald-200 p-2 rounded flex-1 focus:ring-2 focus:ring-emerald-400 outline-none" required>
|
||||
<button type="submit" class="bg-emerald-600 text-white px-4 py-2 rounded hover:bg-emerald-700 transition shadow-md font-semibold">Buchen</button>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="space-y-2">
|
||||
<li v-for="event in events" :key="event.id" class="border p-4 rounded flex justify-between items-center bg-gray-50">
|
||||
|
||||
<ul class="space-y-3">
|
||||
<li v-for="event in events" :key="event.id" class="border border-gray-200 p-4 rounded flex justify-between items-center bg-white shadow-sm hover:shadow transition">
|
||||
<div>
|
||||
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-bold" v-text="event.type"></span>
|
||||
<span class="ml-2 text-sm text-gray-600">Unit: <span v-text="getUnitName(event.unitId)"></span></span>
|
||||
<span class="bg-emerald-100 text-emerald-800 px-2 py-1 rounded text-xs font-bold uppercase tracking-wide border border-emerald-200" v-text="event.type"></span>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Verknüpft mit: <strong class="text-gray-800" v-text="getUnitName(event.unitId)"></strong>
|
||||
</div>
|
||||
<button @click="deleteEvent(event.id)" class="text-red-500 hover:underline text-sm">Löschen</button>
|
||||
</div>
|
||||
<button @click="deleteEvent(event.id)" class="text-red-400 hover:text-red-600 hover:bg-red-50 px-2 py-1 rounded transition text-sm font-bold">✖ Löschen</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,35 +135,66 @@
|
||||
data() {
|
||||
return {
|
||||
units: [], events: [],
|
||||
newUnit: { name: "", traits: "{}" },
|
||||
newUnit: { name: "", parentId: "", traitsObj: { is_rentable: true, base_price: null, area_sqm: null, sleep_capacity: null } },
|
||||
newEvent: { unitId: "", type: "booking" },
|
||||
errorMsg: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchUnits() {
|
||||
try {
|
||||
const res = await fetch("/api/units");
|
||||
const data = await res.json();
|
||||
this.units = data.map(u => ({ ...u, traitsRaw: JSON.stringify(u.traits || {}, null, 2) }));
|
||||
this.units = data.map(u => ({
|
||||
...u,
|
||||
traitsObj: {
|
||||
is_rentable: u.traits?.is_rentable || false,
|
||||
base_price: u.traits?.base_price || null,
|
||||
area_sqm: u.traits?.area_sqm || null,
|
||||
sleep_capacity: u.traits?.sleep_capacity || null,
|
||||
...(u.traits || {})
|
||||
}
|
||||
}));
|
||||
} catch (e) { this.errorMsg = "Fehler beim Laden der Units: " + e.message; }
|
||||
},
|
||||
async fetchEvents() {
|
||||
try {
|
||||
const res = await fetch("/api/events");
|
||||
this.events = await res.json();
|
||||
} catch (e) { this.errorMsg = "Fehler beim Laden der Events: " + e.message; }
|
||||
},
|
||||
getUnitName(id) {
|
||||
const u = this.units.find(u => u.id === id);
|
||||
return u ? u.name : id;
|
||||
},
|
||||
async createUnit() {
|
||||
const body = { name: this.newUnit.name, traits: JSON.parse(this.newUnit.traits || "{}") };
|
||||
try {
|
||||
const traits = { ...this.newUnit.traitsObj };
|
||||
Object.keys(traits).forEach(k => traits[k] === null && delete traits[k]);
|
||||
|
||||
const body = {
|
||||
name: this.newUnit.name,
|
||||
parentId: this.newUnit.parentId || null,
|
||||
traits
|
||||
};
|
||||
await fetch("/api/units", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(body) });
|
||||
this.newUnit.name = "";
|
||||
this.newUnit = { name: "", parentId: "", traitsObj: { is_rentable: true, base_price: null, area_sqm: null, sleep_capacity: null } };
|
||||
this.fetchUnits();
|
||||
} catch (e) { alert(e.message); }
|
||||
},
|
||||
async updateUnit(unit) {
|
||||
const body = { name: unit.name, traits: JSON.parse(unit.traitsRaw) };
|
||||
try {
|
||||
const traits = { ...unit.traitsObj };
|
||||
Object.keys(traits).forEach(k => traits[k] === null && delete traits[k]);
|
||||
|
||||
const body = {
|
||||
name: unit.name,
|
||||
parentId: unit.parentId || null,
|
||||
traits
|
||||
};
|
||||
await fetch(`/api/units/${unit.id}`, { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(body) });
|
||||
alert("Gespeichert!");
|
||||
this.fetchUnits();
|
||||
} catch (e) { alert(e.message); }
|
||||
},
|
||||
async deleteUnit(id) {
|
||||
await fetch(`/api/units/${id}`, { method: "DELETE" });
|
||||
@@ -105,6 +203,7 @@
|
||||
async createEvent() {
|
||||
const body = { unitId: this.newEvent.unitId, type: this.newEvent.type, traits: {} };
|
||||
await fetch("/api/events", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(body) });
|
||||
this.newEvent.type = "booking";
|
||||
this.fetchEvents();
|
||||
},
|
||||
async deleteEvent(id) {
|
||||
@@ -116,9 +215,9 @@
|
||||
this.fetchUnits();
|
||||
this.fetchEvents();
|
||||
}
|
||||
}).mount("#app")
|
||||
}).mount("#app");
|
||||
} catch (err) {
|
||||
document.body.innerHTML += "<div style=\"color:red; padding:20px;\">Error: " + err.message + "</div>";
|
||||
document.body.innerHTML += "<div style='color:red; padding:20px;'>Error: " + err.message + "</div>";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user