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.
This commit is contained in:
BIN
.dockerignore
BIN
.dockerignore
Binary file not shown.
@@ -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<Condo[]>([]);
|
||||
const [activeCondo, setActiveCondo] = useState<Condo | undefined>(undefined);
|
||||
const [showCondoDropdown, setShowCondoDropdown] = useState(false);
|
||||
const [settings, setSettings] = useState<AppSettings | null>(null);
|
||||
|
||||
// Notice Modal State
|
||||
const [activeNotice, setActiveNotice] = useState<Notice | null>(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 (
|
||||
<div className="flex h-screen bg-slate-50 overflow-hidden">
|
||||
|
||||
{/* Active Notice Modal */}
|
||||
{activeNotice && (
|
||||
{showNotice && activeNotice && (
|
||||
<div className="fixed inset-0 bg-black/60 z-[100] flex items-center justify-center p-4 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full overflow-hidden transform transition-all scale-100">
|
||||
<div className={`p-6 ${activeNotice.type === 'warning' ? 'bg-amber-50' : 'bg-blue-50'} border-b border-slate-100 flex items-start gap-4`}>
|
||||
@@ -147,8 +168,8 @@ export const Layout: React.FC = () => {
|
||||
<h1 className="font-bold text-xl text-slate-800 tracking-tight">CondoPay</h1>
|
||||
</div>
|
||||
|
||||
{/* Condo Switcher (Admin Only) */}
|
||||
{isAdmin && (
|
||||
{/* Condo Switcher (Admin Only & MultiCondo Enabled) */}
|
||||
{isAdmin && settings?.features.multiCondo && (
|
||||
<div className="relative mt-2">
|
||||
<div className="flex items-center gap-1.5 mb-2 text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||
<LayoutDashboard className="w-3 h-3" />
|
||||
@@ -178,7 +199,8 @@ export const Layout: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isAdmin && activeCondo && (
|
||||
{/* Static info if not multi-condo or not admin */}
|
||||
{(!isAdmin || (isAdmin && !settings?.features.multiCondo)) && activeCondo && (
|
||||
<div className="px-3 py-2 bg-slate-50 border border-slate-100 rounded-lg text-sm text-slate-500 truncate">
|
||||
{activeCondo.name}
|
||||
</div>
|
||||
@@ -192,7 +214,7 @@ export const Layout: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Mobile Condo Switcher */}
|
||||
{isAdmin && (
|
||||
{isAdmin && settings?.features.multiCondo && (
|
||||
<div className="lg:hidden px-4 py-2 border-b border-slate-100">
|
||||
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<LayoutDashboard className="w-3 h-3" />
|
||||
@@ -218,10 +240,13 @@ export const Layout: React.FC = () => {
|
||||
<span className="font-medium">Famiglie</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to="/tickets" className={navClass} onClick={closeMenu}>
|
||||
<MessageSquareWarning className="w-5 h-5" />
|
||||
<span className="font-medium">Segnalazioni</span>
|
||||
</NavLink>
|
||||
{/* Hide Tickets if disabled */}
|
||||
{settings?.features.tickets && (
|
||||
<NavLink to="/tickets" className={navClass} onClick={closeMenu}>
|
||||
<MessageSquareWarning className="w-5 h-5" />
|
||||
<span className="font-medium">Segnalazioni</span>
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
<NavLink to="/settings" className={navClass} onClick={closeMenu}>
|
||||
<Settings className="w-5 h-5" />
|
||||
|
||||
@@ -161,6 +161,8 @@ export const FamilyDetail: React.FC = () => {
|
||||
handlePaymentSuccess();
|
||||
};
|
||||
|
||||
const isPayPalEnabled = condo?.paypalClientId && settings?.features.payPal;
|
||||
|
||||
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>;
|
||||
|
||||
@@ -202,7 +204,7 @@ export const FamilyDetail: React.FC = () => {
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setPaymentMethod(condo?.paypalClientId ? 'paypal' : 'manual');
|
||||
setPaymentMethod(isPayPalEnabled ? 'paypal' : 'manual');
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
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"
|
||||
@@ -305,7 +307,7 @@ export const FamilyDetail: React.FC = () => {
|
||||
<button
|
||||
onClick={() => {
|
||||
setNewPaymentMonth(month.monthIndex + 1);
|
||||
setPaymentMethod(condo?.paypalClientId ? 'paypal' : 'manual');
|
||||
setPaymentMethod(isPayPalEnabled ? 'paypal' : 'manual');
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
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"
|
||||
@@ -376,7 +378,7 @@ export const FamilyDetail: React.FC = () => {
|
||||
|
||||
<div className="p-6">
|
||||
{/* Payment Method Switcher */}
|
||||
{condo?.paypalClientId && (
|
||||
{isPayPalEnabled && (
|
||||
<div className="flex bg-slate-100 rounded-lg p-1 mb-6">
|
||||
<button
|
||||
onClick={() => setPaymentMethod('manual')}
|
||||
@@ -452,8 +454,8 @@ export const FamilyDetail: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-h-[150px] flex items-center justify-center">
|
||||
{condo?.paypalClientId && (
|
||||
<PayPalScriptProvider options={{ clientId: condo.paypalClientId, currency: "EUR" }}>
|
||||
{isPayPalEnabled && (
|
||||
<PayPalScriptProvider options={{ clientId: condo.paypalClientId!, currency: "EUR" }}>
|
||||
<PayPalButtons
|
||||
style={{ layout: "vertical" }}
|
||||
createOrder={(data, actions) => {
|
||||
|
||||
@@ -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<Condo | undefined>(undefined);
|
||||
const [notices, setNotices] = useState<Notice[]>([]);
|
||||
const [userReadIds, setUserReadIds] = useState<string[]>([]);
|
||||
const [settings, setSettings] = useState<AppSettings | null>(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 = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notices Section (Visible to Users) */}
|
||||
{notices.length > 0 && (
|
||||
{/* Notices Section (Visible to Users only if feature enabled) */}
|
||||
{settings?.features.notices && notices.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-slate-700 flex items-center gap-2">
|
||||
<Bell className="w-5 h-5" /> Bacheca Avvisi
|
||||
|
||||
@@ -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<TabType>(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;
|
||||
@@ -464,17 +476,42 @@ 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}[] = [
|
||||
{ id: 'profile', label: 'Profilo', icon: <UserIcon className="w-4 h-4"/> },
|
||||
];
|
||||
if (isAdmin) {
|
||||
tabs.push(
|
||||
{ id: 'general', label: 'Condominio', icon: <Building className="w-4 h-4"/> },
|
||||
{ id: 'condos', label: 'Lista Condomini', icon: <List className="w-4 h-4"/> },
|
||||
{ id: 'features', label: 'Funzionalità', icon: <LayoutGrid className="w-4 h-4"/> },
|
||||
{ id: 'general', label: 'Condominio', icon: <Building className="w-4 h-4"/> }
|
||||
);
|
||||
|
||||
if (globalSettings?.features.multiCondo) {
|
||||
tabs.push({ id: 'condos', label: 'Lista Condomini', icon: <List className="w-4 h-4"/> });
|
||||
}
|
||||
|
||||
tabs.push(
|
||||
{ id: 'families', label: 'Famiglie', icon: <Coins className="w-4 h-4"/> },
|
||||
{ id: 'users', label: 'Utenti', icon: <UserCog className="w-4 h-4"/> },
|
||||
{ id: 'notices', label: 'Bacheca', icon: <Megaphone className="w-4 h-4"/> },
|
||||
{ id: 'users', label: 'Utenti', icon: <UserCog className="w-4 h-4"/> }
|
||||
);
|
||||
|
||||
if (globalSettings?.features.notices) {
|
||||
tabs.push({ id: 'notices', label: 'Bacheca', icon: <Megaphone className="w-4 h-4"/> });
|
||||
}
|
||||
|
||||
tabs.push(
|
||||
{ id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> },
|
||||
{ id: 'smtp', label: 'SMTP', icon: <Mail className="w-4 h-4"/> }
|
||||
);
|
||||
@@ -523,11 +560,77 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features Tab */}
|
||||
{isAdmin && activeTab === 'features' && globalSettings && (
|
||||
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6 max-w-3xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
||||
<LayoutGrid className="w-5 h-5 text-blue-600" /> Funzionalità Piattaforma
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFeaturesSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* Multi Condo */}
|
||||
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<div>
|
||||
<p className="font-bold text-slate-800">Gestione Multicondominio</p>
|
||||
<p className="text-sm text-slate-500">Abilita la gestione di più stabili. Se disattivo, il sistema gestirà un solo condominio.</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => toggleFeature('multiCondo')} className={`${globalSettings.features.multiCondo ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}>
|
||||
<span className={`${globalSettings.features.multiCondo ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tickets */}
|
||||
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<div>
|
||||
<p className="font-bold text-slate-800">Gestione Tickets</p>
|
||||
<p className="text-sm text-slate-500">Abilita il sistema di segnalazione guasti e richieste (Segnalazioni).</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => toggleFeature('tickets')} className={`${globalSettings.features.tickets ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}>
|
||||
<span className={`${globalSettings.features.tickets ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* PayPal */}
|
||||
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<div>
|
||||
<p className="font-bold text-slate-800">Pagamenti PayPal</p>
|
||||
<p className="text-sm text-slate-500">Permetti ai condomini di pagare le rate tramite PayPal.</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => toggleFeature('payPal')} className={`${globalSettings.features.payPal ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}>
|
||||
<span className={`${globalSettings.features.payPal ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Notices */}
|
||||
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<div>
|
||||
<p className="font-bold text-slate-800">Bacheca Avvisi</p>
|
||||
<p className="text-sm text-slate-500">Mostra la bacheca digitale per comunicazioni ai condomini.</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => toggleFeature('notices')} className={`${globalSettings.features.notices ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}>
|
||||
<span className={`${globalSettings.features.notices ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex justify-between items-center">
|
||||
<span className="text-green-600 font-medium">{successMsg}</span>
|
||||
<button type="submit" className="bg-blue-600 text-white px-6 py-2.5 rounded-lg font-medium hover:bg-blue-700 flex gap-2">
|
||||
<Save className="w-4 h-4" /> Salva Configurazione
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* General Tab */}
|
||||
{isAdmin && activeTab === 'general' && (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{!activeCondo ? (
|
||||
<div className="bg-amber-50 border border-amber-200 text-amber-800 p-4 rounded-lg">Nessun condominio selezionato. Crea un condominio nella sezione "Lista Condomini".</div>
|
||||
<div className="bg-amber-50 border border-amber-200 text-amber-800 p-4 rounded-lg">Nessun condominio selezionato.</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8 max-w-2xl">
|
||||
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2"><Building className="w-5 h-5 text-blue-600" /> Dati Condominio Corrente</h3>
|
||||
@@ -610,11 +713,6 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
@@ -994,22 +1092,24 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* PayPal Integration Section */}
|
||||
<div className="bg-blue-50 p-3 rounded-lg border border-blue-100">
|
||||
<div className="flex items-center gap-2 mb-2 text-blue-800">
|
||||
<CreditCard className="w-4 h-4"/>
|
||||
<span className="text-xs font-bold uppercase">Configurazione Pagamenti</span>
|
||||
{globalSettings?.features.payPal && (
|
||||
<div className="bg-blue-50 p-3 rounded-lg border border-blue-100">
|
||||
<div className="flex items-center gap-2 mb-2 text-blue-800">
|
||||
<CreditCard className="w-4 h-4"/>
|
||||
<span className="text-xs font-bold uppercase">Configurazione Pagamenti</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-slate-600 block mb-1">PayPal Client ID (REST API App)</label>
|
||||
<input
|
||||
className="w-full border p-2 rounded text-slate-700 text-sm"
|
||||
placeholder="Es: Afg... (Ottienilo da developer.paypal.com)"
|
||||
value={condoForm.paypalClientId}
|
||||
onChange={e => setCondoForm({...condoForm, paypalClientId: e.target.value})}
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 mt-1">Necessario per abilitare i pagamenti online delle rate.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-slate-600 block mb-1">PayPal Client ID (REST API App)</label>
|
||||
<input
|
||||
className="w-full border p-2 rounded text-slate-700 text-sm"
|
||||
placeholder="Es: Afg... (Ottienilo da developer.paypal.com)"
|
||||
value={condoForm.paypalClientId}
|
||||
onChange={e => setCondoForm({...condoForm, paypalClientId: e.target.value})}
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 mt-1">Necessario per abilitare i pagamenti online delle rate.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
|
||||
37
server/db.js
37
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
|
||||
|
||||
@@ -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 }); }
|
||||
});
|
||||
|
||||
8
types.ts
8
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 {
|
||||
|
||||
Reference in New Issue
Block a user