Fix dashboard mobile layout: header overflow, welcome banner, and stats grid

Hide nav icon bar and volunteer button on mobile (accessible via drawer),
stack welcome banner vertically with proper nowrap, replace status bar
flex row with 3-column CSS grid using MobileQuickStat component.

Bunker Admin
This commit is contained in:
bunker-admin 2026-04-02 15:12:27 -06:00
parent 6db44eadc6
commit 610f547dbf
2 changed files with 157 additions and 71 deletions

View File

@ -638,7 +638,7 @@ export default function AppLayout() {
/>
</Tooltip>
{pageHeader?.actions}
{(() => {
{!isMobile && (() => {
const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS);
const withOverrides = applyAdminOverrides(merged);
const flags = buildFeatureFlags(settings);
@ -676,7 +676,7 @@ export default function AppLayout() {
placement="bottomRight"
>
<Button type="text" size="small" icon={getIcon(item.icon)}>
{!isMobile && !collapsed && item.label}
{!collapsed && item.label}
</Button>
</Dropdown>
);
@ -689,13 +689,14 @@ export default function AppLayout() {
icon={getIcon(item.icon)}
onClick={() => handleItemClick(item)}
>
{!isMobile && !collapsed && item.label}
{!collapsed && item.label}
</Button>
</Tooltip>
);
});
})()}
{/* Volunteer Portal button — always visible for quick switching */}
{!isMobile && (
<Tooltip title="Switch to Volunteer Portal">
<Button
type="text"
@ -703,9 +704,10 @@ export default function AppLayout() {
icon={<TeamOutlined />}
onClick={() => navigate('/volunteer')}
>
{!isMobile && !collapsed && 'Volunteer'}
{!collapsed && 'Volunteer'}
</Button>
</Tooltip>
)}
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<Button type="text" icon={<UserOutlined />} data-tour="user-menu">
{!isMobile && !collapsed && (

View File

@ -314,17 +314,32 @@ export default function DashboardPage() {
{/* === Welcome Banner === */}
<Card
style={{ marginBottom: 16, background: 'linear-gradient(135deg, #1890ff 0%, #722ed1 100%)', border: 'none' }}
styles={{ body: { padding: '10px 16px' } }}
styles={{ body: { padding: isMobile ? '10px 12px' : '10px 16px' } }}
>
<Flex justify="space-between" align="center" wrap="wrap" gap={8}>
<Flex align="center" gap={12}>
<Flex vertical={isMobile} gap={8}>
<Flex justify="space-between" align="center" gap={8} style={{ minWidth: 0 }}>
<Flex align="center" gap={8} style={{ minWidth: 0 }}>
<Text strong style={{ color: '#fff', fontSize: 16, whiteSpace: 'nowrap' }}>
Welcome{user?.name ? `, ${user.name}` : ''}
</Text>
<Text style={{ color: 'rgba(255,255,255,0.55)', fontSize: 12 }}>
<Text style={{ color: 'rgba(255,255,255,0.55)', fontSize: 12, whiteSpace: 'nowrap' }}>
{lastRefresh && `Updated ${lastRefresh.toLocaleTimeString()}`}
</Text>
{isSuperAdmin && homepageUrl && (
</Flex>
{!isMobile && isSuperAdmin && homepageUrl && (
<Segmented
size="small"
value={activeView}
onChange={(val) => setActiveView(val as 'dashboard' | 'homepage')}
options={[
{ label: 'Dashboard', value: 'dashboard', icon: <DashboardOutlined /> },
{ label: 'Homepage', value: 'homepage', icon: <HomeOutlined /> },
]}
style={{ background: 'rgba(255,255,255,0.2)', borderRadius: 6, flexShrink: 0 }}
/>
)}
</Flex>
{isMobile && isSuperAdmin && homepageUrl && (
<Segmented
size="small"
value={activeView}
@ -336,9 +351,8 @@ export default function DashboardPage() {
style={{ background: 'rgba(255,255,255,0.2)', borderRadius: 6 }}
/>
)}
</Flex>
{activeView === 'dashboard' && (
<Flex gap={4} wrap="wrap" justify="flex-end" data-tour-dashboard-actions>
<Flex gap={4} wrap="wrap" justify={isMobile ? 'flex-start' : 'flex-end'} data-tour-dashboard-actions>
{showInfluence && <Tooltip title="New Campaign"><Button type="text" icon={<PlusOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/campaigns')} /></Tooltip>}
{showMap && <Tooltip title="Locations"><Button type="text" icon={<EnvironmentOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/map')} /></Tooltip>}
{showMedia && <Tooltip title="Videos"><Button type="text" icon={<UploadOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/media/library')} /></Tooltip>}
@ -433,8 +447,50 @@ export default function DashboardPage() {
{/* === Status Bar (weather + stats + pending actions + connectivity) === */}
{summary && (
<Card size="small" style={{ marginBottom: 16 }} styles={{ body: { padding: isMobile ? '8px 10px' : '10px 16px' } }} data-tour-dashboard-stats>
{isMobile ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
{weather && (
<div style={{ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 6, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<span style={{ fontSize: 22 }}>{getWeatherIcon(weather.weatherCode, weather.isDay)}</span>
<Text strong style={{ fontSize: 15 }}>{Math.round(weather.temperature)}°C</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{weather.weatherDescription}</Text>
</div>
)}
<MobileQuickStat icon={<TeamOutlined />} color="#1890ff" value={summary.users.total} label="Users" onClick={() => navigate('/app/users')} />
{showInfluence && <MobileQuickStat icon={<SendOutlined />} color="#52c41a" value={summary.campaigns.active} label={`of ${summary.campaigns.total}`} onClick={() => navigate('/app/campaigns')} />}
{showMap && <MobileQuickStat icon={<EnvironmentOutlined />} color="#722ed1" value={summary.locations.total.toLocaleString()} label={`${geocodePct}%`} onClick={() => navigate('/app/map')} />}
{showInfluence && <MobileQuickStat icon={<MailOutlined />} color="#faad14" value={summary.emails.sent} label="sent" onClick={() => navigate('/app/email-queue')} />}
{showMedia && <MobileQuickStat icon={<VideoCameraOutlined />} color="#13c2c2" value={summary.videos.published} label={`of ${summary.videos.total}`} onClick={() => navigate('/app/media/library')} />}
{showMap && <MobileQuickStat icon={<ScheduleOutlined />} color="#eb2f96" value={summary.shifts.upcoming} label={`${summary.shifts.open} open`} onClick={() => navigate('/app/map/shifts')} />}
{/* Pending action tags */}
{(summary.responses.pending > 0 || (summary.locations.total > 0 && summary.locations.total - summary.locations.geocoded > 0) || summary.emails.queued > 0) && (
<div style={{ gridColumn: '1 / -1', display: 'flex', flexWrap: 'wrap', gap: 4, paddingTop: 4, borderTop: '1px solid rgba(255,255,255,0.08)' }}>
{summary.responses.pending > 0 && (
<Tag color="orange" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/responses')}>
{summary.responses.pending} pending
</Tag>
)}
{summary.locations.total > 0 && summary.locations.total - summary.locations.geocoded > 0 && (
<Tag color="purple" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/map')}>
{summary.locations.total - summary.locations.geocoded} ungeocoded
</Tag>
)}
{summary.emails.queued > 0 && (
<Tag color="blue" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/email-queue')}>
{summary.emails.queued} queued
</Tag>
)}
{summary.campaignModeration.pendingReview > 0 && (
<Tag color="gold" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/campaign-moderation')}>
{summary.campaignModeration.pendingReview} review
</Tag>
)}
</div>
)}
</div>
) : (
<Flex justify="space-between" align="center" wrap="wrap" gap={8}>
<Flex gap={0} align="center" style={isMobile ? { overflowX: 'auto', maxWidth: '100%', WebkitOverflowScrolling: 'touch' } : { flexWrap: 'wrap' }}>
<Flex gap={0} align="center" style={{ flexWrap: 'wrap' }}>
{weather && (
<Flex align="center" gap={6} style={{ padding: '0 14px 0 0', borderRight: '1px solid rgba(255,255,255,0.08)' }}>
<span style={{ fontSize: 24 }}>{getWeatherIcon(weather.weatherCode, weather.isDay)}</span>
@ -474,7 +530,7 @@ export default function DashboardPage() {
</Tag>
)}
</Flex>
{isSuperAdmin && connectivity && !isMobile && (
{isSuperAdmin && connectivity && (
<Flex gap={6} align="center" style={{ flexShrink: 0 }}>
<ConnectivityDot label="SMTP" online={connectivity.smtp} />
<ConnectivityDot label="Listmonk" online={connectivity.listmonk} />
@ -483,6 +539,7 @@ export default function DashboardPage() {
</Flex>
)}
</Flex>
)}
</Card>
)}
@ -1097,6 +1154,33 @@ function QuickStat({ icon, color, value, label, onClick }: {
);
}
function MobileQuickStat({ icon, color, value, label, onClick }: {
icon: React.ReactNode;
color: string;
value: string | number;
label: string;
onClick: () => void;
}) {
return (
<div
onClick={onClick}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '6px 8px',
cursor: 'pointer',
borderRadius: 6,
background: 'rgba(255,255,255,0.04)',
}}
>
<span style={{ color, fontSize: 15 }}>{icon}</span>
<Text strong style={{ fontSize: 15 }}>{value}</Text>
<Text type="secondary" style={{ fontSize: 11 }}>{label}</Text>
</div>
);
}
function ConnectivityDot({ label, online }: { label: string; online: boolean }) {
return (
<Tooltip title={`${label}: ${online ? 'Connected' : 'Offline'}`}>