feat(cache): add stale get caching for report filters and summaries

This commit is contained in:
2026-04-30 16:13:35 +03:30
parent 0e8d43f1ea
commit a5a7a01da0
8 changed files with 356 additions and 131 deletions

147
src/api/cache.ts Normal file
View File

@@ -0,0 +1,147 @@
import { buildApiError, authFetch } from "./client"
import { getAccessToken, SESSION_CHANGED_EVENT } from "../lib/session"
const CACHE_PREFIX = "api-get-cache:v2:"
interface CacheEntry<T> {
expiresAt: number
data: T
namespaces: string[]
}
const memoryCache = new Map<string, CacheEntry<unknown>>()
const cloneData = <T>(value: T): T => {
if (typeof structuredClone === "function") {
return structuredClone(value)
}
return JSON.parse(JSON.stringify(value)) as T
}
const decodeBase64Url = (value: string) => {
const normalized = value.replace(/-/g, "+").replace(/_/g, "/")
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=")
return atob(padded)
}
const getCurrentUserCacheKey = () => {
const token = getAccessToken()
if (!token) return "anon"
try {
const payload = JSON.parse(decodeBase64Url(token.split(".")[1] || ""))
return String(payload.user_id || payload.sub || "anon")
} catch {
return "anon"
}
}
const normalizeEndpoint = (endpoint: string) => {
const base = endpoint.startsWith("http") ? undefined : "https://cache.local"
const url = new URL(endpoint, base)
const entries = Array.from(url.searchParams.entries()).sort(([aKey, aValue], [bKey, bValue]) => {
if (aKey === bKey) return aValue.localeCompare(bValue)
return aKey.localeCompare(bKey)
})
const normalizedSearch = new URLSearchParams()
entries.forEach(([key, value]) => normalizedSearch.append(key, value))
const queryString = normalizedSearch.toString()
return `${url.pathname}${queryString ? `?${queryString}` : ""}`
}
const buildStorageKey = (endpoint: string) => `${CACHE_PREFIX}${getCurrentUserCacheKey()}:${normalizeEndpoint(endpoint)}`
const readStoredEntry = <T>(key: string): CacheEntry<T> | null => {
const cached = memoryCache.get(key)
if (cached) {
if (cached.expiresAt > Date.now()) {
return cached as CacheEntry<T>
}
memoryCache.delete(key)
}
const raw = sessionStorage.getItem(key)
if (!raw) return null
try {
const parsed = JSON.parse(raw) as CacheEntry<T>
if (parsed.expiresAt <= Date.now()) {
sessionStorage.removeItem(key)
return null
}
memoryCache.set(key, parsed as CacheEntry<unknown>)
return parsed
} catch {
sessionStorage.removeItem(key)
return null
}
}
const writeEntry = <T>(key: string, entry: CacheEntry<T>) => {
memoryCache.set(key, entry as CacheEntry<unknown>)
sessionStorage.setItem(key, JSON.stringify(entry))
}
export const invalidateApiCache = (namespaces: string[]) => {
if (!namespaces.length) return
const namespaceSet = new Set(namespaces)
const removeIfMatches = (key: string, entry: CacheEntry<unknown>) => {
if (entry.namespaces.some((namespace) => namespaceSet.has(namespace))) {
memoryCache.delete(key)
sessionStorage.removeItem(key)
}
}
memoryCache.forEach((entry, key) => removeIfMatches(key, entry))
for (let index = sessionStorage.length - 1; index >= 0; index -= 1) {
const key = sessionStorage.key(index)
if (!key || !key.startsWith(CACHE_PREFIX)) continue
try {
const parsed = JSON.parse(sessionStorage.getItem(key) || "null") as CacheEntry<unknown> | null
if (parsed) {
removeIfMatches(key, parsed)
}
} catch {
sessionStorage.removeItem(key)
}
}
}
export const clearApiCache = () => {
memoryCache.clear()
for (let index = sessionStorage.length - 1; index >= 0; index -= 1) {
const key = sessionStorage.key(index)
if (key?.startsWith(CACHE_PREFIX)) {
sessionStorage.removeItem(key)
}
}
}
export const cachedGetJson = async <T>(
endpoint: string,
options: { ttlMs: number; namespaces: string[]; bypass?: boolean },
): Promise<T> => {
const storageKey = buildStorageKey(endpoint)
if (!options.bypass) {
const cached = readStoredEntry<T>(storageKey)
if (cached) {
return cloneData(cached.data)
}
}
const response = await authFetch(endpoint)
if (!response.ok) {
throw await buildApiError(response)
}
const data = (await response.json()) as T
writeEntry(storageKey, {
expiresAt: Date.now() + options.ttlMs,
data,
namespaces: options.namespaces,
})
return cloneData(data)
}
window.addEventListener(SESSION_CHANGED_EVENT, clearApiCache)

