feat(extraordinary-expenses): Add module for extraordinary expenses
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.
This commit is contained in:
148
pages/ExtraordinaryUser.tsx
Normal file
148
pages/ExtraordinaryUser.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user