Enforced approval rights

This commit is contained in:
2026-10-22 11:25:55 +02:00
parent 0261a6093e
commit 92891438f6
32 changed files with 1891 additions and 870 deletions
@@ -19,6 +19,7 @@ import {
ShieldCheck,
Building,
Send,
Lock,
} from 'lucide-react';
import {
getProjects,
@@ -114,12 +115,15 @@ export const SiteVisitsPage: React.FC = () => {
visitForm.reset();
success('Site Visit Logged', 'Field inspection recorded with inspector findings.');
},
onError: (err: any) => toastError('Logging Failed', err),
onError: (err: unknown) => toastError('Logging Failed', err),
});
const updateVisitMutation = useMutation({
mutationFn: (vals: SiteVisitFormValues) => {
if (!editingVisit) throw new Error('No visit selected');
if (Number(editingVisit.status) === ApprovalStatus.Approved) {
throw new Error('Approved site visits are verified and immutable. No modifications are permitted.');
}
return updateSiteVisit({
siteVisitId: editingVisit.id,
inspectorName: vals.inspectorName,
@@ -135,7 +139,7 @@ export const SiteVisitsPage: React.FC = () => {
visitForm.reset();
success('Site Visit Updated', 'Inspection report and verification status updated.');
},
onError: (err: any) => toastError('Update Failed', err),
onError: (err: unknown) => toastError('Update Failed', err),
});
// Filtered & Sorted Visits
@@ -165,6 +169,10 @@ export const SiteVisitsPage: React.FC = () => {
];
const handleOpenEdit = async (v: SiteVisit) => {
if (Number(v.status) === ApprovalStatus.Approved) {
toastError('Record Immutable', 'Approved site visits are verified and cannot be edited.');
return;
}
try {
const fresh = await getSiteVisit(v.id);
setEditingVisit(fresh);
@@ -446,13 +454,22 @@ export const SiteVisitsPage: React.FC = () => {
>
View & Feedback
</button>
<button
onClick={() => handleOpenEdit(v)}
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
title="Edit site visit"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
{Number(v.status) === ApprovalStatus.Approved ? (
<span
className="p-1 text-emerald-400 cursor-default"
title="Approved site visits are verified and immutable."
>
<Lock className="w-3.5 h-3.5" />
</span>
) : (
<button
onClick={() => handleOpenEdit(v)}
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
title="Edit site visit"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
)}
</td>
</tr>
))
@@ -505,30 +522,40 @@ export const SiteVisitsPage: React.FC = () => {
{editingVisit && (
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Verification Status</label>
<select
{...visitForm.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-emerald-500"
>
{(() => {
const canApproveVisit = canApproveRecord('site_visits', editingVisit?.createdBy);
const isSelf = isMaker(editingVisit?.createdBy);
return APPROVAL_STATUS_OPTIONS.map((opt) => {
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
const isOptDisabled = isApprovedOpt && !canApproveVisit;
let reason = '';
if (isApprovedOpt && !canApprove('site_visits')) {
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>
{Number(editingVisit.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 site visits are immutable."
>
<Lock className="w-3.5 h-3.5 text-emerald-400" />
<span>Approved &amp; Verified (Immutable - Cannot Be Changed)</span>
</div>
) : (
<select
{...visitForm.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-emerald-500"
>
{(() => {
const canApproveVisit = canApproveRecord('site_visits', editingVisit?.createdBy);
const isSelf = isMaker(editingVisit?.createdBy);
return APPROVAL_STATUS_OPTIONS.map((opt) => {
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
const isOptDisabled = isApprovedOpt && !canApproveVisit;
let reason = '';
if (isApprovedOpt && !canApprove('site_visits')) {
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>
)}