// routes/tenant/messages/index.tsx import { define } from "../../../utils.ts"; import { page } from "fresh"; import { SessionCookie } from "../../auth/callback.ts"; import { DashboardMessage, deleteMessagesForUser, getMessagesForUser, markMessagesAsRead, 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, ): 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 `/tenant/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 filters: Filters = { status: sanitizeEnum( url.searchParams.get("status"), STATUS_VALUES, "all", ), type: sanitizeEnum( url.searchParams.get("type"), TYPE_VALUES, "all", ), direction: sanitizeEnum( url.searchParams.get("direction"), DIRECTION_VALUES, "all", ), sort: sanitizeEnum( url.searchParams.get("sort"), SORT_VALUES, "newest", ), search: url.searchParams.get("q") || "", pageSize, }; const messagePage = await getMessagesForUser(user_id, { page: pageNumber, pageSize, sort: filters.sort, status: filters.status, type: filters.type, direction: filters.direction, search: filters.search, }); return page({ stateData, messagePage, filters, }); } catch (error) { console.error("Error loading messages:", error); return page({ stateData: ctx.state.data as SessionCookie, 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 { user_id } = ctx.state.data; if (!user_id) { return new Response("", { status: 302, headers: { Location: "/auth/login" }, }); } try { const formData = await ctx.req.formData(); const action = formData.get("action") as string; const message_ids_raw = formData.get("message_ids") as string; const message_ids = message_ids_raw ? message_ids_raw.split(",") : []; if (message_ids.length === 0) { return new Response("", { status: 302, headers: { Location: "/tenant/messages" }, }); } if (action === "mark_read") { await markMessagesAsRead(user_id, message_ids); } else if (action === "delete_messages") { await deleteMessagesForUser(user_id, message_ids); } return new Response("", { status: 302, headers: { Location: "/tenant/messages" }, }); } catch (error) { console.error("Error updating message:", error); return new Response("Failed to update message", { status: 500 }); } }, }); export default define.page(function TenantMessages({ data }) { const { stateData, messagePage, filters, error } = data as MessagesPageData; const { messages, page, total, totalPages } = messagePage; const totalMessages = messagePage.totalMessages ?? total; const unread = messagePage.unread ?? 0; const FilterButton = ( { name, value, label }: { name: keyof Filters; value: string; label: string; }, ) => { const currentUrl = new URL( `http://dummy.com/tenant/messages${location.search}`, ); const isActive = filters[name] === value; const handleClick = (e: Event) => { e.preventDefault(); const newUrl = new URL(currentUrl.toString()); newUrl.searchParams.set(name, value); newUrl.searchParams.set("page", "1"); if (value === "all") { newUrl.searchParams.delete(name); } window.location.href = newUrl.pathname + newUrl.search; }; return ( ); }; const SortSelect = () => { const currentUrl = new URL( `http://dummy.com/tenant/messages${location.search}`, ); const handleChange = (e: Event) => { const target = e.target as HTMLSelectElement; const newUrl = new URL(currentUrl.toString()); newUrl.searchParams.set("sort", target.value); newUrl.searchParams.set("page", "1"); window.location.href = newUrl.pathname + newUrl.search; }; return ( ); }; const SearchForm = () => { const currentUrl = new URL( `http://dummy.com/tenant/messages${location.search}`, ); const handleSubmit = (e: Event) => { e.preventDefault(); const form = e.target as HTMLFormElement; const formData = new FormData(form); const q = formData.get("q") as string; const newUrl = new URL(currentUrl.toString()); newUrl.searchParams.set("q", q.trim()); newUrl.searchParams.set("page", "1"); window.location.href = newUrl.pathname + newUrl.search; }; return (
); }; const MessageRow = ({ message }: { message: DashboardMessage }) => { const isUnread = !message.is_read; const senderName = message.sender_id === stateData.user_id ? "You" : "Landlord"; const isReply = !!message.parent_message_id; return (
  • {isReply && ( Reply )} {message.subject || "No Subject"}

    {senderName}: {" "} {(message.content_encrypted || message.content)?.substring( 0, 100, ) || "No content preview"} ...

    {new Date(message.created_on!).toLocaleDateString()}
    View Thread
  • ); }; return (
    {/* Header */}

    Messages

    Communication with your landlord

    {/* Main Content */}
    {error && (
    Error: {error}
    )}
    {/* Filters and Search */}
    Sort:
    {/* Bulk Actions and Message List */}

    Showing{" "} {(page - 1) * filters.pageSize + 1} {" "} to{" "} {Math.min(page * filters.pageSize, totalMessages)} {" "} of{" "} {totalMessages} {" "} results {unread > 0 && ( ({unread} unread) )}

    {messages.length > 0 ? (
      {messages.map((message) => )}
    ) : (

    No messages found

    Try adjusting your filters or search query.

    )}
    {/* Pagination */}
    Previous
    {Array.from({ length: totalPages }, (_, index) => index + 1) .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2)) .map((pageNumber) => ( {pageNumber} ))}
    Next
    {/* Islands for interactive features */}
    ); });