Files
evidencija-rezija/app/ui/LocationCard.tsx
Knee Cola aa573c68a3 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>
2025-12-07 13:11:17 +01:00

112 lines
5.8 KiB
TypeScript

'use client';
import { CheckCircleIcon, Cog8ToothIcon, PlusCircleIcon, ShareIcon, BanknotesIcon, EyeIcon, TicketIcon } from "@heroicons/react/24/outline";
import { FC } from "react";
import { BillBadge } from "./BillBadge";
import { BillingLocation } from "../lib/db-types";
import { formatYearMonth } from "../lib/format";
import { formatCurrency } from "../lib/formatStrings";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import { toast } from "react-toastify";
import { get } from "http";
export interface LocationCardProps {
location: BillingLocation;
currency?: string | null;
}
export const LocationCard: FC<LocationCardProps> = ({ location, currency }) => {
const {
_id,
name,
yearMonth,
bills,
seenByTenantAt,
// NOTE: only the fileName is projected from the DB to reduce data transfer
utilBillsProofOfPayment,
} = location;
const t = useTranslations("home-page.location-card");
const currentLocale = useLocale();
// sum all the paid bill amounts (regardless of who pays)
const monthlyExpense = bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0);
const handleCopyLinkClick = () => {
// copy URL to clipboard
const url = `${window.location.origin}/${currentLocale}/share/location/${_id}`;
navigator.clipboard.writeText(url);
// use NextJS toast to notiy user that the link was copied
toast.success(t("link-copy-message"), { theme: "dark" });
}
return (
<div data-key={_id} className="card card-compact card-bordered max-w-[30em] bg-base-100 border-1 border-neutral my-1">
<div className="card-body">
<Link href={`/home/location/${_id}/edit`} className="card-subtitle tooltip" data-tip={t("edit-card-tooltip")}>
<Cog8ToothIcon className="h-[1em] w-[1em] absolute cursor-pointer top-[-.2rem] right-0 text-2xl" />
</Link>
<h2 className="card-title mr-[2em] mt-[-1em] text-[1rem]">{formatYearMonth(yearMonth)} {name}</h2>
{
bills.length > 0 ? (
<div className="card-actions mb-1">
{
bills.map(bill => <BillBadge key={`${_id}-${bill._id}`} locationId={_id} bill={bill} />)
}
</div>
) : null
}
<div className="flex justify-between items-center mb-0">
<Link href={`/home/bill/${_id}/add`} className="tooltip" data-tip={t("add-bill-button-tooltip")}>
<PlusCircleIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline" /><span className="text-xs ml-[0.2rem]">{t("add-bill-button-tooltip")}</span>
</Link>
<ShareIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline hover:text-red-500" title="create sharable link" onClick={handleCopyLinkClick} />
</div>
{ monthlyExpense > 0 || seenByTenantAt || utilBillsProofOfPayment?.uploadedAt ?
<>
<div className="flex ml-1">
<div className="divider divider-horizontal p-0 m-0"></div>
<div className="card rounded-box grid grow place-items-left place-items-top p-0">
{
monthlyExpense > 0 ?
<div className="flex ml-1">
<span className="w-5 min-w-5 mr-2"><BanknotesIcon className="mt-[.1rem]" /></span>
<span>
{t("payed-total-label")}&nbsp;<strong>{formatCurrency(monthlyExpense, currency ?? "EUR")}</strong>
<CheckCircleIcon className="h-5 w-5 ml-1 mt-[-.2rem] text-success inline-block" />
</span>
</div>
: null
}
{seenByTenantAt && (
<div className="flex mt-1 ml-1">
<span className="w-5 mr-2 min-w-5"><EyeIcon className="mt-[.1rem]" /></span>
<span>
<span>{t("seen-by-tenant-label")} at {seenByTenantAt.toLocaleString()}</span>
<CheckCircleIcon className="h-5 w-5 ml-1 mt-[-.2rem] text-success inline-block" />
</span>
</div>
)}
{utilBillsProofOfPayment?.uploadedAt && (
<Link
href={`/share/proof-of-payment/${_id}/`}
target="_blank"
className="flex mt-1 ml-1">
<span className="w-5 min-w-5 mr-2"><TicketIcon className="mt-[.1rem]" /></span>
<span>
<span className="underline">{t("download-proof-of-payment-label")}</span>
<CheckCircleIcon className="h-5 w-5 ml-2 mt-[-.2rem] text-success inline-block" />
</span>
</Link>
)}
</div>
</div>
</> : null
}
</div>
</div>);
};