implemenation on the bot
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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={
|
||||
|
||||
Reference in New Issue
Block a user