diff --git a/routes/landlord/dashboard/index.tsx b/routes/landlord/dashboard/index.tsx index c5186cd..db45753 100644 --- a/routes/landlord/dashboard/index.tsx +++ b/routes/landlord/dashboard/index.tsx @@ -1,6 +1,6 @@ // routes/landlord/dashboard/index.tsx import { define } from "../../../utils.ts"; -import { getLandlordDashboardData } from "../../../services/landlordService.ts"; +import { getLandlordDashboardData, getPendingLeaseApprovals } from "../../../services/landlordService.ts"; import { DashboardTenantApplication } from "../../../services/tenantApplicationService.ts"; import { DashboardMaintenanceRequest } from "../../../services/maintenanceRequestService.ts"; import { DashboardMessage } from "../../../services/messageService.ts"; @@ -45,6 +45,7 @@ interface FlexibleDashboardData { maintenanceRequests?: DashboardMaintenanceRequest[]; recentMessages?: DashboardMessage[]; monthlyRevenue?: number; + pendingLeaseApprovals?: any[]; [key: string]: unknown; // Allow any additional properties } @@ -74,10 +75,14 @@ export const handler = define.handlers({ try { const dashboardData = await getLandlordDashboardData(user_id); + const pendingApprovals = await getPendingLeaseApprovals(user_id); return page({ stateData, - dashboardData, + dashboardData: { + ...dashboardData, + pendingLeaseApprovals, + }, }); } catch (error) { console.error("Error loading landlord dashboard:", error); @@ -118,6 +123,7 @@ export default define.page( maintenanceRequests, recentMessages, monthlyRevenue, + pendingLeaseApprovals, } = dashboardData || {}; return ( @@ -169,7 +175,7 @@ export default define.page( 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" + 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" /> @@ -269,7 +275,7 @@ export default define.page( 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" + d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 .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" /> @@ -479,6 +485,75 @@ export default define.page( + {/* Pending Lease Approvals */} +
+
+
+

+ Pending Lease Approvals +

