Create Documents.tsx
This commit is contained in:
320
pages/Documents.tsx
Normal file
320
pages/Documents.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user