// 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) => (
{c === 0 &&
}
))}
))}
); }; // 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 ( }>
setSite(e.target.value)} options={siteOptions} disabled={lockSite} /> {specific && (
{roster.map((t) => { const on = sel.includes(t.name); return ( ); })} {roster.length === 0 &&
No tenants at this site.
}
)}