open-swe-dev-andrinoid[bot] 7edefd59d1 Apply patch [skip ci]
2026-01-22 16:39:10 +00:00

547 lines
21 KiB
TypeScript

// routes/landlord/dashboard/index.tsx
import { define } from "../../../utils.ts";
import { getLandlordDashboardData } from "../../../services/landlordService.ts";
import { DashboardTenantApplication } from "../../../services/tenantApplicationService.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";
// 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
tenantApplications?: DashboardTenantApplication[];
maintenanceRequests?: DashboardMaintenanceRequest[];
recentMessages?: DashboardMessage[];
monthlyRevenue?: 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;
if (!user_id) {
return new Response("", {
status: 302,
headers: { Location: "/auth/login" },
});
}
try {
const dashboardData = await getLandlordDashboardData(user_id);
return page({
stateData,
dashboardData,
});
} 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, dashboardData, error } = data;
if (error) {
return (
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
<div class="text-center">
<h1 class="text-2xl font-bold text-gray-900 mb-4">
Dashboard Error
</h1>
<p class="text-gray-600">{error}</p>
<a
href="/onboarding#landlord"
class="mt-4 inline-block text-blue-600 hover:text-blue-500"
>
Return to Onboarding
</a>
</div>
</div>
);
}
const {
properties,
tenantApplications,
maintenanceRequests,
recentMessages,
monthlyRevenue,
} = dashboardData || {};
return (
<div class="min-h-screen bg-gray-50">
{/* Header */}
<div class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-6">
<div>
<h1 class="text-3xl font-bold text-gray-900">
Landlord Dashboard
</h1>
<p class="mt-1 text-sm text-gray-500">
Welcome back! Here's what's happening with your properties.
</p>
</div>
<div class="flex space-x-3">
<a
href="/landlord/properties/add"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Add Property
</a>
<a
href="/landlord/tenants/invite"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Invite Tenant
</a>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Stats Overview */}
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 mb-8">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg
class="h-6 w-6 text-gray-400"
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-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
Total Properties
</dt>
<dd class="text-lg font-medium text-gray-900">
{properties?.length || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg
class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
Pending Applications
</dt>
<dd class="text-lg font-medium text-gray-900">
{tenantApplications?.filter((app) =>
app.status === "pending"
).length || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg
class="h-6 w-6 text-gray-400"
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="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
Open Maintenance
</dt>
<dd class="text-lg font-medium text-gray-900">
{maintenanceRequests?.filter((req) =>
req.status === "open"
).length || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg
class="h-6 w-6 text-gray-400"
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="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
Monthly Revenue
</dt>
<dd class="text-lg font-medium text-gray-900">
${monthlyRevenue?.toLocaleString() || "0"}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
{/* Recent Properties */}
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Your Properties
</h3>
<a
href="/landlord/properties"
class="text-sm text-blue-600 hover:text-blue-500"
>
View all
</a>
</div>
<div class="space-y-4">
{properties && properties.length > 0
? (
properties.slice(0, 3).map((property) => (
<div
key={property.id}
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div>
<p class="text-sm font-medium text-gray-900">
{property.address_1}
</p>
<p class="text-sm text-gray-500">
{property.city}, {property.state}
</p>
<p class="text-xs text-gray-400 capitalize">
{property.rental_status}
</p>
</div>
<div class="text-right">
<p class="text-sm font-medium text-gray-900">
${property.monthly_rent}/mo
</p>
<p class="text-xs text-gray-500">
{property.bedrooms}bd/{property.bathrooms}ba
</p>
</div>
</div>
))
)
: (
<div class="text-center py-4">
<p class="text-sm text-gray-500">No properties yet</p>
<a
href="/landlord/properties/add"
class="text-sm text-blue-600 hover:text-blue-500"
>
Add your first property
</a>
</div>
)}
</div>
</div>
</div>
{/* Recent Messages */}
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Recent Messages
</h3>
<a
href="/landlord/messages"
class="text-sm text-blue-600 hover:text-blue-500"
>
View all
</a>
</div>
<div class="space-y-4">
{recentMessages && recentMessages.length > 0
? (
recentMessages.slice(0, 3).map((message) => (
<div
key={message.id}
class="flex items-start space-x-3"
>
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center">
<span class="text-xs font-medium text-gray-700">
{message.sender_name?.charAt(0) || "T"}
</span>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900">
{message.sender_name || "Tenant"}
</p>
<p class="text-sm text-gray-500 truncate">
{message.subject}
</p>
<p class="text-xs text-gray-400">
{MakeNewDate(
(message as DashboardMessage)
.created_on as number,
)
.toLocaleDateString()}
</p>
</div>
{!message.is_read && (
<div class="flex-shrink-0">
<div class="w-2 h-2 bg-blue-600 rounded-full">
</div>
</div>
)}
</div>
))
)
: (
<div class="text-center py-4">
<p class="text-sm text-gray-500">No messages yet</p>
</div>
)}
</div>
</div>
</div>
{/* Pending Applications */}
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Pending Applications
</h3>
<a
href="/landlord/applications"
class="text-sm text-blue-600 hover:text-blue-500"
>
View all
</a>
</div>
<div class="space-y-4">
{tenantApplications &&
tenantApplications.filter((app) =>
app.status === "pending"
).length > 0
? (
tenantApplications.filter((app) =>
app.status === "pending"
).slice(0, 3).map((application) => (
<div
key={application.id}
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
>
<div>
<p class="text-sm font-medium text-gray-900">
{application.applicant_name}
</p>
<p class="text-sm text-gray-500">
{application.applicant_email}
</p>
<p class="text-xs text-gray-400">
Applied {application.submitted_on
? new Date(application.submitted_on)
.toLocaleDateString()
: "Unknown date"}
</p>
</div>
<div class="flex space-x-2">
<button class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
Approve
</button>
<button class="text-xs bg-red-100 text-red-800 px-2 py-1 rounded">
Reject
</button>
</div>
</div>
))
)
: (
<div class="text-center py-4">
<p class="text-sm text-gray-500">
No pending applications
</p>
</div>
)}
</div>
</div>
</div>
{/* Maintenance Requests */}
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Maintenance Requests
</h3>
<a
href="/landlord/maintenance"
class="text-sm text-blue-600 hover:text-blue-500"
>
View all
</a>
</div>
<div class="space-y-4">
{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="flex items-center justify-between p-3 bg-red-50 rounded-lg"
>
<div>
<p class="text-sm font-medium text-gray-900">
{request.description}
</p>
<p class="text-sm text-gray-500 capitalize">
Priority: {request.priority}
</p>
<p class="text-xs text-gray-400">
{request.created_on
? new Date(request.created_on)
.toLocaleDateString()
: "Unknown date"}
</p>
</div>
<div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
{request.status}
</span>
</div>
</div>
))
)
: (
<div class="text-center py-4">
<p class="text-sm text-gray-500">
No open maintenance requests
</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
},
);