Updated partner project listing to use returned data
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { api, extractApiErrors } from '../../services/api';
|
import { uploadEvidenceFile, extractApiErrors, toIntegerId } from '../../services/api';
|
||||||
import { Activity, Milestone, Project, EvidenceType } from '../../services/api/models';
|
import { Activity, Milestone, Project, EvidenceType } from '../../services/api/models';
|
||||||
|
import { EVIDENCE_TYPE_OPTIONS } from '../../lib/formatters';
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
UploadCloud,
|
UploadCloud,
|
||||||
@@ -17,7 +18,7 @@ interface EvidenceUploadModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
project: Project;
|
project: Project;
|
||||||
milestone?: Milestone;
|
milestone?: Milestone | null;
|
||||||
activity: Activity;
|
activity: Activity;
|
||||||
onEvidenceUploaded: () => void;
|
onEvidenceUploaded: () => void;
|
||||||
}
|
}
|
||||||
@@ -32,8 +33,7 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [documentUrl, setDocumentUrl] = useState('');
|
const [evidenceType, setEvidenceType] = useState<number>(4); // Default: FinancialInvoiceOrReceipt
|
||||||
const [evidenceType, setEvidenceType] = useState<EvidenceType>(1); // Invoice / Document
|
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
||||||
@@ -56,53 +56,47 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
setErrorMessages([]);
|
setErrorMessages([]);
|
||||||
setSuccessMessage(null);
|
setSuccessMessage(null);
|
||||||
|
|
||||||
if (!title.trim() && !selectedFile && !documentUrl.trim()) {
|
if (!selectedFile) {
|
||||||
setErrorMessages(['Please provide an evidence title, file upload, or document URL.']);
|
setErrorMessages(['A supporting document file is required. Please choose a file to upload.']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numProjectId = toIntegerId(project.id);
|
||||||
|
const numActivityId = toIntegerId(activity.id);
|
||||||
|
const numMilestoneId = toIntegerId(milestone?.id || activity.milestoneId || 0);
|
||||||
|
|
||||||
|
if (!numProjectId || !numActivityId) {
|
||||||
|
setErrorMessages(['Cannot upload evidence: Missing projectId or activityId reference.']);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build upload payload
|
// Build multipart/form-data adhering strictly to EvidenceService.CreateEvidenceAsync
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (selectedFile) {
|
formData.append('file', selectedFile);
|
||||||
formData.append('file', selectedFile);
|
formData.append('projectId', String(numProjectId));
|
||||||
|
formData.append('activityId', String(numActivityId));
|
||||||
|
formData.append('milestoneId', String(numMilestoneId));
|
||||||
|
formData.append('indicatorId', '0');
|
||||||
|
formData.append('type', String(Number(evidenceType) || 4));
|
||||||
|
formData.append('title', title.trim() || selectedFile.name);
|
||||||
|
if (description.trim()) {
|
||||||
|
formData.append('description', description.trim());
|
||||||
}
|
}
|
||||||
formData.append('title', title.trim() || selectedFile?.name || 'Evidence Deliverable');
|
|
||||||
formData.append('projectId', String(project.id || ''));
|
|
||||||
if (milestone?.id) formData.append('milestoneId', String(milestone.id));
|
|
||||||
if (activity?.id) formData.append('activityId', String(activity.id));
|
|
||||||
formData.append('type', String(evidenceType));
|
|
||||||
if (documentUrl.trim()) formData.append('documentUrl', documentUrl.trim());
|
|
||||||
if (description.trim()) formData.append('description', description.trim());
|
|
||||||
|
|
||||||
// Also support JSON payload fallback if server expects JSON body
|
await uploadEvidenceFile(formData);
|
||||||
try {
|
|
||||||
await api.evidence.createEvidenceFile(formData);
|
|
||||||
} catch (err: any) {
|
|
||||||
// Fallback: Attempt JSON payload if multipart failed or route expects JSON
|
|
||||||
const jsonPayload = {
|
|
||||||
projectId: project.id,
|
|
||||||
milestoneId: milestone?.id,
|
|
||||||
activityId: activity.id,
|
|
||||||
title: title.trim() || selectedFile?.name || 'Evidence Deliverable',
|
|
||||||
type: evidenceType,
|
|
||||||
documentUrl: documentUrl.trim() || (selectedFile ? `s3://evidence/${selectedFile.name}` : undefined),
|
|
||||||
description: description.trim(),
|
|
||||||
};
|
|
||||||
await api.evidence.createEvidenceFile(jsonPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSuccessMessage('Evidence successfully uploaded and linked to activity!');
|
setSuccessMessage('Evidence successfully uploaded and linked to activity!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onEvidenceUploaded();
|
onEvidenceUploaded();
|
||||||
onClose();
|
onClose();
|
||||||
}, 1000);
|
}, 800);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[Evidence Upload Error]:', err);
|
console.error('[Evidence Upload Error]:', err);
|
||||||
const errors = extractApiErrors(err);
|
const errors = extractApiErrors(err);
|
||||||
setErrorMessages(errors);
|
setErrorMessages(errors.length > 0 ? errors : ['Failed to upload evidence file. Please try again.']);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -124,6 +118,7 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
<h3 className="text-base font-bold text-white">Upload Activity Deliverable</h3>
|
<h3 className="text-base font-bold text-white">Upload Activity Deliverable</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-1.5 rounded-xl hover:bg-white/10 text-slate-400 hover:text-white transition-colors cursor-pointer"
|
className="p-1.5 rounded-xl hover:bg-white/10 text-slate-400 hover:text-white transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
@@ -146,12 +141,12 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Alerts (Comma-delimited error list from ProblemDetails) */}
|
{/* Error Alerts */}
|
||||||
{errorMessages.length > 0 && (
|
{errorMessages.length > 0 && (
|
||||||
<div className="mb-4 p-3.5 rounded-2xl bg-rose-950/60 border border-rose-500/30 text-rose-200 text-xs space-y-1">
|
<div className="mb-4 p-3.5 rounded-2xl bg-rose-950/60 border border-rose-500/30 text-rose-200 text-xs space-y-1">
|
||||||
<div className="flex items-center gap-1.5 font-bold text-rose-300">
|
<div className="flex items-center gap-1.5 font-bold text-rose-300">
|
||||||
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
|
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
|
||||||
<span>Validation Notice ({errorMessages.length} issue{errorMessages.length > 1 ? 's' : ''}):</span>
|
<span>Upload Notice ({errorMessages.length} issue{errorMessages.length > 1 ? 's' : ''}):</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="list-disc pl-5 space-y-0.5 text-[11px] text-rose-200/90 font-mono">
|
<ul className="list-disc pl-5 space-y-0.5 text-[11px] text-rose-200/90 font-mono">
|
||||||
{errorMessages.map((msg, idx) => (
|
{errorMessages.map((msg, idx) => (
|
||||||
@@ -173,10 +168,13 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
<form onSubmit={handleSubmit} className="space-y-4 text-xs">
|
<form onSubmit={handleSubmit} className="space-y-4 text-xs">
|
||||||
{/* File Dropzone */}
|
{/* File Dropzone */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-300 font-medium mb-1.5">Evidence File (Document, Receipt, Report)</label>
|
<label className="block text-slate-300 font-medium mb-1.5">
|
||||||
|
Evidence File (Document, Invoice, Certificate, Photo) <span className="text-emerald-400">*</span>
|
||||||
|
</label>
|
||||||
<div className="relative border-2 border-dashed border-white/15 hover:border-emerald-500/50 rounded-2xl p-4 text-center bg-white/[0.02] hover:bg-emerald-500/[0.02] transition-colors cursor-pointer">
|
<div className="relative border-2 border-dashed border-white/15 hover:border-emerald-500/50 rounded-2xl p-4 text-center bg-white/[0.02] hover:bg-emerald-500/[0.02] transition-colors cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
required
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
@@ -193,8 +191,8 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-slate-300">Click to upload file</span>
|
<span className="font-medium text-slate-300">Click to choose document</span>
|
||||||
<p className="text-[11px] text-slate-500 mt-0.5">PDF, DOCX, XLSX, PNG, JPG (Max 25MB)</p>
|
<p className="text-[11px] text-slate-500 mt-0.5">PDF, DOCX, XLSX, PNG, JPG (Multiple uploads supported)</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +205,7 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
<label className="block text-slate-300 font-medium mb-1">Evidence Title</label>
|
<label className="block text-slate-300 font-medium mb-1">Evidence Title</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. Audit Signoff Dossier"
|
placeholder="e.g. Activity Verification Proof"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-xl bg-slate-950/70 border border-white/10 text-slate-200 placeholder:text-slate-500 focus:outline-none focus:border-emerald-500/50"
|
className="w-full px-3 py-2 rounded-xl bg-slate-950/70 border border-white/10 text-slate-200 placeholder:text-slate-500 focus:outline-none focus:border-emerald-500/50"
|
||||||
@@ -217,37 +215,21 @@ export const EvidenceUploadModal: React.FC<EvidenceUploadModalProps> = ({
|
|||||||
<label className="block text-slate-300 font-medium mb-1">Evidence Type</label>
|
<label className="block text-slate-300 font-medium mb-1">Evidence Type</label>
|
||||||
<select
|
<select
|
||||||
value={evidenceType}
|
value={evidenceType}
|
||||||
onChange={(e) => setEvidenceType(Number(e.target.value) as EvidenceType)}
|
onChange={(e) => setEvidenceType(Number(e.target.value))}
|
||||||
className="w-full px-3 py-2 rounded-xl bg-slate-950/70 border border-white/10 text-slate-200 focus:outline-none focus:border-emerald-500/50 cursor-pointer"
|
className="w-full px-3 py-2 rounded-xl bg-slate-950/70 border border-white/10 text-slate-200 focus:outline-none focus:border-emerald-500/50 cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value={0}>Document</option>
|
{EVIDENCE_TYPE_OPTIONS.map((opt) => (
|
||||||
<option value={1}>Invoice</option>
|
<option key={opt.value} value={opt.value} className="bg-slate-900 text-slate-100">
|
||||||
<option value={2}>Receipt</option>
|
{opt.label}
|
||||||
<option value={3}>Site Photo</option>
|
</option>
|
||||||
<option value={4}>Verification Report</option>
|
))}
|
||||||
<option value={5}>Other Deliverable</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* External Secure URL (Optional) */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-slate-300 font-medium mb-1">Document Link / S3 URI (Optional)</label>
|
|
||||||
<div className="relative">
|
|
||||||
<LinkIcon className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="https://... or s3://..."
|
|
||||||
value={documentUrl}
|
|
||||||
onChange={(e) => setDocumentUrl(e.target.value)}
|
|
||||||
className="w-full pl-8 pr-3 py-2 rounded-xl bg-slate-950/70 border border-white/10 text-slate-200 placeholder:text-slate-500 focus:outline-none focus:border-emerald-500/50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description / Notes */}
|
{/* Description / Notes */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-300 font-medium mb-1">Deliverable Notes</label>
|
<label className="block text-slate-300 font-medium mb-1">Deliverable Notes (Optional)</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="Provide verification notes or audit references..."
|
placeholder="Provide verification notes or audit references..."
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -810,6 +810,7 @@ export const updateActivity = async (payload: UpdateActivityRequest): Promise<Ac
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
|
activityId: toIntegerId(payload.activityId),
|
||||||
projectId: toIntegerId(payload.projectId),
|
projectId: toIntegerId(payload.projectId),
|
||||||
milestoneId: toNullableIntegerId(payload.milestoneId),
|
milestoneId: toNullableIntegerId(payload.milestoneId),
|
||||||
category: payload.category || 'Operations',
|
category: payload.category || 'Operations',
|
||||||
@@ -830,6 +831,46 @@ export const updateActivity = async (payload: UpdateActivityRequest): Promise<Ac
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const submitActivityForApproval = async (
|
||||||
|
activity: Activity,
|
||||||
|
projectId: number | string,
|
||||||
|
evidences?: Evidence[]
|
||||||
|
): Promise<void> => {
|
||||||
|
if (evidences && evidences.length > 0) {
|
||||||
|
for (const ev of evidences) {
|
||||||
|
if (Number(ev.status) === ApprovalStatus.Pending) {
|
||||||
|
try {
|
||||||
|
await updateEvidence({
|
||||||
|
evidenceId: ev.id,
|
||||||
|
version: ev.version || 1,
|
||||||
|
evidenceType: (ev as any).evidenceType ?? ev.type,
|
||||||
|
status: ApprovalStatus.Review,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not update evidence status', ev.id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateActivity({
|
||||||
|
activityId: activity.id,
|
||||||
|
projectId,
|
||||||
|
milestoneId: activity.milestoneId,
|
||||||
|
category: activity.category || 'Operations',
|
||||||
|
amount: activity.amount || 0,
|
||||||
|
title: activity.title || 'Activity',
|
||||||
|
description: activity.description || '',
|
||||||
|
performedAt: new Date().toISOString(),
|
||||||
|
status: ApprovalStatus.Review,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not update activity fulfillment record', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getMilestones = async (projectId: string | number): Promise<Milestone[]> => {
|
export const getMilestones = async (projectId: string | number): Promise<Milestone[]> => {
|
||||||
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user