diff --git a/Dockerfile b/Dockerfile index 10eba53..f01ea1c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,15 @@ -# ---- Stage 1: Frontend bauen ---- -FROM node:20-slim AS frontend-build -WORKDIR /app/frontend -COPY frontend/package.json ./ -RUN npm install -COPY frontend/ ./ -RUN npm run build - -# ---- Stage 2: Backend + gebautes Frontend ---- -FROM node:20-slim AS runtime -WORKDIR /app +FROM node:20-slim +WORKDIR /app/backend COPY backend/package.json ./ RUN npm install --omit=dev COPY backend/ ./ -# Vom Build-Stage kommt das fertige Frontend nach backend/public -COPY --from=frontend-build /app/backend/public ./public +COPY frontend/ /app/frontend ENV NODE_ENV=production -ENV PORT=3000 +ENV PORT=3001 -EXPOSE 3000 +EXPOSE 3001 CMD ["node", "src/index.js"] diff --git a/README.md b/README.md index 90a8473..a411312 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,16 @@ Kapazitaetsplanung pro Person. ## Architektur - **Backend**: Node.js + Express, liefert eine REST-API (`/api/...`) und das - gebaute Frontend aus. + statische Frontend aus. - **Datenhaltung**: MongoDB (eigener Container, offizieller Node-Treiber `mongodb`). Vier Collections: `people`, `projects`, `tasks`, `absences`. Die Mongo-`_id` wird beim Ausliefern an das Frontend in ein einfaches `id`-Feld (String) umgewandelt; Referenzen zwischen Entitaeten (z. B. `assigneeId`, `projectId`) werden als dieser String gespeichert. -- **Frontend**: React + Vite, wird beim Docker-Build in `backend/public` - gebaut und vom Backend ausgeliefert. +- **Frontend**: Reines HTML/CSS/JS mit [Alpine.js](https://alpinejs.dev/) + (lokal in `frontend/js/vendor` eingebettet, keine externe CDN-Abhaengigkeit + zur Laufzeit). Kein Build-Step: `frontend/` wird beim Docker-Build 1:1 nach + `backend/public` kopiert und vom Backend ausgeliefert. ## Lokale Entwicklung (optional) @@ -34,17 +36,18 @@ Kapazitaetsplanung pro Person. # MongoDB lokal starten (z. B. per Docker) docker run -d --name jp-mongo -p 27017:27017 mongo:7 -# Backend +# Backend (liefert API + Frontend zusammen aus, kein separater Frontend-Server noetig) cd backend npm install -MONGODB_URI=mongodb://localhost:27017/jahresplanung npm start # http://localhost:3000 - -# Frontend (in zweitem Terminal) -cd frontend -npm install -npm run dev # http://localhost:5173, proxied /api zu :3000 +MONGODB_URI=mongodb://localhost:27017/jahresplanung npm start # http://localhost:3001 ``` +Das Frontend liegt unter `frontend/` als reine statische Dateien (kein +`npm install`/Build noetig). Das Backend liefert es direkt aus dem +Geschwisterverzeichnis `../frontend` aus (lokal wie im Docker-Image +identisch, siehe `backend/src/index.js`). Einfach im Browser +`http://localhost:3001` oeffnen. + ## Deployment in Coolify 1. **Repository verbinden**: Diesen Ordner in ein Git-Repository pushen @@ -53,11 +56,15 @@ npm run dev # http://localhost:5173, proxied /api zu :3000 2. **Build-Pack**: **Docker Compose** waehlen. Das mitgelieferte `docker-compose.yml` startet automatisch zwei Container: die App und MongoDB, inklusive persistentem Volume fuer die Datenbank. -3. **Port**: Der App-Container lauscht auf Port `3000` (im - `docker-compose.yml` gemappt). In Coolify unter "Domains" die gewuenschte - Domain/Subdomain zuweisen. Der MongoDB-Container hat bewusst **keinen** - nach aussen gemappten Port und ist nur innerhalb des Compose-Netzwerks - fuer die App erreichbar. +3. **Port**: Der App-Container lauscht auf Port `3001` (im + `docker-compose.yml` gemappt). In Coolify unter "Domains"/der + Port-Konfiguration der Ressource **muss dieser Port (3001) explizit + eingetragen werden** — das passiert nicht automatisch, wenn sich der Port + nach einem vorherigen Deployment aendert. Falls der Host-Port bereits von + einer anderen Ressource belegt ist, in `docker-compose.yml` einen anderen + freien Port waehlen und die Coolify-Konfiguration entsprechend anpassen. + Der MongoDB-Container hat bewusst **keinen** nach aussen gemappten Port + und ist nur innerhalb des Compose-Netzwerks fuer die App erreichbar. 4. **Persistenz**: Das Volume `jahresplanung-mongo-data` sichert die MongoDB-Datendateien. Bei Redeploys bleiben die Daten erhalten, solange das Volume nicht geloescht wird. diff --git a/backend/src/index.js b/backend/src/index.js index 34da026..02abe0d 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -10,7 +10,7 @@ const absencesRouter = require("./routes/absences"); const capacityRouter = require("./routes/capacity"); const app = express(); -const PORT = process.env.PORT || 3000; +const PORT = process.env.PORT || 3001; app.use(cors()); app.use(express.json()); @@ -23,13 +23,14 @@ app.use("/api/tasks", tasksRouter); app.use("/api/absences", absencesRouter); app.use("/api/capacity", capacityRouter); -// Gebautes Frontend (Vite -> dist) ausliefern -const frontendDist = path.join(__dirname, "..", "public"); -app.use(express.static(frontendDist)); +// Statisches Frontend (HTML/CSS/JS, kein Build-Step) aus dem +// Geschwisterverzeichnis "frontend" ausliefern +const frontendDir = path.join(__dirname, "..", "..", "frontend"); +app.use(express.static(frontendDir)); -// SPA-Fallback: alles, was nicht /api ist, liefert die index.html aus +// Fallback: alles, was nicht /api ist, liefert die index.html aus app.get(/^(?!\/api).*/, (req, res) => { - res.sendFile(path.join(frontendDist, "index.html")); + res.sendFile(path.join(frontendDir, "index.html")); }); app.use((err, req, res, next) => { diff --git a/docker-compose.yml b/docker-compose.yml index c6a054b..40979d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,9 +5,9 @@ services: dockerfile: Dockerfile restart: unless-stopped ports: - - "3000:3000" + - "3001:3001" environment: - - PORT=3000 + - PORT=3001 - MONGODB_URI=mongodb://mongo:27017/jahresplanung depends_on: - mongo diff --git a/frontend/src/styles.css b/frontend/css/styles.css similarity index 99% rename from frontend/src/styles.css rename to frontend/css/styles.css index b50600b..72f8bfb 100644 --- a/frontend/src/styles.css +++ b/frontend/css/styles.css @@ -1,3 +1,7 @@ +[x-cloak] { + display: none !important; +} + :root { --bg: #f6f3ec; --surface: #ffffff; diff --git a/frontend/index.html b/frontend/index.html index ff5b3c5..53a1600 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,9 +10,530 @@ href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" /> + -
- +
+ + +
+
+

