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 */}
+
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,
+ };
+}