This commit refactors the API service to use a consistent `fetch` wrapper for all requests, improving error handling and authorization logic. It also updates UI components to reflect changes in API endpoints and data structures, particularly around notifications and extraordinary expenses. Docker configurations are removed as they are no longer relevant for this stage of development.
155 lines
9.1 KiB
TypeScript
155 lines
9.1 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 } from 'lucide-react';
|
|
|
|
export const ExtraordinaryUser: React.FC = () => {
|
|
const [expenses, setExpenses] = useState<any[]>([]); // Using any for composite object from specific API
|
|
const [loading, setLoading] = useState(true);
|
|
const [paypalClientId, setPaypalClientId] = useState<string>('');
|
|
const [successMsg, setSuccessMsg] = useState('');
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
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"); }
|
|
};
|
|
|
|
if (loading) return <div className="p-8 text-center text-slate-400">Caricamento spese...</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} className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
|
|
<div className="p-5 border-b border-slate-100 bg-slate-50/50 flex justify-between items-start">
|
|
<div>
|
|
<h3 className="font-bold text-lg text-slate-800">{exp.title}</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: `${(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: € {remaining.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-5 border-t border-slate-100 bg-slate-50">
|
|
{!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>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|