Files
evidencija-rezija/web-app/app/ui/LocationCard.tsx
Nikola Derežić a3ec20544c (refactor) Move generateShareId to locationActions and apply to LocationCard
- Moved generateShareId from shareChecksum.ts to locationActions.ts as a server action
- Updated LocationCard to use shareID with checksum for proof of payment download link
- Replaced Link with AsyncLink to handle async shareID generation
- Commented out debug console.log in Pdf417Barcode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 18:18:17 +01:00

160 lines
9.1 KiB
TypeScript

'use client';
import { CheckCircleIcon, Cog8ToothIcon, PlusCircleIcon, ShareIcon, BanknotesIcon, EyeIcon, TicketIcon, ShoppingCartIcon, EnvelopeIcon, ExclamationTriangleIcon, ClockIcon } from "@heroicons/react/24/outline";
import { FC, useEffect, useState } from "react";
import { BillBadge } from "./BillBadge";
import { BillingLocation, EmailStatus } from "../lib/db-types";
import { formatYearMonth } from "../lib/format";
import { formatCurrency } from "../lib/formatStrings";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { toast } from "react-toastify";
import { generateShareId, generateShareLink } from "../lib/actions/locationActions";
import { AsyncLink } from "./AsyncLink";
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,
tenantEmail,
tenantEmailStatus,
} = location;
const t = useTranslations("home-page.location-card");
// sum all the unpaid and paid bill amounts (regardless of who pays)
const totalUnpaid = bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0);
const totalPayed = bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0);
/**
* Share ID which can be used in shareable links
* Note: not to be used in share button directly since `generateShareLink` sets sharing TTL in the DB
* */
const [shareID, setShareID] = useState<string>("not-yet-generated");
useEffect(() => {
// share ID can be generated server-side since it requires a secret key
// which we don't want to expose to the client
(async () => setShareID(await generateShareId(_id)))();
}, [_id]);
const handleCopyLinkClick = async () => {
// copy URL to clipboard
const shareLink = await generateShareLink(_id);
if(shareLink.error) {
toast.error(shareLink.error, { theme: "dark" });
} else {
navigator.clipboard.writeText(shareLink.shareUrl as string);
toast.success(t("link-copy-message"), { theme: "dark" });
}
}
return (
<div data-key={_id} className="card card-compact card-bordered sm:max-w-[35em] 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>
{ totalUnpaid > 0 || totalPayed > 0 || seenByTenantAt || utilBillsProofOfPayment?.uploadedAt || (tenantEmail && tenantEmailStatus && tenantEmailStatus !== EmailStatus.Verified) ?
<>
<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">
{
totalUnpaid > 0 ?
<div className="flex ml-1">
<span className="w-5 min-w-5 mr-2"><ShoppingCartIcon className="mt-[.1rem]" /></span>
<span>
{t("total-due-label")}&nbsp;<strong>{formatCurrency(totalUnpaid, currency ?? "EUR")}</strong>
</span>
</div>
: null
}
{
totalPayed > 0 ?
<div className="flex ml-1">
<span className="w-5 min-w-5 mr-2"><BanknotesIcon className="mt-[.1rem]" /></span>
<span>
{t("total-payed-label")}&nbsp;<strong>{formatCurrency(totalPayed, currency ?? "EUR")}</strong>
<CheckCircleIcon className="h-5 w-5 ml-1 mt-[-.2rem] text-success inline-block" />
</span>
</div>
: null
}
{tenantEmail && tenantEmailStatus && tenantEmailStatus !== EmailStatus.Verified && (
<div className="flex ml-1">
<span className="w-5 min-w-5 mr-2">
{tenantEmailStatus === EmailStatus.Unverified && <ExclamationTriangleIcon className="mt-[.1rem] text-warning" />}
{tenantEmailStatus === EmailStatus.VerificationPending && <ClockIcon className="mt-[.1rem] text-info" />}
{tenantEmailStatus === EmailStatus.Unsubscribed && <EnvelopeIcon className="mt-[.1rem] text-error" />}
</span>
<span className={
tenantEmailStatus === EmailStatus.Unverified ? "text-warning" :
tenantEmailStatus === EmailStatus.VerificationPending ? "text-info" :
"text-error"
}>
{tenantEmailStatus === EmailStatus.Unverified && `${t("email-status.unverified")}`}
{tenantEmailStatus === EmailStatus.VerificationPending && `${t("email-status.verification-pending")}`}
{tenantEmailStatus === EmailStatus.Unsubscribed && `${t("email-status.unsubscribed")}`}
</span>
</div>
)}
{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 && (
<AsyncLink
href={`/share/proof-of-payment/combined/${shareID}`}
target="_blank"
className="flex mt-1 ml-1"
disabled={!shareID} >
<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>
</AsyncLink>
)}
</div>
</div>
</> : null
}
</div>
</div>);
};