// routes/tenant/dashboard/index.tsx import { define } from "../../../utils.ts"; import { sqlGetUser } from "../../../db/sqlUserRepository.ts"; import { getUserById } from "../../../services/userService.ts"; import { getOnboardingStatus } from "../../../services/onboardingService.ts"; import { getLeasesByTenant } from "../../../services/leaseService.ts"; import { getMessagesByUser } from "../../../services/messageService.ts"; import { getMaintenanceRequestsByUser } from "../../../services/maintenanceRequestService.ts"; import { getPaymentTransactionsByUser } from "../../../services/paymentService.ts"; import { getPropertyById } from "../../../services/propertyService.ts"; import { getTenantInvitesByEmail } from "../../../services/tenantInviteService.ts"; import { page } from "fresh"; import { Lease, MaintenanceRequest, Message, PaymentTransaction, Property, TenantInvite, User, } from "../../../db/interfaces.ts"; type InviteNotice = { id: string; propertyId?: string | null; propertyLabel: string; propertyAddress: string; inviteUrl: string; }; function buildPropertyLabel(property: Property | null) { return property?.title || property?.address_1 || "Property Invite"; } function buildPropertyAddress(property: Property | null) { if (!property) return ""; return [ property.address_1, property.address_2, property.city, property.state, property.zip, ].filter(Boolean).join(", "); } async function buildInviteNotices( invites: TenantInvite[], leases: Lease[], ) { // Only hide invites for properties where the lease is final (approved/active). // Keep the invite visible while the lease is pending (until "Start Lease" → landlord approval). const finalLeasePropertyIds = new Set( (leases || []) .filter( (lease) => lease.lease_status === "active" || lease.approval_status === "approved", ) .map((lease) => lease.property_id) .filter(Boolean) as string[], ); const pendingInvites = invites.filter((invite) => { const propertyId = invite.property_id || ""; return !propertyId || !finalLeasePropertyIds.has(propertyId); }); const notices = await Promise.all(pendingInvites.map(async (invite) => { const property = invite.property_id ? await getPropertyById(invite.property_id) : null; return { id: invite.id, propertyId: invite.property_id, propertyLabel: buildPropertyLabel(property), propertyAddress: buildPropertyAddress(property), inviteUrl: `/tenant/invite/${encodeURIComponent(invite.id)}`, }; })); return notices; } export const handler = define.handlers({ async GET(ctx) { const { user_id } = ctx.state.data; const onboardingStatus = await getOnboardingStatus(user_id); if (!onboardingStatus?.onboarded_tenant) { return new Response("", { status: 303, headers: { Location: "/tenant/onboarding" }, }); } try { const user = await getUserById(user_id); const leases = await getLeasesByTenant(user_id); const messages = await getMessagesByUser(user_id); const maintenanceRequests = await getMaintenanceRequestsByUser(user_id); const recentTransactions = await getPaymentTransactionsByUser(user_id); // Invites are looked up by email; KV user may have no email if OAuth didn't provide it at login. const emailForInvites = user?.email?.trim() || (await sqlGetUser(user_id))?.email?.trim() || ""; const invites = emailForInvites ? await getTenantInvitesByEmail(emailForInvites) : []; const inviteNotices = await buildInviteNotices(invites, leases || []); // Fetch property data for the current lease const currentLease = leases?.[0]; let currentProperty = null; if (currentLease?.property_id) { currentProperty = await getPropertyById(currentLease.property_id); } return page({ user, leases, messages, maintenanceRequests, recentTransactions, inviteNotices, currentProperty, }); } catch (error) { console.error("Error loading tenant dashboard:", error); return page({ error: "Failed to load dashboard data", }); } }, }); export default define.page(function TenantDashboard({ data }) { const { user, leases, messages, maintenanceRequests, recentTransactions, inviteNotices, currentProperty, error, } = data as { user: User; leases: Lease[]; messages: Message[]; maintenanceRequests: MaintenanceRequest[]; recentTransactions: PaymentTransaction[]; inviteNotices?: InviteNotice[]; currentProperty?: Property | null; error?: string; }; if (error) { return (

Dashboard Error

{error}

); } const currentLease = leases?.[0]; // Assuming tenant has one active lease const unreadMessageCount = messages?.filter( (m) => m.recipient_id === user?.id && !m.is_read, ).length || 0; const openRequestCount = maintenanceRequests?.filter((request) => request.status === "open" || request.status === "in_progress" ).length || 0; const inviteCount = inviteNotices?.length || 0; const hasInvites = inviteCount > 0; const notificationTotal = unreadMessageCount + openRequestCount + inviteCount; return (
{/* Header */}
{/* Notifications */}

Notification Center

Your invites, updates, and action items in one place.

{notificationTotal} total notifications
{hasInvites && (

Invites

Pending property invitations

{inviteCount}
{hasInvites ? ( inviteNotices?.slice(0, 2).map((invite) => (

{invite.propertyLabel}

{invite.propertyAddress && (

{invite.propertyAddress}

)}
Review
)) ) : (

You have no pending invites.

)}

Messages

Unread landlord updates

{unreadMessageCount}

Maintenance

Open or active requests

{openRequestCount}
{/* Property Overview */} {currentLease && (

Your Rental Property

Address
{currentProperty ? buildPropertyAddress(currentProperty) || "Property Address" : currentLease.property_address || "Property Address"}
Monthly Rent
${currentProperty?.monthly_rent?.toLocaleString() || currentLease.rent?.toLocaleString() || "N/A"}
Lease End Date
{currentLease.end_date ? new Date(currentLease.end_date).toLocaleDateString() : "N/A"}
Status
{currentLease.lease_status || "active"}
)} {/* Quick Actions */}
{/* Recent Messages */}

Recent Messages

View all
{messages && messages.length > 0 ? ( messages.slice(0, 3).map((message) => (
L

Landlord

{message.subject}

{new Date(message.created_on as number) .toLocaleDateString()}

{!message.is_read && ( )) ) : (

No messages yet

)}
{/* Maintenance Requests */}

Maintenance Requests

View all
{maintenanceRequests && maintenanceRequests.length > 0 ? ( maintenanceRequests.slice(0, 3).map((request) => (

{request.request_type}

Priority: {request.priority}

{new Date(request.created_on as number) .toLocaleDateString()}

{request.status?.replace("_", " ")}
)) ) : (

No maintenance requests

Submit a request
)}
{/* Recent Payments */}

Recent Payments

View all
{recentTransactions && recentTransactions.length > 0 ? ( recentTransactions.map((transaction) => ( )) ) : ( )}
Date Description Amount Status
{new Date(transaction.created_on as number) .toLocaleDateString()} {transaction.payment_method} ${transaction.amount?.toLocaleString()} {transaction.transaction_status}
No payment history yet
); });