Implemented session and account lockout management

Enabled account sync control and session expiration management
Applied some fixes to the govername page and project page
This commit is contained in:
2026-09-18 10:19:24 +02:00
parent 41bf9b63bf
commit ec777fabf8
24 changed files with 3888 additions and 500 deletions
@@ -21,6 +21,7 @@ import {
Download,
Upload,
Link as LinkIcon,
Edit2,
} from 'lucide-react';
import {
getAwards,
@@ -39,6 +40,7 @@ import {
getDisbursements,
getDisbursement,
createDisbursement,
updateDisbursement,
approveDisbursement,
getOrganisations,
getAllProgrammes,
@@ -122,6 +124,7 @@ export const GrantsPage: React.FC = () => {
const [selectedContractForExecution, setSelectedContractForExecution] = useState<Contract | null>(null);
const [isBudgetModalOpen, setIsBudgetModalOpen] = useState(false);
const [isDisbursementModalOpen, setIsDisbursementModalOpen] = useState(false);
const [editingDisbursement, setEditingDisbursement] = useState<Disbursement | null>(null);
// Queries
const { data: awards = [], isLoading: loadingAwards } = useQuery({
@@ -227,7 +230,7 @@ export const GrantsPage: React.FC = () => {
awardForm.reset();
},
onError: (err: unknown) => {
toastError('Award registration failed', String(err));
toastError('Award registration failed', err);
},
});
@@ -240,7 +243,7 @@ export const GrantsPage: React.FC = () => {
contractForm.reset();
},
onError: (err: unknown) => {
toastError('Contract creation failed', String(err));
toastError('Contract creation failed', err);
},
});
@@ -256,7 +259,7 @@ export const GrantsPage: React.FC = () => {
executeForm.reset();
},
onError: (err: unknown) => {
toastError('Execution trigger failed', String(err));
toastError('Execution trigger failed', err);
},
});
@@ -269,23 +272,60 @@ export const GrantsPage: React.FC = () => {
budgetForm.reset();
},
onError: (err: unknown) => {
toastError('Budget creation failed', String(err));
toastError('Budget creation failed', err);
},
});
const disbursementMutation = useMutation({
mutationFn: (data: CreateDisbursementRequest) => createDisbursement(data),
mutationFn: async (data: DisbursementFormValues) => {
if (editingDisbursement) {
return updateDisbursement({
disbursementId: editingDisbursement.id,
milestoneId: data.milestoneId ? Number(data.milestoneId) : null,
amount: Number(data.amount),
});
}
return createDisbursement({
projectId: Number(data.projectId),
milestoneId: data.milestoneId ? Number(data.milestoneId) : null,
amount: Number(data.amount),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['disbursements'] });
success('Disbursement Scheduled', 'Payment tranche added in Pending Review state');
success(
editingDisbursement ? 'Disbursement Updated' : 'Disbursement Scheduled',
editingDisbursement ? 'Payment tranche details updated' : 'Payment tranche added in Pending Review state'
);
setIsDisbursementModalOpen(false);
setEditingDisbursement(null);
disbursementForm.reset();
},
onError: (err: unknown) => {
toastError('Disbursement creation failed', String(err));
toastError(editingDisbursement ? 'Disbursement update failed' : 'Disbursement creation failed', err);
},
});
const openCreateDisbursement = () => {
setEditingDisbursement(null);
disbursementForm.reset({
projectId: projects[0]?.id ? Number(projects[0].id) : 401,
milestoneId: undefined,
amount: 150000,
});
setIsDisbursementModalOpen(true);
};
const openEditDisbursement = (d: Disbursement) => {
setEditingDisbursement(d);
disbursementForm.reset({
projectId: Number(d.projectId) || (projects[0]?.id ? Number(projects[0].id) : 401),
milestoneId: d.milestoneId ? Number(d.milestoneId) : undefined,
amount: d.amount,
});
setIsDisbursementModalOpen(true);
};
const approveMutation = useMutation({
mutationFn: (id: string | number) => approveDisbursement(id),
onSuccess: () => {
@@ -297,7 +337,7 @@ export const GrantsPage: React.FC = () => {
);
},
onError: (err: unknown) => {
toastError('Disbursement approval failed', String(err));
toastError('Disbursement approval failed', err);
},
});
@@ -321,7 +361,7 @@ export const GrantsPage: React.FC = () => {
toastError('Download Error', 'No active secure download link generated.');
}
} catch (err: any) {
toastError('Link Error', err?.message || 'Failed to fetch contract secure link.');
toastError('Link Error', err);
}
};
@@ -331,7 +371,7 @@ export const GrantsPage: React.FC = () => {
success('Document Attached', `Contract #${contractId} document uploaded successfully.`);
queryClient.invalidateQueries({ queryKey: ['contracts'] });
} catch (err: any) {
toastError('Upload Failed', err?.message || 'Contract file upload failed.');
toastError('Upload Failed', err);
}
};
@@ -341,7 +381,7 @@ export const GrantsPage: React.FC = () => {
success('Contract Updated', `Contract status updated to ${getContractStatusBadge(status).label}.`);
queryClient.invalidateQueries({ queryKey: ['contracts'] });
} catch (err: any) {
toastError('Update Failed', err?.message || 'Failed to update contract status.');
toastError('Update Failed', err);
}
};
@@ -398,7 +438,7 @@ export const GrantsPage: React.FC = () => {
)}
{activeTab === 'disbursements' && (
<button
onClick={() => setIsDisbursementModalOpen(true)}
onClick={openCreateDisbursement}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
<Plus className="w-4 h-4" />
@@ -735,21 +775,33 @@ export const GrantsPage: React.FC = () => {
</span>
</td>
<td className="py-3.5 px-5 text-right">
{isApproved ? (
<span className="inline-flex items-center gap-1 text-xs text-emerald-400 font-semibold bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
<CheckCircle className="w-3.5 h-3.5" />
Released
</span>
) : (
<button
onClick={() => approveMutation.mutate(d.id)}
disabled={approveMutation.isPending}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-lg shadow-md shadow-blue-500/20 transition"
>
<CheckCircle className="w-3.5 h-3.5" />
Approve Payment
</button>
)}
<div className="flex items-center justify-end gap-1.5">
{isApproved ? (
<span className="inline-flex items-center gap-1 text-xs text-emerald-400 font-semibold bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
<CheckCircle className="w-3.5 h-3.5" />
Released
</span>
) : (
<>
<button
type="button"
onClick={() => openEditDisbursement(d)}
title="Edit Disbursement"
className="p-1.5 rounded-lg text-slate-400 hover:text-white bg-slate-800 hover:bg-slate-700 transition cursor-pointer"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
<button
onClick={() => approveMutation.mutate(d.id)}
disabled={approveMutation.isPending}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-lg shadow-md shadow-blue-500/20 transition cursor-pointer"
>
<CheckCircle className="w-3.5 h-3.5" />
Approve Payment
</button>
</>
)}
</div>
</td>
</tr>
);
@@ -1052,27 +1104,36 @@ export const GrantsPage: React.FC = () => {
</form>
</Modal>
{/* Modal: Schedule Disbursement */}
{/* Modal: Schedule or Edit Disbursement */}
<Modal
isOpen={isDisbursementModalOpen}
onClose={() => setIsDisbursementModalOpen(false)}
title="Schedule Disbursement Tranche"
subtitle="Link disbursement to milestone verification and conditions precedent"
onClose={() => {
setIsDisbursementModalOpen(false);
setEditingDisbursement(null);
}}
title={editingDisbursement ? 'Edit Disbursement Tranche' : 'Schedule Disbursement Tranche'}
subtitle={
editingDisbursement
? `Updating payment tranche DSB-${editingDisbursement.id}`
: 'Link disbursement to milestone verification and conditions precedent'
}
>
<form onSubmit={disbursementForm.handleSubmit((data) => disbursementMutation.mutate(data as any))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Select Project *</label>
<select
{...disbursementForm.register('projectId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
{!editingDisbursement && (
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Select Project *</label>
<select
{...disbursementForm.register('projectId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Linked Milestone Deliverable</label>
@@ -1101,7 +1162,10 @@ export const GrantsPage: React.FC = () => {
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsDisbursementModalOpen(false)}
onClick={() => {
setIsDisbursementModalOpen(false);
setEditingDisbursement(null);
}}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
@@ -1111,7 +1175,11 @@ export const GrantsPage: React.FC = () => {
disabled={disbursementMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
{disbursementMutation.isPending ? 'Scheduling...' : 'Schedule Disbursement'}
{disbursementMutation.isPending
? 'Saving...'
: editingDisbursement
? 'Update Disbursement'
: 'Schedule Disbursement'}
</button>
</div>
</form>