Finalize CRM module: bookings, pricing, survey with full UI integration and validation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
AppBar,
|
||||
@@ -26,7 +26,8 @@ import LogoutIcon from '@mui/icons-material/Logout';
|
||||
|
||||
import Logo from '@/components/Logo';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { fetchPoints, COLLECTIONS } from '@/utils/qdrant';
|
||||
import { fetchClients, fetchUsers } from '@/utils/apiClient';
|
||||
import { toClient } from '@/utils/mappers';
|
||||
|
||||
const RED = '#C01227';
|
||||
|
||||
@@ -34,25 +35,50 @@ export default function Header({ onToggle }) {
|
||||
const navigate = useNavigate();
|
||||
const [account, setAccount] = useState(null);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
// Read user from localStorage
|
||||
let storedUserObj = { name: 'Admin', role: 'Operations Admin', id: 0 };
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
storedUserObj = JSON.parse(storedUser);
|
||||
}
|
||||
} 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';
|
||||
const displayInitial = activeUserName.charAt(0).toUpperCase();
|
||||
|
||||
// Live client search for the top bar.
|
||||
const searchRef = useRef(null);
|
||||
const [clients, setClients] = useState([]);
|
||||
const [loadedClients, setLoadedClients] = useState(false);
|
||||
const [loadingClients, setLoadingClients] = useState(false);
|
||||
const [openResults, setOpenResults] = useState(false);
|
||||
|
||||
// Fetch users to get the real name if the login payload didn't have it
|
||||
useEffect(() => {
|
||||
fetchUsers().then((users) => {
|
||||
if (storedUserObj.email) {
|
||||
// Strictly use email to match, as stored ID might belong to the doormile_auth table instead of appusers table
|
||||
const matchingUser = users.find(u => u.email === storedUserObj.email);
|
||||
if (matchingUser && matchingUser.first_name) {
|
||||
setActiveUserName(matchingUser.first_name);
|
||||
// Also update localStorage so it's fresh
|
||||
const updatedUser = { ...storedUserObj, name: matchingUser.first_name };
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
}
|
||||
}
|
||||
}).catch(console.error);
|
||||
}, []);
|
||||
|
||||
const ensureClients = () => {
|
||||
if (loadedClients || loadingClients) return;
|
||||
setLoadingClients(true);
|
||||
fetchPoints(COLLECTIONS.clients)
|
||||
.then((points) => setClients(points.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.payload?.name || '—',
|
||||
city: p.payload?.city || '',
|
||||
businessType: p.payload?.businessType || '',
|
||||
phone: p.payload?.phone || ''
|
||||
}))))
|
||||
fetchClients()
|
||||
.then((points) => setClients(points.map(toClient)))
|
||||
.catch(() => {})
|
||||
.finally(() => { setLoadedClients(true); setLoadingClients(false); });
|
||||
};
|
||||
@@ -84,24 +110,22 @@ export default function Header({ onToggle }) {
|
||||
<AppBar
|
||||
position="fixed"
|
||||
elevation={0}
|
||||
sx={{ bgcolor: RED, color: '#fff', zIndex: (t) => t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }}
|
||||
sx={{ bgcolor: '#fff', color: 'grey.800', zIndex: (t) => t.zIndex.drawer + 1, borderBottom: 1, borderColor: 'divider' }}
|
||||
>
|
||||
<Toolbar sx={{ minHeight: 64, px: { xs: 1.5, sm: 2.5 }, gap: 1 }}>
|
||||
<IconButton color="inherit" edge="start" onClick={onToggle} sx={{ mr: 0.5 }}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* Brand wordmark — left side */}
|
||||
<Box
|
||||
onClick={() => navigate('/dashboard')}
|
||||
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
>
|
||||
<Logo onDark height={22} />
|
||||
<Logo height={22} />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
{/* Search — live client lookup */}
|
||||
<ClickAwayListener onClickAway={() => setOpenResults(false)}>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' }, position: 'relative' }}>
|
||||
<Box
|
||||
@@ -111,23 +135,23 @@ export default function Header({ onToggle }) {
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
bgcolor: alpha('#fff', 0.16),
|
||||
bgcolor: 'grey.100',
|
||||
borderRadius: 2,
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
width: { sm: 240, md: 320 },
|
||||
'&:hover': { bgcolor: alpha('#fff', 0.22) },
|
||||
'&:focus-within': { bgcolor: alpha('#fff', 0.26) }
|
||||
'&:hover': { bgcolor: 'grey.200' },
|
||||
'&:focus-within': { bgcolor: 'grey.200' }
|
||||
}}
|
||||
>
|
||||
<SearchIcon sx={{ fontSize: 20, mr: 1, opacity: 0.9 }} />
|
||||
<SearchIcon sx={{ fontSize: 20, mr: 1, color: 'grey.500' }} />
|
||||
<InputBase
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
onFocus={() => { ensureClients(); if (search.trim()) setOpenResults(true); }}
|
||||
placeholder="Search clients…"
|
||||
sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }}
|
||||
inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }}
|
||||
sx={{ color: 'grey.800', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: 'grey.500' } }}
|
||||
inputProps={{ 'aria-label': 'search' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -171,20 +195,19 @@ export default function Header({ onToggle }) {
|
||||
|
||||
<Box
|
||||
onClick={(e) => setAccount(e.currentTarget)}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 0.5, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: alpha('#fff', 0.14) } }}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 0.5, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: 'grey.100' } }}
|
||||
>
|
||||
<Avatar sx={{ width: 34, height: 34, bgcolor: '#fff', color: RED, fontWeight: 700 }}>A</Avatar>
|
||||
<Avatar sx={{ width: 34, height: 34, bgcolor: RED, color: '#fff', fontWeight: 700 }}>{displayInitial}</Avatar>
|
||||
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: '#fff', fontWeight: 600 }}>
|
||||
Admin
|
||||
<Typography variant="subtitle2" sx={{ color: 'grey.800', fontWeight: 600 }}>
|
||||
{activeUserName}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: alpha('#fff', 0.8) }}>
|
||||
Operations Admin
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', textTransform: 'capitalize' }}>
|
||||
{displayRole}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Account dropdown */}
|
||||
<Menu
|
||||
anchorEl={account}
|
||||
open={Boolean(account)}
|
||||
@@ -195,10 +218,10 @@ export default function Header({ onToggle }) {
|
||||
>
|
||||
<Box sx={{ px: 2, py: 1.5 }}>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Avatar sx={{ width: 38, height: 38, bgcolor: RED, color: '#fff', fontWeight: 700 }}>A</Avatar>
|
||||
<Avatar sx={{ width: 38, height: 38, bgcolor: RED, color: '#fff', fontWeight: 700 }}>{displayInitial}</Avatar>
|
||||
<Box sx={{ lineHeight: 1.2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>Admin</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Operations Admin</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>{activeUserName}</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'capitalize' }}>{displayRole}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -208,7 +231,7 @@ export default function Header({ onToggle }) {
|
||||
Settings
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={() => { setAccount(null); navigate('/login'); }} sx={{ color: 'error.main' }}>
|
||||
<MenuItem onClick={() => { setAccount(null); localStorage.removeItem('auth_token'); navigate('/login'); }} sx={{ color: 'error.main' }}>
|
||||
<ListItemIcon><LogoutIcon fontSize="small" color="error" /></ListItemIcon>
|
||||
Logout
|
||||
</MenuItem>
|
||||
|
||||
Reference in New Issue
Block a user