import { define } from "../../../utils.ts"; import { page } from "fresh"; import type { SessionCookie } from "../../auth/callback.ts"; import { markRoleOnboarded, startGuestOnboarding, } from "../../../services/onboardingService.ts"; import { getPropertyById } from "../../../services/propertyService.ts"; import { getPhotosByProperty } from "../../../services/propertyMediaService.ts"; import { getTenantInviteById, removeTenantInviteEmail, } from "../../../services/tenantInviteService.ts"; import { getUserById } from "../../../services/userService.ts"; import { getLeaseById } from "../../../services/leaseService.ts"; import { createMessage } from "../../../services/messageService.ts"; import LoginPurposeButton from "../../../islands/LoginPurposeButton.tsx"; import type { Lease, Property, PropertyMedia, TenantInvite, User, } from "../../../db/interfaces.ts"; type InvitePreviewData = { invite: TenantInvite | null; property: Property | null; photos: PropertyMedia[]; user: User | null; inviteeEmail: string; status?: string; error?: string; lease?: Lease | null; }; function buildPropertyLabel(property: Property | null) { return property?.title?.trim() || property?.address_1?.trim() || "Rental Property"; } function buildPropertyAddress(property: Property | null) { if (!property) return ""; return [ property.address_1, property.address_2, property.city, property.state, property.zip, ].filter(Boolean).join(", "); } function formatCurrency(amount?: number | null) { if (amount === null || amount === undefined) return "N/A"; return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }).format(amount); } function formatNumber(value?: number | null) { if (value === null || value === undefined) return "N/A"; return new Intl.NumberFormat("en-US").format(value); } async function buildInvitePreview( ctx: { params: Record; req: Request; state: { data: SessionCookie }; }, options: { error?: string } = {}, ) { const inviteId = decodeURIComponent(ctx.params.invite_id || ""); const url = new URL(ctx.req.url); const status = url.searchParams.get("status") || undefined; const stateData = ctx.state.data as SessionCookie; const userId = stateData?.user_id; const user = userId ? await getUserById(userId) : null; const invite = inviteId ? await getTenantInviteById(inviteId) : null; const property = invite?.property_id ? await getPropertyById(invite.property_id) : null; const photos = property?.id ? await getPhotosByProperty(property.id) : []; const inviteeEmail = user?.email?.trim() || ""; const lease = invite?.lease_id ? await getLeaseById(invite.lease_id) : null; return page({ invite, property, photos, user, inviteeEmail, status, lease, error: options.error, }); } export const handler = define.handlers({ async GET(ctx) { return await buildInvitePreview(ctx); }, async POST(ctx) { const inviteId = decodeURIComponent(ctx.params.invite_id || ""); const formData = await ctx.req.formData(); const intent = (formData.get("intent") || "").toString(); const stateData = ctx.state.data as SessionCookie; const userId = stateData?.user_id; const user = userId ? await getUserById(userId) : null; const invite = inviteId ? await getTenantInviteById(inviteId) : null; if (!invite) { return await buildInvitePreview(ctx, { error: "We could not find that invite.", }); } if (intent === "start") { if (!userId) { return new Response("", { status: 302, headers: { Location: "/auth/login" }, }); } if (!invite.property_id) { return await buildInvitePreview(ctx, { error: "This invite is missing the property details.", }); } await startGuestOnboarding(userId, "tenant"); const cookieHeader = ctx.req.headers.get("Cookie") ?? ""; const requestUrl = new URL(ctx.req.url); const apiUrl = `${requestUrl.origin}/api/lease/request-approval`; const response = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json", "Cookie": cookieHeader, }, body: JSON.stringify({ invite_id: invite.id, property_id: invite.property_id, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: "Failed to request lease approval", })); return await buildInvitePreview(ctx, { error: errorData.error || "Failed to request lease approval", }); } const responseData = await response.json().catch(() => ({})); const lease = responseData?.lease; const leaseId = lease?.id ?? null; if (invite.landlord_id && leaseId) { await createMessage( leaseId, invite.landlord_id, userId, "notice", "Lease documents – next steps", "The landlord was notified of your lease request. A follow-up message will contain more information about your lease documents.", null, null, ); } await markRoleOnboarded(userId, "tenant"); return new Response("", { status: 302, headers: { Location: `/tenant/invite/${ encodeURIComponent(invite.id) }?status=pending`, }, }); } if (intent === "decline") { const inviteeEmail = (user?.email || formData.get("invitee_email") || "") .toString() .trim(); if (!inviteeEmail) { return await buildInvitePreview(ctx, { error: "Enter the email tied to the invite to decline it.", }); } const removal = await removeTenantInviteEmail(invite.id, inviteeEmail); if (!removal.removed) { return await buildInvitePreview(ctx, { error: "We could not match that email to this invite.", }); } return new Response("", { status: 302, headers: { Location: `/tenant/invite/${ encodeURIComponent(invite.id) }?status=declined`, }, }); } return new Response("", { status: 302, headers: { Location: `/tenant/invite/${encodeURIComponent(inviteId)}` }, }); }, }); export default define.page(function TenantInvitePreview({ data }) { const { invite, property, photos, user, inviteeEmail, status, lease, error, } = data as InvitePreviewData; const approvalStatus = lease?.approval_status; const isPending = approvalStatus === "pending_approval"; const isApproved = approvalStatus === "approved"; const isRejected = approvalStatus === "rejected"; const hasLease = Boolean(lease); // Allow "Start Lease" when no lease or when previous request was rejected (re-invite same property) const canStartLease = !hasLease || isRejected; if (!invite || !property) { return (

Invite Preview

{error || "We could not load this invite."}

Ask your landlord to resend the invite or verify the link.

Back to Home
); } const propertyLabel = buildPropertyLabel(property); const propertyAddress = buildPropertyAddress(property); const photoUrls = photos .map((photo) => photo.file_url) .filter((url): url is string => Boolean(url && url.trim().length > 0)); const fallbackPhoto = property.image_url?.trim() || ""; const displayedPhotos = photoUrls.length > 0 ? photoUrls : fallbackPhoto ? [fallbackPhoto] : []; const availableOn = invite.available_on ? new Date(invite.available_on).toLocaleDateString() : null; const cancelHref = user?.id ? "/tenant/dashboard" : "/"; const isLoggedIn = Boolean(user?.id); const approvalSteps = [ { label: "Request Lease", completed: hasLease && !isRejected }, { label: "Landlord Review", completed: isApproved }, { label: "Lease Active", completed: isApproved }, ]; const getStepStatus = (stepIndex: number) => { if (!hasLease) return stepIndex === 0 ? "current" : "pending"; if (isRejected) { return stepIndex === 0 ? "current" : stepIndex === 1 ? "error" : "pending"; } if (isPending) return stepIndex <= 1 ? "current" : "pending"; if (isApproved) return stepIndex <= 2 ? "current" : "completed"; return "pending"; }; return (

Tenant Invitation

{propertyLabel}

{propertyAddress && (

{propertyAddress}

)} {inviteeEmail && (

Invite sent to {inviteeEmail}

)} {status === "declined" && (
Invite declined. If this was a mistake, reach out to your landlord for a fresh link.
)} {error && (
{error}
)}

Property Photos

{displayedPhotos.length} photo {displayedPhotos.length === 1 ? "" : "s"}
{displayedPhotos.length > 0 ? (
{`${propertyLabel} {displayedPhotos.slice(1, 3).map((photo, index) => ( {`${propertyLabel} ))}
) : (
No photos yet. Your landlord can add them later.
)}

Property Overview

{property.description || "No description has been added yet."}

Bedrooms

{formatNumber(property.bedrooms)}

Bathrooms

{formatNumber(property.bathrooms)}

Square Feet

{formatNumber(property.square_footage)}

Property Type

{property.property_type || "N/A"}

{property.amenities && (

Amenities

{property.amenities}

)}

Ready to proceed?

{canStartLease && "Start your lease request, decline the invite, or come back later."} {isPending && "Your request is pending landlord review. Check back here for updates."} {isApproved && "Your lease is approved! Continue to your tenant portal."} {isRejected && !canStartLease && "Your lease request was declined. Contact your landlord for more information."}

{canStartLease && isLoggedIn && (
)} {isApproved && isLoggedIn && ( Continue to Tenant Portal )} {!isLoggedIn && ( Sign In to Start Lease )}
{!inviteeEmail && ( )}
Cancel
{!isLoggedIn && (

Sign in to save your decision and view the full tenant portal.

)}
); });