CI / Generate TypeScript types (push) Successful in 13s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m47s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s
Vedle Bolt Food jde nově sledovat i objednávka z Woltu — stačí u objednané skupiny vložit odkaz z track.wolt.com. Logika sledování je zobecněná do registru rozvozových služeb (trackingProviders.ts): každá služba umí vytáhnout kód ze sdílecího odkazu a dotázat se svého API, scheduler i stepper jsou společné. - pole skupiny bolt* přejmenována na tracking* + nové trackingProvider - endpoint /groups/setBoltTracking → /groups/setTracking - Wolt: čas doručení z delivery_eta v časové zóně objednávky, 404 ukončí sledování, stav kurýra odvozen z is_delivering/is_delivering_other_order - DEV simulace zůstává jen pro Bolt Food
175 lines
7.2 KiB
TypeScript
175 lines
7.2 KiB
TypeScript
import { useState } from "react";
|
||
import { Modal, Button, Form, Alert, Badge } from "react-bootstrap";
|
||
import {
|
||
OrderGroup,
|
||
simulateBoltTracking, advanceBoltTracking, setBoltTrackingState,
|
||
pollBoltTracking, stopBoltTrackingSimulation,
|
||
} from "../../../../types";
|
||
|
||
type Props = {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
group: OrderGroup;
|
||
};
|
||
|
||
/** Nabídka stavů pro ruční nastavení (odpovídá mapování v OrderProgress). */
|
||
const STATE_OPTIONS: { key: string; label: string; order_state: string; courier_state?: string }[] = [
|
||
{ key: 'accepted', label: 'Přijato', order_state: 'accepted' },
|
||
{ key: 'preparing', label: 'Příprava', order_state: 'preparing' },
|
||
{ key: 'waiting_delivery', label: 'Příprava (čeká na vyzvednutí)', order_state: 'waiting_delivery' },
|
||
{ key: 'pickup', label: 'Vyzvedávání', order_state: 'waiting_delivery', courier_state: 'arrived_to_provider' },
|
||
{ key: 'in_delivery', label: 'Na cestě', order_state: 'in_delivery', courier_state: 'heading_to_client' },
|
||
{ 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). */
|
||
export default function BoltSimulationModal({ isOpen, onClose, group }: Readonly<Props>) {
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [info, setInfo] = useState<string | null>(null);
|
||
const [manualKey, setManualKey] = useState<string>('preparing');
|
||
|
||
const running = !!group.trackingCode;
|
||
|
||
/** Obecný runner — spustí akci, ošetří chybu a krátce zobrazí výsledek. */
|
||
const run = async (action: () => Promise<{ error?: unknown }>, okMsg: string) => {
|
||
setError(null);
|
||
setInfo(null);
|
||
setLoading(true);
|
||
try {
|
||
const response = await action();
|
||
if (response.error) {
|
||
setError((response.error as any)?.error || 'Akce simulace selhala');
|
||
} else {
|
||
setInfo(okMsg);
|
||
}
|
||
} catch (e: any) {
|
||
setError(e?.message || 'Akce simulace selhala');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleStart = () => run(
|
||
() => simulateBoltTracking({ body: { groupId: group.id } }),
|
||
'Simulace spuštěna — objednávka přijata.',
|
||
);
|
||
const handleAdvance = () => run(
|
||
() => advanceBoltTracking({ body: { groupId: group.id } }),
|
||
'Posunuto na další krok.',
|
||
);
|
||
const handleSetState = () => {
|
||
const opt = STATE_OPTIONS.find(o => o.key === manualKey);
|
||
if (!opt) return;
|
||
return run(
|
||
() => setBoltTrackingState({
|
||
body: {
|
||
groupId: group.id,
|
||
order_state: opt.order_state,
|
||
courier_state: opt.courier_state,
|
||
},
|
||
}),
|
||
`Stav nastaven na „${opt.label}".`,
|
||
);
|
||
};
|
||
const handlePoll = () => run(
|
||
() => pollBoltTracking(),
|
||
'Scheduler spuštěn (poll proběhl).',
|
||
);
|
||
const handleStop = () => run(
|
||
() => stopBoltTrackingSimulation({ body: { groupId: group.id } }),
|
||
'Simulace ukončena.',
|
||
);
|
||
|
||
const handleClose = () => {
|
||
setError(null);
|
||
setInfo(null);
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<Modal show={isOpen} onHide={handleClose}>
|
||
<Modal.Header closeButton>
|
||
<Modal.Title><h2>Simulace sledování Bolt</h2></Modal.Title>
|
||
</Modal.Header>
|
||
<Modal.Body>
|
||
<Alert variant="warning">
|
||
<strong>DEV režim</strong> – simuluje sledování objednávky Bolt pro skupinu{' '}
|
||
<strong>{group.name}</strong> bez reálné objednávky.
|
||
</Alert>
|
||
|
||
{error && (
|
||
<Alert variant="danger" onClose={() => setError(null)} dismissible>
|
||
{error}
|
||
</Alert>
|
||
)}
|
||
{info && (
|
||
<Alert variant="success" onClose={() => setInfo(null)} dismissible>
|
||
{info}
|
||
</Alert>
|
||
)}
|
||
|
||
<p className="mb-2">
|
||
Stav simulace:{' '}
|
||
{running
|
||
? <Badge bg="success">běží{group.trackingOrderState ? ` — ${group.trackingOrderState}` : ''}</Badge>
|
||
: <Badge bg="secondary">neběží</Badge>}
|
||
</p>
|
||
|
||
{!running ? (
|
||
<p className="text-muted">
|
||
Spuštěním se skupina přepne do stavu „Objednáno", přiřadí se simulovaný
|
||
sledovací token a provede se první poll (stav „Přijato").
|
||
</p>
|
||
) : (
|
||
<>
|
||
<p className="text-muted">
|
||
Krokuj objednávku tlačítkem <strong>Další krok</strong> (Přijato → Příprava →
|
||
Vyzvedávání → Na cestě → Doručeno) nebo nastav konkrétní stav ručně:
|
||
</p>
|
||
<Form.Group className="mb-3 d-flex gap-2 align-items-end">
|
||
<div className="flex-grow-1">
|
||
<Form.Label>Konkrétní stav</Form.Label>
|
||
<Form.Select
|
||
value={manualKey}
|
||
onChange={e => setManualKey(e.target.value)}
|
||
>
|
||
{STATE_OPTIONS.map(o => (
|
||
<option key={o.key} value={o.key}>{o.label}</option>
|
||
))}
|
||
</Form.Select>
|
||
</div>
|
||
<Button variant="outline-primary" onClick={handleSetState} disabled={loading}>
|
||
Nastavit
|
||
</Button>
|
||
</Form.Group>
|
||
</>
|
||
)}
|
||
</Modal.Body>
|
||
<Modal.Footer className="d-flex flex-wrap gap-2">
|
||
{!running ? (
|
||
<Button variant="primary" onClick={handleStart} disabled={loading}>
|
||
{loading ? 'Spouštím…' : 'Spustit simulaci'}
|
||
</Button>
|
||
) : (
|
||
<>
|
||
<Button variant="success" onClick={handleAdvance} disabled={loading}>
|
||
Další krok
|
||
</Button>
|
||
<Button variant="outline-secondary" onClick={handlePoll} disabled={loading}>
|
||
Aktualizovat teď
|
||
</Button>
|
||
<Button variant="outline-danger" onClick={handleStop} disabled={loading}>
|
||
Ukončit simulaci
|
||
</Button>
|
||
</>
|
||
)}
|
||
<Button variant="secondary" onClick={handleClose} disabled={loading}>
|
||
Zavřít
|
||
</Button>
|
||
</Modal.Footer>
|
||
</Modal>
|
||
);
|
||
}
|