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 <Suspense
fallback={ fallback={
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}> <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> <style>{`@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`}</style>
</div> </div>
} }

View File

@@ -8,7 +8,7 @@ export default function StatCard({ title, value, icon: Icon, color = 'primary',
// Map theme colors to CSS values // Map theme colors to CSS values
const colorMap = { 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)' }, 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)' }, 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)' }, 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', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: 'rgba(192, 18, 39, 0.08)', backgroundColor: 'rgba(10, 19, 23, 0.08)',
color: '#C01227' color: '#0A1317'
}} }}
> >
<Icon size={isCompact ? 14 : 18} /> <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-primary: #0A1317;
--color-text-secondary: #4E606F; --color-text-secondary: #4E606F;
--color-text-disabled: #A4B0BC; --color-text-disabled: #A4B0BC;
--color-text-accent: #C01227; --color-text-accent: #0A1317;
/* Icon tokens */ /* Icon tokens */
--color-icon-primary: #0A1317; --color-icon-primary: #0A1317;
--color-icon-secondary: #4E606F; --color-icon-secondary: #4E606F;
--color-icon-disabled: #A4B0BC; --color-icon-disabled: #A4B0BC;
--color-icon-accent: #C01227; --color-icon-accent: #0A1317;
/* Border tokens */ /* Border tokens */
--color-border: rgba(5, 54, 89, 0.1); --color-border: rgba(5, 54, 89, 0.1);
@@ -44,11 +44,18 @@
--color-track: #CCD3DB; --color-track: #CCD3DB;
--color-shadow: rgba(5, 54, 89, 0.1); --color-shadow: rgba(5, 54, 89, 0.1);
/* Brand accent — Doormile red, replaces the theme-neutral default */ /* Brand accent — black/white, replaces the theme-neutral default. Hover and
--color-accent: #C01227; active states resolve to this token, so it drives the black hover fills
--color-accent-muted: rgba(192, 18, 39, 0.12); 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; --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 */ /* App-level overrides */
background-color: #f8fafc; background-color: #f8fafc;
color: #0f172a; 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 { input:not(.search-bar-input):not([class]):focus, select:not([class]):focus, textarea:not([class]):focus {
border-color: #C01227 !important; border-color: #0A1317 !important;
box-shadow: 0 0 0 3px rgba(192, 18, 39, 0.12) !important; box-shadow: 0 0 0 3px rgba(10, 19, 23, 0.12) !important;
} }
/* Premium Select Dropdown Styling */ /* Premium Select Dropdown Styling */
@@ -135,8 +142,8 @@ select:not([class]) {
} }
.search-bar-form:focus-within { .search-bar-form:focus-within {
border-color: #C01227; border-color: #0A1317;
box-shadow: 0 0 0 4px rgba(192, 18, 39, 0.12), 0 10px 15px -3px rgba(0, 0, 0, 0.05); 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 */ max-width: 380px; /* Expand slightly on focus for premium desktop feel */
} }

View File

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

View File

