diff --git a/App.tsx b/App.tsx index 93b7268..9266fd7 100644 --- a/App.tsx +++ b/App.tsx @@ -1,5 +1,5 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { HashRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { Layout } from './components/Layout'; import { FamilyList } from './pages/FamilyList'; @@ -13,6 +13,49 @@ import { CondoFinancialsPage } from './pages/CondoFinancials.tsx'; import { DocumentsPage } from './pages/Documents.tsx'; import { LoginPage } from './pages/Login'; import { CondoService } from './services/mockDb'; +import { BrandingConfig } from './types'; + +// Palette predefinite basate su chiavi stringa +const COLOR_PALETTES: Record = { + blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' }, + purple: { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' }, + green: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16' }, + red: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a' }, + orange: { 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' }, + slate: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' } +}; + +/** + * Genera una palette completa Tailwind (50-950) partendo da un singolo colore HEX. + */ +function generatePaletteFromHex(hex: string) { + // Rimuoviamo eventuale # se presente + const cleanHex = hex.replace('#', ''); + const r = parseInt(cleanHex.slice(0, 2), 16); + const g = parseInt(cleanHex.slice(2, 4), 16); + const b = parseInt(cleanHex.slice(4, 6), 16); + + const adjust = (color: number, amount: number) => { + return Math.max(0, Math.min(255, Math.round(color + (amount * (amount > 0 ? 255 - color : color))))); + }; + + const toHex = (c: number) => c.toString(16).padStart(2, '0'); + const getHex = (amt: number) => `#${toHex(adjust(r, amt))}${toHex(adjust(g, amt))}${toHex(adjust(b, amt))}`; + + return { + 50: getHex(0.95), + 100: getHex(0.85), + 200: getHex(0.7), + 300: getHex(0.5), + 400: getHex(0.3), + 500: `#${cleanHex}`, + 600: getHex(-0.1), + 700: getHex(-0.25), + 800: getHex(-0.45), + 900: getHex(-0.6), + 950: getHex(-0.8) + }; +} const ProtectedRoute = ({ children }: { children?: React.ReactNode }) => { const user = CondoService.getCurrentUser(); @@ -25,27 +68,63 @@ 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 = () => { + const [branding, setBranding] = useState({ appName: 'CondoPay', primaryColor: 'blue', logoUrl: '', loginBackgroundUrl: '' }); + + const applyBranding = (config: BrandingConfig) => { + // Merge with defaults ensures properties like logoUrl aren't undefined if API returns partial object + const mergedConfig = { appName: 'CondoPay', primaryColor: 'blue', logoUrl: '', loginBackgroundUrl: '', ...config }; + setBranding(mergedConfig); + document.title = mergedConfig.appName || 'CondoPay Manager'; + + let palette; + if (mergedConfig.primaryColor && mergedConfig.primaryColor.startsWith('#')) { + palette = generatePaletteFromHex(mergedConfig.primaryColor); + } else { + palette = COLOR_PALETTES[mergedConfig.primaryColor] || COLOR_PALETTES['blue']; + } + + const root = document.documentElement; + Object.keys(palette).forEach(key => { + root.style.setProperty(`--color-primary-${key}`, palette[key]); + }); + }; + + useEffect(() => { + const loadBranding = async () => { + try { + const config = await CondoService.getPublicBranding(); + if (config) { + applyBranding(config); + } + } catch(e) { console.error("Error loading branding", e); } + }; + loadBranding(); + + // Listen for branding updates from settings without full reload + const handleUpdate = () => loadBranding(); + window.addEventListener('branding-updated', handleUpdate); + return () => window.removeEventListener('branding-updated', handleUpdate); + }, []); + return ( - } /> + } /> - + }> } /> diff --git a/components/Layout.tsx b/components/Layout.tsx index f1b0620..1ee6d9d 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -3,14 +3,17 @@ 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, Briefcase, ReceiptEuro, FileText } from 'lucide-react'; import { CondoService } from '../services/mockDb'; -import { Condo, Notice, AppSettings } from '../types'; +import { Condo, Notice, AppSettings, BrandingConfig } from '../types'; -export const Layout: React.FC = () => { +interface LayoutProps { + branding?: BrandingConfig; +} + +export const Layout: React.FC = ({ branding }) => { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const user = CondoService.getCurrentUser(); // Logic: "isPrivileged" includes Admin AND PowerUser. - // This allows PowerUsers to see Reports and other admin-like features. const isPrivileged = user?.role === 'admin' || user?.role === 'poweruser'; const [condos, setCondos] = useState([]); @@ -26,7 +29,6 @@ export const Layout: React.FC = () => { const [ticketBadgeCount, setTicketBadgeCount] = useState(0); const fetchContext = async () => { - // Fetch global settings to check features try { const globalSettings = await CondoService.getSettings(); setSettings(globalSettings); @@ -49,7 +51,7 @@ export const Layout: React.FC = () => { // 1. Tickets Badge Logic try { - if (settings?.features.tickets || true) { // Check features if available or default + if (settings?.features.tickets || true) { const tickets = await CondoService.getTickets(); let count = 0; @@ -59,7 +61,6 @@ export const Layout: React.FC = () => { const isArchived = t.status === 'RESOLVED' || t.status === 'CLOSED'; if (isPrivileged) { - // Admin/PowerUser: Count new unarchived tickets OR tickets with new comments from users if (isTicketNew && !isArchived) { count++; } else { @@ -71,7 +72,6 @@ export const Layout: React.FC = () => { } } } else { - // User: Count tickets with new comments from Admin (or others) const updatedDate = new Date(t.updatedAt).getTime(); if (updatedDate > lastViewedTickets) { const comments = await CondoService.getTicketComments(t.id); @@ -85,21 +85,17 @@ export const Layout: React.FC = () => { } catch(e) { console.error("Error calc ticket badges", e); } - // Check for notices & expenses for User (non-privileged mostly, but logic works for all if needed) if (!isPrivileged && active && user) { try { - // 2. Check Notices const unread = await CondoService.getUnreadNoticesForUser(user.id, active.id); if (unread.length > 0) { setActiveNotice(unread[0]); } - // 3. Check New Extraordinary Expenses const myExpenses = await CondoService.getMyExpenses(); const lastViewed = localStorage.getItem('lastViewedExpensesTime'); const lastViewedTime = lastViewed ? parseInt(lastViewed) : 0; - // Count expenses created AFTER the last visit const count = myExpenses.filter((e: any) => new Date(e.createdAt).getTime() > lastViewedTime).length; setNewExpensesCount(count); @@ -109,12 +105,10 @@ export const Layout: React.FC = () => { useEffect(() => { fetchContext(); - - // Listen for updates from Settings or Expense views const handleUpdate = () => fetchContext(); window.addEventListener('condo-updated', handleUpdate); window.addEventListener('expenses-viewed', handleUpdate); - window.addEventListener('tickets-viewed', handleUpdate); // Listen for ticket view + window.addEventListener('tickets-viewed', handleUpdate); return () => { window.removeEventListener('condo-updated', handleUpdate); window.removeEventListener('expenses-viewed', handleUpdate); @@ -134,8 +128,6 @@ export const Layout: React.FC = () => { } }; - const closeNoticeModal = () => setActiveNotice(null); - const navClass = ({ isActive }: { isActive: boolean }) => `flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 ${ isActive @@ -154,9 +146,12 @@ export const Layout: React.FC = () => { } }; - // Check if notices are actually enabled before showing modal const showNotice = activeNotice && settings?.features.notices; + // Use props passed from App.tsx directly + const appName = branding?.appName || 'CondoPay'; + const logoUrl = branding?.logoUrl; + return (
@@ -195,11 +190,15 @@ export const Layout: React.FC = () => { {/* Mobile Header */}
-
- -
+ {logoUrl ? ( + Logo + ) : ( +
+ +
+ )}
-

CondoPay

+

{appName}

{activeCondo &&

{activeCondo.name}

}
@@ -221,10 +220,14 @@ export const Layout: React.FC = () => { {/* Desktop Logo & Condo Switcher */}
-
- -
-

CondoPay

+ {logoUrl ? ( + Logo + ) : ( +
+ +
+ )} +

{appName}

{/* Condo Switcher (Privileged Only & MultiCondo Enabled) */} diff --git a/docker-compose.yml b/docker-compose.yml index f5093e9..2dd2853 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: build: . restart: always ports: - - "8080:80" + - "${EXT_PORT}:80" depends_on: - backend diff --git a/index.html b/index.html index 9d0c1f4..c1df09f 100644 --- a/index.html +++ b/index.html @@ -1,3 +1,4 @@ + @@ -5,8 +6,46 @@ CondoPay Manager +