From 919be985c9f91b42237b96202661b7552afc4d4f Mon Sep 17 00:00:00 2001 From: frakarr Date: Sun, 7 Dec 2025 20:21:01 +0100 Subject: [PATCH] feat: Introduce app feature flags This commit refactors the application settings to include a new `AppFeatures` interface. This allows for granular control over which features are enabled for the application. The `AppFeatures` object includes boolean flags for: - `multiCondo`: Enables or disables the multi-condominium management feature. - `tickets`: Placeholder for future ticket system integration. - `payPal`: Enables or disables PayPal payment gateway integration. - `notices`: Enables or disables the display and management of notices. These flags are now fetched and stored in the application state, influencing UI elements and logic across various pages to conditionally render features based on their enabled status. For example, the multi-condo selection in `Layout.tsx` and the notice display in `FamilyList.tsx` are now gated by these feature flags. The `FamilyDetail.tsx` page also uses the `payPal` flag to conditionally enable the PayPal payment option. The `SettingsPage.tsx` has been updated to include a new 'features' tab for managing these flags. --- .dockerignore | Bin 105 -> 123 bytes components/Layout.tsx | 65 +++++++++++------ pages/FamilyDetail.tsx | 12 ++-- pages/FamilyList.tsx | 17 +++-- pages/Settings.tsx | 154 +++++++++++++++++++++++++++++++++-------- server/db.js | 39 +++++++++-- server/server.js | 15 ++-- types.ts | 8 +++ 8 files changed, 243 insertions(+), 67 deletions(-) diff --git a/.dockerignore b/.dockerignore index b3e90c29e43ec3e8cab67b74444927db2400e19e..feecb7ce76f0eb8519a12fa6a453dac8f3d7e733 100644 GIT binary patch literal 123 zcmaFAfA9PKd*gsOOBP6k196$QE)xfkW&~m&VlV*`UO=o}2_$5I7=nu6EC?gh8A8j! O#jB35hO;3I6ng;g3{@Ba literal 105 zcmaFAfA9PKd*gsOOBP6k196$QZXS>-;ZOpS>_7}e3=m2?147F|C { 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 {