feat: Improve payment modal logic and Docker configuration

Refactor the family detail page to introduce a new `handleOpenAddModal` function. This function intelligently sets the default payment method based on the current user's role: 'manual' for admins/power users and 'paypal' for others. This enhances user experience by pre-selecting the most appropriate payment option.

Additionally, the Docker configuration files have been updated. The mult-stage build setup for the frontend and backend has been removed in favor of a simpler structure, streamlining the Docker build process. The `nginx.conf` has also been updated to reflect these changes and ensure proper proxying.
This commit is contained in:
2025-12-07 23:16:47 +01:00
parent 759322c4c6
commit 80d658a536
5 changed files with 129 additions and 161 deletions

Binary file not shown.

View File

@@ -1,14 +0,0 @@
# Stage 1: Build Frontend
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,39 +0,0 @@
worker_processes 1;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# Limite upload per allegati (es. foto/video ticket)
client_max_body_size 50M;
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# Compressione Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Gestione SPA (React Router)
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API verso il backend
location /api/ {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}

View File

@@ -14,6 +14,8 @@ const MONTH_NAMES = [
export const FamilyDetail: React.FC = () => { export const FamilyDetail: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const currentUser = CondoService.getCurrentUser();
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
const [family, setFamily] = useState<Family | null>(null); const [family, setFamily] = useState<Family | null>(null);
const [payments, setPayments] = useState<Payment[]>([]); const [payments, setPayments] = useState<Payment[]>([]);
@@ -163,6 +165,22 @@ export const FamilyDetail: React.FC = () => {
const isPayPalEnabled = condo?.paypalClientId && settings?.features.payPal; 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 (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>; if (!family) return <div className="p-8 text-center text-red-500">Famiglia non trovata.</div>;
@@ -203,10 +221,7 @@ export const FamilyDetail: React.FC = () => {
</select> </select>
<button <button
onClick={() => { onClick={() => handleOpenAddModal()}
setPaymentMethod(isPayPalEnabled ? 'paypal' : 'manual');
setShowAddModal(true);
}}
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" 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" /> <Plus className="w-5 h-5" />
@@ -305,11 +320,7 @@ export const FamilyDetail: React.FC = () => {
<p className="text-slate-400 italic text-xs">Nessun pagamento</p> <p className="text-slate-400 italic text-xs">Nessun pagamento</p>
{month.status === PaymentStatus.UNPAID && ( {month.status === PaymentStatus.UNPAID && (
<button <button
onClick={() => { onClick={() => handleOpenAddModal(month.monthIndex)}
setNewPaymentMonth(month.monthIndex + 1);
setPaymentMethod(isPayPalEnabled ? 'paypal' : 'manual');
setShowAddModal(true);
}}
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" 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 Paga
@@ -377,8 +388,8 @@ export const FamilyDetail: React.FC = () => {
</div> </div>
<div className="p-6"> <div className="p-6">
{/* Payment Method Switcher */} {/* Payment Method Switcher - ONLY FOR PRIVILEGED USERS */}
{isPayPalEnabled && ( {isPayPalEnabled && isPrivileged && (
<div className="flex bg-slate-100 rounded-lg p-1 mb-6"> <div className="flex bg-slate-100 rounded-lg p-1 mb-6">
<button <button
onClick={() => setPaymentMethod('manual')} onClick={() => setPaymentMethod('manual')}
@@ -395,96 +406,113 @@ export const FamilyDetail: React.FC = () => {
</div> </div>
)} )}
{paymentMethod === 'manual' ? ( {/* Non-privileged users (regular tenants) without PayPal enabled: Show Block Message */}
<form onSubmit={handleManualSubmit} className="space-y-5"> {!isPrivileged && !isPayPalEnabled ? (
<div> <div className="text-center py-6">
<label className="block text-sm font-semibold text-slate-700 mb-1.5">Mese di Riferimento</label> <AlertCircle className="w-16 h-16 text-slate-300 mx-auto mb-4" />
<select <h4 className="text-lg font-bold text-slate-700">Pagamenti Online non attivi</h4>
value={newPaymentMonth} <p className="text-slate-500 mt-2 text-sm">Contatta l'amministratore per saldare la tua rata in contanti o bonifico.</p>
onChange={(e) => setNewPaymentMonth(parseInt(e.target.value))} <button
className="w-full border border-slate-300 rounded-xl p-3 focus:ring-2 focus:ring-blue-500 outline-none bg-white" onClick={() => setShowAddModal(false)}
> className="mt-6 px-6 py-2 bg-slate-100 text-slate-600 rounded-lg font-medium hover:bg-slate-200"
{MONTH_NAMES.map((name, i) => ( >
<option key={i} value={i + 1}>{name}</option> Chiudi
))} </button>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-1.5">Importo ()</label>
<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 focus:ring-2 focus:ring-blue-500 outline-none text-lg font-medium"
/>
</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.toString(),
},
},
],
});
}}
onApprove={(data, actions) => {
if(!actions.order) return Promise.resolve();
return actions.order.capture().then((details) => {
handlePaymentSuccess(details);
});
}}
/>
</PayPalScriptProvider>
)}
</div>
)}
<p className="text-xs text-center text-slate-400">Il pagamento sarà registrato automaticamente.</p>
</div> </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>
<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 focus:ring-2 focus:ring-blue-500 outline-none text-lg font-medium"
/>
</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.toString(),
},
},
],
});
}}
onApprove={(data, actions) => {
if(!actions.order) return Promise.resolve();
return actions.order.capture().then((details) => {
handlePaymentSuccess(details);
});
}}
/>
</PayPalScriptProvider>
)}
</div>
)}
<p className="text-xs text-center text-slate-400">Il pagamento sarà registrato automaticamente.</p>
</div>
)}
</>
)} )}
</div> </div>
</div> </div>

View File

@@ -1,7 +0,0 @@
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3001
CMD ["node", "server.js"]