Files
pfm/PostFundManagement.ConsoleUI/src/services/api.ts
T
2026-10-25 18:58:30 +02:00

2218 lines
77 KiB
TypeScript

import axios, { AxiosError } from 'axios';
import { apiClient } from './httpClient';
import { toolsService } from '../utils/hashids';
import {
notifyTenantManagersAndExecutivesOnRecordStatusChange,
notifyInviteApproved,
notifyNewUserCreated,
isApprovedOrActiveStatus,
} from './notificationRulesService';
export {
notifyTenantManagersAndExecutivesOnRecordStatusChange,
notifyInviteApproved,
notifyNewUserCreated,
isApprovedOrActiveStatus,
};
import {
getCurrentUserSub,
getCurrentUserTenantId,
getCurrentUserHashedTenantId,
} from '../auth/AuthProvider';
export { getCurrentUserTenantId, getCurrentUserHashedTenantId };
import {
Dashboard,
Organisation,
CreateOrganisationRequest,
UpdateOrganisationRequest,
Portfolio,
CreatePortfolioRequest,
UpdatePortfolioRequest,
Programme,
CreateProgrammeRequest,
UpdateProgrammeRequest,
Instrument,
CreateInstrumentRequest,
UpdateInstrumentRequest,
Instruction,
CreateInstructionRequest,
UpdateInstructionRequest,
Project,
CreateProjectRequest,
UpdateProjectRequest,
UpdateProjectStatusRequest,
Activity,
CreateActivityRequest,
UpdateActivityRequest,
Milestone,
CreateMilestoneRequest,
UpdateMilestoneRequest,
Award,
CreateAwardRequest,
UpdateAwardRequest,
Contract,
ContractStatus,
CreateContractRequest,
UpdateContractRequest,
ExecuteContractRequest,
Budget,
CreateBudgetRequest,
UpdateBudgetRequest,
Disbursement,
CreateDisbursementRequest,
UpdateDisbursementRequest,
Indicator,
CreateIndicatorRequest,
Evidence,
UpdateEvidenceRequest,
Risk,
CreateRiskRequest,
UpdateRiskRequest,
User,
UserFilterRequest,
Invite,
InviteRequest,
InviteUserRequest,
AuditLog,
ProblemDetails,
ActivityStatus,
ApprovalStatus,
Tenant,
TenantOrganisation,
TenantUser,
CreateTenantRequest,
UpdateTenantRequest,
Beneficiary,
CreateBeneficiaryRequest,
UpdateBeneficiaryRequest,
Outcome,
CreateOutcomeRequest,
UpdateOutcomeRequest,
ChangeRequest,
CreateChangeRequest,
UpdateChangeRequest,
ComplianceItem,
CreateComplianceItemRequest,
UpdateComplianceItemRequest,
Issue,
CreateIssueRequest,
UpdateIssueRequest,
SiteVisit,
CreateSiteVisitRequest,
UpdateSiteVisitRequest,
Rule,
CreateRuleRequest,
UpdateRuleRequest,
Pagination,
Notification,
NotificationPlatform,
NotificationStatus,
Role,
Claim,
CreateUserRequest,
AddUserClaimRequest,
CreateNotificationRequest,
UpdateNotificationRequest,
} from '../types';
import {
encodeIdNumberHex,
encodeEntityId,
toSafeInt64String,
} from '../utils/hashids';
import {
parseFriendlyError,
FriendlyError,
inspectErrorResponseObject,
parseCommaDelimitedErrors,
} from '../utils/apiErrorParser';
import { registerUserEmail, registerUserEmails, getUserIdByEmail } from '../lib/formatters';
// Helper to extract ProblemDetails from Axios error or unknown error
export function parseApiError(error: unknown): ProblemDetails {
const returnObj = inspectErrorResponseObject(error);
if (returnObj) {
return {
type: returnObj.type || 'https://tools.ietf.org/html/rfc9110#section-15.5.1',
title: returnObj.title || 'Error',
status: returnObj.status || (axios.isAxiosError(error) ? error.response?.status : 400),
detail: returnObj.detail || null,
instance: returnObj.instance || null,
};
}
const friendly = parseFriendlyError(error);
return {
type: 'https://tools.ietf.org/html/rfc9110#section-15.5.1',
title: friendly.title,
detail: friendly.message,
status: friendly.statusCode || 500,
};
}
export { parseFriendlyError, inspectErrorResponseObject, parseCommaDelimitedErrors };
export type { FriendlyError };
// ==========================================
// 1. Executive Dashboard APIs
// ==========================================
export const getDashboard = async (): Promise<Dashboard> => {
const res = await apiClient.get<Dashboard>('/v1/api/governance/dashboard');
return res.data;
};
// ==========================================
// 2. Governance & Structure APIs
// ==========================================
/**
* Ensures an ID is converted to a canonical int64 integer number.
* Decodes HashIDs if a HashID string is passed, ensuring request body schemas
* (pattern: ^-?(?:0|[1-9]\d*)$) receive numeric integers, never raw HashIDs.
*/
export const toIntegerId = (id: number | string | null | undefined): number => {
return toolsService.decodeToNumberSync(id);
};
export const toNullableIntegerId = (id: number | string | null | undefined): number | null => {
if (id === null || id === undefined || id === '') return null;
return toolsService.decodeToNumberSync(id);
};
/**
* Normalizes an Organisation object from backend responses.
* Robustly maps status across variations (status, Status, activityStatus, approvalStatus, statusId, isActive).
*/
export const normalizeOrganisation = (raw: any): Organisation => {
if (!raw) return raw;
const status =
raw.status ??
raw.Status ??
raw.activityStatus ??
raw.ActivityStatus ??
raw.approvalStatus ??
raw.ApprovalStatus ??
raw.statusId ??
raw.StatusId ??
raw.state ??
raw.State ??
(raw.isActive !== undefined ? (raw.isActive ? 1 : 2) : undefined) ??
(raw.IsActive !== undefined ? (raw.IsActive ? 1 : 2) : undefined) ??
ActivityStatus.Active;
return {
...raw,
id: raw.id ?? raw.Id ?? raw.organisationId ?? raw.OrganisationId,
tenantId: raw.tenantId ?? raw.TenantId ?? raw.tenant_id ?? null,
name: raw.name ?? raw.Name ?? null,
registrationNo: raw.registrationNo ?? raw.RegistrationNo ?? raw.regNo ?? raw.RegNo ?? null,
email: raw.email ?? raw.Email ?? null,
userId: raw.userId ?? raw.UserId ?? raw.organisationUser ?? raw.OrganisationUser ?? raw.user_id ?? raw.linkedUser ?? raw.LinkedUser ?? null,
idNumber: raw.idNumber ?? raw.IdNumber ?? raw.id_number ?? null,
type: raw.type ?? raw.Type ?? raw.organisationType ?? raw.OrganisationType ?? 5,
status,
createdAt: raw.createdAt ?? raw.CreatedAt ?? new Date().toISOString(),
createdBy: raw.createdBy ?? raw.CreatedBy ?? '',
};
};
export const getOrganisations = async (page = 1, pageSize = 100): Promise<Organisation[]> => {
const res = await apiClient.get<any[]>('/v1/api/governance/organisations', {
params: { page, pageSize },
});
const list = res.data || [];
return list.map(normalizeOrganisation);
};
export const getOrganisation = async (organisationId?: string | number): Promise<Organisation> => {
const targetOrgId =
organisationId !== undefined && organisationId !== '' && organisationId !== null
? organisationId
: getCurrentUserTenantId();
if (targetOrgId === undefined || targetOrgId === null || targetOrgId === '') {
throw new Error('Organisation ID is required');
}
const hashedOrgId = await toolsService.resolveOrganisationId(targetOrgId);
const res = await apiClient.get<any>(`/v1/api/governance/organisations/${hashedOrgId}`);
return normalizeOrganisation(res.data);
};
export const createOrganisation = async (payload: CreateOrganisationRequest): Promise<Organisation> => {
const res = await apiClient.post('/v1/api/governance/organisations', {
name: payload.name,
registrationNo: payload.registrationNo,
email: payload.email,
idNumber: payload.idNumber || null,
organisationUser: payload.organisationUser,
type: payload.type !== undefined ? Number(payload.type) : 5,
});
return res.data;
};
export const updateOrganisation = async (payload: UpdateOrganisationRequest): Promise<Organisation> => {
const orgStatus = payload.status !== undefined ? Number(payload.status) : ActivityStatus.Active;
const res = await apiClient.put('/v1/api/governance/organisations', {
organisationId: toIntegerId(payload.organisationId),
name: payload.name,
registrationNo: payload.registrationNo,
email: payload.email,
idNumber: payload.idNumber || null,
organisationUser: payload.organisationUser,
type: Number(payload.type),
status: orgStatus,
});
if (isApprovedOrActiveStatus(orgStatus)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Organisation',
recordId: payload.organisationId,
newStatus: orgStatus,
title: payload.name,
}).catch(console.warn);
}
return res.data;
};
export const getOrganisationPortfolios = async (organisationId?: string | number): Promise<Portfolio[]> => {
const targetOrgId =
organisationId !== undefined && organisationId !== '' && organisationId !== null
? organisationId
: getCurrentUserTenantId();
if (targetOrgId === undefined || targetOrgId === null || targetOrgId === '') {
return [];
}
const hashedOrgId = await toolsService.resolveOrganisationId(targetOrgId);
try {
const res = await apiClient.get<Portfolio[]>(`/v1/api/governance/organisations/${hashedOrgId}/portfolio`);
return res.data || [];
} catch (err) {
// If hashed ID fails or is not recognized, attempt with raw targetOrgId
if (String(targetOrgId) !== String(hashedOrgId)) {
try {
const fallbackRes = await apiClient.get<Portfolio[]>(`/v1/api/governance/organisations/${targetOrgId}/portfolio`);
return fallbackRes.data || [];
} catch {
// ignore
}
}
console.warn('[getOrganisationPortfolios] Failed to retrieve portfolios for organisation:', err);
return [];
}
};
export const getPortfolios = async (
arg?: string | number | Pagination
): Promise<Portfolio[]> => {
if (arg === undefined || arg === null || typeof arg === 'object') {
return getAllPortfolios(typeof arg === 'object' ? arg : undefined);
}
return getOrganisationPortfolios(arg);
};
export const getPortfolio = async (portfolioId: string | number): Promise<Portfolio> => {
const hashedPortId = await toolsService.resolvePortfolioId(portfolioId);
const res = await apiClient.get<Portfolio | Portfolio[]>(`/v1/api/governance/portfolios/${hashedPortId}`);
return Array.isArray(res.data) ? res.data[0] : res.data;
};
export const getAllPortfolios = async (
pagination: Pagination = { page: 1, pageSize: 100, index: 0 }
): Promise<Portfolio[]> => {
try {
const res = await apiClient.post<Portfolio[]>('/v1/api/governance/organisations/portfolios', {
index: pagination.index ? toIntegerId(pagination.index) : 0,
page: pagination.page || 1,
pageSize: pagination.pageSize || 100,
});
if (Array.isArray(res.data)) {
return res.data;
}
} catch (err) {
console.warn('[getAllPortfolios] POST /v1/api/governance/organisations/portfolios failed, attempting fallback:', err);
try {
const getRes = await apiClient.get<Portfolio[]>('/v1/api/governance/organisations/portfolios', {
params: { page: pagination.page || 1, pageSize: pagination.pageSize || 100 },
});
if (Array.isArray(getRes.data)) {
return getRes.data;
}
} catch {
// Fallback to iterating organisations if bulk endpoint is unavailable
const orgs = await getOrganisations(1, 10);
if (orgs.length > 0) {
const results = await Promise.allSettled(
orgs.map((org) => getOrganisationPortfolios(org.id))
);
const combined = results
.filter((r): r is PromiseFulfilledResult<Portfolio[]> => r.status === 'fulfilled')
.flatMap((r) => r.value);
if (combined.length > 0) return combined;
}
}
}
return [];
};
export const getPortfolioProgrammes = async (portfolioId: string | number): Promise<Programme[]> => {
const hashedPortId = await toolsService.resolvePortfolioId(portfolioId);
const res = await apiClient.get<Programme[]>(`/v1/api/governance/portfolios/${hashedPortId}/programmes`);
return res.data || [];
};
export const createPortfolio = async (payload: CreatePortfolioRequest): Promise<Portfolio> => {
const userSub = getCurrentUserSub();
let ownerId = payload.ownerId;
if (ownerId && typeof ownerId === 'string' && ownerId.includes('@')) {
ownerId = getUserIdByEmail(ownerId) || userSub || ownerId;
}
const tenantId = getCurrentUserTenantId();
const orgId =
payload.organisationId !== undefined && payload.organisationId !== '' && payload.organisationId !== null
? toIntegerId(payload.organisationId)
: (tenantId !== null ? tenantId : 0);
const res = await apiClient.post('/v1/api/governance/organisations/portfolio', {
name: payload.name,
description: payload.description,
organisationId: orgId,
ownerId: ownerId || userSub || '00000000-0000-0000-0000-000000000000',
});
return res.data;
};
export const updatePortfolio = async (payload: UpdatePortfolioRequest): Promise<Portfolio> => {
const userSub = getCurrentUserSub();
let ownerId = payload.ownerId;
if (ownerId && typeof ownerId === 'string' && ownerId.includes('@')) {
ownerId = getUserIdByEmail(ownerId) || userSub || ownerId;
}
const res = await apiClient.put('/v1/api/governance/organisations/portfolio', {
portfolioId: toIntegerId(payload.portfolioId),
name: payload.name,
description: payload.description,
ownerId: ownerId || userSub || '00000000-0000-0000-0000-000000000000',
});
return res.data;
};
export const getProgrammes = async (portfolioId: string | number): Promise<Programme[]> => {
const hashedPortId = await toolsService.resolvePortfolioId(portfolioId);
const res = await apiClient.get<Programme[]>(`/v1/api/governance/programmes/${hashedPortId}`);
return res.data || [];
};
export const getPortfolioValue = async (portfolioId: string | number): Promise<number> => {
const hashedPortId = await toolsService.resolvePortfolioId(portfolioId);
try {
const res = await apiClient.get<number>(`/v1/api/governance/portfolios/${hashedPortId}/value`);
return typeof res.data === 'number' ? res.data : Number(res.data) || 0;
} catch (err) {
if (String(portfolioId) !== String(hashedPortId)) {
try {
const fallbackRes = await apiClient.get<number>(`/v1/api/governance/portfolios/${portfolioId}/value`);
return typeof fallbackRes.data === 'number' ? fallbackRes.data : Number(fallbackRes.data) || 0;
} catch {
// ignore
}
}
console.warn('[getPortfolioValue] Failed to get portfolio value:', err);
return 0;
}
};
export const getProgrammeValue = async (programmeId: string | number): Promise<number> => {
const hashedProgId = await toolsService.resolveProgrammeId(programmeId);
try {
const res = await apiClient.get<number>(`/v1/api/governance/programmes/${hashedProgId}/value`);
return typeof res.data === 'number' ? res.data : Number(res.data) || 0;
} catch (err) {
if (String(programmeId) !== String(hashedProgId)) {
try {
const fallbackRes = await apiClient.get<number>(`/v1/api/governance/programmes/${programmeId}/value`);
return typeof fallbackRes.data === 'number' ? fallbackRes.data : Number(fallbackRes.data) || 0;
} catch {
// ignore
}
}
console.warn('[getProgrammeValue] Failed to get programme value:', err);
return 0;
}
};
export const purgeStoredInstructionDuplicates = (_apiItems?: Instrument[]): void => {};
export const getStoredInstructions = (): Instrument[] => [];
export const saveStoredInstruction = (_inst?: Instrument): void => {};
export const getInstruments = async (programmeId: string | number): Promise<Instrument[]> => {
try {
const hashedProgId = await toolsService.resolveProgrammeId(programmeId);
const res = await apiClient.get<Instrument[]>(`/v1/api/governance/instruments/${hashedProgId}`);
return Array.isArray(res.data) ? res.data : [];
} catch (err) {
console.warn(`[getInstruments] Could not fetch instruments from API for prog ${programmeId}:`, err);
return [];
}
};
export const getInstructions = getInstruments;
export const getAllInstruments = async (programmesList?: Programme[]): Promise<Instrument[]> => {
let progList = programmesList;
if (!progList || progList.length === 0) {
try {
progList = await getAllProgrammes(1, 100);
} catch {
progList = [];
}
}
const map = new Map<string, Instrument>();
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.forEach((inst) => map.set(String(inst.id), inst));
}
});
}
return Array.from(map.values());
};
export const getAllInstructions = getAllInstruments;
export const createInstrument = async (payload: CreateInstrumentRequest): Promise<Instrument> => {
const code = payload.code || `INST-${Date.now().toString().slice(-4)}`;
const body = {
programmeId: toIntegerId(payload.programmeId),
name: payload.name,
code: code,
description: payload.description,
};
const res = await apiClient.post<Instrument>('/v1/api/governance/instruments', body);
return res.data;
};
export const createInstruction = createInstrument;
export const updateInstrument = async (payload: UpdateInstrumentRequest): Promise<Instrument> => {
const targetId = payload.instructionId ?? payload.instrumentId ?? payload.id;
const numId = toIntegerId(targetId);
const body = {
instructionId: numId,
code: payload.code || '',
name: payload.name,
description: payload.description,
status: Number(payload.status),
};
const res = await apiClient.put<Instrument>('/v1/api/governance/instruments', body);
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Funding Instrument',
recordId: targetId!,
newStatus: payload.status,
title: payload.name,
}).catch(console.warn);
}
return res.data;
};
export const updateInstruction = updateInstrument;
// ==========================================
// 3. Project & Milestone Management APIs
// ==========================================
export const getAllProgrammes = async (page = 1, pageSize = 100): Promise<Programme[]> => {
const res = await apiClient.get<Programme[]>(`/v1/api/projects/programme/${page}/${pageSize}`);
return res.data || [];
};
export const getProgramme = async (programmeId: string | number): Promise<Programme> => {
const hashedProgId = await toolsService.resolveProgrammeId(programmeId);
const res = await apiClient.get<Programme>(`/v1/api/projects/programme/${hashedProgId}`);
return res.data;
};
export const getProgrammeProjects = async (programmeId: string | number): Promise<Project[]> => {
const hashedProgId = await toolsService.resolveProgrammeId(programmeId);
const res = await apiClient.get<Project[]>(`/v1/api/projects/${hashedProgId}/programme`);
return res.data || [];
};
export const createProgramme = async (payload: CreateProgrammeRequest): Promise<Programme> => {
const userSub = getCurrentUserSub();
let ownerId = payload.ownerId;
if (ownerId && typeof ownerId === 'string' && ownerId.includes('@')) {
ownerId = getUserIdByEmail(ownerId) || userSub || ownerId;
}
const res = await apiClient.post('/v1/api/projects/programme', {
portfolioId: toIntegerId(payload.portfolioId),
name: payload.name,
ownerId: ownerId || userSub || '00000000-0000-0000-0000-000000000000',
});
return res.data;
};
export const updateProgramme = async (payload: UpdateProgrammeRequest): Promise<Programme> => {
const userSub = getCurrentUserSub();
let ownerId = payload.ownerId;
if (ownerId && typeof ownerId === 'string' && ownerId.includes('@')) {
ownerId = getUserIdByEmail(ownerId) || userSub || ownerId;
}
const progStatus = Number(payload.status) || 1;
const res = await apiClient.put('/v1/api/projects/programme', {
programmeId: toIntegerId(payload.programmeId),
name: payload.name,
ownerId: ownerId || userSub || '00000000-0000-0000-0000-000000000000',
status: progStatus,
});
if (isApprovedOrActiveStatus(progStatus)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Programme',
recordId: payload.programmeId,
newStatus: progStatus,
title: payload.name,
}).catch(console.warn);
}
return res.data;
};
export const getProjects = async (page = 1, pageSize = 100): Promise<Project[]> => {
const res = await apiClient.get<Project[]>('/v1/api/projects', {
params: { page, pageSize },
});
return res.data || [];
};
export const getProject = async (projectId: string | number): Promise<Project> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
const res = await apiClient.get<Project | Project[]>(`/v1/api/projects/${hashedProjectId}`);
return Array.isArray(res.data) ? res.data[0] : res.data;
};
export const createProject = async (payload: CreateProjectRequest): Promise<Project> => {
const res = await apiClient.post('/v1/api/projects', {
programmeId: toIntegerId(payload.programmeId),
name: payload.name,
description: payload.description,
sector: Number(payload.sector),
projectType: Number(payload.projectType),
});
return res.data;
};
export const updateProject = async (payload: UpdateProjectRequest): Promise<Project> => {
const startedAt = payload.startedAt
? (payload.startedAt.includes('T') ? payload.startedAt : new Date(payload.startedAt).toISOString())
: null;
const endedAt = payload.endedAt
? (payload.endedAt.includes('T') ? payload.endedAt : new Date(payload.endedAt).toISOString())
: null;
const res = await apiClient.put('/v1/api/projects', {
projectId: toIntegerId(payload.projectId),
name: payload.name,
description: payload.description,
startedAt,
endedAt,
sector: Number(payload.sector),
projectType: Number(payload.projectType),
});
return res.data;
};
export const updateProjectStatus = async (payload: UpdateProjectStatusRequest): Promise<void> => {
await apiClient.put('/v1/api/projects/status', {
projectId: toIntegerId(payload.projectId),
status: Number(payload.status),
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Project',
recordId: payload.projectId,
newStatus: payload.status,
}).catch(console.warn);
}
};
export const getActivities = async (projectId: string | number): Promise<Activity[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
const res = await apiClient.get<Activity[]>(`/v1/api/projects/${hashedProjectId}/activities`);
return res.data || [];
};
export const getActivity = async (activityId: string | number): Promise<Activity> => {
const hashedActivityId = await toolsService.encodeId(activityId);
const res = await apiClient.get<Activity>(`/v1/api/projects/activities/${hashedActivityId}`);
return res.data;
};
export const createActivity = async (payload: CreateActivityRequest): Promise<Activity> => {
const performedAt = payload.performedAt
? (payload.performedAt.includes('T') ? payload.performedAt : new Date(payload.performedAt).toISOString())
: null;
const body = {
projectId: toIntegerId(payload.projectId),
milestoneId: toNullableIntegerId(payload.milestoneId),
category: payload.category || 'Operations',
amount: Number(payload.amount) || 0,
title: payload.title,
description: payload.description || '',
performedAt,
};
const res = await apiClient.post('/v1/api/projects/activities', body);
return res.data;
};
export const updateActivity = async (payload: UpdateActivityRequest): Promise<Activity> => {
const performedAt = payload.performedAt
? (payload.performedAt.includes('T') ? payload.performedAt : new Date(payload.performedAt).toISOString())
: null;
const body = {
projectId: toIntegerId(payload.projectId),
milestoneId: toNullableIntegerId(payload.milestoneId),
category: payload.category || 'Operations',
amount: Number(payload.amount) || 0,
title: payload.title,
description: payload.description || '',
performedAt,
};
try {
const res = await apiClient.put('/v1/api/projects/activities', body);
return res.data;
} catch (err: any) {
if (err?.response?.status === 404 || err?.response?.status === 405) {
const res = await apiClient.put('/v1/api/projects/activity', body);
return res.data;
}
throw err;
}
};
export const getMilestones = async (projectId: string | number): Promise<Milestone[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
const res = await apiClient.get<Milestone[]>(`/v1/api/projects/${hashedProjectId}/milestones`);
return res.data || [];
};
export const getAllMilestones = async (page = 1, pageSize = 100): Promise<Milestone[]> => {
const res = await apiClient.get<Milestone[]>(`/v1/api/projects/milestones/${page}/${pageSize}`);
return res.data || [];
};
export const getMilestone = async (milestoneId: string | number): Promise<Milestone> => {
const hashedMilestoneId = await toolsService.encodeId(milestoneId);
const res = await apiClient.get<Milestone>(`/v1/api/projects/milestones/${hashedMilestoneId}`);
return res.data;
};
export const createMilestone = async (payload: CreateMilestoneRequest): Promise<Milestone> => {
const dueAtIso = payload.dueAt.includes('T')
? payload.dueAt
: new Date(payload.dueAt).toISOString();
const res = await apiClient.post('/v1/api/evidence/milestones', {
projectId: toIntegerId(payload.projectId),
dueAt: dueAtIso,
name: payload.name,
priority: Number(payload.priority) || 1,
});
return res.data;
};
export const updateMilestone = async (payload: UpdateMilestoneRequest): Promise<void> => {
const dueAtIso = payload.dueAt.includes('T')
? payload.dueAt
: new Date(payload.dueAt).toISOString();
await apiClient.put('/v1/api/projects/milestone', {
milestoneId: toIntegerId(payload.milestoneId),
dueAt: dueAtIso,
name: payload.name,
priority: Number(payload.priority) || 1,
status: Number(payload.status) || 1,
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Milestone',
recordId: payload.milestoneId,
newStatus: payload.status,
title: payload.name,
}).catch(console.warn);
}
};
export const getProjectIndicators = async (projectId: string | number): Promise<Indicator[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
const res = await apiClient.get<Indicator[]>(`/v1/api/projects/indicators/project/${hashedProjectId}`);
return res.data || [];
};
export const getProjectRisks = async (projectId: string | number): Promise<Risk[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
const res = await apiClient.get<any[]>(`/v1/api/projects/${hashedProjectId}/risks`);
const list = res.data || [];
return list.map(normalizeRisk);
};
// ==========================================
// 4. Grant & Financial Management APIs
// ==========================================
export const getAwards = async (page = 1, pageSize = 100): Promise<Award[]> => {
const res = await apiClient.get<Award[]>('/v1/api/grants/awards', {
params: { page, pageSize },
});
return res.data || [];
};
export const getAward = async (awardId: string | number): Promise<Award> => {
const hashedAwardId = await toolsService.resolveAwardId(awardId);
const res = await apiClient.get<Award>(`/v1/api/grants/awards/${hashedAwardId}`);
return res.data;
};
export const createAward = async (payload: CreateAwardRequest): Promise<Award> => {
const tenantId = getCurrentUserTenantId();
const orgId =
payload.organisationId !== undefined && payload.organisationId !== '' && payload.organisationId !== null
? toIntegerId(payload.organisationId)
: (tenantId !== null ? tenantId : 0);
const res = await apiClient.post('/v1/api/grants/awards', {
organisationId: orgId,
programmeId: toIntegerId(payload.programmeId),
amount: Number(payload.amount),
});
return res.data;
};
export const updateAward = async (payload: UpdateAwardRequest): Promise<void> => {
const numericAwardId = toIntegerId(payload.awardId);
const numericOrgId = toIntegerId(payload.organisationId);
const numericProgId = toIntegerId(payload.programmeId);
await apiClient.put('/v1/api/grants/awards', {
awardId: numericAwardId,
organisationId: numericOrgId,
programmeId: numericProgId,
amount: Number(payload.amount),
...(payload.status !== undefined ? { status: Number(payload.status) } : {}),
});
if (payload.status === undefined || isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Award',
recordId: payload.awardId,
newStatus: payload.status !== undefined ? payload.status : ApprovalStatus.Approved,
}).catch(console.warn);
}
};
export const fetchHashId = async (id64: number | string): Promise<string> => {
try {
const res = await apiClient.get<string>(`/v1/api/tools/hashid/${id64}`);
if (res.data && typeof res.data === 'string') {
return res.data;
}
} catch {
// Fall back to client-side deterministic Hashids
}
return toolsService.encodeIdSync(id64);
};
export const getContracts = async (page = 1, pageSize = 100): Promise<Contract[]> => {
const res = await apiClient.get<Contract[]>('/v1/api/grants/contracts', {
params: { page, pageSize },
});
return res.data || [];
};
export const getContract = async (contractId: string | number): Promise<Contract> => {
const hashedContractId = await toolsService.resolveContractId(contractId);
const res = await apiClient.get<Contract>(`/v1/api/grants/contracts/${hashedContractId}`);
return res.data;
};
export const createContract = async (payload: CreateContractRequest): Promise<Contract> => {
// Ensure awardId is numeric int64 as required by OpenAPI schema (pattern: ^-?(?:0|[1-9]\d*)$)
let numericAwardId: number = Number(payload.awardId);
if (isNaN(numericAwardId) || !/^-?\d+$/.test(String(payload.awardId))) {
const decoded = toolsService.decodeIdSync(String(payload.awardId));
if (decoded && /^-?\d+$/.test(String(decoded))) {
numericAwardId = Number(decoded);
}
}
const effectiveAtIso = payload.effectiveAt.includes('T')
? payload.effectiveAt
: new Date(payload.effectiveAt).toISOString();
const expiresAtIso = payload.expiresAt.includes('T')
? payload.expiresAt
: new Date(payload.expiresAt).toISOString();
const res = await apiClient.post('/v1/api/grants/contracts', {
awardId: !isNaN(numericAwardId) ? numericAwardId : payload.awardId,
totalValue: Number(payload.totalValue),
effectiveAt: effectiveAtIso,
expiresAt: expiresAtIso,
});
// Extract ID from headers (e.g. Location or Content-Location) if present
const locationHeader = res.headers?.['location'] || res.headers?.['Location'];
let headerId: string | undefined;
if (locationHeader && typeof locationHeader === 'string') {
const parts = locationHeader.split('/');
headerId = parts[parts.length - 1];
}
const hashedAwardId = toolsService.encodeIdSync(numericAwardId || payload.awardId);
// Case 1: Backend returns string contractId/hash directly
if (typeof res.data === 'string' && res.data.trim().length > 0) {
const rawStr = res.data.trim();
return {
id: rawStr,
contractId: rawStr,
awardId: hashedAwardId,
totalValue: Number(payload.totalValue),
effectiveAt: effectiveAtIso,
expiresAt: expiresAtIso,
status: ContractStatus.Draft,
} as unknown as Contract;
}
// Case 2: Backend returns object
if (res.data && typeof res.data === 'object') {
const candidateId =
res.data.id ??
res.data.contractId ??
res.data.Id ??
res.data.ContractId ??
headerId;
if (candidateId) {
return {
...res.data,
id: candidateId,
contractId: candidateId,
awardId: res.data.awardId || hashedAwardId,
};
}
}
// Case 3: ID extracted from Location header
if (headerId) {
return {
id: headerId,
contractId: headerId,
awardId: hashedAwardId,
totalValue: Number(payload.totalValue),
effectiveAt: effectiveAtIso,
expiresAt: expiresAtIso,
status: ContractStatus.Draft,
} as unknown as Contract;
}
// Case 4: If backend returns empty 200 OK without body, locate newly created contract by awardId
try {
const existing = await getContracts(1, 10);
const matched = existing.find(
(c) =>
String(c.awardId) === String(hashedAwardId) ||
String(c.awardId) === String(numericAwardId) ||
String(c.awardId) === String(payload.awardId)
);
if (matched) return matched;
} catch {}
return (res.data || {
id: hashedAwardId,
contractId: hashedAwardId,
awardId: hashedAwardId,
totalValue: Number(payload.totalValue),
effectiveAt: effectiveAtIso,
expiresAt: expiresAtIso,
status: ContractStatus.Draft,
}) as unknown as Contract;
};
export const updateContract = async (payload: UpdateContractRequest): Promise<void> => {
await apiClient.put('/v1/api/grants/contracts', {
contractId: toIntegerId(payload.contractId),
status: Number(payload.status),
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Contract',
recordId: payload.contractId,
newStatus: payload.status,
}).catch(console.warn);
}
};
export const uploadContractFile = async (
contractId: string | number,
file: File | FormData
): Promise<void> => {
const hashedContractId = await toolsService.resolveContractId(contractId);
const formData = file instanceof FormData ? file : new FormData();
if (file instanceof File) {
formData.append('file', file);
}
await apiClient.post(`/v1/api/grants/contracts/${hashedContractId}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
};
export const getContractSecureLink = async (contractId: string | number): Promise<string> => {
const hashedContractId = await toolsService.resolveContractId(contractId);
const res = await apiClient.get<string | { url?: string; link?: string }>(
`/v1/api/grants/contracts/${hashedContractId}/securelink`
);
if (typeof res.data === 'string') return res.data;
return (res.data as any)?.url || (res.data as any)?.link || String(res.data);
};
export const executeContract = async (payload: ExecuteContractRequest): Promise<void> => {
await apiClient.post('/v1/api/grants/contracts/execute', {
contractId: toIntegerId(payload.contractId),
externalSignatureId: payload.externalSignatureId,
signedDocumentUrl: payload.signedDocumentUrl,
});
};
export const getBudgets = async (page = 1, pageSize = 100): Promise<Budget[]> => {
const res = await apiClient.get<Budget[]>('/v1/api/grants/budgets', {
params: { page, pageSize },
});
return res.data || [];
};
export const getBudget = async (budgetId: string | number): Promise<Budget> => {
const hashedBudgetId = await toolsService.encodeId(budgetId);
const res = await apiClient.get<Budget>(`/v1/api/grants/budgets/${hashedBudgetId}`);
return res.data;
};
export const createBudget = async (payload: CreateBudgetRequest): Promise<Budget> => {
const res = await apiClient.post('/v1/api/grants/budgets', {
projectId: toIntegerId(payload.projectId),
name: payload.name,
amount: Number(payload.amount),
});
return res.data;
};
export const updateBudget = async (payload: UpdateBudgetRequest): Promise<void> => {
await apiClient.put('/v1/api/grants/budgets', {
budgetId: toIntegerId(payload.budgetId),
status: Number(payload.status),
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Budget',
recordId: payload.budgetId,
newStatus: payload.status,
}).catch(console.warn);
}
};
// ==========================================
// 5. Execution (Disbursements & Risks) APIs
// ==========================================
export const getDisbursements = async (page = 1, pageSize = 100): Promise<Disbursement[]> => {
const res = await apiClient.get<Disbursement[]>('/v1/api/execution/disbursements', {
params: { page, pageSize },
});
return res.data || [];
};
export const getDisbursement = async (disbursementId: string | number): Promise<Disbursement> => {
const hashedDisbId = await toolsService.encodeId(disbursementId);
const res = await apiClient.get<Disbursement>(`/v1/api/execution/disbursements/${hashedDisbId}`);
return res.data;
};
export const createDisbursement = async (payload: CreateDisbursementRequest): Promise<Disbursement> => {
const res = await apiClient.post('/v1/api/execution/disbursements', {
projectId: toIntegerId(payload.projectId),
milestoneId: toNullableIntegerId(payload.milestoneId),
amount: Number(payload.amount),
});
return res.data;
};
export const updateDisbursement = async (payload: UpdateDisbursementRequest): Promise<void> => {
await apiClient.put('/v1/api/execution/disbursements', {
disbursementId: toIntegerId(payload.disbursementId),
milestoneId: toNullableIntegerId(payload.milestoneId),
amount: Number(payload.amount),
});
};
export const approveDisbursement = async (disbursementId: string | number): Promise<void> => {
const hashedDisbId = await toolsService.encodeId(disbursementId);
await apiClient.post(`/v1/api/execution/disbursements/${hashedDisbId}/approve`, {});
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Disbursement',
recordId: disbursementId,
newStatus: ApprovalStatus.Approved,
}).catch(console.warn);
};
/**
* Normalizes a Risk record from backend responses.
* Robustly parses Likelihood (1-5) and Impact (1-5), supporting strings, numbers, and case variations.
*/
export const normalizeRisk = (raw: any): Risk => {
if (!raw) return raw;
const rawLik = raw.likelihood ?? raw.Likelihood ?? raw.riskLikelihood ?? raw.RiskLikelihood;
const rawImp = raw.impact ?? raw.Impact ?? raw.riskImpact ?? raw.RiskImpact;
const parseScore = (val: any, defaultVal: number): number => {
if (typeof val === 'number' && !isNaN(val)) {
return Math.min(5, Math.max(1, Math.round(val)));
}
if (typeof val === 'string') {
const parsed = parseInt(val, 10);
if (!isNaN(parsed) && parsed >= 1 && parsed <= 5) {
return parsed;
}
const lower = val.toLowerCase().trim();
if (lower.includes('rare') || lower.includes('insignificant') || lower.includes('trivial') || lower === '1') return 1;
if (lower.includes('unlikely') || lower.includes('minor') || lower === '2') return 2;
if (lower.includes('possible') || lower.includes('moderate') || lower.includes('medium') || lower === '3') return 3;
if (lower.includes('likely') || lower.includes('major') || lower.includes('high') || lower === '4') return 4;
if (lower.includes('almost') || lower.includes('certain') || lower.includes('severe') || lower.includes('critical') || lower.includes('catastrophic') || lower === '5') return 5;
}
return defaultVal;
};
const likelihood = parseScore(rawLik, 3);
const impact = parseScore(rawImp, 2);
return {
...raw,
id: raw.id ?? raw.Id ?? raw.riskId ?? raw.RiskId,
projectId: raw.projectId ?? raw.ProjectId,
name: raw.name ?? raw.Name ?? null,
description: raw.description ?? raw.Description ?? null,
likelihood,
impact,
createdBy: raw.createdBy ?? raw.CreatedBy ?? '',
createdAt: raw.createdAt ?? raw.CreatedAt ?? new Date().toISOString(),
owner: raw.owner ?? raw.Owner ?? raw.riskOwner ?? raw.RiskOwner ?? raw.createdBy ?? raw.CreatedBy ?? '',
mitigationAction: raw.mitigationAction ?? raw.MitigationAction ?? raw.mitigation ?? raw.Mitigation ?? '',
status: raw.status ?? raw.Status,
};
};
export const getAllRisks = async (page = 1, pageSize = 100): Promise<Risk[]> => {
const res = await apiClient.get<any[]>('/v1/api/execution/risks', {
params: { page, pageSize },
});
const list = res.data || [];
return list.map(normalizeRisk);
};
export const getRisk = async (riskId: string | number): Promise<Risk> => {
const hashedRiskId = await toolsService.encodeId(riskId);
const res = await apiClient.get<any>(`/v1/api/execution/risks/${hashedRiskId}`);
return normalizeRisk(res.data);
};
export const createRisk = async (payload: CreateRiskRequest): Promise<Risk> => {
const res = await apiClient.post('/v1/api/execution/risks', {
projectId: toIntegerId(payload.projectId),
likelihood: Number(payload.likelihood),
impact: Number(payload.impact),
name: payload.name,
description: payload.description || '',
});
return normalizeRisk(res.data);
};
export const updateRisk = async (payload: UpdateRiskRequest): Promise<void> => {
await apiClient.put('/v1/api/execution/risks', {
riskId: toIntegerId(payload.riskId),
likelihood: Number(payload.likelihood),
impact: Number(payload.impact),
name: payload.name,
description: payload.description || '',
});
};
// ==========================================
// 6. Evidence & M&E APIs
// ==========================================
export const getAllIndicators = async (page = 1, pageSize = 100): Promise<Indicator[]> => {
const res = await apiClient.get<Indicator[]>(`/v1/api/evidence/indicators/${page}/${pageSize}`);
return res.data || [];
};
export const getIndicator = async (indicatorId: string | number): Promise<Indicator> => {
const hashedIndId = await toolsService.encodeId(indicatorId);
const res = await apiClient.get<Indicator>(`/v1/api/evidence/indicators/${hashedIndId}`);
return res.data;
};
export const createIndicator = async (payload: CreateIndicatorRequest): Promise<Indicator> => {
const res = await apiClient.post('/v1/api/evidence/indicators', {
projectId: toIntegerId(payload.projectId),
activityId: toNullableIntegerId(payload.activityId),
name: payload.name,
unitOfMeasure: Number(payload.unitOfMeasure) || 1,
baselineAmount: Number(payload.baselineAmount) || 0,
targetAmount: Number(payload.targetAmount) || 0,
targetCost: Number(payload.targetCost) || 0,
});
return res.data;
};
export const recordIndicatorActual = async (indicatorId: string | number, actualAmount: number): Promise<void> => {
const hashedIndId = await toolsService.encodeId(indicatorId);
await apiClient.post(`/v1/api/evidence/indicators/${hashedIndId}/actuals/${actualAmount}`, {});
};
export const getEvidenceFiles = async (page = 1, pageSize = 100): Promise<Evidence[]> => {
try {
const res = await apiClient.get<Evidence[]>(`/v1/api/evidence/files/${page}/${pageSize}`);
return res.data || [];
} catch (err: any) {
if (err?.response?.status === 404) {
return [];
}
throw err;
}
};
export const getAllEvidence = async (page = 1, pageSize = 100): Promise<Evidence[]> => {
return getEvidenceFiles(page, pageSize);
};
export const getEvidence = async (evidenceId: string | number): Promise<Evidence> => {
const hashedEvId = await toolsService.encodeId(evidenceId);
const res = await apiClient.get<Evidence>(`/v1/api/evidence/${hashedEvId}`);
return res.data;
};
export const getEvidenceSecureLink = async (evidenceId: string | number): Promise<string> => {
const hashedEvId = await toolsService.encodeId(evidenceId);
const res = await apiClient.get<string>(`/v1/api/evidence/${hashedEvId}/securelink`);
return res.data;
};
export const getMilestoneEvidence = async (milestoneId: string | number): Promise<Evidence[]> => {
try {
const hashedMId = await toolsService.encodeId(milestoneId);
const res = await apiClient.get<Evidence[]>(`/v1/api/evidence/milestone/${hashedMId}`);
return res.data || [];
} catch (err: any) {
if (err?.response?.status === 404) {
return [];
}
throw err;
}
};
export const getProjectEvidence = async (projectId: string | number): Promise<Evidence[]> => {
try {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
const res = await apiClient.get<Evidence[]>(`/v1/api/evidence/projects/${hashedProjectId}`);
return res.data || [];
} catch (err: any) {
// If hashed path parameter returns 404 or 400 and projectId differs, attempt with raw ID
if ((err?.response?.status === 404 || err?.response?.status === 400) && String(projectId) !== '') {
try {
const rawRes = await apiClient.get<Evidence[]>(`/v1/api/evidence/projects/${projectId}`);
return rawRes.data || [];
} catch (fallbackErr: any) {
if (fallbackErr?.response?.status === 404) {
return [];
}
}
}
if (err?.response?.status === 404) {
return [];
}
throw err;
}
};
export const uploadEvidenceFile = async (formData: FormData): Promise<Evidence> => {
const userSub = getCurrentUserSub();
if (userSub && !formData.get('userId')) {
formData.append('userId', userSub);
}
const res = await apiClient.put<Evidence>('/v1/api/evidence/file', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
};
export const updateEvidence = async (payload: UpdateEvidenceRequest): Promise<void> => {
await apiClient.put('/v1/api/evidence', {
evidenceId: toIntegerId(payload.evidenceId),
version: Number(payload.version) || 1,
evidenceType: Number(payload.evidenceType),
status: Number(payload.status),
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Evidence',
recordId: payload.evidenceId,
newStatus: payload.status,
}).catch(console.warn);
}
};
// ==========================================
// 7. Users & Background Integration APIs
// ==========================================
export const getUsers = async (
page = 1,
pageSize = 100,
filter: UserFilterRequest = {}
): Promise<User[]> => {
const tenantId = getCurrentUserTenantId();
// Unless otherwise specified by the user, ensure tenant_id is passed into endpoints where organisationId is required for system activities
const orgId =
filter.organisationId !== undefined
? (filter.organisationId !== null && filter.organisationId !== ''
? toIntegerId(filter.organisationId)
: null)
: tenantId;
const requestBody: Record<string, any> = {
...filter,
};
if (orgId !== null && orgId !== undefined) {
requestBody.organisationId = orgId;
}
// HashID query parameter for query string protection
const queryParams: Record<string, any> = {};
if (orgId !== null && orgId !== undefined) {
const hashedOrgId = await toolsService.encodeId(orgId);
if (hashedOrgId) {
queryParams.organisationId = hashedOrgId;
}
}
const res = await apiClient.post<User[]>(
`/v1/api/users/${page}/${pageSize}`,
requestBody,
{ params: Object.keys(queryParams).length > 0 ? queryParams : undefined }
);
const list = res.data || [];
registerUserEmails(list);
return list;
};
export const getUser = async (userId: string): Promise<User> => {
const res = await apiClient.get<User>(`/v1/api/users/${userId}`);
if (res.data?.id && res.data?.email) {
registerUserEmail(res.data.id, res.data.email);
}
return res.data;
};
export const getCurrentUser = async (): Promise<User> => {
const userSub = getCurrentUserSub();
if (userSub) {
try {
const u = await getUser(userSub);
if (u?.id && u?.email) {
registerUserEmail(u.id, u.email);
}
return u;
} catch {
// Fallback
}
}
return {
id: userSub || 'user-1',
email: null,
status: ActivityStatus.Active,
lastLoginAt: new Date().toISOString(),
};
};
export const updateUserStatus = async (userId: string, status: ActivityStatus | number): Promise<void> => {
await apiClient.get(`/v1/api/users/${userId}/status/${status}`);
};
export const createUser = async (
tenantId: number | string,
idNumber: string,
payload: CreateUserRequest
): Promise<void> => {
const encodedTenant = encodeEntityId(tenantId) || String(tenantId);
const encodedIdNumber = encodeIdNumberHex(idNumber);
// Normalize and sanitize userName: Duende / Skoruba Identity Server rejects usernames with spaces
const rawEmail = payload.email?.trim().toLowerCase() || '';
const rawUserName = payload.userName?.trim();
let sanitizedUserName = rawUserName
? rawUserName.toLowerCase().replace(/\s+/g, '.').replace(/[^a-z0-9_.\-@+]/g, '')
: rawEmail.split('@')[0];
if (!sanitizedUserName) {
sanitizedUserName = rawEmail;
}
await apiClient.post(`/v1/api/users/${encodeURIComponent(encodedTenant)}/${encodeURIComponent(encodedIdNumber)}`, {
id: payload.id || null,
userName: sanitizedUserName,
email: rawEmail || null,
emailConfirmed: payload.emailConfirmed ?? true,
phoneNumber: payload.phoneNumber || null,
phoneNumberConfirmed: payload.phoneNumberConfirmed ?? false,
LockoutEnabled: payload.LockoutEnabled ?? false,
twoFactorEnabled: payload.twoFactorEnabled ?? false,
accessFailedCount: payload.accessFailedCount !== undefined ? Number(payload.accessFailedCount) : 0,
lockoutEnd: payload.lockoutEnd || null,
});
};
import {
fetchAndCacheSystemRoles,
fetchAndCacheRoleClaims,
getSystemClaims,
getCachedSystemRoles,
getCachedRoleClaims,
preloadAndCacheAllRoleClaims,
} from './roleClaimsCache';
export {
getSystemClaims,
getCachedSystemRoles,
getCachedRoleClaims,
preloadAndCacheAllRoleClaims,
};
export const getSystemRoles = async (): Promise<Role[]> => {
return await fetchAndCacheSystemRoles();
};
export const getUserRoles = async (userId: string): Promise<Role[]> => {
const res = await apiClient.get<Role[]>(`/v1/api/users/${userId}/roles`);
return res.data || [];
};
export const assignUserRole = async (userId: string, roleId: string): Promise<void> => {
await apiClient.patch(`/v1/api/users/${userId}/roles/${roleId}`);
};
export const removeUserRole = async (userId: string, roleId: string): Promise<void> => {
await apiClient.delete(`/v1/api/users/${userId}/roles/${roleId}`);
};
export const getRoleClaims = async (roleId: string, roleName?: string): Promise<Claim[]> => {
return await fetchAndCacheRoleClaims(roleId, roleName);
};
export const getUserClaims = async (userId: string): Promise<Claim[]> => {
const res = await apiClient.get<Claim[]>(`/v1/api/users/${userId}/claims`);
return res.data || [];
};
export const addUserClaim = async (payload: AddUserClaimRequest): Promise<void> => {
await apiClient.patch('/v1/api/users/user/claims', {
userId: payload.userId,
claimId: Number(payload.claimId),
claimType: payload.claimType,
claimValue: payload.claimValue,
});
};
export const deleteUserClaim = async (targetUserId: string, claimId: string | number): Promise<void> => {
await apiClient.delete(`/v1/api/users/${targetUserId}/claims/${claimId}`);
};
export const forceLogoutUser = async (userId: string): Promise<void> => {
await apiClient.put(`/v1/api/users/${userId}/session/logout`);
};
export const setUserLockout = async (userId: string, disable: boolean): Promise<void> => {
await apiClient.put(`/v1/api/users/${userId}/session/lockout/${disable}`);
};
import {
syncUserAccount,
ensureUserSynced,
isUserSyncedInBrowser,
saveUserSyncInBrowser,
clearUserSyncInBrowser,
getUserSyncRecord,
isActivationPending,
setActivationPending,
getActivationPendingReason,
} from './userSyncService';
export {
syncUserAccount,
ensureUserSynced,
isUserSyncedInBrowser,
saveUserSyncInBrowser,
clearUserSyncInBrowser,
getUserSyncRecord,
isActivationPending,
setActivationPending,
getActivationPendingReason,
};
/**
* Synchronizes the authenticated user with the backend to enable the user in the system.
* By default, sync is saved in the browser to prevent hotspotting the sync endpoint.
*/
export const syncUser = async (
userData?: {
id?: string;
sub?: string;
email?: string | null;
name?: string | null;
role?: string | null;
tenant?: string | null;
},
force = false
): Promise<boolean> => {
return ensureUserSynced(userData?.sub || userData?.id, undefined, userData, force);
};
export const syncIntegration = async (force = true): Promise<void> => {
try {
await apiClient.post('/v1/api/integration/sync', {});
} catch {
await ensureUserSynced(undefined, undefined, undefined, force);
}
};
// ==========================================
// 8. Communications & Invites APIs
// ==========================================
export const sendInvite = async (payload: InviteRequest): Promise<Invite> => {
const tenantId = getCurrentUserTenantId();
const hostOrgId =
payload.organisationId !== undefined && payload.organisationId !== '' && payload.organisationId !== null
? toSafeInt64String(payload.organisationId)
: (tenantId !== null ? String(tenantId) : '0');
const body = {
organisationId: hostOrgId,
invitedOrganisationId: toSafeInt64String(payload.invitedOrganisationId),
awardId: toSafeInt64String(payload.awardId),
contractId: toSafeInt64String(payload.contractId),
recipient: payload.recipient,
message: payload.message,
platform: payload.platform !== undefined ? Number(payload.platform) : 1,
};
const res = await apiClient.post<Invite>('/v1/api/communications/invites', body);
return res.data;
};
export const getInvites = async (page = 1, pageSize = 100): Promise<Invite[]> => {
const res = await apiClient.get<Invite[]>(`/v1/api/communications/invites/${page}/${pageSize}`);
return res.data || [];
};
export const getApplicantData = async (applicantUserId: string): Promise<Invite> => {
const res = await apiClient.get<Invite>(`/v1/api/communications/applicants/${applicantUserId}/all`);
return res.data;
};
export const getApplicantInvite = async (applicantUserId: string): Promise<Invite> => {
try {
const res = await apiClient.get<Invite>(`/v1/api/communications/invites/applicants/${applicantUserId}`);
return res.data;
} catch (err: any) {
if (err?.response?.status === 404) {
const res = await apiClient.get<Invite>(`/v1/api/communications/invites/applicant/${applicantUserId}`);
return res.data;
}
throw err;
}
};
export const getApplicantInvites = async (applicantUserId: string): Promise<Invite[]> => {
const res = await apiClient.get<Invite[]>(`/v1/api/communications/invites/${applicantUserId}`);
return res.data || [];
};
export const getNotifications = async (organisationId?: string | number): Promise<Notification[]> => {
const targetOrgId = organisationId !== undefined && organisationId !== '' && organisationId !== null && organisationId !== 'all'
? organisationId
: getCurrentUserTenantId();
if (!targetOrgId || targetOrgId === 'all') {
return [];
}
const hashedOrgId = await toolsService.resolveOrganisationId(targetOrgId);
try {
const res = await apiClient.get<Notification[]>(
`/v1/api/communications/notifications/${encodeURIComponent(hashedOrgId)}`
);
if (res.data && Array.isArray(res.data)) {
return res.data;
}
} catch (err: any) {
console.warn('[getNotifications] API error fetching notifications:', err);
}
return [];
};
export const getUserNotifications = async (userId?: string): Promise<Notification[]> => {
try {
const url = userId ? `/v1/api/communications/notifications/user/${encodeURIComponent(userId)}` : `/v1/api/communications/notifications/inbox`;
const res = await apiClient.get<Notification[]>(url);
if (res.data && Array.isArray(res.data)) {
return res.data;
}
} catch (err: any) {
console.warn('[getUserNotifications] API error:', err);
}
return [];
};
export const createNotification = async (payload: CreateNotificationRequest): Promise<void> => {
const tenantId = getCurrentUserTenantId();
const orgId = payload.organisationId !== undefined && payload.organisationId !== '' && payload.organisationId !== null
? toIntegerId(payload.organisationId)
: (tenantId !== null ? tenantId : 1);
await apiClient.post('/v1/api/communications/notifications', {
organisationId: orgId,
recipient: payload.recipient,
platform: payload.platform,
subject: payload.subject || null,
destination: payload.destination || 'In-App User Inbox',
message: payload.message,
status: payload.status !== undefined ? payload.status : undefined,
});
};
export const updateNotification = async (payload: UpdateNotificationRequest): Promise<void> => {
await apiClient.put('/v1/api/communications/notifications', {
notificationId: toIntegerId(payload.notificationId),
destination: payload.destination || undefined,
status: payload.status,
sentAt: payload.sentAt || new Date().toISOString(),
error: payload.error || null,
});
};
export const markNotificationAsRead = async (notificationId: number | string): Promise<void> => {
await updateNotification({
notificationId: toIntegerId(notificationId),
status: NotificationStatus.Read,
});
};
// ==========================================
// 9. Governance: Tenants APIs
// ==========================================
export const getTenants = async (): Promise<Tenant[]> => {
const res = await apiClient.get<Tenant[]>('/v1/api/governance/tenants');
return res.data || [];
};
export const getTenant = async (tenantId: string | number): Promise<Tenant> => {
const hashedTenantId = await toolsService.encodeId(tenantId);
const res = await apiClient.get<Tenant>(`/v1/api/governance/tenants/${encodeURIComponent(hashedTenantId)}`);
return res.data;
};
export const createTenant = async (payload: CreateTenantRequest): Promise<void> => {
await apiClient.post('/v1/api/governance/tenants', {
name: payload.name,
});
};
export const updateTenant = async (payload: UpdateTenantRequest): Promise<void> => {
await apiClient.put('/v1/api/governance/tenants', {
tenantId: toIntegerId(payload.tenantId),
name: payload.name,
status: Number(payload.status),
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Tenant',
recordId: payload.tenantId,
newStatus: payload.status,
title: payload.name,
}).catch(console.warn);
}
};
export const getTenantOrganisations = async (tenantId: string | number): Promise<TenantOrganisation[]> => {
const hashedTenantId = await toolsService.encodeId(tenantId);
try {
const res = await apiClient.get<TenantOrganisation[]>(
`/v1/api/governance/tenants/${encodeURIComponent(hashedTenantId)}/organisations`
);
return res.data || [];
} catch (err: any) {
if (String(tenantId) !== String(hashedTenantId)) {
try {
const fallbackRes = await apiClient.get<TenantOrganisation[]>(
`/v1/api/governance/tenants/${encodeURIComponent(String(tenantId))}/organisations`
);
return fallbackRes.data || [];
} catch {}
}
throw err;
}
};
export const getTenantUsers = async (tenantId: string | number): Promise<TenantUser[]> => {
const hashedTenantId = await toolsService.encodeId(tenantId);
try {
const res = await apiClient.get<TenantUser[]>(
`/v1/api/governance/tenants/${encodeURIComponent(hashedTenantId)}/users`
);
return res.data || [];
} catch (err: any) {
if (String(tenantId) !== String(hashedTenantId)) {
try {
const fallbackRes = await apiClient.get<TenantUser[]>(
`/v1/api/governance/tenants/${encodeURIComponent(String(tenantId))}/users`
);
return fallbackRes.data || [];
} catch {}
}
throw err;
}
};
// ==========================================
// 10. Projects: Beneficiaries & Outcomes APIs
// ==========================================
export const getProjectBeneficiaries = async (projectId: string | number): Promise<Beneficiary[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
try {
const res = await apiClient.get<Beneficiary[]>(`/v1/api/projects/${hashedProjectId}/beneficiaries`);
return res.data || [];
} catch (err: any) {
if (String(projectId) !== String(hashedProjectId)) {
try {
const fallbackRes = await apiClient.get<Beneficiary[]>(`/v1/api/projects/${projectId}/beneficiaries`);
return fallbackRes.data || [];
} catch {
// ignore
}
}
throw err;
}
};
export const getBeneficiary = async (beneficiaryId: string | number): Promise<Beneficiary> => {
const hashedBeneficiaryId = await toolsService.encodeId(beneficiaryId);
try {
const res = await apiClient.get<Beneficiary>(`/v1/api/projects/beneficiaries/${hashedBeneficiaryId}`);
return res.data;
} catch (err: any) {
if (String(beneficiaryId) !== String(hashedBeneficiaryId)) {
try {
const fallbackRes = await apiClient.get<Beneficiary>(`/v1/api/projects/beneficiaries/${beneficiaryId}`);
return fallbackRes.data;
} catch {}
}
throw err;
}
};
export const createBeneficiary = async (payload: CreateBeneficiaryRequest): Promise<void> => {
await apiClient.post('/v1/api/projects/beneficiaries', {
projectId: toIntegerId(payload.projectId),
name: payload.name,
category: payload.category,
location: payload.location,
targetCount: Number(payload.targetCount),
reachedCount: Number(payload.reachedCount),
});
};
export const updateBeneficiary = async (payload: UpdateBeneficiaryRequest): Promise<void> => {
await apiClient.put('/v1/api/projects/beneficiaries', {
beneficiaryId: toIntegerId(payload.beneficiaryId),
name: payload.name,
category: payload.category,
location: payload.location,
targetCount: Number(payload.targetCount),
reachedCount: Number(payload.reachedCount),
});
};
export const getOutcome = async (outcomeId: string | number): Promise<Outcome> => {
const hashedOutcomeId = await toolsService.encodeId(outcomeId);
try {
const res = await apiClient.get<Outcome>(`/v1/api/projects/outcome/${hashedOutcomeId}`);
return res.data;
} catch {
const res = await apiClient.get<Outcome>(`/v1/api/projects/outcomes/single/${hashedOutcomeId}`);
return res.data;
}
};
export const getBeneficiaryOutcomes = async (beneficiaryId: string | number): Promise<Outcome[]> => {
const hashedBeneficiaryId = await toolsService.encodeId(beneficiaryId);
try {
const res = await apiClient.get<Outcome[]>(`/v1/api/projects/beneficiaries/${hashedBeneficiaryId}/outcomes`);
return Array.isArray(res.data) ? res.data : (res.data ? [res.data] : []);
} catch (err: any) {
if (err?.response?.status === 404) {
try {
const res = await apiClient.get<Outcome[]>(`/v1/api/projects/outcomes/${hashedBeneficiaryId}`);
return Array.isArray(res.data) ? res.data : (res.data ? [res.data] : []);
} catch {
return [];
}
}
return [];
}
};
export const getProjectOutcomes = async (projectId: string | number): Promise<Outcome[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
try {
const res = await apiClient.get<Outcome[]>(`/v1/api/projects/${hashedProjectId}/outcomes`);
return Array.isArray(res.data) ? res.data : [];
} catch {
return [];
}
};
export const createOutcome = async (payload: CreateOutcomeRequest): Promise<void> => {
const body = {
beneficiaryId: toIntegerId(payload.beneficiaryId),
name: payload.name,
description: payload.description,
status: payload.status !== undefined ? Number(payload.status) : 1,
};
try {
await apiClient.post('/v1/api/projects/beneficiaries/outcomes', body);
} catch (err: any) {
if (err?.response?.status === 404) {
await apiClient.post('/v1/api/projects/beneficiaris/outcomes', body);
} else {
throw err;
}
}
};
export const updateOutcome = async (payload: UpdateOutcomeRequest): Promise<void> => {
await apiClient.put('/v1/api/projects/outcomes', {
outcomeId: toIntegerId(payload.outcomeId),
name: payload.name,
description: payload.description,
status: Number(payload.status),
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Outcome',
recordId: payload.outcomeId,
newStatus: payload.status,
title: payload.name,
}).catch(console.warn);
}
};
// ==========================================
// 11. Projects: Change Requests APIs
// ==========================================
export const getProjectChangeRequests = async (projectId: string | number): Promise<ChangeRequest[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
try {
const res = await apiClient.get<ChangeRequest[]>(`/v1/api/projects/${hashedProjectId}/changerequests`);
return res.data || [];
} catch (err: any) {
if (String(projectId) !== String(hashedProjectId)) {
try {
const fallbackRes = await apiClient.get<ChangeRequest[]>(`/v1/api/projects/${projectId}/changerequests`);
return fallbackRes.data || [];
} catch {
// ignore
}
}
throw err;
}
};
export const getChangeRequest = async (changeRequestId: string | number): Promise<ChangeRequest> => {
const hashedChangeReqId = await toolsService.encodeId(changeRequestId);
try {
const res = await apiClient.get<ChangeRequest>(`/v1/api/projects/changerequests/${hashedChangeReqId}`);
return res.data;
} catch (err: any) {
if (String(changeRequestId) !== String(hashedChangeReqId)) {
try {
const fallbackRes = await apiClient.get<ChangeRequest>(`/v1/api/projects/changerequests/${changeRequestId}`);
return fallbackRes.data;
} catch {}
}
throw err;
}
};
export const createChangeRequest = async (payload: CreateChangeRequest): Promise<void> => {
const revisionDateIso = payload.revisionDate.includes('T')
? payload.revisionDate
: new Date(payload.revisionDate).toISOString();
await apiClient.post('/v1/api/projects/changerequests', {
projectId: toIntegerId(payload.projectId),
title: payload.title,
justification: payload.justification,
revisedBudget: Number(payload.revisedBudget),
revisionDate: revisionDateIso,
});
};
export const updateChangeRequest = async (payload: UpdateChangeRequest): Promise<void> => {
await apiClient.put('/v1/api/projects/changerequests', {
changeRequestId: toIntegerId(payload.changeRequestId),
title: payload.title,
justification: payload.justification,
revisedBudget: Number(payload.revisedBudget),
status: Number(payload.status),
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Change Request',
recordId: payload.changeRequestId,
newStatus: payload.status,
title: payload.title,
}).catch(console.warn);
}
};
// ==========================================
// 12. Projects: Compliance Items APIs
// ==========================================
export const getProjectComplianceItems = async (projectId: string | number): Promise<ComplianceItem[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
try {
// Primary updated route: /v1/api/projects/{projectId}/complianceitems
const res = await apiClient.get<ComplianceItem[]>(`/v1/api/projects/${hashedProjectId}/complianceitems`);
return res.data || [];
} catch (err: any) {
if (err?.response?.status === 404 || err?.response?.status === 400) {
try {
// Fallback without /v1 prefix: /api/projects/{projectId}/complianceitems
const fallbackRes = await apiClient.get<ComplianceItem[]>(`/api/projects/${hashedProjectId}/complianceitems`);
return fallbackRes.data || [];
} catch {
try {
// Unencoded project ID fallbacks
const rawIdRes = await apiClient.get<ComplianceItem[]>(`/v1/api/projects/${projectId}/complianceitems`);
return rawIdRes.data || [];
} catch {
try {
const rawApiRes = await apiClient.get<ComplianceItem[]>(`/api/projects/${projectId}/complianceitems`);
return rawApiRes.data || [];
} catch {
throw err;
}
}
}
}
throw err;
}
};
export const getComplianceItem = async (complianceItemId: string | number): Promise<ComplianceItem> => {
const hashedItemId = await toolsService.encodeId(complianceItemId);
const res = await apiClient.get<ComplianceItem>(`/v1/api/projects/complianceitems/${hashedItemId}`);
return res.data;
};
export const createComplianceItem = async (payload: CreateComplianceItemRequest): Promise<void> => {
await apiClient.post('/v1/api/projects/complianceitems', {
projectId: toIntegerId(payload.projectId),
title: payload.title,
requirements: payload.requirements,
status: payload.status !== undefined ? Number(payload.status) : 1,
beneficiaryId: payload.beneficiaryId ? toIntegerId(payload.beneficiaryId) : null,
});
};
export const updateComplianceItem = async (payload: UpdateComplianceItemRequest): Promise<void> => {
await apiClient.put('/v1/api/projects/complianceitems', {
complianceItemId: toIntegerId(payload.complianceItemId),
title: payload.title,
requirements: payload.requirements,
status: Number(payload.status),
beneficiaryId: payload.beneficiaryId ? toIntegerId(payload.beneficiaryId) : null,
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Compliance Item',
recordId: payload.complianceItemId,
newStatus: payload.status,
title: payload.title,
}).catch(console.warn);
}
};
// ==========================================
// 13. Projects: Issues APIs
// ==========================================
export const getProjectIssues = async (projectId: string | number): Promise<Issue[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
try {
const res = await apiClient.get<Issue[]>(`/v1/api/projects/${hashedProjectId}/issues`);
return res.data || [];
} catch (err: any) {
if (String(projectId) !== String(hashedProjectId)) {
try {
const fallbackRes = await apiClient.get<Issue[]>(`/v1/api/projects/${projectId}/issues`);
return fallbackRes.data || [];
} catch {
// ignore
}
}
throw err;
}
};
export const getIssue = async (issueId: string | number): Promise<Issue> => {
const hashedIssueId = await toolsService.encodeId(issueId);
const res = await apiClient.get<Issue>(`/v1/api/projects/issues/${hashedIssueId}`);
return res.data;
};
export const createIssue = async (payload: CreateIssueRequest): Promise<void> => {
await apiClient.post('/v1/api/projects/issues', {
projectId: toIntegerId(payload.projectId),
title: payload.title,
description: payload.description,
priority: Number(payload.priority),
assignedTo: payload.assignedTo,
});
};
export const updateIssue = async (payload: UpdateIssueRequest): Promise<void> => {
let resolvedAtIso: string | null = null;
if (payload.resolvedAt) {
try {
resolvedAtIso = payload.resolvedAt.includes('T') && payload.resolvedAt.endsWith('Z')
? payload.resolvedAt
: new Date(payload.resolvedAt).toISOString();
} catch {
resolvedAtIso = new Date().toISOString();
}
} else {
resolvedAtIso = new Date().toISOString();
}
const finalResolution = payload.resolution && payload.resolution.trim() !== ''
? payload.resolution
: (Number(payload.status) === ActivityStatus.Inactive ? 'Resolved' : 'Under active investigation');
await apiClient.put('/v1/api/projects/issues', {
issueId: toIntegerId(payload.issueId),
title: payload.title,
description: payload.description,
resolution: finalResolution,
resolvedAt: resolvedAtIso,
priority: Number(payload.priority),
status: payload.status !== undefined ? Number(payload.status) : ActivityStatus.Active,
assignedTo: payload.assignedTo,
});
const issueStatus = payload.status !== undefined ? payload.status : ActivityStatus.Active;
if (isApprovedOrActiveStatus(issueStatus)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Issue',
recordId: payload.issueId,
newStatus: issueStatus,
title: payload.title,
}).catch(console.warn);
}
};
// ==========================================
// 14. Projects: Site Visits APIs
// ==========================================
export const getProjectSiteVisits = async (projectId: string | number): Promise<SiteVisit[]> => {
const hashedProjectId = await toolsService.resolveProjectId(projectId);
try {
const res = await apiClient.get<SiteVisit[]>(`/v1/api/projects/${hashedProjectId}/sitevisits`);
return res.data || [];
} catch (err: any) {
if (String(projectId) !== String(hashedProjectId)) {
try {
const fallbackRes = await apiClient.get<SiteVisit[]>(`/v1/api/projects/${projectId}/sitevisits`);
return fallbackRes.data || [];
} catch {
// ignore
}
}
throw err;
}
};
export const getSiteVisit = async (siteVisitId: string | number): Promise<SiteVisit> => {
const hashedVisitId = await toolsService.encodeId(siteVisitId);
try {
const res = await apiClient.get<SiteVisit>(`/v1/api/projects/sitevisits/${hashedVisitId}`);
return res.data;
} catch (err: any) {
if (String(siteVisitId) !== String(hashedVisitId)) {
try {
const fallbackRes = await apiClient.get<SiteVisit>(`/v1/api/projects/sitevisits/${siteVisitId}`);
return fallbackRes.data;
} catch {}
}
throw err;
}
};
export const createSiteVisit = async (payload: CreateSiteVisitRequest): Promise<void> => {
const visitedAtIso = payload.visitedAt.includes('T')
? payload.visitedAt
: new Date(payload.visitedAt).toISOString();
await apiClient.post('/v1/api/projects/sitevisits', {
projectId: toIntegerId(payload.projectId),
inspectorName: payload.inspectorName,
findings: payload.findings,
visitedAt: visitedAtIso,
});
};
export const updateSiteVisit = async (payload: UpdateSiteVisitRequest): Promise<void> => {
const visitedAtIso = payload.visitedAt.includes('T')
? payload.visitedAt
: new Date(payload.visitedAt).toISOString();
await apiClient.put('/v1/api/projects/sitevisits', {
siteVisitId: toIntegerId(payload.siteVisitId),
inspectorName: payload.inspectorName,
findings: payload.findings,
status: Number(payload.status),
visitedAt: visitedAtIso,
});
if (isApprovedOrActiveStatus(payload.status)) {
notifyTenantManagersAndExecutivesOnRecordStatusChange({
recordType: 'Site Visit',
recordId: payload.siteVisitId,
newStatus: payload.status,
title: payload.inspectorName,
}).catch(console.warn);
}
};
export const purgeStoredRuleDuplicates = (_apiRules?: Rule[]): void => {};
export const getStoredRules = (): Rule[] => [];
export const saveStoredRule = (_rule?: Rule): void => {};
// ==========================================
// 15. Execution: Programme Rules APIs
// ==========================================
export const getRule = async (ruleId: string | number): Promise<Rule> => {
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');
};
const toValidUuid = (userId?: string | null): string => {
if (!userId) return '00000000-0000-0000-0000-000000000000';
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (uuidPattern.test(userId)) return userId;
const userSub = getCurrentUserSub();
if (userSub && uuidPattern.test(userSub)) return userSub;
return '00000000-0000-0000-0000-000000000000';
};
export const getRules = async (
pagination: Pagination = { page: 1, pageSize: 100 },
programmeId?: string | number
): Promise<Rule[]> => {
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') {
result = result.filter((r) => String(r.programmeId) === String(programmeId));
}
return result;
};
export const createRule = async (payload: CreateRuleRequest): Promise<Rule> => {
const validUserId = toValidUuid(payload.userId);
const body = {
programmeId: toIntegerId(payload.programmeId),
userId: validUserId,
name: payload.name,
description: payload.description,
};
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);
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) } : {}),
});
};
// ==========================================
// 12. Audit Logs & System Telemetry APIs
// ==========================================
export const getAuditLogs = async (page = 1, pageSize = 50): Promise<AuditLog[]> => {
try {
const res = await apiClient.post<AuditLog[]>('/v1/api/governance/auditlog', {
index: (page - 1) * pageSize,
page,
pageSize,
});
if (res.data && Array.isArray(res.data)) {
return res.data;
}
return [];
} catch (err) {
console.error('[getAuditLogs] API request failed:', err);
return [];
}
};
export const getAuditLog = async (auditId: string | number): Promise<AuditLog | null> => {
try {
const logs = await getAuditLogs(1, 100);
return logs.find((l) => String(l.id) === String(auditId)) || null;
} catch {
return null;
}
};