whole ui update

This commit is contained in:
2026-10-10 12:43:37 +05:30
parent de7a833678
commit af3e8ae53e
10 changed files with 150 additions and 120 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@astryxdesign/core/Button';
import { Switch } from '@astryxdesign/core/Switch';
@@ -60,6 +61,8 @@ export interface CatalogueBrowserProps {
isReadOnly?: boolean;
/** Why importing is unavailable, if it is. */
blockedReason?: string;
/** Whether to pull the search and filter controls up into the tab row. */
alignWithTabs?: boolean;
}
/**
@@ -87,6 +90,7 @@ export function CatalogueBrowser({
scope,
blockedReason,
isReadOnly,
alignWithTabs,
}: CatalogueBrowserProps) {
const client = useQueryClient();
@@ -132,6 +136,13 @@ export function CatalogueBrowser({
*/
const [isFiltersOpen, setIsFiltersOpen] = useState(true);
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
useEffect(() => {
if (alignWithTabs) {
setPortalTarget(document.getElementById('inventory-header-actions'));
}
}, [alignWithTabs]);
/**
* 400ms, and it costs more than it looks to get wrong.
*
@@ -461,8 +472,10 @@ export function CatalogueBrowser({
of the row, matching where search sits on Sales, Reports and Products,
rather than above the brand rail where they read as the rail's own
controls instead of the whole catalogue's. */}
<HStack gap={1} align="center" wrap="wrap" justify="end" width="100%">
<div style={{ width: 216, display: 'flex', gap: 8, alignItems: 'center' }}>
{(() => {
const controls = (
<>
<div style={{ pointerEvents: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
<IconButton
label={isFiltersOpen ? 'Hide filters' : 'Show filters'}
icon={<Funnel size={15} />}
@@ -470,7 +483,7 @@ export function CatalogueBrowser({
size="sm"
onClick={() => setIsFiltersOpen((open) => !open)}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ width: 216, minWidth: 0 }}>
<SearchInput
label="Search the catalogue"
value={keyword}
@@ -481,13 +494,34 @@ export function CatalogueBrowser({
</div>
</div>
<div style={{ pointerEvents: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}>
{activeFilters.map((filter) => (
<Token key={filter.key} label={filter.label} size="sm" onRemove={filter.clear} />
))}
{activeFilters.length > 1 ? (
<Button label="Clear all" variant="ghost" size="sm" onClick={clearFilters} />
) : null}
</div>
</>
);
if (alignWithTabs && portalTarget) {
return createPortal(
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>{controls}</div>,
portalTarget
);
}
if (!alignWithTabs) {
return (
<HStack gap={1} align="center" wrap="wrap" justify="end" width="100%">
{controls}
</HStack>
);
}
return null;
})()}
<div className="catalogue-layout" data-rail={isFiltersOpen ? 'open' : 'closed'}>
{isFiltersOpen ? (
@@ -507,7 +541,7 @@ export function CatalogueBrowser({
/>
) : null}
<VStack gap={2}>
<VStack gap={2} className="catalogue-main" style={{ minWidth: 0 }}>
{/* No result-count line. It said "24 products on this page", which is
the page size restated — a number that never changes and answers a
question nobody asked. Where the page belongs in the whole is what

View File

@@ -31,6 +31,7 @@ export function CataloguePanel() {
{...(!tenantid || !locationid
? { blockedReason: 'No outlet is selected, so there is nowhere to file an import.' }
: {})}
alignWithTabs
/>
);
}

View File

@@ -166,7 +166,7 @@ export function StoreOpenControl({
<div style={{ display: 'flex', gap: 8 }}>
<Button
label="Keep open"
variant="ghost"
variant="secondary"
size="sm"
onClick={() => {
setConfirming(false);
@@ -175,7 +175,7 @@ export function StoreOpenControl({
/>
<Button
label={save.isPending ? 'Closing…' : 'Close the branch'}
variant="destructive"
variant="primary"
size="sm"
isLoading={save.isPending}
isDisabled={save.isPending}

View File

@@ -22,9 +22,7 @@ import {
TriangleAlert,
} from 'lucide-react';
import type { DeliveryRow, OrderRow, RiderInfo, TenantLocation } from '@/api/types';
import { matchesWindow, type WindowFilterKey } from '../deliveryWindow';
import { groupByWindow } from '../dispatchModel';
import { WindowFilterSelect } from '../WindowFilterSelect';
import { KpiCard } from '@/components/KpiCard';
import { Tab, TabBar } from '@/components/TabBar';
import { StickyRow } from '@/components/StickyRow';
@@ -101,15 +99,7 @@ export function DispatchBoard({ scope }: { scope: DispatchScope }) {
const [detail, setDetail] = useState<Stop | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'in_progress' | 'delivered' | 'unassigned'>('all');
/*
Which delivery window the board is showing.
Applied to the DELIVERIES, before they become stops — so the map, the
totals, the rider groups and the list all narrow together. Filtering only
the visible list would leave the counters above it describing a different
day's work to the one on screen.
*/
const [windowFilter, setWindowFilter] = useState<WindowFilterKey>('all');
/* One id or the other, never both — see `DispatchScope`. */
const isPartnerBoard = scope.kind === 'partner';
@@ -161,13 +151,7 @@ export function DispatchBoard({ scope }: { scope: DispatchScope }) {
vanished from every chip except "All". The date filter in the top bar already
narrows the day and never drops a row.
*/
const filteredDeliveries = useMemo(
() =>
windowFilter === 'all'
? rawDeliveries
: rawDeliveries.filter((row) => matchesWindow(windowFilter, row)),
[rawDeliveries, windowFilter],
);
const filteredDeliveries = rawDeliveries;
const isOneDay = Boolean(
dates.range.fromdate && dates.range.fromdate === dates.range.todate,
@@ -347,13 +331,6 @@ export function DispatchBoard({ scope }: { scope: DispatchScope }) {
/>
</TabBar>
{/* Beside the board switcher, not down with the status pills.
It narrows every board — the groups, the counters above them and the
map — so it belongs with the controls that change what the whole
screen is about, rather than looking like one more status filter
over a single list. */}
<WindowFilterSelect value={windowFilter} onChange={setWindowFilter} align="end" />
</div>
</StickyRow>

View File

@@ -138,6 +138,7 @@ export function InventoryPage() {
"N awaiting approval" badge went with it, because the Stock requests
tab already carries that count and a number stated twice on one screen
invites the reader to check whether the two agree. */
actions={<div id="inventory-header-actions" style={{ display: 'flex', alignItems: 'center' }} />}
tabs={
/* The tab row carries the Products action on its right, rather than
giving it a row of its own below. It only appears on that tab —

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useDateScope } from '@/components/shell/DateScope';
import { VStack } from '@astryxdesign/core/VStack';
import { PageHeader } from '@/components/PageHeader';
import { useAuth } from '@/auth/AuthContext';
import { useBranchScope } from '../BranchScope';
import { UploadsPanel } from '@/features/uploads/UploadsPanel';
@@ -78,9 +78,7 @@ export function UploadsPage() {
/>
) : null}
<PageHeader
title="Uploads"
/>
<UploadsPanel
tenantid={user?.tenantid ?? 0}
range={dates.range}

View File

@@ -407,7 +407,7 @@ function RiderTable({
<Th>Partner</Th>
<Th>On shift</Th>
<Th>Status</Th>
<Th />
<Th align="right" />
</tr>
</thead>
<tbody>
@@ -445,7 +445,7 @@ function RiderTable({
tone={(rider.status ?? 'Active') === 'Active' ? 'success' : 'warning'}
/>
</Td>
<Td>
<Td align="right">
<Button label="Edit" variant="secondary" size="sm" onClick={() => onEdit(rider)} />
</Td>
</tr>
@@ -502,7 +502,7 @@ function StaffTable({
<Th>Mobile</Th>
{showBranch ? <Th>Branch</Th> : null}
<Th>Status</Th>
<Th />
<Th align="right" />
</tr>
</thead>
<tbody>
@@ -551,7 +551,7 @@ function StaffTable({
<StatusChip status={person.status} />
)}
</Td>
<Td>
<Td align="right">
<span style={{ display: 'inline-flex', gap: 6, justifyContent: 'flex-end' }}>
{person.issetup === false ? <ResendInvite userid={person.userid} /> : null}
<Button label="Edit" variant="secondary" size="sm" onClick={() => onEdit(person)} />
@@ -608,7 +608,7 @@ function TillTable({
{showBranch ? <Th>Branch</Th> : null}
<Th>Shift</Th>
<Th>Status</Th>
<Th />
<Th align="right" />
</tr>
</thead>
<tbody>
@@ -648,7 +648,7 @@ function TillTable({
<Td>
<StatusChip status={person.status} />
</Td>
<Td>
<Td align="right">
<Button
label="Edit"
variant="secondary"
@@ -756,17 +756,20 @@ function Empty({ icon, title, body }: { icon: React.ReactNode; title: string; bo
);
}
function Th({ children }: { children?: React.ReactNode; }) {
function Th({ children, align = 'left' }: { children?: React.ReactNode; align?: 'left' | 'center' | 'right' }) {
return (
<th
style={{
textAlign: 'center',
padding: '10px 12px',
textAlign: align,
padding: '12px 16px',
borderBottom: '1px solid var(--color-line)',
fontSize: 12,
fontSize: 11.5,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.04em',
color: 'var(--color-ink-3)',
whiteSpace: 'nowrap',
backgroundColor: '#f8fafc',
}}
>
{children}
@@ -778,20 +781,22 @@ function Td({
children,
isMuted,
isStrong,
align = 'left',
}: {
children: React.ReactNode;
isMuted?: boolean;
isStrong?: boolean;
align?: 'left' | 'center' | 'right';
}) {
return (
<td
style={{
textAlign: 'center',
padding: '10px 12px',
textAlign: align,
padding: '14px 16px',
borderBottom: '1px solid color-mix(in oklab, var(--color-line) 55%, transparent)',
color: isMuted ? 'var(--color-ink-3)' : 'var(--color-ink-1)',
fontWeight: isStrong ? 600 : 400,
verticalAlign: 'top',
verticalAlign: 'middle',
}}
>
{children}

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useDateScope } from '@/components/shell/DateScope';
import { VStack } from '@astryxdesign/core/VStack';
import { PageHeader } from '@/components/PageHeader';
import { useAuth } from '@/auth/AuthContext';
import { useBranchScope } from '@/features/store-admin/BranchScope';
import { UploadsPanel } from '@/features/uploads/UploadsPanel';
@@ -33,9 +33,7 @@ export function StoreUploadsPage() {
return (
<VStack gap={3}>
<PageHeader
title="Uploads"
/>
<UploadsPanel
tenantid={user?.tenantid ?? 0}
locationid={locationid}

View File

@@ -33,9 +33,11 @@ import { Card } from '@astryxdesign/core/Card';
import { HStack } from '@astryxdesign/core/HStack';
import { Text } from '@astryxdesign/core/Text';
import { VStack } from '@astryxdesign/core/VStack';
import { FileSpreadsheet, UploadCloud } from 'lucide-react';
import { FileSpreadsheet, UploadCloud, Files, CheckCircle2, XCircle, Loader2 } from 'lucide-react';
import { Tab, TabBar } from '@/components/TabBar';
import { SearchInput } from '@/components/SearchInput';
import { PageHeader } from '@/components/PageHeader';
import { SectionHeader } from '@/components/SectionHeader';
import { errorMessage } from '@/api/client';
import {
fetchBatch,
@@ -332,36 +334,12 @@ export function UploadsPanel({
return (
<VStack gap={2}>
{/* ── Toolbar ──────────────────────────────────────────────────────── */}
<div className="up-toolbar">
<TabBar size="sm" aria-label="Filter uploads">
{TABS.map((entry) => (
<Tab
key={entry.id}
label={entry.label}
count={counts[entry.id]}
isActive={tab === entry.id}
onClick={() => setTab(entry.id)}
/>
))}
</TabBar>
<div className="up-toolbar-spacer" />
<SearchInput
label="Search by file name"
value={search}
onChange={setSearch}
placeholder="File name…"
width={230}
/>
{/* "Check again" stood here. It is gone because the page reads the
moving rows by itself now — every fifteen seconds while something is
unfinished, and again the moment the tab comes forward. A refresh
button beside a list that refreshes itself is a button that never
changes anything, and pressing it to find out whether it did is
worse than not having it. */}
{onUpload ? (
{/* ── Headers ──────────────────────────────────────────────────────── */}
<PageHeader
isTabsInline
title="Uploads"
actions={
onUpload ? (
<Button
label="Upload spreadsheet"
variant="primary"
@@ -369,8 +347,37 @@ export function UploadsPanel({
icon={<UploadCloud size={14} />}
onClick={onUpload}
/>
) : null}
</div>
) : null
}
tabs={
<TabBar aria-label="Filter uploads">
{TABS.map((entry) => (
<Tab
key={entry.id}
label={entry.label}
icon={entry.icon}
count={counts[entry.id]}
isActive={tab === entry.id}
onClick={() => setTab(entry.id)}
/>
))}
</TabBar>
}
/>
<VStack gap={1.5}>
<SectionHeader
title="Upload history"
action={
<SearchInput
label="Search by file name"
value={search}
onChange={setSearch}
placeholder="File name…"
width={230}
/>
}
/>
{/* ── The list ─────────────────────────────────────────────────────── */}
{shown.length === 0 ? (
@@ -488,6 +495,7 @@ export function UploadsPanel({
</div>
</Panel>
)}
</VStack>
{open ? (
<UploadDetailDrawer
@@ -514,11 +522,11 @@ export function UploadsPanel({
}
/** The tabs, and the order they read in. */
const TABS: { id: UploadTab; label: string }[] = [
{ id: 'all', label: 'All uploads' },
{ id: 'done', label: 'Successful' },
{ id: 'failed', label: 'Failed' },
{ id: 'running', label: 'In progress' },
const TABS: { id: UploadTab; label: string; icon: React.ReactNode }[] = [
{ id: 'all', label: 'All uploads', icon: <Files size={14} /> },
{ id: 'done', label: 'Successful', icon: <CheckCircle2 size={14} /> },
{ id: 'failed', label: 'Failed', icon: <XCircle size={14} /> },
{ id: 'running', label: 'In progress', icon: <Loader2 size={14} /> },
];
/**

View File

@@ -833,6 +833,11 @@ main {
align-items: start;
}
.catalogue-main {
container-type: inline-size;
container-name: cataloguemain;
}
@media (min-width: 900px) {
/* The rail is fixed, the grid takes what is left — a rail that grows with the
window steals width from the photographs, which are the content. */
@@ -1288,15 +1293,18 @@ main {
cursor: default;
}
@media (min-width: 560px) {
@container cataloguemain (min-width: 440px) {
.catalogue-layout .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1100px) {
@container cataloguemain (min-width: 600px) {
.catalogue-layout .product-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1400px) {
@container cataloguemain (min-width: 780px) {
.catalogue-layout .product-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@container cataloguemain (min-width: 1000px) {
.catalogue-layout .product-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
/* ────────────────────────────────────────────────────────────────────────────
@@ -2109,7 +2117,7 @@ main {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
align-items: start;
align-items: stretch;
}
@media (max-width: 900px) { .sp-cards { grid-template-columns: minmax(0, 1fr); } }