Improved compliance monitoring
This commit is contained in:
@@ -42,9 +42,9 @@ import { ExportButton } from '../components/ui/ExportButton';
|
||||
import { ExportColumn } from '../utils/exportUtils';
|
||||
import { useTableSort, SortableTh } from '../utils/tableSort';
|
||||
import { useRbac } from '../auth/rbac';
|
||||
import { ApprovalStatusBadge } from '../components/ui/StatusBadge';
|
||||
import { Beneficiary, ComplianceItem, Outcome, ApprovalStatus } from '../types';
|
||||
import { getApprovalStatusBadge, formatDate, APPROVAL_STATUS_OPTIONS } from '../lib/formatters';
|
||||
import { ApprovalStatusBadge, ComplianceStatusBadge } from '../components/ui/StatusBadge';
|
||||
import { Beneficiary, ComplianceItem, Outcome, ApprovalStatus, ComplianceStatus } from '../types';
|
||||
import { getApprovalStatusBadge, getComplianceStatusBadge, formatDate, APPROVAL_STATUS_OPTIONS, COMPLIANCE_STATUS_OPTIONS } from '../lib/formatters';
|
||||
|
||||
const beneficiarySchema = z.object({
|
||||
name: z.string().min(2, 'Beneficiary name required'),
|
||||
@@ -58,11 +58,13 @@ const complianceSchema = z.object({
|
||||
title: z.string().min(3, 'Compliance title required'),
|
||||
requirements: z.string().min(5, 'Requirements detail required'),
|
||||
status: z.coerce.number().optional(),
|
||||
beneficiaryId: z.string().optional(),
|
||||
});
|
||||
|
||||
const outcomeSchema = z.object({
|
||||
name: z.string().min(3, 'Outcome name required'),
|
||||
description: z.string().min(5, 'Description required'),
|
||||
status: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
type BeneficiaryFormValues = z.infer<typeof beneficiarySchema>;
|
||||
@@ -224,7 +226,13 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
mutationFn: (vals: ComplianceFormValues) => {
|
||||
const targetProjId = compTargetProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id);
|
||||
if (!targetProjId) throw new Error('No project selected');
|
||||
return createComplianceItem({ projectId: targetProjId, title: vals.title, requirements: vals.requirements });
|
||||
return createComplianceItem({
|
||||
projectId: targetProjId,
|
||||
title: vals.title,
|
||||
requirements: vals.requirements,
|
||||
status: vals.status ?? ComplianceStatus.NotMet,
|
||||
beneficiaryId: vals.beneficiaryId && vals.beneficiaryId !== 'none' ? Number(vals.beneficiaryId) : null,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['complianceItems'] });
|
||||
@@ -242,7 +250,8 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
complianceItemId: editingComp.id,
|
||||
title: vals.title,
|
||||
requirements: vals.requirements,
|
||||
status: vals.status ?? ApprovalStatus.Pending,
|
||||
status: vals.status ?? ComplianceStatus.NotMet,
|
||||
beneficiaryId: vals.beneficiaryId && vals.beneficiaryId !== 'none' ? Number(vals.beneficiaryId) : null,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -258,7 +267,12 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
const createOutcomeMutation = useMutation({
|
||||
mutationFn: (vals: OutcomeFormValues) => {
|
||||
if (!selectedBenForDetail) throw new Error('No beneficiary selected');
|
||||
return createOutcome({ beneficiaryId: selectedBenForDetail.id, name: vals.name, description: vals.description });
|
||||
return createOutcome({
|
||||
beneficiaryId: selectedBenForDetail.id,
|
||||
name: vals.name,
|
||||
description: vals.description,
|
||||
status: vals.status ?? ComplianceStatus.NotMet,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
refetchOutcomes();
|
||||
@@ -360,7 +374,8 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
compForm.reset({
|
||||
title: fresh.title || '',
|
||||
requirements: fresh.requirements || '',
|
||||
status: fresh.status ?? ApprovalStatus.Pending,
|
||||
status: fresh.status ?? ComplianceStatus.NotMet,
|
||||
beneficiaryId: fresh.beneficiaryId ? String(fresh.beneficiaryId) : 'none',
|
||||
});
|
||||
setIsCompModalOpen(true);
|
||||
} catch {
|
||||
@@ -368,7 +383,8 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
compForm.reset({
|
||||
title: c.title || '',
|
||||
requirements: c.requirements || '',
|
||||
status: c.status ?? ApprovalStatus.Pending,
|
||||
status: c.status ?? ComplianceStatus.NotMet,
|
||||
beneficiaryId: c.beneficiaryId ? String(c.beneficiaryId) : 'none',
|
||||
});
|
||||
setIsCompModalOpen(true);
|
||||
}
|
||||
@@ -868,7 +884,7 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
{c.requirements || 'None'}
|
||||
</td>
|
||||
<td className="py-3.5 px-4 whitespace-nowrap">
|
||||
<ApprovalStatusBadge status={c.status} />
|
||||
<ComplianceStatusBadge status={c.status} />
|
||||
</td>
|
||||
<td className="py-3.5 px-4 whitespace-nowrap text-slate-400">{formatDate(c.createdAt)}</td>
|
||||
<td className="py-3.5 px-5 text-right whitespace-nowrap">
|
||||
@@ -1018,45 +1034,36 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editingComp && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Verification Status</label>
|
||||
{Number(editingComp.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 compliance records are immutable."
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Approved (Immutable - Cannot Be Changed)</span>
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
{...compForm.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-blue-500"
|
||||
>
|
||||
{(() => {
|
||||
const canApproveComp = canApproveRecord('beneficiaries', editingComp?.createdBy);
|
||||
const isSelf = isMaker(editingComp?.createdBy);
|
||||
return APPROVAL_STATUS_OPTIONS.map((opt) => {
|
||||
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
|
||||
const isOptDisabled = isApprovedOpt && !canApproveComp;
|
||||
let reason = '';
|
||||
if (isApprovedOpt && !canApprove('beneficiaries')) {
|
||||
reason = ' (Requires Update Permission)';
|
||||
} else if (isApprovedOpt && isSelf) {
|
||||
reason = ' (Maker-Checker: Cannot approve own record)';
|
||||
}
|
||||
return (
|
||||
<option key={opt.value} value={opt.value} disabled={isOptDisabled}>
|
||||
{opt.label}{reason}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Linked Beneficiary (Optional)</label>
|
||||
<select
|
||||
{...compForm.register('beneficiaryId')}
|
||||
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-blue-500 cursor-pointer"
|
||||
>
|
||||
<option value="none">-- Project-Wide (All Beneficiaries) --</option>
|
||||
{beneficiaries
|
||||
.filter((b) => !compTargetProjectId || String(b.projectId) === String(compTargetProjectId))
|
||||
.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{b.name} ({b.category})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Compliance Status</label>
|
||||
<select
|
||||
{...compForm.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-blue-500 cursor-pointer"
|
||||
>
|
||||
{COMPLIANCE_STATUS_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-slate-800">
|
||||
<button
|
||||
@@ -1194,6 +1201,20 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">Fulfillment Status</label>
|
||||
<select
|
||||
{...outcomeForm.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-blue-500 cursor-pointer"
|
||||
>
|
||||
{COMPLIANCE_STATUS_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user