Fix client-side cache staleness after proof of payment upload

Added cache revalidation to ensure ViewLocationCard reflects uploaded
proof of payment when navigating back from ViewBillCard:

- Server-side: Added revalidatePath() to upload actions in billActions
  and locationActions to invalidate Next.js server cache
- Client-side: Added router.refresh() calls in ViewBillCard and
  ViewLocationCard to refresh client router cache after successful upload

This maintains the current UX (no redirect on upload) while ensuring
fresh data is displayed on navigation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-07 16:57:00 +01:00
parent cfa6a4c5b7
commit 0f8b5678f4
4 changed files with 32 additions and 13 deletions

View File

@@ -2,14 +2,14 @@
import { z } from 'zod';
import { getDbClient } from '../dbClient';
import { Bill, BilledTo, FileAttachment, BillingLocation, YearMonth } from '../db-types';
import { Bill, BilledTo, FileAttachment, BillingLocation } from '../db-types';
import { ObjectId } from 'mongodb';
import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth';
import { gotoHome, gotoHomeWithMessage } from './navigationActions';
import { gotoHomeWithMessage } from './navigationActions';
import { getTranslations, getLocale } from "next-intl/server";
import { IntlTemplateFn } from '@/app/i18n';
import { unstable_noStore } from 'next/cache';
import { unstable_noStore, revalidatePath } from 'next/cache';
export type State = {
errors?: {
@@ -116,6 +116,8 @@ const serializeAttachment = async (billAttachment: File | null): Promise<FileAtt
*/
export const updateOrAddBill = withUser(async (user: AuthenticatedUser, locationId: string, billId: string | undefined, billYear: number | undefined, billMonth: number | undefined, prevState: State, formData: FormData) => {
unstable_noStore();
const { id: userId } = user;
const t = await getTranslations("bill-edit-form.validation");
@@ -351,6 +353,7 @@ export const fetchBillByUserAndId = withUser(async (user:AuthenticatedUser, loca
export const fetchBillById = async (locationID: string, billID: string, includeAttachmentBinary: boolean = false) => {
unstable_noStore();
const dbClient = await getDbClient();
@@ -387,6 +390,8 @@ export const fetchBillById = async (locationID: string, billID: string, includeA
export const deleteBillById = withUser(async (user: AuthenticatedUser, locationID: string, billID: string, year: number, month: number, _prevState: any, formData?: FormData) => {
unstable_noStore();
const { id: userId } = user;
const dbClient = await getDbClient();
@@ -488,6 +493,7 @@ export const deleteBillById = withUser(async (user: AuthenticatedUser, locationI
* @returns Promise with success status
*/
export const uploadProofOfPayment = async (locationID: string, billID: string, formData: FormData): Promise<{ success: boolean; error?: string }> => {
unstable_noStore();
try {
@@ -503,7 +509,7 @@ export const uploadProofOfPayment = async (locationID: string, billID: string, f
}
// Validate file type
if (attachmentFile && attachmentFile.size > 0 && attachmentFile.type !== 'application/pdf') {
if (file && file.size > 0 && file.type !== 'application/pdf') {
return { success: false, error: 'Only PDF files are accepted' };
}
@@ -564,6 +570,9 @@ export const uploadProofOfPayment = async (locationID: string, billID: string, f
]
});
// Invalidate the location view cache
revalidatePath(`/share/location/${locationID}`, 'page');
return { success: true };
} catch (error: any) {
console.error('Error uploading proof of payment for a bill:', error);

View File

@@ -7,7 +7,7 @@ import { ObjectId } from 'mongodb';
import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth';
import { gotoHomeWithMessage } from './navigationActions';
import { unstable_noStore as noStore } from 'next/cache';
import { unstable_noStore, revalidatePath } from 'next/cache';
import { IntlTemplateFn } from '@/app/i18n';
import { getTranslations, getLocale } from "next-intl/server";
@@ -106,7 +106,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({
*/
export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locationId: string | undefined, yearMonth: YearMonth | undefined, prevState:State, formData: FormData) => {
noStore();
unstable_noStore();
const t = await getTranslations("location-edit-form.validation");
@@ -373,7 +373,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:number) => {
noStore();
unstable_noStore();
const dbClient = await getDbClient();
@@ -470,7 +470,7 @@ ova metoda je zamijenjena sa jednostavnijom `fetchLocationById`, koja brže radi
export const fetchLocationByUserAndId = withUser(async (user:AuthenticatedUser, locationID:string) => {
noStore();
unstable_noStore();
const dbClient = await getDbClient();
@@ -499,7 +499,7 @@ export const fetchLocationByUserAndId = withUser(async (user:AuthenticatedUser,
export const fetchLocationById = async (locationID:string) => {
noStore();
unstable_noStore();
const dbClient = await getDbClient();
@@ -526,7 +526,7 @@ export const fetchLocationById = async (locationID:string) => {
export const deleteLocationById = withUser(async (user:AuthenticatedUser, locationID:string, yearMonth:YearMonth, _prevState:any, formData: FormData) => {
noStore();
unstable_noStore();
const dbClient = await getDbClient();
@@ -642,7 +642,8 @@ const serializeAttachment = async (file: File | null):Promise<FileAttachment | n
* @returns Promise with success status
*/
export const uploadUtilBillsProofOfPayment = async (locationID: string, formData: FormData): Promise<{ success: boolean; error?: string }> => {
noStore();
unstable_noStore();
try {
@@ -689,6 +690,9 @@ export const uploadUtilBillsProofOfPayment = async (locationID: string, formData
} }
);
// Invalidate the location view cache
revalidatePath(`/share/location/${locationID}`, 'page');
return { success: true };
} catch (error: any) {
console.error('Error uploading util bills proof of payment:', error);

View File

@@ -1,9 +1,10 @@
"use client";
import { DocumentIcon, CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/outline";
import { TicketIcon, CheckCircleIcon, XCircleIcon, DocumentIcon } from "@heroicons/react/24/outline";
import { Bill, BillingLocation } from "../lib/db-types";
import { FC, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { formatYearMonth } from "../lib/format";
import { useTranslations } from "next-intl";
import { Pdf417Barcode } from "./Pdf417Barcode";
@@ -16,6 +17,7 @@ export interface ViewBillCardProps {
export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill }) => {
const router = useRouter();
const t = useTranslations("bill-edit-form");
const { _id: billID, name, paid, attachment, notes, payedAmount, hub3aText, proofOfPayment } = bill ?? { _id: undefined, name: "", paid: false, notes: "" };
@@ -49,6 +51,7 @@ export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill }) => {
if (result.success) {
setProofOfPaymentFilename(file.name);
setProofOfPaymentUploadedAt(new Date());
router.refresh();
} else {
setUploadError(result.error || 'Upload failed');
}
@@ -125,7 +128,7 @@ export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill }) => {
target="_blank"
className='text-center w-full max-w-[20rem] text-nowrap truncate inline-block'
>
<DocumentIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1" />
<TicketIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1" />
{ decodeURIComponent(proofOfPaymentFilename) }
</Link>
</div>

View File

@@ -5,6 +5,7 @@ import { BilledTo, BillingLocation, UserSettings } from "../lib/db-types";
import { formatYearMonth } from "../lib/format";
import { formatCurrency, formatIban } from "../lib/formatStrings";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { ViewBillBadge } from "./ViewBillBadge";
import { Pdf417Barcode } from "./Pdf417Barcode";
import { EncodePayment, PaymentParams } from "hub-3a-payment-encoder";
@@ -34,6 +35,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
proofOfPaymentType,
} = location;
const router = useRouter();
const t = useTranslations("home-page.location-card");
const [isUploading, setIsUploading] = useState(false);
@@ -64,6 +66,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
if (result.success) {
setAttachmentFilename(file.name);
setAttachmentUploadedAt(new Date());
router.refresh();
} else {
setUploadError(result.error || 'Upload failed');
}