/* ======================================================================
KOREALM — Admin workspace
Dashboard · Inspections · Applications · KYC · Rent & commission · Maintenance
====================================================================== */
function adminNav(db) {
const insp = db.inspections.filter(i => i.status === "requested").length;
const apps = db.applications.filter(a => a.status === "pending").length;
const kyc = db.kyc.filter(k => k.status === "submitted").length;
const maint = db.maintenance.filter(m => m.status === "open").length;
return [
{ view: "dashboard", label: "Overview", icon: "grid" },
{ view: "inspections", label: "Inspections", icon: "calendar", count: insp },
{ view: "applications", label: "Applications", icon: "doc", count: apps },
{ view: "kyc", label: "Landlord KYC", icon: "shield", count: kyc },
{ view: "rent", label: "Rent & commission", icon: "wallet" },
{ view: "maintenance", label: "Maintenance", icon: "wrench", count: maint },
];
}
/* ====================================================================
DASHBOARD
==================================================================== */
function AdminDashboard({ app }) {
const { db, go, user } = app;
const reqInsp = db.inspections.filter(i => i.status === "requested");
const pendApps = db.applications.filter(a => a.status === "pending");
const subKyc = db.kyc.filter(k => k.status === "submitted");
const openMaint = db.maintenance.filter(m => m.status === "open");
const paidRent = db.payments.filter(p => p.type === "rent" && p.status === "paid");
const commission = db.payments.filter(p => p.status === "paid").reduce((s, p) => s + p.commissionAmount, 0);
const gmv = db.payments.filter(p => p.status === "paid").reduce((s, p) => s + p.amount, 0);
const leased = db.properties.filter(p => p.status === "leased").length;
const queues = [
{ label: "Inspections to confirm", count: reqInsp.length, icon: "calendar", view: "inspections", tone: "warning" },
{ label: "Applications to review", count: pendApps.length, icon: "doc", view: "applications", tone: "warning" },
{ label: "KYC to verify", count: subKyc.length, icon: "shield", view: "kyc", tone: "info" },
{ label: "Maintenance to assign", count: openMaint.length, icon: "wrench", view: "maintenance", tone: "danger" },
];
return (
k.status === "verified").length} icon="shield" />
Action queues
{queues.map(q => (
go(q.view)} className="anim-rise">
{q.count > 0 ? {q.count} waiting : Clear}
{q.count}
{q.label}
Review
))}
Latest applications
{db.applications.slice(0, 4).map(a => { const p = db.properties.find(x => x.id === a.propertyId); const t = user(a.tenantId); return (
); })}
Recent payments
{db.payments.filter(p => p.status === "paid").slice(0, 4).map(p => { const t = user(p.tenantId); return (
{p.type} · {t?.name.split(" ")[0]}
+{window.fmt.money(p.commissionAmount)} commission
{window.fmt.money(p.amount)}
); })}
);
}
/* ====================================================================
INSPECTIONS — confirm / decline / reschedule
==================================================================== */
function AdminInspections({ app }) {
const { db, actions, user, toast } = app;
const [tab, setTab] = useState("requested");
const [resch, setResch] = useState(null);
const all = db.inspections.slice().sort((a, b) => new Date(b.requestedDate) - new Date(a.requestedDate));
const counts = {
requested: all.filter(i => i.status === "requested").length,
confirmed: all.filter(i => i.status === "confirmed").length,
completed: all.filter(i => i.status === "completed").length,
};
const list = all.filter(i => tab === "all" ? true : i.status === tab);
return (
{list.length === 0 ?
No inspections in this view. : list.map(i => {
const p = db.properties.find(x => x.id === i.propertyId); const t = user(i.tenantId); const ll = user(p?.landlordId);
const [dt, tm] = i.slot.split("|");
return (
{new Date(dt).toLocaleDateString("en-US", { month: "short" })}{new Date(dt).getDate()}
{p?.title}
{tm}
Tenant {t?.name} · Landlord {ll?.name} · requested {window.fmt.fmtShort(i.requestedDate)}
{i.notes &&
"{i.notes}"
}
{i.status === "requested" && <>
>}
{i.status === "confirmed" && }
);
})}
{resch &&
setResch(null)} onSave={(slot) => { actions.rescheduleInspection(resch.id, slot); toast("Rescheduled & confirmed — tenant notified", "ok"); setResch(null); }} />}
);
}
function RescheduleModal({ inspection, onClose, onSave }) {
const [date, setDate] = useState(null);
const [time, setTime] = useState(null);
const days = useMemo(() => Array.from({ length: 6 }, (_, i) => { const d = window.fmt.today(); d.setDate(d.getDate() + i + 1); return d; }), []);
const times = ["9:30 AM", "11:00 AM", "1:00 PM", "3:00 PM", "4:30 PM"];
return (
>}>
Date
{days.map((d, i) => )}
Time
{times.map(t => )}
);
}
/* ====================================================================
APPLICATIONS — approve / reject
==================================================================== */
function AdminApplications({ app }) {
const { db, actions, user, toast } = app;
const [tab, setTab] = useState("pending");
const [sel, setSel] = useState(null);
const [reject, setReject] = useState(null);
const all = db.applications.slice().sort((a, b) => new Date(b.submittedDate) - new Date(a.submittedDate));
const counts = { pending: all.filter(a => a.status === "pending").length, approved: all.filter(a => a.status === "approved").length, rejected: all.filter(a => a.status === "rejected").length };
const list = all.filter(a => tab === "all" ? true : a.status === tab);
const selApp = all.find(a => a.id === sel);
return (
{list.length === 0 ?
Nothing in this view. : (
| Applicant | Property | Income | Ratio | Move-in | Status | |
{list.map(a => { const p = db.properties.find(x => x.id === a.propertyId); const t = user(a.tenantId); const ratio = (a.income / p?.price); return (
setSel(a.id)}>
|
{p?.title} |
{window.fmt.money(a.income)} |
= 2.5 ? "var(--success)" : "var(--warning)", fontWeight: 600 }}>{ratio.toFixed(1)}× |
{a.moveIn ? window.fmt.fmtShort(a.moveIn) : "—"} |
|
|
); })}
)}
{selApp && (() => {
const p = db.properties.find(x => x.id === selApp.propertyId); const t = user(selApp.tenantId); const ratio = selApp.income / p?.price;
return (
setSel(null)}
footer={selApp.status === "pending" ? <>
> : }>
{t.name}
{t.email} · {t.phone}
= 2.5 ? "ok" : "warn")} style={{ marginBottom: 16 }}>
= 2.5 ? "checkCircle" : "alert"} size={16} />
Income-to-rent ratio {ratio.toFixed(1)}× — {ratio >= 2.5 ? "meets" : "below"} the 2.5× requirement
Applicant details
{[["Property", p?.title], ["Monthly rent", window.fmt.money(p?.price)], ["Employment", selApp.employment], ["Gross income", window.fmt.money(selApp.income)], ["Occupants", selApp.occupants], ["Pets", selApp.pets], ["Move-in", selApp.moveIn ? window.fmt.fmtDate(selApp.moveIn) : "—"], ["Submitted", window.fmt.fmtDate(selApp.submittedDate)], ["Credit consent", selApp.creditConsent ? "Authorized" : "—"]].map(([k, v]) => (
{k}{v}
))}
{selApp.notes && <>Applicant note"{selApp.notes}"
>}
{selApp.status === "rejected" && selApp.reason && {selApp.reason}
}
{selApp.status === "approved" && Approved {window.fmt.fmtDate(selApp.decisionDate)} by {selApp.decisionBy}
}
);
})()}
{reject &&
setReject(null)} onConfirm={(reason) => { actions.rejectApplication(reject.id, reason); toast("Application rejected — tenant notified", "info"); setReject(null); setSel(null); }} />}
);
}
function RejectModal({ title, label, presets, onClose, onConfirm }) {
const [reason, setReason] = useState("");
return (
>}>
Quick reasons
{presets.map(pr => )}
);
}
/* ====================================================================
KYC — verify / reject landlord documents
==================================================================== */
function AdminKyc({ app }) {
const { db, actions, user, toast } = app;
const [tab, setTab] = useState("submitted");
const [sel, setSel] = useState(null);
const [reject, setReject] = useState(null);
const all = db.kyc.slice().sort((a, b) => (b.submittedDate ? new Date(b.submittedDate) : 0) - (a.submittedDate ? new Date(a.submittedDate) : 0));
const counts = { submitted: all.filter(k => k.status === "submitted").length, verified: all.filter(k => k.status === "verified").length };
const list = all.filter(k => tab === "all" ? true : tab === "other" ? !["submitted", "verified"].includes(k.status) : k.status === tab);
const selKyc = all.find(k => k.id === sel);
return (
{list.length === 0 ?
No landlords in this view. : list.map(k => {
const ll = user(k.landlordId);
return (
{ll.name}
{ll.company} · {ll.email}
{k.docs.length} document{k.docs.length !== 1 ? "s" : ""}{k.submittedDate ? ` · submitted ${window.fmt.fmtShort(k.submittedDate)}` : ""}
{k.status === "submitted" ? (
) : k.status === "not_started" ? (
Awaiting upload
) : (
)}
);
})}
{selKyc && (() => { const ll = user(selKyc.landlordId); return (
setSel(null)}
footer={selKyc.status === "submitted" ? <>
> : }>
{ll.company}
{ll.name} · {ll.email}
Submitted documents
{selKyc.docs.map(d => (
))}
{selKyc.docs.slice(0, 2).map(d =>
)}
{selKyc.status === "verified" && Verified {window.fmt.fmtDate(selKyc.decisionDate)}
}
{selKyc.status === "rejected" && selKyc.rejectReason && {selKyc.rejectReason}
}
); })()}
{reject &&
setReject(null)} onConfirm={(reason) => { actions.rejectKyc(reject.id, reason); toast("Verification rejected — landlord notified", "info"); setReject(null); setSel(null); }} />}
);
}
/* ====================================================================
RENT & COMMISSION
==================================================================== */
function AdminRent({ app }) {
const { db, user } = app;
const [tab, setTab] = useState("all");
const pays = db.payments.slice().sort((a, b) => new Date(b.date) - new Date(a.date));
const paid = pays.filter(p => p.status === "paid");
const gmv = paid.reduce((s, p) => s + p.amount, 0);
const commission = paid.reduce((s, p) => s + p.commissionAmount, 0);
const payouts = paid.reduce((s, p) => s + p.landlordPayout, 0);
const list = pays.filter(p => tab === "all" ? true : p.type === tab);
return (
Commission split
Onboarding fees are retained in full; rent splits 10% / 90%.
Korealm commission · {window.fmt.money(commission)}
Landlord payouts · {window.fmt.money(payouts)}
Ledger
{[["all", "All"], ["onboarding", "Onboarding"], ["rent", "Rent"]].map(([v, l]) => )}
| Reference | Type | Tenant | Property | Date | Amount | Commission | Payout | Status |
{list.map(p => { const prop = db.properties.find(x => x.id === p.propertyId); const t = user(p.tenantId); return (
| {p.reference} |
{p.type} |
{t?.name} |
{prop?.title} |
{window.fmt.fmtShort(p.date)} |
{window.fmt.money(p.amount)} |
{window.fmt.money(p.commissionAmount)} |
{window.fmt.money(p.landlordPayout)} |
|
); })}
);
}
/* ====================================================================
MAINTENANCE — assign vendor
==================================================================== */
function AdminMaintenance({ app }) {
const { db, actions, user, toast } = app;
const [tab, setTab] = useState("open");
const [assign, setAssign] = useState(null);
const all = db.maintenance.slice().sort((a, b) => new Date(b.createdDate) - new Date(a.createdDate));
const counts = { open: all.filter(m => m.status === "open").length, assigned: all.filter(m => m.status === "assigned").length, resolved: all.filter(m => m.status === "resolved").length };
const list = all.filter(m => tab === "all" ? true : m.status === tab);
return (
{list.length === 0 ?
No requests in this view. : list.map(m => {
const p = db.properties.find(x => x.id === m.propertyId); const t = user(m.tenantId); const v = db.vendors.find(x => x.id === m.vendorId);
return (
{m.category} · {p?.title} · {t?.name} · {window.fmt.fmtShort(m.createdDate)}
{m.description}
{v &&
{v.name} · {v.trade}
}
{m.status === "open" && }
{m.status === "assigned" && }
);
})}
{assign &&
setAssign(null)} onAssign={(vid) => { actions.assignVendor(assign.id, vid); toast("Vendor assigned — tenant & landlord notified", "ok"); setAssign(null); }} />}
);
}
function AssignVendorModal({ request, vendors, onClose, onAssign }) {
const recommended = vendors.filter(v => v.trade === request.category);
const others = vendors.filter(v => v.trade !== request.category);
const ordered = [...recommended, ...others];
const [sel, setSel] = useState(recommended[0]?.id || vendors[0]?.id);
return (
>}>
{ordered.map(v => (
))}
);
}
/* ---------------- Router ---------------- */
function AdminScreen({ view, params, app }) {
switch (view) {
case "dashboard": return ;
case "inspections": return ;
case "applications": return ;
case "kyc": return ;
case "rent": return ;
case "maintenance": return ;
default: return ;
}
}
window.KOREALM.roles.admin = { nav: adminNav, Screen: AdminScreen };