475 lines
18 KiB
TypeScript

// routes/landlord/dashboard/index.tsx
import { define } from "../../../utils.ts";
import {
getLandlordDashboardData,
getPendingLeaseApprovals,
} from "../../../services/landlordService.ts";
import { getUnreadCount } from "../../../services/messageService.ts";
import { DashboardMaintenanceRequest } from "../../../services/maintenanceRequestService.ts";
import { DashboardMessage } from "../../../services/messageService.ts";
import { page } from "fresh";
import { SessionCookie } from "../../auth/callback.ts";
import { MakeNewDate } from "../../../helpers.ts";
import { Lease, Property, User } from "../../../db/interfaces.ts";
import LandlordPendingApprovals from "../../../islands/LandlordPendingApprovals.tsx";
// Interface for pending lease approval with nested property and tenant data
interface PendingLeaseApproval extends Lease {
property?: Property | null;
tenant?: Pick<User, "id" | "name" | "email" | "phone"> | null;
}
// Flexible interface for dashboard data - allows any structure
interface FlexibleDashboardData {
// Core dashboard stats from LandlordDashboardData
properties?: {
total: number;
id: string;
address_1: string;
city: string;
state: string;
rental_status: string;
monthly_rent: number;
bedrooms: number;
bathrooms: number;
available: number;
occupied: number;
draft: number;
}[];
leases?: {
active: number;
draft: number;
expired: number;
}[];
applications?: {
pending: number;
total_this_month: number;
title: string;
}[];
payments?: {
expected_this_month: number;
received_this_month: number;
overdue: number;
}[];
// Additional UI properties with proper types from service layer
maintenanceRequests?: DashboardMaintenanceRequest[];
recentMessages?: DashboardMessage[];
monthlyRevenue?: number;
pendingLeaseApprovals?: PendingLeaseApproval[];
unreadMessageCount?: number;
[key: string]: unknown; // Allow any additional properties
}
// Flexible interface for component data
interface LandlordDashboardPageData {
stateData?: SessionCookie;
dashboardData?: FlexibleDashboardData;
error?: string;
[key: string]: unknown; // Allow additional properties with unknown type
}
interface LandlordDashboardProps {
data: LandlordDashboardPageData;
}
export const handler = define.handlers({
async GET(ctx) {
const stateData: SessionCookie = ctx.state.data;
const { user_id } = stateData;
try {
const dashboardData = await getLandlordDashboardData(user_id);
const pendingApprovals = await getPendingLeaseApprovals(user_id);
const unreadMessageCount = await getUnreadCount(user_id);
return page({
stateData,
dashboardData: {
...dashboardData,
pendingLeaseApprovals: pendingApprovals,
unreadMessageCount,
},
});
} catch (error) {
console.error("Error loading landlord dashboard:", error);
return page({
stateData,
error: "Failed to load dashboard data",
});
}
},
});
export default define.page(
function LandlordDashboard({ data }: LandlordDashboardProps) {
const { stateData: _stateData, dashboardData, error } = data;
if (error) {
return (
<div class="dashboard-error-page landlord-route">
<div class="dashboard-error-inner">
<h1 class="dashboard-error-title">Dashboard Error</h1>
<p class="dashboard-error-text">{error}</p>
<a href="/onboarding#landlord" class="dashboard-error-link">
Return to Onboarding
</a>
</div>
</div>
);
}
const {
properties,
maintenanceRequests,
recentMessages,
monthlyRevenue,
pendingLeaseApprovals,
unreadMessageCount = 0,
} = dashboardData || {};
return (
<div class="dashboard-page landlord-route">
{/* Header */}
<div class="dashboard-header">
<div class="dashboard-header-inner">
<div class="dashboard-header-row">
<div>
<h1 class="dashboard-header-title">Landlord Dashboard</h1>
<p class="dashboard-header-subtitle">
Welcome back! Here's what's happening with your properties.
</p>
</div>
<div class="dashboard-header-actions">
<a
href="/landlord/properties/add"
class="dashboard-btn dashboard-btn--primary"
>
Add Property
</a>
<a
href="/landlord/tenants/invite"
class="dashboard-btn dashboard-btn--secondary"
>
Invite Tenant
</a>
<a
href="/landlord/messages"
class={`dashboard-btn ${
unreadMessageCount > 0
? "dashboard-btn--messages-new"
: "dashboard-btn--secondary"
}`}
>
Messages
</a>
</div>
</div>
</div>
</div>
<div class="dashboard-content">
{/* Stats Overview */}
<div class="dashboard-stats-grid">
<div class="dashboard-card">
<div class="dashboard-card-body">
<div class="dashboard-stat-card">
<div class="dashboard-stat-icon-wrap">
<svg
class="dashboard-stat-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
</div>
<div class="dashboard-stat-content">
<dl>
<dt class="dashboard-stat-label">Total Properties</dt>
<dd class="dashboard-stat-value">
{properties?.length || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="dashboard-card">
<div class="dashboard-card-body">
<div class="dashboard-stat-card">
<div class="dashboard-stat-icon-wrap">
<svg
class="dashboard-stat-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div class="dashboard-stat-content">
<dl>
<dt class="dashboard-stat-label">Pending Approvals</dt>
<dd class="dashboard-stat-value">
{pendingLeaseApprovals?.length || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="dashboard-card">
<div class="dashboard-card-body">
<div class="dashboard-stat-card">
<div class="dashboard-stat-icon-wrap">
<svg
class="dashboard-stat-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div class="dashboard-stat-content">
<dl>
<dt class="dashboard-stat-label">Open Maintenance</dt>
<dd class="dashboard-stat-value">
{maintenanceRequests?.filter((req) =>
req.status === "open"
).length || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="dashboard-card">
<div class="dashboard-card-body">
<div class="dashboard-stat-card">
<div class="dashboard-stat-icon-wrap">
<svg
class="dashboard-stat-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div class="dashboard-stat-content">
<dl>
<dt class="dashboard-stat-label">Monthly Revenue</dt>
<dd class="dashboard-stat-value">
${monthlyRevenue?.toLocaleString() || "0"}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div class="dashboard-two-col">
{/* Recent Properties */}
<div class="dashboard-card dashboard-card--no-overflow">
<div class="dashboard-card-body">
<div class="dashboard-section-header">
<h3 class="dashboard-section-title">Your Properties</h3>
<a href="/landlord/properties" class="dashboard-section-link">
View all
</a>
</div>
<div class="dashboard-list-spaced">
{properties && properties.length > 0
? (
properties.slice(0, 3).map((property) => (
<div key={property.id} class="dashboard-row">
<div>
<p class="dashboard-row-label">
{property.address_1}
</p>
<p class="dashboard-row-muted">
{property.city}, {property.state}
</p>
<p class="dashboard-row-meta capitalize">
{property.rental_status}
</p>
</div>
<div style="text-align: right;">
<p class="dashboard-row-value">
${property.monthly_rent}/mo
</p>
<p class="dashboard-row-sub">
{property.bedrooms}bd/{property.bathrooms}ba
</p>
<a
href={`/landlord/properties/${property.id}`}
class="dashboard-btn-link"
>
Details
</a>
</div>
</div>
))
)
: (
<div class="dashboard-empty">
<p class="dashboard-empty-text">No properties yet</p>
<a
href="/landlord/properties/add"
class="dashboard-empty-link"
>
Add your first property
</a>
</div>
)}
</div>
</div>
</div>
{/* Recent Messages */}
<div class="dashboard-card dashboard-card--no-overflow">
<div class="dashboard-card-body">
<div class="dashboard-section-header">
<h3 class="dashboard-section-title">Recent Messages</h3>
<a href="/landlord/messages" class="dashboard-section-link">
View all
</a>
</div>
<div class="dashboard-list-spaced">
{recentMessages && recentMessages.length > 0
? (
recentMessages.slice(0, 3).map((message) => (
<div key={message.id} class="dashboard-message-row">
<div class="dashboard-message-avatar">
<span class="dashboard-message-avatar-text">
{message.sender_name?.charAt(0) || "T"}
</span>
</div>
<div class="dashboard-message-body">
<p class="dashboard-message-from">
{message.sender_name || "Tenant"}
</p>
<p class="dashboard-message-preview">
{message.subject}
</p>
<p class="dashboard-message-meta">
{MakeNewDate(
(message as DashboardMessage)
.created_on as number,
)
.toLocaleDateString()}
</p>
</div>
{!message.is_read && (
<div
class="dashboard-message-dot"
aria-hidden="true"
>
</div>
)}
</div>
))
)
: (
<div class="dashboard-empty">
<p class="dashboard-empty-text">No messages yet</p>
</div>
)}
</div>
</div>
</div>
{/* Pending Lease Approvals */}
<LandlordPendingApprovals
approvals={pendingLeaseApprovals ?? []}
/>
{/* Maintenance Requests */}
<div class="dashboard-card dashboard-card--no-overflow">
<div class="dashboard-card-body">
<div class="dashboard-section-header">
<h3 class="dashboard-section-title">
Maintenance Requests
</h3>
<a
href="/landlord/maintenance"
class="dashboard-section-link"
>
View all
</a>
</div>
<div class="dashboard-list-spaced">
{maintenanceRequests &&
maintenanceRequests.filter((req) => req.status === "open")
.length > 0
? (
maintenanceRequests.filter((req) => req.status === "open")
.slice(0, 3).map((request) => (
<div
key={request.id}
class="dashboard-row dashboard-row--red"
>
<div>
<p class="dashboard-row-label">
{request.description}
</p>
<p class="dashboard-row-muted capitalize">
Priority: {request.priority}
</p>
<p class="dashboard-row-meta">
{request.created_on
? new Date(request.created_on)
.toLocaleDateString()
: "Unknown date"}
</p>
</div>
<div>
<span class="dashboard-badge dashboard-badge--red-pill">
{request.status}
</span>
</div>
</div>
))
)
: (
<div class="dashboard-empty">
<p class="dashboard-empty-text">
No open maintenance requests
</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
},
);