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,