// Woodbury Operator — shared data, roles, page header + toast (window globals).
(function () {
window.WB_SITES = ["Lashibi Townhouses", "Lashibi Executive Houses", "Mirage", "Onehive Tse Addo", "Hamlet", "Cantonments City"];
// Role model. `nav` lists which sidebar entries the role sees; `scope` is the
// context shown in the sidebar. Super Admin is national; others are site-scoped.
window.WB_ROLES = [
{ key: "super", label: "Super Administrator", scope: "All Ghana Locations", scopeLabel: "Admin Scope",
nav: ["Dashboard", "Properties", "Tenants", "Screening", "Leases", "Maintenance", "Vendors", "Inspections", "Move In/Out", "Documents", "Messages", "Reports", "Users", "Audit Log", "Settings"] },
{ key: "site", label: "Site Administrator", scope: "Lashibi Townhouses", scopeLabel: "Assigned Site",
nav: ["Dashboard", "Properties", "Tenants", "Screening", "Leases", "Maintenance", "Inspections", "Move In/Out", "Documents", "Messages", "Reports"] },
{ key: "operator", label: "Site Operator", scope: "Lashibi Townhouses", scopeLabel: "Assigned Site",
nav: ["Dashboard", "Maintenance", "Vendors", "Inspections", "Messages"] },
{ key: "agent", label: "Leasing Agent", scope: "Cantonments City + 2", scopeLabel: "Assigned Sites",
nav: ["Dashboard", "Screening", "Leases", "Tenants", "Move In/Out", "Documents", "Messages"] },
];
// ---- Permissions (real capability gating, not just nav) ----
// Capabilities each role holds. Screens call window.WBCan("cap") to gate actions.
window.WB_PERMS = {
super: ["*"],
site: ["property.edit", "unit.add", "tenant.add", "tenant.edit", "lease.new", "lease.renew", "screening.decide", "maintenance.new", "maintenance.assign", "inspection.new", "doc.upload", "move.manage", "alert.send"],
operator: ["maintenance.new", "maintenance.assign", "inspection.new"],
agent: ["tenant.add", "tenant.edit", "lease.new", "lease.renew", "screening.decide", "screening.invite", "doc.upload", "move.manage"],
};
window.WBCan = function (cap) {
const role = window.WBRoleKey || "super";
const perms = window.WB_PERMS[role] || [];
return perms.indexOf("*") !== -1 || perms.indexOf(cap) !== -1;
};
// ---- Cross-screen store + event bus (connects the lifecycle loops) ----
// Newly created records added by one screen and picked up live by others.
window.WB_NEW = { tenants: [], moves: [], tickets: [] };
window.WBbus = new EventTarget();
window.WBadd = function (kind, item) {
(window.WB_NEW[kind] = window.WB_NEW[kind] || []).push(item);
window.WBbus.dispatchEvent(new CustomEvent(kind));
};
// React hook: returns the current WB_NEW[kind] list, re-rendering on change.
window.WBuseNew = function (kind) {
const [, tick] = React.useReducer((x) => x + 1, 0);
React.useEffect(() => {
const h = () => tick();
window.WBbus.addEventListener(kind, h);
return () => window.WBbus.removeEventListener(kind, h);
}, [kind]);
return window.WB_NEW[kind] || [];
};
// ---- Resident → Operator bridge (same-origin localStorage channel) ----
// Resident portal writes maintenance requests here; operator Maintenance reads them.
window.WBportalKey = "wb_portal_requests";
window.WBreadPortalRequests = function () {
try { return JSON.parse(localStorage.getItem(window.WBportalKey) || "[]"); } catch (e) { return []; }
};
// Convenience: render children only if permitted.
window.WBGate = function ({ cap, children, fallback }) {
return window.WBCan(cap) ? children : (fallback || null);
};
// ---- Empty & loading states (uniform across tables) ----
window.WBEmpty = function ({ icon, title, message, action }) {
return (
{title}
{message &&
{message}
}
{action &&
{action}
}
);
};
window.WBSkeletonRows = function ({ rows, cols }) {
rows = rows || 4; cols = cols || 4;
return (
{Array.from({ length: rows }).map((_, r) => (
{Array.from({ length: cols }).map((__, c) => (
))}
))}
);
};
// Hook: brief loading state on first mount (demonstrates skeletons).
window.WBuseLoad = function (ms) {
const [loading, setLoading] = React.useState(true);
React.useEffect(() => { const t = setTimeout(() => setLoading(false), ms || 550); return () => clearTimeout(t); }, []);
return loading;
};
window.WB_DATA = {
applicants: [
{ name: "Efua Asante", email: "efua.asante@gmail.com", phone: "+233 24 411 9283", asset: "Lashibi Townhouses", status: "Pending", avatar: "../../assets/images/av-efua.jpg" },
{ name: "Kofi Mensah", email: "kofi.mensah@yahoo.com", phone: "+233 20 887 3341", asset: "Lashibi Townhouses", status: "Verified", avatar: "../../assets/images/av-kofi.jpg" },
{ name: "Ama Osei", email: "ama.osei@outlook.com", phone: "+233 55 120 9987", asset: "Cantonments City", status: "Verified", avatar: "../../assets/images/av-ama.jpg" },
{ name: "Yaw Boateng", email: "yboateng@gmail.com", phone: "+233 24 330 7765", asset: "Mirage", status: "Pending", avatar: "../../assets/images/av-yaw.jpg" },
{ name: "Nana Adjei", email: "nana.adjei@ghana.com", phone: "+233 50 445 2210", asset: "Onehive Tse Addo", status: "Verified", avatar: "../../assets/images/av-nana.jpg" },
],
maintenance: [
{ unit: "House 1 — AC Leak", meta: "Reported 2 hours ago · HVAC team notified", tone: "danger", badge: "Urgent" },
{ unit: "Villa 7 — Roof Tile Repair", meta: "Scheduled for tomorrow · Roofer assigned", tone: "accent" },
{ unit: "Flat 2B — Geyser Fault", meta: "Parts ordered · ETA 3 days", tone: "success" },
{ unit: "Villa 12 — Gate Remote Sync", meta: "Low priority · Backlog", tone: "neutral" },
],
properties: [
{ name: "Lashibi Townhouses", type: "3-Bedroom Townhouses", units: 16, occ: 56, vacant: 7, status: "Premium", img: "../../assets/images/featured-cantonments.jpg" },
{ name: "Lashibi Executive Houses", type: "Executive Houses", units: 8, occ: 88, vacant: 1, status: "Premium", img: "../../assets/images/property-1.jpg" },
{ name: "Mirage", type: "Apartments", units: 24, occ: 92, vacant: 2, status: "Standard", img: "../../assets/images/building-hero.jpg" },
{ name: "Onehive Tse Addo", type: "Serviced Apartments", units: 18, occ: 78, vacant: 4, status: "Standard", img: "../../assets/images/featured-cantonments.jpg" },
{ name: "Hamlet", type: "Townhouses", units: 12, occ: 100, vacant: 0, status: "Flagship", img: "../../assets/images/property-1.jpg" },
{ name: "Cantonments City", type: "Luxury Apartments", units: 30, occ: 90, vacant: 3, status: "Flagship", img: "../../assets/images/building-hero.jpg" },
],
documents: [
{ name: "Signed Tenancy Agreement.pdf", meta: "248 KB · Signed 12 Apr", tone: "brand" },
{ name: "Land Registration Papers.pdf", meta: "1.1 MB · Uploaded 30 Mar", tone: "sunken" },
{ name: "Insurance Certificate 2026.pdf", meta: "512 KB · Uploaded 04 Feb", tone: "sunken" },
{ name: "Lashibi Townhouses Floor Plan.pdf", meta: "3.4 MB · Uploaded 21 Jan", tone: "accent" },
],
vendors: [
{ name: "Kojo Roofing & Works", trade: "Roofing", phone: "+233 24 555 1200", jobs: 12, rating: "On-site", tone: "accent", icon: "home" },
{ name: "Mensah Plumbing Co.", trade: "Plumbing", phone: "+233 20 771 4432", jobs: 28, rating: "Preferred", tone: "brand", icon: "droplet" },
{ name: "AirCool HVAC Services", trade: "HVAC", phone: "+233 55 880 9910", jobs: 19, rating: "Preferred", tone: "success", icon: "wind" },
{ name: "Accra Electricals Ltd", trade: "Electrical", phone: "+233 24 330 0055", jobs: 9, rating: "Standard", tone: "sunken", icon: "zap" },
{ name: "GateTech Automation", trade: "Access & Gates", phone: "+233 50 442 8821", jobs: 6, rating: "Standard", tone: "sunken", icon: "door-closed" },
],
users: [
{ name: "Kwame Ansah", email: "kwame.ansah@woodbury.com", role: "Super Administrator", site: "All Ghana Locations", status: "Active", twoFA: true, avatar: "../../assets/images/avatar-1.jpg" },
{ name: "Adjoa Owusu", email: "adjoa.owusu@woodbury.com", role: "Site Administrator", site: "Lashibi Townhouses", status: "Active", twoFA: true, avatar: "../../assets/images/av-ama.jpg" },
{ name: "Kwesi Appiah", email: "kwesi.appiah@woodbury.com", role: "Site Operator", site: "Cantonments City", status: "Active", twoFA: false, avatar: "../../assets/images/av-yaw.jpg" },
{ name: "Abena Sarpong", email: "abena.sarpong@woodbury.com", role: "Leasing Agent", site: "Mirage + 2", status: "Invited", twoFA: false, avatar: "../../assets/images/av-efua.jpg" },
{ name: "Yaw Darko", email: "yaw.darko@woodbury.com", role: "Site Operator", site: "Onehive Tse Addo", status: "Suspended", twoFA: true, avatar: "../../assets/images/av-nana.jpg" },
],
};
window.WBPageHeader = function ({ eyebrow, title, sub, actions }) {
return (
{eyebrow}
{title}
{sub &&
{sub}
}
{actions &&
{actions}
}
);
};
// 6-digit verification code input (auto-advances). onChange(code), onComplete(code).
window.WBCodeInput = function ({ onChange, onComplete }) {
const [vals, setVals] = React.useState(["", "", "", "", "", ""]);
const refs = React.useRef([]);
const set = (i, v) => {
v = v.replace(/\D/g, "").slice(-1);
const next = vals.slice(); next[i] = v; setVals(next);
const code = next.join("");
onChange && onChange(code);
if (v && i < 5) refs.current[i + 1] && refs.current[i + 1].focus();
if (code.length === 6 && !next.includes("")) onComplete && onComplete(code);
};
const onKey = (i, e) => { if (e.key === "Backspace" && !vals[i] && i > 0) refs.current[i - 1] && refs.current[i - 1].focus(); };
return (
{vals.map((v, i) => (
(refs.current[i] = el)} value={v} inputMode="numeric" maxLength={1}
onChange={(e) => set(i, e.target.value)} onKeyDown={(e) => onKey(i, e)}
style={{ width: 52, height: 60, textAlign: "center", fontFamily: "var(--font-serif)", fontWeight: 700, fontSize: 26, color: "var(--text-heading)", border: "1px solid var(--border-strong)", borderRadius: "var(--radius-md)", background: "var(--surface)", outline: "none", boxShadow: "var(--shadow-xs)" }}
onFocus={(e) => { e.target.style.borderColor = "var(--brand)"; e.target.style.boxShadow = "var(--focus-ring)"; }}
onBlur={(e) => { e.target.style.borderColor = "var(--border-strong)"; e.target.style.boxShadow = "var(--shadow-xs)"; }} />
))}
);
};
// Units per property (drives the Add Tenant unit picker + Properties unit list).
window.WB_UNITS = {
"Lashibi Townhouses": [
{ label: "House 1", status: "Occupied", tenant: "Kofi Mensah" }, { label: "House 2", status: "Occupied", tenant: "Ama Osei" }, { label: "House 3", status: "Vacant" }, { label: "House 4", status: "Occupied", tenant: "Nii Armah" },
{ label: "House 5", status: "Occupied", tenant: "Efua Asante" }, { label: "House 6", status: "Vacant" }, { label: "House 7", status: "Occupied", tenant: "Yaw Boateng" }, { label: "House 8", status: "Vacant" },
{ label: "House 9", status: "Occupied", tenant: "Adjoa Owusu" }, { label: "House 10", status: "Occupied", tenant: "Kwesi Appiah" }, { label: "House 11", status: "Vacant" }, { label: "House 12", status: "Occupied", tenant: "Abena Sarpong" },
{ label: "House 13", status: "Vacant" }, { label: "House 14", status: "Vacant" }, { label: "House 15", status: "Occupied", tenant: "Nana Adjei" }, { label: "House 16", status: "Vacant" },
],
"Lashibi Executive Houses": [
{ label: "House 1", status: "Occupied", tenant: "Kojo Danso" }, { label: "House 2", status: "Occupied", tenant: "Akua Frimpong" }, { label: "House 3", status: "Occupied", tenant: "Kwabena Osei" }, { label: "House 4", status: "Vacant" },
{ label: "House 5", status: "Occupied", tenant: "Yaa Serwaa" }, { label: "House 6", status: "Occupied", tenant: "Kofi Boadu" }, { label: "House 7", status: "Occupied", tenant: "Esi Quaye" }, { label: "House 8", status: "Occupied", tenant: "Kojo Danso" },
],
"Mirage": [
{ label: "Apartment A1", status: "Occupied", tenant: "Ama Osei" }, { label: "Apartment A2", status: "Occupied", tenant: "Nii Armah" }, { label: "Apartment B3", status: "Vacant" }, { label: "Apartment C1", status: "Vacant" },
],
"Onehive Tse Addo": [
{ label: "Unit 101", status: "Occupied", tenant: "Nana Adjei" }, { label: "Unit 104", status: "Vacant" }, { label: "Unit 202", status: "Occupied", tenant: "Efua Asante" }, { label: "Unit 205", status: "Vacant" },
],
"Hamlet": [
{ label: "House 1", status: "Occupied", tenant: "Abena Sarpong" }, { label: "House 2", status: "Occupied", tenant: "Yaw Boateng" }, { label: "House 3", status: "Occupied", tenant: "Kwesi Appiah" }, { label: "House 4", status: "Occupied", tenant: "Adjoa Owusu" },
],
"Cantonments City": [
{ label: "Apt 5A", status: "Occupied", tenant: "Kwabena Osei" }, { label: "Apt 9B", status: "Occupied", tenant: "Akua Frimpong" }, { label: "Apt 11C", status: "Vacant" }, { label: "Penthouse 2", status: "Vacant" },
],
};
window.WBunitsFor = function (site, onlyVacant) {
const list = window.WB_UNITS[site] || [];
return (onlyVacant ? list.filter((u) => u.status === "Vacant") : list).map((u) => u.label);
};
// Tenant names available to link to units.
window.WB_TENANT_NAMES = ["Kofi Mensah", "Ama Osei", "Efua Asante", "Nana Adjei", "Nii Armah", "Yaw Boateng", "Adjoa Owusu", "Kwesi Appiah", "Abena Sarpong", "Kojo Danso", "Akua Frimpong", "Kwabena Osei", "Yaa Serwaa", "Kofi Boadu", "Esi Quaye"];
// Tenant-linked documents. Keyed by tenant name; each files into a folder too.
window.WB_TENANT_DOCS = {
"Kofi Mensah": [
{ name: "Signed Tenancy Agreement.pdf", meta: "248 KB · Signed 12 Apr", folder: "Tenancy Agreements" },
{ name: "ID Verification.pdf", meta: "512 KB · Verified 10 Apr", folder: "Screening" },
],
};
window.WBAddTenantDoc = function (tenant, doc) {
if (!window.WB_TENANT_DOCS[tenant]) window.WB_TENANT_DOCS[tenant] = [];
window.WB_TENANT_DOCS[tenant].unshift(doc);
};
// Tenant roster (name + site) for targeted alerts.
window.WB_TENANT_ROSTER = [
{ name: "Kofi Mensah", site: "Lashibi Townhouses" },
{ name: "Efua Asante", site: "Lashibi Townhouses" },
{ name: "Yaw Mensah", site: "Lashibi Townhouses" },
{ name: "Ama Osei", site: "Cantonments City" },
{ name: "Adjoa Boateng", site: "Cantonments City" },
{ name: "Yaw Boateng", site: "Mirage" },
{ name: "Akua Darko", site: "Mirage" },
{ name: "Nana Adjei", site: "Onehive Tse Addo" },
{ name: "Kojo Owusu", site: "Hamlet" },
{ name: "Abena Sarfo", site: "Lashibi Executive Houses" },
];
// Reusable image-upload dropzone with live thumbnail previews.
window.WBImageUpload = function ({ label }) {
const [imgs, setImgs] = React.useState([]);
const ref = React.useRef(null);
const onFiles = (e) => {
const files = Array.from(e.target.files || []);
const urls = files.map((f) => URL.createObjectURL(f));
setImgs((s) => [...s, ...urls]);
if (urls.length) window.WBToast(urls.length + " image" + (urls.length === 1 ? "" : "s") + " added");
};
const remove = (u) => setImgs((s) => s.filter((x) => x !== u));
return (
{label &&
{label}
}
ref.current && ref.current.click()} style={{ border: "2px dashed var(--border-strong)", borderRadius: "var(--radius-lg)", padding: "24px", textAlign: "center", background: "var(--cream-50)", cursor: "pointer" }}>
Click to upload images
JPG or PNG · up to 10 MB each
{imgs.length > 0 && (
{imgs.map((u) => (
))}
)}
);
};
// Shared Notify / Send Alert modal with tenant targeting.
window.WBNotifyModal = function ({ open, onClose, title, defaultSite, lockSite }) {
const NS = window.WoodburyPropertiesDesignSystem_7290ca;
const { Modal, Input, Select, Textarea, Button } = NS;
const [audience, setAudience] = React.useState("Residents only");
const [site, setSite] = React.useState(defaultSite || "All Ghana Locations");
const [sel, setSel] = React.useState([]);
React.useEffect(() => { if (open) { setSel([]); setSite(defaultSite || "All Ghana Locations"); setAudience("Residents only"); } }, [open]);
const specific = audience === "Specific tenants";
const roster = window.WB_TENANT_ROSTER.filter((t) => site === "All Ghana Locations" ? true : t.site === site);
const toggle = (n) => setSel((s) => s.includes(n) ? s.filter((x) => x !== n) : [...s, n]);
const allSel = roster.length > 0 && sel.length === roster.length;
const send = () => {
let desc = specific ? (sel.length + " selected tenant" + (sel.length === 1 ? "" : "s")) : (audience + " · " + (site === "All Ghana Locations" ? "all sites" : site));
onClose();
window.WBToast("Alert sent to " + desc);
};
const siteOptions = lockSite ? [defaultSite] : ["All Ghana Locations", ...window.WB_SITES];
return (
}>
);
};
// Lightweight DOM toast — works across all screen React trees.
window.WBToast = function (msg, tone) {
let host = document.getElementById("wb-toast-host");
if (!host) {
host = document.createElement("div");
host.id = "wb-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);
};
// Responsive: returns { mobile (<768), tablet (<1080) }.
window.WBuseMedia = function () {
const get = () => ({ mobile: window.matchMedia("(max-width: 767px)").matches, tablet: window.matchMedia("(max-width: 1079px)").matches });
const [m, setM] = React.useState(get);
React.useEffect(() => {
const on = () => setM(get());
window.addEventListener("resize", on);
return () => window.removeEventListener("resize", on);
}, []);
return m;
};
// Confirm dialog (Promise-based, DOM-rendered). await window.WBConfirm({...}).
window.WBConfirm = function (opts) {
opts = opts || {};
return new Promise((resolve) => {
const scrim = document.createElement("div");
scrim.style.cssText = "position:fixed;inset:0;z-index:3000;background:rgba(40,24,9,.42);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:24px;font-family:'Plus Jakarta Sans',sans-serif;opacity:0;transition:opacity .14s";
const danger = opts.danger;
scrim.innerHTML = ''
+ '
' + (opts.title || "Are you sure?") + '
'
+ '
' + (opts.message || "") + '
'
+ '
'
+ ''
+ ''
+ '
';
document.body.appendChild(scrim);
requestAnimationFrame(() => { scrim.style.opacity = "1"; });
const done = (v) => { scrim.style.opacity = "0"; setTimeout(() => scrim.remove(), 160); resolve(v); };
scrim.querySelector('[data-x="o"]').onclick = () => done(true);
scrim.querySelector('[data-x="c"]').onclick = () => done(false);
scrim.onclick = (e) => { if (e.target === scrim) done(false); };
});
};
// Notifications feed (super-admin view).
window.WB_NOTIFS = [
{ icon: "triangle-alert", tone: "danger", title: "Urgent maintenance", body: "House 1 — AC Leak reported at Lashibi Townhouses.", time: "2h ago", unread: true, screen: "Maintenance" },
{ icon: "user-check", tone: "accent", title: "New applicant", body: "Efua Asante applied for Lashibi Townhouses.", time: "4h ago", unread: true, screen: "Screening" },
{ icon: "calendar-clock", tone: "accent", title: "Lease expiring", body: "Kofi Mensah's lease ends in 28 days.", time: "1d ago", unread: true, screen: "Leases" },
{ icon: "shield-check", tone: "success", title: "Screening cleared", body: "Ama Osei passed identity verification.", time: "1d ago", unread: false, screen: "Screening" },
{ icon: "file-text", tone: "brand", title: "Document uploaded", body: "Land Registration Papers.pdf added.", time: "2d ago", unread: false, screen: "Documents" },
];
// Audit trail.
window.WB_AUDIT = [
{ who: "Kwame Ansah", avatar: "../../assets/images/avatar-1.jpg", action: "changed role", target: "Abena Sarpong → Leasing Agent", cat: "Access", time: "Today, 09:42" },
{ who: "Adjoa Owusu", avatar: "../../assets/images/av-ama.jpg", action: "approved tenancy", target: "Ama Osei · Cantonments City", cat: "Screening", time: "Today, 08:15" },
{ who: "Kwesi Appiah", avatar: "../../assets/images/av-yaw.jpg", action: "assigned vendor", target: "AirCool HVAC → House 1", cat: "Maintenance", time: "Yesterday, 16:30" },
{ who: "Kwame Ansah", avatar: "../../assets/images/avatar-1.jpg", action: "enabled 2FA", target: "Yaw Darko", cat: "Security", time: "Yesterday, 11:02" },
{ who: "Adjoa Owusu", avatar: "../../assets/images/av-ama.jpg", action: "uploaded document", target: "Signed Tenancy Agreement.pdf", cat: "Documents", time: "12 Apr, 14:20" },
{ who: "Kwame Ansah", avatar: "../../assets/images/avatar-1.jpg", action: "sent broadcast", target: "Water maintenance notice · All sites", cat: "Comms", time: "10 Apr, 10:05" },
];
// Inspections (move-in / move-out / routine).
window.WB_INSPECTIONS = [
{ unit: "Villa 2 — Cantonments City", tenant: "Ama Osei", type: "Move-in", date: "22 Apr 2026", status: "Scheduled", tone: "accent", inspector: "Kwesi Appiah" },
{ unit: "House 1 — Lashibi Townhouses", tenant: "Kofi Mensah", type: "Routine", date: "18 Apr 2026", status: "In Progress", tone: "warning", inspector: "Adjoa Owusu" },
{ unit: "House 5 — Lashibi Townhouses", tenant: "Efua Asante", type: "Move-out", date: "30 Apr 2026", status: "Scheduled", tone: "accent", inspector: "Adjoa Owusu" },
{ unit: "Unit 101 — Onehive Tse Addo", tenant: "Nana Adjei", type: "Routine", date: "02 Apr 2026", status: "Completed", tone: "success", inspector: "Kwesi Appiah" },
];
window.WB_INSPECTION_CHECKLIST = [
{ area: "Walls & ceilings", done: true },
{ area: "Flooring", done: true },
{ area: "Plumbing & fixtures", done: true },
{ area: "Electrical & lighting", done: false },
{ area: "Appliances", done: false },
{ area: "Doors, locks & keys", done: false },
{ area: "Windows & screens", done: false },
];
// Compliance documents with expiry tracking.
window.WB_COMPLIANCE = [
{ doc: "Fire Safety Certificate", site: "Lashibi Townhouses", expires: "12 May 2026", days: 24, status: "Expiring", tone: "danger" },
{ doc: "Property Insurance", site: "Portfolio-wide", expires: "04 Feb 2027", days: 292, status: "Valid", tone: "success" },
{ doc: "Land Registration", site: "Cantonments City", expires: "30 Sep 2026", days: 165, status: "Valid", tone: "success" },
{ doc: "Lift Inspection Permit", site: "Mirage", expires: "28 Apr 2026", days: 10, status: "Expiring", tone: "danger" },
{ doc: "Waste Management License", site: "Onehive Tse Addo", expires: "15 Jun 2026", days: 58, status: "Due Soon", tone: "warning" },
];
// Broadcast / announcement history.
window.WB_ANNOUNCEMENTS = [
{ subject: "Scheduled water maintenance", scope: "All Ghana Locations", audience: "All residents", sent: "10 Apr 2026", by: "Kwame Ansah" },
{ subject: "Gate access upgrade", scope: "Lashibi Townhouses", audience: "Residents", sent: "02 Apr 2026", by: "Adjoa Owusu" },
{ subject: "Q2 inspection schedule", scope: "Cantonments City", audience: "Residents & staff", sent: "28 Mar 2026", by: "Abena Sarpong" },
];
// ==========================================================================
// DEV BACKEND WIRING (WOO-19) — hydrate all seed data from wb-dev-api.
// Same-origin GET /api/operator/bootstrap (Caddy proxies /api/* -> :8090).
// Synchronous so server data is in place before any screen renders; falls
// back SILENTLY to the inline design-system seed above if the API is
// unreachable, so the prototype look never breaks. Also write-throughs any
// records created in-app back to the dev backend.
// ==========================================================================
(function wireDevApi() {
window.WB_API_BASE = "/api/operator";
try {
var xhr = new XMLHttpRequest();
xhr.open("GET", window.WB_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);
if (B.sites) window.WB_SITES = B.sites;
if (B.roles) window.WB_ROLES = B.roles;
if (B.perms) window.WB_PERMS = B.perms;
if (B.data) window.WB_DATA = B.data;
if (B.units) window.WB_UNITS = B.units;
if (B.tenantNames) window.WB_TENANT_NAMES = B.tenantNames;
if (B.tenantDocs) window.WB_TENANT_DOCS = B.tenantDocs;
if (B.tenantRoster) window.WB_TENANT_ROSTER = B.tenantRoster;
if (B.notifs) window.WB_NOTIFS = B.notifs;
if (B.audit) window.WB_AUDIT = B.audit;
if (B.inspections) window.WB_INSPECTIONS = B.inspections;
if (B.inspectionChecklist) window.WB_INSPECTION_CHECKLIST = B.inspectionChecklist;
if (B.compliance) window.WB_COMPLIANCE = B.compliance;
if (B.announcements) window.WB_ANNOUNCEMENTS = B.announcements;
if (B.live) {
window.WB_NEW = { tenants: (B.live.tenants || []).slice(), moves: (B.live.moves || []).slice(), tickets: (B.live.tickets || []).slice() };
window.WB_SERVER_PORTAL = B.live.portalRequests || [];
}
window.WB_DATA_SOURCE = "dev.api";
} catch (e) {
window.WB_DATA_SOURCE = "inline-fallback";
if (window.console && console.warn) console.warn("[WOO-19] dev.api bootstrap unavailable — using inline seed:", e && e.message);
}
// Write-through: records created in one screen also POST to the dev backend.
var _post = function (path, body) {
try {
var x = new XMLHttpRequest();
x.open("POST", window.WB_API_BASE + path, true);
x.withCredentials = true;
x.setRequestHeader("Content-Type", "application/json");
x.send(JSON.stringify(body || {}));
} catch (e) {}
};
var _endpoint = { tenants: "/tenants", moves: "/moves", tickets: "/tickets" };
var _origAdd = window.WBadd;
window.WBadd = function (kind, item) {
_origAdd(kind, item);
if (_endpoint[kind]) _post(_endpoint[kind], item);
};
// Merge server-side resident portal requests into the operator's reader
// (resident -> operator bridge now also flows through dev.api, not just
// the same-browser localStorage channel).
var _origPortal = window.WBreadPortalRequests;
window.WBreadPortalRequests = function () {
var local = _origPortal();
var server = window.WB_SERVER_PORTAL || [];
var seen = {}, out = [];
local.concat(server).forEach(function (r) {
var k = r && (r.id || JSON.stringify(r));
if (k && seen[k]) return;
if (k) seen[k] = 1;
out.push(r);
});
return out;
};
})();
})();