Add seenByTenant tracking feature

- Add seenByTenant field to BillingLocation interface
- Implement setSeenByTenant function to mark locations as viewed by tenant
  - Checks if flag is already set to avoid unnecessary DB updates
  - Includes TypeDoc documentation
- Update LocationViewPage to call setSeenByTenant when non-owner visits
- Add seenByTenant to fetchAllLocations projection
- Update LocationCard to show "seen by tenant" status indicator
  - Displays in "Monthly statement" fieldset with checkmark icon
  - Shows alongside monthly expense total
- Add localization strings for monthly statement and seen status

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-11-18 23:51:23 +01:00
parent 3540ef596b
commit cbd13cbae3
6 changed files with 79 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
import { ViewLocationCard } from '@/app/ui/ViewLocationCard';
import { fetchLocationById } from '@/app/lib/actions/locationActions';
import { fetchLocationById, setSeenByTenant } from '@/app/lib/actions/locationActions';
import { notFound } from 'next/navigation';
import { myAuth } from '@/app/lib/auth';
export default async function LocationViewPage({ locationId }: { locationId:string }) {
const location = await fetchLocationById(locationId);
@@ -9,5 +10,14 @@ export default async function LocationViewPage({ locationId }: { locationId:stri
return(notFound());
}
// Check if the page was accessed by an authenticated user who is the owner
const session = await myAuth();
const isOwner = session?.user?.id === location.userId;
// If the page is not visited by the owner, mark it as seen by tenant
if (!isOwner) {
await setSeenByTenant(locationId);
}
return (<ViewLocationCard location={location} />);
}

View File

@@ -411,6 +411,7 @@ export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:nu
"yearMonth.year": 1,
"yearMonth.month": 1,
"bills": 1,
"seenByTenant": 1,
// "bills.attachment": 0,
// "bills.notes": 0,
// "bills.barcodeImage": 1,
@@ -529,4 +530,36 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati
message: null,
errors: undefined,
};
})
})
/**
* Sets the `seenByTenant` flag to true for a specific location.
*
* This function marks a location as viewed by the tenant. It first checks if the flag
* is already set to true to avoid unnecessary database updates.
*
* @param {string} locationID - The ID of the location to update
* @returns {Promise<void>}
*
* @example
* await setSeenByTenant("507f1f77bcf86cd799439011");
*/
export const setSeenByTenant = async (locationID: string): Promise<void> => {
const dbClient = await getDbClient();
// First check if the location exists and if seenByTenant is already true
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne({ _id: locationID });
// If location doesn't exist or seenByTenant is already true, no update needed
if (!location || location.seenByTenant === true) {
return;
}
// Update the location to mark it as seen by tenant
await dbClient.collection<BillingLocation>("lokacije")
.updateOne(
{ _id: locationID },
{ $set: { seenByTenant: true } }
);
}

View File

@@ -63,6 +63,8 @@ export interface BillingLocation {
rentDueDay?: number | null;
/** (optional) monthly rent amount in cents */
rentAmount?: number | null;
/** (optional) whether the location has been seen by tenant */
seenByTenant?: boolean | null;
};
export enum BilledTo {

View File

@@ -1,6 +1,6 @@
'use client';
import { Cog8ToothIcon, PlusCircleIcon, ShareIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon, Cog8ToothIcon, PlusCircleIcon, ShareIcon, BanknotesIcon } from "@heroicons/react/24/outline";
import { FC } from "react";
import { BillBadge } from "./BillBadge";
import { BillingLocation } from "../lib/db-types";
@@ -14,7 +14,10 @@ export interface LocationCardProps {
location: BillingLocation
}
export const LocationCard:FC<LocationCardProps> = ({location: { _id, name, yearMonth, bills }}) => {
export const LocationCard:FC<LocationCardProps> = ({location}) => {
const { _id, name, yearMonth, bills, seenByTenant } = location;
console.log("seenByTenant:", seenByTenant);
const t = useTranslations("home-page.location-card");
const currentLocale = useLocale();
@@ -46,12 +49,27 @@ export const LocationCard:FC<LocationCardProps> = ({location: { _id, name, yearM
<PlusCircleIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline-block" /><span className="text-xs ml-[0.2rem] mr-[3rem]">{t("add-bill-button-tooltip")}</span>
</Link>
</div>
{
monthlyExpense > 0 ?
<p>
{ t("payed-total-label") } <strong>${formatCurrency(monthlyExpense)}</strong>
</p>
: null
{ monthlyExpense > 0 || seenByTenant ?
<fieldset className="card card-compact card-bordered border-1 border-neutral p-3 mt-2 mr-20">
<legend className="fieldset-legend px-2 text-sm font-semibold uppercase">{t("monthly-statement-legend")}</legend>
{
monthlyExpense > 0 ?
<div className="flex items-center gap-2">
<BanknotesIcon className="h-5 w-5" />
{ t("payed-total-label") } <strong>${formatCurrency(monthlyExpense)}</strong>
</div>
: null
}
{seenByTenant && (
<div className="flex items-center gap-2 mt-2">
<CheckCircleIcon className="h-5 w-5 text-success" />
<span className="text-sm">{t("seen-by-tenant-label")}</span>
</div>
)}
</fieldset> : null
}
<ShareIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline-block hover:text-red-500" title="create sharable link" style={{ position: "absolute", bottom: ".6em", right: "1.2em" }} onClick={handleCopyLinkClick} />

View File

@@ -55,7 +55,9 @@
"edit-card-tooltip": "Edit realestate",
"add-bill-button-tooltip": "Add a new bill",
"payed-total-label": "Payed total:",
"link-copy-message": "Link copied to clipboard"
"link-copy-message": "Link copied to clipboard",
"monthly-statement-legend": "Monthly statement",
"seen-by-tenant-label": "Seen by tenant"
},
"month-card": {
"payed-total-label": "Total monthly expenditure:",

View File

@@ -55,7 +55,9 @@
"edit-card-tooltip": "Izmjeni nekretninu",
"add-bill-button-tooltip": "Dodaj novi račun",
"payed-total-label": "Ukupno plaćeno:",
"link-copy-message": "Link kopiran na clipboard"
"link-copy-message": "Link kopiran na clipboard",
"monthly-statement-legend": "Obračun",
"seen-by-tenant-label": "Viđeno od strane podstanara"
},
"month-card": {
"payed-total-label": "Ukupni mjesečni trošak:",