/* ======================================================================
KOREALM — Landlord workspace
Dashboard · Properties · KYC verification · Rent & payouts · Maintenance
====================================================================== */
function landlordNav(db, me) {
const kyc = db.kyc.find(k => k.landlordId === me.id);
const myProps = db.properties.filter(p => p.landlordId === me.id);
const myPropIds = myProps.map(p => p.id);
const openMaint = db.maintenance.filter(m => myPropIds.includes(m.propertyId) && m.status !== "resolved");
const compIssues = (db.compliance || []).filter(c => myPropIds.includes(c.propertyId) && c.status !== "valid").length;
const kycBadge = kyc && kyc.status === "verified" ? 0 : 1;
return [
{ view: "dashboard", label: "Dashboard", icon: "home" },
{ view: "properties", label: "My properties", icon: "building" },
{ view: "services", label: "Plans & services", icon: "layers", count: compIssues },
{ view: "rent", label: "Rent & payouts", icon: "wallet" },
{ view: "kyc", label: "Verification", icon: "shield", count: kycBadge },
{ view: "maintenance", label: "Maintenance", icon: "wrench", count: openMaint.length },
];
}
/* ---------------- Dashboard ---------------- */
function LandlordDashboard({ app }) {
const { db, session, go, user } = app;
const me = user(session.landlordId);
const myProps = db.properties.filter(p => p.landlordId === me.id);
const myIds = myProps.map(p => p.id);
const kyc = db.kyc.find(k => k.landlordId === me.id);
const verified = kyc?.status === "verified";
const rentPaid = db.payments.filter(p => myIds.includes(p.propertyId) && p.type === "rent" && p.status === "paid");
const payoutTotal = rentPaid.reduce((s, p) => s + p.landlordPayout, 0);
const leased = myProps.filter(p => p.status === "leased").length;
const occupancy = myProps.length ? Math.round((leased / myProps.length) * 100) : 0;
const openMaint = db.maintenance.filter(m => myIds.includes(m.propertyId) && m.status !== "resolved");
const apps = db.applications.filter(a => myIds.includes(a.propertyId) && a.status === "pending");
return (
go("properties")}>List a property} />
{!verified && (
{kyc?.status === "submitted" ? "Verification under review" : "Verify your account to receive payouts"}
{kyc?.status === "submitted" ? "Korealm is reviewing your documents — usually within 1 business day." : "Upload your ID and proof of ownership to start collecting rent through Korealm."}
{kyc?.status !== "submitted" &&
go("kyc")}>Complete verification }
)}
Your properties go("properties")}>Manage
{myProps.length === 0 ? List your first home to get started. : (
{myProps.slice(0, 4).map(p => (
{p.title}
{window.fmt.money(p.price)}/mo · {p.neighborhood}
))}
)}
Rent received Last 6 mo
{apps.length > 0 && (
Applications on your homes {apps.length} pending
Korealm screens and decides on applications. You'll be notified once a tenant is approved.
{apps.map(a => { const p = db.properties.find(x => x.id === a.propertyId); const t = user(a.tenantId); return (
{t.name}
{p?.title} · {(a.income / p?.price).toFixed(1)}× income
); })}
)}
);
}
function RentChart({ payments }) {
// group last 6 months
const months = [];
const base = window.fmt.today();
for (let i = 5; i >= 0; i--) { const d = new Date(base); d.setMonth(d.getMonth() - i); months.push({ key: d.toLocaleDateString("en-US", { month: "short" }), m: d.getMonth(), y: d.getFullYear(), total: 0 }); }
payments.forEach(p => { const d = new Date(p.date); const mm = months.find(x => x.m === d.getMonth() && x.y === d.getFullYear()); if (mm) mm.total += p.landlordPayout; });
// seed some history so the chart reads well even with sparse data
const seedVals = [0, 0, 1305, 2601, 3906, 0];
months.forEach((mm, i) => { if (mm.total === 0 && seedVals[i]) mm.total = seedVals[i]; });
const max = Math.max(...months.map(m => m.total), 1);
return (
{months.map((m, i) => (
))}
);
}
/* ---------------- Properties ---------------- */
function LandlordProperties({ app }) {
const { db, session, user, toast } = app;
const me = user(session.landlordId);
const myProps = db.properties.filter(p => p.landlordId === me.id);
const [add, setAdd] = useState(false);
return (
setAdd(true)}>List a property} />
{myProps.length === 0 ? setAdd(true)}>List a property}>Add your first home to start receiving inspections and applications. : (
{myProps.map(p => {
const apps = db.applications.filter(a => a.propertyId === p.id);
const insp = db.inspections.filter(i => i.propertyId === p.id);
return (
{p.title}
{p.address}
{window.fmt.money(p.price)} pcm
{p.beds === 0 ? "Studio" : p.beds + " bd"}
{insp.length} viewings
{apps.length} applications
{ app.setSession(s => ({ ...s, role: "public", view: "detail", params: { id: p.id } })); }}>Preview
toast("Editing is out of scope for this POC", "info")}>Edit
);
})}
)}
{add && setAdd(false)} onAdd={(data) => {
app.actions.addProperty({ landlordId: me.id, ...data });
toast("Property listed — pending review by Korealm", "ok");
setAdd(false);
}} />}
);
}
function AddPropertyModal({ landlordId, onClose, onAdd }) {
const [d, setD] = useState({ title: "", type: "Flat", address: "", neighborhood: window.NEIGHBORHOODS[0], price: "", beds: "2", baths: "1", sqft: "" });
const [err, setErr] = useState({});
const submit = () => { const e = {}; if (!d.title.trim()) e.title = "Add a title"; if (!d.address.trim()) e.address = "Add an address"; if (!d.price || +d.price <= 0) e.price = "Enter monthly rent"; if (!d.sqft) e.sqft = "Enter size"; setErr(e); if (Object.keys(e).length) return; onAdd(d); };
return (
Cancel Submit listing >}>
setD({ ...d, title: e.target.value })} error={err.title} />
setD({ ...d, type: e.target.value })}>{["Flat", "Loft", "Studio", "Townhouse", "House", "Maisonette"].map(t => {t} )}
setD({ ...d, neighborhood: e.target.value })}>{window.NEIGHBORHOODS.map(n => {n} )}
setD({ ...d, address: e.target.value })} error={err.address} />
setD({ ...d, price: e.target.value.replace(/[^\d]/g, "") })} error={err.price} />
setD({ ...d, sqft: e.target.value.replace(/[^\d]/g, "") })} error={err.sqft} />
setD({ ...d, beds: e.target.value })}>{["0", "1", "2", "3", "4"].map(n => {n === "0" ? "Studio" : n} )}
setD({ ...d, baths: e.target.value })}>{["1", "2", "3"].map(n => {n} )}
Upload photos Drag & drop or click (optional for POC)
);
}
/* ---------------- Rent & payouts ---------------- */
function LandlordRent({ app }) {
const { db, session, user } = app;
const me = user(session.landlordId);
const myIds = db.properties.filter(p => p.landlordId === me.id).map(p => p.id);
const rent = db.payments.filter(p => myIds.includes(p.propertyId) && p.type === "rent").sort((a, b) => new Date(b.date) - new Date(a.date));
const paid = rent.filter(p => p.status === "paid");
const gross = paid.reduce((s, p) => s + p.amount, 0);
const commission = paid.reduce((s, p) => s + p.commissionAmount, 0);
const payout = paid.reduce((s, p) => s + p.landlordPayout, 0);
const pct = gross ? Math.round((payout / gross) * 100) : 0;
return (
Payout breakdown
How collected rent splits between you and Korealm.
Your payout · {window.fmt.money(payout)}
Commission · {window.fmt.money(commission)}
Payout history
Period Property Tenant Gross Commission Payout Status
{rent.map(p => { const prop = db.properties.find(x => x.id === p.propertyId); const t = user(p.tenantId); return (
{p.period || window.fmt.fmtShort(p.date)}
{prop?.title}
{t?.name}
{window.fmt.money(p.amount)}
−{window.fmt.money(p.commissionAmount)}
{window.fmt.money(p.landlordPayout)}
); })}
);
}
/* ---------------- KYC verification ---------------- */
const KYC_DOCS = [
{ type: "Government ID", icon: "user", hint: "Passport or driver's license" },
{ type: "Proof of Ownership", icon: "doc", hint: "Title deed or property tax bill" },
{ type: "Bank Account", icon: "wallet", hint: "Void cheque or bank letter for payouts" },
];
function LandlordKyc({ app }) {
const { db, session, user, actions, toast } = app;
const me = user(session.landlordId);
const kyc = db.kyc.find(k => k.landlordId === me.id);
const status = kyc?.status || "not_started";
const [uploads, setUploads] = useState(() => {
const init = {};
if (kyc?.docs?.length) kyc.docs.forEach(d => init[d.type] = { name: d.name, status: d.status });
return init;
});
const fileFor = (type) => {
const names = { "Government ID": "passport_scan.pdf", "Proof of Ownership": "title_deed.pdf", "Bank Account": "void_cheque.pdf" };
return names[type];
};
const upload = (type) => setUploads(u => ({ ...u, [type]: { name: fileFor(type), status: "pending" } }));
const remove = (type) => setUploads(u => { const n = { ...u }; delete n[type]; return n; });
const allUploaded = KYC_DOCS.every(d => uploads[d.type]);
const submit = () => {
const docs = KYC_DOCS.map(d => ({ type: d.type, name: uploads[d.type].name, status: "pending" }));
actions.submitKyc(me.id, docs);
toast("Documents submitted for verification", "ok");
};
if (status === "verified") return (
You're verified Verified on {window.fmt.fmtDate(kyc.decisionDate)} · You can list properties and receive payouts.
Verified
);
if (status === "submitted") return (
Under review Submitted {window.fmt.fmtDate(kyc.submittedDate)}. Korealm typically verifies within one business day.
);
return (
{status === "rejected" && kyc?.rejectReason && (
Previous submission needs attention: {kyc.rejectReason}
)}
{KYC_DOCS.map(d => {
const up = uploads[d.type];
return (
{d.type}
{up ? up.name : d.hint}
{up ? (
Uploaded remove(d.type)} />
) : (
upload(d.type)}>Upload
)}
);
})}
Documents are encrypted and used only for verification.
Submit for verification
);
}
/* ---------------- Maintenance (landlord view) ---------------- */
function LandlordMaintenance({ app }) {
const { db, session, user } = app;
const me = user(session.landlordId);
const myIds = db.properties.filter(p => p.landlordId === me.id).map(p => p.id);
const reqs = db.maintenance.filter(m => myIds.includes(m.propertyId)).sort((a, b) => new Date(b.createdDate) - new Date(a.createdDate));
return (
{reqs.length === 0 ?
Maintenance requests on your homes will appear here. : (
{reqs.map(m => { const p = db.properties.find(x => x.id === m.propertyId); const v = db.vendors.find(x => x.id === m.vendorId); const t = user(m.tenantId); return (
{m.category} · {p?.title} · reported by {t?.name.split(" ")[0]} · {window.fmt.fmtShort(m.createdDate)}
{m.description}
{v && Korealm assigned {v.name} · {v.trade}
}
); })}
)}
);
}
/* ---------------- Plans & Services ---------------- */
const COMP_TONE = { valid: "success", expiring: "warning", expired: "danger", missing: "neutral" };
const COMP_LABEL = { valid: "Valid", expiring: "Expiring", expired: "Expired", missing: "Not on file" };
const daysUntil = (iso) => iso ? Math.round((new Date(iso) - window.fmt.today()) / 86400000) : null;
function PlanPill({ planId }) {
const p = window.PRICING.PLAN_BY[planId] || window.PRICING.PLAN_BY.full;
return {p.name} ;
}
function LandlordServices({ app }) {
const { db, session, user, actions, toast } = app;
const { PLANS, SERVICES, VAT_RATE, vatOf } = window.PRICING;
const me = user(session.landlordId);
const myProps = db.properties.filter(p => p.landlordId === me.id);
const myIds = myProps.map(p => p.id);
const myComp = (db.compliance || []).filter(c => myIds.includes(c.propertyId));
const myOrders = (db.serviceOrders || []).filter(o => myIds.includes(o.propertyId)).sort((a, b) => new Date(b.orderedDate) - new Date(a.orderedDate));
const issues = myComp.filter(c => c.status !== "valid").length;
const [tab, setTab] = useState("plans");
const [changePlan, setChangePlan] = useState(null); // property
const [orderFor, setOrderFor] = useState(null); // { service } or { property }
const compFor = (pid, type) => myComp.find(c => c.propertyId === pid && c.type === type);
return (
setOrderFor({})}>Order a service} />
Prices are Korealm's set rates and exclude VAT . 20% VAT is added at checkout. Typical UK market ranges are shown for context.
{/* ---- PLANS ---- */}
{tab === "plans" && (
{PLANS.map(pl => (
{pl.range}
{pl.name}
{pl.tagline}
{pl.oneOff != null
? <>{window.fmt.money(pl.oneOff)} +VAT · one-off >
: <>{Math.round(pl.rate * 100)}% of monthly rent >}
{pl.includes.map(it =>
{it}
)}
{myProps.filter(p => p.plan === pl.id).length} of your properties on this plan
))}
Your properties Commission is derived from each property's plan
Property Plan Rent Commission
{myProps.map(p => { const pl = window.PRICING.PLAN_BY[p.plan]; return (
{p.title}
{p.neighborhood}
{window.fmt.money(p.price)} pcm
{pl.oneOff != null ? window.fmt.money(pl.oneOff) + " one-off" : Math.round(p.commissionRate * 100) + "%"}
setChangePlan(p)}>Change
); })}
)}
{/* ---- COMPLIANCE ---- */}
{tab === "compliance" && (
{["valid", "expiring", "expired", "missing"].map(s => (
{COMP_LABEL[s]} · {myComp.filter(c => c.status === s).length}
))}
{myProps.map(p => (
{window.PRICING.COMPLIANCE_TYPES.map(type => {
const s = window.PRICING.SVC[type]; const rec = compFor(p.id, type);
const status = rec?.status || "missing"; const dleft = daysUntil(rec?.expires);
return (
{COMP_LABEL[status]}
{type.toUpperCase()}
{status === "missing" ? "No certificate on file"
: status === "expired" ? `Expired ${window.fmt.fmtShort(rec.expires)}`
: `Valid until ${window.fmt.fmtShort(rec.expires)}${dleft != null && dleft < 120 ? ` · ${dleft}d` : ""}`}
setOrderFor({ property: p, service: s })}>
{status === "valid" ? "Renew early" : status === "missing" ? "Order" : "Renew"} · {window.fmt.money(s.price)}+VAT
);
})}
))}
)}
{/* ---- ADD-ONS ---- */}
{tab === "addons" && (
{["Letting", "Tenancy", "Management", "Compliance"].map(cat => {
const items = SERVICES.filter(s => s.cat === cat);
if (!items.length) return null;
return (
{cat}
{items.map(s => (
{s.name}
{s.desc}
{window.fmt.money(s.price)}+VAT
Typical UK: {window.fmt.money(s.low)}–{window.fmt.money(s.high)} · {s.unit}
setOrderFor({ service: s })}>Order
))}
);
})}
)}
{/* ---- ORDER HISTORY ---- */}
{tab === "orders" && (
myOrders.length === 0
? setOrderFor({})}>Order a service}>Add-on services you order will appear here with their VAT breakdown.
:
Reference Service Property Date Net VAT Total Status
{myOrders.map(o => { const p = db.properties.find(x => x.id === o.propertyId); return (
{o.reference}
{o.name}
{p?.title}
{window.fmt.fmtShort(o.orderedDate)}
{window.fmt.money(o.amount)}
{window.fmt.money(o.vat)}
{window.fmt.money(o.total)}
); })}
)}
{changePlan && setChangePlan(null)}
onSave={(planId) => { actions.setPropertyPlan(changePlan.id, planId); toast(`${changePlan.title} moved to ${window.PRICING.PLAN_BY[planId].name}`, "ok"); setChangePlan(null); }} />}
{orderFor && setOrderFor(null)}
onOrder={({ propertyId, serviceId }) => { actions.orderService({ propertyId, serviceId, landlordId: me.id }); const s = window.PRICING.SVC[serviceId]; toast(`${s.name} ordered — ${window.fmt.money(window.PRICING.withVat(s.price))} inc. VAT`, "ok"); setOrderFor(null); }} />}
);
}
function ChangePlanModal({ property, onClose, onSave }) {
const { PLANS } = window.PRICING;
const [sel, setSel] = useState(property.plan);
return (
Cancel onSave(sel)}>Move to this plan >}>
{PLANS.map(pl => (
setSel(pl.id)}>
{pl.name} {pl.oneOff != null ? window.fmt.money(pl.oneOff) + "+VAT" : Math.round(pl.rate * 100) + "%"}
{pl.tagline} · typical {pl.range}
))}
Changing plan updates the commission applied to future rent collected on this property.
);
}
function OrderServiceModal({ myProps, preset, onClose, onOrder }) {
const { SERVICES, vatOf } = window.PRICING;
const [propertyId, setPropertyId] = useState(preset.property?.id || myProps[0]?.id || "");
const [serviceId, setServiceId] = useState(preset.service?.id || SERVICES[0].id);
const s = window.PRICING.SVC[serviceId];
const vat = vatOf(s.price);
return (
Cancel onOrder({ propertyId, serviceId })}>Confirm order · {window.fmt.money(s.price + vat)} >}>
setPropertyId(e.target.value)}>{myProps.map(p => {p.title} )}
setServiceId(e.target.value)}>{SERVICES.map(x => {x.name} — £{x.price}+VAT )}
Korealm price ({s.unit}) {window.fmt.money(s.price)}
VAT (20%) {window.fmt.money(vat)}
Total {window.fmt.money(s.price + vat)}
Typical UK range: {window.fmt.money(s.low)}–{window.fmt.money(s.high)} ({s.unit})
);
}
/* ---------------- Router ---------------- */
function LandlordScreen({ view, params, app }) {
switch (view) {
case "dashboard": return ;
case "properties": return ;
case "services": return ;
case "rent": return ;
case "kyc": return ;
case "maintenance": return ;
default: return ;
}
}
window.KOREALM.roles.landlord = { nav: landlordNav, Screen: LandlordScreen };