/* ====================================================================== 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" && }
)}

Your properties

{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) => (
{m.key}
))}
); } /* ---------------- 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
); })}
)} {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 ( }>
setD({ ...d, title: e.target.value })} error={err.title} />
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} />
Upload photosDrag & 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

{rent.map(p => { const prop = db.properties.find(x => x.id === p.propertyId); const t = user(p.tenantId); return ( ); })}
PeriodPropertyTenantGrossCommissionPayoutStatus
{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
{kyc.docs.map(d => (
{d.type}
{d.name}
Verified
))}
); if (status === "submitted") return (

Under review

Submitted {window.fmt.fmtDate(kyc.submittedDate)}. Korealm typically verifies within one business day.

{kyc.docs.map(d => (
{d.type}
{d.name}
Pending review
))}
); 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
) : ( )}
); })}
Documents are encrypted and used only 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.title}

{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
{myProps.map(p => { const pl = window.PRICING.PLAN_BY[p.plan]; return ( ); })}
PropertyPlanRentCommission
{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) + "%"}
)} {/* ---- COMPLIANCE ---- */} {tab === "compliance" && (
{["valid", "expiring", "expired", "missing"].map(s => ( {COMP_LABEL[s]} · {myComp.filter(c => c.status === s).length} ))}
{myProps.map(p => (

{p.title}

{p.address}
{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` : ""}`}
); })}
))}
)} {/* ---- 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}
))}
); })}
)} {/* ---- ORDER HISTORY ---- */} {tab === "orders" && ( myOrders.length === 0 ? setOrderFor({})}>Order a service}>Add-on services you order will appear here with their VAT breakdown. : {myOrders.map(o => { const p = db.properties.find(x => x.id === o.propertyId); return ( ); })}
ReferenceServicePropertyDateNetVATTotalStatus
{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 ( }>
{PLANS.map(pl => ( ))}
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 ( }>
{s.name}
{s.desc}
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 };