rspace-online/modules/rschedule/components/folk-schedule-admin.ts

107 lines
4.0 KiB
TypeScript

/**
* <folk-schedule-admin> — admin dashboard for rSchedule.
*
* Passkey-gated (server enforces moderator role on every admin API call).
* Phase A: minimal shell with tab placeholders. Phase E builds full UI
* (availability rules, overrides, bookings list, settings, gcal connect).
*/
class FolkScheduleAdmin extends HTMLElement {
private shadow: ShadowRoot;
private space = "";
private tab: "overview" | "availability" | "bookings" | "calendar" | "settings" = "overview";
private err = "";
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.space = this.getAttribute("space") || "demo";
this.render();
}
private authHeaders(): Record<string, string> {
try {
const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
const token = sess?.token;
return token ? { Authorization: `Bearer ${token}` } : {};
} catch { return {}; }
}
private async ping() {
const path = window.location.pathname;
const m = path.match(/^(\/[^/]+)?\/rschedule/);
const base = `${m?.[0] || "/rschedule"}/api/admin`;
const res = await fetch(`${base}/settings`, { headers: this.authHeaders() });
if (res.status === 401) { this.err = "Sign in to manage this booking page."; return; }
if (res.status === 403) { this.err = "You need moderator or higher role in this space."; return; }
if (!res.ok) { this.err = `Error ${res.status}`; return; }
this.err = "";
}
private render() {
void this.ping().then(() => this.paint());
this.paint();
}
private paint() {
this.shadow.innerHTML = `
<style>
:host { display:block; color:#e2e8f0; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background:#0b1120; min-height:100vh; }
.wrap { max-width:1100px; margin:0 auto; padding:32px 24px; }
h1 { margin:0 0 6px; font-size:1.4rem; }
.sub { color:#94a3b8; margin:0 0 24px; font-size:0.9rem; }
.tabs { display:flex; gap:4px; border-bottom:1px solid rgba(148,163,184,0.12); margin-bottom:24px; overflow-x:auto; }
.tab { background:transparent; color:#94a3b8; border:0; padding:10px 16px; cursor:pointer; font-size:0.9rem; border-bottom:2px solid transparent; }
.tab.active { color:#06b6d4; border-bottom-color:#06b6d4; }
.card { background:#111827; border:1px solid rgba(148,163,184,0.12); border-radius:12px; padding:24px; }
.err { padding:16px; border:1px solid rgba(239,68,68,0.3); background:rgba(239,68,68,0.08); border-radius:8px; color:#fca5a5; margin-bottom:16px; }
.placeholder { color:#94a3b8; padding:40px; text-align:center; border:1px dashed rgba(148,163,184,0.25); border-radius:10px; }
</style>
<div class="wrap">
<h1>rSchedule admin &middot; ${escapeHtml(this.space)}</h1>
<p class="sub">Configure availability, bookings, and Google Calendar for this space.</p>
${this.err ? `<div class="err">${escapeHtml(this.err)}</div>` : ""}
<div class="tabs">
${this.tabBtn("overview", "Overview")}
${this.tabBtn("availability", "Availability")}
${this.tabBtn("bookings", "Bookings")}
${this.tabBtn("calendar", "Google Calendar")}
${this.tabBtn("settings", "Settings")}
</div>
<div class="card">
<div class="placeholder">
<strong>${escapeHtml(this.tab)}</strong><br>
Full admin UI arrives in Phase E. For now, this confirms auth gate works.
</div>
</div>
</div>
`;
this.shadow.querySelectorAll<HTMLButtonElement>(".tab").forEach((btn) => {
btn.addEventListener("click", () => {
this.tab = btn.dataset.tab as any;
this.paint();
});
});
}
private tabBtn(id: string, label: string): string {
return `<button class="tab ${this.tab === id ? "active" : ""}" data-tab="${id}">${label}</button>`;
}
}
function escapeHtml(s: string): string {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
customElements.define("folk-schedule-admin", FolkScheduleAdmin);
export {};