569 lines
28 KiB
TypeScript
569 lines
28 KiB
TypeScript
|
|
import React, { useEffect, useState, useMemo } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { CondoService } from '../services/mockDb';
|
|
import { Family, Payment, AppSettings, MonthStatus, PaymentStatus, Condo } from '../types';
|
|
import { ArrowLeft, CheckCircle2, AlertCircle, Plus, Calendar, CreditCard, TrendingUp, Euro, Building as BuildingIcon } from 'lucide-react';
|
|
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";
|
|
|
|
const MONTH_NAMES = [
|
|
"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
|
|
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"
|
|
];
|
|
|
|
export const FamilyDetail: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const currentUser = CondoService.getCurrentUser();
|
|
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
|
|
|
|
const [family, setFamily] = useState<Family | null>(null);
|
|
const [payments, setPayments] = useState<Payment[]>([]);
|
|
const [settings, setSettings] = useState<AppSettings | null>(null);
|
|
const [condo, setCondo] = useState<Condo | undefined>(undefined);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
|
|
const [availableYears, setAvailableYears] = useState<number[]>([]);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
|
|
const [newPaymentMonth, setNewPaymentMonth] = useState<number>(new Date().getMonth() + 1);
|
|
const [newPaymentAmount, setNewPaymentAmount] = useState<number>(0);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
// Payment Method Selection
|
|
const [paymentMethod, setPaymentMethod] = useState<'manual' | 'paypal'>('manual');
|
|
const [paypalSuccessMsg, setPaypalSuccessMsg] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [famList, famPayments, appSettings, years, activeCondo] = await Promise.all([
|
|
CondoService.getFamilies(),
|
|
CondoService.getPaymentsByFamily(id),
|
|
CondoService.getSettings(),
|
|
CondoService.getAvailableYears(),
|
|
CondoService.getActiveCondo()
|
|
]);
|
|
|
|
const foundFamily = famList.find(f => f.id === id);
|
|
if (foundFamily) {
|
|
setFamily(foundFamily);
|
|
setPayments(famPayments);
|
|
setSettings(appSettings);
|
|
setCondo(activeCondo);
|
|
|
|
// Use Family Custom Quota OR Condo Default
|
|
const defaultAmount = foundFamily.customMonthlyQuota ?? activeCondo?.defaultMonthlyQuota ?? 100;
|
|
setNewPaymentAmount(defaultAmount);
|
|
|
|
setAvailableYears(years);
|
|
setSelectedYear(appSettings.currentYear);
|
|
} else {
|
|
navigate('/');
|
|
}
|
|
} catch (e) {
|
|
console.error("Error loading family details", e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadData();
|
|
}, [id, navigate]);
|
|
|
|
const monthlyStatus: MonthStatus[] = useMemo(() => {
|
|
const now = new Date();
|
|
const currentRealYear = now.getFullYear();
|
|
const currentRealMonth = now.getMonth() + 1; // 1-12
|
|
const currentDay = now.getDate();
|
|
|
|
// Default due day is 10 if not set
|
|
const dueDay = condo?.dueDay || 10;
|
|
|
|
return Array.from({ length: 12 }, (_, i) => {
|
|
const monthNum = i + 1;
|
|
const payment = payments.find(p => p.forMonth === monthNum && p.forYear === selectedYear);
|
|
|
|
let status = PaymentStatus.UPCOMING;
|
|
|
|
if (payment) {
|
|
status = PaymentStatus.PAID;
|
|
} else {
|
|
if (selectedYear < currentRealYear) {
|
|
status = PaymentStatus.UNPAID; // Past year unpaid
|
|
} else if (selectedYear === currentRealYear) {
|
|
if (monthNum < currentRealMonth) {
|
|
// Past month in current year -> Unpaid
|
|
status = PaymentStatus.UNPAID;
|
|
} else if (monthNum === currentRealMonth) {
|
|
// Current month
|
|
if (currentDay > dueDay) {
|
|
status = PaymentStatus.UNPAID; // Passed due date
|
|
} else if (currentDay >= (dueDay - 10)) {
|
|
status = PaymentStatus.PENDING; // Within 10 days before due
|
|
} else {
|
|
status = PaymentStatus.UPCOMING; // Early in the month
|
|
}
|
|
} else {
|
|
status = PaymentStatus.UPCOMING; // Future month
|
|
}
|
|
} else {
|
|
status = PaymentStatus.UPCOMING; // Future year
|
|
}
|
|
}
|
|
|
|
return {
|
|
monthIndex: i,
|
|
status,
|
|
payment
|
|
};
|
|
});
|
|
}, [payments, selectedYear, condo]);
|
|
|
|
const chartData = useMemo(() => {
|
|
return monthlyStatus.map(m => ({
|
|
label: MONTH_NAMES[m.monthIndex].substring(0, 3),
|
|
fullLabel: MONTH_NAMES[m.monthIndex],
|
|
amount: m.payment ? m.payment.amount : 0,
|
|
isPaid: m.status === PaymentStatus.PAID,
|
|
isFuture: m.status === PaymentStatus.UPCOMING
|
|
}));
|
|
}, [monthlyStatus]);
|
|
|
|
const maxChartValue = useMemo(() => {
|
|
const max = Math.max(...chartData.map(d => d.amount));
|
|
// Check family specific quota first
|
|
const baseline = family?.customMonthlyQuota ?? condo?.defaultMonthlyQuota ?? 100;
|
|
return max > 0 ? Math.max(max * 1.2, baseline) : baseline;
|
|
}, [chartData, condo, family]);
|
|
|
|
const handlePaymentSuccess = async (details?: any) => {
|
|
if (!family || !id) return;
|
|
setIsSubmitting(true);
|
|
try {
|
|
// Format date to SQL compatible string: YYYY-MM-DD HH:mm:ss
|
|
// This is safe for both MySQL (Strict Mode) and PostgreSQL
|
|
const now = new Date();
|
|
const sqlDate = now.toISOString().slice(0, 19).replace('T', ' ');
|
|
|
|
const payment = await CondoService.addPayment({
|
|
familyId: id,
|
|
amount: newPaymentAmount,
|
|
forMonth: newPaymentMonth,
|
|
forYear: selectedYear,
|
|
datePaid: sqlDate,
|
|
notes: details ? `Pagato con PayPal (ID: ${details.id})` : ''
|
|
});
|
|
|
|
setPayments([...payments, payment]);
|
|
if (!availableYears.includes(selectedYear)) {
|
|
setAvailableYears([...availableYears, selectedYear].sort((a,b) => b-a));
|
|
}
|
|
|
|
if (details) {
|
|
setPaypalSuccessMsg("Pagamento riuscito!");
|
|
setTimeout(() => {
|
|
setShowAddModal(false);
|
|
setPaypalSuccessMsg("");
|
|
}, 2000);
|
|
} else {
|
|
setShowAddModal(false);
|
|
}
|
|
} catch (e: any) {
|
|
console.error("Failed to add payment", e);
|
|
// Clean error message if it's a JSON string from backend
|
|
let msg = e.message || "Errore sconosciuto";
|
|
try {
|
|
const parsed = JSON.parse(msg);
|
|
if (parsed.error && parsed.error.includes("Incorrect datetime")) {
|
|
msg = "Errore data sistema (DB strict mode). Contatta l'assistenza.";
|
|
} else if (parsed.error) {
|
|
msg = parsed.error;
|
|
}
|
|
} catch {}
|
|
alert(`Errore durante il salvataggio: ${msg}`);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleManualSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
handlePaymentSuccess();
|
|
};
|
|
|
|
const isPayPalEnabled = condo?.paypalClientId && settings?.features.payPal;
|
|
|
|
const handleOpenAddModal = (monthIndex?: number) => {
|
|
if (monthIndex !== undefined) {
|
|
setNewPaymentMonth(monthIndex + 1);
|
|
}
|
|
|
|
// LOGIC:
|
|
// Admin -> Defaults to Manual (can switch if PayPal enabled)
|
|
// User -> Defaults to PayPal (cannot switch to manual)
|
|
if (isPrivileged) {
|
|
setPaymentMethod('manual');
|
|
} else {
|
|
setPaymentMethod('paypal');
|
|
}
|
|
setShowAddModal(true);
|
|
};
|
|
|
|
if (loading) return <div className="p-8 text-center text-slate-500">Caricamento dettagli...</div>;
|
|
if (!family) return <div className="p-8 text-center text-red-500">Famiglia non trovata.</div>;
|
|
|
|
return (
|
|
<div className="space-y-6 md:space-y-8 animate-fade-in pb-20 md:pb-12">
|
|
{/* Header Responsive */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
className="p-2 rounded-full hover:bg-slate-200 text-slate-600 transition-colors flex-shrink-0"
|
|
>
|
|
<ArrowLeft className="w-6 h-6" />
|
|
</button>
|
|
<div className="min-w-0">
|
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-900 truncate">{family.name}</h2>
|
|
<div className="flex items-center gap-2 text-slate-500 mt-1 text-sm md:text-base">
|
|
<BuildingIcon className="w-4 h-4 flex-shrink-0" />
|
|
<span>Interno: {family.unitNumber}</span>
|
|
{family.customMonthlyQuota && (
|
|
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-bold">
|
|
Quota Personalizzata: €{family.customMonthlyQuota}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
|
<select
|
|
value={selectedYear}
|
|
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
|
|
className="flex-1 md:flex-none border border-slate-300 rounded-lg px-3 py-2.5 bg-white text-slate-700 font-medium focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
{availableYears.map(year => (
|
|
<option key={year} value={year}>{year}</option>
|
|
))}
|
|
</select>
|
|
|
|
<button
|
|
onClick={() => handleOpenAddModal()}
|
|
className="flex-1 md:flex-none flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2.5 rounded-lg shadow-sm font-medium transition-all active:scale-95 whitespace-nowrap"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
<span className="hidden sm:inline">Nuovo Pagamento</span>
|
|
<span className="sm:hidden">Paga</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Summary - Stack on mobile */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex items-center gap-4">
|
|
<div className="p-3 bg-green-100 rounded-lg text-green-600 flex-shrink-0">
|
|
<CheckCircle2 className="w-6 h-6 md:w-8 md:h-8" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs md:text-sm font-medium text-slate-500 uppercase">Mesi Saldati</p>
|
|
<p className="text-xl md:text-2xl font-bold text-slate-800">
|
|
{monthlyStatus.filter(m => m.status === PaymentStatus.PAID).length} <span className="text-sm font-normal text-slate-400">/ 12</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex items-center gap-4">
|
|
<div className="p-3 bg-red-100 rounded-lg text-red-600 flex-shrink-0">
|
|
<AlertCircle className="w-6 h-6 md:w-8 md:h-8" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs md:text-sm font-medium text-slate-500 uppercase">Mesi Insoluti</p>
|
|
<p className="text-xl md:text-2xl font-bold text-slate-800">
|
|
{monthlyStatus.filter(m => m.status === PaymentStatus.UNPAID).length}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex items-center gap-4">
|
|
<div className="p-3 bg-blue-100 rounded-lg text-blue-600 flex-shrink-0">
|
|
<CreditCard className="w-6 h-6 md:w-8 md:h-8" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs md:text-sm font-medium text-slate-500 uppercase">Totale Versato</p>
|
|
<p className="text-xl md:text-2xl font-bold text-slate-800">
|
|
€ {payments.filter(p => p.forYear === selectedYear).reduce((acc, curr) => acc + curr.amount, 0).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Monthly Grid */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
<div className="px-5 py-4 border-b border-slate-100 bg-slate-50/50">
|
|
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
|
<Calendar className="w-5 h-5 text-slate-500" />
|
|
Dettaglio {selectedYear} <span className="text-xs font-normal text-slate-400 ml-2">(Scadenza: Giorno {condo?.dueDay || 10})</span>
|
|
</h3>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-0 divide-y sm:divide-y-0 sm:gap-px bg-slate-200 border-collapse">
|
|
{monthlyStatus.map((month) => (
|
|
<div
|
|
key={month.monthIndex}
|
|
className={`
|
|
p-5 bg-white transition-colors
|
|
${month.status === PaymentStatus.UNPAID ? 'bg-red-50/30' : ''}
|
|
${month.status === PaymentStatus.PAID ? 'bg-green-50/30' : ''}
|
|
${month.status === PaymentStatus.PENDING ? 'bg-yellow-50/30' : ''}
|
|
`}
|
|
>
|
|
<div className="flex justify-between items-center mb-3">
|
|
<span className="font-semibold text-slate-700 capitalize">{MONTH_NAMES[month.monthIndex]}</span>
|
|
{month.status === PaymentStatus.PAID && (
|
|
<span className="bg-green-100 text-green-700 text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wide">Saldato</span>
|
|
)}
|
|
{month.status === PaymentStatus.UNPAID && (
|
|
<span className="bg-red-100 text-red-700 text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wide">Insoluto</span>
|
|
)}
|
|
{month.status === PaymentStatus.PENDING && (
|
|
<span className="bg-yellow-100 text-yellow-700 text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wide">In Scadenza</span>
|
|
)}
|
|
{month.status === PaymentStatus.UPCOMING && (
|
|
<span className="bg-slate-100 text-slate-500 text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wide">Futuro</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="min-h-[3rem] flex flex-col justify-end">
|
|
{month.payment ? (
|
|
<div className="text-sm">
|
|
<p className="text-slate-600 flex justify-between">
|
|
<span>Importo:</span>
|
|
<span className="font-bold text-slate-900">€ {month.payment.amount}</span>
|
|
</p>
|
|
<p className="text-slate-400 text-xs mt-1 text-right">
|
|
{new Date(month.payment.datePaid).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 text-sm mt-auto">
|
|
<p className="text-slate-400 italic text-xs">Nessun pagamento</p>
|
|
{(month.status === PaymentStatus.UNPAID || month.status === PaymentStatus.PENDING) && (
|
|
<button
|
|
onClick={() => handleOpenAddModal(month.monthIndex)}
|
|
className="ml-auto text-blue-600 hover:text-blue-800 text-xs font-bold uppercase tracking-wide px-2 py-1 rounded hover:bg-blue-50 transition-colors"
|
|
>
|
|
Paga
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Payment Trend Chart (Scrollable) */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
<div className="px-5 py-4 border-b border-slate-100 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
|
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5 text-slate-500" />
|
|
Andamento
|
|
</h3>
|
|
<div className="text-xs font-medium text-slate-400 flex items-center gap-3">
|
|
<span className="flex items-center gap-1.5"><div className="w-2.5 h-2.5 rounded-full bg-blue-500"></div>Versato</span>
|
|
<span className="flex items-center gap-1.5"><div className="w-2.5 h-2.5 rounded-full bg-slate-200"></div>Mancante</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 md:p-8 overflow-x-auto">
|
|
<div className="h-56 md:h-64 min-w-[600px] w-full flex justify-between gap-3">
|
|
{chartData.map((data, index) => {
|
|
const heightPercentage = Math.max((data.amount / maxChartValue) * 100, 4);
|
|
|
|
return (
|
|
<div key={index} className="flex-1 flex flex-col items-center gap-2 group relative">
|
|
<div className="w-full flex-1 flex flex-col justify-end relative rounded-t-lg overflow-hidden bg-slate-50 hover:bg-slate-100 transition-colors">
|
|
{data.amount > 0 && (
|
|
<div
|
|
style={{ height: `${heightPercentage}%` }}
|
|
className={`w-full transition-all duration-500 ease-out rounded-t-sm ${
|
|
data.isPaid ? 'bg-blue-500' : 'bg-slate-300'
|
|
}`}
|
|
></div>
|
|
)}
|
|
{data.amount === 0 && !data.isFuture && (
|
|
<div className="w-full h-1 bg-red-300 absolute bottom-0"></div>
|
|
)}
|
|
</div>
|
|
<span className={`text-[10px] md:text-xs font-bold uppercase tracking-wide ${
|
|
data.amount > 0 ? 'text-slate-700' : 'text-slate-400'
|
|
}`}>
|
|
{data.label}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Add Payment Modal */}
|
|
{showAddModal && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm md:max-w-md overflow-hidden animate-in fade-in zoom-in duration-200">
|
|
<div className="bg-slate-50 px-6 py-4 border-b border-slate-100 flex justify-between items-center">
|
|
<h3 className="font-bold text-lg text-slate-800">Registra Pagamento</h3>
|
|
<button onClick={() => setShowAddModal(false)} className="text-slate-400 hover:text-slate-600 w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-200">✕</button>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
{/* Payment Method Switcher - ONLY FOR PRIVILEGED USERS */}
|
|
{isPayPalEnabled && isPrivileged && (
|
|
<div className="flex bg-slate-100 rounded-lg p-1 mb-6">
|
|
<button
|
|
onClick={() => setPaymentMethod('manual')}
|
|
className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-all ${paymentMethod === 'manual' ? 'bg-white text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
|
>
|
|
Manuale
|
|
</button>
|
|
<button
|
|
onClick={() => setPaymentMethod('paypal')}
|
|
className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-all ${paymentMethod === 'paypal' ? 'bg-white text-blue-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
|
>
|
|
PayPal
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Non-privileged users (regular tenants) without PayPal enabled: Show Block Message */}
|
|
{!isPrivileged && !isPayPalEnabled ? (
|
|
<div className="text-center py-6">
|
|
<AlertCircle className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
|
<h4 className="text-lg font-bold text-slate-700">Pagamenti Online non attivi</h4>
|
|
<p className="text-slate-500 mt-2 text-sm">Contatta l'amministratore per saldare la tua rata in contanti o bonifico.</p>
|
|
<button
|
|
onClick={() => setShowAddModal(false)}
|
|
className="mt-6 px-6 py-2 bg-slate-100 text-slate-600 rounded-lg font-medium hover:bg-slate-200"
|
|
>
|
|
Chiudi
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{paymentMethod === 'manual' && isPrivileged ? (
|
|
<form onSubmit={handleManualSubmit} className="space-y-5">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">Mese di Riferimento</label>
|
|
<select
|
|
value={newPaymentMonth}
|
|
onChange={(e) => setNewPaymentMonth(parseInt(e.target.value))}
|
|
className="w-full border border-slate-300 rounded-xl p-3 focus:ring-2 focus:ring-blue-500 outline-none bg-white"
|
|
>
|
|
{MONTH_NAMES.map((name, i) => (
|
|
<option key={i} value={i + 1}>{name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">Importo (€)</label>
|
|
<div className="relative">
|
|
<Euro className="absolute left-3 top-3.5 w-5 h-5 text-slate-400" />
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
required
|
|
value={newPaymentAmount}
|
|
onChange={(e) => setNewPaymentAmount(parseFloat(e.target.value))}
|
|
className="w-full border border-slate-300 rounded-xl p-3 pl-10 focus:ring-2 focus:ring-blue-500 outline-none text-lg font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-2 flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAddModal(false)}
|
|
className="flex-1 px-4 py-3 border border-slate-300 text-slate-700 rounded-xl hover:bg-slate-50 font-bold text-sm"
|
|
>
|
|
Annulla
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 font-bold text-sm disabled:opacity-50"
|
|
>
|
|
{isSubmitting ? '...' : 'Conferma'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="bg-blue-50 p-4 rounded-xl text-center">
|
|
<p className="text-sm text-slate-500 mb-1">Stai per pagare</p>
|
|
<p className="text-3xl font-bold text-blue-700">€ {newPaymentAmount.toFixed(2)}</p>
|
|
<p className="text-sm font-medium text-slate-600 mt-1">{MONTH_NAMES[newPaymentMonth - 1]} {selectedYear}</p>
|
|
</div>
|
|
|
|
{paypalSuccessMsg ? (
|
|
<div className="bg-green-100 text-green-700 p-4 rounded-xl text-center font-bold flex items-center justify-center gap-2">
|
|
<CheckCircle2 className="w-5 h-5"/>
|
|
{paypalSuccessMsg}
|
|
</div>
|
|
) : (
|
|
<div className="min-h-[150px] flex items-center justify-center">
|
|
{isPayPalEnabled && (
|
|
<PayPalScriptProvider options={{ clientId: condo.paypalClientId!, currency: "EUR" }}>
|
|
<PayPalButtons
|
|
style={{ layout: "vertical" }}
|
|
createOrder={(data, actions) => {
|
|
return actions.order.create({
|
|
intent: "CAPTURE",
|
|
purchase_units: [
|
|
{
|
|
description: `Quota ${MONTH_NAMES[newPaymentMonth - 1]} ${selectedYear} - Famiglia ${family.name}`,
|
|
amount: {
|
|
currency_code: "EUR",
|
|
value: newPaymentAmount.toFixed(2),
|
|
},
|
|
},
|
|
],
|
|
});
|
|
}}
|
|
onApprove={async (data, actions) => {
|
|
if(!actions.order) return;
|
|
try {
|
|
const details = await actions.order.capture();
|
|
await handlePaymentSuccess(details);
|
|
} catch (err) {
|
|
console.error("PayPal Capture Error", err);
|
|
alert("Errore durante il completamento del pagamento PayPal.");
|
|
}
|
|
}}
|
|
onError={(err) => {
|
|
console.error("PayPal Button Error", err);
|
|
alert("Errore caricamento PayPal: " + err.toString());
|
|
}}
|
|
/>
|
|
</PayPalScriptProvider>
|
|
)}
|
|
</div>
|
|
)}
|
|
<p className="text-xs text-center text-slate-400">Il pagamento sarà registrato automaticamente.</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|