diff --git a/islands/DeleteApplicationButton.tsx b/islands/DeleteApplicationButton.tsx new file mode 100644 index 0000000..ffdef00 --- /dev/null +++ b/islands/DeleteApplicationButton.tsx @@ -0,0 +1,59 @@ +// islands/DeleteApplicationButton.tsx +import { useState } from "preact/hooks"; + +interface DeleteApplicationButtonProps { + applicationId: string; + applicantName: string; + applicantEmail: string; +} + +export default function DeleteApplicationButton({ + applicationId, + applicantName, + applicantEmail +}: DeleteApplicationButtonProps) { + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + const confirmed = confirm( + `Are you sure you want to delete the application from ${applicantName} (${applicantEmail})? This action cannot be undone.` + ); + + if (!confirmed) return; + + setIsDeleting(true); + try { + const formData = new FormData(); + formData.append('id', applicationId); + formData.append('sender', globalThis.location.pathname); + + const response = await fetch('/api/delete/tenant_application', { + method: 'POST', + body: formData + }); + + if (response.redirected) { + globalThis.location.href = response.url; + } else if (response.ok) { + globalThis.location.reload(); + } else { + alert('Failed to delete application. Please try again.'); + } + } catch (_error) { + alert('Error deleting application'); + } finally { + setIsDeleting(false); + } + }; + + return ( + + ); +} diff --git a/islands/DeleteLeaseButton.tsx b/islands/DeleteLeaseButton.tsx new file mode 100644 index 0000000..fbab0f6 --- /dev/null +++ b/islands/DeleteLeaseButton.tsx @@ -0,0 +1,56 @@ +// islands/DeleteLeaseButton.tsx +import { useState } from "preact/hooks"; + +interface DeleteLeaseButtonProps { + leaseId: string; + propertyAddress: string; + tenantName?: string; +} + +export default function DeleteLeaseButton({ leaseId, propertyAddress, tenantName }: DeleteLeaseButtonProps) { + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + const tenantInfo = tenantName ? ` for ${tenantName}` : ""; + const confirmed = confirm( + `Are you sure you want to delete the lease for "${propertyAddress}"${tenantInfo}? This action cannot be undone.` + ); + + if (!confirmed) return; + + setIsDeleting(true); + try { + const formData = new FormData(); + formData.append('lease_id', leaseId); + formData.append('sender', globalThis.location.pathname); + + const response = await fetch('/api/delete/lease', { + method: 'POST', + body: formData + }); + + if (response.redirected) { + globalThis.location.href = response.url; + } else if (response.ok) { + globalThis.location.reload(); + } else { + alert('Failed to delete lease. Please try again.'); + } + } catch (_error) { + alert('Error deleting lease'); + } finally { + setIsDeleting(false); + } + }; + + return ( + + ); +} diff --git a/islands/DeletePropertyButton.tsx b/islands/DeletePropertyButton.tsx new file mode 100644 index 0000000..49b5466 --- /dev/null +++ b/islands/DeletePropertyButton.tsx @@ -0,0 +1,54 @@ +// islands/DeletePropertyButton.tsx +import { useState } from "preact/hooks"; + +interface DeletePropertyButtonProps { + propertyId: string; + propertyAddress: string; +} + +export default function DeletePropertyButton({ propertyId, propertyAddress }: DeletePropertyButtonProps) { + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + const confirmed = confirm( + `Are you sure you want to delete "${propertyAddress}"? This action cannot be undone and will remove all associated data including leases, applications, and maintenance requests.` + ); + + if (!confirmed) return; + + setIsDeleting(true); + try { + const formData = new FormData(); + formData.append('property_id', propertyId); + formData.append('sender', globalThis.location.pathname); + + const response = await fetch('/api/delete/property', { + method: 'POST', + body: formData + }); + + if (response.redirected) { + globalThis.location.href = response.url; + } else if (response.ok) { + globalThis.location.reload(); + } else { + alert('Failed to delete property. Please make sure it has no active leases or applications.'); + } + } catch (_error) { + alert('Error deleting property'); + } finally { + setIsDeleting(false); + } + }; + + return ( + + ); +} diff --git a/islands/DeleteTenantInviteButton.tsx b/islands/DeleteTenantInviteButton.tsx new file mode 100644 index 0000000..02913f8 --- /dev/null +++ b/islands/DeleteTenantInviteButton.tsx @@ -0,0 +1,63 @@ +// islands/DeleteTenantInviteButton.tsx +import { useState } from "preact/hooks"; + +interface DeleteTenantInviteButtonProps { + inviteId: string; + propertyAddress?: string; + invitedEmails?: string[]; +} + +export default function DeleteTenantInviteButton({ + inviteId, + propertyAddress, + invitedEmails +}: DeleteTenantInviteButtonProps) { + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + const propertyInfo = propertyAddress ? `"${propertyAddress}"` : "this property"; + const emailCount = invitedEmails?.length || 0; + const emailInfo = emailCount > 0 ? ` with ${emailCount} invited email${emailCount > 1 ? 's' : ''}` : ""; + + const confirmed = confirm( + `Are you sure you want to delete the tenant invite for ${propertyInfo}${emailInfo}? This action cannot be undone.` + ); + + if (!confirmed) return; + + setIsDeleting(true); + try { + const formData = new FormData(); + formData.append('invite_id', inviteId); + formData.append('sender', globalThis.location.pathname); + + const response = await fetch('/api/delete/tenant_invite', { + method: 'POST', + body: formData + }); + + if (response.redirected) { + globalThis.location.href = response.url; + } else if (response.ok) { + globalThis.location.reload(); + } else { + alert('Failed to delete tenant invite. Please try again.'); + } + } catch (_error) { + alert('Error deleting tenant invite'); + } finally { + setIsDeleting(false); + } + }; + + return ( + + ); +} diff --git a/islands/LeaseApprovalButtons.tsx b/islands/LeaseApprovalButtons.tsx new file mode 100644 index 0000000..5eab6e8 --- /dev/null +++ b/islands/LeaseApprovalButtons.tsx @@ -0,0 +1,78 @@ +// islands/LeaseApprovalButtons.tsx +import { useState } from "preact/hooks"; + +interface LeaseApprovalButtonsProps { + approvalId: string; +} + +export default function LeaseApprovalButtons({ approvalId }: LeaseApprovalButtonsProps) { + const [isApproving, setIsApproving] = useState(false); + const [isRejecting, setIsRejecting] = useState(false); + + const handleApprove = async () => { + setIsApproving(true); + try { + const response = await fetch(`/api/lease/approve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ lease_id: approvalId }) + }); + if (response.ok) { + globalThis.location.reload(); + } else { + alert('Failed to approve lease'); + } + } catch (_error) { + alert('Error approving lease'); + } finally { + setIsApproving(false); + } + }; + + const handleReject = async () => { + const reason = prompt('Rejection reason (optional):'); + if (reason === null) return; // User cancelled + + setIsRejecting(true); + try { + const response = await fetch(`/api/lease/reject`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + lease_id: approvalId, + rejection_reason: reason || '' + }) + }); + if (response.ok) { + globalThis.location.reload(); + } else { + alert('Failed to reject lease'); + } + } catch (_error) { + alert('Error rejecting lease'); + } finally { + setIsRejecting(false); + } + }; + + return ( +
+ + +
+ ); +} diff --git a/islands/LeasedHome.tsx b/islands/LeasedHome.tsx index 2e9fcc8..fe67073 100644 --- a/islands/LeasedHome.tsx +++ b/islands/LeasedHome.tsx @@ -1,5 +1,6 @@ import { Lease, Property } from "../db/interfaces.ts"; import { PopAddLeaseDialog } from "./dialogs/PopAddLeaseDialog.tsx"; +import DeleteLeaseButton from "./DeleteLeaseButton.tsx"; interface LeasedHomeProps { leases: Lease[]; @@ -85,47 +86,12 @@ export default function LeasedHome({ leases, properties }: LeasedHomeProps) { key={lease.id} class="border rounded p-4 bg-white shadow relative group" > -
{ - // Set sender input to current page - const form = e.currentTarget; - form.sender.value = location.pathname; - }} - > - - + - - +
Title: {lease.title}
Rent: {lease.rent}
Deposit: {lease.security_deposit}
diff --git a/routes/api/delete/tenant_invite.ts b/routes/api/delete/tenant_invite.ts new file mode 100644 index 0000000..bec3db1 --- /dev/null +++ b/routes/api/delete/tenant_invite.ts @@ -0,0 +1,31 @@ +// routes/api/delete/tenant_invite.ts +import { define } from "../../../utils.ts"; +import { deleteTenantInvite } from "../../../services/tenantInviteService.ts"; +import { SessionCookie } from "../../auth/callback.ts"; + +export const handler = define.handlers({ + async POST(ctx) { + const formData = await ctx.req.formData(); + const invite_id = formData.get("invite_id"); + const sender = formData.get("sender"); + + if (!invite_id || !sender) { + return new Response("Missing invite_id or sender", { status: 400 }); + } + + await deleteTenantInvite(invite_id.toString()); + + // Redirect back to the sender URL (must be absolute) + const senderPath = sender.toString(); + let redirectUrl = senderPath; + try { + if (senderPath.startsWith("/")) { + const url = new URL(ctx.req.url); + redirectUrl = url.origin + senderPath; + } + } catch (_) { + // fallback: just use senderPath + } + return Response.redirect(redirectUrl, 303); + }, +}); diff --git a/routes/api/lease/approve.ts b/routes/api/lease/approve.ts index 10dfc62..2b7cc33 100644 --- a/routes/api/lease/approve.ts +++ b/routes/api/lease/approve.ts @@ -104,7 +104,6 @@ export const handler = define.handlers({ JSON.stringify({ success: true, lease: approvedLease, - lease: approvedLease, }), { status: 200, diff --git a/routes/api/lease/pending-approvals.ts b/routes/api/lease/pending-approvals.ts index d5e7de0..110747b 100644 --- a/routes/api/lease/pending-approvals.ts +++ b/routes/api/lease/pending-approvals.ts @@ -11,9 +11,8 @@ export const handler = define.handlers({ async GET(ctx) { try { const stateData: SessionCookie = ctx.state.data; - const { user_id } = stateData; - - if (!user_id) { + + if (!stateData || !stateData.user_id) { return new Response( JSON.stringify({ error: "Unauthorized" }), { @@ -23,6 +22,8 @@ export const handler = define.handlers({ ); } + const { user_id } = stateData; + const pendingLeases = await getPendingApprovals(user_id); if (!pendingLeases || pendingLeases.length === 0) { diff --git a/routes/api/lease/reject.ts b/routes/api/lease/reject.ts index cb0a11c..b29db0d 100644 --- a/routes/api/lease/reject.ts +++ b/routes/api/lease/reject.ts @@ -106,7 +106,6 @@ export const handler = define.handlers({ JSON.stringify({ success: true, lease: rejectedLease, - lease: rejectedLease, }), { status: 200, diff --git a/routes/api/lease/request-approval.ts b/routes/api/lease/request-approval.ts index 70af612..9f71a1d 100644 --- a/routes/api/lease/request-approval.ts +++ b/routes/api/lease/request-approval.ts @@ -16,6 +16,7 @@ export const handler = define.handlers({ async POST(ctx) { try { const stateData: SessionCookie = ctx.state.data; + console.log('stateData', stateData); const { user_id } = stateData; if (!user_id) { @@ -113,16 +114,6 @@ export const handler = define.handlers({ monthly_rent: property.monthly_rent ?? undefined, }); } - 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, - }); - } return new Response( JSON.stringify({ diff --git a/routes/landlord/approvals/index.tsx b/routes/landlord/approvals/index.tsx index 45e6222..e352f28 100644 --- a/routes/landlord/approvals/index.tsx +++ b/routes/landlord/approvals/index.tsx @@ -48,19 +48,21 @@ interface LandlordApprovalsProps { export const handler = define.handlers({ async GET(ctx) { const stateData: SessionCookie = ctx.state.data; - const { user_id } = stateData; - - if (!user_id) { + + if (!stateData || !stateData.user_id) { return new Response("", { status: 302, headers: { Location: "/auth/login" }, }); } + const { user_id } = stateData; + try { - const response = await fetch("/api/lease/pending-approvals", { + const response = await fetch(`http://${ctx.req.headers.get("host")}/api/lease/pending-approvals`, { headers: { "Content-Type": "application/json", + "Cookie": ctx.req.headers.get("cookie") || "", }, }); @@ -116,8 +118,8 @@ export const handler = define.handlers({ } const apiEndpoint = action === "approve" - ? "/api/lease/approve" - : "/api/lease/reject"; + ? `http://${ctx.req.headers.get("host")}/api/lease/approve` + : `http://${ctx.req.headers.get("host")}/api/lease/reject`; const body = action === "reject" ? { lease_id, rejection_reason } @@ -127,6 +129,7 @@ export const handler = define.handlers({ method: "POST", headers: { "Content-Type": "application/json", + "Cookie": ctx.req.headers.get("cookie") || "", }, body: JSON.stringify(body), }); diff --git a/routes/landlord/dashboard/index.tsx b/routes/landlord/dashboard/index.tsx index ee95313..ed23d4c 100644 --- a/routes/landlord/dashboard/index.tsx +++ b/routes/landlord/dashboard/index.tsx @@ -1,12 +1,16 @@ // routes/landlord/dashboard/index.tsx import { define } from "../../../utils.ts"; import { getLandlordDashboardData, getPendingLeaseApprovals } from "../../../services/landlordService.ts"; +import { getTenantInvitesByLandlord } from "../../../services/tenantInviteService.ts"; import { DashboardTenantApplication } from "../../../services/tenantApplicationService.ts"; import { DashboardMaintenanceRequest } from "../../../services/maintenanceRequestService.ts"; import { DashboardMessage } from "../../../services/messageService.ts"; +import { TenantInvite, Lease } from "../../../db/interfaces.ts"; import { page } from "fresh"; import { SessionCookie } from "../../auth/callback.ts"; import { MakeNewDate } from "../../../helpers.ts"; +import LeaseApprovalButtons from "../../../islands/LeaseApprovalButtons.tsx"; +import DeleteApplicationButton from "../../../islands/DeleteApplicationButton.tsx"; // Flexible interface for dashboard data - allows any structure interface FlexibleDashboardData { @@ -76,12 +80,14 @@ export const handler = define.handlers({ try { const dashboardData = await getLandlordDashboardData(user_id); const pendingApprovals = await getPendingLeaseApprovals(user_id); + const tenantInvites = await getTenantInvitesByLandlord(user_id); return page({ stateData, dashboardData: { ...dashboardData, pendingApprovals, + tenantInvites, }, }); } catch (error) { @@ -124,6 +130,8 @@ export default define.page( recentMessages, monthlyRevenue, pendingLeaseApprovals, + tenantInvites, + leases, } = dashboardData || {}; return ( @@ -470,6 +478,11 @@ export default define.page( > Reject + )) @@ -520,26 +533,7 @@ export default define.page( : "Unknown date"}

-
-
- - -
-
- - -
-
+ )) ) @@ -554,6 +548,198 @@ export default define.page( + {/* Tenant Invites */} +
+
+
+

+ Tenant Invites +

+ + View all + +
+
+ {tenantInvites && Array.isArray(tenantInvites) && tenantInvites.length > 0 + ? ( + tenantInvites.slice(0, 3).map((invite: TenantInvite) => { + // Parse invited_emails string to get count + const invitedEmails = invite.invited_emails ? + invite.invited_emails.split(',').filter(email => email.trim()) : []; + + return ( +
+
+

+ Property Invite +

+

+ {invitedEmails.length} tenant(s) invited +

+

+ Created {invite.created_on + ? new Date(invite.created_on).toLocaleDateString() + : "Unknown date"} +

+
+
+ + {invite.lease_id ? "Lease Created" : "Pending"} + +
+
+ ); + }) + ) + : ( +
+

+ No tenant invites +

+
+ )} +
+
+
+ + {/* Pending Leases */} +
+
+
+

