Update ExtraordinaryUser.tsx
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
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 { Briefcase, Calendar, CheckCircle2, AlertCircle, Eye, Paperclip, X, FileText, Download, Users, Euro } from 'lucide-react';
|
||||
import { ExtraordinaryExpense } from '../types';
|
||||
|
||||
export const ExtraordinaryUser: React.FC = () => {
|
||||
@@ -11,6 +11,7 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
const [paypalClientId, setPaypalClientId] = useState<string>('');
|
||||
const [successMsg, setSuccessMsg] = useState('');
|
||||
const [hasFamily, setHasFamily] = useState(true);
|
||||
const [userFamilyId, setUserFamilyId] = useState<string | null>(null);
|
||||
|
||||
// Details Modal State
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
@@ -26,6 +27,7 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setUserFamilyId(user.familyId);
|
||||
|
||||
const [myExp, condo] = await Promise.all([
|
||||
CondoService.getMyExpenses(),
|
||||
@@ -50,9 +52,14 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
await CondoService.payExpense(expenseId, amount);
|
||||
setSuccessMsg('Pagamento registrato con successo!');
|
||||
setTimeout(() => setSuccessMsg(''), 3000);
|
||||
// Refresh
|
||||
const updated = await CondoService.getMyExpenses();
|
||||
setExpenses(updated);
|
||||
// Refresh logic
|
||||
const updatedList = await CondoService.getMyExpenses();
|
||||
setExpenses(updatedList);
|
||||
// Also update selected expense if modal is open
|
||||
if (selectedExpense) {
|
||||
const updatedDetails = await CondoService.getExpenseDetails(expenseId);
|
||||
setSelectedExpense(updatedDetails);
|
||||
}
|
||||
} catch(e) { alert("Errore registrazione pagamento"); }
|
||||
};
|
||||
|
||||
@@ -119,7 +126,6 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
) : (
|
||||
<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 (
|
||||
@@ -162,47 +168,13 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
</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>
|
||||
<span>Restante: € {(exp.myShare.amountDue - exp.myShare.amountPaid).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 className="p-4 border-t border-slate-100 bg-slate-50 text-center text-xs text-blue-600 font-medium">
|
||||
Clicca per dettagli e pagamenti
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -212,8 +184,8 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
|
||||
{/* 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="fixed inset-0 bg-black/50 z-[100] 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-3xl 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>
|
||||
@@ -224,51 +196,88 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
</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()}
|
||||
<div className="p-6 overflow-y-auto space-y-8">
|
||||
{/* Top Section: Info & Dates */}
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
<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 className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-blue-50 p-3 rounded-lg border border-blue-100">
|
||||
<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 border border-green-100">
|
||||
<p className="text-xs text-green-600 font-bold uppercase mb-1">Costo Totale Progetto</p>
|
||||
<p className="text-sm font-medium text-slate-700">
|
||||
€ {selectedExpense.totalAmount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
{/* Middle Section: Items and Distribution Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Items List */}
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-700 mb-2 border-b pb-1">Voci di Spesa (Totale)</h4>
|
||||
<div className="bg-slate-50 rounded-lg overflow-hidden border border-slate-200">
|
||||
<table className="w-full text-sm text-left">
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{selectedExpense.items.map((item, i) => (
|
||||
<tr key={i}>
|
||||
<td className="p-3 text-slate-600 text-xs">{item.description}</td>
|
||||
<td className="p-3 text-right text-slate-900 font-medium text-xs">€ {item.amount.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="bg-slate-100 font-bold">
|
||||
<td className="p-3 text-xs">Totale</td>
|
||||
<td className="p-3 text-right text-xs">€ {selectedExpense.totalAmount.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shares Table (Piano di Ripartizione) */}
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-700 mb-2 border-b pb-1 flex items-center gap-2">
|
||||
<Users className="w-4 h-4"/> Piano di Ripartizione
|
||||
</h4>
|
||||
<div className="bg-white rounded-lg overflow-hidden border border-slate-200 h-full max-h-60 overflow-y-auto">
|
||||
<table className="w-full text-xs text-left">
|
||||
<thead className="bg-slate-50 text-slate-500 font-semibold sticky top-0">
|
||||
<tr>
|
||||
<th className="p-2">Condomino</th>
|
||||
<th className="p-2 text-right">%</th>
|
||||
<th className="p-2 text-right">Quota</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{selectedExpense.shares?.map((share, i) => {
|
||||
const isMe = share.familyId === userFamilyId;
|
||||
return (
|
||||
<tr key={i} className={isMe ? "bg-blue-50" : ""}>
|
||||
<td className="p-2 font-medium text-slate-700 truncate max-w-[100px]">
|
||||
{share.familyName}
|
||||
{isMe && <span className="ml-1 text-[9px] bg-blue-100 text-blue-700 px-1 rounded">TU</span>}
|
||||
</td>
|
||||
<td className="p-2 text-right text-slate-500">{share.percentage}%</td>
|
||||
<td className={`p-2 text-right font-bold ${isMe ? 'text-blue-700' : 'text-slate-700'}`}>
|
||||
€ {share.amountDue.toFixed(2)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -297,10 +306,60 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
)}
|
||||
</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>
|
||||
{/* Payment Footer - Only visible here now */}
|
||||
<div className="p-4 border-t border-slate-100 bg-slate-50">
|
||||
{(() => {
|
||||
const myShare = selectedExpense.shares?.find(s => s.familyId === userFamilyId);
|
||||
if (!myShare) return null;
|
||||
const remaining = Math.max(0, myShare.amountDue - myShare.amountPaid);
|
||||
|
||||
if (remaining < 0.01) {
|
||||
return (
|
||||
<div className="flex justify-end items-center gap-4">
|
||||
<span className="text-green-600 font-bold flex items-center gap-2 text-sm"><CheckCircle2 className="w-5 h-5"/> Quota Saldata</span>
|
||||
<button onClick={() => setShowDetails(false)} className="px-6 py-2 bg-slate-200 text-slate-700 rounded-lg hover:bg-slate-300 font-medium text-sm">Chiudi</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="font-bold text-slate-700">Da Saldare:</span>
|
||||
<span className="font-bold text-red-600 text-lg">€ {remaining.toFixed(2)}</span>
|
||||
</div>
|
||||
{paypalClientId ? (
|
||||
<PayPalScriptProvider options={{ clientId: paypalClientId, currency: "EUR" }}>
|
||||
<PayPalButtons
|
||||
style={{ layout: "horizontal", height: 45 }}
|
||||
createOrder={(data, actions) => {
|
||||
return actions.order.create({
|
||||
intent: "CAPTURE",
|
||||
purchase_units: [{
|
||||
description: `Spesa Straordinaria: ${selectedExpense.title}`,
|
||||
amount: { currency_code: "EUR", value: remaining.toFixed(2) }
|
||||
}]
|
||||
});
|
||||
}}
|
||||
onApprove={(data, actions) => {
|
||||
if(!actions.order) return Promise.resolve();
|
||||
return actions.order.capture().then(() => {
|
||||
handlePaymentSuccess(selectedExpense.id, remaining);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PayPalScriptProvider>
|
||||
) : (
|
||||
<div className="bg-red-50 text-red-600 p-2 rounded text-center text-xs">
|
||||
Pagamenti online non configurati.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<button onClick={() => setShowDetails(false)} className="w-full sm:w-auto px-6 py-2 border border-slate-300 text-slate-600 rounded-lg hover:bg-slate-100 font-medium text-sm">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user