@@ -79,17 +79,17 @@ export default function Sidebar({ isCollapsed, onCollapsedChange }) {
height: 18px !important; height: 18px !important;
} }
.doormile-side-nav .astryx-side-nav-item[aria-label]:hover { .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 { .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; outline-offset: 2px;
} }
.doormile-side-nav .astryx-side-nav-item[aria-label][data-selected='selected'] { .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 { .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 /* Expanded rows: rounded hover/selected states with a left accent
@@ -107,37 +107,37 @@ export default function Sidebar({ isCollapsed, onCollapsedChange }) {
position: relative; position: relative;
margin-inline: 2px; margin-inline: 2px;
height: auto; height: auto;
padding-block: 8px !important; padding-block: 12px !important;
transition: background-color 0.15s ease, transform 0.15s ease; transition: background-color 0.15s ease, transform 0.15s ease;
} }
.doormile-side-nav .astryx-side-nav-item:not([aria-label])::before { .doormile-side-nav .astryx-side-nav-item:not([aria-label])::before {
content: ''; content: '';
position: absolute; position: absolute;
left: -2px; left: -2px;
top: 8px; top: 12px;
bottom: 8px; bottom: 12px;
width: 3px; width: 3px;
border-radius: 3px; border-radius: 3px;
background-color: #C01227; background-color: #0A1317;
opacity: 0; opacity: 0;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
} }
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):hover { .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); transform: translateX(2px);
} }
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):focus-visible { .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; outline-offset: 2px;
} }
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] { .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 { .doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected']::before {
opacity: 1; opacity: 1;
} }
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] .astryx-icon { .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 /* 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; padding-block: 8px !important;
} }
/* Collapse/expand toggle (the "<" / ">" chevron button). It sits /* Collapse/expand toggle (the "<" / ">" chevron button). Give it a
right on the sidebar's edge, so the default square ghost-button clean circular hover, centered on the same axis as the nav icons
hover clips against that border and looks broken. Give it a above it (the rail's own 8px inline padding already keeps it clear
clean circular hover detached from the edge instead. */ 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*="sidebar"],
.doormile-side-nav button[aria-label*="navigation"] { .doormile-side-nav button[aria-label*="navigation"] {
border-radius: 50% !important; border-radius: 50% !important;
transition: background-color 0.15s ease !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*="sidebar"]:hover,
.doormile-side-nav button[aria-label*="navigation"]: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> `}</style>
</> </>

View File

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

View File

@@ -22,6 +22,7 @@ import { TextInput } from '@astryxdesign/core/TextInput';
import { Selector } from '@astryxdesign/core/Selector'; import { Selector } from '@astryxdesign/core/Selector';
import { Switch } from '@astryxdesign/core/Switch'; import { Switch } from '@astryxdesign/core/Switch';
import { Badge } from '@astryxdesign/core/Badge'; import { Badge } from '@astryxdesign/core/Badge';
import { Divider } from '@astryxdesign/core/Divider';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
@@ -98,15 +99,15 @@ function Section({ icon: Icon, title, subtitle, danger = false, children }) {
alignItems: 'center', alignItems: 'center',
padding: '18px 24px', padding: '18px 24px',
borderBottom: '1px solid rgba(5, 54, 89, 0.06)', 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' 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} /> <Icon size={18} />
</div> </div>
<div> <div>
<Heading level={5}>{title}</Heading> <Heading level={3}>{title}</Heading>
{subtitle && <Text type="supporting" color="secondary">{subtitle}</Text>} {subtitle && <Text type="supporting" color="secondary">{subtitle}</Text>}
</div> </div>
</div> </div>
@@ -117,22 +118,25 @@ function Section({ icon: Icon, title, subtitle, danger = false, children }) {
function Row({ label, description, controlAlign = 'stretch', children }) { function Row({ label, description, controlAlign = 'stretch', children }) {
return ( 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', flexDirection: 'column', gap: '2px', width: '240px', flexShrink: 0 }}> <div style={{ display: 'flex', alignItems: 'center', padding: '16px 0', flexWrap: 'wrap', gap: '12px' }}>
<Text type="body" weight="semibold">{label}</Text> <div style={{ display: 'flex', flexDirection: 'column', gap: '2px', width: '240px', flexShrink: 0 }}>
{description && <Text type="supporting" color="secondary">{description}</Text>} <Text type="body" weight="semibold">{label}</Text>
{description && <Text type="supporting" color="secondary">{description}</Text>}
</div>
<div
style={{
flexGrow: 1,
minWidth: '220px',
display: controlAlign === 'end' ? 'flex' : 'block',
justifyContent: controlAlign === 'end' ? 'flex-end' : undefined
}}
>
{children}
</div>
</div> </div>
<div <Divider className="settings-row-divider" />
style={{ </>
flexGrow: 1,
minWidth: '220px',
display: controlAlign === 'end' ? 'flex' : 'block',
justifyContent: controlAlign === 'end' ? 'flex-end' : undefined
}}
>
{children}
</div>
</div>
); );
} }
@@ -270,9 +274,9 @@ export default function Settings() {
</List> </List>
</Card> </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={{ 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} /> <HelpCircle size={16} />
</div> </div>
<div> <div>
@@ -300,12 +304,12 @@ export default function Settings() {
gap: '20px', gap: '20px',
alignItems: 'center', alignItems: 'center',
padding: '24px', 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' flexWrap: 'wrap'
}} }}
> >
<div <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 ? ( {logoUrl ? (
<img src={logoUrl} alt="Organisation logo" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> <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; grid-column: span 1 !important;
} }
} }
.settings-row:last-child { .settings-row-divider:last-child {
border-bottom: none !important; display: none;
} }
`}</style> `}</style>
</div> </div>

View File

@@ -60,7 +60,7 @@ export default function Login() {
style={{ style={{
position: 'absolute', position: 'absolute',
inset: 0, 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 EmptyState from '@/components/EmptyState';
import StatusChip from '@/components/StatusChip'; import StatusChip from '@/components/StatusChip';
import TabLabelCount from '@/components/TabLabelCount'; import TabLabelCount from '@/components/TabLabelCount';
import TablePagination from '@/components/TablePagination';
import { fetchClients } from '@/utils/apiClient'; import { fetchClients } from '@/utils/apiClient';
import ClientFormDialog from '../tenants/ClientFormDialog'; import ClientFormDialog from '../tenants/ClientFormDialog';
@@ -225,7 +226,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Customer Details */} {/* Customer Details */}
<div> <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={{ display: 'flex', gap: '16px', alignItems: 'flex-end', flexWrap: 'wrap' }}>
<div style={{ flexGrow: 1, minWidth: '200px' }}> <div style={{ flexGrow: 1, minWidth: '200px' }}>
<Field label="Select Client" inputID="booking-client-input"> <Field label="Select Client" inputID="booking-client-input">
@@ -258,7 +259,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Assigned Provider */} {/* Assigned Provider */}
<div> <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"> <FormLayout direction="horizontal">
<Selector <Selector
label="Provider Name" placeholder="Select Provider" hasClear label="Provider Name" placeholder="Select Provider" hasClear
@@ -278,7 +279,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Pickup Location */} {/* Pickup Location */}
<div> <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"> <FormLayout direction="horizontal">
<TextInput label="Pickup Address" isRequired value={formData.pickupaddress} onChange={set('pickupaddress')} /> <TextInput label="Pickup Address" isRequired value={formData.pickupaddress} onChange={set('pickupaddress')} />
<TextInput label="Pickup Pincode" isRequired value={formData.pickuppincode} onChange={set('pickuppincode')} /> <TextInput label="Pickup Pincode" isRequired value={formData.pickuppincode} onChange={set('pickuppincode')} />
@@ -287,7 +288,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Delivery Destination */} {/* Delivery Destination */}
<div> <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> <FormLayout>
<TextInput label="Delivery Address" value={formData.deliveryaddress} onChange={set('deliveryaddress')} /> <TextInput label="Delivery Address" value={formData.deliveryaddress} onChange={set('deliveryaddress')} />
<FormLayout direction="horizontal"> <FormLayout direction="horizontal">
@@ -305,7 +306,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Parcel Details */} {/* Parcel Details */}
<div> <div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}> <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" /> <Button variant="secondary" size="sm" icon={<Plus size={12} />} onClick={addParcel} label="Add Parcel" />
</div> </div>
@@ -349,7 +350,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Price & Insurance */} {/* Price & Insurance */}
<div> <div>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', marginBottom: '12px', flexWrap: 'wrap' }}> <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" /> <Button variant="secondary" size="sm" onClick={handleCheckPrice} label="Check Price Estimate" />
</div> </div>
{quoteInfo && ( {quoteInfo && (
@@ -388,7 +389,7 @@ function BookingFormDialog({ open, onClose, onSave, clients, surveys }) {
{/* Additional info */} {/* Additional info */}
<div> <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"> <FormLayout direction="horizontal">
<Selector <Selector
label="Select Speed" label="Select Speed"
@@ -582,8 +583,8 @@ export default function Bookings() {
style={{ style={{
padding: '16px 0', padding: '16px 0',
cursor: 'pointer', cursor: 'pointer',
borderBottom: sourceFilter === tab.value ? '2px solid #C01227' : '2px solid transparent', borderBottom: sourceFilter === tab.value ? '2px solid #0A1317' : '2px solid transparent',
color: sourceFilter === tab.value ? '#C01227' : '#64748b', color: sourceFilter === tab.value ? '#0A1317' : '#64748b',
fontWeight: sourceFilter === tab.value ? 600 : 500, fontWeight: sourceFilter === tab.value ? 600 : 500,
fontSize: '0.875rem', fontSize: '0.875rem',
transition: 'all 0.2s' transition: 'all 0.2s'
@@ -596,7 +597,7 @@ export default function Bookings() {
{loading ? ( {loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}> <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> </div>
) : displayBookings.length === 0 ? ( ) : displayBookings.length === 0 ? (
<EmptyState title="No bookings found" caption="Try a different filter or search term, or create a new booking." /> <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 && ( {!loading && displayBookings.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', padding: '12px 24px', borderTop: '1px solid #e2e8f0', gap: '20px', flexWrap: 'wrap' }}> <TablePagination page={page} rpp={rpp} total={displayBookings.length} onPageChange={setPage} onRppChange={setRpp} />
<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>
)} )}
</Card> </Card>

View File

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

View File

@@ -20,10 +20,10 @@ export default function Error404() {
}} }}
> >
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', alignItems: 'center' }}> <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 404
</span> </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> <Heading level={2}>Page Not Found</Heading>
<div style={{ maxWidth: '460px' }}> <div style={{ maxWidth: '460px' }}>
<Text type="body" color="secondary"> <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' }}> <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 500
</span> </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> <Heading level={2}>Internal Server Error</Heading>
<div style={{ maxWidth: '460px' }}> <div style={{ maxWidth: '460px' }}>
<Text type="body" color="secondary"> <Text type="body" color="secondary">

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ import { Banner } from '@astryxdesign/core/Banner';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard'; import StatCard from '@/components/StatCard';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import TablePagination from '@/components/TablePagination';
function isQuoted(row) { function isQuoted(row) {
return !!(row.rate_per_kg && !String(row.rate_per_kg).toLowerCase().includes('not answered')); 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 }) { function SectionCard({ icon: Icon, title, children }) {
return ( return (
<div style={{ height: '100%', borderRadius: '8px', border: '1px solid #e2e8f0', backgroundColor: '#ffffff', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}> <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)' }}> <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: '#c01227' }} /> <Icon size={12} style={{ color: '#0a1317' }} />
<span style={{ fontWeight: 700, color: '#1e293b', fontSize: '0.64rem', letterSpacing: '0.6px', textTransform: 'uppercase' }}>{title}</span> <span style={{ fontWeight: 700, color: '#1e293b', fontSize: '0.64rem', letterSpacing: '0.6px', textTransform: 'uppercase' }}>{title}</span>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '8px 10px' }}>{children}</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={{ borderTop: '1px dashed #e2e8f0', paddingTop: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}> <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" /> <Button size="sm" variant="secondary" icon={<Plus size={12} />} onClick={addPincode} label="Add Pincode" />
</div> </div>
{(formData.pincodes || []).map((pincode, index) => ( {(formData.pincodes || []).map((pincode, index) => (
@@ -347,7 +348,7 @@ function SurveyFormDialog({ open, onClose, onSave, initialData }) {
{/* Pricing slabs */} {/* Pricing slabs */}
<div style={{ borderTop: '1px dashed #e2e8f0', paddingTop: '16px' }}> <div style={{ borderTop: '1px dashed #e2e8f0', paddingTop: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}> <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" /> <Button size="sm" variant="secondary" icon={<Plus size={12} />} onClick={addSlab} label="Add Slab" />
</div> </div>
{(formData.slabs || []).map((slab, index) => ( {(formData.slabs || []).map((slab, index) => (
@@ -419,11 +420,11 @@ function BranchCard({ row, onEdit, onDelete, users }) {
}} }}
className="branch-card" 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 */} {/* Left: Location & Contact */}
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', minWidth: '200px' }}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center', minWidth: '160px', flexShrink: 0 }}>
<div style={{ padding: '6px', borderRadius: '6px', backgroundColor: 'rgba(192, 18, 39, 0.05)', color: '#c01227', display: 'flex' }}> <div style={{ padding: '6px', borderRadius: '6px', backgroundColor: 'rgba(10, 19, 23, 0.05)', color: '#0a1317', display: 'flex' }}>
<MapPin size={14} /> <MapPin size={14} />
</div> </div>
<div> <div>
@@ -442,8 +443,8 @@ function BranchCard({ row, onEdit, onDelete, users }) {
</div> </div>
{/* Middle: Rates */} {/* Middle: Rates */}
<div style={{ display: 'flex', gap: '16px', flexGrow: 1 }}> <div style={{ display: 'flex', gap: '16px', flexGrow: 1, overflow: 'hidden' }}>
<div style={{ width: '130px', flexShrink: 0, 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> <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 /> <Pill label={row.rate_per_kg} color={quoted ? 'success' : 'warning'} truncate />
</div> </div>
@@ -534,9 +535,9 @@ function BranchCard({ row, onEdit, onDelete, users }) {
fontSize: '0.7rem', fontSize: '0.7rem',
fontWeight: 600, fontWeight: 600,
borderRadius: '4px', borderRadius: '4px',
color: '#c01227', color: '#0a1317',
border: '1px solid rgba(192, 18, 39, 0.2)', border: '1px solid rgba(10, 19, 23, 0.2)',
backgroundColor: 'rgba(192, 18, 39, 0.05)', backgroundColor: 'rgba(10, 19, 23, 0.05)',
textDecoration: 'none', textDecoration: 'none',
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
@@ -589,7 +590,7 @@ function CompanyGroup({ companyName, branches, onEdit, onDelete, users, isLast }
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
> >
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}> <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()} {(companyName || 'A')[0].toUpperCase()}
</div> </div>
<div> <div>
@@ -680,8 +681,8 @@ export default function Survey() {
} }
}; };
const filtered = data.filter(r => const filtered = data.filter(r =>
(r.company || '').toLowerCase().includes(search.toLowerCase()) || (r.company || '').toLowerCase().includes(search.toLowerCase()) ||
(r.area || '').toLowerCase().includes(search.toLowerCase()) || (r.area || '').toLowerCase().includes(search.toLowerCase()) ||
(r.phone || '').toLowerCase().includes(search.toLowerCase()) (r.phone || '').toLowerCase().includes(search.toLowerCase())
); );
@@ -798,38 +799,7 @@ export default function Survey() {
</div> </div>
)} )}
{/* Pagination Section */} <TablePagination page={page} rpp={rowsPerPage} total={companyKeys.length} onPageChange={setPage} onRppChange={setRowsPerPage} unitLabel="Companies" />
<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>
</Card> </Card>
<SurveyFormDialog <SurveyFormDialog

View File

@@ -26,6 +26,7 @@ import PageHeader from '@/components/PageHeader';
import StatusChip from '@/components/StatusChip'; import StatusChip from '@/components/StatusChip';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import TabLabelCount from '@/components/TabLabelCount'; import TabLabelCount from '@/components/TabLabelCount';
import TablePagination from '@/components/TablePagination';
import { fetchUsers, deleteUser } from '@/utils/apiClient'; import { fetchUsers, deleteUser } from '@/utils/apiClient';
import { toUser } from '@/utils/mappers'; import { toUser } from '@/utils/mappers';
import { titleCase } from '@/utils/format'; import { titleCase } from '@/utils/format';
@@ -136,7 +137,7 @@ export default function TeamUsers() {
const columns = useMemo(() => [ const columns = useMemo(() => [
{ {
key: '_rowNumber', key: '_rowNumber',
header: <div style={{ paddingLeft: '24px', paddingTop: '12px' }}>S.No</div>, header: <div style={{ paddingLeft: '24px' }}>S.No</div>,
width: pixel(72), width: pixel(72),
renderCell: (r) => ( renderCell: (r) => (
<div style={{ paddingLeft: '24px' }}> <div style={{ paddingLeft: '24px' }}>
@@ -146,20 +147,20 @@ export default function TeamUsers() {
}, },
{ {
key: 'name', key: 'name',
header: <div style={{ padding: '12px 16px 0' }}>User</div>, header: <div style={{ padding: '0 12px' }}>User</div>,
width: proportional(2), width: proportional(2),
renderCell: (r) => ( renderCell: (r) => (
<div style={{ padding: '10px 16px' }}> <div style={{ padding: '2px 12px' }}>
<UserIdentity name={r.name} email={r.email} /> <UserIdentity name={r.name} email={r.email} />
</div> </div>
) )
}, },
{ {
key: 'email', key: 'email',
header: <div style={{ padding: '12px 16px 0' }}>Email</div>, header: <div style={{ padding: '0 12px' }}>Email</div>,
width: proportional(2), width: proportional(2),
renderCell: (r) => ( renderCell: (r) => (
<div style={{ padding: '10px 16px' }}> <div style={{ padding: '2px 12px' }}>
{r.email ? ( {r.email ? (
<a href={`mailto:${r.email}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', textDecoration: 'none' }} className="link-hover"> <a href={`mailto:${r.email}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', textDecoration: 'none' }} className="link-hover">
<Mail size={14} style={{ color: '#94a3b8' }} /> <Mail size={14} style={{ color: '#94a3b8' }} />
@@ -171,10 +172,10 @@ export default function TeamUsers() {
}, },
{ {
key: 'phone', key: 'phone',
header: <div style={{ padding: '12px 16px 0' }}>Phone</div>, header: <div style={{ padding: '0 12px' }}>Phone</div>,
width: proportional(1), width: proportional(1),
renderCell: (r) => ( renderCell: (r) => (
<div style={{ padding: '10px 16px' }}> <div style={{ padding: '2px 12px' }}>
{r.phone ? ( {r.phone ? (
<a href={`tel:${r.phone}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', textDecoration: 'none' }} className="link-hover"> <a href={`tel:${r.phone}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', textDecoration: 'none' }} className="link-hover">
<Phone size={14} style={{ color: '#94a3b8' }} /> <Phone size={14} style={{ color: '#94a3b8' }} />
@@ -186,17 +187,17 @@ export default function TeamUsers() {
}, },
{ {
key: 'role', key: 'role',
header: <div style={{ padding: '12px 16px 0' }}>Role</div>, header: <div style={{ padding: '0 12px' }}>Role</div>,
width: pixel(140), width: pixel(140),
renderCell: (r) => ( renderCell: (r) => (
<div style={{ padding: '10px 16px' }}> <div style={{ padding: '2px 12px' }}>
<RoleCell role={r.role} /> <RoleCell role={r.role} />
</div> </div>
) )
}, },
{ {
key: 'actions', key: 'actions',
header: <div style={{ paddingRight: '24px', paddingTop: '12px' }}>Actions</div>, header: <div style={{ paddingRight: '24px' }}>Actions</div>,
width: pixel(120), width: pixel(120),
align: 'end', align: 'end',
renderCell: (r) => ( renderCell: (r) => (
@@ -277,8 +278,8 @@ export default function TeamUsers() {
style={{ style={{
padding: '16px 0', padding: '16px 0',
cursor: 'pointer', cursor: 'pointer',
borderBottom: tab === i ? '2px solid #C01227' : '2px solid transparent', borderBottom: tab === i ? '2px solid #0A1317' : '2px solid transparent',
color: tab === i ? '#C01227' : '#64748b', color: tab === i ? '#0A1317' : '#64748b',
fontWeight: tab === i ? 600 : 500, fontWeight: tab === i ? 600 : 500,
fontSize: '0.875rem', fontSize: '0.875rem',
transition: 'all 0.2s' transition: 'all 0.2s'
@@ -301,7 +302,7 @@ export default function TeamUsers() {
{loading ? ( {loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}> <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> </div>
) : paged.length === 0 ? ( ) : paged.length === 0 ? (
<EmptyState title="No team users found" caption="Try a different role or search term, or add a user." /> <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> </div>
)} )}
{/* Pagination bar */} <TablePagination page={page} rpp={rpp} total={filtered.length} onPageChange={setPage} onRppChange={setRpp} />
<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>
</Card> </Card>
<UserFormDialog <UserFormDialog

View File

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