286 lines
9.2 KiB
TypeScript
286 lines
9.2 KiB
TypeScript
// routes/tenant/messages/new/index.tsx
|
|
import { define } from "../../../../utils.ts";
|
|
import { page } from "fresh";
|
|
import { getLeasesByTenant } from "../../../../services/leaseService.ts";
|
|
import {
|
|
createMessage,
|
|
getMessageById,
|
|
} from "../../../../services/messageService.ts";
|
|
import { uploadToLocalBlobStorage } from "../../../../utils/blobStorage.ts";
|
|
import { Lease, Message } from "../../../../db/interfaces.ts";
|
|
import { FormField } from "../../maintenance/new/components/FormField.tsx";
|
|
import { TextAreaField } from "../../maintenance/new/components/TextAreaField.tsx";
|
|
import { FileUpload } from "../../maintenance/new/components/FileUpload.tsx";
|
|
import { AlertMessage } from "../../maintenance/new/components/AlertMessage.tsx";
|
|
|
|
interface NewMessagePageData {
|
|
currentLease?: Lease;
|
|
landlordId?: string;
|
|
replyToMessage?: Message | null;
|
|
initialSubject: string;
|
|
error?: string;
|
|
}
|
|
|
|
export const handler = define.handlers({
|
|
async GET(ctx) {
|
|
const { user_id } = ctx.state.data;
|
|
|
|
try {
|
|
const url = new URL(ctx.req.url);
|
|
const replyToId = url.searchParams.get("reply_to");
|
|
|
|
const leases = await getLeasesByTenant(user_id);
|
|
const currentLease = leases?.[0];
|
|
const landlordId = currentLease?.owner_id;
|
|
|
|
if (!landlordId) {
|
|
return page<NewMessagePageData>({
|
|
error: "Could not find an active lease or landlord ID.",
|
|
initialSubject: "",
|
|
});
|
|
}
|
|
|
|
let replyToMessage: Message | null = null;
|
|
let initialSubject = "";
|
|
|
|
if (replyToId) {
|
|
const fetchedMessage = await getMessageById(replyToId);
|
|
if (fetchedMessage) {
|
|
replyToMessage = fetchedMessage;
|
|
// Use the root message ID for threading if it's a reply
|
|
const rootId = replyToMessage.parent_message_id || replyToMessage.id;
|
|
replyToMessage.id = rootId; // Overwrite ID to ensure reply goes to the root
|
|
|
|
const originalSubject = replyToMessage.subject || "No Subject";
|
|
initialSubject = originalSubject.startsWith("Re:")
|
|
? originalSubject
|
|
: `Re: ${originalSubject}`;
|
|
}
|
|
}
|
|
|
|
return page<NewMessagePageData>({
|
|
currentLease,
|
|
landlordId,
|
|
replyToMessage,
|
|
initialSubject,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error loading new message form:", error);
|
|
return page<NewMessagePageData>({
|
|
error: "Failed to load form",
|
|
initialSubject: "",
|
|
});
|
|
}
|
|
},
|
|
|
|
async POST(ctx) {
|
|
const { user_id } = ctx.state.data;
|
|
|
|
try {
|
|
const formData = await ctx.req.formData();
|
|
const lease_id = formData.get("lease_id") as string;
|
|
const recipient_id = formData.get("recipient_id") as string;
|
|
const subject = formData.get("subject") as string;
|
|
const content = formData.get("content") as string;
|
|
const parent_message_id = formData.get("parent_message_id") as string ||
|
|
null;
|
|
|
|
if (!lease_id || !recipient_id || !subject || !content) {
|
|
return new Response("Missing required fields", { status: 400 });
|
|
}
|
|
|
|
const attachments: { url: string; name: string; type: string }[] = [];
|
|
const files = formData.getAll("attachments") as File[];
|
|
|
|
for (const file of files) {
|
|
if (file.size > 0) {
|
|
const blob = new Blob([await file.arrayBuffer()], {
|
|
type: file.type,
|
|
});
|
|
const uploadResult = await uploadToLocalBlobStorage(
|
|
file.name,
|
|
blob,
|
|
"image",
|
|
{ useTimestamp: true },
|
|
);
|
|
if (uploadResult.success && uploadResult.url) {
|
|
attachments.push({
|
|
url: uploadResult.url,
|
|
name: file.name,
|
|
type: file.type,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
await createMessage(
|
|
lease_id,
|
|
user_id,
|
|
recipient_id,
|
|
"general", // Default message type for tenant-initiated messages
|
|
subject,
|
|
content,
|
|
parent_message_id,
|
|
attachments.length > 0 ? attachments : null,
|
|
);
|
|
|
|
// Redirect to the thread if it was a reply, otherwise to the main list
|
|
const redirectPath = parent_message_id
|
|
? `/tenant/messages/${parent_message_id}`
|
|
: "/tenant/messages";
|
|
|
|
return new Response("", {
|
|
status: 302,
|
|
headers: { Location: redirectPath },
|
|
});
|
|
} catch (error) {
|
|
console.error("Error submitting new message:", error);
|
|
return page<NewMessagePageData>({
|
|
error: "Failed to send message. Please try again.",
|
|
initialSubject: "",
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
export default define.page(function NewTenantMessage({ data }) {
|
|
const {
|
|
currentLease,
|
|
landlordId,
|
|
replyToMessage,
|
|
initialSubject,
|
|
error,
|
|
} = data as NewMessagePageData;
|
|
|
|
const isReply = !!replyToMessage;
|
|
const title = isReply ? "Reply to Message" : "Compose New Message";
|
|
const subtitle = isReply
|
|
? `Replying to: ${replyToMessage.subject || "No Subject"}`
|
|
: "Send a new message to your landlord.";
|
|
|
|
if (error && error !== "Failed to load form") {
|
|
// If there's an error after POST, we should show it
|
|
// but we don't have the form data to re-render the form, so we show a generic error page
|
|
return (
|
|
<div class="min-h-screen bg-gray-50 tenant-route p-8">
|
|
<AlertMessage type="error">{error}</AlertMessage>
|
|
<div class="mt-6">
|
|
<a
|
|
href="/tenant/messages"
|
|
class="text-blue-600 hover:text-blue-500 font-medium"
|
|
>
|
|
Go back to Messages
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!currentLease || !landlordId) {
|
|
return (
|
|
<div class="min-h-screen bg-gray-50 tenant-route p-8">
|
|
<AlertMessage
|
|
type="error"
|
|
message={error ||
|
|
"You must have an active lease to send a message. Please contact support."}
|
|
/>
|
|
<div class="mt-6">
|
|
<a
|
|
href="/tenant/dashboard"
|
|
class="text-blue-600 hover:text-blue-500 font-medium"
|
|
>
|
|
Go to Dashboard
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div class="min-h-screen bg-gray-50 tenant-route">
|
|
{/* Header */}
|
|
<div class="bg-white shadow">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="py-6">
|
|
<h1 class="text-3xl font-bold text-gray-900">{title}</h1>
|
|
<p class="mt-1 text-sm text-gray-500">{subtitle}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8 py-8">
|
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<div class="p-6">
|
|
<form method="POST" encType="multipart/form-data">
|
|
{/* Hidden Fields */}
|
|
<input
|
|
type="hidden"
|
|
name="lease_id"
|
|
value={currentLease.id}
|
|
/>
|
|
<input
|
|
type="hidden"
|
|
name="recipient_id"
|
|
value={landlordId}
|
|
/>
|
|
{isReply && (
|
|
<input
|
|
type="hidden"
|
|
name="parent_message_id"
|
|
value={replyToMessage.id}
|
|
/>
|
|
)}
|
|
|
|
<div class="space-y-6">
|
|
{error && <AlertMessage type="error">{error}</AlertMessage>}
|
|
|
|
<FormField label="Subject" required>
|
|
<input
|
|
type="text"
|
|
name="subject"
|
|
id="subject"
|
|
required
|
|
defaultValue={initialSubject}
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField label="Message Content" required>
|
|
<TextAreaField
|
|
id="content"
|
|
name="content"
|
|
rows={8}
|
|
required
|
|
placeholder="Write your message here..."
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField label="Attachments (Optional)">
|
|
<FileUpload name="attachments" multiple />
|
|
</FormField>
|
|
|
|
{/* Submit Button */}
|
|
<div class="flex justify-between pt-6">
|
|
<a
|
|
href="/tenant/messages"
|
|
class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
>
|
|
Cancel
|
|
</a>
|
|
<button
|
|
type="submit"
|
|
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
>
|
|
{isReply ? "Send Reply" : "Send Message"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|