Create Documents.tsx

This commit is contained in:
2025-12-11 23:37:04 +01:00
committed by GitHub
parent 101d8bf555
commit d21596ba94

320
pages/Documents.tsx Normal file
View File

@@ -0,0 +1,320 @@
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 } 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);
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());
} 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 {
const reader = new FileReader();
reader.readAsDataURL(uploadForm.file);
reader.onload = async () => {
const base64 = reader.result as string;
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) {
alert("Errore upload");
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">
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">
<input className="w-full border p-2.5 rounded-lg" placeholder="Titolo Documento" value={uploadForm.title} onChange={e => setUploadForm({...uploadForm, title: e.target.value})} required />
<textarea className="w-full border p-2.5 rounded-lg h-24" placeholder="Descrizione..." value={uploadForm.description} onChange={e => setUploadForm({...uploadForm, description: e.target.value})} />
<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">
<input
className="flex-1 border p-2 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">
{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>
);
};