Add utility bills proof of payment file upload functionality
Changes: - Updated BillingLocation interface: - Added utilBillsProofOfPaymentAttachment field (BillAttachment type) - Added server action uploadUtilBillsProofOfPayment: - Validates PDF file type - Serializes file attachment to base64 - Stores attachment in BillingLocation document - Returns success/error status - Updated ViewLocationCard component: - Added file upload input with PDF-only accept - Implemented handleFileChange with immediate upload - Added upload state management (isUploading, uploadError, attachment) - Shows spinner while uploading - Input disabled during upload - Conditionally renders file input or download link - Link displayed after successful upload - Created route handler for serving proof of payment PDFs: - GET /share/proof-of-payment/[id]/route.tsx - Fetches attachment from database - Converts base64 to binary - Returns PDF with proper headers - Added not-found page for proof of payment route - Updated middleware to include proof-of-payment in public pages - Added translations: - en: "Upload proof of payment (PDF only)" - hr: "Priložite potvrdu o uplati:" File uploads immediately on selection without page reload. Only PDF files accepted with client and server-side validation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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<ViewLocationCardProps> = ({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<string | null>(null);
|
||||
const [attachment, setAttachment] = useState(utilBillsProofOfPaymentAttachment);
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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<ViewLocationCardProps> = ({location, userSettin
|
||||
</>
|
||||
: null
|
||||
}
|
||||
<Link href={`/proof-of-payment/locationID/`} target="_blank" className='text-center w-full max-w-[20em] text-nowrap truncate inline-block mt-4'>
|
||||
<DocumentIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1" />
|
||||
utility-bills-proof-of-payment.pdf
|
||||
{/*decodeURIComponent(utilBillsProofOfPaymentAttachment.fileName)*/}
|
||||
</Link>
|
||||
<div className="form-control w-full mb-4">
|
||||
<label className="label">
|
||||
<span className="label-text">{t("upload-proof-of-payment-label")}</span>
|
||||
</label>
|
||||
<input id="utilBillsProofOfPaymentAttachment" name="utilBillsProofOfPaymentAttachment" type="file" className="file-input file-input-bordered grow file-input-s my-2 block max-w-[17em] md:max-w-[80em] break-words" />
|
||||
</div>
|
||||
{attachment ? (
|
||||
<div className="mt-4">
|
||||
<Link
|
||||
href={`/share/proof-of-payment/${_id}/`}
|
||||
target="_blank"
|
||||
className='text-center w-full max-w-[20em] text-nowrap truncate inline-block'
|
||||
>
|
||||
<DocumentIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1" />
|
||||
{decodeURIComponent(attachment.fileName)}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-control w-full mb-4 mt-4">
|
||||
<label className="label">
|
||||
<span className="label-text">{t("upload-proof-of-payment-label")}</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="utilBillsProofOfPaymentAttachment"
|
||||
name="utilBillsProofOfPaymentAttachment"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
className="file-input file-input-bordered grow file-input-sm my-2 block max-w-[17em] md:max-w-[80em] break-words"
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
{isUploading && (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
</div>
|
||||
{uploadError && (
|
||||
<p className="text-sm text-red-500 mt-1">{uploadError}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
Reference in New Issue
Block a user