diff --git a/src/api/doormile/queries.js b/src/api/doormile/queries.js
index 7d26ad6..78025e8 100644
--- a/src/api/doormile/queries.js
+++ b/src/api/doormile/queries.js
@@ -87,6 +87,7 @@ const BOOKING_STATUS_TO_DELIVERY_STATUS = {
arrived: 'arrived',
converted_to_consignment: 'picked',
collected_by_miler: 'picked',
+ collected: 'picked',
picked: 'picked',
out_for_delivery: 'active',
in_transit: 'active',
@@ -115,7 +116,7 @@ const BOOKING_STATUS_TO_DELIVERY_STATUS = {
export const deriveDeliveryStatus = (bookingOrStatus, consignmentMapOrReachedAt) => {
if (!bookingOrStatus) return 'pending';
- // If passed a raw status string
+ // If passed a simple status string
if (typeof bookingOrStatus === 'string') {
const raw = bookingOrStatus.trim().toLowerCase();
if (raw === 'pickup_scheduled') {
@@ -134,27 +135,29 @@ export const deriveDeliveryStatus = (bookingOrStatus, consignmentMapOrReachedAt)
const cStatus = consignmentStatusFor(b, typeof consignmentMapOrReachedAt === 'object' && consignmentMapOrReachedAt instanceof Map ? consignmentMapOrReachedAt : undefined);
const reached = b.reachedat ?? b.reached_at ?? b.reachedAt ?? b.reachedtime ?? b.reached_time;
- // If booking is Converted_To_Consignment, switch directly to consignmentstatus:
- // Converted_To_Consignment is a handoff marker, not a display state.
- if (rawBookingStatus === 'converted_to_consignment') {
- if (cStatus) {
- return mapBookingStatusToDeliveryStatus(cStatus);
- }
+ // 1. If consignment status is present, it defines the live state after pickup:
+ // - 'collected_by_miler' / 'collected' / 'picked' -> 'picked' (Picked tab)
+ // - 'out_for_delivery' / 'in_transit' / 'active' -> 'active' (Active tab, rider started delivery)
+ // - 'delivered' -> 'delivered' (Delivered tab)
+ // - 'cancelled' -> 'cancelled' (Cancelled tab)
+ if (cStatus) {
+ const rawC = String(cStatus).trim().toLowerCase();
+ const mappedC = BOOKING_STATUS_TO_DELIVERY_STATUS[rawC];
+ if (mappedC) return mappedC;
+ }
+
+ // 2. Booking level handoff states:
+ if (rawBookingStatus === 'converted_to_consignment' || rawBookingStatus === 'collected_by_miler' || rawBookingStatus === 'picked') {
return 'picked';
}
- // If consignment status is present, it wins:
- if (cStatus) {
- return mapBookingStatusToDeliveryStatus(cStatus);
- }
-
- // Before pickup (booking lifecycle):
+ // 3. Before pickup (booking lifecycle):
if (rawBookingStatus === 'pickup_scheduled') {
const isReached = reached != null && reached !== false && reached !== '';
return isReached ? 'arrived' : 'accepted';
}
- return mapBookingStatusToDeliveryStatus(rawBookingStatus, reached);
+ return BOOKING_STATUS_TO_DELIVERY_STATUS[rawBookingStatus] || rawBookingStatus;
};
export const mapBookingStatusToDeliveryStatus = (statusOrRow, maybeReachedAt) => {
@@ -852,8 +855,8 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
// Pickup_Scheduled + reachedat present -> Arrived
// Pickup_Scheduled + reachedat absent -> Accepted
orderstatus: effectiveDeliveryStatus,
- consignmentstatus: cStatus,
- statusfromconsignment: cStatus != null || String(b.status || '').toLowerCase() === 'converted_to_consignment',
+ consignmentstatus: cStatus || effectiveDeliveryStatus,
+ statusfromconsignment: Boolean(cStatus && !['accepted', 'pending', 'arrived'].includes(effectiveDeliveryStatus)),
droplat: b.deliverylatitude,
droplon: b.deliverylongitude
};
diff --git a/src/components/assistant/DoormileAI.css b/src/components/assistant/DoormileAI.css
index 745eed3..36d46f3 100644
--- a/src/components/assistant/DoormileAI.css
+++ b/src/components/assistant/DoormileAI.css
@@ -953,40 +953,50 @@ body.dai-docked .astryx-layout-content {
}
}
/* --------------------------------------------------------------------------
- Write-action confirm card
+ Write-action confirm card & buttons
-------------------------------------------------------------------------- */
.dai-root .dai-action {
- padding: 10px 11px;
- border: 1px solid var(--dai-border-strong);
- border-radius: 5px;
- background: var(--dai-surface-alt);
+ padding: 12px 14px;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ background: #ffffff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
}
.dai-root .dai-action-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
- width: 26px;
- height: 26px;
- border-radius: 5px;
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
color: #4f46e5;
background: rgba(99, 102, 241, 0.1);
}
.dai-root .dai-action-summary {
font-size: 13px;
- line-height: 1.35;
- font-weight: 550;
+ line-height: 1.4;
+ font-weight: 600;
color: var(--dai-text);
}
.dai-root .dai-action-note {
font-size: 12px;
- color: var(--dai-text-secondary);
+ color: #64748b;
+ line-height: 1.4;
}
.dai-root .dai-ok {
color: #047857;
+ font-weight: 500;
}
.dai-root .dai-err {
color: #b91c1c;
+ font-weight: 500;
+}
+.dai-root .dai-action button {
+ cursor: pointer;
}
/* --------------------------------------------------------------------------
Overflow containment
diff --git a/src/components/assistant/DoormileAI/AIFlowStep.jsx b/src/components/assistant/DoormileAI/AIFlowStep.jsx
index d3f0ad4..6ccdc70 100644
--- a/src/components/assistant/DoormileAI/AIFlowStep.jsx
+++ b/src/components/assistant/DoormileAI/AIFlowStep.jsx
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { VStack } from '@astryxdesign/core/VStack';
import { HStack } from '@astryxdesign/core/HStack';
import { Text } from '@astryxdesign/core/Text';
-import { Button } from '@astryxdesign/core/Button';
+import { Button } from '@/components/ui/button';
import { Selector } from '@astryxdesign/core/Selector';
// ==============================|| Doormile AI — one dropdown step ||============================== //
@@ -70,7 +70,16 @@ const AIFlowStep = ({ step, onChoose, onCancel, draft, isBusy }) => {
? 'This tenant has no saved business locations — add one on the Tenants page before creating an order.'
: step.emptyHint || 'There’s nothing to choose from here.'}
-
+
+
+ Cancel
+
+
);
}
@@ -90,19 +99,26 @@ const AIFlowStep = ({ step, onChoose, onCancel, draft, isBusy }) => {
hasSearch={options.length > 8}
isDisabled={isBusy}
/>
-
+
onChoose(value, chosen)}
- />
-
-
+ >
+ Continue
+
+
+ Cancel
+
+
);
};
diff --git a/src/components/assistant/DoormileAI/AIMessage.jsx b/src/components/assistant/DoormileAI/AIMessage.jsx
index 3d31fff..a619585 100644
--- a/src/components/assistant/DoormileAI/AIMessage.jsx
+++ b/src/components/assistant/DoormileAI/AIMessage.jsx
@@ -6,8 +6,7 @@ import { LuChevronDown } from 'react-icons/lu';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Text } from '@astryxdesign/core/Text';
-import { IconButton } from '@astryxdesign/core/IconButton';
-import { Button } from '@astryxdesign/core/Button';
+import { Button } from '@/components/ui/button';
import { ChatToolCalls } from '@astryxdesign/core/Chat';
import { Spark, Metric, StatGrid, StateBlock, AnswerList } from './AIParts';
@@ -138,35 +137,46 @@ const AssistantMessage = ({ message, onCopy, onSubmitForm, onCancelAction, onCho
{['confirmCustomer', 'confirmOrder', 'confirmBulk', 'confirmAssign'].includes(message.form?.kind) && (
{message.form.status === 'open' ? (
-
+
onSubmitForm(message, message.form.payload || message.form.draft)}
- />
- onCancelAction(message)} />
+ >
+ {message.form.confirmLabel
+ ? message.form.confirmLabel
+ : message.form.kind === 'confirmBulk'
+ ? `Create ${message.form.draft?.rows?.length || 0} orders`
+ : message.form.kind === 'confirmOrder'
+ ? 'Create order'
+ : message.form.kind === 'confirmAssign'
+ ? 'Assign rider'
+ : 'Submit'}
+
+
+ onCancelAction(message)}
+ >
+ Cancel
+
+
{/* A second, wider commit — used when rows were excluded by
default but including them is a legitimate choice. */}
{message.form.altDraft && (
onSubmitForm(message, message.form.altDraft)}
- />
+ >
+ {message.form.altLabel || 'Include everything'}
+
)}
-
+
) : (
{message.form.status === 'done'
diff --git a/src/components/assistant/DoormileAI/AIRowsStep.jsx b/src/components/assistant/DoormileAI/AIRowsStep.jsx
index 2f8fa14..c02916d 100644
--- a/src/components/assistant/DoormileAI/AIRowsStep.jsx
+++ b/src/components/assistant/DoormileAI/AIRowsStep.jsx
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { VStack } from '@astryxdesign/core/VStack';
import { HStack } from '@astryxdesign/core/HStack';
import { Text } from '@astryxdesign/core/Text';
-import { Button } from '@astryxdesign/core/Button';
+import { Button } from '@/components/ui/button';
import { TextArea } from '@astryxdesign/core/TextArea';
import { FileInput } from '@astryxdesign/core/FileInput';
@@ -23,41 +23,38 @@ import { parseBulkFile, templateCsv, downloadCsv } from '@/lib/assistant/bulkFil
//
// What is reported before the operator can continue, and why each matters:
// • rows read — so a truncated or half-empty sheet is visible immediately
-// • columns NOT recognised, by name — an unread price column would otherwise
-// silently re-quote every row
-// • rows repeated inside the file — the bulk endpoint has no idempotency key,
-// so a duplicate that gets through is a second real rider at one door
-
-const PLACEHOLDER = `Ramesh Kumar, 9876543210, 12 Cross Cut Rd Gandhipuram, 641012, Coimbatore, 120, 2 meal boxes
-Priya N, 9876500011, 45 RS Puram, 641002, Coimbatore, 95, documents`;
-
+// • unusable rows — bad phones, empty lines, missing fields; surfaced
+// up-front so the operator can fix the sheet instead of creating 9 good
+// orders and wondering where the 10th went
+// • columns ignored — confirms the resolver didn't mistake an unknown
+// column for a standard one
const AIRowsStep = ({ onSubmit, onCancel, isBusy }) => {
- const [file, setFile] = useState(null);
- const [fileParse, setFileParse] = useState(null);
- const [fileError, setFileError] = useState(null);
- const [raw, setRaw] = useState('');
+ const [text, setText] = useState('');
const [isReading, setIsReading] = useState(false);
+ const [parsed, setParsed] = useState(null);
- const pasted = raw.trim() ? parseBulkRows(raw) : null;
- // A loaded file wins; clearing it falls back to whatever is in the textarea.
- const parsed = fileParse || pasted;
-
- const handleFile = async (picked) => {
- const chosen = Array.isArray(picked) ? picked[0] : picked;
- setFile(chosen || null);
- setFileError(null);
- if (!chosen) {
- setFileParse(null);
+ const handlePasteChange = (val) => {
+ setText(val);
+ if (!val.trim()) {
+ setParsed(null);
return;
}
+ setParsed(parseBulkRows(val));
+ };
+
+ const handleFilePicked = async (files) => {
+ const file = Array.isArray(files) ? files[0] : files;
+ if (!file) return;
setIsReading(true);
try {
- const result = await parseBulkFile(chosen);
- setFileParse(result);
- setRaw('');
- } catch (err) {
- setFileParse(null);
- setFileError(err.message || 'Couldn’t read that file.');
+ const res = await parseBulkFile(file);
+ setParsed(res);
+ } catch {
+ setParsed({
+ rows: [],
+ errors: [{ row: 0, reason: 'Failed to read file' }],
+ ignoredColumns: []
+ });
} finally {
setIsReading(false);
}
@@ -66,51 +63,46 @@ const AIRowsStep = ({ onSubmit, onCancel, isBusy }) => {
return (
- {!fileParse && (
-
- )}
+
{isReading && Reading the file… }
- {fileError && {fileError} }
{parsed && (
- {fileParse ? `${fileParse.sheetName} — ` : ''}
- {parsed.rows.length} complete row{parsed.rows.length === 1 ? '' : 's'}
- {parsed.errors.length ? ` · ${parsed.errors.length} unusable` : ''}
+ ✓ Read {parsed.rows.length} usable row{parsed.rows.length === 1 ? '' : 's'}
-
{parsed.ignoredColumns?.length > 0 && (
Columns I didn’t recognise, so didn’t use: {parsed.ignoredColumns.join(', ')}
)}
-
- {parsed.duplicates?.length > 0 && (
-
- {parsed.duplicates.length} row{parsed.duplicates.length === 1 ? '' : 's'} repeat an earlier phone and address (
- {parsed.duplicates.map((d) => `line ${d.line}`).join(', ')}) — they’d become separate orders.
-
+ {parsed.errors?.length > 0 && (
+
+
+ ⚠️ {parsed.errors.length} row{parsed.errors.length === 1 ? '' : 's'} couldn’t be read:
+
+ {parsed.errors.slice(0, 5).map((e, idx) => (
+
+ Row {e.row}: {e.reason}
+
+ ))}
+ {parsed.errors.length > 5 && (
+ …and {parsed.errors.length - 5} more unusable rows.
+ )}
+
)}
-
- {/* Keyed by line AND index: one row can fail several validations, so
- `line` alone is not unique and React logged "two children with the
- same key" for every repeat — seen live as a 1/2/3 cycle. Same
- shape the list renderer in AIParts already uses. */}
- {parsed.errors.slice(0, 5).map((e, i) => (
-
- Line {e.line}: {e.reason}
-
- ))}
- {parsed.errors.length > 5 && …and {parsed.errors.length - 5} more unusable rows. }
-
{parsed.rows.length > BULK_MAX && (
Over {BULK_MAX} — I’ll send them in {Math.ceil(parsed.rows.length / BULK_MAX)} batches.
@@ -119,23 +111,35 @@ const AIRowsStep = ({ onSubmit, onCancel, isBusy }) => {
)}
-
+
onSubmit(parsed)}
- />
-
+ >
+ {parsed?.rows.length ? `Use these ${parsed.rows.length} rows` : 'Continue'}
+
+
+ Cancel
+
downloadCsv('doormile-bulk-orders-template.csv', templateCsv())}
- />
-
+ >
+ Template
+
+
Pasted rows: name, phone, address, pincode, city, price, description — comma, tab or pipe separated. Leave the price blank and I’ll
diff --git a/src/lib/ZoneContext.jsx b/src/lib/ZoneContext.jsx
index a808b1e..8f4722a 100644
--- a/src/lib/ZoneContext.jsx
+++ b/src/lib/ZoneContext.jsx
@@ -108,6 +108,29 @@ export function ZoneProvider({ children }) {
const currentHub = selectedZone;
const targetHubId = String(currentHub.hubid);
+ // Rider matching: riders belong to the tenant fleet and serve all tenant kitchen hubs
+ const isRiderItem =
+ item.milerprofileid != null ||
+ item.authname != null ||
+ (item.userid != null && item.availabilitystatus != null);
+
+ if (isRiderItem) {
+ if (isTenantUser) {
+ return true;
+ }
+ if (item.applocationid != null && (String(item.applocationid) === targetHubId || String(item.applocationid) === String(currentHub.applocationid))) {
+ return true;
+ }
+ if (item.hubid != null && String(item.hubid) === targetHubId) {
+ return true;
+ }
+ const hubCity = String(currentHub.city || '').trim().toLowerCase();
+ if (hubCity && String(item.city || '').trim().toLowerCase() === hubCity) {
+ return true;
+ }
+ return false;
+ }
+
// 1. Direct hub/location id match
const itemHubId =
item.hubid ??
diff --git a/src/lib/assistant/DoormileAI.css b/src/lib/assistant/DoormileAI.css
index 745eed3..36d46f3 100644
--- a/src/lib/assistant/DoormileAI.css
+++ b/src/lib/assistant/DoormileAI.css
@@ -953,40 +953,50 @@ body.dai-docked .astryx-layout-content {
}
}
/* --------------------------------------------------------------------------
- Write-action confirm card
+ Write-action confirm card & buttons
-------------------------------------------------------------------------- */
.dai-root .dai-action {
- padding: 10px 11px;
- border: 1px solid var(--dai-border-strong);
- border-radius: 5px;
- background: var(--dai-surface-alt);
+ padding: 12px 14px;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ background: #ffffff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
}
.dai-root .dai-action-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
- width: 26px;
- height: 26px;
- border-radius: 5px;
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
color: #4f46e5;
background: rgba(99, 102, 241, 0.1);
}
.dai-root .dai-action-summary {
font-size: 13px;
- line-height: 1.35;
- font-weight: 550;
+ line-height: 1.4;
+ font-weight: 600;
color: var(--dai-text);
}
.dai-root .dai-action-note {
font-size: 12px;
- color: var(--dai-text-secondary);
+ color: #64748b;
+ line-height: 1.4;
}
.dai-root .dai-ok {
color: #047857;
+ font-weight: 500;
}
.dai-root .dai-err {
color: #b91c1c;
+ font-weight: 500;
+}
+.dai-root .dai-action button {
+ cursor: pointer;
}
/* --------------------------------------------------------------------------
Overflow containment
diff --git a/src/pages/doormile/deliveries/Deliveries.jsx b/src/pages/doormile/deliveries/Deliveries.jsx
index 8804aed..918fc4a 100644
--- a/src/pages/doormile/deliveries/Deliveries.jsx
+++ b/src/pages/doormile/deliveries/Deliveries.jsx
@@ -279,10 +279,7 @@ export default function Deliveries() {
sortable: true,
cell: (row) => (
-
- {/* Where the value came from. Without this, a row reading "Active"
- while the booking still says Converted_To_Consignment looks
- exactly like a bug. */}
+
{row.statusfromconsignment ? (
from consignment
) : null}