updates on the full ui and followed by astryx

This commit is contained in:
2026-07-15 17:53:46 +05:30
parent 03f07c79ee
commit d4227a0b12
18 changed files with 231 additions and 281 deletions

View File

@@ -12,7 +12,7 @@ const load = (factory) => {
<Suspense
fallback={
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #C01227', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #0A1317', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<style>{`@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`}</style>
</div>
}

View File

@@ -8,7 +8,7 @@ export default function StatCard({ title, value, icon: Icon, color = 'primary',
// Map theme colors to CSS values
const colorMap = {
primary: { main: '#C01227', light: 'rgba(192, 18, 39, 0.04)', border: 'rgba(192, 18, 39, 0.1)' },
primary: { main: '#0A1317', light: 'rgba(10, 19, 23, 0.04)', border: 'rgba(10, 19, 23, 0.1)' },
secondary: { main: '#475569', light: 'rgba(71, 85, 105, 0.04)', border: 'rgba(71, 85, 105, 0.1)' },
success: { main: '#10b981', light: 'rgba(16, 185, 129, 0.04)', border: 'rgba(16, 185, 129, 0.1)' },
warning: { main: '#f59e0b', light: 'rgba(245, 158, 11, 0.04)', border: 'rgba(245, 158, 11, 0.1)' },
@@ -54,8 +54,8 @@ export default function StatCard({ title, value, icon: Icon, color = 'primary',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(192, 18, 39, 0.08)',
color: '#C01227'
backgroundColor: 'rgba(10, 19, 23, 0.08)',
color: '#0A1317'
}}
>
<Icon size={isCompact ? 14 : 18} />

View File

@@ -0,0 +1,44 @@
import { Button } from '@astryxdesign/core/Button';
// ==============================|| SHARED TABLE PAGINATION BAR ||============================== //
// Rows-per-page select + "x-y of z" + Prev/Next, used by every list/table page.
// `total` is the count of the page's own unit of pagination (rows for most
// pages, groups for Survey's company-grouped view) — the caller still owns
// its page/rpp state and slicing.
export default function TablePagination({ page, rpp, total, onPageChange, onRppChange, unitLabel = 'Rows' }) {
const lastPage = Math.max(0, Math.ceil(total / rpp) - 1);
return (
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', padding: '12px 24px', borderTop: '1px solid #e2e8f0', gap: '20px', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#475569' }}>
<span>{unitLabel} per page:</span>
<select
value={rpp}
onChange={(e) => { onRppChange(Number(e.target.value)); onPageChange(0); }}
className="rpp-select"
>
{[5, 10, 25].map((opt) => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
<span style={{ fontSize: '0.875rem', color: '#475569' }}>
{total === 0 ? '0-0' : `${page * rpp + 1}-${Math.min((page + 1) * rpp, total)}`} of {total}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
label="Prev"
variant="secondary"
size="sm"
onClick={() => onPageChange(Math.max(0, page - 1))}
isDisabled={page === 0}
/>
<Button
label="Next"
variant="secondary"
size="sm"
onClick={() => onPageChange(Math.min(lastPage, page + 1))}
isDisabled={page >= lastPage}
/>
</div>
</div>
);
}

View File

@@ -21,13 +21,13 @@
--color-text-primary: #0A1317;
--color-text-secondary: #4E606F;
--color-text-disabled: #A4B0BC;
--color-text-accent: #C01227;
--color-text-accent: #0A1317;
/* Icon tokens */
--color-icon-primary: #0A1317;
--color-icon-secondary: #4E606F;
--color-icon-disabled: #A4B0BC;
--color-icon-accent: #C01227;
--color-icon-accent: #0A1317;
/* Border tokens */
--color-border: rgba(5, 54, 89, 0.1);
@@ -44,11 +44,18 @@
--color-track: #CCD3DB;
--color-shadow: rgba(5, 54, 89, 0.1);
/* Brand accent — Doormile red, replaces the theme-neutral default */
--color-accent: #C01227;
--color-accent-muted: rgba(192, 18, 39, 0.12);
/* Brand accent — black/white, replaces the theme-neutral default. Hover and
active states resolve to this token, so it drives the black hover fills
site-wide (see @astryxdesign/core Badge/Button/etc. :hover rules). */
--color-accent: #0A1317;
--color-accent-muted: rgba(10, 19, 23, 0.12);
--color-on-accent: #ffffff;
/* "red" Badge variant (StatusChip/TabLabelCount) — kept monochrome, matches
the neutral tokens so no red renders anywhere in the UI. */
--color-background-red: rgba(5, 54, 89, 0.1);
--color-text-red: #0A1317;
/* App-level overrides */
background-color: #f8fafc;
color: #0f172a;
@@ -104,8 +111,8 @@ input:not(.search-bar-input):not([class]), select:not([class]) {
}
input:not(.search-bar-input):not([class]):focus, select:not([class]):focus, textarea:not([class]):focus {
border-color: #C01227 !important;
box-shadow: 0 0 0 3px rgba(192, 18, 39, 0.12) !important;
border-color: #0A1317 !important;
box-shadow: 0 0 0 3px rgba(10, 19, 23, 0.12) !important;
}
/* Premium Select Dropdown Styling */
@@ -135,8 +142,8 @@ select:not([class]) {
}
.search-bar-form:focus-within {
border-color: #C01227;
box-shadow: 0 0 0 4px rgba(192, 18, 39, 0.12), 0 10px 15px -3px rgba(0, 0, 0, 0.05);
border-color: #0A1317;
box-shadow: 0 0 0 4px rgba(10, 19, 23, 0.12), 0 10px 15px -3px rgba(0, 0, 0, 0.05);
max-width: 380px; /* Expand slightly on focus for premium desktop feel */
}

View File

@@ -15,7 +15,7 @@ export default function Header({ isSidebarCollapsed }) {
try {
const storedUser = localStorage.getItem('user');
if (storedUser) storedUserObj = JSON.parse(storedUser);
} catch (e) {}
} catch (e) { }
const [activeUserName, setActiveUserName] = useState(storedUserObj.name || 'Admin');
@@ -45,12 +45,12 @@ export default function Header({ isSidebarCollapsed }) {
logo={
<Logo
compact={isSidebarCollapsed}
size={32}
size={isSidebarCollapsed ? 36 : 32}
height={28}
// Scaling visually enlarges the mark without growing its layout
// box, so the navbar row keeps its original width/height instead
// of stretching taller every time the logo gets bigger.
style={isSidebarCollapsed ? { transform: 'scale(1.6)' } : undefined}
style={isSidebarCollapsed ? { transform: 'scale(1.4)' } : undefined}
/>
}
headingHref="/dashboard"

View File

@@ -79,17 +79,17 @@ export default function Sidebar({ isCollapsed, onCollapsedChange }) {
height: 18px !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label]:hover {
background-color: rgba(192, 18, 39, 0.08) !important;
background-color: rgba(10, 19, 23, 0.08) !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label]:focus-visible {
outline: 2px solid rgba(192, 18, 39, 0.4);
outline: 2px solid rgba(10, 19, 23, 0.4);
outline-offset: 2px;
}
.doormile-side-nav .astryx-side-nav-item[aria-label][data-selected='selected'] {
background-color: rgba(192, 18, 39, 0.12) !important;
background-color: rgba(10, 19, 23, 0.12) !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label][data-selected='selected'] .astryx-icon {
color: #C01227 !important;
color: #0A1317 !important;
}
/* Expanded rows: rounded hover/selected states with a left accent
@@ -107,37 +107,37 @@ export default function Sidebar({ isCollapsed, onCollapsedChange }) {
position: relative;
margin-inline: 2px;
height: auto;
padding-block: 8px !important;
padding-block: 12px !important;
transition: background-color 0.15s ease, transform 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])::before {
content: '';
position: absolute;
left: -2px;
top: 8px;
bottom: 8px;
top: 12px;
bottom: 12px;
width: 3px;
border-radius: 3px;
background-color: #C01227;
background-color: #0A1317;
opacity: 0;
transition: opacity 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):hover {
background-color: rgba(192, 18, 39, 0.05) !important;
background-color: rgba(10, 19, 23, 0.05) !important;
transform: translateX(2px);
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):focus-visible {
outline: 2px solid rgba(192, 18, 39, 0.4);
outline: 2px solid rgba(10, 19, 23, 0.4);
outline-offset: 2px;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] {
background-color: rgba(192, 18, 39, 0.08) !important;
background-color: rgba(10, 19, 23, 0.08) !important;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected']::before {
opacity: 1;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] .astryx-icon {
color: #C01227 !important;
color: #0A1317 !important;
}
/* Astryx's sticky-bottom footer wrapper (the toggle button's direct
@@ -150,19 +150,18 @@ export default function Sidebar({ isCollapsed, onCollapsedChange }) {
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
clean circular hover detached from the edge instead. */
/* Collapse/expand toggle (the "<" / ">" chevron button). Give it a
clean circular hover, centered on the same axis as the nav icons
above it (the rail's own 8px inline padding already keeps it clear
of the sidebar's edge, so no extra offset is needed here). */
.doormile-side-nav button[aria-label*="sidebar"],
.doormile-side-nav button[aria-label*="navigation"] {
border-radius: 50% !important;
transition: background-color 0.15s ease !important;
transform: translateX(-6px) !important;
}
.doormile-side-nav button[aria-label*="sidebar"]:hover,
.doormile-side-nav button[aria-label*="navigation"]:hover {
background-color: rgba(192, 18, 39, 0.08) !important;
background-color: rgba(10, 19, 23, 0.08) !important;
}
`}</style>
</>

View File

@@ -19,6 +19,7 @@ import { Table, proportional, pixel } from '@astryxdesign/core/Table';
import { Badge } from '@astryxdesign/core/Badge';
import { Text, Heading } from '@astryxdesign/core/Text';
import { Banner } from '@astryxdesign/core/Banner';
import { Divider } from '@astryxdesign/core/Divider';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
@@ -56,7 +57,7 @@ function Panel({ icon: Icon, title, action, color = 'primary', noPadding = false
alignItems: 'center',
padding: '10px 16px',
borderBottom: '1px solid rgba(5, 54, 89, 0.06)',
background: 'linear-gradient(90deg, rgba(192, 18, 39, 0.03) 0%, rgba(255, 255, 255, 0) 100%)',
background: 'linear-gradient(90deg, rgba(10, 19, 23, 0.03) 0%, rgba(255, 255, 255, 0) 100%)',
gap: '10px',
flexWrap: 'wrap'
}}
@@ -69,8 +70,8 @@ function Panel({ icon: Icon, title, action, color = 'primary', noPadding = false
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(192, 18, 39, 0.08)',
color: '#C01227',
backgroundColor: 'rgba(10, 19, 23, 0.08)',
color: '#0A1317',
flexShrink: 0
}}
>
@@ -210,8 +211,8 @@ export default function Dashboard() {
width: '150px'
}}
onFocus={(e) => {
e.target.style.borderColor = '#C01227';
e.target.style.boxShadow = '0 0 0 3px rgba(192, 18, 39, 0.12)';
e.target.style.borderColor = '#0A1317';
e.target.style.boxShadow = '0 0 0 3px rgba(10, 19, 23, 0.12)';
}}
onBlur={(e) => {
e.target.style.borderColor = 'rgba(5, 54, 89, 0.12)';
@@ -225,7 +226,7 @@ export default function Dashboard() {
<div style={{ fontFamily: 'system-ui, sans-serif' }}>
<PageHeader title="Dashboard" action={datePickerAction} />
<div style={{ display: 'flex', justifyContent: 'center', padding: '96px 0' }}>
<div className="spinner" style={{ width: '40px', height: '40px', border: '4px solid #f1f5f9', borderTop: '4px solid #C01227', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<div className="spinner" style={{ width: '40px', height: '40px', border: '4px solid #f1f5f9', borderTop: '4px solid #0A1317', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<style>{`@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`}</style>
</div>
</div>
@@ -241,20 +242,25 @@ export default function Dashboard() {
<Banner
status="error"
title={error}
endContent={<Button label="Retry" variant="ghost" size="sm" onClick={load} />}
endContent={<Button label="Reload" variant="ghost" size="sm" onClick={load} />}
/>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '12px', marginBottom: '16px' }}>
{/* Overview section */}
<Heading level={3}>Overview</Heading>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '12px', marginTop: '12px', marginBottom: '20px' }}>
<StatCard size="sm" title="Total Clients" value={stats.total} icon={Building2} color="primary" />
<StatCard size="sm" title="New Clients" value={stats.newCount} icon={UserPlus} color="info" />
<StatCard size="sm" title="Total Parcel Volume" value={stats.parcels.toLocaleString('en-IN')} icon={Package} color="success" />
<StatCard size="sm" title="Active Contracts" value={stats.contracts} icon={Handshake} color="warning" />
<StatCard size="sm" title="New Clients" value={stats.newCount} icon={UserPlus} color="primary" />
<StatCard size="sm" title="Total Parcel Volume" value={stats.parcels.toLocaleString('en-IN')} icon={Package} color="primary" />
<StatCard size="sm" title="Active Contracts" value={stats.contracts} icon={Handshake} color="primary" />
</div>
{/* Main dashboard panels */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '16px', marginBottom: '16px' }}>
<Divider />
{/* Clients section */}
<Heading level={3}>Clients</Heading>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '16px', marginTop: '12px', marginBottom: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', lg: 'repeat(12, 1fr)', gap: '16px' }} className="responsive-row">
{/* Recent Clients */}
<div style={{ gridColumn: 'span 8' }} className="col-span-8">
@@ -265,7 +271,7 @@ export default function Dashboard() {
noPadding
action={recent.length > 0 && (
<Button
label="View all"
label="View more"
variant="ghost"
size="sm"
icon={<ArrowRight size={14} />}
@@ -305,10 +311,16 @@ export default function Dashboard() {
</Panel>
</div>
</div>
</div>
<Divider />
{/* Team section */}
<Heading level={3}>Team</Heading>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '16px', marginTop: '12px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', lg: 'repeat(12, 1fr)', gap: '16px' }} className="responsive-row">
{/* Business Type bar chart */}
<div style={{ gridColumn: 'span 8' }} className="col-span-8">
<div style={{ gridColumn: 'span 6' }} className="col-span-6">
<Panel icon={Layers} title="Clients by Business Type" color="primary">
{byType.length === 0 ? (
<EmptyState icon={Layers} title="No business types" caption="No business type distribution available." />
@@ -336,7 +348,7 @@ export default function Dashboard() {
</div>
{/* App Users */}
<div style={{ gridColumn: 'span 4' }} className="col-span-4">
<div style={{ gridColumn: 'span 6' }} className="col-span-6">
<Panel
icon={Users}
title="App Users"
@@ -345,8 +357,8 @@ export default function Dashboard() {
{team.length === 0 ? (
<EmptyState title="No team users" />
) : (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{team.slice(0, 6).map((u) => (
<div style={{ display: 'flex', flexDirection: 'column', maxHeight: '324px', overflowY: 'auto', overflowX: 'hidden' }}>
{team.map((u) => (
<div
key={u.id}
style={{
@@ -387,12 +399,15 @@ export default function Dashboard() {
.col-span-4 {
grid-column: span 4 !important;
}
.col-span-6 {
grid-column: span 6 !important;
}
}
@media (max-width: 1023px) {
.responsive-row {
grid-template-columns: 1fr !important;
}
.col-span-8, .col-span-4 {
.col-span-8, .col-span-4, .col-span-6 {
grid-column: span 1 !important;
}
}
@@ -404,12 +419,12 @@ export default function Dashboard() {
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08) !important;
border-color: rgba(192, 18, 39, 0.2) !important;
border-color: rgba(10, 19, 23, 0.2) !important;
}
/* Team member row hover polish */
.team-row-hover:hover {
background-color: rgba(192, 18, 39, 0.04) !important;
background-color: rgba(10, 19, 23, 0.04) !important;
}
`}</style>
</div>

View File

@@ -22,6 +22,7 @@ import { TextInput } from '@astryxdesign/core/TextInput';
import { Selector } from '@astryxdesign/core/Selector';
import { Switch } from '@astryxdesign/core/Switch';
import { Badge } from '@astryxdesign/core/Badge';
import { Divider } from '@astryxdesign/core/Divider';
import PageHeader from '@/components/PageHeader';
@@ -98,15 +99,15 @@ function Section({ icon: Icon, title, subtitle, danger = false, children }) {
alignItems: 'center',
padding: '18px 24px',
borderBottom: '1px solid rgba(5, 54, 89, 0.06)',
background: danger ? 'linear-gradient(90deg, rgba(239, 68, 68, 0.04) 0%, #ffffff 72%)' : 'linear-gradient(90deg, rgba(192, 18, 39, 0.04) 0%, #ffffff 72%)',
background: danger ? 'linear-gradient(90deg, rgba(239, 68, 68, 0.04) 0%, #ffffff 72%)' : 'linear-gradient(90deg, rgba(10, 19, 23, 0.04) 0%, #ffffff 72%)',
gap: '12px'
}}
>
<div style={{ width: '40px', height: '40px', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: danger ? 'rgba(239, 68, 68, 0.08)' : 'rgba(192, 18, 39, 0.08)', color: danger ? '#ef4444' : '#C01227' }}>
<div style={{ width: '40px', height: '40px', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: danger ? 'rgba(239, 68, 68, 0.08)' : 'rgba(10, 19, 23, 0.08)', color: danger ? '#ef4444' : '#0A1317' }}>
<Icon size={18} />
</div>
<div>
<Heading level={5}>{title}</Heading>
<Heading level={3}>{title}</Heading>
{subtitle && <Text type="supporting" color="secondary">{subtitle}</Text>}
</div>
</div>
@@ -117,7 +118,8 @@ function Section({ icon: Icon, title, subtitle, danger = false, children }) {
function Row({ label, description, controlAlign = 'stretch', children }) {
return (
<div style={{ display: 'flex', alignItems: 'center', padding: '16px 0', borderBottom: '1px solid #f1f5f9', flexWrap: 'wrap', gap: '12px' }} className="settings-row">
<>
<div style={{ display: 'flex', alignItems: 'center', padding: '16px 0', flexWrap: 'wrap', gap: '12px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', width: '240px', flexShrink: 0 }}>
<Text type="body" weight="semibold">{label}</Text>
{description && <Text type="supporting" color="secondary">{description}</Text>}
@@ -133,6 +135,8 @@ function Row({ label, description, controlAlign = 'stretch', children }) {
{children}
</div>
</div>
<Divider className="settings-row-divider" />
</>
);
}
@@ -270,9 +274,9 @@ export default function Settings() {
</List>
</Card>
<Card style={{ padding: '20px', backgroundColor: 'rgba(192, 18, 39, 0.05)', border: '1px solid rgba(192, 18, 39, 0.1)' }}>
<Card style={{ padding: '20px', backgroundColor: 'rgba(10, 19, 23, 0.05)', border: '1px solid rgba(10, 19, 23, 0.1)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ width: '38px', height: '38px', borderRadius: '8px', backgroundColor: '#C01227', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: '38px', height: '38px', borderRadius: '8px', backgroundColor: 'rgba(10, 19, 23, 0.08)', color: '#0A1317', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<HelpCircle size={16} />
</div>
<div>
@@ -300,12 +304,12 @@ export default function Settings() {
gap: '20px',
alignItems: 'center',
padding: '24px',
background: 'linear-gradient(90deg, rgba(192, 18, 39, 0.05) 0%, #ffffff 75%)',
background: 'linear-gradient(90deg, rgba(10, 19, 23, 0.05) 0%, #ffffff 75%)',
flexWrap: 'wrap'
}}
>
<div
style={{ width: '64px', height: '64px', borderRadius: '8px', backgroundColor: logoUrl ? 'transparent' : '#C01227', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, overflow: 'hidden' }}
style={{ width: '64px', height: '64px', borderRadius: '8px', backgroundColor: logoUrl ? 'transparent' : '#0A1317', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, overflow: 'hidden' }}
>
{logoUrl ? (
<img src={logoUrl} alt="Organisation logo" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
@@ -456,8 +460,8 @@ export default function Settings() {
grid-column: span 1 !important;
}
}
.settings-row:last-child {
border-bottom: none !important;
.settings-row-divider:last-child {
display: none;
}
`}</style>
</div>

View File

@@ -60,7 +60,7 @@ export default function Login() {
style={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(180deg, rgba(15,23,42,0.6) 0%, rgba(192, 18, 39, 0.8) 100%)'
background: 'linear-gradient(180deg, rgba(15,23,42,0.6) 0%, rgba(10, 19, 23, 0.8) 100%)'
}}
/>

View File

@@ -18,6 +18,7 @@ import PageHeader from '@/components/PageHeader';
import EmptyState from '@/components/EmptyState';
import StatusChip from '@/components/StatusChip';
import TabLabelCount from '@/components/TabLabelCount';
import TablePagination from '@/components/TablePagination';
import { fetchClients } from '@/utils/apiClient';
import ClientFormDialog from '../tenants/ClientFormDialog';
@@ -225,7 +226,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Customer Details */}
<div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#C01227', fontSize: '0.9rem', textTransform: 'uppercase' }}>Customer Details</div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#0A1317', fontSize: '0.9rem', textTransform: 'uppercase' }}>Customer Details</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-end', flexWrap: 'wrap' }}>
<div style={{ flexGrow: 1, minWidth: '200px' }}>
<Field label="Select Client" inputID="booking-client-input">
@@ -258,7 +259,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Assigned Provider */}
<div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#C01227', fontSize: '0.9rem', textTransform: 'uppercase' }}>Assigned Provider (From Survey)</div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#0A1317', fontSize: '0.9rem', textTransform: 'uppercase' }}>Assigned Provider (From Survey)</div>
<FormLayout direction="horizontal">
<Selector
label="Provider Name" placeholder="Select Provider" hasClear
@@ -278,7 +279,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Pickup Location */}
<div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#C01227', fontSize: '0.9rem', textTransform: 'uppercase' }}>Pickup Location</div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#0A1317', fontSize: '0.9rem', textTransform: 'uppercase' }}>Pickup Location</div>
<FormLayout direction="horizontal">
<TextInput label="Pickup Address" isRequired value={formData.pickupaddress} onChange={set('pickupaddress')} />
<TextInput label="Pickup Pincode" isRequired value={formData.pickuppincode} onChange={set('pickuppincode')} />
@@ -287,7 +288,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Delivery Destination */}
<div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#C01227', fontSize: '0.9rem', textTransform: 'uppercase' }}>Delivery Destination</div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#0A1317', fontSize: '0.9rem', textTransform: 'uppercase' }}>Delivery Destination</div>
<FormLayout>
<TextInput label="Delivery Address" value={formData.deliveryaddress} onChange={set('deliveryaddress')} />
<FormLayout direction="horizontal">
@@ -305,7 +306,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Parcel Details */}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ fontWeight: 700, color: '#C01227', fontSize: '0.9rem', textTransform: 'uppercase' }}>Parcel Details</div>
<div style={{ fontWeight: 700, color: '#0A1317', fontSize: '0.9rem', textTransform: 'uppercase' }}>Parcel Details</div>
<Button variant="secondary" size="sm" icon={<Plus size={12} />} onClick={addParcel} label="Add Parcel" />
</div>
@@ -349,7 +350,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Price & Insurance */}
<div>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', marginBottom: '12px', flexWrap: 'wrap' }}>
<div style={{ fontWeight: 700, color: '#C01227', fontSize: '0.9rem', textTransform: 'uppercase' }}>Price & Insurance</div>
<div style={{ fontWeight: 700, color: '#0A1317', fontSize: '0.9rem', textTransform: 'uppercase' }}>Price & Insurance</div>
<Button variant="secondary" size="sm" onClick={handleCheckPrice} label="Check Price Estimate" />
</div>
{quoteInfo && (
@@ -388,7 +389,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Additional info */}
<div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#C01227', fontSize: '0.9rem', textTransform: 'uppercase' }}>Additional Information</div>
<div style={{ fontWeight: 700, marginBottom: '8px', color: '#0A1317', fontSize: '0.9rem', textTransform: 'uppercase' }}>Additional Information</div>
<FormLayout direction="horizontal">
<Selector
label="Select Speed"
@@ -582,8 +583,8 @@ export default function Bookings() {
style={{
padding: '16px 0',
cursor: 'pointer',
borderBottom: sourceFilter === tab.value ? '2px solid #C01227' : '2px solid transparent',
color: sourceFilter === tab.value ? '#C01227' : '#64748b',
borderBottom: sourceFilter === tab.value ? '2px solid #0A1317' : '2px solid transparent',
color: sourceFilter === tab.value ? '#0A1317' : '#64748b',
fontWeight: sourceFilter === tab.value ? 600 : 500,
fontSize: '0.875rem',
transition: 'all 0.2s'
@@ -596,7 +597,7 @@ export default function Bookings() {
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}>
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #C01227', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #0A1317', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
</div>
) : displayBookings.length === 0 ? (
<EmptyState title="No bookings found" caption="Try a different filter or search term, or create a new booking." />
@@ -616,37 +617,7 @@ export default function Bookings() {
)}
{!loading && displayBookings.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', padding: '12px 24px', borderTop: '1px solid #e2e8f0', gap: '20px', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#475569' }}>
<span>Rows per page:</span>
<select
value={rpp}
onChange={(e) => { setRpp(Number(e.target.value)); setPage(0); }}
className="rpp-select"
>
{[5, 10, 25].map((opt) => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
<span style={{ fontSize: '0.875rem', color: '#475569' }}>
{displayBookings.length === 0 ? '0-0' : `${page * rpp + 1}-${Math.min((page + 1) * rpp, displayBookings.length)}`} of {displayBookings.length}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
label="Prev"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.max(0, p - 1))}
isDisabled={page === 0}
/>
<Button
label="Next"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.min(Math.ceil(displayBookings.length / rpp) - 1, p + 1))}
isDisabled={page >= Math.ceil(displayBookings.length / rpp) - 1}
/>
</div>
</div>
<TablePagination page={page} rpp={rpp} total={displayBookings.length} onPageChange={setPage} onRppChange={setRpp} />
)}
</Card>

View File

@@ -43,11 +43,11 @@ export default function ComingSoon() {
width: '84px',
padding: '16px 0',
borderRadius: '12px',
backgroundColor: 'rgba(192, 18, 39, 0.08)',
border: '1px solid rgba(192, 18, 39, 0.2)'
backgroundColor: 'rgba(10, 19, 23, 0.08)',
border: '1px solid rgba(10, 19, 23, 0.2)'
}}
>
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#C01227', lineHeight: 1 }}>
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#0A1317', lineHeight: 1 }}>
{String(c.value).padStart(2, '0')}
</div>
<div style={{ fontSize: '0.65rem', color: '#64748b', textTransform: 'uppercase', letterSpacing: '1px', marginTop: '4px' }}>
@@ -55,7 +55,7 @@ export default function ComingSoon() {
</div>
</div>
{i < COUNTDOWN.length - 1 && (
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#C01227' }}>:</div>
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#0A1317' }}>:</div>
)}
</div>
))}

View File

@@ -20,10 +20,10 @@ export default function Error404() {
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', alignItems: 'center' }}>
<span style={{ fontWeight: 900, fontSize: 'clamp(3.5rem, 18vw, 8rem)', lineHeight: 1, color: '#C01227' }}>
<span style={{ fontWeight: 900, fontSize: 'clamp(3.5rem, 18vw, 8rem)', lineHeight: 1, color: '#0A1317' }}>
404
</span>
<div style={{ width: '64px', height: '4px', borderRadius: '2px', backgroundColor: '#C01227' }} />
<div style={{ width: '64px', height: '4px', borderRadius: '2px', backgroundColor: '#0A1317' }} />
<Heading level={2}>Page Not Found</Heading>
<div style={{ maxWidth: '460px' }}>
<Text type="body" color="secondary">

View File

@@ -20,10 +20,10 @@ export default function Error500() {
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', alignItems: 'center' }}>
<span style={{ fontWeight: 900, fontSize: 'clamp(3.5rem, 18vw, 8rem)', lineHeight: 1, color: '#C01227' }}>
<span style={{ fontWeight: 900, fontSize: 'clamp(3.5rem, 18vw, 8rem)', lineHeight: 1, color: '#0A1317' }}>
500
</span>
<div style={{ width: '64px', height: '4px', borderRadius: '2px', backgroundColor: '#C01227' }} />
<div style={{ width: '64px', height: '4px', borderRadius: '2px', backgroundColor: '#0A1317' }} />
<Heading level={2}>Internal Server Error</Heading>
<div style={{ maxWidth: '460px' }}>
<Text type="body" color="secondary">

View File

@@ -25,8 +25,8 @@ export default function UnderConstruction() {
width: '110px',
height: '110px',
borderRadius: '50%',
backgroundColor: 'rgba(192, 18, 39, 0.08)',
color: '#C01227',
backgroundColor: 'rgba(10, 19, 23, 0.08)',
color: '#0A1317',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'

View File

@@ -140,7 +140,7 @@ function PricingFormDialog({ open, onClose, onSave, initialData, isCell = false,
{error && <Banner status="error" title={error} />}
{isCell ? (
<Card style={{ padding: '12px 16px', backgroundColor: 'rgba(192, 18, 39, 0.04)', border: '1px solid rgba(192, 18, 39, 0.15)' }}>
<Card style={{ padding: '12px 16px', backgroundColor: 'rgba(10, 19, 23, 0.04)', border: '1px solid rgba(10, 19, 23, 0.15)' }}>
<Text type="label" color="secondary">Updating rate for</Text>
<div style={{ marginTop: '4px' }}>
<Heading level={5}>{formData.company}</Heading>
@@ -445,7 +445,7 @@ export default function Pricing() {
<Card
style={{
borderRadius: '16px',
border: locationsOpen ? '1px solid rgba(192, 18, 39, 0.25)' : '1px solid rgba(5, 54, 89, 0.08)',
border: locationsOpen ? '1px solid rgba(10, 19, 23, 0.25)' : '1px solid rgba(5, 54, 89, 0.08)',
backgroundColor: '#ffffff',
padding: '0',
overflow: 'hidden',
@@ -457,7 +457,7 @@ export default function Pricing() {
onClick={() => setLocationsOpen(!locationsOpen)}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: 'rgba(192, 18, 39, 0.08)', color: '#c01227', fontWeight: 800, fontSize: '0.875rem', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: 'rgba(10, 19, 23, 0.08)', color: '#0a1317', fontWeight: 800, fontSize: '0.875rem', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
{(selectedProvider || 'A')[0].toUpperCase()}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
@@ -552,9 +552,9 @@ export default function Pricing() {
{/* Pricing Data Matrix */}
<Card style={{ padding: '0', borderRadius: '16px', border: '1px solid rgba(5, 54, 89, 0.08)', backgroundColor: '#ffffff', overflow: 'hidden', boxShadow: '0 10px 30px -5px rgba(0,0,0,0.02), 0 4px 12px -3px rgba(0,0,0,0.02)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '20px 24px', borderBottom: '1px solid rgba(5, 54, 89, 0.06)', background: 'linear-gradient(90deg, rgba(192, 18, 39, 0.03) 0%, #ffffff 70%)', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '20px 24px', borderBottom: '1px solid rgba(5, 54, 89, 0.06)', background: 'linear-gradient(90deg, rgba(10, 19, 23, 0.03) 0%, #ffffff 70%)', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '40px', height: '40px', borderRadius: '8px', backgroundColor: 'rgba(192, 18, 39, 0.08)', color: '#C01227', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: '40px', height: '40px', borderRadius: '8px', backgroundColor: 'rgba(10, 19, 23, 0.08)', color: '#0A1317', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<LayoutGrid size={18} />
</div>
<div>
@@ -572,7 +572,7 @@ export default function Pricing() {
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}>
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #C01227', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #0A1317', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
</div>
) : providerZones.length === 0 ? (
<EmptyState title="No pricing data for this provider yet" caption="Add a rate to start building the pricing matrix." />
@@ -621,7 +621,7 @@ export default function Pricing() {
border: 1px solid rgba(5, 54, 89, 0.1);
background-color: #ffffff;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.12);
color: #C01227;
color: #0A1317;
cursor: pointer;
opacity: 0;
pointer-events: none;
@@ -632,14 +632,14 @@ export default function Pricing() {
pointer-events: auto;
}
.matrix-cell-edit:hover {
background-color: rgba(192, 18, 39, 0.08) !important;
background-color: rgba(10, 19, 23, 0.08) !important;
}
.location-card {
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.location-card:hover {
border-color: rgba(192, 18, 39, 0.3) !important;
box-shadow: 0 4px 12px rgba(192, 18, 39, 0.08);
border-color: rgba(10, 19, 23, 0.3) !important;
box-shadow: 0 4px 12px rgba(10, 19, 23, 0.08);
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
`}</style>

View File

@@ -34,6 +34,7 @@ import { Banner } from '@astryxdesign/core/Banner';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import EmptyState from '@/components/EmptyState';
import TablePagination from '@/components/TablePagination';
function isQuoted(row) {
return !!(row.rate_per_kg && !String(row.rate_per_kg).toLowerCase().includes('not answered'));
@@ -99,8 +100,8 @@ function Field({ label, children }) {
function SectionCard({ icon: Icon, title, children }) {
return (
<div style={{ height: '100%', borderRadius: '8px', border: '1px solid #e2e8f0', backgroundColor: '#ffffff', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '6px', padding: '6px 10px', borderBottom: '1px solid #e2e8f0', backgroundColor: 'rgba(192, 18, 39, 0.08)' }}>
<Icon size={12} style={{ color: '#c01227' }} />
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '6px', padding: '6px 10px', borderBottom: '1px solid #e2e8f0', backgroundColor: 'rgba(10, 19, 23, 0.08)' }}>
<Icon size={12} style={{ color: '#0a1317' }} />
<span style={{ fontWeight: 700, color: '#1e293b', fontSize: '0.64rem', letterSpacing: '0.6px', textTransform: 'uppercase' }}>{title}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '8px 10px' }}>{children}</div>
@@ -320,7 +321,7 @@ function SurveyFormDialog({ open, onClose, onSave, initialData }) {
<div style={{ borderTop: '1px dashed #e2e8f0', paddingTop: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<span style={{ fontSize: '0.875rem', fontWeight: 700, color: '#C01227' }}>SERVICEABLE PINCODES</span>
<span style={{ fontSize: '0.875rem', fontWeight: 700, color: '#0A1317' }}>SERVICEABLE PINCODES</span>
<Button size="sm" variant="secondary" icon={<Plus size={12} />} onClick={addPincode} label="Add Pincode" />
</div>
{(formData.pincodes || []).map((pincode, index) => (
@@ -347,7 +348,7 @@ function SurveyFormDialog({ open, onClose, onSave, initialData }) {
{/* Pricing slabs */}
<div style={{ borderTop: '1px dashed #e2e8f0', paddingTop: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<span style={{ fontSize: '0.875rem', fontWeight: 700, color: '#C01227' }}>PRICING SLABS (OPTIONAL)</span>
<span style={{ fontSize: '0.875rem', fontWeight: 700, color: '#0A1317' }}>PRICING SLABS (OPTIONAL)</span>
<Button size="sm" variant="secondary" icon={<Plus size={12} />} onClick={addSlab} label="Add Slab" />
</div>
{(formData.slabs || []).map((slab, index) => (
@@ -419,11 +420,11 @@ function BranchCard({ row, onEdit, onDelete, users }) {
}}
className="branch-card"
>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '10px' }} onClick={() => setOpen(!open)}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'nowrap', gap: '10px', overflow: 'hidden' }} onClick={() => setOpen(!open)}>
{/* Left: Location & Contact */}
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', minWidth: '200px' }}>
<div style={{ padding: '6px', borderRadius: '6px', backgroundColor: 'rgba(192, 18, 39, 0.05)', color: '#c01227', display: 'flex' }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', minWidth: '160px', flexShrink: 0 }}>
<div style={{ padding: '6px', borderRadius: '6px', backgroundColor: 'rgba(10, 19, 23, 0.05)', color: '#0a1317', display: 'flex' }}>
<MapPin size={14} />
</div>
<div>
@@ -442,8 +443,8 @@ function BranchCard({ row, onEdit, onDelete, users }) {
</div>
{/* Middle: Rates */}
<div style={{ display: 'flex', gap: '16px', flexGrow: 1 }}>
<div style={{ width: '130px', flexShrink: 0, overflow: 'hidden' }}>
<div style={{ display: 'flex', gap: '16px', flexGrow: 1, overflow: 'hidden' }}>
<div style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}>
<span style={{ fontSize: '0.6rem', color: '#64748b', fontWeight: 700, letterSpacing: '0.05em', display: 'block', marginBottom: '2px', textTransform: 'uppercase' }}>Rate/kg</span>
<Pill label={row.rate_per_kg} color={quoted ? 'success' : 'warning'} truncate />
</div>
@@ -534,9 +535,9 @@ function BranchCard({ row, onEdit, onDelete, users }) {
fontSize: '0.7rem',
fontWeight: 600,
borderRadius: '4px',
color: '#c01227',
border: '1px solid rgba(192, 18, 39, 0.2)',
backgroundColor: 'rgba(192, 18, 39, 0.05)',
color: '#0a1317',
border: '1px solid rgba(10, 19, 23, 0.2)',
backgroundColor: 'rgba(10, 19, 23, 0.05)',
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
@@ -589,7 +590,7 @@ function CompanyGroup({ companyName, branches, onEdit, onDelete, users, isLast }
onClick={() => setOpen(!open)}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: 'rgba(192, 18, 39, 0.08)', color: '#C01227', fontWeight: 700, fontSize: '0.8rem', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', backgroundColor: 'rgba(10, 19, 23, 0.08)', color: '#0A1317', fontWeight: 700, fontSize: '0.8rem', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
{(companyName || 'A')[0].toUpperCase()}
</div>
<div>
@@ -798,38 +799,7 @@ export default function Survey() {
</div>
)}
{/* Pagination Section */}
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', padding: '12px 24px', borderTop: '1px solid #e2e8f0', gap: '20px', flexWrap: 'wrap', backgroundColor: '#ffffff' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#475569' }}>
<span>Companies per page:</span>
<select
value={rowsPerPage}
onChange={(e) => { setRowsPerPage(Number(e.target.value)); setPage(0); }}
className="rpp-select"
>
{[5, 10, 25].map((opt) => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
<span style={{ fontSize: '0.875rem', color: '#475569' }}>
{companyKeys.length === 0 ? '0-0' : `${page * rowsPerPage + 1}-${Math.min((page + 1) * rowsPerPage, companyKeys.length)}`} of {companyKeys.length}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
label="Prev"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.max(0, p - 1))}
isDisabled={page === 0}
/>
<Button
label="Next"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.min(Math.ceil(companyKeys.length / rowsPerPage) - 1, p + 1))}
isDisabled={page >= Math.ceil(companyKeys.length / rowsPerPage) - 1}
/>
</div>
</div>
<TablePagination page={page} rpp={rowsPerPage} total={companyKeys.length} onPageChange={setPage} onRppChange={setRowsPerPage} unitLabel="Companies" />
</Card>
<SurveyFormDialog

View File

@@ -26,6 +26,7 @@ import PageHeader from '@/components/PageHeader';
import StatusChip from '@/components/StatusChip';
import EmptyState from '@/components/EmptyState';
import TabLabelCount from '@/components/TabLabelCount';
import TablePagination from '@/components/TablePagination';
import { fetchUsers, deleteUser } from '@/utils/apiClient';
import { toUser } from '@/utils/mappers';
import { titleCase } from '@/utils/format';
@@ -136,7 +137,7 @@ export default function TeamUsers() {
const columns = useMemo(() => [
{
key: '_rowNumber',
header: <div style={{ paddingLeft: '24px', paddingTop: '12px' }}>S.No</div>,
header: <div style={{ paddingLeft: '24px' }}>S.No</div>,
width: pixel(72),
renderCell: (r) => (
<div style={{ paddingLeft: '24px' }}>
@@ -146,20 +147,20 @@ export default function TeamUsers() {
},
{
key: 'name',
header: <div style={{ padding: '12px 16px 0' }}>User</div>,
header: <div style={{ padding: '0 12px' }}>User</div>,
width: proportional(2),
renderCell: (r) => (
<div style={{ padding: '10px 16px' }}>
<div style={{ padding: '2px 12px' }}>
<UserIdentity name={r.name} email={r.email} />
</div>
)
},
{
key: 'email',
header: <div style={{ padding: '12px 16px 0' }}>Email</div>,
header: <div style={{ padding: '0 12px' }}>Email</div>,
width: proportional(2),
renderCell: (r) => (
<div style={{ padding: '10px 16px' }}>
<div style={{ padding: '2px 12px' }}>
{r.email ? (
<a href={`mailto:${r.email}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', textDecoration: 'none' }} className="link-hover">
<Mail size={14} style={{ color: '#94a3b8' }} />
@@ -171,10 +172,10 @@ export default function TeamUsers() {
},
{
key: 'phone',
header: <div style={{ padding: '12px 16px 0' }}>Phone</div>,
header: <div style={{ padding: '0 12px' }}>Phone</div>,
width: proportional(1),
renderCell: (r) => (
<div style={{ padding: '10px 16px' }}>
<div style={{ padding: '2px 12px' }}>
{r.phone ? (
<a href={`tel:${r.phone}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', textDecoration: 'none' }} className="link-hover">
<Phone size={14} style={{ color: '#94a3b8' }} />
@@ -186,17 +187,17 @@ export default function TeamUsers() {
},
{
key: 'role',
header: <div style={{ padding: '12px 16px 0' }}>Role</div>,
header: <div style={{ padding: '0 12px' }}>Role</div>,
width: pixel(140),
renderCell: (r) => (
<div style={{ padding: '10px 16px' }}>
<div style={{ padding: '2px 12px' }}>
<RoleCell role={r.role} />
</div>
)
},
{
key: 'actions',
header: <div style={{ paddingRight: '24px', paddingTop: '12px' }}>Actions</div>,
header: <div style={{ paddingRight: '24px' }}>Actions</div>,
width: pixel(120),
align: 'end',
renderCell: (r) => (
@@ -277,8 +278,8 @@ export default function TeamUsers() {
style={{
padding: '16px 0',
cursor: 'pointer',
borderBottom: tab === i ? '2px solid #C01227' : '2px solid transparent',
color: tab === i ? '#C01227' : '#64748b',
borderBottom: tab === i ? '2px solid #0A1317' : '2px solid transparent',
color: tab === i ? '#0A1317' : '#64748b',
fontWeight: tab === i ? 600 : 500,
fontSize: '0.875rem',
transition: 'all 0.2s'
@@ -301,7 +302,7 @@ export default function TeamUsers() {
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}>
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #C01227', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #0A1317', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
</div>
) : paged.length === 0 ? (
<EmptyState title="No team users found" caption="Try a different role or search term, or add a user." />
@@ -320,38 +321,7 @@ export default function TeamUsers() {
</div>
)}
{/* Pagination bar */}
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', padding: '12px 24px', borderTop: '1px solid #e2e8f0', gap: '20px', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#475569' }}>
<span>Rows per page:</span>
<select
value={rpp}
onChange={(e) => { setRpp(Number(e.target.value)); setPage(0); }}
className="rpp-select"
>
{[5, 10, 25].map((opt) => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
<span style={{ fontSize: '0.875rem', color: '#475569' }}>
{filtered.length === 0 ? '0-0' : `${page * rpp + 1}-${Math.min((page + 1) * rpp, filtered.length)}`} of {filtered.length}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
label="Prev"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.max(0, p - 1))}
isDisabled={page === 0}
/>
<Button
label="Next"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.min(Math.ceil(filtered.length / rpp) - 1, p + 1))}
isDisabled={page >= Math.ceil(filtered.length / rpp) - 1}
/>
</div>
</div>
<TablePagination page={page} rpp={rpp} total={filtered.length} onPageChange={setPage} onRppChange={setRpp} />
</Card>
<UserFormDialog

View File

@@ -34,6 +34,7 @@ import StatusChip from '@/components/StatusChip';
import EmptyState from '@/components/EmptyState';
import UserAvatar from '@/components/UserAvatar';
import TabLabelCount from '@/components/TabLabelCount';
import TablePagination from '@/components/TablePagination';
import { fetchClients, deleteClient } from '@/utils/apiClient';
import { toClient } from '@/utils/mappers';
import { titleCase } from '@/utils/format';
@@ -104,8 +105,8 @@ function ReadField({ label, value }) {
function SectionCard({ icon: Icon, title, accent = 'primary', children }) {
return (
<div style={{ height: '100%', borderRadius: '8px', border: '1px solid #e2e8f0', backgroundColor: '#ffffff', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '8px', padding: '10px 16px', borderBottom: '1px solid #e2e8f0', backgroundColor: 'rgba(192, 18, 39, 0.04)' }}>
<Icon size={16} style={{ color: '#C01227' }} />
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '8px', padding: '10px 16px', borderBottom: '1px solid #e2e8f0', backgroundColor: 'rgba(10, 19, 23, 0.04)' }}>
<Icon size={16} style={{ color: '#0A1317' }} />
<span style={{ fontWeight: 700, color: '#1e293b', fontSize: '0.68rem', letterSpacing: '0.05em', textTransform: 'uppercase' }}>{title}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px', padding: '16px' }}>{children}</div>
@@ -116,7 +117,7 @@ function SectionCard({ icon: Icon, title, accent = 'primary', children }) {
function StatTile({ label, value, icon: Icon }) {
return (
<div style={{ flex: 1, padding: '12px', borderRadius: '6px', border: '1px solid #e2e8f0', backgroundColor: '#f8fafc', display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '4px', backgroundColor: 'rgba(192, 18, 39, 0.08)', color: '#C01227', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '4px', backgroundColor: 'rgba(10, 19, 23, 0.08)', color: '#0A1317', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon size={16} />
</div>
<div>
@@ -147,7 +148,7 @@ function ClientDetail({ row, onEdit }) {
alignItems: 'center',
padding: '14px 20px',
borderBottom: '1px solid #e2e8f0',
background: 'linear-gradient(90deg, rgba(192, 18, 39, 0.05) 0%, #ffffff 75%)',
background: 'linear-gradient(90deg, rgba(10, 19, 23, 0.05) 0%, #ffffff 75%)',
flexWrap: 'wrap',
gap: '12px'
}}
@@ -201,7 +202,7 @@ function ClientDetail({ row, onEdit }) {
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' }}>
{stops.map((stop, idx) => (
<Fragment key={`${stop}-${idx}`}>
{idx > 0 && <ArrowRight size={14} style={{ color: '#C01227' }} />}
{idx > 0 && <ArrowRight size={14} style={{ color: '#0A1317' }} />}
<Pill label={titleCase(stop)} color={idx === 0 ? 'neutral' : 'info'} />
</Fragment>
))}
@@ -242,9 +243,9 @@ function ClientDetail({ row, onEdit }) {
fontSize: '0.75rem',
fontWeight: 600,
borderRadius: '20px',
color: '#C01227',
border: '1px solid rgba(192, 18, 39, 0.1)',
backgroundColor: 'rgba(192, 18, 39, 0.05)',
color: '#0A1317',
border: '1px solid rgba(10, 19, 23, 0.1)',
backgroundColor: 'rgba(10, 19, 23, 0.05)',
textDecoration: 'none'
}}
>
@@ -289,7 +290,7 @@ function ClientRow({ row, onEdit, onDelete }) {
onClick={() => setOpen((o) => !o)}
style={{
cursor: 'pointer',
backgroundColor: open ? 'rgba(192, 18, 39, 0.02)' : undefined
backgroundColor: open ? 'rgba(10, 19, 23, 0.02)' : undefined
}}
>
<TableCell>
@@ -301,7 +302,7 @@ function ClientRow({ row, onEdit, onDelete }) {
</button>
</TableCell>
<TableCell style={{ whiteSpace: 'nowrap' }}>
<span style={{ fontFamily: 'monospace', fontWeight: 700, color: '#C01227', backgroundColor: 'rgba(192, 18, 39, 0.08)', padding: '2px 8px', borderRadius: '4px', border: '1px solid rgba(192, 18, 39, 0.1)', fontSize: '0.75rem' }}>
<span style={{ fontFamily: 'monospace', fontWeight: 700, color: '#0A1317', backgroundColor: 'rgba(10, 19, 23, 0.08)', padding: '2px 8px', borderRadius: '4px', border: '1px solid rgba(10, 19, 23, 0.1)', fontSize: '0.75rem' }}>
{row.logicalId}
</span>
</TableCell>
@@ -342,7 +343,7 @@ function ClientRow({ row, onEdit, onDelete }) {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px' }}>
<Metric label="Parcels" value={Number(row.parcelVolume).toLocaleString()} />
<div style={{ width: '1px', height: '24px', backgroundColor: '#e2e8f0' }} />
<Metric label="Contracts" value={row.activeContracts} color="#C01227" />
<Metric label="Contracts" value={row.activeContracts} color="#0A1317" />
</div>
</TableCell>
<TableCell>
@@ -514,8 +515,8 @@ export default function Tenants() {
width: '150px'
}}
onFocus={(e) => {
e.target.style.borderColor = '#C01227';
e.target.style.boxShadow = '0 0 0 3px rgba(192, 18, 39, 0.12)';
e.target.style.borderColor = '#0A1317';
e.target.style.boxShadow = '0 0 0 3px rgba(10, 19, 23, 0.12)';
}}
onBlur={(e) => {
e.target.style.borderColor = 'rgba(5, 54, 89, 0.12)';
@@ -552,8 +553,8 @@ export default function Tenants() {
style={{
padding: '16px 0',
cursor: 'pointer',
borderBottom: tab === i ? '2px solid #C01227' : '2px solid transparent',
color: tab === i ? '#C01227' : '#64748b',
borderBottom: tab === i ? '2px solid #0A1317' : '2px solid transparent',
color: tab === i ? '#0A1317' : '#64748b',
fontWeight: tab === i ? 600 : 500,
fontSize: '0.875rem',
transition: 'all 0.2s'
@@ -573,7 +574,7 @@ export default function Tenants() {
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}>
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #C01227', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<div className="spinner" style={{ width: '32px', height: '32px', border: '3px solid #f1f5f9', borderTop: '3px solid #0A1317', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
</div>
) : paged.length === 0 ? (
<EmptyState title="No clients found" caption="Try a different tab or search term, or add a client." />
@@ -604,38 +605,7 @@ export default function Tenants() {
</Table>
)}
{/* Pagination bar */}
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', padding: '12px 24px', borderTop: '1px solid #e2e8f0', gap: '20px', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#475569' }}>
<span>Rows per page:</span>
<select
value={rpp}
onChange={(e) => { setRpp(Number(e.target.value)); setPage(0); }}
className="rpp-select"
>
{[5, 10, 25].map((opt) => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
<span style={{ fontSize: '0.875rem', color: '#475569' }}>
{filtered.length === 0 ? '0-0' : `${page * rpp + 1}-${Math.min((page + 1) * rpp, filtered.length)}`} of {filtered.length}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
label="Prev"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.max(0, p - 1))}
isDisabled={page === 0}
/>
<Button
label="Next"
variant="secondary"
size="sm"
onClick={() => setPage((p) => Math.min(Math.ceil(filtered.length / rpp) - 1, p + 1))}
isDisabled={page >= Math.ceil(filtered.length / rpp) - 1}
/>
</div>
</div>
<TablePagination page={page} rpp={rpp} total={filtered.length} onPageChange={setPage} onRppChange={setRpp} />
</Card>
<ClientFormDialog