diff --git a/src/components/Logo.jsx b/src/components/Logo.jsx index 80a98f5..4564b68 100644 --- a/src/components/Logo.jsx +++ b/src/components/Logo.jsx @@ -6,7 +6,7 @@ const LOGO_SRC = '/Doormile-logo.png'; const NAVBAR_MARK_SRC = '/navbarLogo.png'; -export default function Logo({ onDark = false, compact = false, height = 26, style = {} }) { +export default function Logo({ onDark = false, compact = false, height = 26, size = 64, style = {} }) { if (compact) { return (
@@ -14,8 +14,8 @@ export default function Logo({ onDark = false, compact = false, height = 26, sty src={NAVBAR_MARK_SRC} alt="Doormile" style={{ - width: 64, - height: 64, + width: size, + height: size, display: 'block', objectFit: 'cover', flexShrink: 0 diff --git a/src/layout/MainLayout/Header.jsx b/src/layout/MainLayout/Header.jsx index 98057d8..a160bca 100644 --- a/src/layout/MainLayout/Header.jsx +++ b/src/layout/MainLayout/Header.jsx @@ -8,7 +8,7 @@ import Logo from '@/components/Logo'; import UserAvatar from '@/components/UserAvatar'; import { fetchUsers } from '@/utils/apiClient'; -export default function Header() { +export default function Header({ isSidebarCollapsed }) { const navigate = useNavigate(); let storedUserObj = { name: 'Admin', role: 'Operations Admin', id: 0 }; @@ -18,9 +18,6 @@ export default function Header() { } catch (e) {} const [activeUserName, setActiveUserName] = useState(storedUserObj.name || 'Admin'); - const displayRole = storedUserObj.role === 'admin' ? 'Administrator' : - storedUserObj.role === 'manager' ? 'Manager' : - storedUserObj.role === 'executive' ? 'Executive' : 'Operations Admin'; useEffect(() => { fetchUsers().then((users) => { @@ -36,7 +33,6 @@ export default function Header() { }, []); return ( - <> } - heading="Doormile CRM" - subheading={displayRole} + logo={ + + } headingHref="/dashboard" /> } @@ -74,6 +78,5 @@ export default function Header() {
} /> - ); } diff --git a/src/layout/MainLayout/Sidebar.jsx b/src/layout/MainLayout/Sidebar.jsx index a33bc85..f5305b2 100644 --- a/src/layout/MainLayout/Sidebar.jsx +++ b/src/layout/MainLayout/Sidebar.jsx @@ -6,10 +6,12 @@ import navItems from '@/menu/navItems'; // ==============================|| DOORMILE - SIDE NAV ||============================== // // Thin wrapper around Astryx's SideNav template: sections + items driven by navItems. -// Branding lives in the TopNav heading, so this stays icon+label only (Astryx guidance: -// avoid a SideNavHeading when TopNav already carries app identity). +// Branding lives in the TopNav logo only — a second logo here (above the nav items) +// duplicated it, so this stays icon+label only. Collapse state is controlled from +// MainLayout so the TopNav logo can track it (round mark collapsed, full wordmark +// expanded). -export default function Sidebar() { +export default function Sidebar({ isCollapsed, onCollapsedChange }) { const location = useLocation(); const isActive = (url) => !!url && location.pathname.startsWith(url); @@ -17,7 +19,7 @@ export default function Sidebar() { <> div:last-child { + padding-block: 8px !important; + } + /* Collapse/expand toggle (the "<" / ">" chevron button). It sits right on the sidebar's edge, so the default square ghost-button hover clips against that border and looks broken. Give it a diff --git a/src/layout/MainLayout/index.jsx b/src/layout/MainLayout/index.jsx index 5504d9f..dd135d5 100644 --- a/src/layout/MainLayout/index.jsx +++ b/src/layout/MainLayout/index.jsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Outlet } from 'react-router-dom'; import { AppShell } from '@astryxdesign/core/AppShell'; @@ -9,13 +10,17 @@ import Sidebar from './Sidebar'; // and the auto-generated mobile drawer. Pages keep their own internal padding. export default function MainLayout() { + // Lifted here (rather than left as SideNav's own uncontrolled state) so the + // navbar's logo can track the sidebar's collapse state — one logo, not two. + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true); + return ( } - sideNav={} + topNav={
} + sideNav={} mobileNav={{ breakpoint: 'lg' }} >
diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index 03939ee..d1f6ce9 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { Save, Sliders, @@ -40,6 +40,24 @@ const INITIAL_NOTIFY = { }; const INITIAL_SECURITY = { currentPassword: '', newPassword: '', confirmPassword: '', twoFactor: false }; +// No backend endpoint exists yet for organisation settings (confirmed: apiClient.js has +// none, and there's no swagger/docs to design one against — see git history for this +// page). Persisting to localStorage keeps Save/Discard honest and durable across reloads +// without pretending to sync to a server. Password fields are deliberately excluded — +// never write plaintext credentials to localStorage. +const SETTINGS_STORAGE_KEY = 'doormile_settings_v1'; +// data-URL logos bloat localStorage fast (5-10MB browser quota); cap the source file. +const MAX_LOGO_BYTES = 1.5 * 1024 * 1024; + +function loadSavedSettings() { + try { + const raw = localStorage.getItem(SETTINGS_STORAGE_KEY); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } +} + const NAV = [ { icon: Sliders, label: 'General', desc: 'Organisation profile' }, { icon: Bell, label: 'Notifications', desc: 'Alerts & channels' }, @@ -161,19 +179,58 @@ export default function Settings() { const [toast, setToast] = useState(false); const [dirty, setDirty] = useState(false); - const [general, setGeneral] = useState(INITIAL_GENERAL); - const [notify, setNotify] = useState(INITIAL_NOTIFY); - const [security, setSecurity] = useState(INITIAL_SECURITY); + // savedRef tracks the last-persisted snapshot so Discard reverts to "what's actually + // saved" rather than jumping back to hardcoded factory defaults. + const savedRef = useRef(loadSavedSettings()); + const saved = savedRef.current; + + const [general, setGeneral] = useState(saved?.general || INITIAL_GENERAL); + const [notify, setNotify] = useState(saved?.notify || INITIAL_NOTIFY); + const [security, setSecurity] = useState({ ...INITIAL_SECURITY, twoFactor: saved?.twoFactor ?? false }); + const [logoUrl, setLogoUrl] = useState(saved?.logoUrl ?? null); + const [logoError, setLogoError] = useState(''); + const logoInputRef = useRef(null); const setG = (k) => (val) => { setGeneral((p) => ({ ...p, [k]: val })); setDirty(true); }; const setN = (k) => (val) => { setNotify((p) => ({ ...p, [k]: val })); setDirty(true); }; const setSText = (k) => (val) => { setSecurity((p) => ({ ...p, [k]: val })); setDirty(true); }; - const save = () => { setToast(true); setDirty(false); setTimeout(() => setToast(false), 2500); }; + const handleLogoChange = (e) => { + const file = e.target.files?.[0]; + e.target.value = ''; + if (!file) return; + if (file.size > MAX_LOGO_BYTES) { + setLogoError(`Logo is too large (${(file.size / 1024 / 1024).toFixed(1)}MB) — please use an image under ${MAX_LOGO_BYTES / 1024 / 1024}MB.`); + return; + } + setLogoError(''); + const reader = new FileReader(); + reader.onload = () => { setLogoUrl(reader.result); setDirty(true); }; + reader.readAsDataURL(file); + }; + + const save = () => { + const snapshot = { general, notify, twoFactor: security.twoFactor, logoUrl }; + try { + localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(snapshot)); + } catch { + // Most likely QuotaExceededError from a large logo data-URL — surface it instead + // of showing a false "saved successfully" toast. + setLogoError('Could not save — local storage is full. Try a smaller logo image.'); + return; + } + savedRef.current = snapshot; + setToast(true); + setDirty(false); + setTimeout(() => setToast(false), 2500); + }; const discard = () => { - setGeneral(INITIAL_GENERAL); - setNotify(INITIAL_NOTIFY); - setSecurity(INITIAL_SECURITY); + const last = savedRef.current; + setGeneral(last?.general || INITIAL_GENERAL); + setNotify(last?.notify || INITIAL_NOTIFY); + setSecurity({ ...INITIAL_SECURITY, twoFactor: last?.twoFactor ?? false }); + setLogoUrl(last?.logoUrl ?? null); + setLogoError(''); setDirty(false); }; @@ -247,8 +304,14 @@ export default function Settings() { flexWrap: 'wrap' }} > -
- +
+ {logoUrl ? ( + Organisation logo + ) : ( + + )}
@@ -257,7 +320,17 @@ export default function Settings() {
{general.supportEmail} · {general.contact}
-
diff --git a/src/pages/team/TeamUsers.jsx b/src/pages/team/TeamUsers.jsx index b3a484d..7b02c71 100644 --- a/src/pages/team/TeamUsers.jsx +++ b/src/pages/team/TeamUsers.jsx @@ -252,6 +252,8 @@ export default function TeamUsers() { hasClear value={search} onChange={(v) => { setSearch(v); setPage(0); }} + htmlName="team-users-search" + autoComplete="off" />