// Woodbury Resident Portal — shared data, header, toast, modal helpers. (function () { window.WBR = { resident: { name: "Kofi Mensah", first: "Kofi", email: "kofi.mensah@yahoo.com", phone: "+233 20 887 3341", avatar: "../../assets/images/av-kofi.jpg", property: "Lashibi Townhouses", unit: "House 1", emergency: "Akosua Mensah · +233 24 118 0092", moveIn: "01 Jun 2024", leaseEnds: "28 May 2026", leaseLeft: 28, term: "12 months", }, lease: { ref: "WB-LSE-2024-0417", type: "Assured Shorthold Tenancy", term: "12 months", bedrooms: 3, start: "01 Jun 2024", end: "28 May 2026", left: 28, total: 365, landlord: "Woodbury Properties Ltd", manager: "Lashibi Townhouses Office", terms: [ { icon: "calendar-days", label: "Tenancy term", value: "12 months (renewable)" }, { icon: "home", label: "Property", value: "Lashibi Townhouses · House 1" }, { icon: "bed", label: "Bedrooms", value: "3-bedroom townhouse" }, { icon: "paw-print", label: "Pets", value: "By written permission" }, { icon: "users", label: "Occupancy", value: "Up to 5 residents" }, { icon: "wrench", label: "Maintenance", value: "Landlord-covered (structural)" }, ], }, notifications: [ { id: "n1", icon: "user-check", tone: "accent", title: "Vendor assigned", body: "AirCool HVAC will attend your AC request (MR-2041).", time: "2h ago", unread: true, go: ["Requests", "MR-2041"] }, { id: "n2", icon: "droplet", tone: "accent", title: "Water maintenance", body: "Supply interrupted tomorrow 9am–12pm.", time: "5h ago", unread: true, go: ["Announcements"] }, { id: "n3", icon: "calendar", tone: "brand", title: "Inspection scheduled", body: "Routine unit inspection on 22 Aug, 11:00.", time: "1d ago", unread: true, go: ["Announcements"] }, { id: "n4", icon: "check-check", tone: "success", title: "Request resolved", body: "Your Wi-Fi router replacement is complete.", time: "3d ago", unread: false, go: ["Requests", "MR-1975"] }, ], requests: [ { id: "MR-2041", title: "AC not cooling", cat: "HVAC / Cooling", icon: "wind", stage: 1, priority: "Urgent", tone: "danger", desc: "Unit making a loud grinding noise and not cooling. Started overnight.", vendor: "AirCool HVAC Services", reported: "Today · 09:12", log: [ { icon: "user-check", tone: "accent", title: "Vendor assigned", body: "AirCool HVAC Services assigned.", time: "Today · 09:40" }, { icon: "wrench", tone: "danger", title: "Request submitted", body: "You reported this issue.", time: "Today · 09:12" } ] }, { id: "MR-1998", title: "Repaint living room wall", cat: "General", icon: "paintbrush", stage: 4, priority: "Low", tone: "success", desc: "Scuff marks on the north wall.", vendor: "Woodbury Facilities", reported: "08 Mar", log: [ { icon: "check-check", tone: "success", title: "Resolved", body: "Repaint completed and signed off.", time: "12 Mar · 16:40" }, { icon: "wrench", tone: "brand", title: "Request submitted", body: "You reported this issue.", time: "08 Mar · 10:00" } ] }, { id: "MR-1975", title: "Wi-Fi router replacement", cat: "Appliances", icon: "wifi", stage: 4, priority: "Medium", tone: "success", desc: "Router not holding connection.", vendor: "Accra Electricals Ltd", reported: "31 Jan", log: [ { icon: "check-check", tone: "success", title: "Resolved", body: "New router installed.", time: "02 Feb · 14:20" } ] }, ], stages: ["Submitted", "Assigned", "Scheduled", "In Progress", "Resolved"], announcements: [ { id: "a1", icon: "droplet", tone: "accent", title: "Scheduled water maintenance", body: "Water supply interrupted tomorrow 9am–12pm for pipe maintenance.", time: "2h ago", unread: true }, { id: "a2", icon: "shield-check", tone: "success", title: "Gate access upgrade complete", body: "Your new gate remote code is active. Contact the office for a physical fob.", time: "3d ago", unread: false }, { id: "a3", icon: "calendar", tone: "brand", title: "Quarterly inspection", body: "Routine unit inspection scheduled for 22 Aug, 11:00. No action needed.", time: "5d ago", unread: false }, ], documents: [ { name: "Signed Tenancy Agreement.pdf", meta: "248 KB · Signed 12 Apr", tag: "Lease", tone: "brand" }, { name: "House Rules & Community Guide.pdf", meta: "1.2 MB · Updated Jan 2026", tag: "Guide", tone: "neutral" }, { name: "ID Verification.pdf", meta: "512 KB · Verified 10 Apr", tag: "Screening", tone: "success" }, { name: "Move-in Inspection Report.pdf", meta: "890 KB · 01 Jun 2024", tag: "Inspection", tone: "accent" }, ], messages: [ { me: false, text: "Akwaaba Kofi! This is the Lashibi Townhouses office. How can we help?", time: "09:00" }, { me: true, text: "Hi, the AC in my unit is making a loud noise and not cooling.", time: "09:12" }, { me: false, text: "Sorry to hear that — I've logged it as urgent and assigned AirCool HVAC. You can track progress under Requests.", time: "09:31" }, ], }; // ========================================================================== // DEV BACKEND WIRING (WOO-19) — hydrate the resident portal from wb-dev-api. // Same-origin GET /api/resident/bootstrap (Caddy proxies /api/* -> :8090). // Synchronous so server data is in place before any screen renders; falls // back SILENTLY to the inline seed above if the API is unreachable. // ========================================================================== (function wireDevApi() { window.WBR_API_BASE = "/api/resident"; try { var xhr = new XMLHttpRequest(); xhr.open("GET", window.WBR_API_BASE + "/bootstrap", false); // sync: block until seeded xhr.withCredentials = true; xhr.send(null); if (xhr.status !== 200) throw new Error("HTTP " + xhr.status); var B = JSON.parse(xhr.responseText); var R = window.WBR; if (B.resident) R.resident = B.resident; if (B.lease) R.lease = B.lease; if (B.notifications) R.notifications = B.notifications; if (B.requests) R.requests = B.requests; if (B.stages) R.stages = B.stages; if (B.announcements) R.announcements = B.announcements; if (B.documents) R.documents = B.documents; if (B.messages) R.messages = B.messages; window.WBR_DATA_SOURCE = "dev.api"; } catch (e) { window.WBR_DATA_SOURCE = "inline-fallback"; if (window.console && console.warn) console.warn("[WOO-19] dev.api resident bootstrap unavailable — using inline seed:", e && e.message); } // Write-through: a submitted maintenance request also POSTs to dev.api, // where it surfaces to the operator portal (resident -> operator bridge). window.WBRsubmitRequest = function (payload) { try { var x = new XMLHttpRequest(); x.open("POST", window.WBR_API_BASE + "/requests", true); x.withCredentials = true; x.setRequestHeader("Content-Type", "application/json"); x.send(JSON.stringify(payload || {})); } catch (e) {} }; })(); window.WBRHeader = function ({ eyebrow, title, sub, actions }) { return (
{eyebrow && {eyebrow}}

