Files
2026-04-26 22:00:31 +02:00

226 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>RoggioApp - API Dashboard</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 text-gray-800">
<div id="app" class="container mx-auto p-8">
<h1 class="text-4xl font-bold mb-8 text-indigo-600">RoggioApp Dashboard</h1>
<div v-if="errorMsg" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
<strong class="font-bold">Fehler! </strong>
<span class="block sm:inline" v-text="errorMsg"></span>
</div>
<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 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>
<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"></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 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 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-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-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>
</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>
<script>
document.addEventListener("DOMContentLoaded", () => {
try {
const { createApp } = Vue;
createApp({
data() {
return {
units: [], events: [],
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,
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() {
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: "", parentId: "", traitsObj: { is_rentable: true, base_price: null, area_sqm: null, sleep_capacity: null } };
this.fetchUnits();
} catch (e) { alert(e.message); }
},
async updateUnit(unit) {
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) });
this.fetchUnits();
} catch (e) { alert(e.message); }
},
async deleteUnit(id) {
await fetch(`/api/units/${id}`, { method: "DELETE" });
this.fetchUnits();
},
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) {
await fetch(`/api/events/${id}`, { method: "DELETE" });
this.fetchEvents();
}
},
mounted() {
this.fetchUnits();
this.fetchEvents();
}
}).mount("#app");
} catch (err) {
document.body.innerHTML += "<div style='color:red; padding:20px;'>Error: " + err.message + "</div>";
}
});
</script>
</body>
</html>