342 lines
18 KiB
TypeScript
342 lines
18 KiB
TypeScript
|
||
import React, { useEffect, useState } from 'react';
|
||
import { CondoService } from '../services/mockDb';
|
||
import { Document, Condo } from '../types';
|
||
import { FileText, Download, Eye, Upload, Tag, Search, Trash2, X, Plus, Filter, Image as ImageIcon, File, Type, AlignLeft } from 'lucide-react';
|
||
|
||
export const DocumentsPage: React.FC = () => {
|
||
const user = CondoService.getCurrentUser();
|
||
const isPrivileged = user?.role === 'admin' || user?.role === 'poweruser';
|
||
|
||
const [documents, setDocuments] = useState<Document[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [activeCondo, setActiveCondo] = useState<Condo | undefined>(undefined);
|
||
|
||
// Filtering
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||
const [allTags, setAllTags] = useState<string[]>([]);
|
||
|
||
// Upload Modal
|
||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||
const [uploadForm, setUploadForm] = useState<{
|
||
title: string;
|
||
description: string;
|
||
tags: string[];
|
||
currentTagInput: string;
|
||
file: File | null;
|
||
}>({ title: '', description: '', tags: [], currentTagInput: '', file: null });
|
||
const [isUploading, setIsUploading] = useState(false);
|
||
|
||
// Preview Modal
|
||
const [previewDoc, setPreviewDoc] = useState<{doc: Document, data: string} | null>(null);
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
}, []);
|
||
|
||
const loadData = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const condo = await CondoService.getActiveCondo();
|
||
setActiveCondo(condo);
|
||
// Verify method exists before calling to avoid crash if service is outdated
|
||
if (typeof CondoService.getDocuments === 'function') {
|
||
const docs = await CondoService.getDocuments();
|
||
setDocuments(docs);
|
||
|
||
// Extract unique tags
|
||
const tags = new Set<string>();
|
||
docs.forEach(d => d.tags.forEach(t => tags.add(t)));
|
||
setAllTags(Array.from(tags).sort());
|
||
} else {
|
||
console.error("CondoService.getDocuments is missing");
|
||
}
|
||
} catch(e) { console.error(e); }
|
||
finally { setLoading(false); }
|
||
};
|
||
|
||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.files && e.target.files.length > 0) {
|
||
setUploadForm({ ...uploadForm, file: e.target.files[0] });
|
||
}
|
||
};
|
||
|
||
const addTag = () => {
|
||
if (uploadForm.currentTagInput.trim() && !uploadForm.tags.includes(uploadForm.currentTagInput.trim())) {
|
||
setUploadForm({
|
||
...uploadForm,
|
||
tags: [...uploadForm.tags, uploadForm.currentTagInput.trim()],
|
||
currentTagInput: ''
|
||
});
|
||
}
|
||
};
|
||
|
||
const removeTag = (tagToRemove: string) => {
|
||
setUploadForm({
|
||
...uploadForm,
|
||
tags: uploadForm.tags.filter(t => t !== tagToRemove)
|
||
});
|
||
};
|
||
|
||
const handleUploadSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!uploadForm.file) return;
|
||
|
||
setIsUploading(true);
|
||
try {
|
||
// Convert FileReader to Promise to handle errors in the main try/catch block
|
||
const base64 = await new Promise<string>((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.readAsDataURL(uploadForm.file!);
|
||
reader.onload = () => resolve(reader.result as string);
|
||
reader.onerror = error => reject(error);
|
||
});
|
||
|
||
if (typeof CondoService.uploadDocument !== 'function') {
|
||
throw new Error("Il metodo uploadDocument non è disponibile nel servizio.");
|
||
}
|
||
|
||
await CondoService.uploadDocument({
|
||
title: uploadForm.title,
|
||
description: uploadForm.description,
|
||
fileName: uploadForm.file.name,
|
||
fileType: uploadForm.file.type,
|
||
fileSize: uploadForm.file.size,
|
||
tags: uploadForm.tags,
|
||
fileData: base64
|
||
});
|
||
|
||
setIsUploading(false);
|
||
setShowUploadModal(false);
|
||
setUploadForm({ title: '', description: '', tags: [], currentTagInput: '', file: null });
|
||
loadData();
|
||
} catch(e) {
|
||
console.error("Upload error:", e);
|
||
alert("Errore durante l'upload. Riprova o contatta l'assistenza.");
|
||
setIsUploading(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (id: string) => {
|
||
if (!confirm("Eliminare questo documento?")) return;
|
||
try {
|
||
await CondoService.deleteDocument(id);
|
||
loadData();
|
||
} catch(e) { alert("Errore eliminazione"); }
|
||
};
|
||
|
||
const handlePreview = async (doc: Document) => {
|
||
try {
|
||
const data = await CondoService.getDocumentDownload(doc.id);
|
||
if (doc.fileType === 'application/pdf' || doc.fileType.startsWith('image/')) {
|
||
setPreviewDoc({ doc, data: data.data });
|
||
} else {
|
||
// Force download
|
||
const link = document.createElement("a");
|
||
link.href = data.data;
|
||
link.download = data.fileName;
|
||
link.click();
|
||
}
|
||
} catch(e) { alert("Errore apertura file"); }
|
||
};
|
||
|
||
const filteredDocs = documents.filter(d => {
|
||
const matchesSearch = d.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
(d.description && d.description.toLowerCase().includes(searchTerm.toLowerCase()));
|
||
const matchesTags = selectedTags.length === 0 || selectedTags.every(t => d.tags.includes(t));
|
||
return matchesSearch && matchesTags;
|
||
});
|
||
|
||
const toggleFilterTag = (tag: string) => {
|
||
if (selectedTags.includes(tag)) {
|
||
setSelectedTags(selectedTags.filter(t => t !== tag));
|
||
} else {
|
||
setSelectedTags([...selectedTags, tag]);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col md:flex-row h-[calc(100vh-100px)] gap-6 animate-fade-in">
|
||
{/* Sidebar Filters */}
|
||
<div className="w-full md:w-64 flex-shrink-0 space-y-6">
|
||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||
<h3 className="font-bold text-slate-800 mb-4 flex items-center gap-2">
|
||
<Filter className="w-4 h-4"/> Filtra per Tag
|
||
</h3>
|
||
<div className="flex flex-wrap gap-2">
|
||
{allTags.length === 0 && <p className="text-xs text-slate-400">Nessun tag disponibile.</p>}
|
||
{allTags.map(tag => (
|
||
<button
|
||
key={tag}
|
||
onClick={() => toggleFilterTag(tag)}
|
||
className={`px-3 py-1 rounded-full text-xs font-medium transition-all border ${
|
||
selectedTags.includes(tag)
|
||
? 'bg-blue-100 text-blue-700 border-blue-200'
|
||
: 'bg-slate-50 text-slate-600 border-slate-200 hover:bg-slate-100'
|
||
}`}
|
||
>
|
||
{tag}
|
||
</button>
|
||
))}
|
||
</div>
|
||
{selectedTags.length > 0 && (
|
||
<button onClick={() => setSelectedTags([])} className="mt-4 text-xs text-red-500 hover:underline w-full text-center">
|
||
Rimuovi filtri
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Content */}
|
||
<div className="flex-1 flex flex-col min-h-0">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-slate-800">Documenti</h2>
|
||
<p className="text-slate-500 text-sm">Archivio digitale {activeCondo?.name}</p>
|
||
</div>
|
||
{isPrivileged && (
|
||
<button onClick={() => setShowUploadModal(true)} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 hover:bg-blue-700 shadow-sm">
|
||
<Upload className="w-4 h-4"/> Carica Documento
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Search Bar */}
|
||
<div className="mb-6 relative">
|
||
<input
|
||
type="text"
|
||
placeholder="Cerca documento..."
|
||
value={searchTerm}
|
||
onChange={e => setSearchTerm(e.target.value)}
|
||
className="w-full pl-10 pr-4 py-3 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm"
|
||
/>
|
||
<Search className="w-5 h-5 text-slate-400 absolute left-3 top-3.5"/>
|
||
</div>
|
||
|
||
{/* Grid */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 overflow-y-auto pb-20">
|
||
{filteredDocs.length === 0 ? (
|
||
<div className="col-span-full text-center p-12 text-slate-400 border border-dashed rounded-xl">
|
||
{loading ? "Caricamento..." : "Nessun documento trovato."}
|
||
</div>
|
||
) : (
|
||
filteredDocs.map(doc => (
|
||
<div key={doc.id} className="bg-white rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow flex flex-col group">
|
||
<div className="p-4 flex items-start gap-3">
|
||
<div className="p-3 bg-slate-100 rounded-lg flex-shrink-0">
|
||
{doc.fileType === 'application/pdf' ? <FileText className="w-6 h-6 text-red-500"/> :
|
||
doc.fileType.startsWith('image/') ? <ImageIcon className="w-6 h-6 text-blue-500"/> :
|
||
<File className="w-6 h-6 text-slate-500"/>}
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<h4 className="font-bold text-slate-800 truncate" title={doc.title}>{doc.title}</h4>
|
||
<p className="text-xs text-slate-500 mt-0.5">{new Date(doc.uploadDate).toLocaleDateString()} • {(doc.fileSize / 1024 / 1024).toFixed(2)} MB</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-4 pb-2">
|
||
<div className="flex flex-wrap gap-1 mb-2">
|
||
{doc.tags.map(t => (
|
||
<span key={t} className="text-[10px] px-2 py-0.5 bg-slate-100 text-slate-600 rounded-full font-medium">
|
||
{t}
|
||
</span>
|
||
))}
|
||
</div>
|
||
{doc.description && <p className="text-sm text-slate-600 line-clamp-2 mb-3 h-10">{doc.description}</p>}
|
||
</div>
|
||
|
||
<div className="mt-auto border-t border-slate-100 p-2 flex justify-between bg-slate-50 rounded-b-xl">
|
||
<button onClick={() => handlePreview(doc)} className="flex-1 flex items-center justify-center gap-1 text-xs font-medium text-slate-600 hover:bg-white hover:text-blue-600 py-1.5 rounded transition-colors">
|
||
{doc.fileType === 'application/pdf' || doc.fileType.startsWith('image/') ? <><Eye className="w-3 h-3"/> Vedi</> : <><Download className="w-3 h-3"/> Scarica</>}
|
||
</button>
|
||
{isPrivileged && (
|
||
<button onClick={() => handleDelete(doc.id)} className="px-3 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors">
|
||
<Trash2 className="w-3 h-3"/>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Upload Modal */}
|
||
{showUploadModal && (
|
||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 animate-in fade-in zoom-in duration-200">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h3 className="font-bold text-lg text-slate-800">Carica Documento</h3>
|
||
<button onClick={() => setShowUploadModal(false)}><X className="w-6 h-6 text-slate-400"/></button>
|
||
</div>
|
||
<form onSubmit={handleUploadSubmit} className="space-y-4">
|
||
<div className="relative">
|
||
<Type className="absolute left-3 top-2.5 w-5 h-5 text-slate-400" />
|
||
<input className="w-full border p-2.5 pl-10 rounded-lg" placeholder="Titolo Documento" value={uploadForm.title} onChange={e => setUploadForm({...uploadForm, title: e.target.value})} required />
|
||
</div>
|
||
<div className="relative">
|
||
<AlignLeft className="absolute left-3 top-2.5 w-5 h-5 text-slate-400" />
|
||
<textarea className="w-full border p-2.5 pl-10 rounded-lg h-24" placeholder="Descrizione..." value={uploadForm.description} onChange={e => setUploadForm({...uploadForm, description: e.target.value})} />
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Tags</label>
|
||
<div className="flex gap-2 mb-2 flex-wrap">
|
||
{uploadForm.tags.map(tag => (
|
||
<span key={tag} className="bg-blue-100 text-blue-700 text-xs px-2 py-1 rounded-full flex items-center gap-1">
|
||
{tag} <button type="button" onClick={() => removeTag(tag)} className="hover:text-blue-900">×</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="flex gap-2 relative">
|
||
<Tag className="absolute left-3 top-2.5 w-4 h-4 text-slate-400" />
|
||
<input
|
||
className="flex-1 border p-2 pl-9 rounded-lg text-sm"
|
||
placeholder="Nuovo tag (es. Verbale)"
|
||
value={uploadForm.currentTagInput}
|
||
onChange={e => setUploadForm({...uploadForm, currentTagInput: e.target.value})}
|
||
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), addTag())}
|
||
/>
|
||
<button type="button" onClick={addTag} className="bg-slate-100 px-3 rounded-lg text-slate-600 font-bold">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center hover:bg-slate-50 transition-colors relative">
|
||
<input type="file" onChange={handleFileUpload} className="absolute inset-0 opacity-0 cursor-pointer" required accept=".pdf,image/*,.doc,.docx,.xls,.xlsx" />
|
||
<div className="flex flex-col items-center gap-2">
|
||
<Upload className="w-8 h-8 text-blue-500"/>
|
||
<span className="text-sm font-medium text-slate-600">{uploadForm.file ? uploadForm.file.name : 'Clicca o trascina file qui'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" disabled={isUploading || !uploadForm.file} className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 disabled:opacity-50 mt-4 flex justify-center items-center gap-2">
|
||
{isUploading ? 'Caricamento...' : 'Salva Documento'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Preview Modal */}
|
||
{previewDoc && (
|
||
<div className="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm">
|
||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl h-[85vh] flex flex-col">
|
||
<div className="flex justify-between items-center p-4 border-b">
|
||
<h3 className="font-bold text-slate-800">{previewDoc.doc.title}</h3>
|
||
<button onClick={() => setPreviewDoc(null)}><X className="w-6 h-6 text-slate-500"/></button>
|
||
</div>
|
||
<div className="flex-1 bg-slate-100 p-4 overflow-hidden flex items-center justify-center">
|
||
{previewDoc.doc.fileType === 'application/pdf' ? (
|
||
<iframe src={previewDoc.data} className="w-full h-full rounded shadow-sm" title="PDF Preview"></iframe>
|
||
) : (
|
||
<img src={previewDoc.data} alt="Preview" className="max-w-full max-h-full object-contain shadow-lg rounded" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|