{title}

{sub &&

{sub}

}
{actions &&
{actions}
}
); }; window.WBRToast = function (msg, tone) { let host = document.getElementById("wbr-toast-host"); if (!host) { host = document.createElement("div"); host.id = "wbr-toast-host"; host.style.cssText = "position:fixed;right:24px;bottom:24px;z-index:2000;display:flex;flex-direction:column;gap:10px;align-items:flex-end"; document.body.appendChild(host); } const el = document.createElement("div"); const bg = tone === "danger" ? "#B00320" : tone === "accent" ? "#A66533" : "#38230F"; el.style.cssText = "display:flex;align-items:center;gap:10px;padding:13px 18px;border-radius:12px;background:" + bg + ";color:#FAF9F6;font-family:'Plus Jakarta Sans',sans-serif;font-size:14px;font-weight:600;box-shadow:0 14px 34px -12px rgba(58,36,16,.5);opacity:0;transform:translateY(8px);transition:all .18s cubic-bezier(.22,1,.36,1);max-width:360px"; el.textContent = "✓ " + msg; host.appendChild(el); requestAnimationFrame(() => { el.style.opacity = "1"; el.style.transform = "none"; }); setTimeout(() => { el.style.opacity = "0"; el.style.transform = "translateY(8px)"; setTimeout(() => el.remove(), 220); }, 2600); }; window.WBRuseMedia = function () { const get = () => ({ mobile: window.matchMedia("(max-width: 767px)").matches }); const [m, setM] = React.useState(get); React.useEffect(() => { const on = () => setM(get()); window.addEventListener("resize", on); return () => window.removeEventListener("resize", on); }, []); return m; }; // Brief loading state on first mount (drives skeletons). window.WBRuseLoad = function (ms) { const [loading, setLoading] = React.useState(true); React.useEffect(() => { const t = setTimeout(() => setLoading(false), ms || 550); return () => clearTimeout(t); }, []); return loading; }; window.WBREmpty = function ({ icon, title, message, action }) { return (
{title}
{message &&

{message}

} {action &&
{action}
}
); }; window.WBRSkeleton = function ({ rows }) { rows = rows || 3; return (
{Array.from({ length: rows }).map((_, r) => (
))}
); }; // Image upload dropzone with live previews (self-contained for the resident portal). window.WBImageUpload = function ({ label }) { const [imgs, setImgs] = React.useState([]); const ref = React.useRef(null); const onFiles = (e) => { const urls = Array.from(e.target.files || []).map((f) => URL.createObjectURL(f)); setImgs((s) => [...s, ...urls]); if (urls.length) window.WBRToast(urls.length + " photo" + (urls.length === 1 ? "" : "s") + " added"); }; return (
{label &&
{label}
}
ref.current && ref.current.click()} style={{ border: "2px dashed var(--border-strong)", borderRadius: "var(--radius-lg)", padding: "22px", textAlign: "center", background: "var(--cream-50)", cursor: "pointer" }}>
Click to upload photos
{imgs.length > 0 &&
{imgs.map((u) =>
)}
}
); }; })();