updates on the ui changes and the ai bot implementation

This commit is contained in:
2026-08-19 11:19:51 +05:30
parent 5284fc75bc
commit bdb21766f2
27 changed files with 2607 additions and 278 deletions

View File

@@ -0,0 +1,330 @@
/* ==========================================================================
Create Order — premium logistics layout
--------------------------------------------------------------------------
Restyles the existing Create Order structure into a Stripe/Linear-grade
form: a real page header, a stage indicator, distinct Pickup → Drop journey
cards, and a pricing card that reads as a quote rather than a form field.
Follows the same convention as OrdersRedesign.css / Dispatch.css — a
page-scoped stylesheet imported by its own page — so hover, focus, sticky
and breakpoint behaviour are all expressible, which inline styles can't do.
Every colour is a local `--co-*` token defined once here so the whole page
moves together.
========================================================================== */
.co-root {
--co-brand: #d7193f;
--co-pickup: #10b981;
--co-drop: #f97316;
--co-text: #1f2937;
--co-text-2: #6b7280;
--co-text-3: #9ca3af;
--co-border: #e5e7eb;
--co-surface: #ffffff;
--co-bg: #f8fafc;
--co-radius: 5px;
--co-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
}
/* --------------------------------------------------------------------------
Page header — informative, not tall
-------------------------------------------------------------------------- */
.co-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
padding: 0 0 16px;
border-bottom: 1px solid var(--co-border);
margin-bottom: 20px;
}
.co-root .co-title {
font-size: 24px;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.2;
color: var(--co-text);
}
.co-root .co-subtitle {
font-size: 13px;
line-height: 1.5;
color: var(--co-text-2);
max-width: 60ch;
}
/* --------------------------------------------------------------------------
Stage indicator
--------------------------------------------------------------------------
The layout rules below are NOT scoped under .co-root. Only theming needs
the scope; structure must survive it. When `co-root` was being dropped by
PageShell (it didn't forward className), every scoped rule failed silently
and the chips rendered as run-together plain text — "1Pickup & Drop2Schedule".
Keeping layout unscoped means the worst case is unthemed, not unreadable.
-------------------------------------------------------------------------- */
.co-steps {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 20px;
}
.co-step {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 11px;
border: 1px solid var(--co-border);
border-radius: 999px;
background: var(--co-surface);
font-size: 12.5px;
font-weight: 500;
color: var(--co-text-3);
white-space: nowrap;
}
.co-root .co-step[data-state='active'] {
color: var(--co-brand);
border-color: color-mix(in srgb, var(--co-brand) 35%, transparent);
background: color-mix(in srgb, var(--co-brand) 6%, #fff);
}
.co-root .co-step[data-state='done'] {
color: var(--co-pickup);
border-color: color-mix(in srgb, var(--co-pickup) 30%, transparent);
background: color-mix(in srgb, var(--co-pickup) 6%, #fff);
}
.co-step-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 17px;
height: 17px;
border-radius: 999px;
font-size: 10.5px;
font-weight: 600;
background: currentColor;
color: #fff;
}
.co-root .co-step[data-state='done'] .co-step-num::after {
content: '✓';
}
.co-step-sep {
flex: 0 0 auto;
width: 14px;
height: 1px;
background: var(--co-border);
}
/* --------------------------------------------------------------------------
Journey — Pickup ●───→● Drop
-------------------------------------------------------------------------- */
.co-journey {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 13px;
margin-bottom: 14px;
border: 1px solid var(--co-border);
border-radius: var(--co-radius);
background: var(--co-surface);
font-size: 12.5px;
color: var(--co-text-2);
overflow: hidden;
}
.co-journey-dot {
flex: 0 0 auto;
width: 9px;
height: 9px;
border-radius: 999px;
}
.co-root .co-journey-dot[data-end='pickup'] {
background: var(--co-pickup);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--co-pickup) 18%, transparent);
}
.co-root .co-journey-dot[data-end='drop'] {
background: var(--co-drop);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--co-drop) 18%, transparent);
}
.co-journey-line {
flex: 1 1 auto;
min-width: 24px;
height: 1px;
background: linear-gradient(90deg, var(--co-pickup), var(--co-drop));
opacity: 0.5;
}
.co-journey-label {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: var(--co-text);
}
/* --------------------------------------------------------------------------
Cards — one subtle border, one very light shadow. No heavy chrome.
-------------------------------------------------------------------------- */
.co-root .co-card {
border: 1px solid var(--co-border) !important;
border-radius: var(--co-radius) !important;
background: var(--co-surface);
box-shadow: var(--co-shadow) !important;
overflow: hidden;
}
/* The end-accent strip is what makes Pickup and Drop distinguishable at a
glance without colouring the whole card. */
.co-root .co-card[data-end='pickup'] {
border-top: 2px solid var(--co-pickup) !important;
}
.co-root .co-card[data-end='drop'] {
border-top: 2px solid var(--co-drop) !important;
}
/* --------------------------------------------------------------------------
Pricing — reads as a quote, not a form field
-------------------------------------------------------------------------- */
.co-price-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 7px 0;
font-size: 13px;
color: var(--co-text-2);
border-bottom: 1px dashed var(--co-border);
}
.co-root .co-price-row:last-of-type {
border-bottom: none;
}
.co-root .co-price-row b {
font-weight: 500;
color: var(--co-text);
font-variant-numeric: tabular-nums;
}
.co-price-total {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-top: 10px;
padding-top: 12px;
border-top: 1px solid var(--co-border);
}
.co-root .co-price-total-label {
font-size: 13px;
font-weight: 500;
color: var(--co-text);
}
.co-root .co-price-total-value {
font-size: 24px;
font-weight: 650;
letter-spacing: -0.02em;
color: var(--co-pickup);
font-variant-numeric: tabular-nums;
}
.co-root .co-price-note {
font-size: 11.5px;
color: var(--co-text-3);
}
/* --------------------------------------------------------------------------
Sticky order summary (desktop only)
-------------------------------------------------------------------------- */
.co-root .co-summary {
position: sticky;
top: calc(var(--appshell-header-height, 64px) + 12px);
}
.co-root .co-summary-leg {
display: flex;
gap: 9px;
align-items: flex-start;
}
.co-root .co-summary-leg-text {
min-width: 0;
font-size: 12.5px;
line-height: 1.45;
color: var(--co-text);
overflow-wrap: anywhere;
}
.co-root .co-summary-muted {
font-size: 11.5px;
color: var(--co-text-3);
}
/* --------------------------------------------------------------------------
Field polish — labels above, calm borders, a clear focus ring
-------------------------------------------------------------------------- */
.co-root input:focus-visible,
.co-root textarea:focus-visible {
outline: none;
border-color: var(--co-brand) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--co-brand) 12%, transparent) !important;
}
/* --------------------------------------------------------------------------
Responsive
-------------------------------------------------------------------------- */
@media (max-width: 1023px) {
.co-root .co-summary {
position: static;
}
}
@media (max-width: 640px) {
.co-root .co-title {
font-size: 20px;
}
.co-root .co-header {
gap: 10px;
}
.co-root .co-steps {
gap: 4px;
}
.co-root .co-step {
padding: 5px 9px;
font-size: 11.5px;
}
/* The step separators are decoration; drop them before the chips wrap
awkwardly on a 360px screen. */
.co-root .co-step-sep {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.co-root * {
transition: none !important;
animation: none !important;
}
}

View File

@@ -6,7 +6,7 @@
.total-charge-badge {
background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(101, 56, 122, 0.10) 100%);
border: 1px solid rgba(101, 56, 122, 0.18);
border-radius: 10px;
border-radius: 5px;
padding: 10px 14px;
display: flex;
align-items: center;

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
import { calculateDrivingDistance, calculateTotalCharge, getLastRouteDurationMin } from '../../../utils/distance';
import './CreateOrder.css';
import { FaPhoneAlt, FaUser } from 'react-icons/fa';
import { GiDoorHandle } from 'react-icons/gi';
import { FaLandmarkDome, FaLocationDot } from 'react-icons/fa6';
@@ -140,6 +141,8 @@ const Createorder1 = () => {
const [pricePerKm, setPricePerKm] = useState(0);
const [minKm, setMinKm] = useState(0);
const [totalCharge, setTotalCharge] = useState(0);
// Real routed minutes from OSRM; null when the Haversine fallback ran.
const [durationMin, setDurationMin] = useState(null);
const [category, setCategory] = useState(null);
const [weight, setWeight] = useState('');
// Per express-console-api.md's "Tenant scoping" section: token tenantid
@@ -206,6 +209,10 @@ const Createorder1 = () => {
try {
const roundedDistance = await calculateDrivingDistance(pickup, drop);
setDistance(roundedDistance);
// Real routed minutes from OSRM. Null when the Haversine fallback ran —
// a straight-line estimate has no honest duration, so nothing is shown
// rather than a number derived from a guessed speed.
setDurationMin(getLastRouteDurationMin());
const total = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
setTotalCharge(total);
@@ -606,31 +613,16 @@ const Createorder1 = () => {
<>
{loading && <Loader />}
<PageShell>
<Card padding={3} elevation="none" style={{ position: 'sticky', top: 'var(--appshell-header-height, 64px)', zIndex: 100 }}>
<HStack justify="between" vAlign="center" gap={2} wrap="wrap">
<HStack gap={2} vAlign="center">
<AccentAvatar color={BRAND} selected size={44}>
<MdAddShoppingCart size={22} />
<PageShell className="co-root">
<VStack className="co-header" gap={0} padding={0}>
<HStack justify="between" vAlign="start" gap={2} wrap="wrap" padding={0}>
<HStack gap={2} vAlign="center" padding={0}>
<AccentAvatar color={BRAND} selected size={40}>
<MdAddShoppingCart size={20} />
</AccentAvatar>
<VStack gap={0} padding={0}>
<Text type="heading" weight="bold">
Create Order
</Text>
<HStack gap={1} vAlign="center">
<span
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: '#10b981',
boxShadow: '0 0 0 4px rgba(16,185,129,0.18)'
}}
/>
<Text type="supporting" color="secondary">
Fill pickup, drop, schedule &amp; pricing details
</Text>
</HStack>
<VStack gap={0.5} padding={0}>
<Text className="co-title">Create Order</Text>
<Text className="co-subtitle">Create a new delivery order by entering pickup, drop, schedule and pricing details.</Text>
</VStack>
</HStack>
@@ -710,11 +702,50 @@ const Createorder1 = () => {
</div>
)}
</HStack>
</Card>
</VStack>
{/* Stage indicator. Derived from the form's own state rather than a
wizard step counter — the page is one scrollable form, so this
reports progress instead of gating it. */}
<HStack className="co-steps" padding={0} gap={0}>
{[
{ n: 1, label: 'Pickup & Drop', done: Boolean(pickCust.address && dropCust.address) },
{ n: 2, label: 'Schedule', done: Boolean(selectedtime) },
{ n: 3, label: 'Package', done: Boolean(category) },
{ n: 4, label: 'Review', done: showDistance && totalCharge > 0 }
].map((step, i, all) => {
const active = !step.done && all.slice(0, i).every((prev) => prev.done);
return (
<HStack key={step.n} padding={0} gap={0} vAlign="center">
{i > 0 && <span className="co-step-sep" />}
<span className="co-step" data-state={step.done ? 'done' : active ? 'active' : 'todo'}>
<span className="co-step-num">{step.done ? '' : step.n}</span>
{step.label}
</span>
</HStack>
);
})}
</HStack>
{/* The delivery journey, stated once at the top so the order's shape
is obvious before reading any field. */}
<HStack className="co-journey" padding={0} gap={0} vAlign="center">
<span className="co-journey-dot" data-end="pickup" />
<span className="co-journey-label">{pickCust.address || 'Pickup address'}</span>
<span className="co-journey-line" />
{showDistance && (
<span className="co-price-note">
{distance} km{durationMin ? ` · ${durationMin} min` : ''}
</span>
)}
<span className="co-journey-line" />
<span className="co-journey-label">{dropCust.address || 'Drop address'}</span>
<span className="co-journey-dot" data-end="drop" />
</HStack>
<Grid columns={{ minWidth: 420, repeat: 'fit' }} gap={3}>
{/* ================================================= || Pickup || ================================================= */}
<Card padding={0} elevation="none">
<Card padding={0} elevation="none" className="co-card" data-end="pickup">
<SectionHeader
color="#0ea5e9"
icon={<MdLocationOn size={16} />}
@@ -841,7 +872,7 @@ const Createorder1 = () => {
</Card>
{/* ================================================= || Drop || ================================================= */}
<Card padding={0} elevation="none">
<Card padding={0} elevation="none" className="co-card" data-end="drop">
<SectionHeader
color={BRAND}
icon={<MdLocalShipping size={16} />}
@@ -968,7 +999,7 @@ const Createorder1 = () => {
</Card>
{/* ================================================= || Time || ================================================= */}
<Card padding={0} elevation="none">
<Card padding={0} elevation="none" className="co-card">
<SectionHeader color="#f59e0b" icon={<MdSchedule size={16} />} title="Schedule" subtitle="Pickup date & time slot" />
<VStack gap={2} padding={3}>
<DateInput
@@ -995,7 +1026,7 @@ const Createorder1 = () => {
<div
style={{
padding: 10,
borderRadius: 8,
borderRadius: 5,
border: `1px solid ${DT.divider}`,
background: DT.surfaceAlt,
maxHeight: 150,
@@ -1045,7 +1076,7 @@ const Createorder1 = () => {
</Card>
{/* ================================================= || Distance & Pricing || ================================================= */}
<Card padding={0} elevation="none">
<Card padding={0} elevation="none" className="co-card">
<SectionHeader
color="#10b981"
icon={<MdAttachMoney size={16} />}
@@ -1053,11 +1084,37 @@ const Createorder1 = () => {
subtitle="Auto-calculated from pickup → drop"
/>
<VStack gap={3} padding={3}>
{showDistance && (
<HStack justify="between">
<Badge variant="blue" label={`Distance: ${distance} km`} />
<Badge variant="blue" label={`Charge: ₹${totalCharge.toFixed(2)}`} />
</HStack>
{showDistance ? (
<VStack gap={0} padding={0}>
<HStack className="co-price-row" padding={0} gap={0}>
<span>Distance</span>
<b>{distance} km</b>
</HStack>
{durationMin ? (
<HStack className="co-price-row" padding={0} gap={0}>
<span>Estimated duration</span>
<b>{durationMin} min</b>
</HStack>
) : null}
<HStack className="co-price-row" padding={0} gap={0}>
<span>Base fare</span>
<b>₹{Number(basePrice || 0).toFixed(2)}</b>
</HStack>
<HStack className="co-price-row" padding={0} gap={0}>
<span>
Distance charge
{minKm ? ` · beyond ${minKm} km` : ''}
</span>
<b>₹{Math.max(0, Number(totalCharge || 0) - Number(basePrice || 0)).toFixed(2)}</b>
</HStack>
<HStack className="co-price-total" padding={0} gap={0}>
<span className="co-price-total-label">Total amount</span>
<span className="co-price-total-value">₹{Number(totalCharge || 0).toFixed(2)}</span>
</HStack>
<Text className="co-price-note">Auto-calculated from pickup → drop using this tenant’s pricing.</Text>
</VStack>
) : (
<Text className="co-price-note">Enter both addresses and the distance and price are calculated automatically.</Text>
)}
<VStack gap={1} padding={0}>
@@ -1089,7 +1146,7 @@ const Createorder1 = () => {
<VStack gap={1} padding={0}>
<Text weight="bold">Weight</Text>
<HStack justify="between" gap={1} wrap="wrap" padding={2} style={{ border: '1px solid #eee', borderRadius: 8 }}>
<HStack justify="between" gap={1} wrap="wrap" padding={2} style={{ border: '1px solid #eee', borderRadius: 5 }}>
{WEIGHT_OPTIONS.map((w) => (
<span
key={w}
@@ -1116,7 +1173,7 @@ const Createorder1 = () => {
</Grid>
{/* ============================== || Cash Collect & Quantity || ============================== */}
<Card padding={0} elevation="none">
<Card padding={0} elevation="none" className="co-card">
<SectionHeader
color="#6366f1"
icon={<MdInventory2 size={16} />}
@@ -1132,7 +1189,7 @@ const Createorder1 = () => {
</Card>
{/* ================================================= || Notes || ================================================= */}
<Card padding={0} elevation="none">
<Card padding={0} elevation="none" className="co-card">
<SectionHeader
color={DT.textSecondary}
icon={<MdNotes size={16} />}

View File

@@ -90,7 +90,7 @@ const MetricTile = ({ icon: Icon, label, value, accent, active }) => (
style={{
background: '#fff',
border: '1px solid #eef2f6',
borderRadius: 10,
borderRadius: 5,
padding: '6px 8px',
display: 'flex',
alignItems: 'center',
@@ -103,7 +103,7 @@ const MetricTile = ({ icon: Icon, label, value, accent, active }) => (
style={{
width: 26,
height: 26,
borderRadius: 8,
borderRadius: 5,
background: `${accent}18`,
color: accent,
display: 'flex',
@@ -239,7 +239,7 @@ const SourceTile = ({ icon: Icon, title, sub, accent, disabled, onClick }) => (
textAlign: 'left',
gap: 10,
padding: '10px 12px',
borderRadius: 10,
borderRadius: 5,
border: '1.5px solid #eef2f6',
background: '#fff',
opacity: disabled ? 0.6 : 1,
@@ -250,7 +250,7 @@ const SourceTile = ({ icon: Icon, title, sub, accent, disabled, onClick }) => (
style={{
width: 32,
height: 32,
borderRadius: 8,
borderRadius: 5,
background: `${accent}15`,
color: accent,
display: 'flex',
@@ -1164,13 +1164,13 @@ const MultipleOrders = () => {
gap={2}
vAlign="center"
padding={2}
style={{ border: '1px solid #eef2f6', borderInlineStart: '3px solid #0ea5e9', borderRadius: 10, background: '#fbfcff' }}
style={{ border: '1px solid #eef2f6', borderInlineStart: '3px solid #0ea5e9', borderRadius: 5, background: '#fbfcff' }}
>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
borderRadius: 5,
background: 'linear-gradient(135deg,#0ea5e9,#0284c7)',
color: '#fff',
display: 'flex',
@@ -1312,7 +1312,7 @@ const MultipleOrders = () => {
marginBottom: 8,
background: 'rgba(245,158,11,0.06)',
border: '1.5px solid rgba(245,158,11,0.25)',
borderRadius: 10
borderRadius: 5
}}
>
<VStack gap={0} padding={0}>
@@ -1591,7 +1591,7 @@ const MultipleOrders = () => {
minHeight: 240,
background: 'rgba(24,144,255,0.03)',
border: '1px dashed rgba(24,144,255,0.25)',
borderRadius: 12,
borderRadius: 5,
textAlign: 'center'
}}
>

View File

@@ -1050,7 +1050,13 @@ const Orders = () => {
// readers announce the column's sort state from
// here, not from the clickable content inside.
aria-sort={sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : canSort ? 'none' : undefined}
style={{ width: header.getSize(), minWidth: header.column.columnDef.minSize, maxWidth: header.column.columnDef.maxSize || header.getSize(), textAlign: align, position: 'relative' }}
style={{
width: header.getSize(),
minWidth: header.column.columnDef.minSize,
maxWidth: header.column.columnDef.maxSize || header.getSize(),
textAlign: align,
position: 'relative'
}}
>
<HStack
gap={1}
@@ -1173,7 +1179,10 @@ const Orders = () => {
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{ textAlign: cell.column.columnDef.meta?.align, maxWidth: cell.column.columnDef.maxSize || cell.column.getSize() }}
style={{
textAlign: cell.column.columnDef.meta?.align,
maxWidth: cell.column.columnDef.maxSize || cell.column.getSize()
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>