View File

@@ -1,4 +1,18 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
import { cachedGetJson, invalidateApiCache } from "./cache";
export interface Client {
id: string
name: string
notes?: string
}
interface PaginatedResponse<T> {
count: number
next: string | null
previous: string | null
results: T[]
}
export const getClients = async ( export const getClients = async (
workspaceId: string, workspaceId: string,
@@ -6,7 +20,7 @@ export const getClients = async (
ordering: string = "", ordering: string = "",
limit: number = 10, limit: number = 10,
offset: number = 0 offset: number = 0
) => { ): Promise<PaginatedResponse<Client>> => {
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
workspace: workspaceId, workspace: workspaceId,
limit: limit.toString(), limit: limit.toString(),
@@ -16,11 +30,10 @@ export const getClients = async (
if (search) queryParams.append("search", search); if (search) queryParams.append("search", search);
if (ordering) queryParams.append("ordering", ordering); if (ordering) queryParams.append("ordering", ordering);
const response = await authFetch(`/api/clients/?${queryParams.toString()}`); return cachedGetJson<PaginatedResponse<Client>>(`/api/clients/?${queryParams.toString()}`, {
if (!response.ok) { ttlMs: 5 * 60 * 1000,
throw new Error("Failed to fetch clients"); namespaces: ["clients"],
} });
return response.json();
}; };
export const createClient = async (workspaceId: string, data: { name: string; notes: string }) => { export const createClient = async (workspaceId: string, data: { name: string; notes: string }) => {
@@ -36,7 +49,9 @@ export const createClient = async (workspaceId: string, data: { name: string; no
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to create client"); throw new Error(errorData?.detail || errorData?.message || "Failed to create client");
} }
return response.json(); const payload = await response.json();
invalidateApiCache(["clients", "reports"]);
return payload;
}; };
export const updateClient = async (id: string, data: { name?: string; notes?: string }) => { export const updateClient = async (id: string, data: { name?: string; notes?: string }) => {
@@ -49,7 +64,9 @@ export const updateClient = async (id: string, data: { name?: string; notes?: st
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to update client"); throw new Error(errorData?.detail || errorData?.message || "Failed to update client");
} }
return response.json(); const payload = await response.json();
invalidateApiCache(["clients", "reports"]);
return payload;
}; };
export const deleteClient = async (id: string) => { export const deleteClient = async (id: string) => {
@@ -61,6 +78,7 @@ export const deleteClient = async (id: string) => {
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to delete client"); throw new Error(errorData?.detail || errorData?.message || "Failed to delete client");
} }
invalidateApiCache(["clients", "reports"]);
if (response.status === 204) { if (response.status === 204) {
return { success: true }; return { success: true };

View File

@@ -1,4 +1,5 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
import { cachedGetJson, invalidateApiCache } from "./cache";
interface AuditUser { interface AuditUser {
id: string; id: string;
@@ -56,10 +57,10 @@ export const getProjects = async (
if (params.is_archived !== undefined) queryParams.append("is_archived", params.is_archived.toString()); if (params.is_archived !== undefined) queryParams.append("is_archived", params.is_archived.toString());
if (params.ordering !== undefined) queryParams.append("ordering", params.ordering.toString()); if (params.ordering !== undefined) queryParams.append("ordering", params.ordering.toString());
const response = await authFetch(`/api/projects/?${queryParams.toString()}`); const data = await cachedGetJson<any>(`/api/projects/?${queryParams.toString()}`, {
ttlMs: 5 * 60 * 1000,
if (!response.ok) throw new Error("Failed to fetch projects"); namespaces: ["projects"],
const data = await response.json(); });
if (Array.isArray(data)) return data; if (Array.isArray(data)) return data;
if (Array.isArray(data?.items)) { if (Array.isArray(data?.items)) {
return { return {
@@ -93,7 +94,9 @@ export const createProject = async (
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to create project"); throw new Error(errorData?.detail || errorData?.message || "Failed to create project");
} }
return response.json(); const payload = await response.json();
invalidateApiCache(["projects", "reports"]);
return payload;
}; };
export const updateProject = async ( export const updateProject = async (
@@ -109,7 +112,9 @@ export const updateProject = async (
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to update project"); throw new Error(errorData?.detail || errorData?.message || "Failed to update project");
} }
return response.json(); const payload = await response.json();
invalidateApiCache(["projects", "reports"]);
return payload;
}; };
export const deleteProject = async (id: string) => { export const deleteProject = async (id: string) => {
@@ -121,6 +126,7 @@ export const deleteProject = async (id: string) => {
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to delete project"); throw new Error(errorData?.detail || errorData?.message || "Failed to delete project");
} }
invalidateApiCache(["projects", "reports"]);
if (response.status === 204) return { success: true }; if (response.status === 204) return { success: true };
return response.json().catch(() => ({ success: true })); return response.json().catch(() => ({ success: true }));
@@ -135,5 +141,7 @@ export const toggleArchiveProject = async (id: string) => {
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || `Failed to archive project`); throw new Error(errorData?.detail || errorData?.message || `Failed to archive project`);
} }
return response.json(); const payload = await response.json();
invalidateApiCache(["projects", "reports"]);
return payload;
}; };

View File

@@ -1,4 +1,5 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
import { cachedGetJson, invalidateApiCache } from "./cache";
export interface RateUser { export interface RateUser {
id: string; id: string;
@@ -55,13 +56,35 @@ const ensurePaginated = async <T>(response: Response): Promise<PaginatedResponse
}; };
export const getPriceUnits = async () => { export const getPriceUnits = async () => {
const response = await authFetch("/api/price-units/"); const data = await cachedGetJson<any>("/api/price-units/", {
return ensurePaginated<PriceUnit>(response); ttlMs: 5 * 60 * 1000,
namespaces: ["price-units"],
});
if (Array.isArray(data)) {
return { count: data.length, next: null, previous: null, results: data };
}
return {
count: data.count || data.results?.length || 0,
next: data.next || null,
previous: data.previous || null,
results: data.results || [],
};
}; };
export const getWorkspaceUserRates = async (workspaceId: string) => { export const getWorkspaceUserRates = async (workspaceId: string) => {
const response = await authFetch(`/api/workspace-user-rates/?workspace=${workspaceId}`); const data = await cachedGetJson<any>(`/api/workspace-user-rates/?workspace=${workspaceId}`, {
return ensurePaginated<WorkspaceUserRate>(response); ttlMs: 5 * 60 * 1000,
namespaces: ["workspace-rates"],
});
if (Array.isArray(data)) {
return { count: data.length, next: null, previous: null, results: data };
}
return {
count: data.count || data.results?.length || 0,
next: data.next || null,
previous: data.previous || null,
results: data.results || [],
};
}; };
export const createWorkspaceUserRate = async (data: { export const createWorkspaceUserRate = async (data: {
@@ -78,6 +101,7 @@ export const createWorkspaceUserRate = async (data: {
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to save workspace user rate"); throw new Error(errorData?.detail || errorData?.message || "Failed to save workspace user rate");
} }
invalidateApiCache(["workspace-rates", "reports"]);
return response.json() as Promise<WorkspaceUserRate>; return response.json() as Promise<WorkspaceUserRate>;
}; };
@@ -93,6 +117,7 @@ export const updateWorkspaceUserRate = async (
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to update workspace user rate"); throw new Error(errorData?.detail || errorData?.message || "Failed to update workspace user rate");
} }
invalidateApiCache(["workspace-rates", "reports"]);
return response.json() as Promise<WorkspaceUserRate>; return response.json() as Promise<WorkspaceUserRate>;
}; };
@@ -104,4 +129,5 @@ export const deleteWorkspaceUserRate = async (rateId: string) => {
const errorData = await response.json().catch(() => null); const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || "Failed to delete workspace user rate"); throw new Error(errorData?.detail || errorData?.message || "Failed to delete workspace user rate");
} }
invalidateApiCache(["workspace-rates", "reports"]);
}; };

