Added cache revalidation to ensure ViewLocationCard reflects uploaded proof of payment when navigating back from ViewBillCard: - Server-side: Added revalidatePath() to upload actions in billActions and locationActions to invalidate Next.js server cache - Client-side: Added router.refresh() calls in ViewBillCard and ViewLocationCard to refresh client router cache after successful upload This maintains the current UX (no redirect on upload) while ensuring fresh data is displayed on navigation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
169 lines
8.9 KiB
TypeScript
169 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import { TicketIcon, CheckCircleIcon, XCircleIcon, DocumentIcon } from "@heroicons/react/24/outline";
|
|
import { Bill, BillingLocation } from "../lib/db-types";
|
|
import { FC, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { formatYearMonth } from "../lib/format";
|
|
import { useTranslations } from "next-intl";
|
|
import { Pdf417Barcode } from "./Pdf417Barcode";
|
|
import { uploadProofOfPayment } from "../lib/actions/billActions";
|
|
|
|
export interface ViewBillCardProps {
|
|
location: BillingLocation,
|
|
bill: Bill,
|
|
}
|
|
|
|
export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill }) => {
|
|
|
|
const router = useRouter();
|
|
const t = useTranslations("bill-edit-form");
|
|
|
|
const { _id: billID, name, paid, attachment, notes, payedAmount, hub3aText, proofOfPayment } = bill ?? { _id: undefined, name: "", paid: false, notes: "" };
|
|
const { _id: locationID, proofOfPaymentType } = location;
|
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
const [proofOfPaymentUploadedAt, setProofOfPaymentUploadedAt] = useState<Date | null>(proofOfPayment?.uploadedAt ?? null);
|
|
const [proofOfPaymentFilename, setProofOfPaymentFilename] = useState(proofOfPayment?.fileName);
|
|
|
|
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('proofOfPayment', file);
|
|
|
|
const result = await uploadProofOfPayment(locationID, billID as string, formData);
|
|
|
|
if (result.success) {
|
|
setProofOfPaymentFilename(file.name);
|
|
setProofOfPaymentUploadedAt(new Date());
|
|
router.refresh();
|
|
} else {
|
|
setUploadError(result.error || 'Upload failed');
|
|
}
|
|
} catch (error: any) {
|
|
setUploadError(error.message || 'Upload failed');
|
|
} finally {
|
|
setIsUploading(false);
|
|
e.target.value = ''; // Reset input
|
|
}
|
|
};
|
|
|
|
|
|
return (
|
|
<div className="card card-compact card-bordered bg-base-100 shadow-s">
|
|
<div className="card-body">
|
|
<h2 className="card-title">{`${formatYearMonth(location.yearMonth)} ${location.name}`}</h2>
|
|
<span className="textarea textarea-bordered max-w-[400px] w-full grow">
|
|
<h3 className="text-xl dark:text-neutral-300">{name}</h3>
|
|
</span>
|
|
<p className={`flex textarea textarea-bordered max-w-[400px] w-full block ${paid ? "bg-green-950" : "bg-red-950"}`}>
|
|
<span className="font-bold uppercase">{t("paid-checkbox")}</span>
|
|
<span className="text-right inline-block grow">{paid ? <CheckCircleIcon className="h-[1em] w-[1em] ml-[.5em] text-2xl inline-block text-green-500" /> : <XCircleIcon className="h-[1em] w-[1em] text-2xl inline-block text-red-500" />}</span>
|
|
</p>
|
|
|
|
<p className="flex textarea textarea-bordered max-w-[400px] w-full block">
|
|
<span className="font-bold uppercase">{t("payed-amount")}</span>
|
|
<span className="text-right inline-block grow">{payedAmount ? payedAmount / 100 : ""}</span>
|
|
</p>
|
|
{
|
|
notes ?
|
|
<span className="textarea textarea-bordered max-w-[400px] w-full grow">
|
|
<p className="font-bold uppercase">{t("notes-placeholder")}</p>
|
|
<p className="leading-[1.4em]">
|
|
{notes}
|
|
</p>
|
|
</span>
|
|
: null
|
|
}
|
|
{
|
|
attachment ?
|
|
<span className="textarea textarea-bordered max-w-[400px] w-full grow">
|
|
<p className="font-bold uppercase">{t("attachment")}</p>
|
|
<Link href={`/share/attachment/${locationID}-${billID}/`} target="_blank" className='text-center w-full max-w-[20em] text-nowrap truncate inline-block mt-2'>
|
|
<DocumentIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1" />
|
|
{decodeURIComponent(attachment.fileName)}
|
|
</Link>
|
|
</span>
|
|
: null
|
|
}
|
|
{
|
|
hub3aText ?
|
|
<div className="form-control p-1">
|
|
<label className="label p-2 grow bg-white border border-gray-300 rounded-box justify-center">
|
|
<Pdf417Barcode hub3aText={hub3aText} />
|
|
</label>
|
|
<p className="text-xs my-1">{t.rich('barcode-disclaimer', { br: () => <br /> })}</p>
|
|
</div> : null
|
|
}
|
|
{
|
|
// IF proof of payment type is "per-bill", show upload fieldset
|
|
proofOfPaymentType === "per-bill" &&
|
|
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pb-2 pt-0 mt-2">
|
|
<legend className="fieldset-legend font-semibold uppercase">{t("upload-proof-of-payment-legend")}</legend>
|
|
{
|
|
// IF proof of payment was uploaded
|
|
proofOfPaymentUploadedAt ? (
|
|
// IF file name is available, show link to download
|
|
// ELSE it's not available that means that the uploaded file was purged by housekeeping
|
|
// -> don't show anything
|
|
proofOfPaymentFilename ? (
|
|
<div className="mt-3 ml-[-.5rem]">
|
|
<Link
|
|
href={`/share/proof-of-payment/per-bill/${locationID}-${billID}/`}
|
|
target="_blank"
|
|
className='text-center w-full max-w-[20rem] text-nowrap truncate inline-block'
|
|
>
|
|
<TicketIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1" />
|
|
{ decodeURIComponent(proofOfPaymentFilename) }
|
|
</Link>
|
|
</div>
|
|
) : null
|
|
) : /* ELSE show upload input */ (
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text">{t("upload-proof-of-payment-label")}</span>
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
id="proofOfPayment"
|
|
name="proofOfPayment"
|
|
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>
|
|
)}
|
|
</fieldset>
|
|
}
|
|
|
|
<div className="text-right">
|
|
<Link className="btn btn-neutral ml-3" href={`/share/location/${locationID}`}>{t("back-button")}</Link>
|
|
</div>
|
|
|
|
</div>
|
|
</div>);
|
|
} |