From 531140061582a841721d56aa78671b738752539e Mon Sep 17 00:00:00 2001 From: frakarr Date: Sun, 7 Dec 2025 19:49:59 +0100 Subject: [PATCH] feat: Add tickets module and PayPal integration Introduces a new 'Tickets' module for users to submit and manage issues within their condominium. This includes defining ticket types, statuses, priorities, and categories. Additionally, this commit integrates PayPal as a payment option for family fee payments, enabling users to pay directly via PayPal using their client ID. Key changes: - Added `Ticket` related types and enums. - Implemented `TicketService` functions for CRUD operations. - Integrated `@paypal/react-paypal-js` library. - Added `paypalClientId` to `AppSettings` and `Condo` types. - Updated `FamilyDetail` page to include PayPal payment option. - Added 'Segnalazioni' navigation link to `Layout`. --- .dockerignore | Bin 81 -> 105 bytes App.tsx | 3 + Dockerfile | 14 -- components/Layout.tsx | 9 +- index.html | 3 +- nginx.conf | 21 +-- pages/FamilyDetail.tsx | 209 +++++++++++++++------- pages/Settings.tsx | 48 +++-- pages/Tickets.tsx | 392 +++++++++++++++++++++++++++++++++++++++++ server/Dockerfile | 8 +- server/db.js | 47 ++++- server/server.js | 283 ++++++++++++++++++++++++++--- services/mockDb.ts | 37 +++- types.ts | 49 ++++++ 14 files changed, 977 insertions(+), 146 deletions(-) create mode 100644 pages/Tickets.tsx diff --git a/.dockerignore b/.dockerignore index 10c5075f1f010ae3f81113205ea3014be510d60b..b3e90c29e43ec3e8cab67b74444927db2400e19e 100644 GIT binary patch literal 105 zcmaFAfA9PKd*gsOOBP6k196$QZXS>-;ZOpS>_7}e3=m2?147F|C { }> } /> } /> + } /> } /> diff --git a/Dockerfile b/Dockerfile index e201a86..e69de29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +0,0 @@ -# Stage 1: Build the React application -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/conf.d/default.conf -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] diff --git a/components/Layout.tsx b/components/Layout.tsx index 8e8b524..1d526aa 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { NavLink, Outlet } from 'react-router-dom'; -import { Users, Settings, Building, LogOut, Menu, X, ChevronDown, Check, LayoutDashboard, Megaphone, Info, AlertTriangle, Hammer, Calendar } from 'lucide-react'; +import { Users, Settings, Building, LogOut, Menu, X, ChevronDown, Check, LayoutDashboard, Megaphone, Info, AlertTriangle, Hammer, Calendar, MessageSquareWarning } from 'lucide-react'; import { CondoService } from '../services/mockDb'; import { Condo, Notice } from '../types'; @@ -217,6 +217,11 @@ export const Layout: React.FC = () => { Famiglie + + + + Segnalazioni + @@ -250,4 +255,4 @@ export const Layout: React.FC = () => { ); -}; +}; \ No newline at end of file diff --git a/index.html b/index.html index 7c674d7..13149e7 100644 --- a/index.html +++ b/index.html @@ -40,7 +40,8 @@ "react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.10.1", "lucide-react": "https://aistudiocdn.com/lucide-react@^0.556.0", "vite": "https://aistudiocdn.com/vite@^7.2.6", - "@vitejs/plugin-react": "https://aistudiocdn.com/@vitejs/plugin-react@^5.1.1" + "@vitejs/plugin-react": "https://aistudiocdn.com/@vitejs/plugin-react@^5.1.1", + "@paypal/react-paypal-js": "https://esm.sh/@paypal/react-paypal-js@8.1.3?external=react,react-dom" } } diff --git a/nginx.conf b/nginx.conf index 59b9f8d..97684f0 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,20 +1 @@ -server { - listen 80; - - # Serve React App (SPA) - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - # Proxy API requests to Backend Service - 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; - } -} +���z \ No newline at end of file diff --git a/pages/FamilyDetail.tsx b/pages/FamilyDetail.tsx index 48b53b6..542e657 100644 --- a/pages/FamilyDetail.tsx +++ b/pages/FamilyDetail.tsx @@ -4,6 +4,7 @@ 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 } from 'lucide-react'; +import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js"; const MONTH_NAMES = [ "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", @@ -26,6 +27,10 @@ export const FamilyDetail: React.FC = () => { const [newPaymentMonth, setNewPaymentMonth] = useState(new Date().getMonth() + 1); const [newPaymentAmount, setNewPaymentAmount] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); + + // Payment Method Selection + const [paymentMethod, setPaymentMethod] = useState<'manual' | 'paypal'>('manual'); + const [paypalSuccessMsg, setPaypalSuccessMsg] = useState(''); useEffect(() => { if (!id) return; @@ -117,33 +122,45 @@ export const FamilyDetail: React.FC = () => { return max > 0 ? Math.max(max * 1.2, baseline) : baseline; }, [chartData, condo, family]); - const handleAddPayment = async (e: React.FormEvent) => { - e.preventDefault(); + const handlePaymentSuccess = async (details?: any) => { if (!family || !id) return; - setIsSubmitting(true); try { - const payment = await CondoService.addPayment({ - familyId: id, - amount: newPaymentAmount, - forMonth: newPaymentMonth, - forYear: selectedYear, - datePaid: new Date().toISOString() - }); + const payment = await CondoService.addPayment({ + familyId: id, + amount: newPaymentAmount, + forMonth: newPaymentMonth, + forYear: selectedYear, + datePaid: new Date().toISOString(), + notes: details ? `Pagato con PayPal (ID: ${details.id})` : '' + }); - setPayments([...payments, payment]); - if (!availableYears.includes(selectedYear)) { - setAvailableYears([...availableYears, selectedYear].sort((a,b) => b-a)); - } - - setShowAddModal(false); + 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) { - console.error("Failed to add payment", e); + console.error("Failed to add payment", e); } finally { - setIsSubmitting(false); + setIsSubmitting(false); } }; + const handleManualSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handlePaymentSuccess(); + }; + if (loading) return
Caricamento dettagli...
; if (!family) return
Famiglia non trovata.
; @@ -184,7 +201,10 @@ export const FamilyDetail: React.FC = () => { -
-
- - -
+
+ {/* Payment Method Switcher */} + {condo?.paypalClientId && ( +
+ + +
+ )} -
- - 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" - /> -
+ {paymentMethod === 'manual' ? ( + +
+ + +
-
- - -
- +
+ + 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" + /> +
+ +
+ + +
+ + ) : ( +
+
+

Stai per pagare

+

€ {newPaymentAmount.toFixed(2)}

+

{MONTH_NAMES[newPaymentMonth - 1]} {selectedYear}

+
+ + {paypalSuccessMsg ? ( +
+ + {paypalSuccessMsg} +
+ ) : ( +
+ {condo?.paypalClientId && ( + + { + 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); + }); + }} + /> + + )} +
+ )} +

