Enforced approval rights
This commit is contained in:
@@ -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 & 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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user