implemenation on the bot

This commit is contained in:
2026-08-19 17:08:45 +05:30
parent bdb21766f2
commit 37ca2352e2
52 changed files with 7074 additions and 3568 deletions

View File

@@ -328,3 +328,24 @@
animation: none !important;
}
}
/* --------------------------------------------------------------------------
Package rows
--------------------------------------------------------------------------
Each row is a parcel entry on the wire. Bordered rather than card-nested so
a four-package order doesn't read as four separate sections.
-------------------------------------------------------------------------- */
.co-package {
padding: 12px;
border: 1px solid var(--co-border);
border-radius: var(--co-radius);
background: var(--co-surface);
}
.co-root .co-package-label {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--co-text-3);
}

View File

@@ -113,6 +113,11 @@ const sortLocations = (locations) =>
const PARCEL_CATEGORIES = ['Food', 'Groceries', 'Documents', 'Electronics', 'Clothing & Apparel', 'Medicines', 'Furniture', 'Others'];
const WEIGHT_OPTIONS = ['1-10kgs', '11-20kgs', '21-30kgs'];
// POST /admin/expressbooking's own service tiers. Kept in step with
// assistant/orderActions.js's SERVICE_OPTIONS — these are the API's values,
// not a UI-invented Standard/Express/Same-Day set.
const SERVICE_OPTION_VALUES = ['Normal', 'Fast', 'Superfast'];
const Createorder1 = () => {
const [inputValue2, setInputValue2] = useState('');
const [inputValue3, setInputValue3] = useState('');
@@ -123,7 +128,17 @@ const Createorder1 = () => {
// reason a persistent, visible message instead.
const [locationsStatus, setLocationsStatus] = useState('loading');
const [collectionamt, setCollectionamt] = useState(0);
const [quantity, setQuantity] = useState(1);
// Delivery tier. Was hardcoded to 'Normal' in the submit payload — the
// operator had no way to book a Fast or Superfast delivery from this page at
// all. These three are the API's own service_option values (not the
// Standard/Express/Same-Day set a generic form would offer).
const [serviceOption, setServiceOption] = useState('Normal');
// A parcel entry has NO quantity field (confirmed against
// express-console-api.md) — "N items" is expressed as N entries, which is
// also how the Deliveries page reads it back (`Quantity: b.parcels?.length`).
// So a package list replaces the single category/weight pair: each row
// becomes its own parcel, and `count` expands to that many identical entries.
const [packages, setPackages] = useState([{ category: '', weight: '', description: '', count: 1 }]);
const navigate = useNavigate();
const [open4, setOpen4] = useState(false);
@@ -315,7 +330,7 @@ const Createorder1 = () => {
deliverycity: dropCust.city || '',
deliverylatitude: Number(dropCust.latitude) || 0,
deliverylongitude: Number(dropCust.longitude) || 0,
service_option: 'Normal',
service_option: serviceOption,
finalprice: +totalCharge.toFixed(2) || 0,
notes: otherinstructions || '',
// The parcel object has no numeric quantity field (confirmed against
@@ -325,11 +340,28 @@ const Createorder1 = () => {
// This previously always sent exactly one parcel regardless of what the
// operator typed into the Quantity field, so it silently got dropped —
// matching "I entered 5, deliveries page shows 1".
parcels: Array.from({ length: Math.max(1, Number(quantity) || 1) }, () => ({
itemcategory: category || 'General',
itemdescription: 'Order',
declaredvalue: +totalCharge.toFixed(2) || 0
}))
// One entry per package, expanded by its count. Falls back to the legacy
// single-category behaviour if the operator never touched the package
// section, so an existing habit still works.
parcels: packages.some((pkg) => pkg.category || pkg.description)
? packages.flatMap((pkg) =>
Array.from({ length: Math.max(1, Number(pkg.count) || 1) }, () => ({
itemcategory: pkg.category || 'General',
itemdescription: [pkg.description, pkg.weight].filter(Boolean).join(' · ') || 'Order',
declaredvalue: +totalCharge.toFixed(2) || 0
}))
)
: // Nothing entered in the package section — fall back to the single
// legacy category chip and one parcel. The old free-text Quantity
// field is gone; a package row's `count` is how N items is expressed
// now, so there is no separate quantity state to read.
[
{
itemcategory: category || 'General',
itemdescription: 'Order',
declaredvalue: +totalCharge.toFixed(2) || 0
}
]
};
if (!arr.tenantid) {
opentoast('Pick a tenant before creating an order', 'warning', 2000);
@@ -1172,22 +1204,131 @@ const Createorder1 = () => {
</Card>
</Grid>
{/* ============================== || Cash Collect & Quantity || ============================== */}
{/* ============================== || Package details || ============================== */}
<Card padding={0} elevation="none" className="co-card">
<SectionHeader
color="#6366f1"
icon={<MdInventory2 size={16} />}
title="Collection & Quantity"
subtitle="Cash to collect on delivery, total items"
title="Package Details"
subtitle="What is being sent, and how much to collect"
action={
<Button
label="Add another package"
variant="secondary"
size="sm"
onClick={() => setPackages((prev) => [...prev, { category: '', weight: '', description: '', count: 1 }])}
/>
}
/>
<VStack gap={2} padding={3}>
{packages.map((pkg, i) => (
// Index keys are safe here: rows are only appended or removed,
// never reordered, and each row is fully controlled.
// eslint-disable-next-line react/no-array-index-key
<VStack key={i} className="co-package" gap={2} padding={0}>
<HStack justify="between" vAlign="center" gap={2}>
<Text className="co-package-label">Package {i + 1}</Text>
{packages.length > 1 && (
<Button
label="Remove"
variant="ghost"
size="sm"
onClick={() => setPackages((prev) => prev.filter((_, j) => j !== i))}
/>
)}
</HStack>
<Grid columns={{ minWidth: 170, repeat: 'fit' }} gap={2}>
<Selector
label="Category"
options={PARCEL_CATEGORIES.map((c) => ({ value: c, label: c }))}
value={pkg.category}
onChange={(v) => setPackages((prev) => prev.map((r, j) => (j === i ? { ...r, category: v } : r)))}
placeholder="Select category"
/>
<Selector
label="Weight"
options={WEIGHT_OPTIONS.map((w) => ({ value: w, label: w }))}
value={pkg.weight}
onChange={(v) => setPackages((prev) => prev.map((r, j) => (j === i ? { ...r, weight: v } : r)))}
placeholder="Select weight"
/>
<TextInput
label="Count"
value={String(pkg.count)}
onChange={(v) => setPackages((prev) => prev.map((r, j) => (j === i ? { ...r, count: v.replace(/\D/g, '') } : r)))}
/>
</Grid>
<TextInput
label="Description"
value={pkg.description}
onChange={(v) => setPackages((prev) => prev.map((r, j) => (j === i ? { ...r, description: v } : r)))}
placeholder="2 meal boxes"
/>
</VStack>
))}
<Divider />
<Grid columns={{ minWidth: 200, repeat: 'fit' }} gap={2}>
<Selector
label="Delivery type"
options={SERVICE_OPTION_VALUES.map((v) => ({ value: v, label: v }))}
value={serviceOption}
onChange={setServiceOption}
/>
<TextInput label="Cash Collect" value={String(collectionamt)} onChange={(v) => setCollectionamt(v.replace(/[^\d.]/g, ''))} />
<TextInput label="Quantity" value={String(quantity)} onChange={(v) => setQuantity(v.replace(/\D/g, ''))} />
</Grid>
</VStack>
</Card>
{/* ============================== || Order summary || ============================== */}
{/* Sticky on desktop, static below 1024px (see CreateOrder.css). Reads
straight off form state so it updates as the operator types — there
is no second source of truth to drift from. */}
<Card padding={0} elevation="none" className="co-card co-summary">
<SectionHeader color="#0f172a" icon={<MdCheckCircle size={16} />} title="Order Summary" subtitle="Updates as you fill the form" />
<VStack gap={2} padding={3}>
<HStack className="co-summary-leg" padding={0} gap={0}>
<span className="co-journey-dot" data-end="pickup" />
<VStack gap={0} padding={0}>
<Text className="co-summary-muted">Pickup</Text>
<Text className="co-summary-leg-text">{pickCust.address || '—'}</Text>
</VStack>
</HStack>
<HStack className="co-summary-leg" padding={0} gap={0}>
<span className="co-journey-dot" data-end="drop" />
<VStack gap={0} padding={0}>
<Text className="co-summary-muted">Drop</Text>
<Text className="co-summary-leg-text">{dropCust.address || '—'}</Text>
</VStack>
</HStack>
<Divider />
<HStack className="co-price-row" padding={0} gap={0}>
<span>Schedule</span>
<b>{selectedtime ? `${dayjs(startdate).format('DD MMM')} · ${selectedtime}` : '—'}</b>
</HStack>
<HStack className="co-price-row" padding={0} gap={0}>
<span>Packages</span>
<b>
{packages.reduce((n, pkg) => n + Math.max(1, Number(pkg.count) || 1), 0)}
{packages[0]?.weight ? ` · ${packages[0].weight}` : ''}
</b>
</HStack>
<HStack className="co-price-row" padding={0} gap={0}>
<span>Service</span>
<b>{serviceOption}</b>
</HStack>
<HStack className="co-price-row" padding={0} gap={0}>
<span>Distance</span>
<b>{showDistance ? `${distance} km${durationMin ? ` · ${durationMin} min` : ''}` : '—'}</b>
</HStack>
<HStack className="co-price-total" padding={0} gap={0}>
<span className="co-price-total-label">Estimated total</span>
<span className="co-price-total-value">{showDistance ? `₹${Number(totalCharge || 0).toFixed(2)}` : '—'}</span>
</HStack>
</VStack>
</Card>
{/* ================================================= || Notes || ================================================= */}
<Card padding={0} elevation="none" className="co-card">
<SectionHeader

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, useRef } from 'react';
import { getAdminPricing, getAdminTenants, getTenantLocations, getAdminCustomers, createExpressBookingBulk } from 'pages/api/doormileApi';
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import Papa from 'papaparse';
import { SHEET_HEADER_MAP, requiredSheetColumns, normalizeHeader, cleanReceiverName } from 'utils/bulkOrderColumns';
import * as XLSX from 'xlsx';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router';
@@ -606,30 +607,11 @@ const MultipleOrders = () => {
};
// ============================== CSV / XLSX upload ==============================
const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name);
const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, '');
const headerMap = {
'pickupdate(yyyy-mmm-dd)': 'date',
'sendername*': 'locationname',
'senderphone*': 'locationcontact',
'senderaddress*': 'locationaddress',
'receivername*': 'firstname',
receiverphone: 'contactno',
'receiveralternatephone*': 'altcontactno',
receiverfulladdress: 'address',
// Best-effort — no live sample sheet to confirm these are the tenant's
// actual column headers. Without them deliverycity/deliverypincode in
// the submit payload were always sent blank; if these names are wrong,
// behavior is unchanged from before (still blank), not worse.
receivercity: 'city',
receiverpincode: 'postcode',
receiverlatitude: 'latitude',
receiverlongitude: 'longitude',
'itemdescription*': 'description',
Quantity: 'quantity',
' Collect Cash': 'collectionamt'
};
// The map, the header normaliser and the name cleaner now live in
// utils/bulkOrderColumns.js so Doormile AI's file upload reads the exact same
// sheet this page does. Behaviour here is unchanged — the definitions moved,
// they were not rewritten.
const headerMap = SHEET_HEADER_MAP;
const handleFileDirectUpload = (event) => {
try {
@@ -660,7 +642,7 @@ const MultipleOrders = () => {
}
return newRow;
});
const requiredCols = Object.keys(headerMap).filter((k) => k.trim().endsWith('*'));
const requiredCols = requiredSheetColumns();
const missingRequired = requiredCols.filter((clientCol) => !headers.includes(normalizeHeader(clientCol)));
if (missingRequired.length > 0) {
opentoast(`Missing columns: ${missingRequired.join(', ')}`, 'warning', 3000);

View File

@@ -63,9 +63,8 @@ import StatusBadge from 'components/nearle_components/StatusBadge';
import StatusTabs from 'components/nearle_components/StatusTabs';
import TableLoader from 'components/nearle_components/TableLoader';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import { PageShell, KpiGrid, DataCard } from 'components/nearle_components/PageLayout';
import { PageShell, DataCard } from 'components/nearle_components/PageLayout';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { fetchPercentageData, createAutomationDeliveries, getallriders, buildMilerLookup, notifyRider } from '../../api/api';
@@ -931,25 +930,6 @@ const Orders = () => {
<PageShell fill>
<PageHeader title="Orders" subtitle={`Live · ${locoName} · ${datestatus}`} live />
{/* Derived from the SAME statusCounts map the tab badges below read,
by mapping over ORDERS_STATUS_TABS rather than a second parallel
grouping — so this row can never disagree with the tabs. */}
<KpiGrid>
{ORDERS_STATUS_TABS.map((t) => {
const Icon = t.icon;
return (
<StatCard
key={t.status}
title={t.label}
value={t.statuses.reduce((sum, s) => sum + (statusCounts[s] ?? 0), 0)}
icon={<Icon size={20} />}
color={t.color}
loading={isLoadingGetOrders}
/>
);
})}
</KpiGrid>
<DataCard
fill
tabs={