updates on the ai bot and the queries and the delivereis page

This commit is contained in:
2026-08-27 13:11:05 +05:30
parent c6607ae75d
commit e34e41ca96
8 changed files with 217 additions and 144 deletions

View File

@@ -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
};

View File

@@ -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

View File

@@ -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.'}
</Text>
<Button label="Cancel" variant="secondary" size="sm" onClick={onCancel} />
<div className="pt-1">
<Button
variant="outline"
size="sm"
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
onClick={onCancel}
>
Cancel
</Button>
</div>
</VStack>
);
}
@@ -90,19 +99,26 @@ const AIFlowStep = ({ step, onChoose, onCancel, draft, isBusy }) => {
hasSearch={options.length > 8}
isDisabled={isBusy}
/>
<HStack gap={1.5} padding={0}>
<div className="flex items-center gap-2 pt-1">
<Button
label="Continue"
variant="primary"
variant="default"
size="sm"
isDisabled={!value || isBusy}
// The whole option is passed back, not just its value — steps that
// pick a record (a location, a customer) need the record itself to
// fill several fields at once.
className="bg-slate-900 text-white hover:bg-black font-semibold text-xs px-3.5 h-8 rounded-lg shadow-xs cursor-pointer disabled:opacity-50"
disabled={!value || isBusy}
onClick={() => onChoose(value, chosen)}
/>
<Button label="Cancel" variant="secondary" size="sm" isDisabled={isBusy} onClick={onCancel} />
</HStack>
>
Continue
</Button>
<Button
variant="outline"
size="sm"
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
disabled={isBusy}
onClick={onCancel}
>
Cancel
</Button>
</div>
</VStack>
);
};

View File

