Enforced approval rights

This commit is contained in:
2026-10-22 11:25:55 +02:00
parent 0261a6093e
commit 92891438f6
32 changed files with 1891 additions and 870 deletions
@@ -20,6 +20,7 @@ import {
User,
Clock,
ArrowRight,
Lock,
} from 'lucide-react';
import {
getProjects,
@@ -238,7 +239,7 @@ export const ChangeManagementPage: React.FC = () => {
issueForm.reset();
success('Issue Logged', 'New project issue created and assigned.');
},
onError: (err: any) => toastError('Creation Failed', err),
onError: (err: unknown) => toastError('Creation Failed', err),
});
const updateIssueMutation = useMutation({
@@ -273,7 +274,7 @@ export const ChangeManagementPage: React.FC = () => {
editIssueForm.reset();
success('Issue Updated', 'Issue record and resolution status saved successfully.');
},
onError: (err: any) => toastError('Update Failed', err),
onError: (err: unknown) => toastError('Update Failed', err),
});
const resolveIssueMutation = useMutation({
@@ -297,7 +298,7 @@ export const ChangeManagementPage: React.FC = () => {
resolveIssueForm.reset();
success('Issue Resolved', 'Resolution recorded and issue status updated.');
},
onError: (err: any) => toastError('Resolution Failed', err),
onError: (err: unknown) => toastError('Resolution Failed', err),
});
const createChangeMutation = useMutation({
@@ -317,7 +318,7 @@ export const ChangeManagementPage: React.FC = () => {
changeForm.reset();
success('Change Request Submitted', 'Formal change request submitted for review.');
},
onError: (err: any) => toastError('Submission Failed', err),
onError: (err: unknown) => toastError('Submission Failed', err),
});
const resolveChangeMutation = useMutation({
@@ -338,12 +339,15 @@ export const ChangeManagementPage: React.FC = () => {
resolveChangeForm.reset();
success('Change Decision Applied', 'Change request decision successfully resolved.');
},
onError: (err: any) => toastError('Decision Failed', err),
onError: (err: unknown) => toastError('Decision Failed', err),
});
const updateChangeMutation = useMutation({
mutationFn: (vals: EditChangeFormValues) => {
if (!editingChange) throw new Error('No change request selected');
if (Number(editingChange.status) === ApprovalStatus.Approved) {
throw new Error('Approved change requests are immutable and cannot be updated.');
}
return updateChangeRequest({
changeRequestId: editingChange.id,
title: vals.title,
@@ -359,7 +363,7 @@ export const ChangeManagementPage: React.FC = () => {
editChangeForm.reset();
success('Change Request Updated', 'Change proposal and governance status saved.');
},
onError: (err: any) => toastError('Update Failed', err),
onError: (err: unknown) => toastError('Update Failed', err),
});
// Filtered & Sorted Issues
@@ -481,6 +485,10 @@ export const ChangeManagementPage: React.FC = () => {
};
const openEditChange = async (cr: ChangeRequest) => {
if (Number(cr.status) === ApprovalStatus.Approved) {
toastError('Record Immutable', 'Approved and enacted change requests are immutable.');
return;
}
try {
// 1. Uses GET method (NOT POST)
// 2. Passes ChangeRequestId (NOT ProjectId)
@@ -1009,19 +1017,20 @@ export const ChangeManagementPage: React.FC = () => {
</td>
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
<div className="inline-flex items-center gap-1.5">
<button
onClick={() => openEditChange(cr)}
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
title="Edit Change Request"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
{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">
Resolved & Enacted
{Number(cr.status) === ApprovalStatus.Approved ? (
<span className="inline-flex items-center gap-1 text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20 cursor-default" title="Approved and enacted change requests are immutable.">
<Lock className="w-3 h-3 text-emerald-400" />
<span>Resolved &amp; Enacted (Immutable)</span>
</span>
) : (
<>
<button
onClick={() => openEditChange(cr)}
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
title="Edit Change Request"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
{canApproveRecord('change_management', cr.createdBy) && (
<button
onClick={() => openResolveChange(cr, ApprovalStatus.Approved)}
@@ -1035,14 +1044,6 @@ export const ChangeManagementPage: React.FC = () => {
Maker-Checker
</span>
)}
{canAdd('change_management') && (
<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>
@@ -1521,29 +1522,39 @@ export const ChangeManagementPage: React.FC = () => {
<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>
{(() => {
const canApproveChange = canApproveRecord('change_management', editingChange?.createdBy);
const isSelf = isMaker(editingChange?.createdBy);
let reason = '';
if (!canApprove('change_management')) {
reason = ' (Requires Update Permission)';
} else if (isSelf) {
reason = ' (Maker-Checker: Cannot approve own record)';
}
return (
<option value={ApprovalStatus.Approved} disabled={!canApproveChange}>
Approved &amp; Enacted {reason}
</option>
);
})()}
<option value={ApprovalStatus.Rejected}>Rejected</option>
</select>
{editingChange && Number(editingChange.status) === ApprovalStatus.Approved ? (
<div
className="w-full bg-slate-900 border border-emerald-500/30 rounded-xl px-3 py-2 text-xs text-emerald-300 font-semibold flex items-center gap-1.5 cursor-default"
title="Approved and enacted change requests are immutable."
>
<Lock className="w-3.5 h-3.5 text-emerald-400" />
<span>Approved &amp; Enacted (Immutable - Cannot Be Changed)</span>
</div>
) : (
<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>
{(() => {
const canApproveChange = canApproveRecord('change_management', editingChange?.createdBy);
const isSelf = isMaker(editingChange?.createdBy);
let reason = '';
if (!canApprove('change_management')) {
reason = ' (Requires Update Permission)';
} else if (isSelf) {
reason = ' (Maker-Checker: Cannot approve own record)';
}
return (
<option value={ApprovalStatus.Approved} disabled={!canApproveChange}>
Approved &amp; Enacted {reason}
</option>
);
})()}
<option value={ApprovalStatus.Rejected}>Rejected</option>
</select>
)}
</div>
</div>