+
+ + + + + + + + + + + + +
+ + +
+
+

+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+

+
+ + +
+
+
+
+ + +
+
+

Abwesenheit eintragen

+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+

+
+ + +
+
+
+
+ + +
+
+

+
+
+ + +
+
+ + +
+
+ +
+ +
+
+

+
+ + +
+
+
+
+ + +
+
+

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+

+
+ + +
+
+
+
+
+ + + diff --git a/frontend/src/api.js b/frontend/js/api.js similarity index 100% rename from frontend/src/api.js rename to frontend/js/api.js diff --git a/frontend/js/app.js b/frontend/js/app.js new file mode 100644 index 0000000..0595f99 --- /dev/null +++ b/frontend/js/app.js @@ -0,0 +1,425 @@ +import { api } from "./api.js"; + +const NAV = [ + { key: "timeline", index: "01", label: "Zeitachse" }, + { key: "capacity", index: "02", label: "Kapazitaet" }, + { key: "projects", index: "03", label: "Projekte & Aufgaben" }, + { key: "people", index: "04", label: "Team" } +]; + +const PERSON_PALETTE = ["#2F6F4F", "#C4622D", "#3E5C50", "#8A6D3B", "#4C5B7A", "#A63B2A"]; +const PROJECT_PALETTE = ["#3E5C50", "#2F6F4F", "#8A6D3B", "#4C5B7A", "#A63B2A", "#C4622D"]; +const PRIORITY_LABEL = { hoch: "Hoch", mittel: "Mittel", niedrig: "Niedrig" }; + +const MONTH_LABELS = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]; +const MONTH_LABELS_BY_KEY = { + "01": "Jan", "02": "Feb", "03": "Mär", "04": "Apr", "05": "Mai", "06": "Jun", + "07": "Jul", "08": "Aug", "09": "Sep", "10": "Okt", "11": "Nov", "12": "Dez" +}; + +function isLeap(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} + +function dayOfYear(dateStr, year) { + const d = new Date(`${dateStr}T00:00:00Z`); + const start = new Date(Date.UTC(year, 0, 1)); + return Math.floor((d - start) / 86400000); +} + +function capacityBarClassFor(month) { + if (month.overloaded) return "danger"; + if (month.utilization >= 90) return "warn"; + return "ok"; +} + +document.addEventListener("alpine:init", () => { + Alpine.data("app", () => ({ + // ---------- global state ---------- + currentYear: new Date().getFullYear(), + nav: NAV, + view: "timeline", + people: [], + projects: [], + tasks: [], + absences: [], + loading: true, + loadError: null, + + personPalette: PERSON_PALETTE, + projectPalette: PROJECT_PALETTE, + priorityLabel: PRIORITY_LABEL, + monthLabels: MONTH_LABELS, + + modalError: null, + modalSaving: false, + + // ---------- person form ---------- + personFormOpen: false, + personEditingId: null, + personDraft: { name: "", role: "", weeklyHours: 40, color: PERSON_PALETTE[0] }, + + // ---------- absence form ---------- + absenceFormOpen: false, + absenceDraft: { personId: "", startDate: "", endDate: "", note: "" }, + + // ---------- project form ---------- + projectFormOpen: false, + projectEditingId: null, + projectDraft: { name: "", description: "", color: PROJECT_PALETTE[0] }, + + // ---------- task form ---------- + taskFormOpen: false, + taskEditingId: null, + taskDraft: { + projectId: "", + name: "", + startDate: "", + endDate: "", + estimatedHours: 8, + assigneeId: "", + priority: "mittel", + status: "geplant", + dependsOn: "" + }, + + // ---------- timeline ---------- + timelineYear: new Date().getFullYear(), + + // ---------- capacity ---------- + capacityYear: new Date().getFullYear(), + capacityData: null, + capacityLoading: true, + capacityError: null, + + init() { + this.reloadAll(); + }, + + async reloadAll() { + try { + const [p, pr, t, a] = await Promise.all([ + api.people.list(), + api.projects.list(), + api.tasks.list(), + api.absences.list() + ]); + this.people = p; + this.projects = pr; + this.tasks = t; + this.absences = a; + this.loadError = null; + } catch (err) { + this.loadError = err.message; + } finally { + this.loading = false; + } + }, + + setView(key) { + this.view = key; + if (key === "capacity") this.loadCapacity(); + }, + + personById(id) { + return this.people.find((p) => p.id === id); + }, + + personNameOrUnknown(id) { + return this.personById(id)?.name || "Unbekannt"; + }, + + assigneeName(id) { + return this.personById(id)?.name || "–"; + }, + + sortedAbsences() { + return [...this.absences].sort((a, b) => a.startDate.localeCompare(b.startDate)); + }, + + tasksByProject(projectId) { + return this.tasks + .filter((t) => t.projectId === projectId) + .sort((a, b) => a.startDate.localeCompare(b.startDate)); + }, + + // ---------- person CRUD ---------- + openPersonForm(person = null) { + this.personEditingId = person ? person.id : null; + this.personDraft = person + ? { name: person.name, role: person.role || "", weeklyHours: person.weeklyHours ?? 40, color: person.color || PERSON_PALETTE[0] } + : { name: "", role: "", weeklyHours: 40, color: PERSON_PALETTE[0] }; + this.modalError = null; + this.personFormOpen = true; + this.$nextTick(() => document.getElementById("p-name")?.focus()); + }, + + closePersonForm() { + this.personFormOpen = false; + }, + + async savePerson() { + this.modalError = null; + if (!this.personDraft.name.trim()) { + this.modalError = "Bitte einen Namen eingeben."; + return; + } + this.modalSaving = true; + try { + const payload = { + name: this.personDraft.name, + role: this.personDraft.role, + weeklyHours: Number(this.personDraft.weeklyHours), + color: this.personDraft.color + }; + if (this.personEditingId) { + await api.people.update(this.personEditingId, payload); + } else { + await api.people.create(payload); + } + this.personFormOpen = false; + await this.reloadAll(); + } catch (err) { + this.modalError = err.message; + } finally { + this.modalSaving = false; + } + }, + + async removePerson(id) { + if (!confirm("Diese Person inklusive ihrer Abwesenheiten wirklich entfernen?")) return; + await api.people.remove(id); + await this.reloadAll(); + }, + + // ---------- absence CRUD ---------- + openAbsenceForm() { + this.absenceDraft = { personId: this.people[0]?.id || "", startDate: "", endDate: "", note: "" }; + this.modalError = null; + this.absenceFormOpen = true; + }, + + closeAbsenceForm() { + this.absenceFormOpen = false; + }, + + async saveAbsence() { + this.modalError = null; + const d = this.absenceDraft; + if (!d.personId || !d.startDate || !d.endDate) { + this.modalError = "Bitte Person, Start- und Enddatum angeben."; + return; + } + if (d.startDate > d.endDate) { + this.modalError = "Das Startdatum muss vor dem Enddatum liegen."; + return; + } + this.modalSaving = true; + try { + await api.absences.create({ personId: d.personId, startDate: d.startDate, endDate: d.endDate, note: d.note }); + this.absenceFormOpen = false; + await this.reloadAll(); + } catch (err) { + this.modalError = err.message; + } finally { + this.modalSaving = false; + } + }, + + async removeAbsence(id) { + await api.absences.remove(id); + await this.reloadAll(); + }, + + // ---------- project CRUD ---------- + openProjectForm(project = null) { + this.projectEditingId = project ? project.id : null; + this.projectDraft = project + ? { name: project.name, description: project.description || "", color: project.color || PROJECT_PALETTE[0] } + : { name: "", description: "", color: PROJECT_PALETTE[0] }; + this.modalError = null; + this.projectFormOpen = true; + this.$nextTick(() => document.getElementById("pr-name")?.focus()); + }, + + closeProjectForm() { + this.projectFormOpen = false; + }, + + async saveProject() { + this.modalError = null; + if (!this.projectDraft.name.trim()) { + this.modalError = "Bitte einen Projektnamen eingeben."; + return; + } + this.modalSaving = true; + try { + const payload = { + name: this.projectDraft.name, + description: this.projectDraft.description, + color: this.projectDraft.color + }; + if (this.projectEditingId) { + await api.projects.update(this.projectEditingId, payload); + } else { + await api.projects.create(payload); + } + this.projectFormOpen = false; + await this.reloadAll(); + } catch (err) { + this.modalError = err.message; + } finally { + this.modalSaving = false; + } + }, + + async removeProject(id) { + if (!confirm("Projekt inklusive aller zugehoerigen Aufgaben wirklich loeschen?")) return; + await api.projects.remove(id); + await this.reloadAll(); + }, + + // ---------- task CRUD ---------- + openTaskForm(task = null, projectHint = null) { + this.taskEditingId = task ? task.id : null; + this.taskDraft = { + projectId: task?.projectId || projectHint || this.projects[0]?.id || "", + name: task?.name || "", + startDate: task?.startDate || "", + endDate: task?.endDate || "", + estimatedHours: task?.estimatedHours ?? 8, + assigneeId: task?.assigneeId || "", + priority: task?.priority || "mittel", + status: task?.status || "geplant", + dependsOn: task?.dependsOn || "" + }; + this.modalError = null; + this.taskFormOpen = true; + this.$nextTick(() => document.getElementById("t-name")?.focus()); + }, + + closeTaskForm() { + this.taskFormOpen = false; + }, + + taskDependencyOptions() { + return this.tasks.filter((t) => t.id !== this.taskEditingId); + }, + + async saveTask() { + this.modalError = null; + const d = this.taskDraft; + if (!d.projectId || !d.name.trim() || !d.startDate || !d.endDate) { + this.modalError = "Projekt, Name, Start- und Enddatum sind erforderlich."; + return; + } + if (d.startDate > d.endDate) { + this.modalError = "Das Startdatum muss vor dem Enddatum liegen."; + return; + } + this.modalSaving = true; + try { + const payload = { + projectId: d.projectId, + name: d.name, + startDate: d.startDate, + endDate: d.endDate, + estimatedHours: Number(d.estimatedHours), + assigneeId: d.assigneeId || null, + priority: d.priority, + status: d.status, + dependsOn: d.dependsOn || null + }; + if (this.taskEditingId) { + await api.tasks.update(this.taskEditingId, payload); + } else { + await api.tasks.create(payload); + } + this.taskFormOpen = false; + await this.reloadAll(); + } catch (err) { + this.modalError = err.message; + } finally { + this.modalSaving = false; + } + }, + + async removeTask(id) { + await api.tasks.remove(id); + await this.reloadAll(); + }, + + // ---------- timeline ---------- + timelineDaysInYear() { + return isLeap(this.timelineYear) ? 366 : 365; + }, + + timelineGrouped() { + return this.projects + .map((project) => ({ + project, + tasks: this.tasks + .filter((t) => t.projectId === project.id) + .filter((t) => { + const startYear = Number(t.startDate.slice(0, 4)); + const endYear = Number(t.endDate.slice(0, 4)); + return startYear <= this.timelineYear && endYear >= this.timelineYear; + }) + .sort((a, b) => a.startDate.localeCompare(b.startDate)) + })) + .filter((g) => g.tasks.length > 0); + }, + + timelineBarStyle(task) { + const year = this.timelineYear; + const daysInYear = this.timelineDaysInYear(); + const yearStart = `${year}-01-01`; + const yearEnd = `${year}-12-31`; + const clippedStart = task.startDate < yearStart ? yearStart : task.startDate; + const clippedEnd = task.endDate > yearEnd ? yearEnd : task.endDate; + const startDay = dayOfYear(clippedStart, year); + const endDay = dayOfYear(clippedEnd, year); + const leftPct = (startDay / daysInYear) * 100; + const widthPct = Math.max(((endDay - startDay + 1) / daysInYear) * 100, 0.6); + const person = this.personById(task.assigneeId); + const project = this.projects.find((p) => p.id === task.projectId); + return `left: ${leftPct}%; width: ${widthPct}%; background: ${person?.color || project?.color || "var(--accent)"};`; + }, + + // ---------- capacity ---------- + async loadCapacity() { + this.capacityLoading = true; + try { + const res = await api.capacity.get(`${this.capacityYear}-01-01`, `${this.capacityYear}-12-31`); + this.capacityData = res; + this.capacityError = null; + } catch (err) { + this.capacityError = err.message; + } finally { + this.capacityLoading = false; + } + }, + + capacityPrevYear() { + this.capacityYear -= 1; + this.loadCapacity(); + }, + + capacityNextYear() { + this.capacityYear += 1; + this.loadCapacity(); + }, + + capacityMaxScale(person) { + return Math.max(1, ...person.months.map((m) => Math.max(m.capacityHours, m.assignedHours))); + }, + + capacityBarClass(month) { + return capacityBarClassFor(month); + }, + + monthLabel(monthKey) { + return MONTH_LABELS_BY_KEY[monthKey.slice(5)]; + } + })); +}); diff --git a/frontend/js/vendor/alpine-3.15.12.min.js b/frontend/js/vendor/alpine-3.15.12.min.js new file mode 100644 index 0000000..ab371ef --- /dev/null +++ b/frontend/js/vendor/alpine-3.15.12.min.js @@ -0,0 +1,5 @@ +(()=>{var ee=!1,re=!1,W=[],ne=-1,ie=!1;function Ve(t){Dn(t)}function Ue(){ie=!0}function qe(){ie=!1,We()}function Dn(t){W.includes(t)||W.push(t),We()}function Ke(t){let e=W.indexOf(t);e!==-1&&e>ne&&W.splice(e,1)}function We(){if(!re&&!ee){if(ie)return;ee=!0,queueMicrotask(In)}}function In(){ee=!1,re=!0;for(let t=0;tt.effect(e,{scheduler:r=>{oe?Ve(r):r()}}),se=t.raw}function ae(t){R=t}function Ye(t){let e=()=>{};return[n=>{let i=R(n);return t._x_effects||(t._x_effects=new Set,t._x_runEffects=()=>{t._x_effects.forEach(o=>o())}),t._x_effects.add(i),e=()=>{i!==void 0&&(t._x_effects.delete(i),j(i))},i},()=>{e()}]}function St(t,e){let r=!0,n,i,o=R(()=>{let s=t(),a=JSON.stringify(s);if(!r&&(typeof s=="object"||s!==n)){let c=typeof n=="object"?JSON.parse(i):n;queueMicrotask(()=>{e(s,c)})}n=s,i=a,r=!1});return()=>j(o)}async function Xe(t){Ue();try{await t(),await Promise.resolve()}finally{qe()}}var Ze=[],Qe=[],tr=[];function er(t){tr.push(t)}function et(t,e){typeof e=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(e)):(e=t,Qe.push(e))}function At(t){Ze.push(t)}function Ot(t,e,r){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[e]||(t._x_attributeCleanups[e]=[]),t._x_attributeCleanups[e].push(r)}function ce(t,e){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([r,n])=>{(e===void 0||e.includes(r))&&(n.forEach(i=>i()),delete t._x_attributeCleanups[r])})}function rr(t){for(t._x_effects?.forEach(Ke);t._x_cleanups?.length;)t._x_cleanups.pop()()}var le=new MutationObserver(pe),ue=!1;function ut(){le.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ue=!0}function fe(){kn(),le.disconnect(),ue=!1}var lt=[];function kn(){let t=le.takeRecords();lt.push(()=>t.length>0&&pe(t));let e=lt.length;queueMicrotask(()=>{if(lt.length===e)for(;lt.length>0;)lt.shift()()})}function m(t){if(!ue)return t();fe();let e=t();return ut(),e}var de=!1,vt=[];function nr(){de=!0}function ir(){de=!1,pe(vt),vt=[]}function pe(t){if(de){vt=vt.concat(t);return}let e=[],r=new Set,n=new Map,i=new Map;for(let o=0;o{s.nodeType===1&&s._x_marker&&r.add(s)}),t[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||e.push(s)}})),t[o].type==="attributes")){let s=t[o].target,a=t[o].attributeName,c=t[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ce(s,o)}),n.forEach((o,s)=>{Ze.forEach(a=>a(s,o))});for(let o of r)e.some(s=>s.contains(o))||Qe.forEach(s=>s(o));for(let o of e)o.isConnected&&tr.forEach(s=>s(o));e=null,r=null,n=null,i=null}function Ct(t){return P(F(t))}function N(t,e,r){return t._x_dataStack=[e,...F(r||t)],()=>{t._x_dataStack=t._x_dataStack.filter(n=>n!==e)}}function F(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?F(t.host):t.parentNode?F(t.parentNode):[]}function P(t){return new Proxy({objects:t},$n)}function or(t,e){return t===null||t===Object.prototype?null:Object.prototype.hasOwnProperty.call(t,e)?t:or(Object.getPrototypeOf(t),e)}var $n={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(e=>Object.keys(e))))},has({objects:t},e){return e==Symbol.unscopables?!1:t.some(r=>Object.prototype.hasOwnProperty.call(r,e)||Reflect.has(r,e))},get({objects:t},e,r){return e=="toJSON"?Ln:Reflect.get(t.find(n=>Reflect.has(n,e))||{},e,r)},set({objects:t},e,r,n){let i;for(let s of t)if(i=or(s,e),i)break;i||(i=t[t.length-1]);let o=Object.getOwnPropertyDescriptor(i,e);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,e,r)}};function Ln(){return Reflect.ownKeys(this).reduce((e,r)=>(e[r]=Reflect.get(this,r),e),{})}function rt(t){let e=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(t,c,o):e(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(t)}function Tt(t,e=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return t(this.initialValue,()=>jn(n,i),s=>me(n,i,s),i,o)}};return e(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function jn(t,e){return e.split(".").reduce((r,n)=>r[n],t)}function me(t,e,r){if(typeof e=="string"&&(e=e.split(".")),e.length===1)t[e[0]]=r;else{if(e.length===0)throw error;return t[e[0]]||(t[e[0]]={}),me(t[e[0]],e.slice(1),r)}}var sr={};function x(t,e){sr[t]=e}function H(t,e){let r=Fn(e);return Object.entries(sr).forEach(([n,i])=>{Object.defineProperty(t,`$${n}`,{get(){return i(e,r)},enumerable:!1})}),t}function Fn(t){let[e,r]=he(t),n={interceptor:Tt,...e};return et(t,r),n}function ar(t,e,r,...n){try{return r(...n)}catch(i){nt(i,t,e)}}function nt(...t){return cr(...t)}var cr=Bn;function lr(t){cr=t}function Bn(t,e,r=void 0){t=Object.assign(t??{message:"No error message given."},{el:e,expression:r}),console.warn(`Alpine Expression Error: ${t.message} + +${r?'Expression: "'+r+`" + +`:""}`,e),setTimeout(()=>{throw t},0)}var it=!0;function Mt(t){let e=it;it=!1;let r=t();return it=e,r}function T(t,e,r={}){let n;return _(t,e)(i=>n=i,r),n}function _(...t){return ur(...t)}var ur=()=>{};function fr(t){ur=t}var dr;function pr(t){dr=t}function mr(t,e){let r={};H(r,t);let n=[r,...F(t)],i=typeof e=="function"?zn(n,e):Vn(n,e,t);return ar.bind(null,t,e,i)}function zn(t,e){return(r=()=>{},{scope:n={},params:i=[],context:o}={})=>{if(!it){ft(r,e,P([n,...t]),i);return}let s=e.apply(P([n,...t]),i);ft(r,s)}}var _e={};function Hn(t,e){if(_e[t])return _e[t];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${t}`}),s}catch(s){return nt(s,e,t),Promise.resolve()}})();return _e[t]=o,o}function Vn(t,e,r){let n=Hn(e,r);return(i=()=>{},{scope:o={},params:s=[],context:a}={})=>{n.result=void 0,n.finished=!1;let c=P([o,...t]);if(typeof n=="function"){let l=n.call(a,n,c).catch(u=>nt(u,r,e));n.finished?(ft(i,n.result,c,s,r),n.result=void 0):l.then(u=>{ft(i,u,c,s,r)}).catch(u=>nt(u,r,e)).finally(()=>n.result=void 0)}}}function ft(t,e,r,n,i){if(it&&typeof e=="function"){let o=e.apply(r,n);o instanceof Promise?o.then(s=>ft(t,s,r,n)).catch(s=>nt(s,i,e)):t(o)}else typeof e=="object"&&e instanceof Promise?e.then(o=>t(o)):t(e)}function hr(...t){return dr(...t)}function _r(t,e,r={}){let n={};H(n,t);let i=[n,...F(t)],o=P([r.scope??{},...i]),s=r.params??[];if(e.includes("await")){let a=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e;return new a(["scope"],`with (scope) { let __result = ${c}; return __result }`).call(r.context,o)}else{let a=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(()=>{ ${e} })()`:e,l=new Function(["scope"],`with (scope) { let __result = ${a}; return __result }`).call(r.context,o);return typeof l=="function"&&it?l.apply(o,s):l}}var ye="x-";function O(t=""){return ye+t}function gr(t){ye=t}var Rt={};function p(t,e){return Rt[t]=e,{before(r){if(!Rt[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${t}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,t)}}}function xr(t){return Object.keys(Rt).includes(t)}function pt(t,e,r){if(e=Array.from(e),t._x_virtualDirectives){let o=Object.entries(t._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=be(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),e=e.concat(o)}let n={};return e.map(wr((o,s)=>n[o]=s)).filter(Sr).map(qn(n,r)).sort(Kn).map(o=>Un(t,o))}function be(t){return Array.from(t).map(wr()).filter(e=>!Sr(e))}var ge=!1,dt=new Map,yr=Symbol();function br(t){ge=!0;let e=Symbol();yr=e,dt.set(e,[]);let r=()=>{for(;dt.get(e).length;)dt.get(e).shift()();dt.delete(e)},n=()=>{ge=!1,r()};t(r),n()}function he(t){let e=[],r=a=>e.push(a),[n,i]=Ye(t);return e.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:_.bind(_,t),evaluate:T.bind(T,t)},()=>e.forEach(a=>a())]}function Un(t,e){let r=()=>{},n=Rt[e.type]||r,[i,o]=he(t);Ot(t,e.original,o);let s=()=>{t._x_ignore||t._x_ignoreSelf||(n.inline&&n.inline(t,e,i),n=n.bind(n,t,e,i),ge?dt.get(yr).push(n):n())};return s.runCleanups=o,s}var Nt=(t,e)=>({name:r,value:n})=>(r.startsWith(t)&&(r=r.replace(t,e)),{name:r,value:n}),Pt=t=>t;function wr(t=()=>{}){return({name:e,value:r})=>{let{name:n,value:i}=Er.reduce((o,s)=>s(o),{name:e,value:r});return n!==e&&t(n,e),{name:n,value:i}}}var Er=[];function ot(t){Er.push(t)}function Sr({name:t}){return vr().test(t)}var vr=()=>new RegExp(`^${ye}([^:^.]+)\\b`);function qn(t,e){return({name:r,value:n})=>{r===n&&(n="");let i=r.match(vr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=e||t[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var xe="DEFAULT",G=["ignore","ref","id","data","anchor","bind","init","for","model","modelable","transition","show","if",xe,"teleport"];function Kn(t,e){let r=G.indexOf(t.type)===-1?xe:t.type,n=G.indexOf(e.type)===-1?xe:e.type;return G.indexOf(r)-G.indexOf(n)}function J(t,e,r={},n={}){return t.dispatchEvent(new CustomEvent(e,{detail:r,bubbles:!0,composed:!0,cancelable:!0,...n}))}function D(t,e){if(typeof ShadowRoot=="function"&&t instanceof ShadowRoot){Array.from(t.children).forEach(i=>D(i,e));return}let r=!1;if(e(t,()=>r=!0),r)return;let n=t.firstElementChild;for(;n;)D(n,e,!1),n=n.nextElementSibling}function E(t,...e){console.warn(`Alpine Warning: ${t}`,...e)}var Ar=!1;function Or(){Ar&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Ar=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `