updates on the full ui and followed by astryx
This commit is contained in:
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
44
src/components/TablePagination.jsx
Normal file
44
src/components/TablePagination.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,22 +118,25 @@ 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', flexDirection: 'column', gap: '2px', width: '240px', flexShrink: 0 }}>
|
||||
<Text type="body" weight="semibold">{label}</Text>
|
||||
{description && <Text type="supporting" color="secondary">{description}</Text>}
|
||||
<>
|
||||
<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>}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
minWidth: '220px',
|
||||
display: controlAlign === 'end' ? 'flex' : 'block',
|
||||
justifyContent: controlAlign === 'end' ? 'flex-end' : undefined
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
minWidth: '220px',
|
||||
display: controlAlign === 'end' ? 'flex' : 'block',
|
||||
justifyContent: controlAlign === 'end' ? 'flex-end' : undefined
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
|
||||
@@ -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%)'
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -680,8 +681,8 @@ export default function Survey() {
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = data.filter(r =>
|
||||
(r.company || '').toLowerCase().includes(search.toLowerCase()) ||
|
||||
const filtered = data.filter(r =>
|
||||
(r.company || '').toLowerCase().includes(search.toLowerCase()) ||
|
||||
(r.area || '').toLowerCase().includes(search.toLowerCase()) ||
|
||||
(r.phone || '').toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user