Fixed API calls on governance page

Enforced multitenancy
This commit is contained in:
2026-09-26 10:28:43 +02:00
parent b2688867f3
commit f3a8f78144
16 changed files with 3552 additions and 503 deletions
@@ -26,6 +26,7 @@ import {
} from 'lucide-react';
import {
getAllEvidence,
getProjectEvidence,
getEvidenceSecureLink,
updateEvidence,
uploadEvidenceFile,
@@ -82,8 +83,11 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
const queryClient = useQueryClient();
const { success, error: toastError } = useToast();
// Filter States
// Filter States: default to initialProjectId if given, otherwise when projects load default to first project
const [selectedProjectId, setSelectedProjectId] = useState<string | number | 'all'>(initialProjectId);
const [userHasSelectedProject, setUserHasSelectedProject] = useState(
initialProjectId !== undefined && initialProjectId !== null && initialProjectId !== 'all'
);
const [selectedMilestoneId, setSelectedMilestoneId] = useState<string | number | 'all'>(initialMilestoneId);
const [selectedActivityId, setSelectedActivityId] = useState<string | number | 'all'>(initialActivityId);
const [selectedUserId, setSelectedUserId] = useState<string | 'all'>(initialUserId);
@@ -116,6 +120,21 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
queryFn: () => getUsers(1, 100).catch(() => []),
});
// Keep selectedProjectId in sync when initialProjectId prop changes
React.useEffect(() => {
if (initialProjectId && initialProjectId !== 'all') {
setSelectedProjectId(initialProjectId);
setUserHasSelectedProject(true);
}
}, [initialProjectId]);
// Load evidence by default for the first project in the list
React.useEffect(() => {
if (!userHasSelectedProject && projects.length > 0 && selectedProjectId === 'all') {
setSelectedProjectId(projects[0].id);
}
}, [projects, userHasSelectedProject, selectedProjectId]);
// Query activities based on selected project or fetch all across available projects
const { data: activities = [] } = useQuery<Activity[]>({
queryKey: ['activities-review-queue', selectedProjectId],
@@ -134,104 +153,20 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
enabled: projects.length > 0,
});
// Query evidence: if a project is selected (e.g. the first project by default), fetch via /v1/api/evidence/{projectId}
const { data: rawEvidenceList = [], isLoading: loadingEvidence } = useQuery<Evidence[]>({
queryKey: ['evidence'],
queryFn: () => getAllEvidence().catch(() => []),
queryKey: ['evidence', selectedProjectId],
queryFn: async () => {
if (selectedProjectId && selectedProjectId !== 'all') {
return getProjectEvidence(selectedProjectId).catch(() => []);
}
return getAllEvidence().catch(() => []);
},
enabled: projects.length > 0 || selectedProjectId !== 'all',
});
// Baseline demonstration records to guarantee full functionality when backend vault is initially empty
const defaultEvidenceItems: Evidence[] = useMemo(() => {
const p1 = projects[0]?.id || 401;
const p2 = projects[1]?.id || projects[0]?.id || 402;
const m1 = milestones[0]?.id || 801;
const m2 = milestones[1]?.id || milestones[0]?.id || 802;
const a1 = activities[0]?.id || 101;
const a2 = activities[1]?.id || activities[0]?.id || 102;
const u1 = users[0]?.id || 'usr_awardee_01';
const u2 = users[1]?.id || users[0]?.id || 'usr_awardee_02';
const u3 = users[2]?.id || 'usr_awardee_03';
return [
{
id: 'ev-101',
projectId: p1,
milestoneId: m1,
activityId: a1,
version: 1,
type: EvidenceType.CompletionCertificate,
fileName: 'Civil_Works_Milestone_Completion_Certificate_Signed.pdf',
fileSize: '4.8 MB',
status: ApprovalStatus.PendingReview,
createdBy: u1,
createdAt: '2026-09-18T14:32:00Z',
documentUrl: 'https://storage.googleapis.com/pfm-evidence-vault/projects/401/milestones/801/completion_cert.pdf',
},
{
id: 'ev-102',
projectId: p1,
milestoneId: m1,
activityId: a1,
version: 2,
type: EvidenceType.GeotaggedPhoto,
fileName: 'Geotagged_Substation_Installation_Survey.pdf',
fileSize: '16.2 MB',
status: ApprovalStatus.PendingReview,
createdBy: u1,
createdAt: '2026-09-17T11:15:00Z',
documentUrl: 'https://storage.googleapis.com/pfm-evidence-vault/projects/401/activities/101/geotagged_photos.pdf',
},
{
id: 'ev-103',
projectId: p2,
milestoneId: m2,
activityId: a2,
version: 1,
type: EvidenceType.InspectionReport,
fileName: 'Independent_Structural_Quality_Audit_Report.pdf',
fileSize: '6.4 MB',
status: ApprovalStatus.Approved,
createdBy: u2,
createdAt: '2026-09-15T09:40:00Z',
documentUrl: 'https://storage.googleapis.com/pfm-evidence-vault/projects/402/milestones/802/audit_report.pdf',
},
{
id: 'ev-104',
projectId: p1,
milestoneId: m2,
activityId: a2,
version: 1,
type: EvidenceType.AttendanceRegister,
fileName: 'Community_Skills_Training_Attendance_Register_Q2.xlsx',
fileSize: '2.1 MB',
status: ApprovalStatus.PendingReview,
createdBy: u3,
createdAt: '2026-09-19T08:20:00Z',
documentUrl: 'https://storage.googleapis.com/pfm-evidence-vault/projects/401/activities/102/attendance.xlsx',
},
{
id: 'ev-105',
projectId: p2,
milestoneId: m1,
activityId: a1,
version: 1,
type: EvidenceType.FinancialInvoiceOrReceipt,
fileName: 'Vendor_Procurement_Tax_Invoices_Batch_A.pdf',
fileSize: '3.7 MB',
status: ApprovalStatus.Rejected,
createdBy: u2,
createdAt: '2026-09-14T16:05:00Z',
documentUrl: 'https://storage.googleapis.com/pfm-evidence-vault/projects/402/activities/101/invoices.pdf',
},
];
}, [projects, milestones, activities, users]);
// Merge backend data with demo fallback, deduplicating by ID
const allEvidence: Evidence[] = useMemo(() => {
const map = new Map<string | number, Evidence>();
defaultEvidenceItems.forEach((item) => map.set(item.id, item));
rawEvidenceList.forEach((item) => map.set(item.id, item));
return Array.from(map.values());
}, [defaultEvidenceItems, rawEvidenceList]);
// UI depends entirely on the API response - no mock or demo data
const allEvidence: Evidence[] = rawEvidenceList;
// Review evidence mutation: Facilitates Approve & Reject decisions via PUT /v1/api/evidence
const reviewMutation = useMutation({
@@ -258,7 +193,7 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
},
onSuccess: (_, variables) => {
// Update local query cache for instant feedback
queryClient.setQueryData(['evidence'], (old: Evidence[] = []) => {
queryClient.setQueryData(['evidence', selectedProjectId], (old: Evidence[] = []) => {
const exists = old.some((e) => String(e.id) === String(variables.id));
if (exists) {
return old.map((e) =>
@@ -416,9 +351,10 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
toastError('Missing File', 'Please select a document or certificate to upload.');
return;
}
const targetProjId = ingestProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id) || '';
const formData = new FormData();
formData.append('file', ingestFile);
formData.append('projectId', String(ingestProjectId || projects[0]?.id || '401'));
formData.append('projectId', String(targetProjId));
if (ingestMilestoneId) formData.append('milestoneId', String(ingestMilestoneId));
if (ingestActivityId) formData.append('activityId', String(ingestActivityId));
formData.append('evidenceType', String(ingestType));
@@ -427,7 +363,8 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
};
const handleResetFilters = () => {
setSelectedProjectId('all');
const defaultProjId = projects[0]?.id || 'all';
setSelectedProjectId(defaultProjId);
setSelectedMilestoneId('all');
setSelectedActivityId('all');
setSelectedUserId('all');
@@ -435,8 +372,12 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
setSearchQuery('');
};
const defaultProjectId = projects[0]?.id;
const isDefaultProject =
defaultProjectId !== undefined ? String(selectedProjectId) === String(defaultProjectId) : selectedProjectId === 'all';
const hasActiveFilters =
selectedProjectId !== 'all' ||
!isDefaultProject ||
selectedMilestoneId !== 'all' ||
selectedActivityId !== 'all' ||
selectedUserId !== 'all' ||
@@ -567,7 +508,10 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
</label>
<select
value={selectedProjectId}
onChange={(e) => setSelectedProjectId(e.target.value)}
onChange={(e) => {
setSelectedProjectId(e.target.value);
setUserHasSelectedProject(true);
}}
className="w-full text-xs bg-slate-900/90 border border-slate-700/80 rounded-xl px-2.5 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 [&>option]:bg-slate-900 [&>option]:text-white"
>
<option value="all">All Projects ({projects.length})</option>
@@ -1069,7 +1013,7 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
<div>
<label className="block text-slate-300 font-semibold mb-1">Target Project *</label>
<select
value={ingestProjectId || projects[0]?.id || ''}
value={ingestProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id) || ''}
onChange={(e) => setIngestProjectId(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-xl px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50"
>