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:
BIN
.dockerignore
BIN
.dockerignore
Binary file not shown.
14
Dockerfile
14
Dockerfile
@@ -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;"]
|
|
||||||
|
|||||||
39
nginx.conf
39
nginx.conf
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
FROM node:18-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install --production
|
|
||||||
COPY . .
|
|
||||||
EXPOSE 3001
|
|
||||||
CMD ["node", "server.js"]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user