View File

@@ -1,4 +1,5 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
import { cachedGetJson } from "./cache";
export type ReportPeriod = export type ReportPeriod =
| "this_week" | "this_week"
@@ -150,15 +151,17 @@ const toQueryString = (filters: ReportFilters) => {
}; };
export const getChartReport = async (filters: ReportFilters): Promise<ChartReportResponse> => { export const getChartReport = async (filters: ReportFilters): Promise<ChartReportResponse> => {
const response = await authFetch(`/api/reports/chart/?${toQueryString(filters)}`); return cachedGetJson(`/api/reports/chart/?${toQueryString(filters)}`, {
if (!response.ok) throw new Error("Failed to load chart report"); ttlMs: 60 * 1000,
return response.json(); namespaces: ["reports"],
});
}; };
export const getTableReport = async (filters: ReportFilters): Promise<TableReportResponse> => { export const getTableReport = async (filters: ReportFilters): Promise<TableReportResponse> => {
const response = await authFetch(`/api/reports/table/?${toQueryString(filters)}`); return cachedGetJson(`/api/reports/table/?${toQueryString(filters)}`, {
if (!response.ok) throw new Error("Failed to load table report"); ttlMs: 60 * 1000,
return response.json(); namespaces: ["reports"],
});
}; };
export const getDayDetailsReport = async ( export const getDayDetailsReport = async (
@@ -166,9 +169,10 @@ export const getDayDetailsReport = async (
day: string, day: string,
): Promise<DayDetailsResponse> => { ): Promise<DayDetailsResponse> => {
const query = `${toQueryString(filters)}&day=${encodeURIComponent(day)}`; const query = `${toQueryString(filters)}&day=${encodeURIComponent(day)}`;
const response = await authFetch(`/api/reports/day-details/?${query}`); return cachedGetJson(`/api/reports/day-details/?${query}`, {
if (!response.ok) throw new Error("Failed to load day details"); ttlMs: 30 * 1000,
return response.json(); namespaces: ["reports"],
});
}; };
export const createReportExport = async ( export const createReportExport = async (

View File

@@ -1,4 +1,5 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
import { cachedGetJson, invalidateApiCache } from "./cache";
interface AuditUser { interface AuditUser {
id: string; id: string;
@@ -36,9 +37,10 @@ export const getTags = async (
if (params.search) query.append("search", params.search); if (params.search) query.append("search", params.search);
if (params.ordering) query.append("ordering", params.ordering); if (params.ordering) query.append("ordering", params.ordering);
const response = await authFetch(`/api/tags/?${query.toString()}`); return cachedGetJson(`/api/tags/?${query.toString()}`, {
if (!response.ok) throw new Error("Failed to fetch tags"); ttlMs: 5 * 60 * 1000,
return response.json(); namespaces: ["tags"],
});
}; };
export const createTag = async (workspaceId: string, data: { name: string; color: string }) => { export const createTag = async (workspaceId: string, data: { name: string; color: string }) => {
@@ -50,7 +52,9 @@ export const createTag = async (workspaceId: string, data: { name: string; color
}), }),
}); });
if (!response.ok) throw new Error("Failed to create tag"); if (!response.ok) throw new Error("Failed to create tag");
return response.json(); const payload = await response.json();
invalidateApiCache(["tags", "reports"]);
return payload;
}; };
export const updateTag = async (id: string, data: Partial<Pick<Tag, "name" | "color">>) => { export const updateTag = async (id: string, data: Partial<Pick<Tag, "name" | "color">>) => {
@@ -59,7 +63,9 @@ export const updateTag = async (id: string, data: Partial<Pick<Tag, "name" | "co
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (!response.ok) throw new Error("Failed to update tag"); if (!response.ok) throw new Error("Failed to update tag");
return response.json(); const payload = await response.json();
invalidateApiCache(["tags", "reports"]);
return payload;
}; };
export const deleteTag = async (id: string) => { export const deleteTag = async (id: string) => {
@@ -67,4 +73,5 @@ export const deleteTag = async (id: string) => {
method: "DELETE", method: "DELETE",
}); });
if (!response.ok) throw new Error("Failed to delete tag"); if (!response.ok) throw new Error("Failed to delete tag");
invalidateApiCache(["tags", "reports"]);
}; };

