Replace seenByTenant boolean with seenByTenantAt timestamp field

Update location tracking to record when tenant views a location rather than just whether they've seen it. This provides better audit trail and enables future features like viewing history.

Changes:
- Convert seenByTenant (boolean) to seenByTenantAt (Date) in database schema
- Update setSeenByTenantAt action to store timestamp instead of boolean flag
- Modify LocationCard UI to display when location was seen by tenant
- Update all references across locationActions, monthActions, and view components
- Remove unused imports from ViewLocationCard

🤖 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-26 20:10:18 +01:00
parent 4139e29de8
commit eed92b5ac3
6 changed files with 56 additions and 45 deletions

View File

@@ -1,5 +1,5 @@
import { ViewLocationCard } from '@/app/ui/ViewLocationCard'; import { ViewLocationCard } from '@/app/ui/ViewLocationCard';
import { fetchLocationById, setSeenByTenant } from '@/app/lib/actions/locationActions'; import { fetchLocationById, setSeenByTenantAt } from '@/app/lib/actions/locationActions';
import { getUserSettingsByUserId } from '@/app/lib/actions/userSettingsActions'; import { getUserSettingsByUserId } from '@/app/lib/actions/userSettingsActions';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { myAuth } from '@/app/lib/auth'; import { myAuth } from '@/app/lib/auth';
@@ -20,7 +20,7 @@ export default async function LocationViewPage({ locationId }: { locationId:stri
// If the page is not visited by the owner, mark it as seen by tenant // If the page is not visited by the owner, mark it as seen by tenant
if (!isOwner) { if (!isOwner) {
await setSeenByTenant(locationId); await setSeenByTenantAt(locationId);
} }
return (<ViewLocationCard location={location} userSettings={userSettings} />); return (<ViewLocationCard location={location} userSettings={userSettings} />);

View File

@@ -428,7 +428,7 @@ export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:nu
"yearMonth.year": 1, "yearMonth.year": 1,
"yearMonth.month": 1, "yearMonth.month": 1,
"bills": 1, "bills": 1,
"seenByTenant": 1, "seenByTenantAt": 1,
// "bills.attachment": 0, // "bills.attachment": 0,
// "bills.notes": 0, // "bills.notes": 0,
// "bills.hub3aText": 1, // "bills.hub3aText": 1,
@@ -555,7 +555,7 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati
}) })
/** /**
* Sets the `seenByTenant` flag to true for a specific location. * Sets the `seenByTenantAt` flag to true for a specific location.
* *
* This function marks a location as viewed by the tenant. It first checks if the flag * 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. * is already set to true to avoid unnecessary database updates.
@@ -564,17 +564,17 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati
* @returns {Promise<void>} * @returns {Promise<void>}
* *
* @example * @example
* await setSeenByTenant("507f1f77bcf86cd799439011"); * await setseenByTenantAt("507f1f77bcf86cd799439011");
*/ */
export const setSeenByTenant = async (locationID: string): Promise<void> => { export const setSeenByTenantAt = async (locationID: string): Promise<void> => {
const dbClient = await getDbClient(); const dbClient = await getDbClient();
// First check if the location exists and if seenByTenant is already true // First check if the location exists and if seenByTenantAt is already true
const location = await dbClient.collection<BillingLocation>("lokacije") const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne({ _id: locationID }); .findOne({ _id: locationID });
// If location doesn't exist or seenByTenant is already true, no update needed // If location doesn't exist or seenByTenantAt is already true, no update needed
if (!location || location.seenByTenant === true) { if (!location || location.seenByTenantAt) {
return; return;
} }
@@ -582,7 +582,7 @@ export const setSeenByTenant = async (locationID: string): Promise<void> => {
await dbClient.collection<BillingLocation>("lokacije") await dbClient.collection<BillingLocation>("lokacije")
.updateOne( .updateOne(
{ _id: locationID }, { _id: locationID },
{ $set: { seenByTenant: true } } { $set: { seenByTenantAt: new Date() } }
); );
} }

View File

@@ -40,7 +40,7 @@ export const addMonth = withUser(async (user:AuthenticatedUser, { year, month }:
// copy all the properties from the previous location // copy all the properties from the previous location
...prevLocation, ...prevLocation,
// clear properties specific to the month // clear properties specific to the month
seenByTenant: undefined, seenByTenantAt: undefined,
utilBillsProofOfPaymentUploadedAt: undefined, utilBillsProofOfPaymentUploadedAt: undefined,
utilBillsProofOfPaymentAttachment: undefined, utilBillsProofOfPaymentAttachment: undefined,
// assign a new ID // assign a new ID

View File

@@ -74,7 +74,7 @@ export interface BillingLocation {
/** (optional) monthly rent amount in cents */ /** (optional) monthly rent amount in cents */
rentAmount?: number | null; rentAmount?: number | null;
/** (optional) whether the location has been seen by tenant */ /** (optional) whether the location has been seen by tenant */
seenByTenant?: boolean | null; seenByTenantAt?: Date | null;
/** (optional) utility bills proof of payment attachment */ /** (optional) utility bills proof of payment attachment */
utilBillsProofOfPaymentAttachment?: BillAttachment|null; utilBillsProofOfPaymentAttachment?: BillAttachment|null;
/** (optional) date when utility bills proof of payment was uploaded */ /** (optional) date when utility bills proof of payment was uploaded */

View File

@@ -9,6 +9,7 @@ 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";
export interface LocationCardProps { export interface LocationCardProps {
location: BillingLocation; location: BillingLocation;
@@ -21,7 +22,7 @@ export const LocationCard: FC<LocationCardProps> = ({ location, currency }) => {
name, name,
yearMonth, yearMonth,
bills, bills,
seenByTenant, seenByTenantAt,
// NOTE: only the fileName is projected from the DB to reduce data transfer // NOTE: only the fileName is projected from the DB to reduce data transfer
utilBillsProofOfPaymentUploadedAt utilBillsProofOfPaymentUploadedAt
} = location; } = location;
@@ -63,37 +64,47 @@ 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 || seenByTenant || utilBillsProofOfPaymentUploadedAt ? { monthlyExpense > 0 || seenByTenantAt || utilBillsProofOfPaymentUploadedAt ?
<> <>
<div className="divider m-0 font-bold uppercase">{t("monthly-statement-legend")}</div> <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 ? monthlyExpense > 0 ?
<div className="flex items-center gap-2 ml-2"> <div className="flex ml-1">
<BanknotesIcon className="h-5 w-5" /> <span className="w-5 min-w-5 mr-2"><BanknotesIcon className="mt-[.1rem]" /></span>
{t("payed-total-label")} <strong>{formatCurrency(monthlyExpense, currency ?? "EUR")}</strong> <span>
<CheckCircleIcon className="h-5 w-5 text-success" /> {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> </div>
: null : null
} }
{seenByTenant && ( {seenByTenantAt && (
<div className="flex items-center gap-2 mt-[-0.2rem] ml-2"> <div className="flex mt-1 ml-1">
<EyeIcon className="h-5 w-5" /> <span className="w-5 mr-2 min-w-5"><EyeIcon className="mt-[.1rem]" /></span>
<span className="text-sm">{t("seen-by-tenant-label")}</span> <span>
<CheckCircleIcon className="h-5 w-5 text-success" /> <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> </div>
)} )}
{utilBillsProofOfPaymentUploadedAt && ( {utilBillsProofOfPaymentUploadedAt && (
<Link <Link
href={`/share/proof-of-payment/${_id}/`} href={`/share/proof-of-payment/${_id}/`}
target="_blank" target="_blank"
className="flex items-center gap-2 mt-[-0.2rem] ml-2" className="flex mt-1 ml-1">
> <span className="w-5 min-w-5 mr-2"><TicketIcon className="mt-[.1rem]" /></span>
<TicketIcon className="h-5 w-5" /> <span>
<span className="text-sm">{t("download-proof-of-payment-label")}</span> <span className="underline">{t("download-proof-of-payment-label")}</span>
<CheckCircleIcon className="h-5 w-5 text-success" /> <CheckCircleIcon className="h-5 w-5 ml-2 mt-[-.2rem] text-success inline-block" />
</span>
</Link> </Link>
)} )}
</>: null </div>
</div>
</> : null
} }
</div> </div>

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { FC, useEffect, useMemo, useState } from "react"; import { FC, useMemo, useState } from "react";
import { BillAttachment, BilledTo, BillingLocation, UserSettings } from "../lib/db-types"; import { BilledTo, BillingLocation, UserSettings } from "../lib/db-types";
import { formatYearMonth } from "../lib/format"; import { formatYearMonth } from "../lib/format";
import { formatCurrency, formatIban } from "../lib/formatStrings"; import { formatCurrency, formatIban } from "../lib/formatStrings";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";