Files
Condopay/pages/ExtraordinaryUser.tsx
2025-12-10 23:45:33 +01:00

311 lines
19 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { CondoService } from '../services/mockDb';
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";
import { Briefcase, Calendar, CheckCircle2, AlertCircle, Eye, Paperclip, X, FileText, Download } from 'lucide-react';
import { ExtraordinaryExpense } from '../types';
export const ExtraordinaryUser: React.FC = () => {
const [expenses, setExpenses] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [paypalClientId, setPaypalClientId] = useState<string>('');
const [successMsg, setSuccessMsg] = useState('');
const [hasFamily, setHasFamily] = useState(true);
// Details Modal State
const [showDetails, setShowDetails] = useState(false);
const [selectedExpense, setSelectedExpense] = useState<ExtraordinaryExpense | null>(null);
const [loadingDetails, setLoadingDetails] = useState(false);
useEffect(() => {
const load = async () => {
try {
const user = CondoService.getCurrentUser();
if (!user?.familyId) {
setHasFamily(false);
setLoading(false);
return;
}
const [myExp, condo] = await Promise.all([
CondoService.getMyExpenses(),
CondoService.getActiveCondo()
]);
setExpenses(myExp);
if (condo?.paypalClientId) setPaypalClientId(condo.paypalClientId);
// Update "Last Viewed" timestamp to clear notification
localStorage.setItem('lastViewedExpensesTime', Date.now().toString());
// Trigger event to update Sidebar immediately
window.dispatchEvent(new Event('expenses-viewed'));
} catch(e) { console.error(e); }
finally { setLoading(false); }
};
load();
}, []);
const handlePaymentSuccess = async (expenseId: string, amount: number) => {
try {
await CondoService.payExpense(expenseId, amount);
setSuccessMsg('Pagamento registrato con successo!');
setTimeout(() => setSuccessMsg(''), 3000);
// Refresh
const updated = await CondoService.getMyExpenses();
setExpenses(updated);
} catch(e) { alert("Errore registrazione pagamento"); }
};
const handleCardClick = async (expenseId: string) => {
setLoadingDetails(true);
try {
// Fetch full details including attachments
const fullDetails = await CondoService.getExpenseDetails(expenseId);
setSelectedExpense(fullDetails);
setShowDetails(true);
} catch (e) {
console.error(e);
alert("Impossibile caricare i dettagli.");
} finally {
setLoadingDetails(false);
}
};
const openAttachment = async (expenseId: string, attId: string) => {
try {
const file = await CondoService.getExpenseAttachment(expenseId, attId);
const win = window.open();
if (win) {
if (file.fileType.startsWith('image/') || file.fileType === 'application/pdf') {
win.document.write(`<iframe src="${file.data}" frameborder="0" style="border:0; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%;" allowfullscreen></iframe>`);
} else {
win.document.write(`<a href="${file.data}" download="${file.fileName}">Download ${file.fileName}</a>`);
}
}
} catch(e) { alert("Errore apertura file"); }
};
if (loading) return <div className="p-8 text-center text-slate-400">Caricamento spese...</div>;
if (!hasFamily) {
return (
<div className="p-8 text-center bg-amber-50 rounded-xl border border-amber-200 text-amber-800">
<AlertCircle className="w-12 h-12 mx-auto mb-3 text-amber-500" />
<h3 className="text-lg font-bold mb-2">Nessuna Famiglia Associata</h3>
<p>Il tuo utente (probabilmente Admin) non è collegato ad alcuna famiglia, quindi non ci sono spese personali da mostrare.</p>
<p className="text-sm mt-2 text-amber-700">Vai in Impostazioni &gt; Utenti e associa il tuo account a una famiglia per testare questa vista.</p>
</div>
);
}
return (
<div className="space-y-6 pb-20 animate-fade-in">
<div>
<h2 className="text-2xl font-bold text-slate-800">Le Mie Spese Extra</h2>
<p className="text-slate-500 text-sm">Lavori straordinari e ripartizioni</p>
</div>
{successMsg && (
<div className="bg-green-100 text-green-700 p-4 rounded-xl flex items-center gap-2 mb-4">
<CheckCircle2 className="w-5 h-5"/> {successMsg}
</div>
)}
{expenses.length === 0 ? (
<div className="text-center p-12 bg-white rounded-xl border border-dashed border-slate-300">
<Briefcase className="w-12 h-12 text-slate-300 mx-auto mb-3"/>
<p className="text-slate-500">Nessuna spesa straordinaria attiva.</p>
</div>
) : (
<div className="grid gap-6 md:grid-cols-2">
{expenses.map(exp => {
const remaining = exp.myShare.amountDue - exp.myShare.amountPaid;
const isPaid = exp.myShare.status === 'PAID';
return (
<div
key={exp.id}
onClick={() => handleCardClick(exp.id)}
className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col cursor-pointer hover:shadow-md transition-all group"
>
<div className="p-5 border-b border-slate-100 bg-slate-50/50 flex justify-between items-start group-hover:bg-blue-50/30 transition-colors">
<div>
<h3 className="font-bold text-lg text-slate-800 group-hover:text-blue-700 transition-colors flex items-center gap-2">
{exp.title} <Eye className="w-4 h-4 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity"/>
</h3>
<p className="text-xs text-slate-500 mt-1 flex items-center gap-1">
<Calendar className="w-3 h-3"/> {new Date(exp.startDate).toLocaleDateString()}
</p>
</div>
<div className={`px-3 py-1 rounded-full text-xs font-bold uppercase ${
isPaid ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
}`}>
{isPaid ? 'Saldato' : 'In Sospeso'}
</div>
</div>
<div className="p-5 flex-1 space-y-4">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500">Quota Totale (100%)</span>
<span className="font-medium text-slate-800"> {exp.totalAmount.toLocaleString()}</span>
</div>
<div className="p-3 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex justify-between items-center text-sm mb-1">
<span className="text-blue-800 font-medium">La tua quota ({exp.myShare.percentage}%)</span>
<span className="font-bold text-blue-900"> {exp.myShare.amountDue.toFixed(2)}</span>
</div>
<div className="w-full bg-blue-200 h-1.5 rounded-full overflow-hidden">
<div
className="bg-blue-600 h-full transition-all duration-500"
style={{ width: `${Math.min(100, (exp.myShare.amountPaid / exp.myShare.amountDue) * 100)}%` }}
/>
</div>
<div className="flex justify-between text-xs mt-1 text-blue-600">
<span>Versato: {exp.myShare.amountPaid.toFixed(2)}</span>
<span>Restante: {Math.max(0, remaining).toFixed(2)}</span>
</div>
</div>
</div>
<div className="p-5 border-t border-slate-100 bg-slate-50" onClick={(e) => e.stopPropagation()}>
{!isPaid && remaining > 0.01 && (
<>
{paypalClientId ? (
<PayPalScriptProvider options={{ clientId: paypalClientId, currency: "EUR" }}>
<PayPalButtons
style={{ layout: "horizontal", height: 40 }}
createOrder={(data, actions) => {
return actions.order.create({
intent: "CAPTURE",
purchase_units: [{
description: `Spesa Straordinaria: ${exp.title}`,
amount: { currency_code: "EUR", value: remaining.toFixed(2) }
}]
});
}}
onApprove={(data, actions) => {
if(!actions.order) return Promise.resolve();
return actions.order.capture().then(() => {
handlePaymentSuccess(exp.id, remaining);
});
}}
/>
</PayPalScriptProvider>
) : (
<div className="text-center text-xs text-red-500 bg-red-50 p-2 rounded">
Pagamenti online non configurati. Contatta l'amministratore.
</div>
)}
</>
)}
{isPaid && (
<div className="text-center text-green-600 font-bold text-sm flex items-center justify-center gap-2">
<CheckCircle2 className="w-5 h-5"/> Nessun importo dovuto
</div>
)}
</div>
</div>
);
})}
</div>
)}
{/* DETAILS MODAL */}
{showDetails && selectedExpense && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm animate-in fade-in">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col overflow-hidden">
<div className="flex justify-between items-center p-5 border-b border-slate-100 bg-slate-50">
<div>
<h3 className="font-bold text-xl text-slate-800">{selectedExpense.title}</h3>
<p className="text-xs text-slate-500 uppercase tracking-wider mt-1">{selectedExpense.contractorName}</p>
</div>
<button onClick={() => setShowDetails(false)} className="p-2 hover:bg-slate-200 rounded-full text-slate-500 transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<div className="p-6 overflow-y-auto space-y-6">
{/* Description */}
<div>
<h4 className="text-sm font-bold text-slate-700 mb-2 flex items-center gap-2"><FileText className="w-4 h-4"/> Descrizione Lavori</h4>
<p className="text-slate-600 text-sm leading-relaxed bg-slate-50 p-4 rounded-lg border border-slate-100">
{selectedExpense.description}
</p>
</div>
{/* Dates & Totals */}
<div className="grid grid-cols-2 gap-4">
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-xs text-blue-600 font-bold uppercase mb-1">Inizio Lavori</p>
<p className="text-sm font-medium text-slate-700">
{new Date(selectedExpense.startDate).toLocaleDateString()}
</p>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-xs text-green-600 font-bold uppercase mb-1">Totale Progetto</p>
<p className="text-sm font-medium text-slate-700">
{selectedExpense.totalAmount.toLocaleString()}
</p>
</div>
</div>
{/* Items List */}
<div>
<h4 className="text-sm font-bold text-slate-700 mb-2">Dettaglio Voci di Spesa</h4>
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600">
<tr>
<th className="p-3 font-semibold">Descrizione</th>
<th className="p-3 font-semibold text-right">Importo</th>
</tr>
</thead>
<tbody className="divide-y">
{selectedExpense.items.map((item, i) => (
<tr key={i}>
<td className="p-3 text-slate-700">{item.description}</td>
<td className="p-3 text-right text-slate-900 font-medium"> {item.amount.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Attachments */}
{selectedExpense.attachments && selectedExpense.attachments.length > 0 && (
<div>
<h4 className="text-sm font-bold text-slate-700 mb-2 flex items-center gap-2"><Paperclip className="w-4 h-4"/> Documenti & Allegati</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{selectedExpense.attachments.map(att => (
<button
key={att.id}
onClick={() => openAttachment(selectedExpense.id, att.id)}
className="flex items-center justify-between p-3 border rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-all text-left group"
>
<div className="flex items-center gap-2 min-w-0">
<div className="bg-slate-100 p-2 rounded group-hover:bg-white transition-colors">
<FileText className="w-4 h-4 text-blue-600"/>
</div>
<span className="text-sm font-medium text-slate-700 truncate">{att.fileName}</span>
</div>
<Download className="w-4 h-4 text-slate-400 group-hover:text-blue-600"/>
</button>
))}
</div>
</div>
)}
</div>
<div className="p-4 border-t border-slate-100 bg-slate-50 flex justify-end">
<button onClick={() => setShowDetails(false)} className="px-6 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 transition-colors font-medium text-sm">
Chiudi
</button>
</div>
</div>
</div>
)}
</div>
);
};