feat: add separate unpaid and paid bill totals to location cards
- Display both unpaid and paid bill amounts in LocationCard and MonthCard - Rename variables for clarity: totalUnpaid, totalPayed, unpaidTotal, payedTotal - ViewLocationCard uses totalAmount for tenant bills (regardless of payment status) - Update Croatian translations: "Ukupno neplaćeno" (unpaid), "Ukupno plaćeno" (paid) - Add ShoppingCartIcon for unpaid amounts, BanknotesIcon for paid amounts - Update HomePage to calculate and pass both totals to month cards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,8 @@ export const HomePage:FC<HomePageProps> = async ({ searchParams }) => {
|
|||||||
[key]: {
|
[key]: {
|
||||||
yearMonth: location.yearMonth,
|
yearMonth: location.yearMonth,
|
||||||
locations: [...locationsInMonth.locations, location],
|
locations: [...locationsInMonth.locations, location],
|
||||||
monthlyExpense: locationsInMonth.monthlyExpense + location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
|
unpaidTotal: locationsInMonth.unpaidTotal + location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0),
|
||||||
|
payedTotal: locationsInMonth.payedTotal + location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -61,13 +62,15 @@ export const HomePage:FC<HomePageProps> = async ({ searchParams }) => {
|
|||||||
[key]: {
|
[key]: {
|
||||||
yearMonth: location.yearMonth,
|
yearMonth: location.yearMonth,
|
||||||
locations: [location],
|
locations: [location],
|
||||||
monthlyExpense: location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
|
unpaidTotal: location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0),
|
||||||
|
payedTotal: location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, {} as {[key:string]:{
|
}, {} as {[key:string]:{
|
||||||
yearMonth: YearMonth,
|
yearMonth: YearMonth,
|
||||||
locations: BillingLocation[],
|
locations: BillingLocation[],
|
||||||
monthlyExpense: number
|
unpaidTotal: number,
|
||||||
|
payedTotal: number
|
||||||
} });
|
} });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { CheckCircleIcon, Cog8ToothIcon, PlusCircleIcon, ShareIcon, BanknotesIcon, EyeIcon, TicketIcon } from "@heroicons/react/24/outline";
|
import { CheckCircleIcon, Cog8ToothIcon, PlusCircleIcon, ShareIcon, BanknotesIcon, EyeIcon, TicketIcon, ShoppingCartIcon } from "@heroicons/react/24/outline";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { BillBadge } from "./BillBadge";
|
import { BillBadge } from "./BillBadge";
|
||||||
import { BillingLocation } from "../lib/db-types";
|
import { BillingLocation } from "../lib/db-types";
|
||||||
@@ -9,7 +9,6 @@ import { formatCurrency } from "../lib/formatStrings";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { get } from "http";
|
|
||||||
import { generateShareLink } from "../lib/actions/locationActions";
|
import { generateShareLink } from "../lib/actions/locationActions";
|
||||||
|
|
||||||
export interface LocationCardProps {
|
export interface LocationCardProps {
|
||||||
@@ -31,8 +30,9 @@ export const LocationCard: FC<LocationCardProps> = ({ location, currency }) => {
|
|||||||
const t = useTranslations("home-page.location-card");
|
const t = useTranslations("home-page.location-card");
|
||||||
const currentLocale = useLocale();
|
const currentLocale = useLocale();
|
||||||
|
|
||||||
// sum all the paid bill amounts (regardless of who pays)
|
// sum all the unpaid and paid bill amounts (regardless of who pays)
|
||||||
const monthlyExpense = bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0);
|
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);
|
||||||
|
|
||||||
const handleCopyLinkClick = async () => {
|
const handleCopyLinkClick = async () => {
|
||||||
// copy URL to clipboard
|
// copy URL to clipboard
|
||||||
@@ -69,17 +69,27 @@ export const LocationCard: FC<LocationCardProps> = ({ location, currency }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
<ShareIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline hover:text-red-500" title="create sharable link" onClick={handleCopyLinkClick} />
|
<ShareIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline hover:text-red-500" title="create sharable link" onClick={handleCopyLinkClick} />
|
||||||
</div>
|
</div>
|
||||||
{ monthlyExpense > 0 || seenByTenantAt || utilBillsProofOfPayment?.uploadedAt ?
|
{ totalUnpaid > 0 || totalPayed > 0 || seenByTenantAt || utilBillsProofOfPayment?.uploadedAt ?
|
||||||
<>
|
<>
|
||||||
<div className="flex ml-1">
|
<div className="flex ml-1">
|
||||||
<div className="divider divider-horizontal p-0 m-0"></div>
|
<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">
|
<div className="card rounded-box grid grow place-items-left place-items-top p-0">
|
||||||
{
|
{
|
||||||
monthlyExpense > 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")} <strong>{formatCurrency(totalUnpaid, currency ?? "EUR")}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
totalPayed > 0 ?
|
||||||
<div className="flex ml-1">
|
<div className="flex ml-1">
|
||||||
<span className="w-5 min-w-5 mr-2"><BanknotesIcon className="mt-[.1rem]" /></span>
|
<span className="w-5 min-w-5 mr-2"><BanknotesIcon className="mt-[.1rem]" /></span>
|
||||||
<span>
|
<span>
|
||||||
{t("payed-total-label")} <strong>{formatCurrency(monthlyExpense, currency ?? "EUR")}</strong>
|
{t("total-payed-label")} <strong>{formatCurrency(totalPayed, currency ?? "EUR")}</strong>
|
||||||
<CheckCircleIcon className="h-5 w-5 ml-1 mt-[-.2rem] text-success inline-block" />
|
<CheckCircleIcon className="h-5 w-5 ml-1 mt-[-.2rem] text-success inline-block" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import { useTranslations } from "next-intl";
|
|||||||
export interface MonthCardProps {
|
export interface MonthCardProps {
|
||||||
yearMonth: YearMonth,
|
yearMonth: YearMonth,
|
||||||
children?: React.ReactNode,
|
children?: React.ReactNode,
|
||||||
monthlyExpense:number,
|
unpaidTotal: number,
|
||||||
|
payedTotal: number,
|
||||||
currency?: string | null,
|
currency?: string | null,
|
||||||
expanded?:boolean,
|
expanded?:boolean,
|
||||||
onToggle: (yearMonth:YearMonth) => void
|
onToggle: (yearMonth:YearMonth) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MonthCard:FC<MonthCardProps> = ({ yearMonth, children, monthlyExpense, currency, expanded, onToggle }) => {
|
export const MonthCard:FC<MonthCardProps> = ({ yearMonth, children, unpaidTotal, payedTotal, currency, expanded, onToggle }) => {
|
||||||
|
|
||||||
const elRef = useRef<HTMLDivElement>(null);
|
const elRef = useRef<HTMLDivElement>(null);
|
||||||
const t = useTranslations("home-page.month-card");
|
const t = useTranslations("home-page.month-card");
|
||||||
@@ -37,9 +38,15 @@ export const MonthCard:FC<MonthCardProps> = ({ yearMonth, children, monthlyExpen
|
|||||||
<div className={`collapse-title text-xl font-medium ${expanded ? "text-white" : ""}`}>
|
<div className={`collapse-title text-xl font-medium ${expanded ? "text-white" : ""}`}>
|
||||||
{`${formatYearMonth(yearMonth)}`}
|
{`${formatYearMonth(yearMonth)}`}
|
||||||
{
|
{
|
||||||
monthlyExpense>0 ?
|
unpaidTotal>0 ?
|
||||||
<p className="text-xs font-medium">
|
<p className="text-xs font-medium">
|
||||||
{t("payed-total-label")} <strong>{ formatCurrency(monthlyExpense, currency ?? "EUR") }</strong>
|
{t("total-due-label")} <strong>{ formatCurrency(unpaidTotal, currency ?? "EUR") }</strong>
|
||||||
|
</p> : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
payedTotal>0 ?
|
||||||
|
<p className="text-xs font-medium">
|
||||||
|
{t("total-payed-label")} <strong>{ formatCurrency(payedTotal, currency ?? "EUR") }</strong>
|
||||||
</p> : null
|
</p> : null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export interface MonthLocationListProps {
|
|||||||
[key: string]: {
|
[key: string]: {
|
||||||
yearMonth: YearMonth;
|
yearMonth: YearMonth;
|
||||||
locations: BillingLocation[];
|
locations: BillingLocation[];
|
||||||
monthlyExpense: number;
|
payedTotal: number;
|
||||||
|
unpaidTotal: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
userSettings?: UserSettings | null;
|
userSettings?: UserSettings | null;
|
||||||
@@ -93,7 +94,7 @@ export const MonthLocationList:React.FC<MonthLocationListProps > = ({
|
|||||||
|
|
||||||
return(
|
return(
|
||||||
<>
|
<>
|
||||||
<MonthCard yearMonth={currentYearMonth} key={`month-${currentYearMonth}`} monthlyExpense={0} currency={userSettings?.currency} onToggle={() => {}} expanded={true} >
|
<MonthCard yearMonth={currentYearMonth} key={`month-${currentYearMonth}`} unpaidTotal={0} payedTotal={0} currency={userSettings?.currency} onToggle={() => {}} expanded={true} >
|
||||||
<AddLocationButton yearMonth={currentYearMonth} />
|
<AddLocationButton yearMonth={currentYearMonth} />
|
||||||
</MonthCard>
|
</MonthCard>
|
||||||
</>)
|
</>)
|
||||||
@@ -117,8 +118,8 @@ export const MonthLocationList:React.FC<MonthLocationListProps > = ({
|
|||||||
return(<>
|
return(<>
|
||||||
<AddMonthButton yearMonth={getNextYearMonth(monthsArray[0][1].locations[0].yearMonth)} />
|
<AddMonthButton yearMonth={getNextYearMonth(monthsArray[0][1].locations[0].yearMonth)} />
|
||||||
{
|
{
|
||||||
monthsArray.map(([monthKey, { yearMonth, locations, monthlyExpense }], monthIx) =>
|
monthsArray.map(([monthKey, { yearMonth, locations, unpaidTotal, payedTotal }], monthIx) =>
|
||||||
<MonthCard yearMonth={yearMonth} key={`month-${monthKey}`} monthlyExpense={monthlyExpense} currency={userSettings?.currency} expanded={ yearMonth.month === expandedMonth } onToggle={handleMonthToggle} >
|
<MonthCard yearMonth={yearMonth} key={`month-${monthKey}`} unpaidTotal={unpaidTotal} payedTotal={payedTotal} currency={userSettings?.currency} expanded={ yearMonth.month === expandedMonth } onToggle={handleMonthToggle} >
|
||||||
{
|
{
|
||||||
yearMonth.month === expandedMonth ?
|
yearMonth.month === expandedMonth ?
|
||||||
locations.map((location, ix) => <LocationCard key={`location-${location._id}`} location={location} currency={userSettings?.currency} />)
|
locations.map((location, ix) => <LocationCard key={`location-${location._id}`} location={location} currency={userSettings?.currency} />)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
};
|
};
|
||||||
|
|
||||||
// sum all the billAmounts (only for bills billed to tenant)
|
// 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);
|
const totalAmount = bills.reduce((acc, bill) => (bill.billedTo ?? BilledTo.Tenant) === BilledTo.Tenant ? acc + (bill.payedAmount ?? 0) : acc, 0);
|
||||||
|
|
||||||
const { hub3aText, paymentParams } = useMemo(() => {
|
const { hub3aText, paymentParams } = useMemo(() => {
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
const locationNameTrimmed_max20 = locationName.trimEnd().trimEnd().substring(0, 19);
|
const locationNameTrimmed_max20 = locationName.trimEnd().trimEnd().substring(0, 19);
|
||||||
|
|
||||||
const paymentParams: PaymentParams = {
|
const paymentParams: PaymentParams = {
|
||||||
Iznos: (monthlyExpense / 100).toFixed(2).replace(".", ","),
|
Iznos: (totalAmount / 100).toFixed(2).replace(".", ","),
|
||||||
ImePlatitelja: tenantName ?? "",
|
ImePlatitelja: tenantName ?? "",
|
||||||
AdresaPlatitelja: tenantStreet ?? "",
|
AdresaPlatitelja: tenantStreet ?? "",
|
||||||
SjedistePlatitelja: tenantTown ?? "",
|
SjedistePlatitelja: tenantTown ?? "",
|
||||||
@@ -132,9 +132,9 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
monthlyExpense > 0 ?
|
totalAmount > 0 ?
|
||||||
<p className="text-[1.2rem]">
|
<p className="text-[1.2rem]">
|
||||||
{t("payed-total-label")} <strong>{formatCurrency(monthlyExpense, userSettings?.currency)}</strong>
|
{t("total-due-label")} <strong>{formatCurrency(totalAmount, userSettings?.currency)}</strong>
|
||||||
</p>
|
</p>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
userSettings?.enableRevolutPayment && tenantPaymentMethod === "revolut" ? (() => {
|
userSettings?.enableRevolutPayment && tenantPaymentMethod === "revolut" ? (() => {
|
||||||
const revolutPaymentUrl = `https://revolut.me/${userSettings.ownerRevolutProfileName?.replace('@', '')}?amount=${(monthlyExpense).toFixed(0)}¤cy=${userSettings.currency}`;
|
const revolutPaymentUrl = `https://revolut.me/${userSettings.ownerRevolutProfileName?.replace('@', '')}?amount=${(totalAmount).toFixed(0)}¤cy=${userSettings.currency}`;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="max-w-[25em] ml-1 mt-1 mb-1">{t("payment-info-header")}</p>
|
<p className="max-w-[25em] ml-1 mt-1 mb-1">{t("payment-info-header")}</p>
|
||||||
|
|||||||
@@ -61,7 +61,8 @@
|
|||||||
"location-card": {
|
"location-card": {
|
||||||
"edit-card-tooltip": "Edit realestate",
|
"edit-card-tooltip": "Edit realestate",
|
||||||
"add-bill-button-tooltip": "Add a new bill",
|
"add-bill-button-tooltip": "Add a new bill",
|
||||||
"payed-total-label": "Payed total:",
|
"total-due-label": "Total due:",
|
||||||
|
"total-payed-label": "Total payed:",
|
||||||
"link-copy-message": "Link copied to clipboard",
|
"link-copy-message": "Link copied to clipboard",
|
||||||
"monthly-statement-legend": "Monthly statement",
|
"monthly-statement-legend": "Monthly statement",
|
||||||
"seen-by-tenant-label": "seen by tenant",
|
"seen-by-tenant-label": "seen by tenant",
|
||||||
@@ -81,7 +82,8 @@
|
|||||||
"revolut-link-text": "Pay with Revolut"
|
"revolut-link-text": "Pay with Revolut"
|
||||||
},
|
},
|
||||||
"month-card": {
|
"month-card": {
|
||||||
"payed-total-label": "Total monthly expenditure:",
|
"total-due-label": "Monthly due total:",
|
||||||
|
"total-payed-label": "Monthly payed total:",
|
||||||
"print-codes-tooltip": "Print 2D codes",
|
"print-codes-tooltip": "Print 2D codes",
|
||||||
"print-codes-label": "Print codes"
|
"print-codes-label": "Print codes"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,7 +61,8 @@
|
|||||||
"location-card": {
|
"location-card": {
|
||||||
"edit-card-tooltip": "Izmjeni nekretninu",
|
"edit-card-tooltip": "Izmjeni nekretninu",
|
||||||
"add-bill-button-tooltip": "Dodaj novi račun",
|
"add-bill-button-tooltip": "Dodaj novi račun",
|
||||||
"payed-total-label": "Ukupno plaćeno:",
|
"total-due-label": "Ukupno neplaćeno:",
|
||||||
|
"total-payed-label": "Ukupno plaćeno:",
|
||||||
"link-copy-message": "Link kopiran na clipboard",
|
"link-copy-message": "Link kopiran na clipboard",
|
||||||
"monthly-statement-legend": "Obračun",
|
"monthly-statement-legend": "Obračun",
|
||||||
"seen-by-tenant-label": "viđeno od strane podstanara",
|
"seen-by-tenant-label": "viđeno od strane podstanara",
|
||||||
@@ -81,7 +82,8 @@
|
|||||||
"revolut-link-text": "Plati pomoću Revoluta"
|
"revolut-link-text": "Plati pomoću Revoluta"
|
||||||
},
|
},
|
||||||
"month-card": {
|
"month-card": {
|
||||||
"payed-total-label": "Ukupni mjesečni trošak:",
|
"total-due-label": "Ukupno neplaćeno u mjesecu:",
|
||||||
|
"total-payed-label": "Ukupno plaćeno u mjesecu:",
|
||||||
"print-codes-tooltip": "Ispis 2d kodova",
|
"print-codes-tooltip": "Ispis 2d kodova",
|
||||||
"print-codes-label": "Ispis kodova"
|
"print-codes-label": "Ispis kodova"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user