import React, { useState } from 'react'; import { api, extractApiErrors } from '../../services/api'; import { Activity, Milestone, Project, EvidenceType } from '../../services/api/models'; import { X, UploadCloud, FileText, CheckCircle2, AlertTriangle, Loader2, Plus, Sparkles, Link as LinkIcon } from 'lucide-react'; interface EvidenceUploadModalProps { isOpen: boolean; onClose: () => void; project: Project; milestone?: Milestone; activity: Activity; onEvidenceUploaded: () => void; } export const EvidenceUploadModal: React.FC = ({ isOpen, onClose, project, milestone, activity, onEvidenceUploaded, }) => { const [selectedFile, setSelectedFile] = useState(null); const [title, setTitle] = useState(''); const [documentUrl, setDocumentUrl] = useState(''); const [evidenceType, setEvidenceType] = useState(1); // Invoice / Document const [description, setDescription] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessages, setErrorMessages] = useState([]); const [successMessage, setSuccessMessage] = useState(null); if (!isOpen) return null; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; setSelectedFile(file); if (!title) { setTitle(file.name.replace(/\.[^/.]+$/, '')); } } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrorMessages([]); setSuccessMessage(null); if (!title.trim() && !selectedFile && !documentUrl.trim()) { setErrorMessages(['Please provide an evidence title, file upload, or document URL.']); return; } setIsSubmitting(true); try { // Build upload payload const formData = new FormData(); if (selectedFile) { formData.append('file', selectedFile); } 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 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!'); setTimeout(() => { onEvidenceUploaded(); onClose(); }, 1000); } catch (err: any) { console.error('[Evidence Upload Error]:', err); const errors = extractApiErrors(err); setErrorMessages(errors); } finally { setIsSubmitting(false); } }; return (
{/* Ambient background glow */}
{/* Modal Header */}
Activity Evidence Submission

Upload Activity Deliverable

{/* Context Summary */}
Project: {project.name || `Project #${project.id}`}
{milestone?.name && (
Milestone: {milestone.name}
)}
Activity: {activity.title || `Activity #${activity.id}`}
{/* Error Alerts (Comma-delimited error list from ProblemDetails) */} {errorMessages.length > 0 && (
Validation Notice ({errorMessages.length} issue{errorMessages.length > 1 ? 's' : ''}):
    {errorMessages.map((msg, idx) => (
  • {msg}
  • ))}
)} {/* Success Alert */} {successMessage && (
{successMessage}
)} {/* Form */}
{/* File Dropzone */}
{selectedFile ? (
{selectedFile.name}
{(selectedFile.size / 1024).toFixed(1)} KB
) : (
Click to upload file

PDF, DOCX, XLSX, PNG, JPG (Max 25MB)

)}
{/* Evidence Title & Type */}
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" />
{/* External Secure URL (Optional) */}
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" />
{/* Description / Notes */}