293 lines
9.2 KiB
TypeScript
293 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
FileText,
|
|
Image,
|
|
File,
|
|
Trash2,
|
|
Download,
|
|
Loader2,
|
|
FileSpreadsheet,
|
|
FileArchive,
|
|
FileVideo,
|
|
FileAudio,
|
|
FilePlus
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { useSession } from 'next-auth/react';
|
|
import { toast } from '@/components/ui/use-toast';
|
|
import { FileUpload } from './file-upload';
|
|
|
|
interface Attachment {
|
|
id: string;
|
|
filename: string;
|
|
filePath: string;
|
|
fileType: string;
|
|
fileSize: number;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface AttachmentsListProps {
|
|
missionId: string;
|
|
initialAttachments?: Attachment[];
|
|
allowUpload?: boolean;
|
|
allowDelete?: boolean;
|
|
onAttachmentAdded?: (attachment: Attachment) => void;
|
|
onAttachmentDeleted?: (attachmentId: string) => void;
|
|
}
|
|
|
|
export function AttachmentsList({
|
|
missionId,
|
|
initialAttachments = [],
|
|
allowUpload = true,
|
|
allowDelete = true,
|
|
onAttachmentAdded,
|
|
onAttachmentDeleted
|
|
}: AttachmentsListProps) {
|
|
const { data: session } = useSession();
|
|
const [attachments, setAttachments] = useState<Attachment[]>(initialAttachments);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [deleteAttachment, setDeleteAttachment] = useState<Attachment | null>(null);
|
|
const [showUpload, setShowUpload] = useState(false);
|
|
|
|
// Fetch attachments for the mission if not provided initially
|
|
useEffect(() => {
|
|
if (initialAttachments.length === 0) {
|
|
fetchAttachments();
|
|
}
|
|
}, [missionId]);
|
|
|
|
const fetchAttachments = async () => {
|
|
if (!missionId || !session?.user?.id) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/missions/${missionId}/attachments`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch attachments');
|
|
}
|
|
|
|
const data = await response.json();
|
|
setAttachments(data);
|
|
} catch (error) {
|
|
console.error('Error fetching attachments:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to load attachments',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAttachmentUploaded = (data: any) => {
|
|
if (data?.attachment) {
|
|
setAttachments(prev => [...prev, data.attachment]);
|
|
setShowUpload(false);
|
|
|
|
if (onAttachmentAdded) {
|
|
onAttachmentAdded(data.attachment);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleAttachmentDelete = async () => {
|
|
if (!deleteAttachment) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/missions/${missionId}/attachments/${deleteAttachment.id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to delete attachment');
|
|
}
|
|
|
|
setAttachments(prev => prev.filter(a => a.id !== deleteAttachment.id));
|
|
|
|
if (onAttachmentDeleted) {
|
|
onAttachmentDeleted(deleteAttachment.id);
|
|
}
|
|
|
|
toast({
|
|
title: 'Success',
|
|
description: 'Attachment deleted successfully',
|
|
variant: 'default',
|
|
});
|
|
} catch (error) {
|
|
console.error('Error deleting attachment:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to delete attachment',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setDeleteAttachment(null);
|
|
}
|
|
};
|
|
|
|
const getFileIcon = (fileType: string) => {
|
|
if (fileType.startsWith('image/')) {
|
|
return <Image className="h-5 w-5 text-blue-500" />;
|
|
} else if (fileType.includes('pdf')) {
|
|
return <FileText className="h-5 w-5 text-red-500" />;
|
|
} else if (fileType.includes('spreadsheet') || fileType.includes('excel') || fileType.includes('csv')) {
|
|
return <FileSpreadsheet className="h-5 w-5 text-green-500" />;
|
|
} else if (fileType.includes('zip') || fileType.includes('compressed')) {
|
|
return <FileArchive className="h-5 w-5 text-purple-500" />;
|
|
} else if (fileType.includes('video')) {
|
|
return <FileVideo className="h-5 w-5 text-pink-500" />;
|
|
} else if (fileType.includes('audio')) {
|
|
return <FileAudio className="h-5 w-5 text-orange-500" />;
|
|
} else {
|
|
return <File className="h-5 w-5 text-gray-500" />;
|
|
}
|
|
};
|
|
|
|
const formatFileSize = (bytes: number): string => {
|
|
if (bytes < 1024) {
|
|
return `${bytes} B`;
|
|
} else if (bytes < 1024 * 1024) {
|
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
} else {
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{(allowUpload && !showUpload) && (
|
|
<div className="flex justify-end">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowUpload(true)}
|
|
className="bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
|
|
>
|
|
<FilePlus className="h-4 w-4 mr-1" />
|
|
Add Attachment
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{showUpload && (
|
|
<div className="mb-6">
|
|
<h4 className="text-sm font-medium mb-2 text-gray-700">Upload New Attachment</h4>
|
|
<FileUpload
|
|
type="attachment"
|
|
missionId={missionId}
|
|
onUploadComplete={handleAttachmentUploaded}
|
|
isNewMission={false}
|
|
/>
|
|
<div className="mt-2 flex justify-end">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowUpload(false)}
|
|
className="text-gray-500"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<div className="flex justify-center items-center p-8">
|
|
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
|
</div>
|
|
) : attachments.length === 0 ? (
|
|
<div className="text-center p-8 bg-gray-50 border border-gray-200 rounded-md">
|
|
<File className="h-10 w-10 text-gray-300 mx-auto mb-2" />
|
|
<p className="text-sm text-gray-500">No attachments yet</p>
|
|
{allowUpload && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowUpload(true)}
|
|
className="mt-4 bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
|
|
>
|
|
<FilePlus className="h-4 w-4 mr-1" />
|
|
Add your first attachment
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="border rounded-md overflow-hidden">
|
|
<ul className="divide-y">
|
|
{attachments.map((attachment) => (
|
|
<li key={attachment.id} className="flex items-center justify-between p-3 hover:bg-gray-50">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="flex-shrink-0">
|
|
{getFileIcon(attachment.fileType)}
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm text-gray-900">{attachment.filename}</p>
|
|
<p className="text-xs text-gray-500">
|
|
{formatFileSize(attachment.fileSize)} • {new Date(attachment.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
asChild
|
|
className="text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
|
>
|
|
<a
|
|
href={`/api/missions/${missionId}/attachments/download/${attachment.id}`}
|
|
download={attachment.filename}
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</a>
|
|
</Button>
|
|
|
|
{allowDelete && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setDeleteAttachment(attachment)}
|
|
className="text-red-500 hover:text-red-700 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
<AlertDialog open={!!deleteAttachment} onOpenChange={(open) => !open && setDeleteAttachment(null)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete Attachment</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete "{deleteAttachment?.filename}"? This action cannot be undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleAttachmentDelete} className="bg-red-600 hover:bg-red-700 text-white">
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|