Enforced approval rights
This commit is contained in:
@@ -74,7 +74,7 @@ export const ProjectActivitiesModal: React.FC<ProjectActivitiesModalProps> = ({
|
||||
});
|
||||
|
||||
const form = useForm<ActivityFormValues>({
|
||||
resolver: zodResolver(activitySchema) as any,
|
||||
resolver: zodResolver(activitySchema),
|
||||
defaultValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
@@ -226,7 +226,7 @@ export const ProjectActivitiesModal: React.FC<ProjectActivitiesModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data as any))} className="space-y-3">
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Activity Title *</label>
|
||||
<input
|
||||
|
||||
@@ -47,7 +47,7 @@ export const ProjectFormModal: React.FC<ProjectFormModalProps> = ({
|
||||
const { success, error: toastError } = useToast();
|
||||
|
||||
const form = useForm<ProjectFormValues>({
|
||||
resolver: zodResolver(projectSchema) as any,
|
||||
resolver: zodResolver(projectSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -125,7 +125,7 @@ export const ProjectFormModal: React.FC<ProjectFormModalProps> = ({
|
||||
helpText="Register or modify a funded project. Ensure the correct parent programme and industrial sector are assigned to preserve statutory reporting compliance."
|
||||
maxWidth="lg"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data as any))} className="space-y-4">
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))} className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<label className="block text-xs font-semibold text-slate-300">Project Name *</label>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const ProjectIndicatorsModal: React.FC<ProjectIndicatorsModalProps> = ({
|
||||
});
|
||||
|
||||
const form = useForm<IndicatorFormValues>({
|
||||
resolver: zodResolver(indicatorSchema) as any,
|
||||
resolver: zodResolver(indicatorSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
unitOfMeasure: UnitOfMeasure.Count,
|
||||
@@ -173,7 +173,7 @@ export const ProjectIndicatorsModal: React.FC<ProjectIndicatorsModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={form.handleSubmit((data) => createMutation.mutate(data as any))} className="space-y-3">
|
||||
<form onSubmit={form.handleSubmit((data) => createMutation.mutate(data))} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Indicator Definition *</label>
|
||||
<input
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TrendingUp,
|
||||
X,
|
||||
RotateCcw,
|
||||
Lock,
|
||||
} from 'lucide-react';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { HelpTooltip } from '../ui/HelpTooltip';
|
||||
@@ -74,7 +75,7 @@ export const ProjectMilestonesModal: React.FC<ProjectMilestonesModalProps> = ({
|
||||
});
|
||||
|
||||
const form = useForm<MilestoneFormValues>({
|
||||
resolver: zodResolver(milestoneSchema) as any,
|
||||
resolver: zodResolver(milestoneSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
dueAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
|
||||
@@ -210,7 +211,7 @@ export const ProjectMilestonesModal: React.FC<ProjectMilestonesModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data as any))} className="space-y-3">
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Deliverable Name / Milestone Title *
|
||||
@@ -257,33 +258,40 @@ export const ProjectMilestonesModal: React.FC<ProjectMilestonesModalProps> = ({
|
||||
{editingMilestone && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Approval / Verification Status</label>
|
||||
{(() => {
|
||||
const canApproveMilestone = canApproveRecord('projects_milestones', editingMilestone.createdBy);
|
||||
const isSelf = isMaker(editingMilestone.createdBy);
|
||||
{Number(editingMilestone.status) === ApprovalStatus.Approved ? (
|
||||
<div className="w-full px-3 py-2 text-xs border border-emerald-500/30 bg-emerald-500/10 rounded-xl text-emerald-300 font-semibold flex items-center gap-1.5 cursor-default">
|
||||
<Lock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Approved & Verified (Immutable)</span>
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
const canApproveMilestone = canApproveRecord('projects_milestones', editingMilestone.createdBy);
|
||||
const isSelf = isMaker(editingMilestone.createdBy);
|
||||
|
||||
return (
|
||||
<select
|
||||
{...form.register('status')}
|
||||
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
|
||||
>
|
||||
{APPROVAL_STATUS_OPTIONS.map((opt) => {
|
||||
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
|
||||
const isOptDisabled = isApprovedOpt && !canApproveMilestone;
|
||||
let reasonText = '';
|
||||
if (isApprovedOpt && !canApprove('projects_milestones')) {
|
||||
reasonText = ' (Requires Update Permission)';
|
||||
} else if (isApprovedOpt && isSelf) {
|
||||
reasonText = ' (Maker-Checker: Cannot approve own record)';
|
||||
}
|
||||
return (
|
||||
<option key={opt.value} value={opt.value} disabled={isOptDisabled}>
|
||||
{opt.label}{reasonText}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
})()}
|
||||
return (
|
||||
<select
|
||||
{...form.register('status')}
|
||||
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
|
||||
>
|
||||
{APPROVAL_STATUS_OPTIONS.map((opt) => {
|
||||
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
|
||||
const isOptDisabled = isApprovedOpt && !canApproveMilestone;
|
||||
let reasonText = '';
|
||||
if (isApprovedOpt && !canApprove('projects_milestones')) {
|
||||
reasonText = ' (Requires Update Permission)';
|
||||
} else if (isApprovedOpt && isSelf) {
|
||||
reasonText = ' (Maker-Checker: Cannot approve own record)';
|
||||
}
|
||||
return (
|
||||
<option key={opt.value} value={opt.value} disabled={isOptDisabled}>
|
||||
{opt.label}{reasonText}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export const ProjectRisksModal: React.FC<ProjectRisksModalProps> = ({
|
||||
});
|
||||
|
||||
const form = useForm<RiskFormValues>({
|
||||
resolver: zodResolver(riskSchema) as any,
|
||||
resolver: zodResolver(riskSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -202,7 +202,7 @@ export const ProjectRisksModal: React.FC<ProjectRisksModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data as any))} className="space-y-3">
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Risk Hazard Title *</label>
|
||||
<input
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Sparkles,
|
||||
ExternalLink,
|
||||
SlidersHorizontal,
|
||||
Lock,
|
||||
} from 'lucide-react';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { HelpTooltip } from '../ui/HelpTooltip';
|
||||
@@ -282,36 +283,46 @@ export const ProjectViewModal: React.FC<ProjectViewModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Real-time Governance Status Dropdown */}
|
||||
<div className="w-36">
|
||||
<select
|
||||
value={project.status}
|
||||
disabled={!canModifyStatus}
|
||||
onChange={(e) =>
|
||||
statusMutation.mutate({
|
||||
projectId: project.id,
|
||||
status: Number(e.target.value) as ApprovalStatus,
|
||||
})
|
||||
}
|
||||
className="w-full text-xs font-semibold bg-[#090d16] border border-slate-700/90 rounded-xl px-2.5 py-2 text-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed truncate shadow-md"
|
||||
title="Project Governance Clearance Status"
|
||||
>
|
||||
{APPROVAL_STATUS_OPTIONS.map((opt) => {
|
||||
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
|
||||
const isOptDisabled = isApprovedOpt && !canApproveThis;
|
||||
let reasonText = '';
|
||||
if (isApprovedOpt && !canApprove('projects_milestones')) {
|
||||
reasonText = ' (Requires Update)';
|
||||
} else if (isApprovedOpt && isSelf) {
|
||||
reasonText = ' (Maker-Checker)';
|
||||
{/* Real-time Governance Status Dropdown / Immutable Locked State */}
|
||||
<div className="min-w-[140px]">
|
||||
{Number(project.status) === ApprovalStatus.Approved ? (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-emerald-500/15 border border-emerald-500/30 text-emerald-300 shadow-sm cursor-default"
|
||||
title="Approved records are immutable. Status cannot be modified."
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Approved (Locked)</span>
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={project.status}
|
||||
disabled={!canModifyStatus}
|
||||
onChange={(e) =>
|
||||
statusMutation.mutate({
|
||||
projectId: project.id,
|
||||
status: Number(e.target.value) as ApprovalStatus,
|
||||
})
|
||||
}
|
||||
return (
|
||||
<option key={opt.value} value={opt.value} disabled={isOptDisabled}>
|
||||
{opt.label}{reasonText}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
className="w-full text-xs font-semibold bg-[#090d16] border border-slate-700/90 rounded-xl px-2.5 py-2 text-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed truncate shadow-md"
|
||||
title="Project Governance Clearance Status"
|
||||
>
|
||||
{APPROVAL_STATUS_OPTIONS.map((opt) => {
|
||||
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
|
||||
const isOptDisabled = isApprovedOpt && !canApproveThis;
|
||||
let reasonText = '';
|
||||
if (isApprovedOpt && !canApprove('projects_milestones')) {
|
||||
reasonText = ' (Requires Update)';
|
||||
} else if (isApprovedOpt && isSelf) {
|
||||
reasonText = ' (Maker-Checker)';
|
||||
}
|
||||
return (
|
||||
<option key={opt.value} value={opt.value} disabled={isOptDisabled}>
|
||||
{opt.label}{reasonText}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
|
||||
Reference in New Issue
Block a user