feat: sledování objednávek přes Wolt
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
This commit is contained in:
2026-08-24 11:27:56 +02:00
parent eeb1630391
commit 5c048fe1f1
25 changed files with 1326 additions and 869 deletions
-104
View File
@@ -1,104 +0,0 @@
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import './BoltOrderProgress.scss';
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.
* Pozor: waiting_delivery znamená "jídlo čeká v podniku na vyzvednutí",
* 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> = {
created: 0,
pending: 0,
waiting_acceptance: 0,
accepted: 0,
waiting_preparation: 0,
preparing: 1,
waiting_delivery: 1,
ready_for_pickup: 1,
waiting_courier: 1,
waiting_pickup: 1,
picked_up: 3,
in_delivery: 3,
delivering: 3,
heading_to_client: 3,
delivered: 4,
finished: 4,
};
/** 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> = {
matched: 0,
accepted: 0,
heading_to_provider: 1,
arrived_to_provider: 1,
picked_up: 3,
heading_to_client: 3,
delivering: 3,
arrived_to_client: 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. */
function stepForOrderState(state: string): number | 'cancelled' {
const s = state.toLowerCase();
if (s in ORDER_STATE_TO_STEP) return ORDER_STATE_TO_STEP[s];
if (/cancel|reject|fail/.test(s)) return 'cancelled';
if (/delivered|finished/.test(s)) return 4;
if (/accept|pending|created/.test(s)) return 0;
if (/^waiting|prepar|ready|cook/.test(s)) return 1;
if (/picked|delivering|heading_to_client|transport/.test(s)) return 3;
return 0;
}
function stepForCourierState(state?: string): number {
if (!state) return 0;
const s = state.toLowerCase();
if (s in COURIER_STATE_TO_STEP) return COURIER_STATE_TO_STEP[s];
if (/picked|client|delivering|transport/.test(s)) return 3;
return 0;
}
interface Props {
/** Raw order_state z Bolt API (např. waiting_preparation) */
state: string;
/** Raw courier.state z Bolt API (např. arrived_to_provider) */
courierState?: string;
/** Zda sledování stále běží (skupina má boltTrackingToken) */
tracking: boolean;
}
/** Mini progress stepper se stavem objednávky Bolt Food. */
export default function BoltOrderProgress({ state, courierState, tracking }: Props) {
const orderStep = stepForOrderState(state);
if (orderStep === 'cancelled') {
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ě)
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;
return (
<OverlayTrigger overlay={<Tooltip>Stav z Bolt Food: {rawInfo}</Tooltip>}>
<div className={`bolt-progress${tracking && step < 4 ? ' live' : ''}`}>
{STEPS.map((label, i) => (
<div key={label} className={`bolt-step${i <= step ? ' done' : ''}${i === step ? ' active' : ''}`}>
<div className="bolt-dot" />
<div className="bolt-label">{label}</div>
</div>
))}
</div>
</OverlayTrigger>
);
}
@@ -1,9 +1,9 @@
.bolt-progress {
.tracking-progress {
display: flex;
align-items: flex-start;
max-width: 400px;
.bolt-step {
.tracking-step {
position: relative;
display: flex;
flex-direction: column;
@@ -11,7 +11,7 @@
flex: 1;
min-width: 64px;
.bolt-dot {
.tracking-dot {
position: relative;
z-index: 1;
width: 12px;
@@ -20,7 +20,7 @@
background: var(--luncher-border, #ced4da);
}
.bolt-label {
.tracking-label {
margin-top: 2px;
font-size: 0.7em;
color: var(--luncher-text-muted, #6c757d);
@@ -40,7 +40,7 @@
}
&.done {
.bolt-dot {
.tracking-dot {
background: var(--bs-success, #198754);
}
@@ -49,19 +49,19 @@
}
}
&.active .bolt-label {
&.active .tracking-label {
font-weight: 600;
color: inherit;
}
}
// Pulzování aktivního kroku, dokud sledování běží
&.live .bolt-step.active .bolt-dot {
animation: bolt-pulse 2s ease-in-out infinite;
&.live .tracking-step.active .tracking-dot {
animation: tracking-pulse 2s ease-in-out infinite;
}
}
@keyframes bolt-pulse {
@keyframes tracking-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(25, 135, 84, 0.5);
}
+145
View File
@@ -0,0 +1,145 @@
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { TrackingProvider } from '../../../types';
import './OrderProgress.scss';
const STEPS = ['Přijato', 'Příprava', 'Vyzvedávání', 'Na cestě', 'Doručeno'];
/** Lidský název rozvozové služby do tooltipu. */
export const PROVIDER_LABEL: Record<TrackingProvider, string> = {
bolt: 'Bolt Food',
wolt: 'Wolt',
};
/**
* Známé stavy objednávky z Bolt API → index kroku ve stepperu.
* Pozor: waiting_delivery znamená "jídlo čeká v podniku na vyzvednutí",
* 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 BOLT_STATE_TO_STEP: Record<string, number> = {
created: 0,
pending: 0,
waiting_acceptance: 0,
accepted: 0,
waiting_preparation: 0,
preparing: 1,
waiting_delivery: 1,
ready_for_pickup: 1,
waiting_courier: 1,
waiting_pickup: 1,
picked_up: 3,
in_delivery: 3,
delivering: 3,
heading_to_client: 3,
delivered: 4,
finished: 4,
};
/** Stavy kurýra z Bolt API → index kroku. Kurýr u podniku ještě neznamená "na cestě". */
const BOLT_COURIER_STATE_TO_STEP: Record<string, number> = {
matched: 0,
accepted: 0,
heading_to_provider: 1,
arrived_to_provider: 1,
picked_up: 3,
heading_to_client: 3,
delivering: 3,
arrived_to_client: 3,
delivered: 4,
};
/**
* Známé stavy objednávky z Wolt tracking API → index kroku ve stepperu.
* Stav "ready" znamená hotové jídlo čekající na kurýra, proto zůstává v „Přípravě"
* a na „Vyzvedávání" se posune, až když je kurýr přiřazený. Že objednávku kurýr
* skutečně veze, se pozná až ze stavu kurýra 'delivering' (viz trackingProviders.ts).
*/
const WOLT_STATE_TO_STEP: Record<string, number> = {
received: 0,
acknowledged: 0,
confirmed: 0,
waiting: 0,
production: 1,
preparing: 1,
ready: 1,
picked_up: 3,
transport: 3,
delivering: 3,
delivered: 4,
};
/** Stavy, ve kterých už jídlo čeká připravené na kurýra. */
const PICKUP_READY_STATES: Record<TrackingProvider, Set<string>> = {
bolt: new Set(['waiting_delivery', 'ready_for_pickup', 'waiting_courier', 'waiting_pickup']),
wolt: new Set(['ready']),
};
/** Neznámé stavy se mapují heuristicky podle klíčových slov. */
function stepForOrderState(provider: TrackingProvider, state: string): number | 'cancelled' {
const s = state.toLowerCase();
const known = provider === 'wolt' ? WOLT_STATE_TO_STEP : BOLT_STATE_TO_STEP;
if (s in known) return known[s];
if (/cancel|reject|fail/.test(s)) return 'cancelled';
if (/delivered|finished/.test(s)) return 4;
if (/accept|pending|created|received/.test(s)) return 0;
if (/^waiting|prepar|ready|cook|production/.test(s)) return 1;
if (/picked|delivering|heading_to_client|transport/.test(s)) return 3;
return 0;
}
function stepForCourierState(provider: TrackingProvider, state?: string): number {
if (!state) return 0;
const s = state.toLowerCase();
// Wolt má jen odvozené stavy: 'assigned' o kroku nic neříká, 'delivering' = veze naši objednávku
if (provider === 'wolt') return s === 'delivering' ? 3 : 0;
if (s in BOLT_COURIER_STATE_TO_STEP) return BOLT_COURIER_STATE_TO_STEP[s];
if (/picked|client|delivering|transport/.test(s)) return 3;
return 0;
}
interface Props {
/** Rozvozová služba, ze které stav pochází */
provider: TrackingProvider;
/** Raw stav objednávky (Bolt order_state, Wolt status) */
state: string;
/** Raw stav kurýra (Bolt courier.state, Wolt 'assigned') */
courierState?: string;
/** Zda sledování stále běží (skupina má trackingCode) */
tracking: boolean;
}
/** Mini progress stepper se stavem objednávky u rozvozové služby. */
export default function OrderProgress({ provider, state, courierState, tracking }: Props) {
const label = PROVIDER_LABEL[provider] ?? provider;
const orderStep = stepForOrderState(provider, state);
if (orderStep === 'cancelled') {
return <small className="text-danger">Objednávka {label} byla zrušena</small>;
}
// Stav kurýra může krok jen zpřesnit dopředu (např. waiting_delivery + picked_up → Na cestě)
let step = Math.max(orderStep, stepForCourierState(provider, 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 BOLT_STATE_TO_STEP.
const courierAtProvider = provider === 'wolt'
? !!courierState
: courierState?.toLowerCase() === 'arrived_to_provider';
const foodReadyForPickup = PICKUP_READY_STATES[provider]?.has(state.toLowerCase());
if (step < 3 && courierAtProvider && foodReadyForPickup) step = 2;
const courierInfo = provider === 'wolt'
? (courierState === 'delivering' ? 'kurýr veze objednávku' : 'kurýr přiřazen')
: `kurýr: ${courierState}`;
const rawInfo = courierState ? `${state} / ${courierInfo}` : state;
return (
<OverlayTrigger overlay={<Tooltip>Stav z {label}: {rawInfo}</Tooltip>}>
<div className={`tracking-progress${tracking && step < 4 ? ' live' : ''}`}>
{STEPS.map((stepLabel, i) => (
<div key={stepLabel} className={`tracking-step${i <= step ? ' done' : ''}${i === step ? ' active' : ''}`}>
<div className="tracking-dot" />
<div className="tracking-label">{stepLabel}</div>
</div>
))}
</div>
</OverlayTrigger>
);
}
@@ -12,7 +12,7 @@ type Props = {
group: OrderGroup;
};
/** 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 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' },
@@ -30,7 +30,7 @@ export default function BoltSimulationModal({ isOpen, onClose, group }: Readonly
const [info, setInfo] = useState<string | null>(null);
const [manualKey, setManualKey] = useState<string>('preparing');
const running = !!group.boltTrackingToken;
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) => {
@@ -113,7 +113,7 @@ export default function BoltSimulationModal({ isOpen, onClose, group }: Readonly
<p className="mb-2">
Stav simulace:{' '}
{running
? <Badge bg="success">běží{group.boltOrderState ? `${group.boltOrderState}` : ''}</Badge>
? <Badge bg="success">běží{group.trackingOrderState ? `${group.trackingOrderState}` : ''}</Badge>
: <Badge bg="secondary">neběží</Badge>}
</p>
@@ -141,13 +141,13 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
id="boltDeliveredCheckbox"
ref={boltDeliveredRef}
type="checkbox"
label="Upozornění na doručení objednávky (Bolt Food)"
label="Upozornění na doručení objednávky (Bolt Food / Wolt)"
defaultChecked={notifSettings.boltDeliveredPush ?? false}
key={`bolt-delivered-${notifSettings.boltDeliveredPush ?? false}`}
/>
<Form.Text className="text-muted">
bude skupinová objednávka sledovaná přes Bolt Food doručena, přijde vám push notifikace.
Zakladateli skupiny se neposílá ten dostane upozornění přímo z aplikace Bolt.
bude sledovaná skupinová objednávka doručena, přijde vám push notifikace.
Zakladateli skupiny se neposílá ten dostane upozornění přímo z aplikace rozvozové služby.
</Form.Text>
</Form.Group>
+70 -32
View File
@@ -7,8 +7,8 @@ import DatePicker, { registerLocale } from 'react-datepicker';
import { cs } from 'date-fns/locale';
import 'react-datepicker/dist/react-datepicker.css';
import {
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, PendingQr,
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, setBoltTracking, getOrderDates,
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, PendingQr, TrackingProvider,
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, setTracking, getOrderDates,
} from '../../../types';
import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees';
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
@@ -24,26 +24,57 @@ import PayForGroupModal from '../components/modals/PayForGroupModal';
import EditGroupFeesModal from '../components/modals/EditGroupFeesModal';
import BoltSimulationModal from '../components/modals/BoltSimulationModal';
import PendingPayments from '../components/PendingPayments';
import BoltOrderProgress from '../components/BoltOrderProgress';
import OrderProgress, { PROVIDER_LABEL } from '../components/OrderProgress';
const SLOT = MealSlot.EXTRA;
const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
const BOLT_SHARE_URL_PREFIX = 'https://food.bolt.eu/sharedActiveOrder/';
const BOLT_TOKEN_REGEX = /^[0-9a-f]{64}$/i;
const IS_DEV = process.env.NODE_ENV === 'development';
/** Vytáhne sledovací token ze sdílecí URL Bolt Food, nebo přijme samotný token. Null = neplatný vstup. */
function extractBoltToken(input: string): string | null {
/** Předpony sdílecích odkazů jednotlivých služeb (pro sestavení odkazu z uloženého kódu). */
const TRACKING_URL_PREFIX: Record<TrackingProvider, string> = {
bolt: 'https://food.bolt.eu/sharedActiveOrder/',
wolt: 'https://track.wolt.com/',
};
/** Tvar kódu sledování u jednotlivých služeb — musí odpovídat serveru (trackingProviders.ts). */
const TRACKING_CODE_REGEX: Record<TrackingProvider, RegExp> = {
bolt: /^[0-9a-f]{64}$/i,
wolt: /^[A-Za-z0-9_-]{20,32}$/,
};
const TRACKING_HOST: Record<TrackingProvider, string> = {
bolt: 'bolt.eu',
wolt: 'wolt.com',
};
/** Sestaví odkaz na sledování objednávky ze služby a kódu. */
function trackingUrlFor(provider: TrackingProvider, code: string): string {
return `${TRACKING_URL_PREFIX[provider] ?? ''}${code}`;
}
/**
* Rozpozná rozvozovou službu podle odkazu na sledování (nebo podle samotného kódu)
* a vytáhne z něj kód sledování. Null = vstup nepatří žádné podporované službě.
*/
function extractTracking(input: string): { provider: TrackingProvider; code: string } | null {
const trimmed = input.trim();
if (!trimmed) return null;
if (BOLT_TOKEN_REGEX.test(trimmed)) return trimmed;
let hostname = '';
let lastSegment = '';
try {
const segments = new URL(trimmed).pathname.split('/').filter(Boolean);
const last = segments[segments.length - 1];
return last && BOLT_TOKEN_REGEX.test(last) ? last : null;
const url = new URL(trimmed);
hostname = url.hostname.toLowerCase();
const segments = url.pathname.split('/').filter(Boolean);
lastSegment = segments[segments.length - 1] ?? '';
} catch {
return null;
// Vstup není URL — zkusíme ho vzít jako samotný kód
}
for (const provider of Object.values(TrackingProvider)) {
const regex = TRACKING_CODE_REGEX[provider];
if (!hostname && regex.test(trimmed)) return { provider, code: trimmed };
const host = TRACKING_HOST[provider];
const hostMatches = hostname === host || hostname.endsWith(`.${host}`);
if (hostMatches && regex.test(lastSegment)) return { provider, code: lastSegment };
}
return null;
}
/** Zkrátí dlouhý odkaz pro zobrazení v řádku (zachová začátek i konec). */
@@ -99,7 +130,7 @@ export default function OrderGroupsPage() {
const [editAmounts, setEditAmounts] = useState<Record<string, string>>({});
const [editNotes, setEditNotes] = useState<Record<string, string>>({});
const [editSurcharges, setEditSurcharges] = useState<Record<string, { text: string; amount: string }>>({});
const [editTimes, setEditTimes] = useState<Record<string, { orderedAt: string; deliveryAt: string; boltUrl: string }>>({});
const [editTimes, setEditTimes] = useState<Record<string, { orderedAt: string; deliveryAt: string; trackingUrl: string }>>({});
const [payModal, setPayModal] = useState<OrderGroup | null>(null);
const [feesModal, setFeesModal] = useState<OrderGroup | null>(null);
const [boltSimModal, setBoltSimModal] = useState<OrderGroup | null>(null);
@@ -276,7 +307,7 @@ export default function OrderGroupsPage() {
const handleSaveTimes = async (group: OrderGroup) => {
const times = editTimes[group.id];
if (!times) return;
const { orderedAt, deliveryAt, boltUrl } = times;
const { orderedAt, deliveryAt, trackingUrl } = times;
if (orderedAt && !TIME_REGEX.test(orderedAt)) {
setPageError('Čas objednání musí být ve formátu HH:MM');
return;
@@ -285,16 +316,16 @@ export default function OrderGroupsPage() {
setPageError('Čas doručení musí být ve formátu HH:MM');
return;
}
// Bolt odkaz se odesílá jen při změně oproti aktuálnímu tokenu skupiny
const boltToken = boltUrl.trim() ? extractBoltToken(boltUrl) : null;
if (boltUrl.trim() && !boltToken) {
setPageError('Neplatný odkaz Bolt (očekávána URL sdílení objednávky)');
// Odkaz na sledování se odesílá jen při změně oproti aktuálnímu kódu skupiny
const tracking = trackingUrl.trim() ? extractTracking(trackingUrl) : null;
if (trackingUrl.trim() && !tracking) {
setPageError('Neplatný odkaz pro sledování (očekáván odkaz Bolt Food nebo Wolt)');
return;
}
const boltChanged = (boltToken ?? undefined) !== group.boltTrackingToken;
const trackingChanged = tracking?.code !== group.trackingCode || tracking?.provider !== group.trackingProvider;
let ok = await refresh(() => updateGroupTimes({ body: { id: group.id, orderedAt, deliveryAt } }));
if (ok && boltChanged) {
ok = await refresh(() => setBoltTracking({ body: { id: group.id, shareUrl: boltUrl.trim() } }));
if (ok && trackingChanged) {
ok = await refresh(() => setTracking({ body: { id: group.id, shareUrl: trackingUrl.trim() } }));
}
if (ok) setEditTimes(prev => { const next = { ...prev }; delete next[group.id]; return next; });
};
@@ -731,13 +762,13 @@ export default function OrderGroupsPage() {
/>
</div>
<div className="d-flex align-items-center gap-1">
<small className="text-muted text-nowrap">Bolt odkaz pro sledování:</small>
<small className="text-muted text-nowrap">Odkaz pro sledování (Bolt Food / Wolt):</small>
<Form.Control
type="text"
size="sm"
placeholder={`${BOLT_SHARE_URL_PREFIX}`}
value={editTimes[group.id]?.boltUrl ?? ''}
onChange={e => setEditTimes(prev => ({ ...prev, [group.id]: { ...prev[group.id], boltUrl: e.target.value } }))}
placeholder={`${TRACKING_URL_PREFIX.wolt}`}
value={editTimes[group.id]?.trackingUrl ?? ''}
onChange={e => setEditTimes(prev => ({ ...prev, [group.id]: { ...prev[group.id], trackingUrl: e.target.value } }))}
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveTimes(group); }}
style={{ width: 260 }}
/>
@@ -750,8 +781,10 @@ export default function OrderGroupsPage() {
{(() => {
const canEdit = !isReadOnly && isCreator;
// Aktivace editačního režimu stejné chování jako tlačítko s tužkou
const startEdit = () => canEdit && setEditTimes(prev => ({ ...prev, [group.id]: { orderedAt: group.orderedAt ?? '', deliveryAt: group.deliveryAt ?? '', boltUrl: group.boltTrackingToken ? `${BOLT_SHARE_URL_PREFIX}${group.boltTrackingToken}` : '' } }));
const trackingUrl = group.boltTrackingToken ? `${BOLT_SHARE_URL_PREFIX}${group.boltTrackingToken}` : null;
const trackingUrl = group.trackingProvider && group.trackingCode
? trackingUrlFor(group.trackingProvider, group.trackingCode)
: null;
const startEdit = () => canEdit && setEditTimes(prev => ({ ...prev, [group.id]: { orderedAt: group.orderedAt ?? '', deliveryAt: group.deliveryAt ?? '', trackingUrl: trackingUrl ?? '' } }));
return (
<>
{canEdit && (
@@ -775,9 +808,9 @@ export default function OrderGroupsPage() {
onClick={startEdit}
>
Doručení v: <strong>{group.deliveryAt ?? '—'}</strong>
{group.boltTrackingToken && (
<OverlayTrigger overlay={<Tooltip>Čas doručení se aktualizuje automaticky z Bolt Food</Tooltip>}>
<Badge bg="success" className="ms-1">Bolt</Badge>
{group.trackingCode && group.trackingProvider && (
<OverlayTrigger overlay={<Tooltip>Čas doručení se aktualizuje automaticky z {PROVIDER_LABEL[group.trackingProvider]}</Tooltip>}>
<Badge bg="success" className="ms-1">{PROVIDER_LABEL[group.trackingProvider]}</Badge>
</OverlayTrigger>
)}
</small>
@@ -802,9 +835,14 @@ export default function OrderGroupsPage() {
})()}
</div>
)}
{group.boltOrderState && (
{group.trackingOrderState && group.trackingProvider && (
<div className="mt-2">
<BoltOrderProgress state={group.boltOrderState} courierState={group.boltCourierState} tracking={!!group.boltTrackingToken} />
<OrderProgress
provider={group.trackingProvider}
state={group.trackingOrderState}
courierState={group.trackingCourierState}
tracking={!!group.trackingCode}
/>
</div>
)}
{IS_DEV && (