feat: uživatelsky přívětivější možnost zadání sledovací URL pro Bolt Food
CI / Generate TypeScript types (push) Successful in 13s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 37s
CI / Playwright E2E tests (push) Successful in 1m24s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s

This commit is contained in:
2026-06-10 19:28:46 +02:00
parent 88b9e20e2d
commit b42b051e6f
2 changed files with 67 additions and 20 deletions
+64 -19
View File
@@ -2,7 +2,7 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Alert, Badge, Button, Card, Form, Modal, OverlayTrigger, Table, Tooltip } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { faBasketShopping, faChevronLeft, faChevronRight, faCircleCheck, faClockRotateLeft, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
import { faBasketShopping, faChevronLeft, faChevronRight, faCircleCheck, faClockRotateLeft, faGear, faLock, faLockOpen, faPen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
import DatePicker, { registerLocale } from 'react-datepicker';
import { cs } from 'date-fns/locale';
import 'react-datepicker/dist/react-datepicker.css';
@@ -43,6 +43,14 @@ function extractBoltToken(input: string): string | null {
}
}
/** Zkrátí dlouhý odkaz pro zobrazení v řádku (zachová začátek i konec). */
function shortenUrl(url: string, max = 48): string {
if (url.length <= max) return url;
const head = Math.ceil((max - 1) / 2);
const tail = Math.floor((max - 1) / 2);
return `${url.slice(0, head)}${url.slice(url.length - tail)}`;
}
// Český lokál pro date picker (názvy měsíců/dnů, pondělí jako první den)
registerLocale('cs', cs);
@@ -706,7 +714,7 @@ export default function OrderGroupsPage() {
/>
</div>
<div className="d-flex align-items-center gap-1">
<small className="text-muted text-nowrap">Bolt odkaz:</small>
<small className="text-muted text-nowrap">Bolt odkaz pro sledování:</small>
<Form.Control
type="text"
size="sm"
@@ -721,23 +729,60 @@ export default function OrderGroupsPage() {
<Button size="sm" variant="outline-secondary" onClick={() => setEditTimes(prev => { const n = { ...prev }; delete n[group.id]; return n; })}>Zrušit</Button>
</div>
) : (
<div
className="d-flex align-items-center gap-3 flex-wrap"
style={{ cursor: !isReadOnly && isCreator ? 'pointer' : undefined }}
onClick={() => !isReadOnly && isCreator && setEditTimes(prev => ({ ...prev, [group.id]: { orderedAt: group.orderedAt ?? '', deliveryAt: group.deliveryAt ?? '', boltUrl: group.boltTrackingToken ? `${BOLT_SHARE_URL_PREFIX}${group.boltTrackingToken}` : '' } }))}
title={!isReadOnly && isCreator ? 'Klikněte pro úpravu časů' : undefined}
>
<small className="text-muted">
Objednáno v: <strong>{group.orderedAt ?? '—'}</strong>
</small>
<small className="text-muted">
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>
</OverlayTrigger>
)}
</small>
<div className="d-flex align-items-center gap-3 flex-wrap">
{(() => {
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;
return (
<>
{canEdit && (
<FontAwesomeIcon
icon={faPen}
className="action-icon"
title="Upravit časy a odkaz pro sledování"
onClick={startEdit}
/>
)}
<small
className="text-muted"
style={{ cursor: canEdit ? 'pointer' : undefined }}
onClick={startEdit}
>
Objednáno v: <strong>{group.orderedAt ?? '—'}</strong>
</small>
<small
className="text-muted"
style={{ cursor: canEdit ? 'pointer' : undefined }}
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>
</OverlayTrigger>
)}
</small>
<small className="text-muted text-nowrap">
URL pro sledování:{' '}
{trackingUrl ? (
<a
href={trackingUrl}
target="_blank"
rel="noopener noreferrer"
title={trackingUrl}
onClick={e => e.stopPropagation()}
>
{shortenUrl(trackingUrl)}
</a>
) : (
<strong></strong>
)}
</small>
</>
);
})()}
</div>
)}
</div>