// routes/landlord/messages/index.tsx import { define } from "../../../utils.ts"; import { page } from "fresh"; import { SessionCookie } from "../../auth/callback.ts"; import { deleteMessagesForUser, getMessagesForUser, MessagePage, } from "../../../services/messageService.ts"; import LandlordMessageSelection from "../../../islands/LandlordMessageSelection.tsx"; import LandlordMessageThreads from "../../../islands/LandlordMessageThreads.tsx"; type FilterValue = "all" | "read" | "unread"; type TypeValue = "all" | "general" | "maintenance" | "payment" | "notice"; type DirectionValue = "all" | "sent" | "received"; type SortValue = "newest" | "oldest"; interface Filters { status: FilterValue; type: TypeValue; direction: DirectionValue; sort: SortValue; search: string; pageSize: number; } interface MessagesPageData { stateData: SessionCookie; messagePage: MessagePage; filters: Filters; error?: string; } const STATUS_VALUES: FilterValue[] = ["all", "read", "unread"]; const TYPE_VALUES: TypeValue[] = [ "all", "general", "maintenance", "payment", "notice", ]; const DIRECTION_VALUES: DirectionValue[] = ["all", "sent", "received"]; const SORT_VALUES: SortValue[] = ["newest", "oldest"]; const PAGE_SIZES = [10, 20, 50]; function sanitizeEnum( value: string | null, allowed: T[], fallback: T, ) { if (!value) return fallback; return allowed.includes(value as T) ? (value as T) : fallback; } function buildPageUrl( filters: Filters, pageNumber: number, ) { const params = new URLSearchParams(); params.set("page", String(pageNumber)); params.set("page_size", String(filters.pageSize)); params.set("status", filters.status); params.set("type", filters.type); params.set("direction", filters.direction); params.set("sort", filters.sort); if (filters.search) params.set("q", filters.search); return `/landlord/messages?${params.toString()}`; } 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 url = new URL(ctx.req.url); const pageNumber = Math.max( 1, Number(url.searchParams.get("page") || "1"), ); const rawPageSize = Number(url.searchParams.get("page_size") || "10"); const pageSize = PAGE_SIZES.includes(rawPageSize) ? rawPageSize : 10; const status = sanitizeEnum( url.searchParams.get("status"), STATUS_VALUES, "all", ); const type = sanitizeEnum( url.searchParams.get("type"), TYPE_VALUES, "all", ); const direction = sanitizeEnum( url.searchParams.get("direction"), DIRECTION_VALUES, "all", ); const sort = sanitizeEnum( url.searchParams.get("sort"), SORT_VALUES, "newest", ); const search = url.searchParams.get("q")?.trim() || ""; const messagePage = await getMessagesForUser(user_id, { page: pageNumber, pageSize, sort, status, type, direction, search, }); return page({ stateData, messagePage, filters: { status, type, direction, sort, search, pageSize, }, }); } catch (error) { console.error("Error loading messages:", error); return page({ stateData, messagePage: { messages: [], page: 1, pageSize: 10, total: 0, totalMessages: 0, totalPages: 1, unread: 0, }, filters: { status: "all", type: "all", direction: "all", sort: "newest", search: "", pageSize: 10, }, error: "Failed to load messages.", }); } }, 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 action = formData.get("action")?.toString() || ""; const returnTo = formData.get("return_to")?.toString() || "/landlord/messages"; const safeReturnTo = returnTo.startsWith("/") ? returnTo : "/landlord/messages"; const redirectUrl = new URL(safeReturnTo, ctx.req.url); if (action !== "delete_selected") { return Response.redirect(redirectUrl, 303); } const ids = formData.getAll("message_ids") .map((value) => value.toString()) .filter(Boolean); if (ids.length > 0) { await deleteMessagesForUser(user_id, ids); } return Response.redirect(redirectUrl, 303); } catch (error) { console.error("Error deleting messages:", error); const fallbackUrl = new URL("/landlord/messages", ctx.req.url); return Response.redirect(fallbackUrl, 303); } }, }); export default define.page(function LandlordMessages({ data }) { const { stateData, messagePage, filters, error } = data as MessagesPageData; const { messages, page, pageSize, total, totalPages } = messagePage; const startIndex = total === 0 ? 0 : (page - 1) * pageSize + 1; const endIndex = Math.min(total, page * pageSize); const returnTo = buildPageUrl(filters, page); const hasMessages = messages.length > 0; return (

Messages

Review all landlord-tenant conversations in one place.

{error && (
{error}
)}

All Messages

showing {startIndex}-{endIndex} of {total} messages

page {page} of {totalPages}
{messages.length === 0 ? (
No messages match the selected filters.
) : (
{messages.map((message) => { const isOutbound = message.sender_id === stateData.user_id; const counterpart = isOutbound ? message.recipient_name || "Recipient" : message.sender_name || "Tenant"; const replyRecipientId = isOutbound ? message.recipient_id : message.sender_id; const replySubject = message.subject ? `Re: ${message.subject}` : "Re: Message"; const canReply = Boolean(replyRecipientId); const isUnreadInbound = !isOutbound && message.recipient_id === stateData.user_id && !message.is_read; const subjectClass = isUnreadInbound ? "mt-2 text-sm font-semibold text-gray-900" : "mt-2 text-sm font-medium text-gray-700"; const previewClass = isUnreadInbound ? "mt-1 text-sm text-gray-700 line-clamp-2" : "mt-1 text-sm text-gray-500 line-clamp-2"; const panelId = `message-panel-${message.id}`; return (