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, 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'; import { 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'; import { useAuth } from '../context/auth'; import { useSettings } from '../context/settings'; import { formatDate, formatDateString } from '../Utils'; import Login from '../Login'; import Header from '../components/Header'; import Footer from '../components/Footer'; import Loader from '../components/Loader'; import StoreAdminModal from '../components/modals/StoreAdminModal'; import PayForGroupModal from '../components/modals/PayForGroupModal'; import EditGroupFeesModal from '../components/modals/EditGroupFeesModal'; import BoltSimulationModal from '../components/modals/BoltSimulationModal'; import PendingPayments from '../components/PendingPayments'; import OrderProgress, { PROVIDER_LABEL } from '../components/OrderProgress'; const SLOT = MealSlot.EXTRA; const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/; const IS_DEV = process.env.NODE_ENV === 'development'; /** Předpony sdílecích odkazů jednotlivých služeb (pro sestavení odkazu z uloženého kódu). */ const TRACKING_URL_PREFIX: Record = { 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 = { bolt: /^[0-9a-f]{64}$/i, wolt: /^[A-Za-z0-9_-]{20,32}$/, }; const TRACKING_HOST: Record = { 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; let hostname = ''; let lastSegment = ''; try { const url = new URL(trimmed); hostname = url.hostname.toLowerCase(); const segments = url.pathname.split('/').filter(Boolean); lastSegment = segments[segments.length - 1] ?? ''; } catch { // 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). */ 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); /** Vrátí ISO datum (YYYY-MM-DD) posunuté o předaný počet dní. */ function shiftIsoDate(iso: string, days: number): string { const date = new Date(`${iso}T00:00:00`); date.setDate(date.getDate() + days); return formatDate(date); } /** Převede ISO datum (YYYY-MM-DD) na lokální Date (půlnoc), nebo null. */ function isoToDate(iso?: string): Date | null { return iso ? new Date(`${iso}T00:00:00`) : null; } function stateBadge(state: GroupState) { const map: Record = { [GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' }, [GroupState.LOCKED]: { bg: 'warning', label: 'Uzamčeno' }, [GroupState.ORDERED]: { bg: 'secondary', label: 'Objednáno' }, }; const { bg, label } = map[state] ?? { bg: 'light', label: state }; return {label}; } export default function OrderGroupsPage() { const auth = useAuth(); const settings = useSettings(); const socket = useContext(SocketContext); const [data, setData] = useState(); const [failure, setFailure] = useState(false); // Vybrané datum pro zobrazení historie (undefined = aktuální den) const [selectedDate, setSelectedDate] = useState(); // ISO datum dnešního dne dle serveru (horní hranice navigace), zjištěné při prvním načtení const [todayIso, setTodayIso] = useState(); // Ref pro socket handler – aby věděl, zda se zobrazuje historie (na ní se živé aktualizace neaplikují) const selectedDateRef = useRef(undefined); // ISO data dnů, ve kterých existuje aspoň jedna objednávka (pro zvýraznění v date pickeru) const [orderDates, setOrderDates] = useState([]); const [newGroupName, setNewGroupName] = useState(''); const [creating, setCreating] = useState(false); const [adminModalOpen, setAdminModalOpen] = useState(false); const [editAmounts, setEditAmounts] = useState>({}); const [editNotes, setEditNotes] = useState>({}); const [editSurcharges, setEditSurcharges] = useState>({}); const [editTimes, setEditTimes] = useState>({}); const [payModal, setPayModal] = useState(null); const [feesModal, setFeesModal] = useState(null); const [boltSimModal, setBoltSimModal] = useState(null); const [confirmOrderGroup, setConfirmOrderGroup] = useState(null); const [pageError, setPageError] = useState(null); const fetchData = async (date?: string) => { try { const r = await getData({ query: { slot: SLOT, date } }); if (r.data) { setData(r.data); // Při zobrazení aktuálního dne si zapamatujeme dnešní ISO datum jako horní hranici navigace if (!date && r.data.isoDate) setTodayIso(r.data.isoDate); } } catch { setFailure(true); } }; // Načte dny s objednávkou pro zvýraznění v date pickeru const fetchOrderDates = async () => { const r = await getOrderDates(); if (r.data?.dates) setOrderDates(r.data.dates); }; useEffect(() => { selectedDateRef.current = selectedDate; }, [selectedDate]); useEffect(() => { if (!auth?.login) return; fetchData(selectedDate); }, [auth?.login, selectedDate]); useEffect(() => { if (!auth?.login) return; fetchOrderDates(); }, [auth?.login]); useEffect(() => { socket.on(EVENT_MESSAGE, (newData: ClientData) => { // Živé aktualizace se týkají vždy dneška – při zobrazení historie je ignorujeme if (selectedDateRef.current) return; if (newData.slot === SLOT) setData(prev => ({ ...newData, stores: newData.stores ?? prev?.stores, })); }); // Nová nevyřízená platba (QR kód) – připojíme do dat, aby se zobrazila i bez znovunačtení stránky socket.on(EVENT_PENDING_QR, (pendingQr: PendingQr) => { if (selectedDateRef.current) return; setData(prev => prev ? { ...prev, pendingQrs: [...(prev.pendingQrs ?? []), pendingQr] } : prev); }); return () => { socket.off(EVENT_MESSAGE); socket.off(EVENT_PENDING_QR); }; }, [socket]); // Připojení do osobní socket místnosti po přihlášení – bez toho nechodí události // o nových nevyřízených platbách (QR kódy se posílají do místnosti user:) useEffect(() => { if (auth?.login) { socket.emit('join', auth.login); } }, [auth?.login, socket]); useEffect(() => { // Po znovupřipojení socketu znovu vstoupíme do osobní místnosti a načteme aktuálně // zobrazený den (mohli jsme přijít o živé aktualizace) const onReconnect = () => { if (auth?.login) socket.emit('join', auth.login); fetchData(selectedDateRef.current); }; socket.io.on('reconnect', onReconnect); return () => { socket.io.off('reconnect', onReconnect); }; }, [socket, auth?.login]); // Navigace mezi dny pomocí klávesových šipek (←/→), obdobně jako na hlavní stránce const handleKeyDown = useCallback((e: KeyboardEvent) => { // Ignorujeme, pokud uživatel právě píše do formulářového pole const tag = (e.target as HTMLElement)?.tagName; if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return; const currentIso = data?.isoDate; if (!currentIso) return; if (e.keyCode === 37) { // Předchozí den – do minulosti bez omezení setSelectedDate(shiftIsoDate(currentIso, -1)); } else if (e.keyCode === 39 && todayIso != null && currentIso < todayIso) { // Následující den – nejvýše po dnešek (na dnešek přes undefined kvůli živým aktualizacím) const target = shiftIsoDate(currentIso, 1); setSelectedDate(target >= todayIso ? undefined : target); } }, [data?.isoDate, todayIso]); useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [handleKeyDown]); const refresh = async (fn: () => Promise): Promise => { setPageError(null); const result = await fn(); if (result?.error) { setPageError((result.error as any).error || 'Nastala chyba'); await fetchData(); return false; } if (result?.data) { setData(result.data); socket.emit?.('message', result.data as ClientData); } // Sada dnů s objednávkou se mohla změnit (vytvoření/smazání skupiny) fetchOrderDates(); return true; }; const handleCreate = async () => { if (!newGroupName || !auth?.login) return; setCreating(true); const ok = await refresh(() => createGroup({ body: { name: newGroupName } })); if (ok) setNewGroupName(''); setCreating(false); }; const handleJoin = (groupId: string) => refresh(() => addGroupMember({ body: { id: groupId } })); const handleToggleLock = (group: OrderGroup) => { const next = group.state === GroupState.OPEN ? GroupState.LOCKED : GroupState.OPEN; return refresh(() => setGroupState({ body: { id: group.id, state: next } })); }; const handleConfirmOrdered = async (group: OrderGroup) => { setConfirmOrderGroup(null); await refresh(() => setGroupState({ body: { id: group.id, state: GroupState.ORDERED } })); }; const handleRevertOrdered = (group: OrderGroup) => refresh(() => setGroupState({ body: { id: group.id, state: GroupState.LOCKED } })); const handleDelete = (groupId: string) => refresh(() => deleteGroup({ body: { id: groupId } })); const handleSaveAmount = async (groupId: string, login: string) => { const key = `${groupId}:${login}`; const raw = editAmounts[key]; const n = parseFloat(raw ?? ''); if (!raw || isNaN(n) || n < 0) { setPageError('Zadejte platnou kladnou částku'); return; } const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, amount: Math.round(n * 100) } })); if (ok) setEditAmounts(prev => { const next = { ...prev }; delete next[key]; return next; }); }; const handleSaveNote = async (groupId: string, login: string) => { const key = `${groupId}:${login}`; const note = editNotes[key] ?? ''; const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, note } })); if (ok) setEditNotes(prev => { const next = { ...prev }; delete next[key]; return next; }); }; const handleSaveSurcharge = async (groupId: string, login: string) => { const key = `${groupId}:${login}`; const surchargeText = editSurcharges[key]?.text ?? ''; const rawAmount = editSurcharges[key]?.amount ?? ''; const surchargeAmount = rawAmount === '' ? 0 : parseFloat(rawAmount.replace(',', '.')); if (rawAmount !== '' && (isNaN(surchargeAmount) || surchargeAmount < 0)) { setPageError('Zadejte platnou výši příplatku'); return; } const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, surchargeText, surchargeAmount: rawAmount === '' ? 0 : Math.round(surchargeAmount * 100) } })); if (ok) setEditSurcharges(prev => { const next = { ...prev }; delete next[key]; return next; }); }; const handleSaveTimes = async (group: OrderGroup) => { const times = editTimes[group.id]; if (!times) return; const { orderedAt, deliveryAt, trackingUrl } = times; if (orderedAt && !TIME_REGEX.test(orderedAt)) { setPageError('Čas objednání musí být ve formátu HH:MM'); return; } if (deliveryAt && !TIME_REGEX.test(deliveryAt)) { setPageError('Čas doručení musí být ve formátu HH:MM'); return; } // 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 trackingChanged = tracking?.code !== group.trackingCode || tracking?.provider !== group.trackingProvider; let ok = await refresh(() => updateGroupTimes({ body: { id: group.id, orderedAt, deliveryAt } })); 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; }); }; // Pozn.: tyto funkce se volají až v renderu, kde je k dispozici `selectedDate`. // Historie (jiný než aktuální den) je vždy read-only. const canEditMember = (group: OrderGroup, targetLogin: string) => { if (selectedDate) return false; if (group.state === GroupState.ORDERED) return false; if (auth?.login === group.creatorLogin) return true; if (auth?.login === targetLogin && group.state === GroupState.OPEN) return true; return false; }; const canManageMembers = (group: OrderGroup) => { if (selectedDate) return false; if (group.state === GroupState.ORDERED) return false; if (auth?.login === group.creatorLogin) return true; return group.state === GroupState.OPEN; }; if (!auth?.login) return ; if (failure) return ( ); if (!data) return ( ); const stores = data.stores ?? []; const groups = data.groups ?? []; // Zobrazené datum a režim historie (vše read-only, pokud nejde o aktuální den) const displayedIso = data.isoDate; const isToday = !selectedDate || (todayIso != null && displayedIso === todayIso); const isReadOnly = !isToday; const canGoNext = todayIso != null && displayedIso != null && displayedIso < todayIso; const goToDay = (offset: number) => { if (!displayedIso) return; const target = shiftIsoDate(displayedIso, offset); // Na dnešek (či dál) se vracíme přes undefined, aby se obnovily živé aktualizace setSelectedDate(todayIso != null && target >= todayIso ? undefined : target); }; const handleDatePick = (value: string) => { if (!value) return; setSelectedDate(todayIso != null && value >= todayIso ? undefined : value); }; // Dny s objednávkou jako Date objekty pro zvýraznění v kalendáři const highlightedOrderDates = orderDates .map(d => isoToDate(d)) .filter((d): d is Date => d != null); return (

Objednání

Skupinové objednávky z obchodů a restaurací

{/* Navigace mezi dny – šipky kolem výběru data (i klávesami ←/→) */}
goToDay(-1)} /> handleDatePick(d ? formatDate(d) : '')} maxDate={isoToDate(todayIso) ?? undefined} highlightDates={[{ 'luncher-order-day': highlightedOrderDates }]} locale="cs" dateFormat="d. M. yyyy" calendarStartDay={1} popperPlacement="bottom" className={`form-control text-center fw-semibold order-date-input ${isReadOnly ? 'text-muted' : ''}`} /> canGoNext && goToDay(1)} />
{isReadOnly && ( Prohlížíte historii ze dne {displayedIso ? formatDateString(displayedIso) : data.date} – data jsou pouze pro čtení. )} {pageError && ( setPageError(null)} className="mt-2"> {pageError} )}
{/* Vytvoření nové skupiny – pouze pro aktuální den */} {!isReadOnly && (
Vytvořit skupinu
{stores.length === 0 ? (

Nejsou přidány žádné obchody.{' '}

) : (
setNewGroupName(e.target.value)} style={{ maxWidth: 260 }} > {stores.map(s => )}
)}
)} {/* Seznam skupin */} {groups.length === 0 && (

{isReadOnly ? 'Pro tento den nejsou žádné skupiny.' : 'Zatím žádné skupiny pro dnešní den.'}

)} {groups.map(group => { const login = auth!.login ?? ''; const isCreator = login === group.creatorLogin; const isMember = login in group.members; const isOrdered = group.state === GroupState.ORDERED; const isLocked = group.state === GroupState.LOCKED; const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][]; const editingTimes = group.id in editTimes; // URL na nabídku podniku (pokud ji má dohledatelný obchod vyplněnou). // Povolíme jen http(s), aby odkaz nemohl být zneužit (např. javascript:). const rawStoreUrl = stores.find(s => s.name === group.name)?.url; const storeUrl = rawStoreUrl && /^https?:\/\//i.test(rawStoreUrl) ? rawStoreUrl : undefined; const totalFees = (group.fees ?? 0) + (group.shipping ?? 0) + (group.tip ?? 0); // Poplatky se dělí jen mezi aktivní strávníky (kdo si reálně něco objednal). const activeCount = countActiveMembers(group.members); const feeShare = computeFeeShare(totalFees, activeCount); const feeParams = { totalFees, discountType: group.discountType, discountValue: group.discountValue ?? 0 }; const getMemberTotal = (m: OrderGroupMember) => computeMemberTotal(m, feeParams, feeShare, activeCount); return (
{storeUrl ? ( {group.name} ) : ( {group.name} )} {stateBadge(group.state)} zakladatel: {group.creatorLogin}
{!isReadOnly && isCreator && !isOrdered && ( <> {isLocked && ( )} )} {!isReadOnly && isCreator && isOrdered && ( <> {settings?.bankAccount && settings?.holderName && !group.qrGenerated && ( )} )} {!isReadOnly && !isMember && !isOrdered && !isLocked && ( )}
{memberEntries.map(([memberLogin, member]) => { const key = `${group.id}:${memberLogin}`; const editingAmount = key in editAmounts; const editingNote = key in editNotes; const editingSurcharge = key in editSurcharges; const canEdit = canEditMember(group, memberLogin); const memberTotal = getMemberTotal(member); return ( ); })} {(() => { const sumBase = memberEntries.reduce((sum, [, m]) => sum + (m.amount ?? 0) + (m.surchargeAmount ?? 0), 0); const dv = group.discountValue ?? 0; const totalDiscount = dv > 0 ? (group.discountType === 'percent' ? Math.round(sumBase * dv / 100) : dv) : 0; const groupTotal = sumBase + totalFees - totalDiscount; return groupTotal > 0 ? ( ) : null; })()}
Člen Částka (bez slev) Příplatek Poznámka Celkem (s poplatky)
{memberLogin} {memberLogin === group.creatorLogin && ( Zakladatel / objednávající}> )} {member.paid && ( Zaplaceno}> )} {canEdit && editingAmount ? (
setEditAmounts(prev => ({ ...prev, [key]: e.target.value }))} onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveAmount(group.id, memberLogin); if (e.key === 'Escape') setEditAmounts(prev => { const n = { ...prev }; delete n[key]; return n; }); }} style={{ width: 95 }} autoFocus />
) : ( canEdit && setEditAmounts(prev => ({ ...prev, [key]: member.amount != null ? String(member.amount / 100) : '' }))} title={canEdit ? 'Klikněte pro úpravu' : undefined} > {member.amount != null ? `${member.amount / 100} Kč` : } )}
{canEdit && editingSurcharge ? (
setEditSurcharges(prev => ({ ...prev, [key]: { ...prev[key], text: e.target.value } }))} onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveSurcharge(group.id, memberLogin); if (e.key === 'Escape') setEditSurcharges(prev => { const n = { ...prev }; delete n[key]; return n; }); }} style={{ width: 80 }} autoFocus /> setEditSurcharges(prev => ({ ...prev, [key]: { ...prev[key], amount: e.target.value } }))} onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveSurcharge(group.id, memberLogin); if (e.key === 'Escape') setEditSurcharges(prev => { const n = { ...prev }; delete n[key]; return n; }); }} style={{ width: 60 }} />
) : ( canEdit && setEditSurcharges(prev => ({ ...prev, [key]: { text: member.surchargeText ?? '', amount: member.surchargeAmount != null ? String(member.surchargeAmount / 100) : '' } }))} title={canEdit ? 'Klikněte pro úpravu příplatku' : undefined} > {member.surchargeAmount != null && member.surchargeAmount > 0 ? ( {member.surchargeText ? `${member.surchargeText}: ` : ''}{member.surchargeAmount / 100} Kč ) : ( )} )}
{canEdit && editingNote ? (
setEditNotes(prev => ({ ...prev, [key]: e.target.value }))} onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveNote(group.id, memberLogin); if (e.key === 'Escape') setEditNotes(prev => { const n = { ...prev }; delete n[key]; return n; }); }} autoFocus />
) : ( canEdit && setEditNotes(prev => ({ ...prev, [key]: member.note ?? '' }))} title={canEdit ? 'Klikněte pro úpravu poznámky' : undefined} > {member.note || '—'} )}
0 ? 'fw-bold' : 'text-muted'}> {memberTotal > 0 ? `${memberTotal / 100} Kč` : '—'}
{canManageMembers(group) && (isCreator || memberLogin === login) && (memberLogin !== group.creatorLogin) && ( refresh(() => removeGroupMember({ body: { id: group.id, login: memberLogin } }))} /> )}
Celkem za skupinu: {groupTotal / 100} Kč
{/* Souhrn poplatků a slevy */} {(totalFees > 0 || (group.discountValue != null && group.discountValue > 0)) && (
{group.fees != null && group.fees > 0 && Poplatky: {group.fees / 100} Kč} {group.shipping != null && group.shipping > 0 && Doprava: {group.shipping / 100} Kč} {group.tip != null && group.tip > 0 && Spropitné: {group.tip / 100} Kč} {feeShare > 0 && {feeShare / 100} Kč/os.} {group.discountValue != null && group.discountValue > 0 && ( Sleva: {group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue / 100} Kč`} )}
)} {/* Časy objednání a doručení */} {isOrdered && (
{!isReadOnly && isCreator && editingTimes ? (
Objednáno v: setEditTimes(prev => ({ ...prev, [group.id]: { ...prev[group.id], orderedAt: e.target.value } }))} onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveTimes(group); }} style={{ width: 75 }} autoFocus />
Doručení v: setEditTimes(prev => ({ ...prev, [group.id]: { ...prev[group.id], deliveryAt: e.target.value } }))} onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveTimes(group); }} style={{ width: 75 }} />
Odkaz pro sledování (Bolt Food / Wolt): 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 }} />
) : (
{(() => { const canEdit = !isReadOnly && isCreator; // Aktivace editačního režimu – stejné chování jako tlačítko s tužkou 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 && ( )} Objednáno v: {group.orderedAt ?? '—'} Doručení v: {group.deliveryAt ?? '—'} {group.trackingCode && group.trackingProvider && ( Čas doručení se aktualizuje automaticky z {PROVIDER_LABEL[group.trackingProvider]}}> {PROVIDER_LABEL[group.trackingProvider]} )} URL pro sledování:{' '} {trackingUrl ? ( e.stopPropagation()} > {shortenUrl(trackingUrl)} ) : ( )} ); })()}
)} {group.trackingOrderState && group.trackingProvider && (
)} {IS_DEV && (
)}
)}
); })} {/* Nevyřízené platby přihlášeného uživatele – jen v režimu aktuálního dne */} {!isReadOnly && ( fetchData()} /> )}
); }