Spaces:
Sleeping
Sleeping
| 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<EmployeeAttachmentsSectionProps> = ({ | |
| employeeId, | |
| canDelete | |
| }) => { | |
| const { userData } = useAuth(); | |
| const [attachments, setAttachments] = useState<Attachment[]>([]); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [isUploading, setIsUploading] = useState(false); | |
| const [downloadingFiles, setDownloadingFiles] = useState<Record<number, boolean>>({}); | |
| const [downloadProgress, setDownloadProgress] = useState<Record<number, number>>({}); | |
| const [downloadSizes, setDownloadSizes] = useState<Record<number, number>>({}); | |
| const [viewerOpen, setViewerOpen] = useState(false); | |
| const [selectedAttachment, setSelectedAttachment] = useState<Attachment | null>(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 <Image className="h-5 w-5 text-purple-500" />; | |
| } | |
| if (fileType.includes('pdf')) { | |
| return <FileText className="h-5 w-5 text-red-500" />; | |
| } | |
| if (fileType.includes('word') || fileType.includes('.doc') || fileType.includes('.docx')) { | |
| return <FileText className="h-5 w-5 text-blue-500" />; | |
| } | |
| if (fileType.includes('excel') || fileType.includes('.xls') || fileType.includes('.xlsx')) { | |
| return <FileText className="h-5 w-5 text-green-500" />; | |
| } | |
| if (fileType.includes('video') || fileType.includes('.mp4') || fileType.includes('.mov')) { | |
| return <Film className="h-5 w-5 text-orange-500" />; | |
| } | |
| if (fileType.includes('audio') || fileType.includes('.mp3') || fileType.includes('.wav')) { | |
| return <Music className="h-5 w-5 text-blue-500" />; | |
| } | |
| if (fileType.includes('zip') || fileType.includes('.rar') || fileType.includes('.7z')) { | |
| return <Archive className="h-5 w-5 text-yellow-500" />; | |
| } | |
| return <File className="h-5 w-5 text-gray-500" />; | |
| }; | |
| // 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<string, string> = { | |
| "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 ( | |
| <div className="w-full"> | |
| {/* Attachment Viewer Dialog */} | |
| <AttachmentViewerDialog | |
| isOpen={viewerOpen} | |
| onClose={() => { | |
| setViewerOpen(false); | |
| setSelectedAttachment(null); | |
| }} | |
| attachment={selectedAttachment} | |
| onDownload={handleDownload} | |
| /> | |
| {/* Document Type Selector Dialog */} | |
| <EmployeeDocumentSelector | |
| isOpen={docSelectorOpen} | |
| onClose={() => setDocSelectorOpen(false)} | |
| documentTypes={EMPLOYEE_DOCUMENT_TYPES} | |
| onSelectAndUpload={handleDocumentUpload} | |
| isUploading={isUploading} | |
| /> | |
| {/* Fixed Action Button for Mobile */} | |
| <div className="sm:hidden fixed bottom-6 right-6 z-10"> | |
| <Button | |
| onClick={openDocumentSelector} | |
| className="h-14 w-14 rounded-full bg-primary text-primary-foreground flex items-center justify-center shadow-lg" | |
| disabled={isUploading} | |
| > | |
| {isUploading ? <Loader2 className="h-6 w-6 animate-spin" /> : <Plus className="h-6 w-6" />} | |
| </Button> | |
| </div> | |
| <Card className="shadow-sm border"> | |
| <CardHeader className="pb-3"> | |
| <div className="flex justify-between items-center"> | |
| <CardTitle className="text-xl flex items-center gap-2"> | |
| <FileText className="h-5 w-5" /> | |
| Documents | |
| </CardTitle> | |
| <div className="hidden sm:block"> | |
| <Button | |
| onClick={openDocumentSelector} | |
| variant="outline" | |
| size="sm" | |
| disabled={isUploading} | |
| className="gap-2" | |
| > | |
| {isUploading ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| <span>Uploading...</span> | |
| </> | |
| ) : ( | |
| <> | |
| <FileUp className="h-4 w-4" /> | |
| <span>Upload Document</span> | |
| </> | |
| )} | |
| </Button> | |
| </div> | |
| </div> | |
| <CardDescription> | |
| Official employee documents and files | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| {isLoading ? ( | |
| <div className="flex justify-center items-center py-8"> | |
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> | |
| </div> | |
| ) : attachments.length === 0 ? ( | |
| <div className="text-center py-8 border border-dashed rounded-lg flex flex-col items-center"> | |
| <FileX className="h-10 w-10 text-muted-foreground mb-2" /> | |
| <p className="text-sm text-muted-foreground mb-2">No documents uploaded yet</p> | |
| <Button | |
| onClick={openDocumentSelector} | |
| variant="default" | |
| className="sm:block hidden" | |
| disabled={isUploading} | |
| > | |
| Upload Documents | |
| </Button> | |
| <p className="text-xs text-muted-foreground mt-2 sm:hidden"> | |
| Tap the + button to upload documents | |
| </p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4"> | |
| <div className="grid gap-3"> | |
| {attachments.map((attachment) => { | |
| // Parse the document type from description (if it exists) | |
| const { type: documentType, rest: plainDescription } = parseDocumentType(attachment.description || ''); | |
| return ( | |
| <div | |
| key={attachment.attachmentId} | |
| className={cn( | |
| "flex items-center justify-between border rounded-lg p-3 bg-card transition-colors", | |
| downloadingFiles[attachment.attachmentId] | |
| ? "bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800" | |
| : "hover:bg-accent/50" | |
| )} | |
| > | |
| <div | |
| className="flex items-center gap-2 sm:gap-3 overflow-hidden flex-1 min-w-0 cursor-pointer" | |
| onClick={() => handleViewAttachment(attachment)} | |
| > | |
| <div className={cn( | |
| "p-2 rounded-md flex-shrink-0", | |
| downloadingFiles[attachment.attachmentId] | |
| ? "bg-blue-100 dark:bg-blue-800/30" | |
| : "bg-accent" | |
| )}> | |
| {downloadingFiles[attachment.attachmentId] ? ( | |
| <Loader2 className="h-5 w-5 text-blue-600 dark:text-blue-400 animate-spin" /> | |
| ) : ( | |
| getFileIcon(attachment) | |
| )} | |
| </div> | |
| <div className="min-w-0 flex-1"> | |
| <p className="font-medium text-sm truncate" title={attachment.fileName}> | |
| {formatDisplayName(attachment.fileName, getResponsiveMaxLength())} | |
| </p> | |
| {/* Display the document type as a badge if it exists */} | |
| <div className="flex items-center gap-2 flex-wrap mt-1"> | |
| {documentType && ( | |
| <Badge | |
| variant="outline" | |
| className={cn( | |
| "px-2 py-0.5 text-xs font-medium", | |
| getDocumentBadgeColor(documentType) | |
| )} | |
| > | |
| {getShortenedDocumentType(documentType)} | |
| </Badge> | |
| )} | |
| {/* Display the plain description if available */} | |
| {plainDescription && ( | |
| <p className="text-xs text-muted-foreground truncate" title={plainDescription}> | |
| <Info className="h-3 w-3 inline mr-1" /> | |
| {plainDescription} | |
| </p> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2 mt-1"> | |
| <Badge variant="secondary" className={cn( | |
| "px-1.5 py-0 h-5 text-xs", | |
| downloadingFiles[attachment.attachmentId] | |
| ? "bg-blue-100 dark:bg-blue-800/30 text-blue-600 dark:text-blue-400" | |
| : "bg-accent" | |
| )}> | |
| {downloadingFiles[attachment.attachmentId] ? "Downloading..." : attachment.fileType || getFileExtension(attachment.fileName)} | |
| </Badge> | |
| {!downloadingFiles[attachment.attachmentId] && ( | |
| <span className="text-xs text-muted-foreground"> | |
| {attachment.createdBy && `Added by ${attachment.createdBy}`} | |
| {attachment.createdAt && attachment.createdBy && ' · '} | |
| {attachment.createdAt && new Date(attachment.createdAt).toLocaleDateString(undefined, { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric' | |
| })} | |
| </span> | |
| )} | |
| {downloadingFiles[attachment.attachmentId] && ( | |
| <div className="mt-2 w-full"> | |
| <Progress | |
| value={downloadProgress[attachment.attachmentId] || 0} | |
| className="h-1.5 bg-blue-100 dark:bg-blue-900/30" | |
| /> | |
| <div className="flex justify-between text-xs text-muted-foreground mt-1"> | |
| <span>{downloadProgress[attachment.attachmentId] || 0}%</span> | |
| {downloadSizes[attachment.attachmentId] && ( | |
| <span>{formatFileSize(downloadSizes[attachment.attachmentId])}</span> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleViewAttachment(attachment); | |
| }} | |
| title="View document" | |
| className="h-8 w-8 text-blue-500 hover:text-blue-600 hover:bg-blue-50" | |
| disabled={downloadingFiles[attachment.attachmentId]} | |
| > | |
| <Eye className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleDownload(attachment); | |
| }} | |
| title="Download document" | |
| className="h-8 w-8 text-blue-500 hover:text-blue-600 hover:bg-blue-50" | |
| disabled={downloadingFiles[attachment.attachmentId]} | |
| > | |
| {downloadingFiles[attachment.attachmentId] ? ( | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| ) : ( | |
| <Download className="h-4 w-4" /> | |
| )} | |
| </Button> | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <div> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleDeleteAttachment(attachment.attachmentId); | |
| }} | |
| title={hasDeletePermission() ? "Delete document" : "You don't have permission to delete"} | |
| className={cn( | |
| "h-8 w-8", | |
| hasDeletePermission() | |
| ? "text-red-500 hover:text-red-600 hover:bg-red-50" | |
| : "text-muted-foreground cursor-not-allowed" | |
| )} | |
| disabled={downloadingFiles[attachment.attachmentId] || !hasDeletePermission()} | |
| > | |
| {hasDeletePermission() ? ( | |
| <Trash2 className="h-4 w-4" /> | |
| ) : ( | |
| <Lock className="h-4 w-4" /> | |
| )} | |
| </Button> | |
| </div> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| {hasDeletePermission() ? "Delete document" : "Only HR and Admin can delete documents"} | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| <CardFooter className="border-t pt-4 flex justify-between"> | |
| <div className="text-sm text-muted-foreground"> | |
| {attachments.length} document{attachments.length !== 1 ? 's' : ''} | |
| </div> | |
| <Button | |
| onClick={openDocumentSelector} | |
| variant="outline" | |
| size="sm" | |
| className="hidden sm:flex items-center gap-2" | |
| disabled={isUploading} | |
| > | |
| {isUploading ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| <span>Uploading...</span> | |
| </> | |
| ) : ( | |
| <> | |
| <FileUp className="h-4 w-4" /> | |
| <span>Upload Document</span> | |
| </> | |
| )} | |
| </Button> | |
| </CardFooter> | |
| </Card> | |
| </div> | |
| ); | |
| }; | |
| export default EmployeeAttachmentsSection; |