' +
- '
Konto
' +
+ function paintKonto() {
+ const { account } = acctData;
+ const tabs = [["overview", "Übersicht"], ["profile", "Profil"], ["security", "Sicherheit"]];
+ const head =
+ '
' +
+ '
Mein Konto
' +
'
' +
- '
' + esc(account.email) + "
";
+ '
' + esc(account.email) + "
" +
+ '
' +
+ tabs.map(([id, label]) =>
+ '"
+ ).join("") + "
";
- if (justOk) html += '
Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.
';
+ const body = { overview: tabOverview, profile: tabProfile, security: tabSecurity }[acctTab]();
+ root.innerHTML = card(head + '
' + body + "
", true);
+
+ root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); };
+ root.querySelectorAll("[data-tab]").forEach((b) =>
+ (b.onclick = () => { acctTab = b.dataset.tab; paintKonto(); })
+ );
+ wireTab();
+ }
+
+ // — Tab: Übersicht (Abo + Instanzen) —
+ function tabOverview() {
+ const { subscription, instances, plans } = acctData;
+ const params = new URLSearchParams(location.search);
+ let h = "";
+ if (params.get("provisioned") === "1")
+ h += '
Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.
';
if (subscription) {
- html += '
Abo' + esc(subscription.plan) +
- " · " + esc(subscription.status) + "
";
+ h += '
Abo' +
+ esc(subscription.plan) + " · " + esc(subscription.status) + "
";
+ if (subscription.current_period_end)
+ h += '
Nächste Verlängerung' +
+ esc(new Date(subscription.current_period_end).toLocaleDateString("de-CH")) + "
";
}
- if (instance) {
- html += '
Instanz' + esc(instance.status) + "
" +
- '
Rapport öffnen →';
+
+ if (instances && instances.length) {
+ h += '
Ihre Instanzen
';
+ h += instances.map((i) =>
+ '
' + esc(i.label || i.studio_slug) + "" +
+ '
' + esc(i.instance_url) + "
" +
+ '
Öffnen →'
+ ).join("");
+ // Weitere Instanz (Multi-Instanz vorbereitet — Backend-Sperre öffnen wir später)
+ h += '
';
} else {
- html += '
Wählen Sie ein Abo, um Ihre Instanz freizuschalten:
' +
+ h += '
Wählen Sie ein Abo, um Ihre Instanz freizuschalten:
' +
'
' + (plans || []).map(planCard).join("") + "
";
}
- root.innerHTML = card(html, true);
- root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); };
- root.querySelectorAll("[data-plan]").forEach((b) => {
- b.onclick = async () => {
- b.disabled = true;
+ return h;
+ }
+
+ // — Tab: Profil (Firma / Rechnungsadresse) —
+ function tabProfile() {
+ const a = acctData.account;
+ const f = (id, label, val, ph) =>
+ '
" +
+ '
';
+ return '
' +
+ f("company", "Firma / Büro", a.company, "Architektur Muster GmbH") +
+ f("contact_name", "Ansprechperson", a.contact_name, "Vor- und Nachname") +
+ f("street", "Strasse & Nr.", a.street) +
+ '
' +
+ '
' + f("zip", "PLZ", a.zip) + "
" +
+ '
' + f("city", "Ort", a.city) + "
" +
+ f("country", "Land", a.country || "CH") +
+ f("phone", "Telefon", a.phone) +
+ '
';
+ }
+
+ // — Tab: Sicherheit (Passwort) —
+ function tabSecurity() {
+ return '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
';
+ }
+
+ // — Event-Wiring je Tab —
+ function wireTab() {
+ if (acctTab === "overview") {
+ root.querySelectorAll("[data-plan]").forEach((b) =>
+ (b.onclick = async () => {
+ b.disabled = true;
+ try { const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan }); go(url); }
+ catch (err) { alert(err.message); b.disabled = false; }
+ })
+ );
+ const add = root.querySelector("#addInstance");
+ if (add) add.onclick = () => alert("Weitere Instanzen kommen bald. Kontaktieren Sie uns für ein Mehrfach-Abo.");
+ }
+ if (acctTab === "profile") {
+ root.querySelector("#saveProfile").onclick = async (e) => {
+ const btn = e.target; btn.disabled = true;
+ const m = root.querySelector("#pmsg");
+ const payload = {};
+ ["company", "contact_name", "street", "zip", "city", "country", "phone"].forEach(
+ (id) => (payload[id] = root.querySelector("#" + id).value.trim())
+ );
try {
- const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan });
- go(url);
- } catch (err) { alert(err.message); b.disabled = false; }
+ const { account } = await api("PATCH", "/account/me", payload);
+ acctData.account = { ...acctData.account, ...account };
+ msg(m, "Gespeichert.", "ok");
+ } catch (err) { msg(m, err.message, "err"); }
+ btn.disabled = false;
};
- });
+ }
+ if (acctTab === "security") {
+ root.querySelector("#savePw").onclick = async (e) => {
+ const btn = e.target; btn.disabled = true;
+ const m = root.querySelector("#smsg");
+ try {
+ await api("POST", "/account/password", {
+ currentPassword: root.querySelector("#curPw").value,
+ newPassword: root.querySelector("#newPw").value,
+ });
+ root.querySelector("#curPw").value = ""; root.querySelector("#newPw").value = "";
+ msg(m, "Passwort geändert.", "ok");
+ } catch (err) { msg(m, err.message, "err"); }
+ btn.disabled = false;
+ };
+ }
}
function planCard(p) {
diff --git a/public/konto/index.html b/public/konto/index.html
index 5dc61b9..a7d4700 100644
--- a/public/konto/index.html
+++ b/public/konto/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/lizenz/index.html b/public/lizenz/index.html
index 62876a9..7d9d8b8 100644
--- a/public/lizenz/index.html
+++ b/public/lizenz/index.html
@@ -40,8 +40,8 @@ Quellcode: git.kgva.ch/karim/RAPPORT Autor: Karim Gabriele Varano Lizenz Lizenzi
RAPPORT RAPPORT — Studio Management Software für Architekturbüros.
Quellcode: git.kgva.ch/karim/RAPPORT Autor: Karim Gabriele Varano Lizenz Lizenziert unter GNU Affero General Public License v3.0 oder höher (AGPL-3.0-or-later ).">
-
-
+
+
diff --git a/public/login/index.html b/public/login/index.html
index b4eea25..170b5c7 100644
--- a/public/login/index.html
+++ b/public/login/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/register/index.html b/public/register/index.html
index fa8916d..8bd880d 100644
--- a/public/register/index.html
+++ b/public/register/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/server/index.html b/public/server/index.html
index 70e0fc0..4f956ed 100644
--- a/public/server/index.html
+++ b/public/server/index.html
@@ -40,8 +40,8 @@ Wann brauchst du Rapport Server? Szenario Lösung Ein Mensch, ein Mac Desktop-Ap
Rapport Server — der vollständige Selfhost-Stack für Rapport. Eigene Daten, eigene Domain, eigener Server. Komplett Open-Source, Docker-Compose, AGPL-3.0.
Wann brauchst du Rapport Server? Szenario Lösung Ein Mensch, ein Mac Desktop-App reicht — Installation Mehrere Personen im Studio Rapport Server auf einem Mac Mini oder Linux-Server Verteiltes Team, Home-Office, Mobile-Zugriff Rapport Server mit Reverse-Proxy + SSL Cloud-Hosting bei einem Anbieter Rapport Server auf VPS/Hetzner/etc. Die Desktop-App speichert lokal als JSON. Rapport Server bringt Postgres + Multi-User + Realtime-Sync — für alle, die zu zweit oder im Team arbeiten.">
-
-
+
+
diff --git a/public/tags/index.html b/public/tags/index.html
index cfe5fd6..bd4166c 100644
--- a/public/tags/index.html
+++ b/public/tags/index.html
@@ -33,8 +33,8 @@
-
-
+
+
diff --git a/static/js/hosting-app.js b/static/js/hosting-app.js
index 6713ba2..4494e1b 100644
--- a/static/js/hosting-app.js
+++ b/static/js/hosting-app.js
@@ -105,51 +105,149 @@
};
}
- // ── Konto / Dashboard ──────────────────────────────────────────────────
+ // ── Konto / Dashboard (Tabs: Übersicht · Profil · Sicherheit) ───────────
+ let acctData = null; // gecachte /account/me-Antwort
+ let acctTab = "overview";
+
async function renderKonto() {
if (!tok.isLoggedIn) return go("/login/");
root.innerHTML = card('
Lädt…
', true);
- let data;
- try { data = await api("GET", "/account/me"); }
+ try { acctData = await api("GET", "/account/me"); }
catch (err) {
if (/angemeldet|abgelaufen|ungültig/i.test(err.message)) { tok.clear(); return go("/login/"); }
root.innerHTML = card('
' + esc(err.message) + "
"); return;
}
- const { account, subscription, instance, plans } = data;
- const params = new URLSearchParams(location.search);
- const justOk = params.get("provisioned") === "1";
+ paintKonto();
+ }
- let html =
- '
' +
- '
Konto
' +
+ function paintKonto() {
+ const { account } = acctData;
+ const tabs = [["overview", "Übersicht"], ["profile", "Profil"], ["security", "Sicherheit"]];
+ const head =
+ '
' +
+ '
Mein Konto
' +
'
' +
- '
' + esc(account.email) + "
";
+ '
' + esc(account.email) + "
" +
+ '
' +
+ tabs.map(([id, label]) =>
+ '"
+ ).join("") + "
";
- if (justOk) html += '
Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.
';
+ const body = { overview: tabOverview, profile: tabProfile, security: tabSecurity }[acctTab]();
+ root.innerHTML = card(head + '
' + body + "
", true);
+
+ root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); };
+ root.querySelectorAll("[data-tab]").forEach((b) =>
+ (b.onclick = () => { acctTab = b.dataset.tab; paintKonto(); })
+ );
+ wireTab();
+ }
+
+ // — Tab: Übersicht (Abo + Instanzen) —
+ function tabOverview() {
+ const { subscription, instances, plans } = acctData;
+ const params = new URLSearchParams(location.search);
+ let h = "";
+ if (params.get("provisioned") === "1")
+ h += '
Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.
';
if (subscription) {
- html += '
Abo' + esc(subscription.plan) +
- " · " + esc(subscription.status) + "
";
+ h += '
Abo' +
+ esc(subscription.plan) + " · " + esc(subscription.status) + "
";
+ if (subscription.current_period_end)
+ h += '
Nächste Verlängerung' +
+ esc(new Date(subscription.current_period_end).toLocaleDateString("de-CH")) + "
";
}
- if (instance) {
- html += '
Instanz' + esc(instance.status) + "
" +
- '
Rapport öffnen →';
+
+ if (instances && instances.length) {
+ h += '
Ihre Instanzen
';
+ h += instances.map((i) =>
+ '
' + esc(i.label || i.studio_slug) + "" +
+ '
' + esc(i.instance_url) + "
" +
+ '
Öffnen →'
+ ).join("");
+ // Weitere Instanz (Multi-Instanz vorbereitet — Backend-Sperre öffnen wir später)
+ h += '
';
} else {
- html += '
Wählen Sie ein Abo, um Ihre Instanz freizuschalten:
' +
+ h += '
Wählen Sie ein Abo, um Ihre Instanz freizuschalten:
' +
'
' + (plans || []).map(planCard).join("") + "
";
}
- root.innerHTML = card(html, true);
- root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); };
- root.querySelectorAll("[data-plan]").forEach((b) => {
- b.onclick = async () => {
- b.disabled = true;
+ return h;
+ }
+
+ // — Tab: Profil (Firma / Rechnungsadresse) —
+ function tabProfile() {
+ const a = acctData.account;
+ const f = (id, label, val, ph) =>
+ '
" +
+ '
';
+ return '
' +
+ f("company", "Firma / Büro", a.company, "Architektur Muster GmbH") +
+ f("contact_name", "Ansprechperson", a.contact_name, "Vor- und Nachname") +
+ f("street", "Strasse & Nr.", a.street) +
+ '
' +
+ '
' + f("zip", "PLZ", a.zip) + "
" +
+ '
' + f("city", "Ort", a.city) + "
" +
+ f("country", "Land", a.country || "CH") +
+ f("phone", "Telefon", a.phone) +
+ '
';
+ }
+
+ // — Tab: Sicherheit (Passwort) —
+ function tabSecurity() {
+ return '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
';
+ }
+
+ // — Event-Wiring je Tab —
+ function wireTab() {
+ if (acctTab === "overview") {
+ root.querySelectorAll("[data-plan]").forEach((b) =>
+ (b.onclick = async () => {
+ b.disabled = true;
+ try { const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan }); go(url); }
+ catch (err) { alert(err.message); b.disabled = false; }
+ })
+ );
+ const add = root.querySelector("#addInstance");
+ if (add) add.onclick = () => alert("Weitere Instanzen kommen bald. Kontaktieren Sie uns für ein Mehrfach-Abo.");
+ }
+ if (acctTab === "profile") {
+ root.querySelector("#saveProfile").onclick = async (e) => {
+ const btn = e.target; btn.disabled = true;
+ const m = root.querySelector("#pmsg");
+ const payload = {};
+ ["company", "contact_name", "street", "zip", "city", "country", "phone"].forEach(
+ (id) => (payload[id] = root.querySelector("#" + id).value.trim())
+ );
try {
- const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan });
- go(url);
- } catch (err) { alert(err.message); b.disabled = false; }
+ const { account } = await api("PATCH", "/account/me", payload);
+ acctData.account = { ...acctData.account, ...account };
+ msg(m, "Gespeichert.", "ok");
+ } catch (err) { msg(m, err.message, "err"); }
+ btn.disabled = false;
};
- });
+ }
+ if (acctTab === "security") {
+ root.querySelector("#savePw").onclick = async (e) => {
+ const btn = e.target; btn.disabled = true;
+ const m = root.querySelector("#smsg");
+ try {
+ await api("POST", "/account/password", {
+ currentPassword: root.querySelector("#curPw").value,
+ newPassword: root.querySelector("#newPw").value,
+ });
+ root.querySelector("#curPw").value = ""; root.querySelector("#newPw").value = "";
+ msg(m, "Passwort geändert.", "ok");
+ } catch (err) { msg(m, err.message, "err"); }
+ btn.disabled = false;
+ };
+ }
}
function planCard(p) {