Enabled account sync control and session expiration management Applied some fixes to the govername page and project page
847 lines
30 KiB
TypeScript
847 lines
30 KiB
TypeScript
import axios, { AxiosError } from 'axios';
|
|
import { apiClient } from './httpClient';
|
|
import { toolsService } from './toolsService';
|
|
import { getCurrentUserSub } from '../auth/AuthProvider';
|
|
import {
|
|
Dashboard,
|
|
Organisation,
|
|
CreateOrganisationRequest,
|
|
UpdateOrganisationRequest,
|
|
Portfolio,
|
|
CreatePortfolioRequest,
|
|
UpdatePortfolioRequest,
|
|
Programme,
|
|
CreateProgrammeRequest,
|
|
UpdateProgrammeRequest,
|
|
Instrument,
|
|
Project,
|
|
CreateProjectRequest,
|
|
UpdateProjectRequest,
|
|
UpdateProjectStatusRequest,
|
|
Activity,
|
|
CreateActivityRequest,
|
|
UpdateActivityRequest,
|
|
Milestone,
|
|
CreateMilestoneRequest,
|
|
UpdateMilestoneRequest,
|
|
Award,
|
|
CreateAwardRequest,
|
|
Contract,
|
|
CreateContractRequest,
|
|
UpdateContractRequest,
|
|
ExecuteContractRequest,
|
|
Budget,
|
|
CreateBudgetRequest,
|
|
Disbursement,
|
|
CreateDisbursementRequest,
|
|
UpdateDisbursementRequest,
|
|
Indicator,
|
|
CreateIndicatorRequest,
|
|
Evidence,
|
|
UpdateEvidenceRequest,
|
|
Risk,
|
|
CreateRiskRequest,
|
|
UpdateRiskRequest,
|
|
User,
|
|
UserFilterRequest,
|
|
Invite,
|
|
InviteRequest,
|
|
InviteUserRequest,
|
|
AuditLog,
|
|
ProblemDetails,
|
|
ActivityStatus,
|
|
} from '../types';
|
|
|
|
import { parseFriendlyError, FriendlyError } from '../utils/apiErrorParser';
|
|
import { registerUserEmail, registerUserEmails } from '../lib/formatters';
|
|
|
|
// Helper to extract ProblemDetails from Axios error or unknown error
|
|
export function parseApiError(error: unknown): ProblemDetails {
|
|
if (axios.isAxiosError(error)) {
|
|
const axiosErr = error as AxiosError<ProblemDetails>;
|
|
if (axiosErr.response?.data && typeof axiosErr.response.data === 'object') {
|
|
return axiosErr.response.data;
|
|
}
|
|
}
|
|
|
|
const friendly = parseFriendlyError(error);
|
|
return {
|
|
title: friendly.title,
|
|
detail: friendly.message,
|
|
status: friendly.statusCode || 500,
|
|
};
|
|
}
|
|
|
|
export { parseFriendlyError };
|
|
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
|
|
// ==========================================
|
|
|
|
/**
|
|
* 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,
|
|
name: raw.name ?? raw.Name ?? null,
|
|
registrationNo: raw.registrationNo ?? raw.RegistrationNo ?? raw.regNo ?? raw.RegNo ?? null,
|
|
email: raw.email ?? raw.Email ?? 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 hashedOrgId = await toolsService.resolveOrganisationId(organisationId);
|
|
const res = await apiClient.get<any>(`/v1/api/governance/organisations/${hashedOrgId}`);
|
|
return normalizeOrganisation(res.data);
|
|
};
|
|
|
|
export const createOrganisation = async (payload: CreateOrganisationRequest): Promise<Organisation> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/governance/organisations', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updateOrganisation = async (payload: UpdateOrganisationRequest): Promise<Organisation> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.put('/v1/api/governance/organisations', {
|
|
...payload,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const getPortfolios = async (organisationId: string | number): Promise<Portfolio[]> => {
|
|
const hashedOrgId = await toolsService.resolveOrganisationId(organisationId);
|
|
const res = await apiClient.get<Portfolio[]>(`/v1/api/governance/organisations/${hashedOrgId}/portfolio`);
|
|
return res.data || [];
|
|
};
|
|
|
|
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 (): Promise<Portfolio[]> => {
|
|
try {
|
|
const orgs = await getOrganisations(1, 10);
|
|
if (orgs.length > 0) {
|
|
const results = await Promise.allSettled(
|
|
orgs.map((org) => getPortfolios(org.id))
|
|
);
|
|
const combined = results
|
|
.filter((r): r is PromiseFulfilledResult<Portfolio[]> => r.status === 'fulfilled')
|
|
.flatMap((r) => r.value);
|
|
if (combined.length > 0) return combined;
|
|
}
|
|
const res = await apiClient.get<Portfolio[]>('/v1/api/governance/organisations/portfolio');
|
|
return res.data || [];
|
|
} catch {
|
|
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();
|
|
const res = await apiClient.post('/v1/api/governance/organisations/portfolio', {
|
|
...payload,
|
|
ownerId: payload.ownerId || userSub || undefined,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updatePortfolio = async (payload: UpdatePortfolioRequest): Promise<Portfolio> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.put('/v1/api/governance/organisations/portfolio', {
|
|
...payload,
|
|
ownerId: payload.ownerId || userSub || undefined,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
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 getInstruments = async (programmeId: string | number): Promise<Instrument[]> => {
|
|
const hashedProgId = await toolsService.resolveProgrammeId(programmeId);
|
|
const res = await apiClient.get<Instrument[]>(`/v1/api/governance/instruments/${hashedProgId}`);
|
|
return res.data || [];
|
|
};
|
|
|
|
// ==========================================
|
|
// 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();
|
|
const res = await apiClient.post('/v1/api/projects/programme', {
|
|
...payload,
|
|
ownerId: payload.ownerId || userSub || undefined,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updateProgramme = async (payload: UpdateProgrammeRequest): Promise<Programme> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.put('/v1/api/projects/programme', {
|
|
...payload,
|
|
ownerId: payload.ownerId || userSub || undefined,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
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 userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/projects', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updateProject = async (payload: UpdateProjectRequest): Promise<Project> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.put('/v1/api/projects', {
|
|
...payload,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updateProjectStatus = async (payload: UpdateProjectStatusRequest): Promise<void> => {
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.put('/v1/api/projects/status', {
|
|
...payload,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
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 userSub = getCurrentUserSub();
|
|
const body: any = {
|
|
...payload,
|
|
milestoneId:
|
|
payload.milestoneId !== undefined && payload.milestoneId !== null && payload.milestoneId !== ''
|
|
? Number(payload.milestoneId)
|
|
: null,
|
|
createdBy: userSub || undefined,
|
|
};
|
|
try {
|
|
const res = await apiClient.post('/v1/api/projects/activities', body);
|
|
return res.data;
|
|
} catch (err: any) {
|
|
if (err?.response?.status === 405) {
|
|
const res = await apiClient.put('/v1/api/projects/activities', body);
|
|
return res.data;
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const updateActivity = async (payload: UpdateActivityRequest): Promise<Activity> => {
|
|
const userSub = getCurrentUserSub();
|
|
const body: any = {
|
|
...payload,
|
|
milestoneId:
|
|
payload.milestoneId !== undefined && payload.milestoneId !== null && payload.milestoneId !== ''
|
|
? Number(payload.milestoneId)
|
|
: null,
|
|
updatedBy: userSub || undefined,
|
|
};
|
|
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 userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/evidence/milestones', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updateMilestone = async (payload: UpdateMilestoneRequest): Promise<void> => {
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.put('/v1/api/projects/milestone', {
|
|
...payload,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
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 userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/grants/awards', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
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> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/grants/contracts', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updateContract = async (payload: UpdateContractRequest): Promise<void> => {
|
|
await apiClient.put('/v1/api/grants/contracts', payload);
|
|
};
|
|
|
|
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>(`/v1/api/grants/contracts/${hashedContractId}/securelink`);
|
|
return res.data;
|
|
};
|
|
|
|
export const executeContract = async (payload: ExecuteContractRequest): Promise<void> => {
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.post('/v1/api/grants/contracts/execute', {
|
|
...payload,
|
|
executedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
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 userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/grants/budgets', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
// ==========================================
|
|
// 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 userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/execution/disbursements', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const updateDisbursement = async (payload: UpdateDisbursementRequest): Promise<void> => {
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.put('/v1/api/execution/disbursements', {
|
|
...payload,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
export const approveDisbursement = async (disbursementId: string | number): Promise<void> => {
|
|
const hashedDisbId = await toolsService.encodeId(disbursementId);
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.post(`/v1/api/execution/disbursements/${hashedDisbId}/approve`, {
|
|
approvedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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 userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/execution/risks', {
|
|
...payload,
|
|
likelihood: Number(payload.likelihood),
|
|
impact: Number(payload.impact),
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return normalizeRisk(res.data);
|
|
};
|
|
|
|
export const updateRisk = async (payload: UpdateRiskRequest): Promise<void> => {
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.put('/v1/api/execution/risks', {
|
|
...payload,
|
|
likelihood: Number(payload.likelihood),
|
|
impact: Number(payload.impact),
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
// ==========================================
|
|
// 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 userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/evidence/indicators', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const recordIndicatorActual = async (indicatorId: string | number, actualAmount: number): Promise<void> => {
|
|
const hashedIndId = await toolsService.encodeId(indicatorId);
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.post(`/v1/api/evidence/indicators/${hashedIndId}/actuals/${actualAmount}`, {
|
|
recordedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
export const getEvidenceFiles = async (page = 1, pageSize = 100): Promise<Evidence[]> => {
|
|
const res = await apiClient.get<Evidence[]>(`/v1/api/evidence/files/${page}/${pageSize}`);
|
|
return res.data || [];
|
|
};
|
|
|
|
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[]> => {
|
|
const hashedMId = await toolsService.encodeId(milestoneId);
|
|
const res = await apiClient.get<Evidence[]>(`/v1/api/evidence/milestone/${hashedMId}`);
|
|
return res.data || [];
|
|
};
|
|
|
|
export const getProjectEvidence = async (projectId: string | number): Promise<Evidence[]> => {
|
|
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
|
const res = await apiClient.get<Evidence[]>(`/v1/api/evidence/${hashedProjectId}`);
|
|
return res.data || [];
|
|
};
|
|
|
|
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> => {
|
|
const userSub = getCurrentUserSub();
|
|
await apiClient.put('/v1/api/evidence', {
|
|
...payload,
|
|
updatedBy: userSub || undefined,
|
|
});
|
|
};
|
|
|
|
// ==========================================
|
|
// 7. Users & Background Integration APIs
|
|
// ==========================================
|
|
export const getUsers = async (
|
|
page = 1,
|
|
pageSize = 100,
|
|
filter: UserFilterRequest = {}
|
|
): Promise<User[]> => {
|
|
const res = await apiClient.post<User[]>(`/v1/api/users/${page}/${pageSize}`, filter);
|
|
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 {
|
|
syncUserAccount,
|
|
ensureUserSynced,
|
|
isUserSyncedInBrowser,
|
|
saveUserSyncInBrowser,
|
|
clearUserSyncInBrowser,
|
|
getUserSyncRecord,
|
|
isActivationPending,
|
|
setActivationPending,
|
|
getActivationPendingReason,
|
|
} from './userSyncService';
|
|
|
|
/**
|
|
* 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> => {
|
|
const { ensureUserSynced } = await import('./userSyncService');
|
|
return ensureUserSynced(userData?.sub || userData?.id, undefined, userData, force);
|
|
};
|
|
|
|
export const syncIntegration = async (force = true): Promise<void> => {
|
|
const { ensureUserSynced } = await import('./userSyncService');
|
|
await ensureUserSynced(undefined, undefined, undefined, force);
|
|
};
|
|
|
|
// ==========================================
|
|
// 8. Communications & Invites APIs
|
|
// ==========================================
|
|
export const sendInvite = async (payload: InviteRequest): Promise<Invite> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.post<Invite>('/v1/api/communications/invite', {
|
|
...payload,
|
|
createdBy: userSub || undefined,
|
|
});
|
|
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 getApplicantInvite = async (applicantUserId: string): Promise<Invite> => {
|
|
const res = await apiClient.get<Invite>(`/v1/api/communications/invites/applicant/${applicantUserId}`);
|
|
return res.data;
|
|
};
|
|
|
|
export const getApplicantInvites = async (applicantUserId: string): Promise<Invite[]> => {
|
|
const res = await apiClient.get<Invite[]>(`/v1/api/communications/invites/${applicantUserId}`);
|
|
return res.data || [];
|
|
};
|
|
|
|
// Legacy compatibility helpers
|
|
export const inviteUser = async (payload: InviteUserRequest): Promise<User> => {
|
|
const userSub = getCurrentUserSub();
|
|
const res = await apiClient.post('/v1/api/governance/users/invite', {
|
|
...payload,
|
|
invitedBy: userSub || undefined,
|
|
});
|
|
return res.data;
|
|
};
|
|
|
|
export const revokeUser = async (userId: string): Promise<void> => {
|
|
await apiClient.delete(`/v1/api/governance/users/${userId}`);
|
|
};
|
|
|
|
export const getAuditLogs = async (): Promise<AuditLog[]> => {
|
|
try {
|
|
const res = await apiClient.get<AuditLog[]>('/v1/api/governance/audit');
|
|
return res.data || [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|