feat: nový krok „Vyzvedávání" ve stepperu Bolt Food
CI / Generate TypeScript types (push) Successful in 14s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 35s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 14s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 35s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
Krok se aktivuje, když je kurýr u podniku a jídlo je zároveň označené jako hotové k vyzvednutí — samotný arrived_to_provider stupínek nezvedá, aby se neopakovala chyba Bolt UI, které „vyzvedávání" hlásí i během přípravy.
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||||
import './BoltOrderProgress.scss';
|
import './BoltOrderProgress.scss';
|
||||||
|
|
||||||
const STEPS = ['Přijato', 'Příprava', 'Na cestě', 'Doručeno'];
|
const STEPS = ['Přijato', 'Příprava', 'Vyzvedávání', 'Na cestě', 'Doručeno'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Známé stavy objednávky z Bolt API → index kroku ve stepperu.
|
* Známé stavy objednávky z Bolt API → index kroku ve stepperu.
|
||||||
* Pozor: waiting_delivery znamená "jídlo čeká v podniku na vyzvednutí",
|
* Pozor: waiting_delivery znamená "jídlo čeká v podniku na vyzvednutí",
|
||||||
* nikoli "na cestě" — tu signalizuje až stav kurýra (picked_up apod.).
|
* nikoli "na cestě" — tu signalizuje až stav kurýra (picked_up apod.).
|
||||||
|
* Krok „Vyzvedávání" je vyhrazen pro kombinaci „kurýr u podniku + jídlo hotové"
|
||||||
|
* (viz logika níže), samotný order_state ho neudělí — jinak bychom hlásili
|
||||||
|
* vyzvedávání i když se ještě peče (chování, na které si Bolt sám občas stěžuje).
|
||||||
*/
|
*/
|
||||||
const ORDER_STATE_TO_STEP: Record<string, number> = {
|
const ORDER_STATE_TO_STEP: Record<string, number> = {
|
||||||
created: 0,
|
created: 0,
|
||||||
@@ -18,33 +21,36 @@ const ORDER_STATE_TO_STEP: Record<string, number> = {
|
|||||||
ready_for_pickup: 1,
|
ready_for_pickup: 1,
|
||||||
waiting_courier: 1,
|
waiting_courier: 1,
|
||||||
waiting_pickup: 1,
|
waiting_pickup: 1,
|
||||||
picked_up: 2,
|
picked_up: 3,
|
||||||
in_delivery: 2,
|
in_delivery: 3,
|
||||||
delivering: 2,
|
delivering: 3,
|
||||||
heading_to_client: 2,
|
heading_to_client: 3,
|
||||||
delivered: 3,
|
delivered: 4,
|
||||||
finished: 3,
|
finished: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Stavy kurýra z Bolt API → index kroku. Kurýr u podniku ještě neznamená "na cestě". */
|
/** Stavy kurýra z Bolt API → index kroku. Kurýr u podniku ještě neznamená "na cestě". */
|
||||||
const COURIER_STATE_TO_STEP: Record<string, number> = {
|
const COURIER_STATE_TO_STEP: Record<string, number> = {
|
||||||
heading_to_provider: 1,
|
heading_to_provider: 1,
|
||||||
arrived_to_provider: 1,
|
arrived_to_provider: 1,
|
||||||
picked_up: 2,
|
picked_up: 3,
|
||||||
heading_to_client: 2,
|
heading_to_client: 3,
|
||||||
delivering: 2,
|
delivering: 3,
|
||||||
arrived_to_client: 2,
|
arrived_to_client: 3,
|
||||||
delivered: 3,
|
delivered: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Order states ve kterých už jídlo čeká připravené na kurýra. */
|
||||||
|
const PICKUP_READY_ORDER_STATES = new Set(['waiting_delivery', 'ready_for_pickup', 'waiting_courier', 'waiting_pickup']);
|
||||||
|
|
||||||
/** Neznámé stavy se mapují heuristicky podle klíčových slov. */
|
/** Neznámé stavy se mapují heuristicky podle klíčových slov. */
|
||||||
function stepForOrderState(state: string): number | 'cancelled' {
|
function stepForOrderState(state: string): number | 'cancelled' {
|
||||||
const s = state.toLowerCase();
|
const s = state.toLowerCase();
|
||||||
if (s in ORDER_STATE_TO_STEP) return ORDER_STATE_TO_STEP[s];
|
if (s in ORDER_STATE_TO_STEP) return ORDER_STATE_TO_STEP[s];
|
||||||
if (/cancel|reject|fail/.test(s)) return 'cancelled';
|
if (/cancel|reject|fail/.test(s)) return 'cancelled';
|
||||||
if (/delivered|finished/.test(s)) return 3;
|
if (/delivered|finished/.test(s)) return 4;
|
||||||
if (/^waiting|prepar|ready|cook/.test(s)) return 1;
|
if (/^waiting|prepar|ready|cook/.test(s)) return 1;
|
||||||
if (/picked|delivering|heading_to_client|transport/.test(s)) return 2;
|
if (/picked|delivering|heading_to_client|transport/.test(s)) return 3;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +58,7 @@ function stepForCourierState(state?: string): number {
|
|||||||
if (!state) return 0;
|
if (!state) return 0;
|
||||||
const s = state.toLowerCase();
|
const s = state.toLowerCase();
|
||||||
if (s in COURIER_STATE_TO_STEP) return COURIER_STATE_TO_STEP[s];
|
if (s in COURIER_STATE_TO_STEP) return COURIER_STATE_TO_STEP[s];
|
||||||
if (/picked|client|delivering|transport/.test(s)) return 2;
|
if (/picked|client|delivering|transport/.test(s)) return 3;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,11 +78,16 @@ export default function BoltOrderProgress({ state, courierState, tracking }: Pro
|
|||||||
return <small className="text-danger">Objednávka Bolt byla zrušena</small>;
|
return <small className="text-danger">Objednávka Bolt byla zrušena</small>;
|
||||||
}
|
}
|
||||||
// Stav kurýra může krok jen zpřesnit dopředu (např. waiting_delivery + picked_up → Na cestě)
|
// Stav kurýra může krok jen zpřesnit dopředu (např. waiting_delivery + picked_up → Na cestě)
|
||||||
const step = Math.max(orderStep, stepForCourierState(courierState));
|
let step = Math.max(orderStep, stepForCourierState(courierState));
|
||||||
|
// Vyzvedávání = kurýr u podniku a jídlo skutečně hotové. Sám arrived_to_provider
|
||||||
|
// (bez toho, že by jídlo bylo hotové) nestačí — viz komentář u ORDER_STATE_TO_STEP.
|
||||||
|
const courierAtProvider = courierState?.toLowerCase() === 'arrived_to_provider';
|
||||||
|
const foodReadyForPickup = PICKUP_READY_ORDER_STATES.has(state.toLowerCase());
|
||||||
|
if (step < 3 && courierAtProvider && foodReadyForPickup) step = 2;
|
||||||
const rawInfo = courierState ? `${state} / kurýr: ${courierState}` : state;
|
const rawInfo = courierState ? `${state} / kurýr: ${courierState}` : state;
|
||||||
return (
|
return (
|
||||||
<OverlayTrigger overlay={<Tooltip>Stav z Bolt Food: {rawInfo}</Tooltip>}>
|
<OverlayTrigger overlay={<Tooltip>Stav z Bolt Food: {rawInfo}</Tooltip>}>
|
||||||
<div className={`bolt-progress${tracking && step < 3 ? ' live' : ''}`}>
|
<div className={`bolt-progress${tracking && step < 4 ? ' live' : ''}`}>
|
||||||
{STEPS.map((label, i) => (
|
{STEPS.map((label, i) => (
|
||||||
<div key={label} className={`bolt-step${i <= step ? ' done' : ''}${i === step ? ' active' : ''}`}>
|
<div key={label} className={`bolt-step${i <= step ? ' done' : ''}${i === step ? ' active' : ''}`}>
|
||||||
<div className="bolt-dot" />
|
<div className="bolt-dot" />
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Nabídka stavů pro ruční nastavení (odpovídá mapování v BoltOrderProgress). */
|
/** Nabídka stavů pro ruční nastavení (odpovídá mapování v BoltOrderProgress). */
|
||||||
const STATE_OPTIONS: { value: string; label: string }[] = [
|
const STATE_OPTIONS: { key: string; label: string; order_state: string; courier_state?: string }[] = [
|
||||||
{ value: 'accepted', label: 'Přijato' },
|
{ key: 'accepted', label: 'Přijato', order_state: 'accepted' },
|
||||||
{ value: 'preparing', label: 'Příprava' },
|
{ key: 'preparing', label: 'Příprava', order_state: 'preparing' },
|
||||||
{ value: 'waiting_delivery', label: 'Příprava (čeká na vyzvednutí)' },
|
{ key: 'waiting_delivery', label: 'Příprava (čeká na vyzvednutí)', order_state: 'waiting_delivery' },
|
||||||
{ value: 'in_delivery', label: 'Na cestě' },
|
{ key: 'pickup', label: 'Vyzvedávání', order_state: 'waiting_delivery', courier_state: 'arrived_to_provider' },
|
||||||
{ value: 'delivered', label: 'Doručeno' },
|
{ key: 'in_delivery', label: 'Na cestě', order_state: 'in_delivery', courier_state: 'heading_to_client' },
|
||||||
{ value: 'cancelled', label: 'Zrušeno' },
|
{ key: 'delivered', label: 'Doručeno', order_state: 'delivered' },
|
||||||
|
{ key: 'cancelled', label: 'Zrušeno', order_state: 'cancelled' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Modální dialog pro simulaci sledování Bolt objednávky (pouze DEV). */
|
/** Modální dialog pro simulaci sledování Bolt objednávky (pouze DEV). */
|
||||||
@@ -27,7 +28,7 @@ export default function BoltSimulationModal({ isOpen, onClose, group }: Readonly
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useState<string | null>(null);
|
||||||
const [manualState, setManualState] = useState<string>('preparing');
|
const [manualKey, setManualKey] = useState<string>('preparing');
|
||||||
|
|
||||||
const running = !!group.boltTrackingToken;
|
const running = !!group.boltTrackingToken;
|
||||||
|
|
||||||
@@ -58,17 +59,20 @@ export default function BoltSimulationModal({ isOpen, onClose, group }: Readonly
|
|||||||
() => advanceBoltTracking({ body: { groupId: group.id } }),
|
() => advanceBoltTracking({ body: { groupId: group.id } }),
|
||||||
'Posunuto na další krok.',
|
'Posunuto na další krok.',
|
||||||
);
|
);
|
||||||
const handleSetState = () => run(
|
const handleSetState = () => {
|
||||||
|
const opt = STATE_OPTIONS.find(o => o.key === manualKey);
|
||||||
|
if (!opt) return;
|
||||||
|
return run(
|
||||||
() => setBoltTrackingState({
|
() => setBoltTrackingState({
|
||||||
body: {
|
body: {
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
order_state: manualState,
|
order_state: opt.order_state,
|
||||||
// "Na cestě" zpřesníme i stavem kurýra
|
courier_state: opt.courier_state,
|
||||||
courier_state: manualState === 'in_delivery' ? 'heading_to_client' : undefined,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
`Stav nastaven na „${STATE_OPTIONS.find(o => o.value === manualState)?.label}".`,
|
`Stav nastaven na „${opt.label}".`,
|
||||||
);
|
);
|
||||||
|
};
|
||||||
const handlePoll = () => run(
|
const handlePoll = () => run(
|
||||||
() => pollBoltTracking(),
|
() => pollBoltTracking(),
|
||||||
'Scheduler spuštěn (poll proběhl).',
|
'Scheduler spuštěn (poll proběhl).',
|
||||||
@@ -122,17 +126,17 @@ export default function BoltSimulationModal({ isOpen, onClose, group }: Readonly
|
|||||||
<>
|
<>
|
||||||
<p className="text-muted">
|
<p className="text-muted">
|
||||||
Krokuj objednávku tlačítkem <strong>Další krok</strong> (Přijato → Příprava →
|
Krokuj objednávku tlačítkem <strong>Další krok</strong> (Přijato → Příprava →
|
||||||
Na cestě → Doručeno) nebo nastav konkrétní stav ručně:
|
Vyzvedávání → Na cestě → Doručeno) nebo nastav konkrétní stav ručně:
|
||||||
</p>
|
</p>
|
||||||
<Form.Group className="mb-3 d-flex gap-2 align-items-end">
|
<Form.Group className="mb-3 d-flex gap-2 align-items-end">
|
||||||
<div className="flex-grow-1">
|
<div className="flex-grow-1">
|
||||||
<Form.Label>Konkrétní stav</Form.Label>
|
<Form.Label>Konkrétní stav</Form.Label>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
value={manualState}
|
value={manualKey}
|
||||||
onChange={e => setManualState(e.target.value)}
|
onChange={e => setManualKey(e.target.value)}
|
||||||
>
|
>
|
||||||
{STATE_OPTIONS.map(o => (
|
{STATE_OPTIONS.map(o => (
|
||||||
<option key={o.value} value={o.value}>{o.label}</option>
|
<option key={o.key} value={o.key}>{o.label}</option>
|
||||||
))}
|
))}
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ interface Simulation {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Výchozí scénář „happy path". Stavy a stavy kurýra odpovídají mapování ve
|
* Výchozí scénář „happy path". Stavy a stavy kurýra odpovídají mapování ve
|
||||||
* client/src/components/BoltOrderProgress.tsx (Přijato → Příprava → Na cestě → Doručeno).
|
* client/src/components/BoltOrderProgress.tsx
|
||||||
|
* (Přijato → Příprava → Vyzvedávání → Na cestě → Doručeno).
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_SCENARIO: SimStep[] = [
|
export const DEFAULT_SCENARIO: SimStep[] = [
|
||||||
{ order_state: 'accepted', etaSeconds: 1800 },
|
{ order_state: 'accepted', etaSeconds: 1800 },
|
||||||
|
|||||||
Reference in New Issue
Block a user