Il pagamento sarà registrato automaticamente.

+
+ )} +
)} diff --git a/pages/Settings.tsx b/pages/Settings.tsx index e374a8f..2d2516c 100644 --- a/pages/Settings.tsx +++ b/pages/Settings.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { CondoService } from '../services/mockDb'; import { AppSettings, Family, User, AlertDefinition, Condo, Notice, NoticeIconType, NoticeRead } from '../types'; -import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, User as UserIcon, Server, Bell, Clock, FileText, Lock, Megaphone, CheckCircle2, Info, Hammer, Link as LinkIcon, Eye, Calendar, List, UserCog, Mail, Power, MapPin } from 'lucide-react'; +import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, User as UserIcon, Server, Bell, Clock, FileText, Lock, Megaphone, CheckCircle2, Info, Hammer, Link as LinkIcon, Eye, Calendar, List, UserCog, Mail, Power, MapPin, CreditCard } from 'lucide-react'; export const SettingsPage: React.FC = () => { const currentUser = CondoService.getCurrentUser(); @@ -40,6 +40,7 @@ export const SettingsPage: React.FC = () => { province: '', zipCode: '', notes: '', + paypalClientId: '', defaultMonthlyQuota: 100 }); @@ -235,7 +236,7 @@ export const SettingsPage: React.FC = () => { // --- Condo Management Handlers --- const openAddCondoModal = () => { setEditingCondo(null); - setCondoForm({ name: '', address: '', streetNumber: '', city: '', province: '', zipCode: '', notes: '', defaultMonthlyQuota: 100 }); + setCondoForm({ name: '', address: '', streetNumber: '', city: '', province: '', zipCode: '', notes: '', paypalClientId: '', defaultMonthlyQuota: 100 }); setShowCondoModal(true); }; @@ -249,6 +250,7 @@ export const SettingsPage: React.FC = () => { province: c.province || '', zipCode: c.zipCode || '', notes: c.notes || '', + paypalClientId: c.paypalClientId || '', defaultMonthlyQuota: c.defaultMonthlyQuota }); setShowCondoModal(true); @@ -266,6 +268,7 @@ export const SettingsPage: React.FC = () => { province: condoForm.province, zipCode: condoForm.zipCode, notes: condoForm.notes, + paypalClientId: condoForm.paypalClientId, defaultMonthlyQuota: condoForm.defaultMonthlyQuota }; @@ -607,6 +610,11 @@ export const SettingsPage: React.FC = () => { )} + {/* Rest of the file (Families, Users, Notices, Alerts, SMTP Tabs) remains mostly same, just update modal */} + {/* ... (Existing Tabs Code for Families, Users, Notices, Alerts, SMTP) ... */} + + {/* Only change is inside CONDO MODAL */} + {/* Families Tab */} {isAdmin && activeTab === 'families' && (
@@ -789,7 +797,7 @@ export const SettingsPage: React.FC = () => {
)} - {/* ALERT MODAL */} + {/* ALERT MODAL (Existing) */} {showAlertModal && (
@@ -826,7 +834,7 @@ export const SettingsPage: React.FC = () => {
)} - {/* NOTICE MODAL */} + {/* NOTICE MODAL (Existing) */} {showNoticeModal && (
@@ -868,7 +876,7 @@ export const SettingsPage: React.FC = () => {
)} - {/* READ DETAILS MODAL */} + {/* READ DETAILS MODAL (Existing) */} {showReadDetailsModal && selectedNoticeId && (
@@ -907,7 +915,7 @@ export const SettingsPage: React.FC = () => {
)} - {/* USER MODAL */} + {/* USER MODAL (Existing) */} {showUserModal && (
@@ -950,10 +958,10 @@ export const SettingsPage: React.FC = () => {
)} - {/* CONDO MODAL */} + {/* CONDO MODAL (UPDATED) */} {showCondoModal && (
-
+

{editingCondo ? 'Modifica Condominio' : 'Nuovo Condominio'}

@@ -985,9 +993,27 @@ export const SettingsPage: React.FC = () => {
+ {/* PayPal Integration Section */} +
+
+ + Configurazione Pagamenti +
+
+ + setCondoForm({...condoForm, paypalClientId: e.target.value})} + /> +

Necessario per abilitare i pagamenti online delle rate.

+
+
+ {/* Notes */}
-