View File

@@ -1,4 +1,5 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
import { invalidateApiCache } from "./cache";
export interface TimeEntryProjectDetails { export interface TimeEntryProjectDetails {
id: string; id: string;
@@ -109,7 +110,9 @@ export const createTimeEntry = async (payload: TimeEntryPayload) => {
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (!response.ok) throw new Error("Failed to create time entry"); if (!response.ok) throw new Error("Failed to create time entry");
return response.json(); const data = await response.json();
invalidateApiCache(["reports"]);
return data;
}; };
export const updateTimeEntry = async (id: string, payload: TimeEntryPayload) => { export const updateTimeEntry = async (id: string, payload: TimeEntryPayload) => {
@@ -118,7 +121,9 @@ export const updateTimeEntry = async (id: string, payload: TimeEntryPayload) =>
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (!response.ok) throw new Error("Failed to update time entry"); if (!response.ok) throw new Error("Failed to update time entry");
return response.json(); const data = await response.json();
invalidateApiCache(["reports"]);
return data;
}; };
export const stopTimeEntry = async (id: string, endTime?: string) => { export const stopTimeEntry = async (id: string, endTime?: string) => {
@@ -127,7 +132,9 @@ export const stopTimeEntry = async (id: string, endTime?: string) => {
body: JSON.stringify(endTime ? { end_time: endTime } : {}), body: JSON.stringify(endTime ? { end_time: endTime } : {}),
}); });
if (!response.ok) throw new Error("Failed to stop time entry"); if (!response.ok) throw new Error("Failed to stop time entry");
return response.json(); const data = await response.json();
invalidateApiCache(["reports"]);
return data;
}; };
export const deleteTimeEntry = async (id: string) => { export const deleteTimeEntry = async (id: string) => {
@@ -135,4 +142,5 @@ export const deleteTimeEntry = async (id: string) => {
method: "DELETE", method: "DELETE",
}); });
if (!response.ok) throw new Error("Failed to delete time entry"); if (!response.ok) throw new Error("Failed to delete time entry");
invalidateApiCache(["reports"]);
}; };

