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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user