Enforced approval rights
This commit is contained in:
@@ -247,7 +247,7 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
|
||||
|
||||
// Target contractId for upload
|
||||
const contractIdToUploadAgainst =
|
||||
createdContract?.id ?? (createdContract as any)?.contractId ?? selectedAwardId;
|
||||
createdContract?.id ?? selectedAwardId;
|
||||
|
||||
// Step 2: Upload PDF document to POST /v1/api/grants/contracts/{contractId}
|
||||
if (contractPdfFile && contractIdToUploadAgainst) {
|
||||
@@ -277,6 +277,9 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
|
||||
mutationFn: async () => {
|
||||
setUploadApiError(null);
|
||||
if (!contractForFileUpload) throw new Error('No contract selected for upload.');
|
||||
if (Number(contractForFileUpload.status) === ContractStatus.Active) {
|
||||
throw new Error('Active contracts are legally executed and immutable. Documents cannot be modified.');
|
||||
}
|
||||
if (!selectedUploadFile) throw new Error('Please select a PDF document to upload.');
|
||||
|
||||
await uploadContractFile(contractForFileUpload.id, selectedUploadFile);
|
||||
@@ -417,21 +420,21 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
|
||||
sortDirection: contractSortDir,
|
||||
handleSort: handleContractSort,
|
||||
} = useTableSort<Contract>(filteredContracts, null, null, {
|
||||
id: (c: any) => c.id,
|
||||
awardId: (c: any) => c.totalValue || c.awardId,
|
||||
programmeId: (c: any) => {
|
||||
id: (c: Contract) => c.id,
|
||||
awardId: (c: Contract) => c.totalValue || c.awardId,
|
||||
programmeId: (c: Contract) => {
|
||||
const a = getAwardForContract(c);
|
||||
const pr = getProgrammeForAward(a);
|
||||
return pr?.name || '';
|
||||
},
|
||||
organisationId: (c: any) => {
|
||||
organisationId: (c: Contract) => {
|
||||
const a = getAwardForContract(c);
|
||||
const o = getOrganisationForAward(a);
|
||||
return o?.name || '';
|
||||
},
|
||||
signedDate: (c: any) => c.signedDate,
|
||||
status: (c: any) => getContractStatusBadge(c.status).label,
|
||||
fileUri: (c: any) => c.fileUri || '',
|
||||
signedDate: (c: Contract) => c.effectiveAt || c.createdAt,
|
||||
status: (c: Contract) => getContractStatusBadge(c.status).label,
|
||||
fileUri: (c: Contract) => c.signedDocumentUrl || '',
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -823,19 +826,21 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
|
||||
<span>Secure PDF</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setContractForFileUpload(contract);
|
||||
setSelectedUploadFile(null);
|
||||
setUploadApiError(null);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] text-slate-400 hover:text-slate-200 bg-slate-800 hover:bg-slate-700 rounded-lg border border-slate-700 transition cursor-pointer"
|
||||
title="Upload fresh PDF or replace existing contract document"
|
||||
>
|
||||
<Upload className="w-3 h-3" />
|
||||
<span>Replace</span>
|
||||
</button>
|
||||
{Number(contract.status) !== ContractStatus.Active && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setContractForFileUpload(contract);
|
||||
setSelectedUploadFile(null);
|
||||
setUploadApiError(null);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] text-slate-400 hover:text-slate-200 bg-slate-800 hover:bg-slate-700 rounded-lg border border-slate-700 transition cursor-pointer"
|
||||
title="Upload fresh PDF or replace existing contract document"
|
||||
>
|
||||
<Upload className="w-3 h-3" />
|
||||
<span>Replace</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-500 mt-1 flex items-center gap-1">
|
||||
<Lock className="w-2.5 h-2.5 text-slate-500" />
|
||||
@@ -845,18 +850,28 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
|
||||
|
||||
{/* Actions */}
|
||||
<td className="py-3.5 px-4 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setContractToUpdate(contract);
|
||||
setNewStatus(Number(contract.status));
|
||||
setUpdateApiError(null);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-medium text-slate-300 hover:text-white bg-slate-800/80 hover:bg-slate-700 rounded-lg border border-slate-700 transition cursor-pointer"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
<span>Status</span>
|
||||
</button>
|
||||
{Number(contract.status) === ContractStatus.Active ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-[11px] font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20 cursor-default"
|
||||
title="Active contracts are executed and immutable."
|
||||
>
|
||||
<Lock className="w-3 h-3 text-emerald-400" />
|
||||
<span>Active (Locked)</span>
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setContractToUpdate(contract);
|
||||
setNewStatus(Number(contract.status));
|
||||
setUpdateApiError(null);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-medium text-slate-300 hover:text-white bg-slate-800/80 hover:bg-slate-700 rounded-lg border border-slate-700 transition cursor-pointer"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
<span>Status</span>
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -1229,34 +1244,41 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
|
||||
<label className="block text-xs font-medium text-slate-200 mb-1">
|
||||
Contract Lifecycle Status <span className="text-rose-400">*</span>
|
||||
</label>
|
||||
{(() => {
|
||||
const canApproveContract = canApproveRecord('grant_financials', contractToUpdate?.createdBy);
|
||||
const isSelf = isMaker(contractToUpdate?.createdBy);
|
||||
{Number(contractToUpdate?.status) === ContractStatus.Active ? (
|
||||
<div className="w-full bg-[#151b2c] 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">
|
||||
<Lock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Active / Executed (Immutable - Cannot Be Changed)</span>
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
const canApproveContract = canApproveRecord('grant_financials', contractToUpdate?.createdBy);
|
||||
const isSelf = isMaker(contractToUpdate?.createdBy);
|
||||
|
||||
return (
|
||||
<select
|
||||
value={newStatus}
|
||||
onChange={(e) => setNewStatus(Number(e.target.value))}
|
||||
className="w-full bg-[#151b2c] border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500 font-medium"
|
||||
>
|
||||
{CONTRACT_STATUS_OPTIONS.map((opt) => {
|
||||
const isActiveOpt = opt.value === ContractStatus.Active;
|
||||
const isOptDisabled = isActiveOpt && !canApproveContract;
|
||||
let reasonText = '';
|
||||
if (isActiveOpt && !canApprove('grant_financials')) {
|
||||
reasonText = ' (Requires Update Permission)';
|
||||
} else if (isActiveOpt && 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
|
||||
value={newStatus}
|
||||
onChange={(e) => setNewStatus(Number(e.target.value))}
|
||||
className="w-full bg-[#151b2c] border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500 font-medium"
|
||||
>
|
||||
{CONTRACT_STATUS_OPTIONS.map((opt) => {
|
||||
const isActiveOpt = opt.value === ContractStatus.Active;
|
||||
const isOptDisabled = isActiveOpt && !canApproveContract;
|
||||
let reasonText = '';
|
||||
if (isActiveOpt && !canApprove('grant_financials')) {
|
||||
reasonText = ' (Requires Update Permission)';
|
||||
} else if (isActiveOpt && isSelf) {
|
||||
reasonText = ' (Maker-Checker: Cannot approve own record)';
|
||||
}
|
||||
return (
|
||||
<option key={opt.value} value={opt.value} disabled={isOptDisabled}>
|
||||
{opt.label}{reasonText}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-3 border-t border-slate-800">
|
||||
@@ -1269,7 +1291,7 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={updateStatusMutation.isPending}
|
||||
disabled={updateStatusMutation.isPending || Number(contractToUpdate?.status) === ContractStatus.Active}
|
||||
className="px-4 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 disabled:opacity-50 rounded-xl shadow-lg shadow-blue-600/30 transition flex items-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
{updateStatusMutation.isPending ? (
|
||||
|
||||
Reference in New Issue
Block a user