+ Pending Leases +

+ + View all + +
+
+ {(leases as unknown as Lease[]) && Array.isArray(leases) && (leases as unknown as Lease[]).length > 0 + ? ( + (leases as unknown as Lease[]).filter((lease: Lease) => lease.approval_status === "pending_approval").slice(0, 3).map((lease: Lease) => ( +
+
+

+ {lease.property_address || lease.title || "Property"} +

+

+ ${lease.rent || 0}/month +

+

+ Created {lease.created_on + ? new Date(lease.created_on).toLocaleDateString() + : "Unknown date"} +

+
+
+ + Pending Approval + + {lease.approval_status === "pending_approval" && ( +
+
+ + + +
+
+ + + +
+
+ )} +
+
+ )) + ) + : ( +
+

+ No pending leases +

+
+ )} +
+
+
+ + {/* Active Leases */} +
+
+
+

+ Active Leases +

+ + View all + +
+
+ {(leases as unknown as Lease[]) && Array.isArray(leases) && (leases as unknown as Lease[]).length > 0 + ? ( + (leases as unknown as Lease[]).filter((lease: Lease) => lease.lease_status === "active").slice(0, 3).map((lease: Lease) => ( +
+
+

+ {lease.property_address || lease.title || "Property"} +

+

+ ${lease.rent || 0}/month +

+

+ Started {lease.start_date + ? new Date(lease.start_date).toLocaleDateString() + : "Unknown date"} +

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

+ No active leases +

+
+ )} +
+
+
+ {/* Maintenance Requests */}
diff --git a/routes/landlord/properties/index.tsx b/routes/landlord/properties/index.tsx index 1b7cddf..6d3fe0b 100644 --- a/routes/landlord/properties/index.tsx +++ b/routes/landlord/properties/index.tsx @@ -4,6 +4,7 @@ import { SessionCookie } from "../../auth/callback.ts"; import { getPropertiesByUser } from "../../../services/propertyService.ts"; import { page } from "fresh"; import { Property } from "../../../db/interfaces.ts"; +import DeletePropertyButton from "../../../islands/DeletePropertyButton.tsx"; export const handler = define.handlers({ async GET(ctx) { @@ -147,10 +148,14 @@ export default define.page(function LandlordProperties({ data }) { > Applications +
{property.rental_status === "draft" && ( - )} diff --git a/routes/landlord/tenants/invites.tsx b/routes/landlord/tenants/invites.tsx new file mode 100644 index 0000000..dc58c5f --- /dev/null +++ b/routes/landlord/tenants/invites.tsx @@ -0,0 +1,240 @@ +// routes/landlord/tenants/invites.tsx +import { define } from "../../../utils.ts"; +import { page } from "fresh"; +import type { SessionCookie } from "../../auth/callback.ts"; +import type { Property, TenantInvite } from "../../../db/interfaces.ts"; +import { getPropertiesByOwner } from "../../../services/propertyService.ts"; +import { getTenantInvitesByLandlord } from "../../../services/tenantInviteService.ts"; +import { getPropertyById } from "../../../services/propertyService.ts"; +import DeleteTenantInviteButton from "../../../islands/DeleteTenantInviteButton.tsx"; + +interface InvitesPageData { + stateData: SessionCookie; + properties: Property[]; + invites: (TenantInvite & { property?: Property })[]; + error?: string; +} + +function parseInvitedEmails(value?: string | null) { + if (!value) return []; + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + return parsed.filter((item) => typeof item === "string"); + } + } catch (_error) { + // Fall back to comma-separated parsing. + } + return value.split(",").map((item) => item.trim()).filter(Boolean); +} + +export const handler = define.handlers({ + async GET(ctx) { + const stateData = ctx.state.data as SessionCookie; + const { user_id } = stateData; + + if (!user_id) { + return new Response("", { + status: 302, + headers: { Location: "/auth/login" }, + }); + } + + try { + const properties = await getPropertiesByOwner(user_id); + const invites = await getTenantInvitesByLandlord(user_id); + + // Enrich invites with property data + const enrichedInvites = await Promise.all( + invites.map(async (invite) => { + const property = invite.property_id + ? await getPropertyById(invite.property_id) + : null; + return { ...invite, property }; + }) + ); + + return page({ + stateData, + properties, + invites: enrichedInvites, + }); + } catch (error) { + console.error("Error loading tenant invites:", error); + return page({ + stateData, + properties: [], + invites: [], + error: "Unable to load tenant invites. Please try again.", + }); + } + }, +}); + +export default define.page(function TenantInvitesPage({ data }) { + const { stateData, properties, invites, error } = data as InvitesPageData; + + return ( +
+
+
+
+
+

+ Tenant Invites +

+

+ Manage your tenant invitations and track their status +

+
+ +
+
+
+ +
+ {error && ( +
+ {error} +
+ )} + + {invites.length === 0 + ? ( +
+ + + +

+ No tenant invites +

+

+ Get started by creating your first tenant invite. +

+ +
+ ) + : ( +
+ {invites.map((invite) => { + const invitedEmails = parseInvitedEmails(invite.invited_emails); + const propertyAddress = invite.property?.address_1 || invite.property?.title || 'Unknown Property'; + + return ( +
+
+
+

+ {propertyAddress} +

+ +
+ +
+
+ Invited Emails: +
+ {invitedEmails.length > 0 + ? ( +
    + {invitedEmails.map((email, index) => ( +
  • {email}
  • + ))} +
+ ) + : ( +

No emails added

+ )} +
+
+ +
+ Available On: +

+ {invite.available_on + ? new Date(invite.available_on).toLocaleDateString() + : "Immediately"} +

+
+ +
+ Status: +
+ {invite.lease_id + ? ( + + Lease Created + + ) + : ( + + Pending + + )} +
+
+ +
+ Created: +

+ {invite.created_on + ? new Date(invite.created_on).toLocaleDateString() + : "Unknown"} +

+
+
+
+
+ ); + })} +
+ )} +
+
+ ); +}); diff --git a/routes/tenant/invite/[invite_id].tsx b/routes/tenant/invite/[invite_id].tsx index 888d861..e580f12 100644 --- a/routes/tenant/invite/[invite_id].tsx +++ b/routes/tenant/invite/[invite_id].tsx @@ -132,9 +132,12 @@ export const handler = define.handlers({ await startGuestOnboarding(userId, "tenant"); - const response = await fetch("/api/lease/request-approval", { + const response = await fetch(`http://${ctx.req.headers.get("host")}/api/lease/request-approval`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "Cookie": ctx.req.headers.get("cookie") || "", + }, body: JSON.stringify({ invite_id: invite.id, property_id: invite.property_id, diff --git a/routes/tenant/payment-methods/index.tsx b/routes/tenant/payment-methods/index.tsx index 1994d25..b34ed02 100644 --- a/routes/tenant/payment-methods/index.tsx +++ b/routes/tenant/payment-methods/index.tsx @@ -21,7 +21,7 @@ export const handler = define.handlers({ } try { - const response = await fetch("/api/retrieve/payment_methods", { + const response = await fetch(`http://${ctx.req.headers.get("host")}/api/retrieve/payment_methods`, { headers: { "Cookie": ctx.req.headers.get("Cookie") || "", }, diff --git a/routes/tenant/rent/pay/index.tsx b/routes/tenant/rent/pay/index.tsx index a2e4aed..dfd11cd 100644 --- a/routes/tenant/rent/pay/index.tsx +++ b/routes/tenant/rent/pay/index.tsx @@ -71,9 +71,12 @@ export const handler = define.handlers({ const lease_id = formData.get("lease_id") as string; // Call the payment intent creation endpoint - const intentResponse = await fetch("/api/payments/create_intent", { + const intentResponse = await fetch(`http://${ctx.req.headers.get("host")}/api/payments/create_intent`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "Cookie": ctx.req.headers.get("cookie") || "", + }, body: JSON.stringify({ lease_id, amount, diff --git a/services/e2eFixtures.ts b/services/e2eFixtures.ts index a987ec5..c4c27a7 100644 --- a/services/e2eFixtures.ts +++ b/services/e2eFixtures.ts @@ -157,9 +157,30 @@ let e2eLeases: Lease[] = [ rent: 1800, security_deposit: 1800, lease_status: "active", + approval_status: "approved", + approval_requested_at: BASE_TIME - 1000 * 60 * 60 * 24 * 115, + approved_at: BASE_TIME - 1000 * 60 * 60 * 24 * 114, + approved_by: e2eUserIds.landlord, created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 120, updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 5, }, + { + id: "e2e-lease-pending-1", + title: "Pending Oak Avenue Lease", + property_id: "e2e-prop-1", + property_address: "123 Oak Ave, Seattle, WA", + tenant_id: e2eUserIds.tenant, + owner_id: e2eUserIds.landlord, + start_date: BASE_TIME + 1000 * 60 * 60 * 24 * 30, + end_date: BASE_TIME + 1000 * 60 * 60 * 24 * 395, + rent: 2200, + security_deposit: 2200, + lease_status: "draft", + approval_status: "pending_approval", + approval_requested_at: BASE_TIME - 1000 * 60 * 60 * 24 * 2, + created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 3, + updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 2, + }, ]; const extraE2EMessages: Message[] = Array.from({ length: 9 }, (_, index) => ({ @@ -1040,6 +1061,19 @@ export function getE2ETwitterInvitesByEmail(email: string) { }).map((invite) => ({ ...invite })); } +export function getE2ETenantInvitesByLandlord(landlordId: string) { + return e2eTenantInvites + .filter((invite) => invite.landlord_id === landlordId) + .sort((a, b) => (b.created_on || 0) - (a.created_on || 0)); +} + +export function deleteE2ETenantInvite(inviteId: string) { + const index = e2eTenantInvites.findIndex((invite) => invite.id === inviteId); + if (index !== -1) { + e2eTenantInvites.splice(index, 1); + } +} + export function getE2EPaymentMethodsByUser(userId: string) { return e2ePaymentMethods.filter((method) => method.user_id === userId); } @@ -1220,12 +1254,13 @@ export function getE2ELandlordDashboardData(userId: string) { maintenanceRequests, recentMessages, monthlyRevenue, + leases, }; } export function getE2EPendingApprovals() { return e2eLeases.filter((lease) => - lease.approval_status === "pending" || lease.approval_status === "requested" + lease.approval_status === "pending_approval" || lease.approval_status === "pending" || lease.approval_status === "requested" ); } diff --git a/services/landlordService.ts b/services/landlordService.ts index e7ec555..c910c99 100644 --- a/services/landlordService.ts +++ b/services/landlordService.ts @@ -75,6 +75,7 @@ export interface LandlordDashboardData { maintenanceRequests?: DashboardMaintenanceRequest[]; recentMessages?: DashboardMessage[]; monthlyRevenue?: number; + leases?: Lease[]; } // Extended dashboard interface with additional UI data arrays @@ -422,6 +423,7 @@ export async function getLandlordDashboardData( (sum, property) => sum + (property.monthly_rent || 0), 0, ), + leases: leasesResult, }; return dashboardData; diff --git a/services/leaseService.ts b/services/leaseService.ts index bf5a782..5564146 100644 --- a/services/leaseService.ts +++ b/services/leaseService.ts @@ -12,6 +12,7 @@ import { isE2EEnabled, getE2EPendingApprovals, addE2ELeaseWithApproval, + getE2ETwitterInviteById, } from "./e2eFixtures.ts"; // export interface Lease { @@ -324,9 +325,21 @@ export async function requestLeaseApproval( invite_id: string, ) { if (isE2EEnabled()) { - return [addE2ELeaseWithApproval(tenant_id, property_id, invite_id)]; + // For E2E, get the invite to extract landlord_id + const invite = getE2ETwitterInviteById(invite_id); + const landlord_id = invite?.landlord_id || "e2e-landlord-1"; + + return [addE2ELeaseWithApproval(property_id, tenant_id, landlord_id, "pending_approval")]; } + // Get the invite to extract landlord_id + const invite = await db.select() + .from(tenant_invites) + .where(eq(tenant_invites.id, invite_id)) + .limit(1); + + const landlord_id = invite[0]?.landlord_id || null; + const now = Date.now(); return await db.insert(leases) .values({ @@ -334,7 +347,7 @@ export async function requestLeaseApproval( title: `Lease for Property ${property_id}`, property_id, tenant_id, - owner_id: null, + owner_id: landlord_id, start_date: now, end_date: now + 365 * 24 * 60 * 60 * 1000, rent: null, diff --git a/services/stateMiddleware.ts b/services/stateMiddleware.ts index 78e927e..38f8705 100755 --- a/services/stateMiddleware.ts +++ b/services/stateMiddleware.ts @@ -49,6 +49,7 @@ export function InitiPGSessionMiddleware() { return define.middleware(async (ctx: Context) => { const url = new URL(ctx.req.url); + console.log('path', url.pathname); if (isE2EEnabled()) { const e2eUser = ctx.req.headers.get("x-e2e-user") ?? url.searchParams.get("e2e-user"); diff --git a/services/tenantInviteService.ts b/services/tenantInviteService.ts index de18a1a..4485801 100644 --- a/services/tenantInviteService.ts +++ b/services/tenantInviteService.ts @@ -1,15 +1,17 @@ // services/tenantInviteService.ts // Handle tenant invite creation and updates -import { and, eq, ilike } from "drizzle-orm"; +import { and, eq, ilike, desc } from "drizzle-orm"; import { db } from "../db/db.ts"; import { tenant_invites } from "../db/schema.ts"; import type { TenantInvite } from "../db/interfaces.ts"; import { addE2ETenantInvite, + deleteE2ETenantInvite, getE2ETwitterInviteById, getE2ETwitterInviteByProperty, getE2ETwitterInvitesByEmail, + getE2ETenantInvitesByLandlord, isE2EEnabled, updateE2ETwitterInvite, } from "./e2eFixtures.ts"; @@ -146,3 +148,28 @@ export async function removeTenantInviteEmail(inviteId: string, email: string) { }); return { removed: true, invite: updated[0] ?? invite }; } + +export async function getTenantInvitesByLandlord(landlordId: string): Promise { + if (isE2EEnabled()) { + return getE2ETenantInvitesByLandlord(landlordId); + } + + const result = await db + .select() + .from(tenant_invites) + .where(eq(tenant_invites.landlord_id, landlordId)) + .orderBy(desc(tenant_invites.created_on)); + + return result; +} + +export async function deleteTenantInvite(inviteId: string): Promise { + if (isE2EEnabled()) { + deleteE2ETenantInvite(inviteId); + return; + } + + await db + .delete(tenant_invites) + .where(eq(tenant_invites.id, inviteId)); +}