Introduces a new module to manage and track extraordinary expenses within condominiums. This includes defining expense items, sharing arrangements, and attaching relevant documents. The module adds new types for `ExpenseItem`, `ExpenseShare`, and `ExtraordinaryExpense`. Mock database functions are updated to support fetching, creating, and managing these expenses. UI components in `Layout.tsx` and `Settings.tsx` are modified to include navigation and feature toggling for extraordinary expenses. Additionally, new routes are added in `App.tsx` for both administrative and user-facing views of these expenses.
149 lines
8.8 KiB
TypeScript
149 lines
8.8 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);
|
|
} 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>
|
|
);
|
|
};
|