224 lines
7.3 KiB
TypeScript
224 lines
7.3 KiB
TypeScript
// routes/landlord/messages/[id]/index.tsx
|
|
import { define } from "../../../../utils.ts";
|
|
import { page } from "fresh";
|
|
import {
|
|
getMessageThread,
|
|
markThreadAsRead,
|
|
} from "../../../../services/messageService.ts";
|
|
import { Message } from "../../../../db/interfaces.ts";
|
|
import { AlertMessage } from "../../../tenant/maintenance/new/components/AlertMessage.tsx";
|
|
|
|
interface MessageThreadPageData {
|
|
thread: Message[];
|
|
error?: string;
|
|
currentUserId: string;
|
|
rootMessageId: string;
|
|
}
|
|
|
|
export const handler = define.handlers({
|
|
async GET(ctx) {
|
|
const { user_id } = ctx.state.data;
|
|
const messageId = ctx.params.id;
|
|
|
|
try {
|
|
const thread = await getMessageThread(messageId);
|
|
|
|
if (!thread || thread.length === 0) {
|
|
return page<MessageThreadPageData>({
|
|
thread: [],
|
|
error: "Message thread not found.",
|
|
currentUserId: user_id,
|
|
rootMessageId: messageId,
|
|
});
|
|
}
|
|
|
|
const rootMessageId = thread[0].parent_message_id || thread[0].id;
|
|
|
|
await markThreadAsRead(user_id, rootMessageId);
|
|
|
|
return page<MessageThreadPageData>({
|
|
thread,
|
|
currentUserId: user_id,
|
|
rootMessageId,
|
|
});
|
|
} catch (err) {
|
|
console.error("Error loading message thread:", err);
|
|
return page<MessageThreadPageData>({
|
|
thread: [],
|
|
error: "Failed to load message thread.",
|
|
currentUserId: user_id,
|
|
rootMessageId: messageId,
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
function MessageBubble(
|
|
{ message, isOutgoing }: { message: Message; isOutgoing: boolean },
|
|
) {
|
|
const bubbleClass = isOutgoing
|
|
? "bg-blue-500 text-white self-end rounded-br-none"
|
|
: "bg-gray-200 text-gray-800 self-start rounded-tl-none";
|
|
const alignmentClass = isOutgoing ? "justify-end" : "justify-start";
|
|
|
|
const date = message.created_on
|
|
? new Date(message.created_on).toLocaleString()
|
|
: "Unknown Date";
|
|
|
|
const attachments = Array.isArray(message.attachments_json)
|
|
? message.attachments_json
|
|
: message.attachments_json
|
|
? JSON.parse(message.attachments_json as unknown as string)
|
|
: [];
|
|
const content = message.content_encrypted || message.content || "";
|
|
|
|
return (
|
|
<div class={`flex ${alignmentClass} mb-4`}>
|
|
<div class={`max-w-3/4 p-4 rounded-lg shadow-md ${bubbleClass}`}>
|
|
<p class="text-xs opacity-80 mb-1">
|
|
{isOutgoing ? "You" : "Tenant"} - {date}
|
|
</p>
|
|
<p class="text-sm whitespace-pre-wrap">{content}</p>
|
|
|
|
{attachments.length > 0 && (
|
|
<div class="mt-3 pt-3 border-t border-opacity-30">
|
|
<p class="text-xs font-semibold mb-1">Attachments:</p>
|
|
<ul class="space-y-1">
|
|
{attachments.map((att: { url: string; name: string }) => (
|
|
<li key={att.url}>
|
|
<a
|
|
href={att.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-xs underline hover:text-opacity-80"
|
|
>
|
|
{att.name}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default define.page(function LandlordMessageThreadPage({ data }) {
|
|
const { thread, error, currentUserId, rootMessageId } =
|
|
data as MessageThreadPageData;
|
|
|
|
const rootMessage = thread[0];
|
|
const subject = rootMessage?.subject || "Conversation";
|
|
const replyRecipientId = rootMessage
|
|
? (rootMessage.sender_id === currentUserId
|
|
? rootMessage.recipient_id
|
|
: rootMessage.sender_id)
|
|
: null;
|
|
const replySubject = rootMessage?.subject
|
|
? `Re: ${rootMessage.subject}`
|
|
: "Re: Message";
|
|
|
|
return (
|
|
<div class="min-h-screen bg-gray-50 landlord-route">
|
|
<div class="bg-white shadow">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="py-6 flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">Message Thread</h1>
|
|
<p class="mt-1 text-sm text-gray-500">Subject: {subject}</p>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<a
|
|
href="/landlord/messages"
|
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 shadow-sm"
|
|
>
|
|
All messages
|
|
</a>
|
|
<a
|
|
href="/landlord/dashboard"
|
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 shadow-sm"
|
|
>
|
|
Dashboard
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8 py-8">
|
|
{error && <AlertMessage type="error">{error}</AlertMessage>}
|
|
|
|
<div class="flex flex-col space-y-4 p-6 bg-white shadow overflow-hidden sm:rounded-lg">
|
|
{thread.map((message) => (
|
|
<MessageBubble
|
|
key={message.id}
|
|
message={message}
|
|
isOutgoing={message.sender_id === currentUserId}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{replyRecipientId && (
|
|
<div class="mt-8 p-6 bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
|
Reply to thread
|
|
</h2>
|
|
<form
|
|
method="POST"
|
|
action="/api/create/message"
|
|
class="space-y-4"
|
|
>
|
|
<input
|
|
type="hidden"
|
|
name="sender"
|
|
value={`/landlord/messages/${rootMessageId}`}
|
|
/>
|
|
<input
|
|
type="hidden"
|
|
name="lease_id"
|
|
value={rootMessage?.lease_id ?? ""}
|
|
/>
|
|
<input
|
|
type="hidden"
|
|
name="recipient_id"
|
|
value={replyRecipientId}
|
|
/>
|
|
<input
|
|
type="hidden"
|
|
name="message_type"
|
|
value={rootMessage?.message_type ?? "general"}
|
|
/>
|
|
<input type="hidden" name="subject" value={replySubject} />
|
|
<div>
|
|
<label
|
|
for="reply-content"
|
|
class="block text-sm font-medium text-gray-700"
|
|
>
|
|
Your reply
|
|
</label>
|
|
<textarea
|
|
id="reply-content"
|
|
name="content"
|
|
rows={4}
|
|
required
|
|
placeholder="Type your reply..."
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<button
|
|
type="submit"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
>
|
|
Send reply
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|