diff --git a/.dockerignore b/.dockerignore index 21edd64..276bbec 100644 Binary files a/.dockerignore and b/.dockerignore differ diff --git a/App.tsx b/App.tsx index 5e214b3..ba900f2 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,3 @@ - import React from 'react'; import { HashRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { Layout } from './components/Layout'; @@ -7,6 +6,8 @@ import { FamilyDetail } from './pages/FamilyDetail'; import { SettingsPage } from './pages/Settings'; import { TicketsPage } from './pages/Tickets'; import { ReportsPage } from './pages/Reports'; +import { ExtraordinaryAdmin } from './pages/ExtraordinaryAdmin'; +import { ExtraordinaryUser } from './pages/ExtraordinaryUser'; import { LoginPage } from './pages/Login'; import { CondoService } from './services/mockDb'; @@ -21,6 +22,18 @@ const ProtectedRoute = ({ children }: { children?: React.ReactNode }) => { return <>{children}; }; +// Route wrapper that checks for Admin/PowerUser +const AdminRoute = ({ children }: { children?: React.ReactNode }) => { + const user = CondoService.getCurrentUser(); + const isAdmin = user?.role === 'admin' || user?.role === 'poweruser'; + + if (!isAdmin) { + // Redirect regular users to their own view + return ; + } + return <>{children}; +}; + const App: React.FC = () => { return ( @@ -36,6 +49,11 @@ const App: React.FC = () => { } /> } /> } /> + + + + } /> } /> @@ -45,4 +63,4 @@ const App: React.FC = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index bb87fa8..e69de29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +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 the nginx configuration file (using the .txt extension as provided in source) -COPY nginx.txt /etc/nginx/nginx.conf -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] diff --git a/components/Layout.tsx b/components/Layout.tsx index 0f26e87..f836ed9 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, MessageSquareWarning, PieChart } from 'lucide-react'; +import { Users, Settings, Building, LogOut, Menu, X, ChevronDown, Check, LayoutDashboard, Megaphone, Info, AlertTriangle, Hammer, Calendar, MessageSquareWarning, PieChart, Briefcase } from 'lucide-react'; import { CondoService } from '../services/mockDb'; import { Condo, Notice, AppSettings } from '../types'; @@ -38,10 +38,6 @@ export const Layout: React.FC = () => { setActiveCondo(active); // Check for notices for User - // ONLY if notices feature is enabled (which we check inside logic or rely on settings state) - // However, `getSettings` is async. For simplicity, we fetch notices and if feature disabled at backend/UI level, it's fine. - // Ideally we check `settings?.features.notices` but `settings` might not be set yet. - // We'll rely on the UI hiding it, but fetching it doesn't hurt. if (!isAdmin && active && user) { try { const unread = await CondoService.getUnreadNoticesForUser(user.id, active.id); @@ -240,6 +236,14 @@ export const Layout: React.FC = () => { Famiglie + {/* New Extraordinary Expenses Link - Conditional */} + {settings?.features.extraordinaryExpenses && ( + + + {isAdmin ? 'Spese Straordinarie' : 'Le Mie Spese Extra'} + + )} + {/* Privileged Links */} {isAdmin && settings?.features.reports && ( diff --git a/nginx.conf b/nginx.conf index f8625d9..e69de29 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,38 +0,0 @@ -worker_processes 1; - -events { worker_connections 1024; } - -http { - include mime.types; - default_type application/octet-stream; - sendfile on; - keepalive_timeout 65; - - server { - listen 80; - root /usr/share/nginx/html; - index index.html; - - # Limite upload per allegati (es. foto/video ticket) - Allineato con il backend - client_max_body_size 50M; - - # 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; - } - } -} diff --git a/pages/ExtraordinaryAdmin.tsx b/pages/ExtraordinaryAdmin.tsx new file mode 100644 index 0000000..8d9152c --- /dev/null +++ b/pages/ExtraordinaryAdmin.tsx @@ -0,0 +1,387 @@ +import React, { useEffect, useState } from 'react'; +import { CondoService } from '../services/mockDb'; +import { ExtraordinaryExpense, Family, ExpenseItem, ExpenseShare } from '../types'; +import { Plus, Calendar, FileText, CheckCircle2, Clock, Users, X, Save, Paperclip, Euro, Trash2, Eye, Briefcase } from 'lucide-react'; + +export const ExtraordinaryAdmin: React.FC = () => { + const [expenses, setExpenses] = useState([]); + const [families, setFamilies] = useState([]); + const [loading, setLoading] = useState(true); + const [showModal, setShowModal] = useState(false); + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [selectedExpense, setSelectedExpense] = useState(null); + + // Form State + const [formTitle, setFormTitle] = useState(''); + const [formDesc, setFormDesc] = useState(''); + const [formStart, setFormStart] = useState(''); + const [formEnd, setFormEnd] = useState(''); + const [formContractor, setFormContractor] = useState(''); + const [formItems, setFormItems] = useState([{ description: '', amount: 0 }]); + const [formShares, setFormShares] = useState([]); // Working state for shares + const [formAttachments, setFormAttachments] = useState<{fileName: string, fileType: string, data: string}[]>([]); + const [selectedFamilyIds, setSelectedFamilyIds] = useState([]); // Helper to track checkboxes + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const [expList, famList] = await Promise.all([ + CondoService.getExpenses(), + CondoService.getFamilies() + ]); + setExpenses(expList); + setFamilies(famList); + } catch(e) { console.error(e); } + finally { setLoading(false); } + }; + + // Calculation Helpers + const totalAmount = formItems.reduce((acc, item) => acc + (item.amount || 0), 0); + + const recalculateShares = (selectedIds: string[], manualMode = false) => { + if (manualMode) return; // If manually editing, don't auto-calc + + const count = selectedIds.length; + if (count === 0) { + setFormShares([]); + return; + } + + const percentage = 100 / count; + const newShares: ExpenseShare[] = selectedIds.map(fid => ({ + familyId: fid, + percentage: parseFloat(percentage.toFixed(2)), + amountDue: parseFloat(((totalAmount * percentage) / 100).toFixed(2)), + amountPaid: 0, + status: 'UNPAID' + })); + + // Adjust rounding error on last item + if (newShares.length > 0) { + const sumDue = newShares.reduce((a, b) => a + b.amountDue, 0); + const diff = totalAmount - sumDue; + if (diff !== 0) { + newShares[newShares.length - 1].amountDue += diff; + } + } + + setFormShares(newShares); + }; + + const handleFamilyToggle = (familyId: string) => { + const newSelected = selectedFamilyIds.includes(familyId) + ? selectedFamilyIds.filter(id => id !== familyId) + : [...selectedFamilyIds, familyId]; + + setSelectedFamilyIds(newSelected); + recalculateShares(newSelected); + }; + + const handleShareChange = (index: number, field: 'percentage', value: number) => { + const newShares = [...formShares]; + newShares[index].percentage = value; + newShares[index].amountDue = (totalAmount * value) / 100; + setFormShares(newShares); + }; + + const handleItemChange = (index: number, field: keyof ExpenseItem, value: any) => { + const newItems = [...formItems]; + // @ts-ignore + newItems[index][field] = value; + setFormItems(newItems); + // Recalculate shares based on new total + // We need a small delay or effect, but for simplicity let's force recalc next render or manual + }; + + // Trigger share recalc when total changes (if not manual override mode - implementing simple auto mode here) + useEffect(() => { + recalculateShares(selectedFamilyIds); + }, [totalAmount]); // eslint-disable-line react-hooks/exhaustive-deps + + const handleFileChange = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const newAtts = []; + for(let i=0; i((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(file); + }); + newAtts.push({ fileName: file.name, fileType: file.type, data: base64 }); + } + setFormAttachments([...formAttachments, ...newAtts]); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await CondoService.createExpense({ + title: formTitle, + description: formDesc, + startDate: formStart, + endDate: formEnd, + contractorName: formContractor, + items: formItems, + shares: formShares, + attachments: formAttachments + }); + setShowModal(false); + loadData(); + // Reset form + setFormTitle(''); setFormDesc(''); setFormItems([{description:'', amount:0}]); setSelectedFamilyIds([]); setFormShares([]); setFormAttachments([]); + } catch(e) { alert('Errore creazione'); } + }; + + const openDetails = async (expense: ExtraordinaryExpense) => { + const fullDetails = await CondoService.getExpenseDetails(expense.id); + setSelectedExpense(fullDetails); + setShowDetailsModal(true); + }; + + const openAttachment = async (expenseId: string, attId: string) => { + try { + const file = await CondoService.getExpenseAttachment(expenseId, attId); + const win = window.open(); + if (win) { + if (file.fileType.startsWith('image/') || file.fileType === 'application/pdf') { + win.document.write(``); + } else { + win.document.write(`Download ${file.fileName}`); + } + } + } catch(e) { alert("Errore file"); } + }; + + return ( +
+
+
+

Spese Straordinarie

+

Gestione lavori e appalti

+
+ +
+ + {/* List */} +
+ {expenses.map(exp => ( +
+
+ Lavori + € {exp.totalAmount.toLocaleString()} +
+

{exp.title}

+

{exp.description}

+ +
+
{exp.contractorName}
+
{new Date(exp.startDate).toLocaleDateString()}
+
+ + +
+ ))} +
+ + {/* CREATE MODAL */} + {showModal && ( +
+
+
+

Crea Progetto Straordinario

+ +
+ +
+
+ {/* General Info */} +
+ setFormTitle(e.target.value)} required /> + setFormContractor(e.target.value)} required /> +
+