diff --git a/routes/api/lease/approve.ts b/routes/api/lease/approve.ts index 410c1f2..2b7cc33 100644 --- a/routes/api/lease/approve.ts +++ b/routes/api/lease/approve.ts @@ -1,8 +1,10 @@ // routes/api/lease/approve.ts import { define } from "../../../utils.ts"; -import { page } from "fresh"; import { SessionCookie } from "../../auth/callback.ts"; import { approveLease, getLeaseById } from "../../../services/leaseService.ts"; +import { getUserById } from "../../../services/userService.ts"; +import { getPropertyById } from "../../../services/propertyService.ts"; +import { sendLeaseApproved } from "../../../services/notificationService.ts"; export const handler = define.handlers({ async POST(ctx) { @@ -78,10 +80,30 @@ export const handler = define.handlers({ ); } + const approvedLease = result[0]; + + if (lease.tenant_id) { + const tenant = await getUserById(lease.tenant_id); + const landlord = await getUserById(user_id); + const property = lease.property_id + ? await getPropertyById(lease.property_id) + : null; + + await sendLeaseApproved(lease.tenant_id, { + lease_id: lease.id, + property_address: property?.address_1 + ? `${property.address_1}, ${property.city}, ${property.state}` + : lease.property_address || "Property", + tenant_name: tenant?.name || "Tenant", + landlord_name: landlord?.name || "Landlord", + monthly_rent: property?.monthly_rent ?? lease.rent ?? undefined, + }); + } + return new Response( JSON.stringify({ success: true, - lease: result[0], + lease: approvedLease, }), { status: 200, diff --git a/routes/api/lease/reject.ts b/routes/api/lease/reject.ts index 8ffb88a..b29db0d 100644 --- a/routes/api/lease/reject.ts +++ b/routes/api/lease/reject.ts @@ -1,8 +1,10 @@ // routes/api/lease/reject.ts import { define } from "../../../utils.ts"; -import { page } from "fresh"; import { SessionCookie } from "../../auth/callback.ts"; -import { rejectLease, getLeaseById } from "../../../services/leaseService.ts"; +import { getLeaseById, rejectLease } from "../../../services/leaseService.ts"; +import { getUserById } from "../../../services/userService.ts"; +import { getPropertyById } from "../../../services/propertyService.ts"; +import { sendLeaseRejected } from "../../../services/notificationService.ts"; export const handler = define.handlers({ async POST(ctx) { @@ -80,10 +82,30 @@ export const handler = define.handlers({ ); } + const rejectedLease = result[0]; + + if (lease.tenant_id) { + const tenant = await getUserById(lease.tenant_id); + const landlord = await getUserById(user_id); + const property = lease.property_id + ? await getPropertyById(lease.property_id) + : null; + + await sendLeaseRejected(lease.tenant_id, { + lease_id: lease.id, + property_address: property?.address_1 + ? `${property.address_1}, ${property.city}, ${property.state}` + : lease.property_address || "Property", + tenant_name: tenant?.name || "Tenant", + landlord_name: landlord?.name || "Landlord", + monthly_rent: property?.monthly_rent ?? lease.rent ?? undefined, + }); + } + return new Response( JSON.stringify({ success: true, - lease: result[0], + lease: rejectedLease, }), { status: 200, diff --git a/routes/api/lease/request-approval.ts b/routes/api/lease/request-approval.ts index 41acc74..211fe83 100644 --- a/routes/api/lease/request-approval.ts +++ b/routes/api/lease/request-approval.ts @@ -1,16 +1,16 @@ // routes/api/lease/request-approval.ts import { define } from "../../../utils.ts"; -import { page } from "fresh"; import { SessionCookie } from "../../auth/callback.ts"; -import { - requestLeaseApproval, - getLeaseById, -} from "../../../services/leaseService.ts"; +import { requestLeaseApproval } from "../../../services/leaseService.ts"; import { getTenantInviteById, updateTenantInvite, } from "../../../services/tenantInviteService.ts"; import { getPropertyById } from "../../../services/propertyService.ts"; +import { getUserById } from "../../../services/userService.ts"; +import { + sendLeaseApprovalRequest, +} from "../../../services/notificationService.ts"; export const handler = define.handlers({ async POST(ctx) { @@ -99,6 +99,21 @@ export const handler = define.handlers({ await updateTenantInvite(invite_id, { lease_id: lease.id }); + if (invite.landlord_id) { + const tenant = await getUserById(user_id); + const landlord = await getUserById(invite.landlord_id); + + await sendLeaseApprovalRequest(invite.landlord_id, { + lease_id: lease.id, + property_address: property.address_1 + ? `${property.address_1}, ${property.city}, ${property.state}` + : property.title || "Property", + tenant_name: tenant?.name || "Tenant", + landlord_name: landlord?.name || "Landlord", + monthly_rent: property.monthly_rent ?? undefined, + }); + } + return new Response( JSON.stringify({ success: true, diff --git a/routes/landlord/approvals/index.tsx b/routes/landlord/approvals/index.tsx new file mode 100644 index 0000000..45e6222 --- /dev/null +++ b/routes/landlord/approvals/index.tsx @@ -0,0 +1,418 @@ +// routes/landlord/approvals/index.tsx +import { define } from "../../../utils.ts"; +import { page } from "fresh"; +import { SessionCookie } from "../../auth/callback.ts"; +import { createPublicId } from "../../../helpers.ts"; + +interface PendingApproval { + id: string; + property_id?: string; + tenant_id?: string; + approval_status?: string; + approval_requested_at?: number; + approved_at?: number; + approved_by?: string; + rejection_reason?: string | null; + property?: { + id: string; + title?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + zip?: string; + monthly_rent?: number; + bedrooms?: number; + bathrooms?: number; + } | null; + tenant?: { + id: string; + name?: string; + email?: string; + phone?: string; + } | null; +} + +interface LandlordApprovalsData { + stateData?: SessionCookie; + approvals?: PendingApproval[]; + error?: string; + publicId?: string; + [key: string]: unknown; +} + +interface LandlordApprovalsProps { + data: LandlordApprovalsData; +} + +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 response = await fetch("/api/lease/pending-approvals", { + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch pending approvals"); + } + + const data = await response.json(); + const publicId = await createPublicId(user_id); + + return page({ + stateData, + approvals: data.approvals || [], + error: "", + publicId, + }); + } catch (error) { + console.error("Error loading pending approvals:", error); + const publicId = await createPublicId(user_id); + return page({ + stateData, + error: "Failed to load pending approvals", + publicId, + }); + } + }, + + async POST(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 formData = await ctx.req.formData(); + const lease_id = formData.get("lease_id") as string; + const action = formData.get("action") as string; + const rejection_reason = formData.get("rejection_reason") as string || ""; + + if (!lease_id || !action) { + return new Response( + JSON.stringify({ error: "lease_id and action are required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const apiEndpoint = action === "approve" + ? "/api/lease/approve" + : "/api/lease/reject"; + + const body = action === "reject" + ? { lease_id, rejection_reason } + : { lease_id }; + + const response = await fetch(apiEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ + error: "Unknown error", + })); + return new Response( + JSON.stringify({ + error: errorData.error || "Failed to process approval", + }), + { + status: response.status, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return new Response("", { + status: 302, + headers: { Location: "/landlord/approvals" }, + }); + } catch (error) { + console.error("Error processing approval:", error); + return new Response( + JSON.stringify({ error: String(error) }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, +}); + +export default define.page( + function LandlordApprovals({ data }: LandlordApprovalsProps) { + const { approvals, error } = data; + + return ( +
+
+
+
+
+

+ Lease Approvals +

+

+ Review and manage pending lease approval requests +

+
+ +
+
+
+ +
+ {error && ( +
+ {error} +
+ )} + + {approvals && approvals.length > 0 + ? ( +
+
    + {approvals.map((approval) => ( +
  • +
    +
    +
    +
    +
    + + {approval.tenant?.name?.charAt(0) || "T"} + +
    +
    +
    +

    + {approval.tenant?.name || "Tenant"} +

    + + {approval.approval_status} + +
    +
    + +
    + + + + +

    {approval.tenant?.email}

    +
    +
    + +
    +
    +
    +

    + Property: {approval.property?.title || + approval.property?.address_1 || + "Property Address"} +

    +
    +
    +

    + Monthly Rent: ${approval.property?.monthly_rent + ? `$${approval.property.monthly_rent.toLocaleString()}/mo` + : "N/A"} +

    +
    +
    +
    +

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

    +
    +
    + + {approval.approval_status === "pending" && ( +
    +
    +
    + + + +
    +
    + + + +
    +
    + +
    + + +
    + +