Fixed mismatched endpoint calling #111
@@ -24,11 +24,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
getProjects,
|
getProjects,
|
||||||
getProjectBeneficiaries,
|
getProjectBeneficiaries,
|
||||||
|
getBeneficiary,
|
||||||
createBeneficiary,
|
createBeneficiary,
|
||||||
updateBeneficiary,
|
updateBeneficiary,
|
||||||
getBeneficiaryOutcomes,
|
getBeneficiaryOutcomes,
|
||||||
createOutcome,
|
createOutcome,
|
||||||
getProjectComplianceItems,
|
getProjectComplianceItems,
|
||||||
|
getComplianceItem,
|
||||||
createComplianceItem,
|
createComplianceItem,
|
||||||
updateComplianceItem,
|
updateComplianceItem,
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
@@ -288,26 +290,50 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
{ header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending Review') },
|
{ header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending Review') },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleOpenEditBen = (b: Beneficiary) => {
|
const handleOpenEditBen = async (b: Beneficiary) => {
|
||||||
setEditingBen(b);
|
try {
|
||||||
benForm.reset({
|
const fresh = await getBeneficiary(b.id);
|
||||||
name: b.name || '',
|
setEditingBen(fresh);
|
||||||
category: b.category || 'General',
|
benForm.reset({
|
||||||
location: b.location || '',
|
name: fresh.name || '',
|
||||||
targetCount: b.targetCount || 0,
|
category: fresh.category || 'General',
|
||||||
reachedCount: b.reachedCount || 0,
|
location: fresh.location || '',
|
||||||
});
|
targetCount: fresh.targetCount || 0,
|
||||||
setIsBenModalOpen(true);
|
reachedCount: fresh.reachedCount || 0,
|
||||||
|
});
|
||||||
|
setIsBenModalOpen(true);
|
||||||
|
} catch {
|
||||||
|
setEditingBen(b);
|
||||||
|
benForm.reset({
|
||||||
|
name: b.name || '',
|
||||||
|
category: b.category || 'General',
|
||||||
|
location: b.location || '',
|
||||||
|
targetCount: b.targetCount || 0,
|
||||||
|
reachedCount: b.reachedCount || 0,
|
||||||
|
});
|
||||||
|
setIsBenModalOpen(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenEditComp = (c: ComplianceItem) => {
|
const handleOpenEditComp = async (c: ComplianceItem) => {
|
||||||
setEditingComp(c);
|
try {
|
||||||
compForm.reset({
|
const fresh = await getComplianceItem(c.id);
|
||||||
title: c.title || '',
|
setEditingComp(fresh);
|
||||||
requirements: c.requirements || '',
|
compForm.reset({
|
||||||
status: c.status ?? ApprovalStatus.Pending,
|
title: fresh.title || '',
|
||||||
});
|
requirements: fresh.requirements || '',
|
||||||
setIsCompModalOpen(true);
|
status: fresh.status ?? ApprovalStatus.Pending,
|
||||||
|
});
|
||||||
|
setIsCompModalOpen(true);
|
||||||
|
} catch {
|
||||||
|
setEditingComp(c);
|
||||||
|
compForm.reset({
|
||||||
|
title: c.title || '',
|
||||||
|
requirements: c.requirements || '',
|
||||||
|
status: c.status ?? ApprovalStatus.Pending,
|
||||||
|
});
|
||||||
|
setIsCompModalOpen(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
getProjects,
|
getProjects,
|
||||||
getProjectIssues,
|
getProjectIssues,
|
||||||
|
getIssue,
|
||||||
createIssue,
|
createIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
getProjectChangeRequests,
|
getProjectChangeRequests,
|
||||||
|
getChangeRequest,
|
||||||
createChangeRequest,
|
createChangeRequest,
|
||||||
updateChangeRequest,
|
updateChangeRequest,
|
||||||
getUsers,
|
getUsers,
|
||||||
@@ -45,6 +47,7 @@ import {
|
|||||||
formatZAR,
|
formatZAR,
|
||||||
getUserEmail,
|
getUserEmail,
|
||||||
PRIORITY_OPTIONS,
|
PRIORITY_OPTIONS,
|
||||||
|
APPROVAL_STATUS_OPTIONS,
|
||||||
} from '../lib/formatters';
|
} from '../lib/formatters';
|
||||||
|
|
||||||
const issueSchema = z.object({
|
const issueSchema = z.object({
|
||||||
@@ -54,6 +57,23 @@ const issueSchema = z.object({
|
|||||||
assignedTo: z.string().min(1, 'Assignee is required'),
|
assignedTo: z.string().min(1, 'Assignee is required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editIssueSchema = z.object({
|
||||||
|
title: z.string().min(3, 'Issue title required'),
|
||||||
|
description: z.string().min(5, 'Description required'),
|
||||||
|
priority: z.coerce.number().min(1).max(4),
|
||||||
|
assignedTo: z.string().min(1, 'Assignee is required'),
|
||||||
|
status: z.coerce.number(),
|
||||||
|
resolution: z.string().min(1, 'Resolution notes required'),
|
||||||
|
resolvedAt: z.string().min(1, 'Resolution date and time required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const toDateTimeLocalValue = (dateString?: string | null): string => {
|
||||||
|
const d = dateString ? new Date(dateString) : new Date();
|
||||||
|
const validDate = isNaN(d.getTime()) ? new Date() : d;
|
||||||
|
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||||
|
return `${validDate.getFullYear()}-${pad(validDate.getMonth() + 1)}-${pad(validDate.getDate())}T${pad(validDate.getHours())}:${pad(validDate.getMinutes())}`;
|
||||||
|
};
|
||||||
|
|
||||||
const resolveIssueSchema = z.object({
|
const resolveIssueSchema = z.object({
|
||||||
resolution: z.string().min(5, 'Resolution notes required'),
|
resolution: z.string().min(5, 'Resolution notes required'),
|
||||||
status: z.coerce.number(),
|
status: z.coerce.number(),
|
||||||
@@ -66,14 +86,23 @@ const changeRequestSchema = z.object({
|
|||||||
revisionDate: z.string().min(4, 'Revision date required'),
|
revisionDate: z.string().min(4, 'Revision date required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editChangeSchema = z.object({
|
||||||
|
title: z.string().min(3, 'Title required'),
|
||||||
|
justification: z.string().min(5, 'Justification required'),
|
||||||
|
revisedBudget: z.coerce.number().min(0, 'Revised budget must be non-negative'),
|
||||||
|
status: z.coerce.number(),
|
||||||
|
});
|
||||||
|
|
||||||
const resolveChangeSchema = z.object({
|
const resolveChangeSchema = z.object({
|
||||||
status: z.coerce.number(),
|
status: z.coerce.number(),
|
||||||
justification: z.string().min(3, 'Decision justification required'),
|
justification: z.string().min(3, 'Decision justification required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
type IssueFormValues = z.infer<typeof issueSchema>;
|
type IssueFormValues = z.infer<typeof issueSchema>;
|
||||||
|
type EditIssueFormValues = z.infer<typeof editIssueSchema>;
|
||||||
type ResolveIssueFormValues = z.infer<typeof resolveIssueSchema>;
|
type ResolveIssueFormValues = z.infer<typeof resolveIssueSchema>;
|
||||||
type ChangeRequestFormValues = z.infer<typeof changeRequestSchema>;
|
type ChangeRequestFormValues = z.infer<typeof changeRequestSchema>;
|
||||||
|
type EditChangeFormValues = z.infer<typeof editChangeSchema>;
|
||||||
type ResolveChangeFormValues = z.infer<typeof resolveChangeSchema>;
|
type ResolveChangeFormValues = z.infer<typeof resolveChangeSchema>;
|
||||||
|
|
||||||
export const ChangeManagementPage: React.FC = () => {
|
export const ChangeManagementPage: React.FC = () => {
|
||||||
@@ -89,12 +118,14 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
|
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
|
||||||
|
const [isEditIssueModalOpen, setIsEditIssueModalOpen] = useState(false);
|
||||||
const [editingIssue, setEditingIssue] = useState<Issue | null>(null);
|
const [editingIssue, setEditingIssue] = useState<Issue | null>(null);
|
||||||
|
|
||||||
const [isResolveIssueModalOpen, setIsResolveIssueModalOpen] = useState(false);
|
const [isResolveIssueModalOpen, setIsResolveIssueModalOpen] = useState(false);
|
||||||
const [resolvingIssue, setResolvingIssue] = useState<Issue | null>(null);
|
const [resolvingIssue, setResolvingIssue] = useState<Issue | null>(null);
|
||||||
|
|
||||||
const [isChangeModalOpen, setIsChangeModalOpen] = useState(false);
|
const [isChangeModalOpen, setIsChangeModalOpen] = useState(false);
|
||||||
|
const [isEditChangeModalOpen, setIsEditChangeModalOpen] = useState(false);
|
||||||
const [editingChange, setEditingChange] = useState<ChangeRequest | null>(null);
|
const [editingChange, setEditingChange] = useState<ChangeRequest | null>(null);
|
||||||
|
|
||||||
const [isResolveChangeModalOpen, setIsResolveChangeModalOpen] = useState(false);
|
const [isResolveChangeModalOpen, setIsResolveChangeModalOpen] = useState(false);
|
||||||
@@ -141,6 +172,21 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
defaultValues: { title: '', description: '', priority: Priority.Medium, assignedTo: '' },
|
defaultValues: { title: '', description: '', priority: Priority.Medium, assignedTo: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editIssueForm = useForm<EditIssueFormValues>({
|
||||||
|
resolver: zodResolver(editIssueSchema),
|
||||||
|
defaultValues: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
priority: Priority.Medium,
|
||||||
|
assignedTo: '',
|
||||||
|
status: ActivityStatus.Active,
|
||||||
|
resolution: '',
|
||||||
|
resolvedAt: toDateTimeLocalValue(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchedEditStatus = editIssueForm.watch('status');
|
||||||
|
|
||||||
const resolveIssueForm = useForm<ResolveIssueFormValues>({
|
const resolveIssueForm = useForm<ResolveIssueFormValues>({
|
||||||
resolver: zodResolver(resolveIssueSchema),
|
resolver: zodResolver(resolveIssueSchema),
|
||||||
defaultValues: { resolution: '', status: ActivityStatus.Inactive },
|
defaultValues: { resolution: '', status: ActivityStatus.Inactive },
|
||||||
@@ -156,6 +202,16 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editChangeForm = useForm<EditChangeFormValues>({
|
||||||
|
resolver: zodResolver(editChangeSchema),
|
||||||
|
defaultValues: {
|
||||||
|
title: '',
|
||||||
|
justification: '',
|
||||||
|
revisedBudget: 0,
|
||||||
|
status: ApprovalStatus.Pending,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const resolveChangeForm = useForm<ResolveChangeFormValues>({
|
const resolveChangeForm = useForm<ResolveChangeFormValues>({
|
||||||
resolver: zodResolver(resolveChangeSchema),
|
resolver: zodResolver(resolveChangeSchema),
|
||||||
defaultValues: { status: ApprovalStatus.Approved, justification: '' },
|
defaultValues: { status: ApprovalStatus.Approved, justification: '' },
|
||||||
@@ -183,25 +239,36 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateIssueMutation = useMutation({
|
const updateIssueMutation = useMutation({
|
||||||
mutationFn: (vals: IssueFormValues) => {
|
mutationFn: (vals: EditIssueFormValues) => {
|
||||||
if (!editingIssue) throw new Error('No issue selected');
|
if (!editingIssue) throw new Error('No issue selected');
|
||||||
|
let isoResolvedAt: string;
|
||||||
|
try {
|
||||||
|
isoResolvedAt = vals.resolvedAt
|
||||||
|
? (vals.resolvedAt.includes('T') && vals.resolvedAt.endsWith('Z')
|
||||||
|
? vals.resolvedAt
|
||||||
|
: new Date(vals.resolvedAt).toISOString())
|
||||||
|
: new Date().toISOString();
|
||||||
|
} catch {
|
||||||
|
isoResolvedAt = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
return updateIssue({
|
return updateIssue({
|
||||||
issueId: editingIssue.id,
|
issueId: editingIssue.id,
|
||||||
title: vals.title,
|
title: vals.title,
|
||||||
description: vals.description,
|
description: vals.description,
|
||||||
priority: vals.priority,
|
priority: vals.priority,
|
||||||
assignedTo: vals.assignedTo,
|
assignedTo: vals.assignedTo,
|
||||||
resolution: editingIssue.resolution || '',
|
resolution: vals.resolution || (Number(vals.status) === ActivityStatus.Inactive ? 'Resolved' : 'Under active investigation'),
|
||||||
resolvedAt: editingIssue.resolvedAt || null,
|
resolvedAt: isoResolvedAt,
|
||||||
status: editingIssue.status || ActivityStatus.Active,
|
status: Number(vals.status),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['issues'] });
|
queryClient.invalidateQueries({ queryKey: ['issues'] });
|
||||||
setIsIssueModalOpen(false);
|
setIsEditIssueModalOpen(false);
|
||||||
setEditingIssue(null);
|
setEditingIssue(null);
|
||||||
issueForm.reset();
|
editIssueForm.reset();
|
||||||
success('Issue Updated', 'Issue record saved.');
|
success('Issue Updated', 'Issue record and resolution status saved successfully.');
|
||||||
},
|
},
|
||||||
onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update issue.'),
|
onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update issue.'),
|
||||||
});
|
});
|
||||||
@@ -271,6 +338,27 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
onError: (err: any) => toastError('Decision Failed', err.message || 'Unable to resolve change request.'),
|
onError: (err: any) => toastError('Decision Failed', err.message || 'Unable to resolve change request.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateChangeMutation = useMutation({
|
||||||
|
mutationFn: (vals: EditChangeFormValues) => {
|
||||||
|
if (!editingChange) throw new Error('No change request selected');
|
||||||
|
return updateChangeRequest({
|
||||||
|
changeRequestId: editingChange.id,
|
||||||
|
title: vals.title,
|
||||||
|
justification: vals.justification,
|
||||||
|
revisedBudget: vals.revisedBudget,
|
||||||
|
status: Number(vals.status),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['changeRequests'] });
|
||||||
|
setIsEditChangeModalOpen(false);
|
||||||
|
setEditingChange(null);
|
||||||
|
editChangeForm.reset();
|
||||||
|
success('Change Request Updated', 'Change proposal and governance status saved.');
|
||||||
|
},
|
||||||
|
onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update change request.'),
|
||||||
|
});
|
||||||
|
|
||||||
// Filtered & Sorted Issues
|
// Filtered & Sorted Issues
|
||||||
const filteredIssues = useMemo(() => {
|
const filteredIssues = useMemo(() => {
|
||||||
return issues.filter((iss) => {
|
return issues.filter((iss) => {
|
||||||
@@ -332,6 +420,88 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
setIsResolveIssueModalOpen(true);
|
setIsResolveIssueModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditIssue = async (iss: Issue) => {
|
||||||
|
try {
|
||||||
|
const fresh = await getIssue(iss.id);
|
||||||
|
setEditingIssue(fresh);
|
||||||
|
const initialStatus =
|
||||||
|
fresh.status !== undefined && fresh.status !== null
|
||||||
|
? Number(fresh.status)
|
||||||
|
: fresh.resolvedAt
|
||||||
|
? ActivityStatus.Inactive
|
||||||
|
: ActivityStatus.Active;
|
||||||
|
|
||||||
|
const initialResolution =
|
||||||
|
fresh.resolution && fresh.resolution.trim() !== ''
|
||||||
|
? fresh.resolution
|
||||||
|
: initialStatus === ActivityStatus.Inactive
|
||||||
|
? 'Resolved and remediated'
|
||||||
|
: 'Remediation currently in progress';
|
||||||
|
|
||||||
|
editIssueForm.reset({
|
||||||
|
title: fresh.title || '',
|
||||||
|
description: fresh.description || '',
|
||||||
|
priority: fresh.priority || Priority.Medium,
|
||||||
|
assignedTo: fresh.assignedTo || '',
|
||||||
|
status: initialStatus,
|
||||||
|
resolution: initialResolution,
|
||||||
|
resolvedAt: toDateTimeLocalValue(fresh.resolvedAt),
|
||||||
|
});
|
||||||
|
setIsEditIssueModalOpen(true);
|
||||||
|
} catch {
|
||||||
|
setEditingIssue(iss);
|
||||||
|
const initialStatus =
|
||||||
|
iss.status !== undefined && iss.status !== null
|
||||||
|
? Number(iss.status)
|
||||||
|
: iss.resolvedAt
|
||||||
|
? ActivityStatus.Inactive
|
||||||
|
: ActivityStatus.Active;
|
||||||
|
|
||||||
|
const initialResolution =
|
||||||
|
iss.resolution && iss.resolution.trim() !== ''
|
||||||
|
? iss.resolution
|
||||||
|
: initialStatus === ActivityStatus.Inactive
|
||||||
|
? 'Resolved and remediated'
|
||||||
|
: 'Remediation currently in progress';
|
||||||
|
|
||||||
|
editIssueForm.reset({
|
||||||
|
title: iss.title || '',
|
||||||
|
description: iss.description || '',
|
||||||
|
priority: iss.priority || Priority.Medium,
|
||||||
|
assignedTo: iss.assignedTo || '',
|
||||||
|
status: initialStatus,
|
||||||
|
resolution: initialResolution,
|
||||||
|
resolvedAt: toDateTimeLocalValue(iss.resolvedAt),
|
||||||
|
});
|
||||||
|
setIsEditIssueModalOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditChange = async (cr: ChangeRequest) => {
|
||||||
|
try {
|
||||||
|
// 1. Uses GET method (NOT POST)
|
||||||
|
// 2. Passes ChangeRequestId (NOT ProjectId)
|
||||||
|
const freshData = await getChangeRequest(cr.id);
|
||||||
|
setEditingChange(freshData);
|
||||||
|
editChangeForm.reset({
|
||||||
|
title: freshData.title || '',
|
||||||
|
justification: freshData.justification || '',
|
||||||
|
revisedBudget: freshData.revisedBudget ?? 0,
|
||||||
|
status: freshData.status ?? ApprovalStatus.Pending,
|
||||||
|
});
|
||||||
|
setIsEditChangeModalOpen(true);
|
||||||
|
} catch {
|
||||||
|
setEditingChange(cr);
|
||||||
|
editChangeForm.reset({
|
||||||
|
title: cr.title || '',
|
||||||
|
justification: cr.justification || '',
|
||||||
|
revisedBudget: cr.revisedBudget ?? 0,
|
||||||
|
status: cr.status ?? ApprovalStatus.Pending,
|
||||||
|
});
|
||||||
|
setIsEditChangeModalOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openResolveChange = (cr: ChangeRequest, statusToApply: ApprovalStatus) => {
|
const openResolveChange = (cr: ChangeRequest, statusToApply: ApprovalStatus) => {
|
||||||
setResolvingChange(cr);
|
setResolvingChange(cr);
|
||||||
resolveChangeForm.reset({
|
resolveChangeForm.reset({
|
||||||
@@ -619,16 +789,7 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
Resolve
|
Resolve
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => openEditIssue(iss)}
|
||||||
setEditingIssue(iss);
|
|
||||||
issueForm.reset({
|
|
||||||
title: iss.title || '',
|
|
||||||
description: iss.description || '',
|
|
||||||
priority: iss.priority || Priority.Medium,
|
|
||||||
assignedTo: iss.assignedTo || '',
|
|
||||||
});
|
|
||||||
setIsIssueModalOpen(true);
|
|
||||||
}}
|
|
||||||
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
|
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
|
||||||
title="Edit issue"
|
title="Edit issue"
|
||||||
>
|
>
|
||||||
@@ -791,26 +952,35 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
<ApprovalStatusBadge status={cr.status} />
|
<ApprovalStatusBadge status={cr.status} />
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
||||||
{cr.status === ApprovalStatus.Approved ? (
|
<div className="inline-flex items-center gap-1.5">
|
||||||
<span className="text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
|
<button
|
||||||
Resolved & Enacted
|
onClick={() => openEditChange(cr)}
|
||||||
</span>
|
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
|
||||||
) : (
|
title="Edit Change Request"
|
||||||
<div className="inline-flex items-center gap-1.5">
|
>
|
||||||
<button
|
<Edit2 className="w-3.5 h-3.5" />
|
||||||
onClick={() => openResolveChange(cr, ApprovalStatus.Approved)}
|
</button>
|
||||||
className="px-2.5 py-1 text-xs font-semibold text-emerald-300 bg-emerald-950/50 hover:bg-emerald-900/60 border border-emerald-700/50 rounded-lg transition cursor-pointer flex items-center gap-1"
|
{cr.status === ApprovalStatus.Approved ? (
|
||||||
>
|
<span className="text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
|
||||||
<Check className="w-3 h-3" /> Approve
|
Resolved & Enacted
|
||||||
</button>
|
</span>
|
||||||
<button
|
) : (
|
||||||
onClick={() => openResolveChange(cr, ApprovalStatus.Rejected)}
|
<>
|
||||||
className="px-2.5 py-1 text-xs font-semibold text-rose-300 bg-rose-950/50 hover:bg-rose-900/60 border border-rose-700/50 rounded-lg transition cursor-pointer flex items-center gap-1"
|
<button
|
||||||
>
|
onClick={() => openResolveChange(cr, ApprovalStatus.Approved)}
|
||||||
<XCircle className="w-3 h-3" /> Reject
|
className="px-2.5 py-1 text-xs font-semibold text-emerald-300 bg-emerald-950/50 hover:bg-emerald-900/60 border border-emerald-700/50 rounded-lg transition cursor-pointer flex items-center gap-1"
|
||||||
</button>
|
>
|
||||||
</div>
|
<Check className="w-3 h-3" /> Approve
|
||||||
)}
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openResolveChange(cr, ApprovalStatus.Rejected)}
|
||||||
|
className="px-2.5 py-1 text-xs font-semibold text-rose-300 bg-rose-950/50 hover:bg-rose-900/60 border border-rose-700/50 rounded-lg transition cursor-pointer flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<XCircle className="w-3 h-3" /> Reject
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@@ -823,15 +993,15 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal: Create/Edit Issue */}
|
{/* Modal: Log Project Issue */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isIssueModalOpen}
|
isOpen={isIssueModalOpen}
|
||||||
onClose={() => setIsIssueModalOpen(false)}
|
onClose={() => setIsIssueModalOpen(false)}
|
||||||
title={editingIssue ? 'Edit Project Issue' : 'Log Project Issue'}
|
title="Log Project Issue"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={issueForm.handleSubmit((d) => (editingIssue ? updateIssueMutation.mutate(d) : createIssueMutation.mutate(d)))}
|
onSubmit={issueForm.handleSubmit((d) => createIssueMutation.mutate(d))}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -841,6 +1011,9 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
placeholder="e.g. Procurement delays for regional solar supplies"
|
placeholder="e.g. Procurement delays for regional solar supplies"
|
||||||
/>
|
/>
|
||||||
|
{issueForm.formState.errors.title && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{issueForm.formState.errors.title.message}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -851,6 +1024,9 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
placeholder="Detail root cause and operational impact..."
|
placeholder="Detail root cause and operational impact..."
|
||||||
/>
|
/>
|
||||||
|
{issueForm.formState.errors.description && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{issueForm.formState.errors.description.message}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -880,6 +1056,9 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{issueForm.formState.errors.assignedTo && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{issueForm.formState.errors.assignedTo.message}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -893,10 +1072,185 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={createIssueMutation.isPending || updateIssueMutation.isPending}
|
disabled={createIssueMutation.isPending}
|
||||||
className="px-4 py-2 rounded-xl text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white transition shadow-md shadow-amber-600/30"
|
className="px-4 py-2 rounded-xl text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white transition shadow-md shadow-amber-600/30 disabled:opacity-50 flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{editingIssue ? 'Update Issue' : 'Log Issue'}
|
{createIssueMutation.isPending && <RefreshCw className="w-3.5 h-3.5 animate-spin" />}
|
||||||
|
<span>Log Issue</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Modal: Edit Project Issue */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isEditIssueModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsEditIssueModalOpen(false);
|
||||||
|
setEditingIssue(null);
|
||||||
|
}}
|
||||||
|
title={`Edit Project Issue: ${editingIssue?.title || ''}`}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={editIssueForm.handleSubmit((d) => updateIssueMutation.mutate(d))}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Issue Title *</label>
|
||||||
|
<input
|
||||||
|
{...editIssueForm.register('title')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
placeholder="e.g. Procurement delays for regional solar supplies"
|
||||||
|
/>
|
||||||
|
{editIssueForm.formState.errors.title && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editIssueForm.formState.errors.title.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Description *</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
{...editIssueForm.register('description')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
placeholder="Detail root cause and operational impact..."
|
||||||
|
/>
|
||||||
|
{editIssueForm.formState.errors.description && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editIssueForm.formState.errors.description.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Priority</label>
|
||||||
|
<select
|
||||||
|
{...editIssueForm.register('priority')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
>
|
||||||
|
{PRIORITY_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Assigned Person *</label>
|
||||||
|
<select
|
||||||
|
{...editIssueForm.register('assignedTo')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
>
|
||||||
|
<option value="">Select Assignee</option>
|
||||||
|
{users.map((u) => (
|
||||||
|
<option key={u.id} value={u.id}>
|
||||||
|
{u.name || u.email || u.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{editIssueForm.formState.errors.assignedTo && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editIssueForm.formState.errors.assignedTo.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status & Resolution Lifecycle Container */}
|
||||||
|
<div className={`p-4 rounded-2xl border transition-colors space-y-4 ${
|
||||||
|
Number(watchedEditStatus) === ActivityStatus.Inactive
|
||||||
|
? 'bg-emerald-950/20 border-emerald-500/30'
|
||||||
|
: 'bg-[#0d121f] border-slate-800'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800/80">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className={`w-4 h-4 ${Number(watchedEditStatus) === ActivityStatus.Inactive ? 'text-emerald-400' : 'text-amber-400'}`} />
|
||||||
|
<span className="text-xs font-semibold text-white">Status & Resolution Lifecycle</span>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2.5 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-wider border ${
|
||||||
|
Number(watchedEditStatus) === ActivityStatus.Inactive
|
||||||
|
? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30'
|
||||||
|
: 'bg-amber-500/10 text-amber-400 border-amber-500/30'
|
||||||
|
}`}>
|
||||||
|
{Number(watchedEditStatus) === ActivityStatus.Inactive ? 'Resolved / Closed' : 'Active / Open'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Issue Status *</label>
|
||||||
|
<select
|
||||||
|
{...editIssueForm.register('status')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
>
|
||||||
|
<option value={ActivityStatus.Active}>Active (Open / In Progress)</option>
|
||||||
|
<option value={ActivityStatus.Inactive}>Inactive (Resolved & Closed)</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-[11px] text-slate-400 mt-1">
|
||||||
|
{Number(watchedEditStatus) === ActivityStatus.Inactive
|
||||||
|
? 'Issue will be marked as closed with full resolution audit.'
|
||||||
|
: 'Issue remains active and tracked as an open impediment.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label className="block text-xs font-semibold text-slate-300">Resolution Date & Time *</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => editIssueForm.setValue('resolvedAt', toDateTimeLocalValue())}
|
||||||
|
className="text-[11px] font-semibold text-amber-400 hover:text-amber-300 transition cursor-pointer flex items-center gap-1"
|
||||||
|
title="Set to current date and time"
|
||||||
|
>
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>Set to Now</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
{...editIssueForm.register('resolvedAt')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
/>
|
||||||
|
{editIssueForm.formState.errors.resolvedAt && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editIssueForm.formState.errors.resolvedAt.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Resolution Summary & Remediation Notes *</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
{...editIssueForm.register('resolution')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
placeholder={
|
||||||
|
Number(watchedEditStatus) === ActivityStatus.Inactive
|
||||||
|
? 'Explain how the issue was remediated, workarounds applied, and validation steps...'
|
||||||
|
: 'Document ongoing remediation steps, progress notes, or blocker details...'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{editIssueForm.formState.errors.resolution && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editIssueForm.formState.errors.resolution.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t border-slate-800">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditIssueModalOpen(false);
|
||||||
|
setEditingIssue(null);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={updateIssueMutation.isPending}
|
||||||
|
className="px-4 py-2 rounded-xl text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white transition shadow-md shadow-amber-600/30 disabled:opacity-50 flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
{updateIssueMutation.isPending && <RefreshCw className="w-3.5 h-3.5 animate-spin" />}
|
||||||
|
<span>Update Issue</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -1050,6 +1404,88 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Modal: Edit Change Request */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isEditChangeModalOpen}
|
||||||
|
onClose={() => setIsEditChangeModalOpen(false)}
|
||||||
|
title={`Edit Change Request: ${editingChange?.title || ''}`}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={editChangeForm.handleSubmit((d) => updateChangeMutation.mutate(d))}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Proposal Title *</label>
|
||||||
|
<input
|
||||||
|
{...editChangeForm.register('title')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
placeholder="e.g. Scope extension for additional beneficiary training"
|
||||||
|
/>
|
||||||
|
{editChangeForm.formState.errors.title && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editChangeForm.formState.errors.title.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Business Justification *</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
{...editChangeForm.register('justification')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
placeholder="Provide statutory and contractual justification..."
|
||||||
|
/>
|
||||||
|
{editChangeForm.formState.errors.justification && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editChangeForm.formState.errors.justification.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Revised Total Budget (ZAR) *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
{...editChangeForm.register('revisedBudget')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
/>
|
||||||
|
{editChangeForm.formState.errors.revisedBudget && (
|
||||||
|
<p className="text-[11px] text-rose-400 mt-1">{editChangeForm.formState.errors.revisedBudget.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1">Governance Status *</label>
|
||||||
|
<select
|
||||||
|
{...editChangeForm.register('status')}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-500"
|
||||||
|
>
|
||||||
|
<option value={ApprovalStatus.Pending}>Pending Review</option>
|
||||||
|
<option value={ApprovalStatus.Review}>Under Active Review</option>
|
||||||
|
<option value={ApprovalStatus.Approved}>Approved & Enacted</option>
|
||||||
|
<option value={ApprovalStatus.Rejected}>Rejected</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t border-slate-800">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEditChangeModalOpen(false)}
|
||||||
|
className="px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={updateChangeMutation.isPending}
|
||||||
|
className="px-4 py-2 rounded-xl text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white transition shadow-md shadow-amber-600/30"
|
||||||
|
>
|
||||||
|
{updateChangeMutation.isPending ? 'Saving...' : 'Update Change Request'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
getProjects,
|
getProjects,
|
||||||
getProjectSiteVisits,
|
getProjectSiteVisits,
|
||||||
|
getSiteVisit,
|
||||||
createSiteVisit,
|
createSiteVisit,
|
||||||
updateSiteVisit,
|
updateSiteVisit,
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
@@ -161,15 +162,27 @@ export const SiteVisitsPage: React.FC = () => {
|
|||||||
{ header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending') },
|
{ header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending') },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleOpenEdit = (v: SiteVisit) => {
|
const handleOpenEdit = async (v: SiteVisit) => {
|
||||||
setEditingVisit(v);
|
try {
|
||||||
visitForm.reset({
|
const fresh = await getSiteVisit(v.id);
|
||||||
inspectorName: v.inspectorName || '',
|
setEditingVisit(fresh);
|
||||||
visitedAt: v.visitedAt ? v.visitedAt.split('T')[0] : new Date().toISOString().split('T')[0],
|
visitForm.reset({
|
||||||
findings: v.findings || '',
|
inspectorName: fresh.inspectorName || '',
|
||||||
status: v.status ?? ApprovalStatus.Pending,
|
visitedAt: fresh.visitedAt ? fresh.visitedAt.split('T')[0] : new Date().toISOString().split('T')[0],
|
||||||
});
|
findings: fresh.findings || '',
|
||||||
setIsVisitModalOpen(true);
|
status: fresh.status ?? ApprovalStatus.Pending,
|
||||||
|
});
|
||||||
|
setIsVisitModalOpen(true);
|
||||||
|
} catch {
|
||||||
|
setEditingVisit(v);
|
||||||
|
visitForm.reset({
|
||||||
|
inspectorName: v.inspectorName || '',
|
||||||
|
visitedAt: v.visitedAt ? v.visitedAt.split('T')[0] : new Date().toISOString().split('T')[0],
|
||||||
|
findings: v.findings || '',
|
||||||
|
status: v.status ?? ApprovalStatus.Pending,
|
||||||
|
});
|
||||||
|
setIsVisitModalOpen(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1549,8 +1549,18 @@ export const getProjectBeneficiaries = async (projectId: string | number): Promi
|
|||||||
|
|
||||||
export const getBeneficiary = async (beneficiaryId: string | number): Promise<Beneficiary> => {
|
export const getBeneficiary = async (beneficiaryId: string | number): Promise<Beneficiary> => {
|
||||||
const hashedBeneficiaryId = await toolsService.encodeId(beneficiaryId);
|
const hashedBeneficiaryId = await toolsService.encodeId(beneficiaryId);
|
||||||
const res = await apiClient.get<Beneficiary>(`/v1/api/projects/beneficiaries/${hashedBeneficiaryId}`);
|
try {
|
||||||
return res.data;
|
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> => {
|
export const createBeneficiary = async (payload: CreateBeneficiaryRequest): Promise<void> => {
|
||||||
@@ -1635,8 +1645,18 @@ export const getProjectChangeRequests = async (projectId: string | number): Prom
|
|||||||
|
|
||||||
export const getChangeRequest = async (changeRequestId: string | number): Promise<ChangeRequest> => {
|
export const getChangeRequest = async (changeRequestId: string | number): Promise<ChangeRequest> => {
|
||||||
const hashedChangeReqId = await toolsService.encodeId(changeRequestId);
|
const hashedChangeReqId = await toolsService.encodeId(changeRequestId);
|
||||||
const res = await apiClient.get<ChangeRequest>(`/v1/api/projects/changerequests/${hashedChangeReqId}`);
|
try {
|
||||||
return res.data;
|
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> => {
|
export const createChangeRequest = async (payload: CreateChangeRequest): Promise<void> => {
|
||||||
@@ -1733,18 +1753,31 @@ export const createIssue = async (payload: CreateIssueRequest): Promise<void> =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateIssue = async (payload: UpdateIssueRequest): Promise<void> => {
|
export const updateIssue = async (payload: UpdateIssueRequest): Promise<void> => {
|
||||||
const resolvedAtIso = payload.resolvedAt
|
let resolvedAtIso: string | null = null;
|
||||||
? (payload.resolvedAt.includes('T') ? payload.resolvedAt : new Date(payload.resolvedAt).toISOString())
|
if (payload.resolvedAt) {
|
||||||
: null;
|
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', {
|
await apiClient.put('/v1/api/projects/issues', {
|
||||||
issueId: toIntegerId(payload.issueId),
|
issueId: toIntegerId(payload.issueId),
|
||||||
title: payload.title,
|
title: payload.title,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
resolution: payload.resolution,
|
resolution: finalResolution,
|
||||||
resolvedAt: resolvedAtIso,
|
resolvedAt: resolvedAtIso,
|
||||||
priority: Number(payload.priority),
|
priority: Number(payload.priority),
|
||||||
status: Number(payload.status),
|
status: payload.status !== undefined ? Number(payload.status) : ActivityStatus.Active,
|
||||||
assignedTo: payload.assignedTo,
|
assignedTo: payload.assignedTo,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1772,8 +1805,18 @@ export const getProjectSiteVisits = async (projectId: string | number): Promise<
|
|||||||
|
|
||||||
export const getSiteVisit = async (siteVisitId: string | number): Promise<SiteVisit> => {
|
export const getSiteVisit = async (siteVisitId: string | number): Promise<SiteVisit> => {
|
||||||
const hashedVisitId = await toolsService.encodeId(siteVisitId);
|
const hashedVisitId = await toolsService.encodeId(siteVisitId);
|
||||||
const res = await apiClient.get<SiteVisit>(`/v1/api/projects/sitevisits/${hashedVisitId}`);
|
try {
|
||||||
return res.data;
|
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> => {
|
export const createSiteVisit = async (payload: CreateSiteVisitRequest): Promise<void> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user