View File

@@ -1,4 +1,5 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
import { cachedGetJson, invalidateApiCache } from "./cache";
export interface Workspace { export interface Workspace {
id: string; id: string;
@@ -52,13 +53,10 @@ const toQueryString = (params?: Record<string, QueryValue>) => {
export const fetchWorkspaces = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<Workspace>> => { export const fetchWorkspaces = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<Workspace>> => {
const query = toQueryString(params); const query = toQueryString(params);
const url = `/api/workspaces/${query ? `?${query}` : ''}`; const url = `/api/workspaces/${query ? `?${query}` : ''}`;
const response = await authFetch(url); const data = await cachedGetJson<any>(url, {
ttlMs: 60 * 1000,
if (!response.ok) { namespaces: ["workspaces"],
throw new Error("Failed to fetch workspaces"); });
}
const data = await response.json();
if (Array.isArray(data)) { if (Array.isArray(data)) {
return { count: data.length, next: null, previous: null, results: data }; return { count: data.length, next: null, previous: null, results: data };
@@ -113,7 +111,9 @@ export const createWorkspace = async (data: {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to create workspace'); throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to create workspace');
} }
return await response.json(); const payload = await response.json();
invalidateApiCache(["workspaces", "workspace-memberships", "reports"]);
return payload;
}; };
export const updateWorkspace = async ( export const updateWorkspace = async (
@@ -148,7 +148,9 @@ export const updateWorkspace = async (
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to update workspace'); throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to update workspace');
} }
return await response.json(); const payload = await response.json();
invalidateApiCache(["workspaces"]);
return payload;
}; };
export const deleteWorkspace = async (id: string): Promise<void> => { export const deleteWorkspace = async (id: string): Promise<void> => {
@@ -159,15 +161,15 @@ export const deleteWorkspace = async (id: string): Promise<void> => {
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to delete workspace'); throw new Error('Failed to delete workspace');
} }
invalidateApiCache(["workspaces", "workspace-memberships", "workspace-rates", "reports"]);
}; };
export const fetchWorkspaceMemberships = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<WorkspaceMembership>> => { export const fetchWorkspaceMemberships = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<WorkspaceMembership>> => {
const queryParams = toQueryString(params); const queryParams = toQueryString(params);
const response = await authFetch(`/api/workspace-memberships/?${queryParams.toString()}`); const data = await cachedGetJson<any>(`/api/workspace-memberships/?${queryParams.toString()}`, {
ttlMs: 5 * 60 * 1000,
if (!response.ok) throw new Error("Failed to fetch workspace memberships"); namespaces: ["workspace-memberships"],
});
const data = await response.json();
if (Array.isArray(data)) { if (Array.isArray(data)) {
return { count: data.length, next: null, previous: null, results: data }; return { count: data.length, next: null, previous: null, results: data };
@@ -192,7 +194,9 @@ export const addWorkspaceMembership = async (data: { workspace: string; user: st
throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to add workspace membership'); throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to add workspace membership');
} }
return await response.json(); const payload = await response.json();
invalidateApiCache(["workspace-memberships", "reports"]);
return payload;
}; };
export const removeWorkspaceMembership = async (membershipId: string): Promise<void> => { export const removeWorkspaceMembership = async (membershipId: string): Promise<void> => {
@@ -203,6 +207,7 @@ export const removeWorkspaceMembership = async (membershipId: string): Promise<v
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to remove workspace membership'); throw new Error('Failed to remove workspace membership');
} }
invalidateApiCache(["workspace-memberships", "reports"]);
}; };
export const updateWorkspaceMembership = async (membershipId: string | number, data: { role: string }) => { export const updateWorkspaceMembership = async (membershipId: string | number, data: { role: string }) => {
@@ -216,5 +221,7 @@ export const updateWorkspaceMembership = async (membershipId: string | number, d
throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to update membership'); throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to update membership');
} }
return await response.json(); const payload = await response.json();
invalidateApiCache(["workspace-memberships", "reports"]);
return payload;
}; };