updates on the ai bot and the queries and the delivereis page
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user