@@ -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) && (
<VStack className="dai-action" gap={1.5} padding={0}>
{message.form.status === 'open' ? (
<HStack gap={1.5} padding={0}>
<div className="flex flex-wrap items-center gap-2 pt-1">
<Button
label={
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'
}
variant="primary"
variant="default"
size="sm"
className="bg-slate-900 text-white hover:bg-black font-semibold text-xs px-3.5 h-8 rounded-lg shadow-xs cursor-pointer"
onClick={() => onSubmitForm(message, message.form.payload || message.form.draft)}
/>
<Button label="Cancel" variant="secondary" size="sm" onClick={() => 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'}
</Button>
<Button
variant="outline"
size="sm"
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
onClick={() => onCancelAction(message)}
>
Cancel
</Button>
{/* A second, wider commit — used when rows were excluded by
default but including them is a legitimate choice. */}
{message.form.altDraft && (
<Button
label={message.form.altLabel || 'Include everything'}
variant="ghost"
size="sm"
className="text-slate-600 hover:bg-slate-100 font-medium text-xs px-3 h-8 rounded-lg cursor-pointer"
onClick={() => onSubmitForm(message, message.form.altDraft)}
/>
>
{message.form.altLabel || 'Include everything'}
</Button>
)}
</HStack>
</div>
) : (
<Text className={`dai-action-note${message.form.status === 'done' ? ' dai-ok' : ''}`}>
{message.form.status === 'done'

View File

@@ -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 (
<VStack className="dai-action" gap={1.5} padding={0}>
<FileInput
label="Order sheet"
isLabelHidden
accept=".csv,.xls,.xlsx"
value={file}
onChange={handleFile}
label="Upload spreadsheet (CSV, Excel)"
accept=".csv,.xlsx,.xls,.tsv"
onChange={handleFilePicked}
isDisabled={isBusy || isReading}
/>
{!fileParse && (
<TextArea label="Or paste rows" rows={4} value={raw} onChange={setRaw} placeholder={PLACEHOLDER} isDisabled={isBusy} />
)}
<TextArea
label="Or paste rows"
placeholder="Paste CSV / spreadsheet rows here…"
value={text}
rows={4}
onChange={handlePasteChange}
isDisabled={isBusy || isReading}
/>
{isReading && <Text className="dai-action-note">Reading the file…</Text>}
{fileError && <Text className="dai-field-err">{fileError}</Text>}
{parsed && (
<VStack gap={0.5} padding={0}>
<Text className="dai-action-note">
{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'}
</Text>
{parsed.ignoredColumns?.length > 0 && (
<Text className="dai-action-note">Columns I didn’t recognise, so didn’t use: {parsed.ignoredColumns.join(', ')}</Text>
)}
{parsed.duplicates?.length > 0 && (
<Text className="dai-field-err">
{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.
</Text>
{parsed.errors?.length > 0 && (
<VStack gap={0.25} padding={0}>
<Text className="dai-field-err">
⚠️ {parsed.errors.length} row{parsed.errors.length === 1 ? '' : 's'} couldn’t be read:
</Text>
{parsed.errors.slice(0, 5).map((e, idx) => (
<Text key={idx} className="dai-field-err">
Row {e.row}: {e.reason}
</Text>
))}
{parsed.errors.length > 5 && (
<Text className="dai-action-note">…and {parsed.errors.length - 5} more unusable rows.</Text>
)}
</VStack>
)}
{/* 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) => (
<Text key={`${e.line}-${i}`} className="dai-field-err">
Line {e.line}: {e.reason}
</Text>
))}
{parsed.errors.length > 5 && <Text className="dai-action-note">…and {parsed.errors.length - 5} more unusable rows.</Text>}
{parsed.rows.length > BULK_MAX && (
<Text className="dai-action-note">
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 }) => {
</VStack>
)}
<HStack gap={1.5} padding={0}>
<div className="flex flex-wrap items-center gap-2 pt-1">
<Button
label={parsed?.rows.length ? `Use these ${parsed.rows.length} rows` : 'Continue'}
variant="primary"
variant="default"
size="sm"
isDisabled={!parsed?.rows.length || isBusy || isReading}
className="bg-slate-900 text-white hover:bg-black font-semibold text-xs px-3.5 h-8 rounded-lg shadow-xs cursor-pointer disabled:opacity-50"
disabled={!parsed?.rows.length || isBusy || isReading}
onClick={() => onSubmit(parsed)}
/>
<Button label="Cancel" variant="secondary" size="sm" isDisabled={isBusy} onClick={onCancel} />
>
{parsed?.rows.length ? `Use these ${parsed.rows.length} rows` : 'Continue'}
</Button>
<Button
variant="outline"
size="sm"
className="border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-medium text-xs px-3 h-8 rounded-lg shadow-2xs cursor-pointer"
disabled={isBusy}
onClick={onCancel}
>
Cancel
</Button>
<Button
label="Template"
variant="ghost"
size="sm"
tooltip="Download a blank sheet with the exact column titles I read"
className="text-slate-600 hover:bg-slate-100 font-medium text-xs px-3 h-8 rounded-lg cursor-pointer"
title="Download a blank sheet with the exact column titles I read"
onClick={() => downloadCsv('doormile-bulk-orders-template.csv', templateCsv())}
/>
</HStack>
>
Template
</Button>
</div>
<Text className="dai-action-note">
Pasted rows: name, phone, address, pincode, city, price, description — comma, tab or pipe separated. Leave the price blank and I’ll

View File

@@ -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 ??

View File

@@ -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

View File

@@ -279,10 +279,7 @@ export default function Deliveries() {
sortable: true,
cell: (row) => (
<div className="flex flex-col items-start gap-0.5">
<StatusBadge status={row.consignmentstatus || row.orderstatus} dot size="sm" />
{/* Where the value came from. Without this, a row reading "Active"
while the booking still says Converted_To_Consignment looks
exactly like a bug. */}
<StatusBadge status={row.orderstatus} dot size="sm" />
{row.statusfromconsignment ? (
<span className="text-[10px] text-ink-4">from consignment</span>
) : null}