import React, { useState, useEffect, useRef } from "react"; import { Attachment, attachmentApi } from "@/services/attachmentApi"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { FileUp, Download, Trash2, File, FileText, Image, Film, Music, Archive, FileX, Loader2, Plus, Eye, Info } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "../ui/badge"; import { cn } from "@/lib/utils"; import { Progress } from "@/components/ui/progress"; import AttachmentViewerDialog from "./AttachmentViewerDialog"; import UploadAttachmentDialog, { FileUploadData } from "./UploadAttachmentDialog"; interface TaskAttachmentsSectionProps { taskId: number; entityType?: "Task" | "Issue" | "Project"; } const TaskAttachmentsSection: React.FC = ({ taskId, entityType = "Task" }) => { const [attachments, setAttachments] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); const [downloadingFiles, setDownloadingFiles] = useState>({}); const [downloadProgress, setDownloadProgress] = useState>({}); const [downloadSizes, setDownloadSizes] = useState>({}); const [viewerOpen, setViewerOpen] = useState(false); const [selectedAttachment, setSelectedAttachment] = useState(null); // New states for the upload dialog const [uploadDialogOpen, setUploadDialogOpen] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const desktopFileInputRef = useRef(null); const mobileFileInputRef = useRef(null); useEffect(() => { if (taskId) { fetchAttachments(); } }, [taskId, entityType]); const fetchAttachments = async () => { try { setIsLoading(true); const data = await attachmentApi.getByEntityTypeAndId(entityType, taskId); setAttachments(data); } catch (error) { console.error(`Error fetching attachments for ${entityType}:`, error); toast.error(`Failed to load attachments for ${entityType}`); } finally { setIsLoading(false); } }; // Handle file selection for the upload dialog const handleFileSelect = (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; // Convert FileList to array of FileUploadData const newFiles = Array.from(files).map(file => ({ file, description: "" })); setSelectedFiles(newFiles); setUploadDialogOpen(true); // Reset the file input if (e.target === desktopFileInputRef.current) { desktopFileInputRef.current.value = ""; } else if (e.target === mobileFileInputRef.current) { mobileFileInputRef.current.value = ""; } }; // Update description for a file const updateFileDescription = (index: number, description: string) => { setSelectedFiles(prev => { const updated = [...prev]; updated[index] = { ...updated[index], description }; return updated; }); }; // Handle the actual file upload const handleUploadFiles = async () => { if (selectedFiles.length === 0) return; try { setIsUploading(true); for (const fileData of selectedFiles) { const { file, description } = fileData; const formData = new FormData(); // Add file data formData.append("AttachmentId", "0"); formData.append("FileName", file.name); formData.append("FileType", file.type || getFileExtension(file.name)); formData.append("FilePath", ""); // Server will set this formData.append("EntityType", entityType); formData.append("EntityId", taskId.toString()); formData.append("Description", description); // Add description formData.append("_IFormFile", file); await attachmentApi.upload(formData); } // Refresh the list after upload fetchAttachments(); toast.success("File(s) uploaded successfully"); // Reset state setSelectedFiles([]); setUploadDialogOpen(false); } catch (error) { console.error("Error uploading file:", error); toast.error("Failed to upload file"); } finally { setIsUploading(false); } }; const handleDeleteAttachment = async (attachmentId: number) => { if (window.confirm("Are you sure you want to delete this attachment?")) { try { await attachmentApi.delete(attachmentId); setAttachments(attachments.filter(a => a.attachmentId !== attachmentId)); toast.success("Attachment deleted successfully"); } catch (error) { console.error("Error deleting attachment:", error); toast.error("Failed to delete attachment"); } } }; const handleDownload = async (attachment: Attachment) => { try { // Set this file as downloading and reset progress setDownloadingFiles(prev => ({ ...prev, [attachment.attachmentId]: true })); setDownloadProgress(prev => ({ ...prev, [attachment.attachmentId]: 0 })); setDownloadSizes(prev => ({ ...prev, [attachment.attachmentId]: 0 })); // Show loading toast const toastId = toast.loading(`Downloading ${attachment.fileName}...`, { position: 'bottom-center' }); // Progress tracking callback const onProgress = (progress: number, totalSize: number) => { setDownloadProgress(prev => ({ ...prev, [attachment.attachmentId]: progress })); setDownloadSizes(prev => ({ ...prev, [attachment.attachmentId]: totalSize })); }; // Use the enhanced downloadAndSave method directly with progress callback await attachmentApi.downloadAndSave(attachment.attachmentId, onProgress); // Dismiss the loading toast and show success toast.dismiss(toastId); toast.success(`File downloaded successfully`, { position: 'bottom-center', duration: 2000 }); } catch (error) { console.error('Error downloading file:', error); toast.error('Failed to download attachment', { position: 'bottom-center' }); } finally { // Clear downloading state for this file setDownloadingFiles(prev => { const newState = { ...prev }; delete newState[attachment.attachmentId]; return newState; }); setDownloadProgress(prev => { const newState = { ...prev }; delete newState[attachment.attachmentId]; return newState; }); setDownloadSizes(prev => { const newState = { ...prev }; delete newState[attachment.attachmentId]; return newState; }); } }; const handleViewAttachment = (attachment: Attachment) => { setSelectedAttachment(attachment); setViewerOpen(true); }; // Helper to get file extension from filename const getFileExtension = (filename: string): string => { return filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2); }; // Helper to get file icon based on file type const getFileIcon = (attachment: Attachment) => { const fileType = attachment.fileType.toLowerCase(); if (fileType.includes('image') || fileType.includes('.jpg') || fileType.includes('.jpeg') || fileType.includes('.png') || fileType.includes('.gif') || fileType.includes('.webp')) { return ; } if (fileType.includes('pdf')) { return ; } if (fileType.includes('word') || fileType.includes('.doc') || fileType.includes('.docx')) { return ; } if (fileType.includes('excel') || fileType.includes('.xls') || fileType.includes('.xlsx')) { return ; } if (fileType.includes('video') || fileType.includes('.mp4') || fileType.includes('.mov')) { return ; } if (fileType.includes('audio') || fileType.includes('.mp3') || fileType.includes('.wav')) { return ; } if (fileType.includes('zip') || fileType.includes('.rar') || fileType.includes('.7z')) { return ; } return ; }; // Helper to format file size const formatFileSize = (bytes: number): string => { if (!bytes) return 'Unknown size'; const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; }; // Helper to format file name for display const formatDisplayName = (fileName: string, maxLength: number = 30): string => { if (fileName.length <= maxLength) return fileName; const extension = fileName.slice((fileName.lastIndexOf(".") - 1 >>> 0) + 1); const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf(".")); if (extension.length + 5 >= maxLength) { return `${nameWithoutExt.slice(0, maxLength - 10)}...${extension}`; } const charsToCut = fileName.length - maxLength + 3; return `${nameWithoutExt.slice(0, nameWithoutExt.length - charsToCut)}...${extension}`; }; // Helper to determine a shorter max length for mobile screens const getResponsiveMaxLength = (): number => { if (typeof window !== 'undefined') { return window.innerWidth < 640 ? 15 : 30; } return 30; }; return (
{/* Attachment Viewer Dialog */} { setViewerOpen(false); setSelectedAttachment(null); }} attachment={selectedAttachment} onDownload={handleDownload} /> {/* Custom Upload Dialog */} setUploadDialogOpen(false)} selectedFiles={selectedFiles} onUpdateDescription={updateFileDescription} onUpload={handleUploadFiles} isUploading={isUploading} entityType={entityType} /> {/* Fixed Action Button for Mobile */}
Attachments
Files and documents related to this {entityType.toLowerCase()}
{isLoading ? (
) : attachments.length === 0 ? (

No attachments yet

Tap the + button to upload files

) : (
{attachments.map((attachment) => (
handleViewAttachment(attachment)} >
{downloadingFiles[attachment.attachmentId] ? ( ) : ( getFileIcon(attachment) )}

{formatDisplayName(attachment.fileName, getResponsiveMaxLength())}

{/* Display the description if it exists */} {attachment.description && (

{attachment.description}

)}
{downloadingFiles[attachment.attachmentId] ? "Downloading..." : attachment.fileType || getFileExtension(attachment.fileName)} {!downloadingFiles[attachment.attachmentId] && ( {attachment.createdBy && `Added by ${attachment.createdBy}`} {attachment.createdAt && attachment.createdBy && ' ยท '} {attachment.createdAt && new Date(attachment.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} )} {downloadingFiles[attachment.attachmentId] && (
{downloadProgress[attachment.attachmentId] || 0}% {downloadSizes[attachment.attachmentId] && ( {formatFileSize(downloadSizes[attachment.attachmentId])} )}
)}
))}
)}
{attachments.length} attachment{attachments.length !== 1 ? 's' : ''}
); }; export default TaskAttachmentsSection;