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:
@@ -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} />);
|
||||||
|
|||||||
@@ -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() } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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")} <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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user