Implement per-bill proof of payment and update field names

Frontend changes:
- Added ViewBillCard proof of payment upload for per-bill mode
  - Conditional rendering based on proofOfPaymentType
  - File upload with PDF validation and loading states
  - Download link to /share/proof-of-payment/per-bill/
- Updated LocationCard to use new utilBillsProofOfPayment field structure

Backend changes:
- Updated locationActions with improved file validation
  - File size validation using MAX_PROOF_OF_PAYMENT_UPLOAD_SIZE_KB
  - PDF type validation before database operations
  - Enhanced serializeAttachment with FileAttachment type
  - Updated database projections for optimized queries
- Updated monthActions to use consolidated field name
- Updated proof-of-payment download route with new field names

Data structure migration:
- Replaced utilBillsProofOfPaymentAttachment + utilBillsProofOfPaymentUploadedAt
  with single utilBillsProofOfPayment object containing uploadedAt
- Consistent use of FileAttachment type across all upload functions

Translations:
- Added upload-proof-of-payment-legend and upload-proof-of-payment-label
  to bill-edit-form section in both English and Croatian

This completes the proof of payment feature implementation for both
combined (location-level) and per-bill modes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-07 13:11:17 +01:00
parent 0facc9c257
commit aa573c68a3
7 changed files with 172 additions and 114 deletions

View File

@@ -2,7 +2,7 @@
import { z } from 'zod';
import { getDbClient } from '../dbClient';
import { BillingLocation, YearMonth } from '../db-types';
import { BillingLocation, FileAttachment, YearMonth } from '../db-types';
import { ObjectId } from 'mongodb';
import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth';
@@ -442,8 +442,8 @@ export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:nu
// "bills.hub3aText": 1,
// project only file name - leave out file content so that
// less data is transferred to the client
"utilBillsProofOfPaymentUploadedAt": 1,
"utilBillsProofOfPaymentAttachment.fileName": 1,
"utilBillsProofOfPayment.fileName": 1,
"utilBillsProofOfPayment.uploadedAt": 1,
},
},
{
@@ -505,7 +505,7 @@ export const fetchLocationById = async (locationID:string) => {
projection: {
// don't include the attachment binary data in the response
"bills.attachment.fileContentsBase64": 0,
"utilBillsProofOfPaymentAttachment.fileContentsBase64": 0,
"utilBillsProofOfPayment.fileContentsBase64": 0,
},
}
);
@@ -599,7 +599,7 @@ export const setSeenByTenantAt = async (locationID: string): Promise<void> => {
* @param file - The file to serialize
* @returns BillAttachment object or null if file is invalid
*/
const serializeAttachment = async (file: File | null) => {
const serializeAttachment = async (file: File | null):Promise<FileAttachment | null> => {
if (!file) {
return null;
}
@@ -625,11 +625,12 @@ const serializeAttachment = async (file: File | null) => {
fileType,
fileLastModified,
fileContentsBase64,
uploadedAt: new Date(),
};
}
/**
* Uploads utility bills proof of payment attachment for a location
* Uploads a single proof of payment for all utility bills in a location
* @param locationID - The ID of the location
* @param formData - FormData containing the file
* @returns Promise with success status
@@ -639,23 +640,32 @@ export const uploadUtilBillsProofOfPayment = async (locationID: string, formData
try {
// check if attachment already exists for the location
const dbClient = await getDbClient();
// First validate that the file is acceptable
const file = formData.get('utilBillsProofOfPayment') as File;
const existingLocation = await dbClient.collection<BillingLocation>("lokacije")
.findOne({ _id: locationID }, { projection: { utilBillsProofOfPaymentAttachment: 1 } });
if (existingLocation?.utilBillsProofOfPaymentAttachment) {
return { success: false, error: 'An attachment already exists for this location' };
// validate max file size from env variable
const maxFileSizeKB = parseInt(process.env.MAX_PROOF_OF_PAYMENT_UPLOAD_SIZE_KB || '1024', 10);
const maxFileSizeBytes = maxFileSizeKB * 1024;
if (file && file.size > maxFileSizeBytes) {
return { success: false, error: `File size exceeds the maximum limit of ${maxFileSizeKB} KB` };
}
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' };
}
// check if attachment already exists for the location
const dbClient = await getDbClient();
const existingLocation = await dbClient.collection<BillingLocation>("lokacije")
.findOne({ _id: locationID }, { projection: { utilBillsProofOfPayment: 1 } });
if (existingLocation?.utilBillsProofOfPayment) {
return { success: false, error: 'An attachment already exists for this location' };
}
const attachment = await serializeAttachment(file);
if (!attachment) {
@@ -667,8 +677,9 @@ export const uploadUtilBillsProofOfPayment = async (locationID: string, formData
.updateOne(
{ _id: locationID },
{ $set: {
utilBillsProofOfPaymentAttachment: attachment,
utilBillsProofOfPaymentUploadedAt: new Date()
utilBillsProofOfPayment: {
...attachment
},
} }
);