import { useContext, useEffect, 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, faCircleCheck, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, } from '../../../types'; import { EVENT_MESSAGE, SocketContext } from '../context/socket'; import { useAuth } from '../context/auth'; import { useSettings } from '../context/settings'; 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'; const SLOT = MealSlot.EXTRA; const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/; 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); 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 [confirmOrderGroup, setConfirmOrderGroup] = useState(null); const [pageError, setPageError] = useState(null); const fetchData = async () => { try { const r = await getData({ query: { slot: SLOT } }); if (r.data) setData(r.data); } catch { setFailure(true); } }; useEffect(() => { if (!auth?.login) return; fetchData(); }, [auth?.login]); useEffect(() => { socket.on(EVENT_MESSAGE, (newData: ClientData) => { if (newData.slot === SLOT) setData(prev => ({ ...newData, stores: newData.stores ?? prev?.stores, })); }); return () => { socket.off(EVENT_MESSAGE); }; }, [socket]); useEffect(() => { const onReconnect = () => fetchData(); socket.io.on('reconnect', onReconnect); return () => { socket.io.off('reconnect', onReconnect); }; }, [socket]); 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); } 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 } = 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; } const ok = await refresh(() => updateGroupTimes({ body: { id: group.id, orderedAt, deliveryAt } })); if (ok) setEditTimes(prev => { const next = { ...prev }; delete next[group.id]; return next; }); }; const canEditMember = (group: OrderGroup, targetLogin: string) => { 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 (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 ?? []; return (

Objednání

Skupinové objednávky z obchodů a restaurací

{pageError && ( setPageError(null)} className="mt-2"> {pageError} )}
{/* Vytvoření nové skupiny */}
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 && (

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 memberCount = memberEntries.length; const editingTimes = group.id in editTimes; const totalFees = (group.fees ?? 0) + (group.shipping ?? 0) + (group.tip ?? 0); const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0; const getMemberTotal = (m: OrderGroupMember) => { const base = m.amount ?? 0; const surcharge = m.surchargeAmount ?? 0; const dv = group.discountValue ?? 0; const discount = dv > 0 ? (group.discountType === 'percent' ? Math.round((base + surcharge) * dv / 100) : Math.round(dv / memberCount)) : 0; return base + surcharge + feeShare - discount; }; return (
{group.name} {stateBadge(group.state)} zakladatel: {group.creatorLogin}
{isCreator && !isOrdered && ( <> {isLocked && ( )} )} {isCreator && isOrdered && ( <> {settings?.bankAccount && settings?.holderName && !group.qrGenerated && ( )} )} {!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 && (
{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 }} />
) : (
isCreator && setEditTimes(prev => ({ ...prev, [group.id]: { orderedAt: group.orderedAt ?? '', deliveryAt: group.deliveryAt ?? '' } }))} title={isCreator ? 'Klikněte pro úpravu časů' : undefined} > Objednáno v: {group.orderedAt ?? '—'} Doručení v: {group.deliveryAt ?? '—'}
)}
)}
); })}
); }