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 { 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 { notFound } from 'next/navigation';
|
||||||
|
import { myAuth } from '@/app/lib/auth';
|
||||||
|
|
||||||
export default async function LocationViewPage({ locationId }: { locationId:string }) {
|
export default async function LocationViewPage({ locationId }: { locationId:string }) {
|
||||||
const location = await fetchLocationById(locationId);
|
const location = await fetchLocationById(locationId);
|
||||||
@@ -9,5 +10,14 @@ export default async function LocationViewPage({ locationId }: { locationId:stri
|
|||||||
return(notFound());
|
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} />);
|
return (<ViewLocationCard location={location} />);
|
||||||
}
|
}
|
||||||
@@ -411,6 +411,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,
|
||||||
// "bills.attachment": 0,
|
// "bills.attachment": 0,
|
||||||
// "bills.notes": 0,
|
// "bills.notes": 0,
|
||||||
// "bills.barcodeImage": 1,
|
// "bills.barcodeImage": 1,
|
||||||
@@ -530,3 +531,35 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati
|
|||||||
errors: undefined,
|
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;
|
rentDueDay?: number | null;
|
||||||
/** (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 */
|
||||||
|
seenByTenant?: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum BilledTo {
|
export enum BilledTo {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'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 { FC } from "react";
|
||||||
import { BillBadge } from "./BillBadge";
|
import { BillBadge } from "./BillBadge";
|
||||||
import { BillingLocation } from "../lib/db-types";
|
import { BillingLocation } from "../lib/db-types";
|
||||||
@@ -14,7 +14,10 @@ export interface LocationCardProps {
|
|||||||
location: BillingLocation
|
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 t = useTranslations("home-page.location-card");
|
||||||
const currentLocale = useLocale();
|
const currentLocale = useLocale();
|
||||||
@@ -46,13 +49,28 @@ 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>
|
<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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{ 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 ?
|
monthlyExpense > 0 ?
|
||||||
<p>
|
<div className="flex items-center gap-2">
|
||||||
|
<BanknotesIcon className="h-5 w-5" />
|
||||||
{ t("payed-total-label") } <strong>${formatCurrency(monthlyExpense)}</strong>
|
{ t("payed-total-label") } <strong>${formatCurrency(monthlyExpense)}</strong>
|
||||||
</p>
|
</div>
|
||||||
: null
|
: 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} />
|
<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} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,9 @@
|
|||||||
"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:",
|
"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": {
|
"month-card": {
|
||||||
"payed-total-label": "Total monthly expenditure:",
|
"payed-total-label": "Total monthly expenditure:",
|
||||||
|
|||||||
@@ -55,7 +55,9 @@
|
|||||||
"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:",
|
"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": {
|
"month-card": {
|
||||||
"payed-total-label": "Ukupni mjesečni trošak:",
|
"payed-total-label": "Ukupni mjesečni trošak:",
|
||||||
|
|||||||
Reference in New Issue
Block a user