diff --git a/.dockerignore b/.dockerignore index b3e90c2..feecb7c 100644 Binary files a/.dockerignore and b/.dockerignore differ diff --git a/components/Layout.tsx b/components/Layout.tsx index 1d526aa..719cdc7 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -3,7 +3,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 } from 'lucide-react'; import { CondoService } from '../services/mockDb'; -import { Condo, Notice } from '../types'; +import { Condo, Notice, AppSettings } from '../types'; export const Layout: React.FC = () => { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); @@ -13,25 +13,43 @@ export const Layout: React.FC = () => { const [condos, setCondos] = useState([]); const [activeCondo, setActiveCondo] = useState(undefined); const [showCondoDropdown, setShowCondoDropdown] = useState(false); + const [settings, setSettings] = useState(null); // Notice Modal State const [activeNotice, setActiveNotice] = useState(null); const fetchContext = async () => { - if (isAdmin) { - const list = await CondoService.getCondos(); - setCondos(list); - } + // Fetch global settings to check features + try { + const globalSettings = await CondoService.getSettings(); + setSettings(globalSettings); + + if (isAdmin && globalSettings.features.multiCondo) { + const list = await CondoService.getCondos(); + setCondos(list); + } else if (isAdmin) { + // If multi-condo disabled, just get the one (which acts as active) + const list = await CondoService.getCondos(); + setCondos(list); // Store list anyway, though dropdown will be hidden + } + } catch(e) { console.error("Error fetching settings", e); } + const active = await CondoService.getActiveCondo(); setActiveCondo(active); - // Check for notices for User (not admin, to avoid spamming admin managing multiple condos) + // 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) { - const unread = await CondoService.getUnreadNoticesForUser(user.id, active.id); - if (unread.length > 0) { - // Show the most recent unread notice - setActiveNotice(unread[0]); - } + try { + const unread = await CondoService.getUnreadNoticesForUser(user.id, active.id); + if (unread.length > 0) { + // Show the most recent unread notice + setActiveNotice(unread[0]); + } + } catch(e) {} } }; @@ -76,11 +94,14 @@ export const Layout: React.FC = () => { } }; + // Check if notices are actually enabled before showing modal + const showNotice = activeNotice && settings?.features.notices; + return (
{/* Active Notice Modal */} - {activeNotice && ( + {showNotice && activeNotice && (
@@ -147,8 +168,8 @@ export const Layout: React.FC = () => {

CondoPay

- {/* Condo Switcher (Admin Only) */} - {isAdmin && ( + {/* Condo Switcher (Admin Only & MultiCondo Enabled) */} + {isAdmin && settings?.features.multiCondo && (
@@ -178,7 +199,8 @@ export const Layout: React.FC = () => { )}
)} - {!isAdmin && activeCondo && ( + {/* Static info if not multi-condo or not admin */} + {(!isAdmin || (isAdmin && !settings?.features.multiCondo)) && activeCondo && (
{activeCondo.name}
@@ -192,7 +214,7 @@ export const Layout: React.FC = () => {
{/* Mobile Condo Switcher */} - {isAdmin && ( + {isAdmin && settings?.features.multiCondo && (

@@ -218,10 +240,13 @@ export const Layout: React.FC = () => { Famiglie - - - Segnalazioni - + {/* Hide Tickets if disabled */} + {settings?.features.tickets && ( + + + Segnalazioni + + )} diff --git a/pages/FamilyDetail.tsx b/pages/FamilyDetail.tsx index 542e657..be0e529 100644 --- a/pages/FamilyDetail.tsx +++ b/pages/FamilyDetail.tsx @@ -161,6 +161,8 @@ export const FamilyDetail: React.FC = () => { handlePaymentSuccess(); }; + const isPayPalEnabled = condo?.paypalClientId && settings?.features.payPal; + if (loading) return

Caricamento dettagli...
; if (!family) return
Famiglia non trovata.
; @@ -202,7 +204,7 @@ export const FamilyDetail: React.FC = () => {
) : (
- {condo?.paypalClientId && ( - + {isPayPalEnabled && ( + { diff --git a/pages/FamilyList.tsx b/pages/FamilyList.tsx index 2241b1d..3e2bbe8 100644 --- a/pages/FamilyList.tsx +++ b/pages/FamilyList.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { CondoService } from '../services/mockDb'; -import { Family, Condo, Notice } from '../types'; +import { Family, Condo, Notice, AppSettings } from '../types'; import { Search, ChevronRight, UserCircle, Building, Bell, AlertTriangle, Hammer, Calendar, Info, Link as LinkIcon, Check } from 'lucide-react'; export const FamilyList: React.FC = () => { @@ -12,21 +12,24 @@ export const FamilyList: React.FC = () => { const [activeCondo, setActiveCondo] = useState(undefined); const [notices, setNotices] = useState([]); const [userReadIds, setUserReadIds] = useState([]); + const [settings, setSettings] = useState(null); const currentUser = CondoService.getCurrentUser(); useEffect(() => { const fetchData = async () => { try { CondoService.seedPayments(); - const [fams, condo, allNotices] = await Promise.all([ + const [fams, condo, allNotices, appSettings] = await Promise.all([ CondoService.getFamilies(), CondoService.getActiveCondo(), - CondoService.getNotices() + CondoService.getNotices(), + CondoService.getSettings() ]); setFamilies(fams); setActiveCondo(condo); + setSettings(appSettings); - if (condo && currentUser) { + if (condo && currentUser && appSettings.features.notices) { const condoNotices = allNotices.filter(n => n.condoId === condo.id && n.active); setNotices(condoNotices); @@ -104,8 +107,8 @@ export const FamilyList: React.FC = () => {
- {/* Notices Section (Visible to Users) */} - {notices.length > 0 && ( + {/* Notices Section (Visible to Users only if feature enabled) */} + {settings?.features.notices && notices.length > 0 && (

Bacheca Avvisi @@ -179,4 +182,4 @@ export const FamilyList: React.FC = () => {

); -}; +}; \ No newline at end of file diff --git a/pages/Settings.tsx b/pages/Settings.tsx index 2d2516c..dcfe311 100644 --- a/pages/Settings.tsx +++ b/pages/Settings.tsx @@ -2,14 +2,14 @@ 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, CreditCard } 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, ToggleLeft, ToggleRight, LayoutGrid } from 'lucide-react'; export const SettingsPage: React.FC = () => { const currentUser = CondoService.getCurrentUser(); const isAdmin = currentUser?.role === 'admin'; // Tab configuration - type TabType = 'profile' | 'general' | 'condos' | 'families' | 'users' | 'notices' | 'alerts' | 'smtp'; + type TabType = 'profile' | 'features' | 'general' | 'condos' | 'families' | 'users' | 'notices' | 'alerts' | 'smtp'; const [activeTab, setActiveTab] = useState(isAdmin ? 'general' : 'profile'); const [loading, setLoading] = useState(true); @@ -204,6 +204,18 @@ export const SettingsPage: React.FC = () => { } }; + const handleFeaturesSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!globalSettings) return; + setSaving(true); + try { + await CondoService.updateSettings(globalSettings); + setSuccessMsg('Funzionalità salvate!'); + setTimeout(() => setSuccessMsg(''), 3000); + window.location.reload(); // Refresh to apply changes to layout + } catch(e) { console.error(e); } finally { setSaving(false); } + }; + const handleSmtpSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!globalSettings) return; @@ -463,6 +475,18 @@ export const SettingsPage: React.FC = () => { const getCondoName = (id: string) => condos.find(c => c.id === id)?.name || 'Sconosciuto'; + + // Helpers for Toggle Feature + const toggleFeature = (key: keyof AppSettings['features']) => { + if (!globalSettings) return; + setGlobalSettings({ + ...globalSettings, + features: { + ...globalSettings.features, + [key]: !globalSettings.features[key] + } + }); + }; // --- TABS CONFIG --- const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [ @@ -470,11 +494,24 @@ export const SettingsPage: React.FC = () => { ]; if (isAdmin) { tabs.push( - { id: 'general', label: 'Condominio', icon: }, - { id: 'condos', label: 'Lista Condomini', icon: }, + { id: 'features', label: 'Funzionalità', icon: }, + { id: 'general', label: 'Condominio', icon: } + ); + + if (globalSettings?.features.multiCondo) { + tabs.push({ id: 'condos', label: 'Lista Condomini', icon: }); + } + + tabs.push( { id: 'families', label: 'Famiglie', icon: }, - { id: 'users', label: 'Utenti', icon: }, - { id: 'notices', label: 'Bacheca', icon: }, + { id: 'users', label: 'Utenti', icon: } + ); + + if (globalSettings?.features.notices) { + tabs.push({ id: 'notices', label: 'Bacheca', icon: }); + } + + tabs.push( { id: 'alerts', label: 'Avvisi Email', icon: }, { id: 'smtp', label: 'SMTP', icon: } ); @@ -523,11 +560,77 @@ export const SettingsPage: React.FC = () => {
)} + {/* Features Tab */} + {isAdmin && activeTab === 'features' && globalSettings && ( +
+
+

+ Funzionalità Piattaforma +

+
+ +
+
+ {/* Multi Condo */} +
+
+

Gestione Multicondominio

+

Abilita la gestione di più stabili. Se disattivo, il sistema gestirà un solo condominio.

+
+ +
+ + {/* Tickets */} +
+
+

Gestione Tickets

+

Abilita il sistema di segnalazione guasti e richieste (Segnalazioni).

+
+ +
+ + {/* PayPal */} +
+
+

Pagamenti PayPal

+

Permetti ai condomini di pagare le rate tramite PayPal.

+
+ +
+ + {/* Notices */} +
+
+

Bacheca Avvisi

+

Mostra la bacheca digitale per comunicazioni ai condomini.

+
+ +
+
+ +
+ {successMsg} + +
+
+
+ )} + {/* General Tab */} {isAdmin && activeTab === 'general' && (
{!activeCondo ? ( -
Nessun condominio selezionato. Crea un condominio nella sezione "Lista Condomini".
+
Nessun condominio selezionato.
) : (

Dati Condominio Corrente

@@ -610,11 +713,6 @@ 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' && (
@@ -994,22 +1092,24 @@ export const SettingsPage: React.FC = () => {
{/* PayPal Integration Section */} -
-
- - Configurazione Pagamenti + {globalSettings?.features.payPal && ( +
+
+ + Configurazione Pagamenti +
+
+ + setCondoForm({...condoForm, paypalClientId: e.target.value})} + /> +

Necessario per abilitare i pagamenti online delle rate.

+
-
- - setCondoForm({...condoForm, paypalClientId: e.target.value})} - /> -

Necessario per abilitare i pagamenti online delle rate.

-
-
+ )} {/* Notes */}
diff --git a/server/db.js b/server/db.js index a3664bf..24790ed 100644 --- a/server/db.js +++ b/server/db.js @@ -67,10 +67,28 @@ const initDb = async () => { CREATE TABLE IF NOT EXISTS settings ( id INT PRIMARY KEY, current_year INT, - smtp_config JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'} + smtp_config JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'}, + features JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'} ) `); + // Migration: Add features column if not exists + try { + let hasFeatures = false; + if (DB_CLIENT === 'postgres') { + const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='settings'"); + hasFeatures = cols.some(c => c.column_name === 'features'); + } else { + const [cols] = await connection.query("SHOW COLUMNS FROM settings"); + hasFeatures = cols.some(c => c.Field === 'features'); + } + + if (!hasFeatures) { + console.log('Migrating: Adding features to settings...'); + await connection.query("ALTER TABLE settings ADD COLUMN features JSON"); + } + } catch(e) { console.warn("Settings migration warning:", e.message); } + // 1. Condos Table await connection.query(` CREATE TABLE IF NOT EXISTS condos ( @@ -300,12 +318,25 @@ const initDb = async () => { // --- SEEDING --- const [rows] = await connection.query('SELECT * FROM settings WHERE id = 1'); + const defaultFeatures = { + multiCondo: true, + tickets: true, + payPal: true, + notices: true + }; + if (rows.length === 0) { const currentYear = new Date().getFullYear(); await connection.query( - 'INSERT INTO settings (id, current_year) VALUES (1, ?)', - [currentYear] + 'INSERT INTO settings (id, current_year, features) VALUES (1, ?, ?)', + [currentYear, JSON.stringify(defaultFeatures)] ); + } else { + // Ensure features column has defaults if null + if (!rows[0].features) { + await connection.query('UPDATE settings SET features = ? WHERE id = 1', [JSON.stringify(defaultFeatures)]); + console.log("Seeded default features settings."); + } } // ENSURE ADMIN EXISTS AND HAS CORRECT ROLE @@ -332,4 +363,4 @@ const initDb = async () => { } }; -module.exports = { pool: dbInterface, initDb }; \ No newline at end of file +module.exports = { pool: dbInterface, initDb }; diff --git a/server/server.js b/server/server.js index 7a7169a..c19835b 100644 --- a/server/server.js +++ b/server/server.js @@ -144,14 +144,21 @@ app.get('/api/settings', authenticateToken, async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM settings WHERE id = 1'); if (rows.length > 0) { - res.json({ currentYear: rows[0].current_year, smtpConfig: rows[0].smtp_config || {} }); + res.json({ + currentYear: rows[0].current_year, + smtpConfig: rows[0].smtp_config || {}, + features: rows[0].features || { multiCondo: true, tickets: true, payPal: true, notices: true } + }); } else { res.status(404).json({ message: 'Settings not found' }); } } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/settings', authenticateToken, requireAdmin, async (req, res) => { - const { currentYear, smtpConfig } = req.body; + const { currentYear, smtpConfig, features } = req.body; try { - await pool.query('UPDATE settings SET current_year = ?, smtp_config = ? WHERE id = 1', [currentYear, JSON.stringify(smtpConfig)]); + await pool.query( + 'UPDATE settings SET current_year = ?, smtp_config = ?, features = ? WHERE id = 1', + [currentYear, JSON.stringify(smtpConfig), JSON.stringify(features)] + ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); @@ -664,4 +671,4 @@ initDb().then(() => { app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); -}); \ No newline at end of file +}); diff --git a/types.ts b/types.ts index 885959b..7796c8d 100644 --- a/types.ts +++ b/types.ts @@ -46,6 +46,13 @@ export interface SmtpConfig { fromEmail: string; } +export interface AppFeatures { + multiCondo: boolean; + tickets: boolean; + payPal: boolean; + notices: boolean; +} + export interface AlertDefinition { id: string; subject: string; @@ -80,6 +87,7 @@ export interface AppSettings { // Global settings only currentYear: number; // The active fiscal year (could be per-condo, but global for simplicity now) smtpConfig?: SmtpConfig; + features: AppFeatures; } export enum PaymentStatus {