diff --git a/app/[locale]/share/proof-of-payment/[id]/not-found.tsx b/app/[locale]/share/proof-of-payment/[id]/not-found.tsx new file mode 100644 index 0000000..ce78ef3 --- /dev/null +++ b/app/[locale]/share/proof-of-payment/[id]/not-found.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Proof of payment not found

+
+ ); +} diff --git a/app/[locale]/share/proof-of-payment/[id]/route.tsx b/app/[locale]/share/proof-of-payment/[id]/route.tsx new file mode 100644 index 0000000..f3122c6 --- /dev/null +++ b/app/[locale]/share/proof-of-payment/[id]/route.tsx @@ -0,0 +1,30 @@ +import { getDbClient } from '@/app/lib/dbClient'; +import { BillingLocation } from '@/app/lib/db-types'; +import { notFound } from 'next/navigation'; + +export async function GET(request: Request, { params:{ id } }: { params: { id:string } }) { + const locationID = id; + + const dbClient = await getDbClient(); + const location = await dbClient.collection("lokacije") + .findOne({ _id: locationID }); + + if(!location?.utilBillsProofOfPaymentAttachment) { + notFound(); + } + + // Convert fileContentsBase64 from Base64 string to binary + const fileContentsBuffer = Buffer.from(location.utilBillsProofOfPaymentAttachment.fileContentsBase64, 'base64'); + + // Convert fileContentsBuffer to format that can be sent to the client + const fileContents = new Uint8Array(fileContentsBuffer); + + return new Response(fileContents, { + status: 200, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${location.utilBillsProofOfPaymentAttachment.fileName}"`, + 'Last-Modified': `${location.utilBillsProofOfPaymentAttachment.fileLastModified}` + } + }); +} diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index df7b458..61831af 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -580,4 +580,77 @@ export const setSeenByTenant = async (locationID: string): Promise => { { _id: locationID }, { $set: { seenByTenant: true } } ); +} + +/** + * Serializes a file attachment to be stored in the database + * @param file - The file to serialize + * @returns BillAttachment object or null if file is invalid + */ +const serializeAttachment = async (file: File | null) => { + if (!file) { + return null; + } + + const { + name: fileName, + size: fileSize, + type: fileType, + lastModified: fileLastModified, + } = file; + + if(!fileName || fileName === 'undefined' || fileSize === 0) { + return null; + } + + // Convert file contents to base64 for database storage + const fileContents = await file.arrayBuffer(); + const fileContentsBase64 = Buffer.from(fileContents).toString('base64'); + + return { + fileName, + fileSize, + fileType, + fileLastModified, + fileContentsBase64, + }; +} + +/** + * Uploads utility bills proof of payment attachment for a location + * @param locationID - The ID of the location + * @param formData - FormData containing the file + * @returns Promise with success status + */ +export const uploadUtilBillsProofOfPayment = async (locationID: string, formData: FormData): Promise<{ success: boolean; error?: string }> => { + noStore(); + + try { + const file = formData.get('utilBillsProofOfPaymentAttachment') as File; + + // Validate file type + if (file && file.type !== 'application/pdf') { + return { success: false, error: 'Only PDF files are accepted' }; + } + + const attachment = await serializeAttachment(file); + + if (!attachment) { + return { success: false, error: 'Invalid file' }; + } + + const dbClient = await getDbClient(); + + // Update the location with the attachment + await dbClient.collection("lokacije") + .updateOne( + { _id: locationID }, + { $set: { utilBillsProofOfPaymentAttachment: attachment } } + ); + + return { success: true }; + } catch (error: any) { + console.error('Error uploading util bills proof of payment:', error); + return { success: false, error: error.message || 'Upload failed' }; + } } \ No newline at end of file diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index 617db8f..5c67800 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -69,6 +69,8 @@ export interface BillingLocation { rentAmount?: number | null; /** (optional) whether the location has been seen by tenant */ seenByTenant?: boolean | null; + /** (optional) utility bills proof of payment attachment */ + utilBillsProofOfPaymentAttachment?: BillAttachment|null; }; export enum BilledTo { diff --git a/app/ui/ViewLocationCard.tsx b/app/ui/ViewLocationCard.tsx index ee41ad6..4bd69ca 100644 --- a/app/ui/ViewLocationCard.tsx +++ b/app/ui/ViewLocationCard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FC } from "react"; +import { FC, useState } from "react"; import { BilledTo, BillingLocation, UserSettings } from "../lib/db-types"; import { formatYearMonth } from "../lib/format"; import { formatCurrency } from "../lib/formatStrings"; @@ -10,6 +10,7 @@ import { Pdf417Barcode } from "./Pdf417Barcode"; import { PaymentParams } from "hub-3a-payment-encoder"; import Link from "next/link"; import { DocumentIcon } from "@heroicons/react/24/outline"; +import { uploadUtilBillsProofOfPayment } from "../lib/actions/locationActions"; export interface ViewLocationCardProps { location: BillingLocation; @@ -18,10 +19,54 @@ export interface ViewLocationCardProps { export const ViewLocationCard:FC = ({location, userSettings}) => { - const { _id, name: locationName, yearMonth, bills, tenantName, tenantStreet, tenantTown, generateTenantCode } = location; + const { _id, name: locationName, yearMonth, bills, tenantName, tenantStreet, tenantTown, generateTenantCode, utilBillsProofOfPaymentAttachment } = location; const t = useTranslations("home-page.location-card"); + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + const [attachment, setAttachment] = useState(utilBillsProofOfPaymentAttachment); + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Validate file type + if (file.type !== 'application/pdf') { + setUploadError('Only PDF files are accepted'); + e.target.value = ''; // Reset input + return; + } + + setIsUploading(true); + setUploadError(null); + + try { + const formData = new FormData(); + formData.append('utilBillsProofOfPaymentAttachment', file); + + const result = await uploadUtilBillsProofOfPayment(_id, formData); + + if (result.success) { + // Update local state with the uploaded attachment + setAttachment({ + fileName: file.name, + fileSize: file.size, + fileType: file.type, + fileLastModified: file.lastModified, + fileContentsBase64: '', // We don't need the contents in the UI + }); + } else { + setUploadError(result.error || 'Upload failed'); + } + } catch (error: any) { + setUploadError(error.message || 'Upload failed'); + } finally { + setIsUploading(false); + e.target.value = ''; // Reset input + } + }; + // sum all the billAmounts (only for bills billed to tenant) const monthlyExpense = bills.reduce((acc, bill) => (bill.paid && (bill.billedTo ?? BilledTo.Tenant) === BilledTo.Tenant) ? acc + (bill.payedAmount ?? 0) : acc, 0); @@ -77,17 +122,41 @@ export const ViewLocationCard:FC = ({location, userSettin : null } - - - utility-bills-proof-of-payment.pdf - {/*decodeURIComponent(utilBillsProofOfPaymentAttachment.fileName)*/} - -
- - -
+ {attachment ? ( +
+ + + {decodeURIComponent(attachment.fileName)} + +
+ ) : ( +
+ +
+ + {isUploading && ( + + )} +
+ {uploadError && ( +

{uploadError}

+ )} +
+ )} ); }; \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index 0c03375..28a1617 100644 --- a/middleware.ts +++ b/middleware.ts @@ -10,7 +10,7 @@ import { locales, defaultLocale } from '@/app/i18n'; import { Session } from 'next-auth'; // http://localhost:3000/share/location/675c41b227d0df76a35f106e -const publicPages = ['/terms', '/policy', '/login', '/share/location/.*', '/share/bill/.*', '/share/attachment/.*']; +const publicPages = ['/terms', '/policy', '/login', '/share/location/.*', '/share/bill/.*', '/share/attachment/.*', '/share/proof-of-payment/.*']; const intlMiddleware = createIntlMiddleware({ locales,