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