Files
Condopay/pages/Documents.tsx
2025-12-11 23:37:04 +01:00

321 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};