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 = () => {
|
||||
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[]>([]);
|
||||
@@ -163,6 +165,22 @@ export const FamilyDetail: React.FC = () => {
|
||||
|
||||
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>;
|
||||
|
||||
@@ -203,10 +221,7 @@ export const FamilyDetail: React.FC = () => {
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setPaymentMethod(isPayPalEnabled ? 'paypal' : 'manual');
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
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" />
|
||||
@@ -305,11 +320,7 @@ export const FamilyDetail: React.FC = () => {
|
||||
<p className="text-slate-400 italic text-xs">Nessun pagamento</p>
|
||||
{month.status === PaymentStatus.UNPAID && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setNewPaymentMonth(month.monthIndex + 1);
|
||||
setPaymentMethod(isPayPalEnabled ? 'paypal' : 'manual');
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
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
|
||||
@@ -377,8 +388,8 @@ export const FamilyDetail: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{/* Payment Method Switcher */}
|
||||
{isPayPalEnabled && (
|
||||
{/* 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')}
|
||||
@@ -395,96 +406,113 @@ export const FamilyDetail: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{paymentMethod === 'manual' ? (
|
||||
<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>
|
||||
{/* 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>
|
||||
<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>
|
||||
@@ -496,4 +524,4 @@ export const FamilyDetail: React.FC = () => {
|
||||
|
||||
const BuildingIcon = ({className}:{className?:string}) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><rect width="16" height="20" x="4" y="2" rx="2" ry="2"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01"/><path d="M16 6h.01"/><path d="M8 10h.01"/><path d="M16 10h.01"/><path d="M8 14h.01"/><path d="M16 14h.01"/></svg>
|
||||
);
|
||||
);
|
||||
|
||||
@@ -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