Remediated test #1 results
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -20,16 +21,12 @@ import {
|
||||
Filter,
|
||||
Link as LinkIcon,
|
||||
Edit2,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getAllIndicators,
|
||||
createIndicator,
|
||||
recordIndicatorActual,
|
||||
getAllEvidence,
|
||||
getMilestoneEvidence,
|
||||
getEvidenceSecureLink,
|
||||
uploadEvidenceFile,
|
||||
updateEvidence,
|
||||
getAllRisks,
|
||||
createRisk,
|
||||
updateRisk,
|
||||
@@ -40,10 +37,10 @@ import { Modal } from '../components/ui/Modal';
|
||||
import { HelpTooltip } from '../components/ui/HelpTooltip';
|
||||
import { useToast } from '../components/ui/Toast';
|
||||
import { ContractsManagement } from '../components/monitoring/ContractsManagement';
|
||||
import { EvidenceReviewQueue } from '../components/monitoring/EvidenceReviewQueue';
|
||||
import { RiskHeatmapMatrix } from '../components/ui/RiskHeatmapMatrix';
|
||||
import {
|
||||
Indicator,
|
||||
Evidence,
|
||||
Risk,
|
||||
ApprovalStatus,
|
||||
UnitOfMeasure,
|
||||
@@ -85,13 +82,40 @@ const riskSchema = z.object({
|
||||
type IndicatorFormValues = z.infer<typeof indicatorSchema>;
|
||||
type RiskFormValues = z.infer<typeof riskSchema>;
|
||||
|
||||
export const WORKFLOW_STAGES = [
|
||||
{ id: 'planned', label: 'Planned', step: 1, description: 'Milestone scope agreed and deliverables planned' },
|
||||
{ id: 'in_progress', label: 'In Progress', step: 2, description: 'Activity underway by recipient organisation' },
|
||||
{ id: 'submitted', label: 'Submitted', step: 3, description: 'Evidence and deliverables submitted by recipient' },
|
||||
{ id: 'under_review', label: 'Under Review', step: 4, description: 'Programme Manager verifying evidence against criteria' },
|
||||
{ id: 'verified_paid', label: 'Verified & Paid', step: 5, description: 'Milestone verified and disbursement authorized' },
|
||||
] as const;
|
||||
|
||||
export const MonitoringPage: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { success, error: toastError } = useToast();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'contracts' | 'evidence' | 'indicators' | 'risks'>('contracts');
|
||||
const tabParam = searchParams.get('tab');
|
||||
const [activeTab, setActiveTab] = useState<'contracts' | 'evidence' | 'indicators' | 'risks'>(
|
||||
tabParam === 'evidence' || tabParam === 'indicators' || tabParam === 'risks' ? tabParam : 'contracts'
|
||||
);
|
||||
const [isCreateContractModalOpen, setIsCreateContractModalOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const t = searchParams.get('tab');
|
||||
if (t === 'evidence' || t === 'indicators' || t === 'risks' || t === 'contracts') {
|
||||
setActiveTab(t);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleTabChange = (tab: 'contracts' | 'evidence' | 'indicators' | 'risks') => {
|
||||
setActiveTab(tab);
|
||||
setSearchParams((prev) => {
|
||||
const next = new URLSearchParams(prev);
|
||||
next.set('tab', tab);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Modals & States
|
||||
const [isIndicatorModalOpen, setIsIndicatorModalOpen] = useState(false);
|
||||
@@ -99,11 +123,6 @@ export const MonitoringPage: React.FC = () => {
|
||||
const [editingRisk, setEditingRisk] = useState<Risk | null>(null);
|
||||
const [selectedIndicatorForActual, setSelectedIndicatorForActual] = useState<Indicator | null>(null);
|
||||
const [actualInput, setActualInput] = useState<number>(0);
|
||||
const [uploadEvidenceType, setUploadEvidenceType] = useState<number>(EvidenceType.CompletionCertificate);
|
||||
|
||||
// Reviewer commentary
|
||||
const [reviewerComment, setReviewerComment] = useState('');
|
||||
const [selectedMilestoneEvidenceFilter, setSelectedMilestoneEvidenceFilter] = useState<string | number | 'all'>('all');
|
||||
|
||||
// Queries
|
||||
const { data: indicators = [], isLoading: loadingIndicators } = useQuery({
|
||||
@@ -111,14 +130,6 @@ export const MonitoringPage: React.FC = () => {
|
||||
queryFn: () => getAllIndicators(),
|
||||
});
|
||||
|
||||
const { data: evidenceList = [], isLoading: loadingEvidence } = useQuery({
|
||||
queryKey: ['evidence', selectedMilestoneEvidenceFilter],
|
||||
queryFn: () =>
|
||||
selectedMilestoneEvidenceFilter !== 'all'
|
||||
? getMilestoneEvidence(selectedMilestoneEvidenceFilter)
|
||||
: getAllEvidence(),
|
||||
});
|
||||
|
||||
const { data: risks = [], isLoading: loadingRisks } = useQuery({
|
||||
queryKey: ['risks'],
|
||||
queryFn: () => getAllRisks(),
|
||||
@@ -184,42 +195,6 @@ export const MonitoringPage: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: (formData: FormData) => uploadEvidenceFile(formData),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['evidence'] });
|
||||
success('Evidence uploaded', 'Supporting file registered for reviewer inspection.');
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toastError('Upload failed', err);
|
||||
},
|
||||
});
|
||||
|
||||
const reviewEvidenceMutation = useMutation({
|
||||
mutationFn: ({ id, status }: { id: string | number; status: ApprovalStatus }) =>
|
||||
updateEvidence({
|
||||
evidenceId: id,
|
||||
version: 2,
|
||||
evidenceType: EvidenceType.DeliverableProof,
|
||||
status,
|
||||
}),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['evidence'] });
|
||||
if (variables.status === ApprovalStatus.Approved) {
|
||||
success(
|
||||
'Evidence Approved & Verified',
|
||||
'Approval recorded. Auto-triggers payment eligibility check in Disbursements.'
|
||||
);
|
||||
} else {
|
||||
success('Rework Requested', 'Evidence returned to recipient for correction.');
|
||||
}
|
||||
setReviewerComment('');
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toastError('Review update failed', err);
|
||||
},
|
||||
});
|
||||
|
||||
const openCreateRisk = () => {
|
||||
setEditingRisk(null);
|
||||
riskForm.reset({
|
||||
@@ -279,40 +254,14 @@ export const MonitoringPage: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('projectId', '401');
|
||||
formData.append('milestoneId', '803');
|
||||
formData.append('evidenceType', String(uploadEvidenceType));
|
||||
uploadMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetEvidenceSecureLink = async (evidenceId: string | number) => {
|
||||
try {
|
||||
const url = await getEvidenceSecureLink(evidenceId);
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
success('Secure URL Retrieved', 'Signed evidence download link opened.');
|
||||
} else {
|
||||
toastError('Download Error', 'No active secure download link generated.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toastError('Link Error', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Performance indicator thresholds:
|
||||
// Green >= 90%, Amber 70-89%, Red < 70%
|
||||
const getIndicatorStatus = (actual: number, target: number) => {
|
||||
if (!target) return { label: 'Pending', badge: 'bg-slate-800/60 text-slate-400 border-slate-700/60' };
|
||||
const pct = (actual / target) * 100;
|
||||
if (pct >= 90) return { label: 'Green (≥90%)', badge: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' };
|
||||
if (pct >= 70) return { label: 'Amber (70–89%)', badge: 'bg-amber-500/10 text-amber-400 border-amber-500/20' };
|
||||
return { label: 'Red (<70%)', badge: 'bg-rose-500/10 text-rose-400 border-rose-500/20' };
|
||||
if (pct >= 90) return { label: '≥90%', badge: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' };
|
||||
if (pct >= 70) return { label: '70–89%', badge: 'bg-amber-500/10 text-amber-400 border-amber-500/20' };
|
||||
return { label: '<70%', badge: 'bg-rose-500/10 text-rose-400 border-rose-500/20' };
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -367,8 +316,8 @@ export const MonitoringPage: React.FC = () => {
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 border-b border-slate-800/80 pb-3 overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setActiveTab('contracts')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${
|
||||
onClick={() => handleTabChange('contracts')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === 'contracts'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
@@ -384,25 +333,25 @@ export const MonitoringPage: React.FC = () => {
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('evidence')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${
|
||||
onClick={() => handleTabChange('evidence')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === 'evidence'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Paperclip className="w-4 h-4" />
|
||||
<span>Milestone Evidence & Review Queue</span>
|
||||
<FileCheck className="w-4 h-4" />
|
||||
<span>Evidence Review & Verification</span>
|
||||
<HelpTooltip
|
||||
title="Verification Workflow"
|
||||
content="Evidence submission, file validation, and peer review sign-off gating milestone payments."
|
||||
title="Evidence Review & Verification"
|
||||
content="Peer review, document inspection, and verification sign-offs for deliverables submitted against projects and milestones."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('indicators')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${
|
||||
onClick={() => handleTabChange('indicators')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === 'indicators'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
@@ -418,8 +367,8 @@ export const MonitoringPage: React.FC = () => {
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('risks')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${
|
||||
onClick={() => handleTabChange('risks')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === 'risks'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
@@ -444,220 +393,14 @@ export const MonitoringPage: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tab 1: Milestone Evidence Submission & Review */}
|
||||
{/* Tab 1: Project & Milestone Evidence Review Hub */}
|
||||
{activeTab === 'evidence' && (
|
||||
<div className="space-y-6">
|
||||
{/* Milestone Evidence Review Container */}
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
||||
{/* Header Bar */}
|
||||
<div className="bg-slate-900/90 text-white px-6 py-4 flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80">
|
||||
<span className="text-xs font-mono font-semibold tracking-wider text-blue-400 uppercase">
|
||||
RECIPIENT PORTAL • Project XYZ • Milestone 3: Prototype Delivery
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-400 font-mono">Milestone Evidence Submission</span>
|
||||
</div>
|
||||
|
||||
{/* Workflow Progress Steps */}
|
||||
<div className="p-6 border-b border-slate-800/80 bg-slate-900/40">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 text-center text-xs font-semibold">
|
||||
<div className="py-2.5 px-3 rounded-xl bg-slate-800/60 text-slate-400 border border-slate-700/50">Planned</div>
|
||||
<div className="py-2.5 px-3 rounded-xl bg-slate-800/60 text-slate-400 border border-slate-700/50">In Progress</div>
|
||||
<div className="py-2.5 px-3 rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-600/30 ring-2 ring-blue-500/40">
|
||||
Submitted
|
||||
</div>
|
||||
<div className="py-2.5 px-3 rounded-xl bg-amber-500/15 text-amber-300 border border-amber-500/30">
|
||||
Under Review
|
||||
</div>
|
||||
<div className="py-2.5 px-3 rounded-xl bg-slate-900/40 text-slate-500 border border-dashed border-slate-800">
|
||||
Verified & Paid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-2 text-slate-300">
|
||||
<span className="font-semibold">Current status:</span>
|
||||
<span className="font-mono text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20 font-bold">
|
||||
SUBMITTED — awaiting Programme Manager review
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upload Evidence & Attached Evidence Panels */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-slate-800/80">
|
||||
{/* Upload Box */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-bold text-white">Upload Evidence</h4>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-[11px] font-medium text-slate-400">Type:</label>
|
||||
<select
|
||||
value={uploadEvidenceType}
|
||||
onChange={(e) => setUploadEvidenceType(Number(e.target.value))}
|
||||
className="text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 max-w-[180px] [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
{EVIDENCE_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">
|
||||
Drag & drop or browse files. Accepted formats: PDF, JPG, DOCX, XLSX (Max 25MB).
|
||||
</p>
|
||||
|
||||
<div
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="border-2 border-dashed border-slate-700/80 hover:border-blue-500/80 bg-slate-900/40 hover:bg-blue-500/5 rounded-2xl p-8 text-center cursor-pointer transition flex flex-col items-center justify-center space-y-3"
|
||||
>
|
||||
<Upload className="w-8 h-8 text-slate-400" />
|
||||
<div>
|
||||
<span className="text-xs font-bold text-white block">Click to Browse or Drag Files Here</span>
|
||||
<span className="text-[10px] text-slate-500">Automated SHA-256 hash stamp applied on intake</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-1.5 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-xl shadow-xs transition"
|
||||
>
|
||||
Browse Files
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.docx,.xlsx,.zip"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Attached Evidence List */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-white">Attached Evidence (v2)</h4>
|
||||
<span className="text-[11px] text-slate-400 font-mono">{evidenceList.length} files loaded</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-400">Milestone:</span>
|
||||
<select
|
||||
value={selectedMilestoneEvidenceFilter}
|
||||
onChange={(e) => setSelectedMilestoneEvidenceFilter(e.target.value === 'all' ? 'all' : e.target.value)}
|
||||
className="text-xs bg-slate-900 border border-slate-700/80 rounded-xl px-2.5 py-1 text-white focus:outline-none focus:ring-1 focus:ring-blue-500 max-w-[200px] [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
<option value="all">All Milestones</option>
|
||||
{milestones.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name || `Milestone #${m.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{evidenceList.map((ev) => (
|
||||
<div
|
||||
key={ev.id}
|
||||
className="p-3 rounded-xl border border-slate-800/80 bg-slate-900/60 flex items-center justify-between gap-3 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<FileText className="w-4 h-4 text-blue-400 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<span className="font-semibold text-slate-200 block truncate font-mono">
|
||||
{ev.fileName || `evidence_file_${ev.id}.pdf`}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 font-mono">
|
||||
{ev.fileSize || '3.2 MB'} • v{ev.version} • {getEvidenceTypeLabel(ev.type)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold bg-amber-500/10 text-amber-400 border border-amber-500/20">
|
||||
Under Review
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleGetEvidenceSecureLink(ev.id)}
|
||||
className="p-1 rounded text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
|
||||
title="Fetch Secure Signed Link"
|
||||
>
|
||||
<LinkIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
{ev.documentUrl && (
|
||||
<a
|
||||
href={ev.documentUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-400 hover:text-white p-1 transition"
|
||||
title="Download evidence file"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Programme Manager Review Section */}
|
||||
<div className="p-6 border-t border-slate-800/80 bg-slate-900/40 space-y-4">
|
||||
<h4 className="text-sm font-bold text-white">Programme Manager Review</h4>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Reviewer comment / rejection reason
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={reviewerComment}
|
||||
onChange={(e) => setReviewerComment(e.target.value)}
|
||||
placeholder="Mandatory if requesting rework; optional for approvals..."
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() =>
|
||||
reviewEvidenceMutation.mutate({
|
||||
id: evidenceList[0]?.id || 1201,
|
||||
status: ApprovalStatus.Rejected,
|
||||
})
|
||||
}
|
||||
disabled={reviewEvidenceMutation.isPending}
|
||||
className="px-4 py-2 text-xs font-semibold text-white bg-rose-600/90 hover:bg-rose-600 rounded-xl shadow-lg shadow-rose-600/20 transition"
|
||||
>
|
||||
Reject / Rework
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
reviewEvidenceMutation.mutate({
|
||||
id: evidenceList[0]?.id || 1201,
|
||||
status: ApprovalStatus.Approved,
|
||||
})
|
||||
}
|
||||
disabled={reviewEvidenceMutation.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"
|
||||
>
|
||||
Approve & Verify
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-slate-400 italic">
|
||||
Approval auto-triggers payment eligibility check.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EvidenceReviewQueue
|
||||
initialProjectId={searchParams.get('projectId') || 'all'}
|
||||
initialMilestoneId={searchParams.get('milestoneId') || 'all'}
|
||||
initialActivityId={searchParams.get('activityId') || 'all'}
|
||||
initialUserId={searchParams.get('userId') || 'all'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tab 2: Indicators Table (M08) */}
|
||||
@@ -964,7 +707,7 @@ export const MonitoringPage: React.FC = () => {
|
||||
<label className="block text-xs font-semibold text-slate-300">Actual Value to Record *</label>
|
||||
<HelpTooltip
|
||||
title="Monitored Measurement"
|
||||
content="Current realized value for this reporting period. Automatically updates Green (>=90%), Amber (70-89%), or Red (<70%) status."
|
||||
content="Current realized value for this reporting period. Automatically updates to ≥90%, 70–89%, or <70% performance threshold status."
|
||||
size="sm"
|
||||
placement="right"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user