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, Lock } 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 "../tasks/AttachmentViewerDialog"; import { useAuth } from "@/lib/auth-context"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import EmployeeDocumentSelector, { DocumentType } from "./EmployeeDocumentSelector"; // Predefined document types for employee attachments export const EMPLOYEE_DOCUMENT_TYPES = [ { name: "Date of Birth Certificate", description: "Copy of the official certificate proving your date of birth." }, { name: "Educational Certificate – SSC", description: "Copy of your Secondary School Certificate (10th Grade)." }, { name: "Educational Certificate – HSC", description: "Copy of your Higher Secondary Certificate (12th Grade)." }, { name: "Educational Certificate – Degree", description: "Copy of your Graduation/Post-Graduation Degree certificate." }, { name: "Educational Certificate – Mark Sheets", description: "Copies of your semester-wise mark sheets for all degrees." }, { name: "Project Certificate", description: "Certificate for any commercial or academic projects completed as part of your degree course." }, { name: "Previous Employment Certificate", description: "Service certificates from all your previous employers, including employment dates." }, { name: "Relieving Letter", description: "Relieving letter from your current/last employer to confirm your exit." }, { name: "Service Certificate", description: "Service certificate from your current/last employer." }, { name: "Pay Slip / Salary Certificate", description: "Copy of your latest pay slip or salary certificate from your current/last employer." }, { name: "Passport Size Photograph", description: "Two recent passport-size colored photographs." }, { name: "Passport Copy", description: "Photocopy of all relevant passport pages, including personal information, issue and expiry dates, and any visa/work permit/entry permit stamps." }, { name: "Marriage Certificate", description: "If married, a copy of your official marriage certificate." }, { name: "Spouse's Passport Copy", description: "Photocopy of relevant pages of your spouse's passport (if applicable)." }, { name: "Child's Birth Certificate", description: "If applicable, birth certificates of your children." }, { name: "Driving License", description: "Photocopy of your two-wheeler or four-wheeler driving license." }, { name: "Medical Fitness Certificate", description: "Original certificate stating your medical fitness from a certified doctor." }, { name: "Aadhaar Card", description: "Photocopy of your Aadhaar card." }, { name: "PAN Card", description: "Photocopy of your PAN card." }, { name: "Other", description: "Any other documents you wish to provide as part of your verification." } ]; interface EmployeeAttachmentsSectionProps { employeeId: number; canDelete?: boolean; // Optional prop to override delete permission } const EmployeeAttachmentsSection: React.FC = ({ employeeId, canDelete }) => { const { userData } = useAuth(); const [attachments, setAttachments] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isUploading, setIsUploading] = useState(false); const [downloadingFiles, setDownloadingFiles] = useState>({}); const [downloadProgress, setDownloadProgress] = useState>({}); const [downloadSizes, setDownloadSizes] = useState>({}); const [viewerOpen, setViewerOpen] = useState(false); const [selectedAttachment, setSelectedAttachment] = useState(null); // State for document selector dialog const [docSelectorOpen, setDocSelectorOpen] = useState(false); // Function to open the document selector dialog const openDocumentSelector = () => { setDocSelectorOpen(true); }; // Check if user has permission to delete attachments const hasDeletePermission = (): boolean => { if (canDelete !== undefined) return canDelete; return userData?.roleName === 'HR' || userData?.roleName === 'Admin'; }; useEffect(() => { if (employeeId) { fetchAttachments(); } }, [employeeId]); const fetchAttachments = async () => { try { setIsLoading(true); const data = await attachmentApi.getByEntityTypeAndId("Employee", employeeId); setAttachments(data); } catch (error) { console.error(`Error fetching attachments for Employee:`, error); toast.error(`Failed to load employee documents`); } finally { setIsLoading(false); } }; // Handle document selection and upload const handleDocumentUpload = async (docType: DocumentType, file: File, description: string) => { try { setIsUploading(true); 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", "Employee"); formData.append("EntityId", employeeId.toString()); // Set the document type as the description const finalDescription = description ? `${docType.name}: ${description}` : docType.name; formData.append("Description", finalDescription); formData.append("_IFormFile", file); await attachmentApi.upload(formData); // Refresh the list after upload fetchAttachments(); toast.success(`${docType.name} uploaded successfully`); // Close the dialog setDocSelectorOpen(false); } catch (error) { console.error("Error uploading file:", error); toast.error("Failed to upload document"); } finally { setIsUploading(false); } }; const handleDeleteAttachment = async (attachmentId: number) => { if (!hasDeletePermission()) { toast.error("You don't have permission to delete documents"); return; } if (window.confirm("Are you sure you want to delete this document?")) { try { await attachmentApi.delete(attachmentId); setAttachments(attachments.filter(a => a.attachmentId !== attachmentId)); toast.success("Document deleted successfully"); } catch (error) { console.error("Error deleting document:", error); toast.error("Failed to delete document"); } } }; 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 document', { 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; }; // Helper to extract document type from description const parseDocumentType = (description: string): { type: string | null, rest: string } => { if (!description) return { type: null, rest: '' }; // Check if the entire description matches a document type for (const docType of EMPLOYEE_DOCUMENT_TYPES) { if (description === docType.name) { return { type: docType.name, rest: '' }; } } // Check if description starts with a document type followed by a colon for (const docType of EMPLOYEE_DOCUMENT_TYPES) { if (description.startsWith(`${docType.name}:`)) { return { type: docType.name, rest: description.substring(docType.name.length + 1).trim() }; } } return { type: null, rest: description }; }; // Helper to get color for document type badge const getDocumentBadgeColor = (documentType: string): string => { if (documentType.includes('Educational')) return "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800"; if (documentType.includes('Passport')) return "bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-400 dark:border-purple-800"; if (documentType.includes('Certificate')) return "bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800"; if (documentType.includes('Aadhaar') || documentType.includes('PAN')) return "bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-900/20 dark:text-orange-400 dark:border-orange-800"; if (documentType.includes('License')) return "bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800"; if (documentType.includes('Employment') || documentType.includes('Relieving') || documentType.includes('Service')) return "bg-indigo-50 text-indigo-700 border-indigo-200 dark:bg-indigo-900/20 dark:text-indigo-400 dark:border-indigo-800"; if (documentType.includes('Medical')) return "bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800"; if (documentType.includes('Pay') || documentType.includes('Salary')) return "bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-900/20 dark:text-cyan-400 dark:border-cyan-800"; return "bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-800/30 dark:text-gray-400 dark:border-gray-700"; }; // Helper to get shortened document type for display const getShortenedDocumentType = (documentType: string): string => { // Shortened labels for common document types if (documentType.includes('Educational Certificate – ')) { return documentType.replace('Educational Certificate – ', ''); } // Shorten other common document types const shortenedMap: Record = { "Date of Birth Certificate": "DOB Cert", "Passport Size Photograph": "Photo", "Passport Copy": "Passport", "Marriage Certificate": "Marriage Cert", "Spouse's Passport Copy": "Spouse Passport", "Child's Birth Certificate": "Child Birth Cert", "Driving License": "Driver License", "Medical Fitness Certificate": "Medical Cert", "Previous Employment Certificate": "Prev Employment", "Pay Slip / Salary Certificate": "Pay Slip" }; return shortenedMap[documentType] || documentType; }; return (
{/* Attachment Viewer Dialog */} { setViewerOpen(false); setSelectedAttachment(null); }} attachment={selectedAttachment} onDownload={handleDownload} /> {/* Document Type Selector Dialog */} setDocSelectorOpen(false)} documentTypes={EMPLOYEE_DOCUMENT_TYPES} onSelectAndUpload={handleDocumentUpload} isUploading={isUploading} /> {/* Fixed Action Button for Mobile */}
Documents
Official employee documents and files
{isLoading ? (
) : attachments.length === 0 ? (

No documents uploaded yet

Tap the + button to upload documents

) : (
{attachments.map((attachment) => { // Parse the document type from description (if it exists) const { type: documentType, rest: plainDescription } = parseDocumentType(attachment.description || ''); return (
handleViewAttachment(attachment)} >
{downloadingFiles[attachment.attachmentId] ? ( ) : ( getFileIcon(attachment) )}

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

{/* Display the document type as a badge if it exists */}
{documentType && ( {getShortenedDocumentType(documentType)} )} {/* Display the plain description if available */} {plainDescription && (

{plainDescription}

)}
{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])} )}
)}
{hasDeletePermission() ? "Delete document" : "Only HR and Admin can delete documents"}
); })}
)}
{attachments.length} document{attachments.length !== 1 ? 's' : ''}
); }; export default EmployeeAttachmentsSection;