ui improvement

This commit is contained in:
2026-09-17 17:57:21 +05:30
parent 2cd048e6f0
commit 115a06a02c
79 changed files with 3574 additions and 2186 deletions

View File

@@ -1,6 +1,5 @@
import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Text } from '@astryxdesign/core/Text';
import { VStack } from '@astryxdesign/core/VStack';
import { Bike, Check, Truck } from 'lucide-react';
import { errorMessage } from '@/api/client';
@@ -8,7 +7,7 @@ import { tenantsApi } from '@/api/tenants';
import { useAllPartners, useAppRegions } from '@/queries/hooks';
import { queryKeys } from '@/queries/keys';
import { Drawer } from '@/features/store-admin/Drawer';
import { DrawerButton, DrawerCard, Note, Row, Section } from '@/features/store-admin/drawerKit';
import { Alert, DrawerButton, DrawerCard, Note, Row, Section } from '@/features/store-admin/drawerKit';
/**
* Which delivery partner supplies a merchant's riders.
@@ -89,11 +88,7 @@ export function PartnerAssignDrawer({
}
>
<VStack gap={2}>
{error ? (
<Text type="body" size="sm" style={{ color: 'var(--color-error, #d64545)' }}>
{error}
</Text>
) : null}
{error ? <Alert>{error}</Alert> : null}
<Note icon={<Bike size={15} />}>
A partner supplies riders to this shop. With one set, the assign screen offers the

View File

@@ -1,12 +1,17 @@
import { useState } from 'react';
import { Button } from '@astryxdesign/core/Button';
import { Text } from '@astryxdesign/core/Text';
import { VStack } from '@astryxdesign/core/VStack';
import { Bike, Plus } from 'lucide-react';
import type { Partner, RiderRosterRow } from '@/api/deliveries';
import { usePartnerRiders } from '@/queries/hooks';
import { Drawer } from '@/features/store-admin/Drawer';
import { Badge, DrawerButton, DrawerCard, Note, Row, Section } from '@/features/store-admin/drawerKit';
import {
Badge,
DrawerButton,
DrawerCard,
LinkAction,
Note,
Row,
Section,
} from '@/features/store-admin/drawerKit';
import { RiderDrawer } from '@/features/store-admin/RiderDrawer';
/**
@@ -59,7 +64,11 @@ export function PartnerRidersDrawer({
</>
}
>
<VStack gap={2}>
{/* No `VStack` around this. The drawer body IS a column with the
system's own gap; a second stack inside it set a second, different
one, which is why this drawer's sections sat closer together than
every other drawer's. */}
<>
<Note icon={<Bike size={15} />}>
These riders deliver for every merchant this partner supplies. A rider hired here does
not appear in the on-duty fleet until they open the rider app and start a shift — that
@@ -67,9 +76,7 @@ export function PartnerRidersDrawer({
</Note>
{riders.isLoading ? (
<Text type="body" size="sm" color="secondary">
Reading riders…
</Text>
<Note>Reading riders…</Note>
) : rows.length === 0 ? (
<Section title="No riders yet">
<DrawerCard>
@@ -88,17 +95,17 @@ export function PartnerRidersDrawer({
key={rider.userid}
label={rider.fullname?.trim() || rider.firstname || `Rider ${rider.userid}`}
value={
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 12 }}>
<Badge
label={rider.isonduty ? 'On duty' : 'Off shift'}
colour={rider.isonduty ? '#0f8a5f' : 'var(--color-ink-4)'}
/>
<Button
label="Edit"
variant="ghost"
size="sm"
onClick={() => setEditing(rider)}
/* The success token, not a hex green typed here. The
#0f8a5f this used was a fourth green in a console
that has one. */
colour={
rider.isonduty ? 'var(--color-success, #1f9d55)' : 'var(--color-ink-4)'
}
/>
<LinkAction label="Edit" onClick={() => setEditing(rider)} />
</span>
}
/>
@@ -106,7 +113,7 @@ export function PartnerRidersDrawer({
</DrawerCard>
</Section>
)}
</VStack>
</>
</Drawer>
{editing ? (

View File

@@ -139,7 +139,7 @@ export function ShiftsPanel({ partnerid }: { partnerid: number }) {
</div>
<div className="fleet-body">
<Card padding={0} elevation="low">
<Card padding={0}>
<VStack gap={1.5} padding={2}>
<Text type="label" size="sm" weight="semibold">
Where the fleet is registered
@@ -162,7 +162,7 @@ export function ShiftsPanel({ partnerid }: { partnerid: number }) {
</VStack>
</Card>
<Card padding={0} elevation="low">
<Card padding={0}>
<VStack gap={1.5} padding={2}>
<Text type="label" size="sm" weight="semibold">
The day, rider by rider
@@ -202,7 +202,7 @@ export function ShiftsPanel({ partnerid }: { partnerid: number }) {
</Card>
</div>
<Card padding={0} elevation="low">
<Card padding={0}>
<VStack gap={1} padding={2}>
<HStack gap={1} align="center">
<span className="fleet-note-icon">

View File

@@ -138,7 +138,7 @@ export function OnboardTenantPage() {
</button>
</div>
<Card padding={4} elevation="low">
<Card padding={4}>
<VStack gap={3}>
<HStack align="center" gap={1.5}>
<CheckCircle2 size={24} style={{ color: 'var(--color-success, #10b981)' }} />
@@ -153,7 +153,7 @@ export function OnboardTenantPage() {
</Text>
{created?.tenantid && created?.locationid ? (
<Card padding={3} elevation="low" variant="transparent">
<Card padding={3} variant="transparent">
<StoreQrPanel
tenantid={created.tenantid}
locationid={created.locationid}
@@ -241,7 +241,7 @@ export function OnboardTenantPage() {
</div>
{/* Section 1: Business Details Card */}
<Card padding={0} elevation="low">
<Card padding={0}>
<VStack gap={2} padding={3}>
<div
style={{
@@ -318,7 +318,7 @@ export function OnboardTenantPage() {
</Card>
{/* Section 2: Head Office Card */}
<Card padding={0} elevation="low">
<Card padding={0}>
<VStack gap={2} padding={3}>
<div
style={{

View File

@@ -8,7 +8,6 @@ import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Badge } from '@astryxdesign/core/Badge';
import { Button } from '@astryxdesign/core/Button';
import { Card } from '@astryxdesign/core/Card';
import { HStack } from '@astryxdesign/core/HStack';
import { Table, type TableColumn } from '@astryxdesign/core/Table';
import { Text } from '@astryxdesign/core/Text';
@@ -28,6 +27,8 @@ import { Selector } from '@astryxdesign/core/Selector';
import { districtOptions, isRunning, matchDistrict } from '../tamilNaduDistricts';
import { DataState } from '@/components/DataState';
import { PageHeader } from '@/components/PageHeader';
import { Panel } from '@/components/Panel';
import { SearchInput } from '@/components/SearchInput';
import { TablePager } from '@/components/TablePager';
import { usePaged } from '@/components/usePaged';
import {
@@ -256,7 +257,11 @@ export function PartnersPage() {
/>
{/* Partners Table Card */}
<Card padding={0} elevation="low" style={{ background: '#ffffff', borderRadius: 12 }}>
<Panel
count={paged.total}
countLabel="partners"
footer={<TablePager paged={paged} label="partners" />}
>
<DataState
isLoading={partners.isLoading}
error={null}
@@ -274,9 +279,8 @@ export function PartnersPage() {
dividers="rows"
/>
</div>
<TablePager paged={paged} label="partners" />
</DataState>
</Card>
</Panel>
{/* Drawers */}
{ridersFor ? (
@@ -553,14 +557,15 @@ function PartnerDrawer({ partner, onClose }: { partner: Partner | null; onClose:
<Text type="label" size="sm" weight="semibold">
District
</Text>
<TextInput
{/* Full width, not a fixed one: this sits above the 38-row district
list it filters, inside a drawer, and a 260px box over a list that
fills the panel reads as unrelated to it. */}
<SearchInput
label="Search districts"
isLabelHidden
size="sm"
value={districtSearch}
onChange={setDistrictSearch}
placeholder="Search all 38 districts…"
hasClear
width="full"
/>
<div className="district-list" role="listbox" aria-label="Tamil Nadu districts">
{shown.map((option) => {

View File

@@ -2,7 +2,6 @@ import { useMemo, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Badge } from '@astryxdesign/core/Badge';
import { Button } from '@astryxdesign/core/Button';
import { Card } from '@astryxdesign/core/Card';
import { HStack } from '@astryxdesign/core/HStack';
import { Table, type TableColumn } from '@astryxdesign/core/Table';
import { Text } from '@astryxdesign/core/Text';
@@ -12,10 +11,10 @@ import { DataState } from '@/components/DataState';
import { Freshness } from '@/components/Freshness';
import { KpiCard } from '@/components/KpiCard';
import { PageHeader } from '@/components/PageHeader';
import { SectionHeader } from '@/components/SectionHeader';
import { useOrderSummary, useOrders, useTenantLocations, useTenants } from '@/queries/hooks';
import { branchOrderStats, NO_ORDERS } from '@/features/store-admin/branchStats';
import type { TenantInfo, TenantLocation } from '@/api/types';
import { Panel } from '@/components/Panel';
import { TablePager } from '@/components/TablePager';
import { usePaged } from '@/components/usePaged';
import { Drawer } from '@/features/store-admin/Drawer';
@@ -279,12 +278,13 @@ export function StoreDetailPage() {
</div>
<VStack gap={1.5}>
<SectionHeader
<Panel
title="Branches"
action={<Freshness updatedAt={summary.dataUpdatedAt} isFetching={summary.isFetching} />}
/>
<Card padding={0} elevation="low">
count={paged.total}
countLabel="branches"
actions={<Freshness updatedAt={summary.dataUpdatedAt} isFetching={summary.isFetching} />}
footer={<TablePager paged={paged} label="branches" />}
>
<DataState
isLoading={isLoading}
error={error}
@@ -307,9 +307,8 @@ export function StoreDetailPage() {
dividers="rows"
/>
</div>
<TablePager paged={paged} label="branches" />
</DataState>
</Card>
</Panel>
</VStack>
{isPartnerOpen && tenantid ? (

View File

@@ -6,13 +6,14 @@ import { Card } from '@astryxdesign/core/Card';
import { Table, type TableColumn } from '@astryxdesign/core/Table';
import { HStack } from '@astryxdesign/core/HStack';
import { Text } from '@astryxdesign/core/Text';
import { TextInput } from '@astryxdesign/core/TextInput';
import { VStack } from '@astryxdesign/core/VStack';
import { Building2, Plus, Store, Users } from 'lucide-react';
import { SearchInput } from '@/components/SearchInput';
import { DataState } from '@/components/DataState';
import { KpiCard } from '@/components/KpiCard';
import { PageHeader } from '@/components/PageHeader';
import { SectionHeader } from '@/components/SectionHeader';
import { Tab, TabBar } from '@/components/TabBar';
import { Panel } from '@/components/Panel';
import { useTenants, useTenantsByApproval } from '@/queries/hooks';
import type { TenantInfo } from '@/api/types';
@@ -240,43 +241,66 @@ export function StoresPage() {
/>
</div>
<HStack gap={0.5} wrap="wrap">
<TabButton
<TabBar>
<Tab
label="Directory"
isActive={tab === 'directory'}
onClick={() => setTab('directory')}
/>
<TabButton
<Tab
label="Awaiting approval"
isActive={tab === 'pending'}
badge={pending.data?.length || undefined}
count={pending.data?.length || undefined}
onClick={() => setTab('pending')}
/>
</HStack>
</TabBar>
{tab === 'pending' ? (
<PendingPanel rows={pending.data ?? []} isLoading={pending.isLoading} />
) : (
<VStack gap={1.5}>
<SectionHeader
title="Tenant directory"
note={`${rows.length} shown`}
action={
<div className="toolbar-stack" style={{ maxWidth: 260 }}>
<TextInput
label="Search tenants"
isLabelHidden
value={search}
onChange={setSearch}
placeholder="Search by name, company or city…"
hasClear
/* The title, the count and the search are the panel's header band now.
The search filters THIS table; floating above the card it read as a
page-level control that happened to sit nearby, and the count was a
note beside a heading rather than the figure you check a filter
against. */
<Panel
footer={
<HStack justify="between" align="center" gap={1} wrap="wrap">
<Text type="body" size="sm" color="secondary">
Page {page}
{search ? ` · filtered from ${rows.length} on this page` : ''}
</Text>
<HStack gap={1}>
<Button
label="Previous"
variant="secondary"
size="sm"
isDisabled={page === 1}
onClick={() => setPage((current) => Math.max(1, current - 1))}
/>
</div>
}
/>
<Card padding={0} elevation="low">
<Button
label="Next"
variant="secondary"
size="sm"
isDisabled={(data?.length ?? 0) <= PAGE_SIZE}
onClick={() => setPage((current) => current + 1)}
/>
</HStack>
</HStack>
}
title="Tenant directory"
count={rows.length}
countLabel="tenants"
actions={
<SearchInput
label="Search tenants"
value={search}
onChange={setSearch}
placeholder="Name, company or city…"
width={260}
/>
}
>
<DataState
isLoading={isLoading}
error={error}
@@ -306,31 +330,7 @@ export function StoresPage() {
/>
</div>
</DataState>
</Card>
<HStack justify="between" align="center" gap={1} wrap="wrap">
<Text type="body" size="sm" color="secondary">
Page {page}
{search ? ` · filtered from ${rows.length} on this page` : ''}
</Text>
<HStack gap={1}>
<Button
label="Previous"
variant="secondary"
size="sm"
isDisabled={page === 1}
onClick={() => setPage((current) => Math.max(1, current - 1))}
/>
<Button
label="Next"
variant="secondary"
size="sm"
isDisabled={(data?.length ?? 0) <= PAGE_SIZE}
onClick={() => setPage((current) => current + 1)}
/>
</HStack>
</HStack>
</VStack>
</Panel>
)}
</VStack>
);
@@ -348,7 +348,7 @@ export function StoresPage() {
function PendingPanel({ rows, isLoading }: { rows: TenantInfo[]; isLoading: boolean }) {
if (isLoading) {
return (
<Card padding={0} elevation="low">
<Card padding={0}>
<VStack padding={3}>
<Text type="body" size="sm" color="secondary">
Reading the approval queue…
@@ -360,7 +360,7 @@ function PendingPanel({ rows, isLoading }: { rows: TenantInfo[]; isLoading: bool
if (rows.length === 0) {
return (
<Card padding={0} elevation="low">
<Card padding={0}>
<VStack gap={0.5} padding={3}>
<Text type="label" size="sm" weight="semibold">
Nothing waiting
@@ -379,7 +379,7 @@ function PendingPanel({ rows, isLoading }: { rows: TenantInfo[]; isLoading: bool
These merchants exist but are not approved, so they do not appear in the directory.
Approving is a database change today — the backend has no endpoint for it.
</Text>
<Card padding={0} elevation="low">
<Card padding={0}>
<div className="table-scroll">
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
@@ -409,59 +409,7 @@ function PendingPanel({ rows, isLoading }: { rows: TenantInfo[]; isLoading: bool
);
}
function TabButton({
label,
isActive,
onClick,
badge,
}: {
label: string;
isActive: boolean;
onClick: () => void;
badge?: number;
}) {
return (
<button
type="button"
onClick={onClick}
aria-pressed={isActive}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
height: 32,
padding: '0 12px',
borderRadius: 12,
border: 0,
background: isActive ? 'var(--color-brand-tint)' : 'transparent',
color: isActive ? 'var(--color-brand)' : 'var(--color-ink-3)',
fontSize: 13,
fontWeight: isActive ? 600 : 500,
cursor: 'pointer',
}}
>
{label}
{badge ? (
<span
style={{
minWidth: 18,
height: 18,
padding: '0 5px',
borderRadius: 999,
background: 'var(--color-warning, #b7860b)',
color: '#fff',
fontSize: 11,
fontWeight: 700,
display: 'grid',
placeItems: 'center',
}}
>
{badge}
</span>
) : null}
</button>
);
}
/* The page-local `TabButton` lived here — see `components/TabBar`. */
function Th({ children }: { children?: React.ReactNode }) {
return (

View File

@@ -2,20 +2,37 @@
* The fleet page: a banner, a map beside a gantt, and the gantt itself.
*/
/* The console's small control: a branch picker, a rider-partner picker and the
Shifts day input all wear this.
Its own shape before was 32px at a 7px corner — a seventh radius in an app
that has one. It takes the search box's measurements now, which matters most
on Inventory, where a `.fleet-select` branch picker sits immediately beside a
`SearchInput`: the two were a different height AND a different corner, six
pixels apart. */
.fleet-select {
height: 32px;
height: 38px;
padding: 0 10px;
border: 1px solid var(--color-border);
border-radius: 7px;
background: var(--color-surface);
border: var(--card-border);
border-radius: var(--card-radius-sm);
background: var(--card-bg);
box-shadow: var(--card-shadow);
font: inherit;
font-size: 13px;
color: var(--color-ink-1);
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.fleet-select:hover { border-color: var(--color-ink-4); }
/* The same focus treatment as the search box — a brand border and a 3px tint
ring, not the global 2px outline, so two controls sitting side by side do
not light up in different ways. */
.fleet-select:focus,
.fleet-select:focus-visible {
outline: 2px solid var(--color-brand);
outline-offset: 1px;
outline: none;
border-color: var(--color-brand);
box-shadow: 0 0 0 3px var(--color-brand-tint);
}
/* ── The recommendation banner ───────────────────────────────────────────── */
@@ -30,8 +47,8 @@
padding: 12px 14px;
border: 1px solid var(--color-border);
border-left-width: 3px;
border-radius: 9px;
background: var(--color-surface);
border-radius: var(--card-radius);
background: var(--card-bg);
}
.fleet-banner > svg {