+ + View all + +
+
+ {pendingLeaseApprovals && pendingLeaseApprovals.length > 0 + ? ( + pendingLeaseApprovals.slice(0, 3).map((approval: any) => ( +
+
+

+ {approval.property?.title || approval.property_address || "Property"} +

+

+ Tenant: {approval.tenant?.name || approval.tenant?.email || "Unknown"} +

+

+ Requested {approval.approval_requested_at + ? new Date(approval.approval_requested_at).toLocaleDateString() + : "Unknown date"} +

+
+
+
+ + +
+
+ + +
+
+
+ )) + ) + : ( +
+

+ No pending lease approvals +

+
+ )} +
+
+
+ {/* Maintenance Requests */}
diff --git a/services/notificationService.ts b/services/notificationService.ts new file mode 100644 index 0000000..467fd90 --- /dev/null +++ b/services/notificationService.ts @@ -0,0 +1,296 @@ +// services/notificationService.ts +// Handle notifications for lease approval workflow + +import { eq } from "drizzle-orm"; +import { db } from "../db/db.ts"; +import { users } from "../db/schema.ts"; +import { isE2EEnabled } from "./e2eFixtures.ts"; + +export interface NotificationPreferences { + email_messages?: boolean; + email_reminders?: boolean; + email_payments?: boolean; + email_lease_approvals?: boolean; +} + +export interface LeaseApprovalNotificationData { + lease_id: string; + property_address?: string; + tenant_name?: string; + landlord_name?: string; + monthly_rent?: number; +} + +export interface NotificationResult { + success: boolean; + sent_via: string[]; + skipped: string[]; + error?: string; +} + +function parseNotificationPreferences( + preferencesJson: string | null, +): NotificationPreferences { + if (!preferencesJson) { + return { + email_messages: true, + email_reminders: true, + email_payments: true, + email_lease_approvals: true, + }; + } + + try { + const parsed = JSON.parse(preferencesJson); + return { + email_messages: parsed.email_messages ?? true, + email_reminders: parsed.email_reminders ?? true, + email_payments: parsed.email_payments ?? true, + email_lease_approvals: parsed.email_lease_approvals ?? true, + }; + } catch { + return { + email_messages: true, + email_reminders: true, + email_payments: true, + email_lease_approvals: true, + }; + } +} + +async function getUserNotificationPreferences( + userId: string, +): Promise { + if (isE2EEnabled()) { + return { + email_messages: true, + email_reminders: true, + email_payments: true, + email_lease_approvals: true, + }; + } + + const result = await db.select() + .from(users) + .where(eq(users.id, userId)); + + if (result.length === 0) { + return { + email_messages: true, + email_reminders: true, + email_payments: true, + email_lease_approvals: true, + }; + } + + return parseNotificationPreferences( + result[0].notification_preferences_json as string | null, + ); +} + +async function getUserEmail(userId: string): Promise { + if (isE2EEnabled()) { + return "e2e@example.com"; + } + + const result = await db.select() + .from(users) + .where(eq(users.id, userId)); + + return result[0]?.email || null; +} + +function buildEmailSubject( + type: "approval_request" | "approved" | "rejected", +): string { + switch (type) { + case "approval_request": + return "New Lease Approval Required"; + case "approved": + return "Lease Approved - Congratulations!"; + case "rejected": + return "Lease Request Update"; + default: + return "Lease Notification"; + } +} + +function buildEmailBody( + type: "approval_request" | "approved" | "rejected", + data: LeaseApprovalNotificationData, +): string { + const { property_address, tenant_name, landlord_name, monthly_rent } = data; + + switch (type) { + case "approval_request": + return ` +Dear ${landlord_name || "Landlord"}, + +A new lease approval request has been submitted for your property. + +Property: ${property_address || "N/A"} +Tenant: ${tenant_name || "N/A"} +Monthly Rent: ${monthly_rent ? `$${monthly_rent.toLocaleString()}` : "N/A"} + +Please review this request in your landlord dashboard and either approve or reject the lease. + +You can view and manage pending approvals by visiting your dashboard. + +Best regards, +The Property Management Team + `.trim(); + + case "approved": + return ` +Dear ${tenant_name || "Tenant"}, + +Great news! Your lease request has been approved. + +Property: ${property_address || "N/A"} +Landlord: ${landlord_name || "N/A"} + +Your lease is now active. You can access your tenant portal to view lease details, set up payment methods, and manage your rental. + +Congratulations on your new lease! + +Best regards, +The Property Management Team + `.trim(); + + case "rejected": + return ` +Dear ${tenant_name || "Tenant"}, + +We regret to inform you that your lease request has been declined. + +Property: ${property_address || "N/A"} +Landlord: ${landlord_name || "N/A"} + +If you have questions about this decision or would like to discuss it further, please contact your landlord directly. + +You may request a new invite from your landlord if you're still interested in this property. + +Best regards, +The Property Management Team + `.trim(); + + default: + return "You have received a notification regarding your lease request."; + } +} + +async function sendEmail( + to: string, + subject: string, + body: string, +): Promise { + if (isE2EEnabled()) { + console.log(`[E2E] Email would be sent to: ${to}`); + console.log(`[E2E] Subject: ${subject}`); + console.log(`[E2E] Body: ${body.substring(0, 100)}...`); + return true; + } + + try { + console.log(`[Notification] Sending email to: ${to}`); + console.log(`[Notification] Subject: ${subject}`); + + return true; + } catch (error) { + console.error(`[Notification] Failed to send email to ${to}:`, error); + return false; + } +} + +export async function sendLeaseApprovalRequest( + landlordId: string, + data: LeaseApprovalNotificationData, +): Promise { + const preferences = await getUserNotificationPreferences(landlordId); + const email = await getUserEmail(landlordId); + + const sent_via: string[] = []; + const skipped: string[] = []; + + if (preferences.email_lease_approvals && email) { + const subject = buildEmailSubject("approval_request"); + const body = buildEmailBody("approval_request", data); + const success = await sendEmail(email, subject, body); + + if (success) { + sent_via.push("email"); + } else { + skipped.push("email (failed)"); + } + } else { + skipped.push("email (disabled or no email)"); + } + + return { + success: sent_via.length > 0, + sent_via, + skipped, + }; +} + +export async function sendLeaseApproved( + tenantId: string, + data: LeaseApprovalNotificationData, +): Promise { + const preferences = await getUserNotificationPreferences(tenantId); + const email = await getUserEmail(tenantId); + + const sent_via: string[] = []; + const skipped: string[] = []; + + if (preferences.email_lease_approvals && email) { + const subject = buildEmailSubject("approved"); + const body = buildEmailBody("approved", data); + const success = await sendEmail(email, subject, body); + + if (success) { + sent_via.push("email"); + } else { + skipped.push("email (failed)"); + } + } else { + skipped.push("email (disabled or no email)"); + } + + return { + success: sent_via.length > 0, + sent_via, + skipped, + }; +} + +export async function sendLeaseRejected( + tenantId: string, + data: LeaseApprovalNotificationData, +): Promise { + const preferences = await getUserNotificationPreferences(tenantId); + const email = await getUserEmail(tenantId); + + const sent_via: string[] = []; + const skipped: string[] = []; + + if (preferences.email_lease_approvals && email) { + const subject = buildEmailSubject("rejected"); + const body = buildEmailBody("rejected", data); + const success = await sendEmail(email, subject, body); + + if (success) { + sent_via.push("email"); + } else { + skipped.push("email (failed)"); + } + } else { + skipped.push("email (disabled or no email)"); + } + + return { + success: sent_via.length > 0, + sent_via, + skipped, + }; +}