Optimised cach usage, and enforced maker checker

This commit is contained in:
2026-10-25 10:11:57 +02:00
parent 9724bbac24
commit aeb7542cfe
20 changed files with 277 additions and 428 deletions
+37 -293
View File
@@ -442,84 +442,21 @@ export const getProgrammeValue = async (programmeId: string | number): Promise<n
}
};
const LOCAL_INSTRUCTIONS_KEY = 'pfm_local_instructions';
export const purgeStoredInstructionDuplicates = (_apiItems?: Instrument[]): void => {};
export const purgeStoredInstructionDuplicates = (apiItems: Instrument[]): void => {
if (typeof window === 'undefined' || !Array.isArray(apiItems) || apiItems.length === 0) return;
try {
const raw = localStorage.getItem(LOCAL_INSTRUCTIONS_KEY);
if (!raw) return;
const local: Instrument[] = JSON.parse(raw);
if (!Array.isArray(local) || local.length === 0) return;
export const getStoredInstructions = (): Instrument[] => [];
const apiIds = new Set(apiItems.map((i) => String(i.id)));
const apiKeys = new Set(apiItems.map((i) => `${i.programmeId}::${(i.name || '').toLowerCase().trim()}`));
const remaining = local.filter(
(l) => !apiIds.has(String(l.id)) && !apiKeys.has(`${l.programmeId}::${(l.name || '').toLowerCase().trim()}`)
);
if (remaining.length !== local.length) {
localStorage.setItem(LOCAL_INSTRUCTIONS_KEY, JSON.stringify(remaining));
}
} catch (e) {
console.warn('Failed to purge stored instruction duplicates', e);
}
};
export const getStoredInstructions = (): Instrument[] => {
if (typeof window === 'undefined') return [];
try {
const raw = localStorage.getItem(LOCAL_INSTRUCTIONS_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
};
export const saveStoredInstruction = (inst: Instrument): void => {
if (typeof window === 'undefined') return;
try {
const current = getStoredInstructions();
const idx = current.findIndex(
(i) => String(i.id) === String(inst.id) ||
(String(i.programmeId) === String(inst.programmeId) && (i.name || '').trim().toLowerCase() === (inst.name || '').trim().toLowerCase())
);
if (idx >= 0) {
current[idx] = { ...current[idx], ...inst, updatedAt: new Date().toISOString() };
} else {
current.unshift(inst);
}
localStorage.setItem(LOCAL_INSTRUCTIONS_KEY, JSON.stringify(current));
} catch (e) {
console.warn('Failed to save stored instruction', e);
}
};
export const saveStoredInstruction = (_inst?: Instrument): void => {};
export const getInstruments = async (programmeId: string | number): Promise<Instrument[]> => {
let apiItems: Instrument[] = [];
let apiSucceeded = false;
try {
const hashedProgId = await toolsService.resolveProgrammeId(programmeId);
const res = await apiClient.get<Instrument[]>(`/v1/api/governance/instruments/${hashedProgId}`);
if (Array.isArray(res.data)) {
apiItems = res.data;
apiSucceeded = true;
}
return Array.isArray(res.data) ? res.data : [];
} catch (err) {
console.warn(`[getInstruments] Could not fetch instruments from API for prog ${programmeId}:`, err);
return [];
}
if (apiSucceeded) {
purgeStoredInstructionDuplicates(apiItems);
return apiItems;
}
// Fallback to local items only when API failed
const localItems = getStoredInstructions().filter(
(item) => String(item.programmeId) === String(programmeId)
);
return localItems;
};
export const getInstructions = getInstruments;
@@ -535,27 +472,17 @@ export const getAllInstruments = async (programmesList?: Programme[]): Promise<I
}
const map = new Map<string, Instrument>();
let hasApiData = false;
if (progList && progList.length > 0) {
const results = await Promise.allSettled(
progList.map((pr) => getInstruments(pr.id))
);
results.forEach((res) => {
if (res.status === 'fulfilled' && Array.isArray(res.value) && res.value.length > 0) {
hasApiData = true;
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
res.value.forEach((inst) => map.set(String(inst.id), inst));
}
});
}
if (hasApiData || map.size > 0) {
const allApiInstruments = Array.from(map.values());
purgeStoredInstructionDuplicates(allApiInstruments);
return allApiInstruments;
}
return getStoredInstructions();
return Array.from(map.values());
};
export const getAllInstructions = getAllInstruments;
@@ -569,40 +496,8 @@ export const createInstrument = async (payload: CreateInstrumentRequest): Promis
description: payload.description,
};
try {
const res = await apiClient.post<Instrument>('/v1/api/governance/instruments', body);
if (res.data && res.data.id) {
purgeStoredInstructionDuplicates([res.data]);
return res.data;
}
} catch (err: any) {
console.warn('[createInstrument] API create failed, falling back to local storage:', err);
const newId = Date.now();
const fallbackInstruction: Instrument = {
id: newId,
programmeId: toIntegerId(payload.programmeId),
code: code,
name: payload.name,
description: payload.description,
status: payload.status !== undefined ? Number(payload.status) : ActivityStatus.Active,
createdBy: payload.userId || 'system',
createdAt: new Date().toISOString(),
};
saveStoredInstruction(fallbackInstruction);
return fallbackInstruction;
}
// If API returned 200 without full body, return formatted item without saving duplicate to localStorage
return {
id: Date.now(),
programmeId: toIntegerId(payload.programmeId),
code: code,
name: payload.name,
description: payload.description,
status: payload.status !== undefined ? Number(payload.status) : ActivityStatus.Active,
createdBy: payload.userId || 'system',
createdAt: new Date().toISOString(),
};
const res = await apiClient.post<Instrument>('/v1/api/governance/instruments', body);
return res.data;
};
export const createInstruction = createInstrument;
@@ -619,32 +514,7 @@ export const updateInstrument = async (payload: UpdateInstrumentRequest): Promis
status: Number(payload.status),
};
try {
const res = await apiClient.put<Instrument>('/v1/api/governance/instruments', body);
if (res.data && res.data.id) {
return res.data;
}
} catch (err) {
console.warn('[updateInstrument] API update failed:', err);
}
const existing = getStoredInstructions().find((i) => String(i.id) === String(targetId));
const updatedInstrument: Instrument = {
id: targetId!,
programmeId: payload.programmeId ? toIntegerId(payload.programmeId) : (existing?.programmeId || 0),
code: payload.code || existing?.code || null,
name: payload.name,
description: payload.description,
status: Number(payload.status),
createdBy: existing?.createdBy || 'system',
createdAt: existing?.createdAt || new Date().toISOString(),
updatedBy: payload.userId || 'system',
updatedAt: new Date().toISOString(),
};
if (existing) {
saveStoredInstruction(updatedInstrument);
}
const res = await apiClient.put<Instrument>('/v1/api/governance/instruments', body);
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Funding Instrument',
@@ -653,7 +523,7 @@ export const updateInstrument = async (payload: UpdateInstrumentRequest): Promis
title: payload.name,
}).catch(console.warn);
}
return updatedInstrument;
return res.data;
};
export const updateInstruction = updateInstrument;
@@ -2246,75 +2116,20 @@ export const updateSiteVisit = async (payload: UpdateSiteVisitRequest): Promise<
}
};
const LOCAL_RULES_KEY = 'pfm_local_rules';
export const purgeStoredRuleDuplicates = (_apiRules?: Rule[]): void => {};
export const purgeStoredRuleDuplicates = (apiRules: Rule[]): void => {
if (typeof window === 'undefined' || !Array.isArray(apiRules) || apiRules.length === 0) return;
try {
const raw = localStorage.getItem(LOCAL_RULES_KEY);
if (!raw) return;
const local: Rule[] = JSON.parse(raw);
if (!Array.isArray(local) || local.length === 0) return;
export const getStoredRules = (): Rule[] => [];
const apiIds = new Set(apiRules.map((r) => String(r.id)));
const apiKeys = new Set(apiRules.map((r) => `${r.programmeId}::${(r.name || '').toLowerCase().trim()}`));
const remaining = local.filter(
(l) => !apiIds.has(String(l.id)) && !apiKeys.has(`${l.programmeId}::${(l.name || '').toLowerCase().trim()}`)
);
if (remaining.length !== local.length) {
localStorage.setItem(LOCAL_RULES_KEY, JSON.stringify(remaining));
}
} catch (e) {
console.warn('Failed to purge stored rule duplicates', e);
}
};
export const getStoredRules = (): Rule[] => {
if (typeof window === 'undefined') return [];
try {
const raw = localStorage.getItem(LOCAL_RULES_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
};
export const saveStoredRule = (rule: Rule): void => {
if (typeof window === 'undefined') return;
try {
const current = getStoredRules();
const idx = current.findIndex(
(r) => String(r.id) === String(rule.id) ||
(String(r.programmeId) === String(rule.programmeId) && (r.name || '').trim().toLowerCase() === (rule.name || '').trim().toLowerCase())
);
if (idx >= 0) {
current[idx] = { ...current[idx], ...rule, updatedAt: new Date().toISOString() };
} else {
current.unshift(rule);
}
localStorage.setItem(LOCAL_RULES_KEY, JSON.stringify(current));
} catch (e) {
console.warn('Failed to save stored rule', e);
}
};
export const saveStoredRule = (_rule?: Rule): void => {};
// ==========================================
// 15. Execution: Programme Rules APIs
// ==========================================
export const getRule = async (ruleId: string | number): Promise<Rule> => {
try {
const hashedRuleId = await toolsService.encodeId(ruleId);
const res = await apiClient.get<Rule>(`/v1/api/execution/rules/${hashedRuleId}`);
if (res.data) {
purgeStoredRuleDuplicates([res.data]);
return res.data;
}
} catch {
const localRule = getStoredRules().find((r) => String(r.id) === String(ruleId));
if (localRule) return localRule;
throw new Error('Rule not found');
const hashedRuleId = await toolsService.encodeId(ruleId);
const res = await apiClient.get<Rule>(`/v1/api/execution/rules/${hashedRuleId}`);
if (res.data) {
return res.data;
}
throw new Error('Rule not found');
};
@@ -2332,38 +2147,16 @@ export const getRules = async (
pagination: Pagination = { page: 1, pageSize: 100 },
programmeId?: string | number
): Promise<Rule[]> => {
let apiRules: Rule[] = [];
let apiSucceeded = false;
try {
const res = await apiClient.post<Rule[]>('/v1/api/execution/programmes/rules', {
index: pagination.index ? toIntegerId(pagination.index) : 0,
page: pagination.page || 1,
pageSize: pagination.pageSize || 100,
});
if (Array.isArray(res.data)) {
apiRules = res.data;
apiSucceeded = true;
}
} catch (err) {
console.warn('[getRules] Error fetching from backend API:', err);
}
if (apiSucceeded) {
purgeStoredRuleDuplicates(apiRules);
let result = apiRules;
if (programmeId != null && programmeId !== '' && programmeId !== 'all') {
result = result.filter((r) => String(r.programmeId) === String(programmeId));
}
return result;
}
// Fallback to local storage only if API call failed
const localRules = getStoredRules();
let combined = localRules;
const res = await apiClient.post<Rule[]>('/v1/api/execution/programmes/rules', {
index: pagination.index ? toIntegerId(pagination.index) : 0,
page: pagination.page || 1,
pageSize: pagination.pageSize || 100,
});
let result = Array.isArray(res.data) ? res.data : [];
if (programmeId != null && programmeId !== '' && programmeId !== 'all') {
combined = combined.filter((r) => String(r.programmeId) === String(programmeId));
result = result.filter((r) => String(r.programmeId) === String(programmeId));
}
return combined;
return result;
};
export const createRule = async (payload: CreateRuleRequest): Promise<Rule> => {
@@ -2375,70 +2168,21 @@ export const createRule = async (payload: CreateRuleRequest): Promise<Rule> => {
description: payload.description,
};
try {
const res = await apiClient.post<Rule>('/v1/api/execution/rules', body);
if (res.data && res.data.id) {
purgeStoredRuleDuplicates([res.data]);
return res.data;
}
} catch (err) {
console.warn('[createRule] API create failed, falling back to local storage:', err);
const newId = Date.now();
const fallbackRule: Rule = {
id: newId,
programmeId: toIntegerId(payload.programmeId),
name: payload.name,
description: payload.description,
status: ActivityStatus.Active,
version: 1,
createdBy: validUserId,
createdAt: new Date().toISOString(),
};
saveStoredRule(fallbackRule);
return fallbackRule;
}
return {
id: Date.now(),
programmeId: toIntegerId(payload.programmeId),
name: payload.name,
description: payload.description,
status: ActivityStatus.Active,
version: 1,
createdBy: validUserId,
createdAt: new Date().toISOString(),
};
const res = await apiClient.post<Rule>('/v1/api/execution/rules', body);
return res.data;
};
export const updateRule = async (payload: UpdateRuleRequest): Promise<void> => {
const validUserId = toValidUuid(payload.userId);
try {
await apiClient.put('/v1/api/execution/rules', {
ruleId: toIntegerId(payload.ruleId),
userId: validUserId,
name: payload.name,
description: payload.description,
status: Number(payload.status),
version: Number(payload.version || 1),
...(payload.programmeId ? { programmeId: toIntegerId(payload.programmeId) } : {}),
});
} catch (err) {
console.warn('[updateRule] API update failed:', err);
const existing = getStoredRules().find((r) => String(r.id) === String(payload.ruleId));
const updatedRule: Rule = {
id: payload.ruleId,
programmeId: payload.programmeId ? toIntegerId(payload.programmeId) : (existing?.programmeId || 0),
name: payload.name,
description: payload.description,
status: Number(payload.status),
version: Number(payload.version || 1),
createdBy: existing?.createdBy || validUserId,
createdAt: existing?.createdAt || new Date().toISOString(),
updatedBy: validUserId,
updatedAt: new Date().toISOString(),
};
saveStoredRule(updatedRule);
}
await apiClient.put('/v1/api/execution/rules', {
ruleId: toIntegerId(payload.ruleId),
userId: validUserId,
name: payload.name,
description: payload.description,
status: Number(payload.status),
version: Number(payload.version || 1),
...(payload.programmeId ? { programmeId: toIntegerId(payload.programmeId) } : {}),
});
};
// ==========================================