Enforced approval rights
This commit is contained in:
@@ -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 & 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 & 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 & 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 & Enacted {reason}
|
||||
</option>
|
||||
);
|
||||
})()}
|
||||
<option value={ApprovalStatus.Rejected}>Rejected</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user