feat: přehlednější zobrazení QR kódů, oprava zobrazení na stránce objednání
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 30s
CI / Build client (push) Successful in 37s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 46s
CI / Notify (push) Successful in 3s

This commit is contained in:
2026-06-11 11:45:24 +02:00
parent b42b051e6f
commit 9152425d2b
2 changed files with 76 additions and 20 deletions
+61 -17
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Button } from 'react-bootstrap'; import { Button, Modal } from 'react-bootstrap';
import { PendingQr, dismissQr } from '../../../types'; import { PendingQr, dismissQr } from '../../../types';
import { formatDateString } from '../Utils'; import { formatDateString } from '../Utils';
import ConfirmModal from './modals/ConfirmModal'; import ConfirmModal from './modals/ConfirmModal';
@@ -13,31 +13,75 @@ type Props = {
// Sekce "Nevyřízené platby" zobrazí QR kódy neuhrazených plateb přihlášeného uživatele // Sekce "Nevyřízené platby" zobrazí QR kódy neuhrazených plateb přihlášeného uživatele
// včetně tlačítka "Zaplatil jsem" a potvrzovacího dialogu. Sdíleno hlavní stránkou i stránkou objednávek. // včetně tlačítka "Zaplatil jsem" a potvrzovacího dialogu. Sdíleno hlavní stránkou i stránkou objednávek.
// Při příchodu nových nevyřízených plateb se navíc automaticky otevře modální dialog,
// aby si uživatel QR kódů určitě všiml (často si jich nevšimnou, protože sekce je dole na stránce).
export default function PendingPayments({ pendingQrs, login, onDismissed }: Readonly<Props>) { export default function PendingPayments({ pendingQrs, login, onDismissed }: Readonly<Props>) {
const [dismissQrId, setDismissQrId] = useState<string | null>(null); const [dismissQrId, setDismissQrId] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
// ID QR kódů, pro které už byl v rámci tohoto načtení stránky automaticky zobrazen
// modální dialog. Drží se jen v paměti (ne v sessionStorage), takže se při každém
// ručním přenačtení stránky vynuluje a dialog se znovu otevře, dokud uživatel platby
// neuhradí. Zároveň se nepřekrývá při pouhém obnovení dat či příchodu už zobrazeného QR.
const autoShownQrIds = useRef<Set<string>>(new Set());
const qrIdsKey = (pendingQrs ?? []).map(qr => qr.id).join(',');
// Automaticky otevřeme modální dialog, jakmile přijdou nové (dosud nezobrazené) platby.
useEffect(() => {
const ids = (pendingQrs ?? []).map(qr => qr.id);
if (ids.length === 0) return;
const unseen = ids.filter(id => !autoShownQrIds.current.has(id));
if (unseen.length > 0) {
setModalOpen(true);
unseen.forEach(id => autoShownQrIds.current.add(id));
}
}, [qrIdsKey, pendingQrs]);
if (!pendingQrs || pendingQrs.length === 0) return null; if (!pendingQrs || pendingQrs.length === 0) return null;
// Vykreslení jednoho QR kódu i s tlačítkem "Zaplatil jsem" sdíleno sekcí i modálem.
const renderQr = (qr: PendingQr) => (
<div key={qr.id} className='qr-code mb-3'>
<p>
<strong>{formatDateString(qr.date)}</strong> {qr.creator} ({qr.totalPrice / 100} )
{qr.purpose && <><br /><span className="text-muted">{qr.purpose}</span></>}
</p>
<img src={`/api/qr?login=${login}&id=${qr.id}`} alt='QR kód' />
<div className='mt-2'>
<Button variant="success" onClick={() => setDismissQrId(qr.id)}>
Zaplatil jsem
</Button>
</div>
</div>
);
return ( return (
<> <>
<div className='pizza-section fade-in mt-4'> <div className='pizza-section fade-in mt-4'>
<h3>Nevyřízené platby</h3> <h3>Nevyřízené platby</h3>
<p>Máte neuhrazené platby.</p> <p>
{pendingQrs.map(qr => ( Máte neuhrazené platby.{' '}
<div key={qr.id} className='qr-code mb-3'> <Button variant="link" className="p-0 align-baseline" onClick={() => setModalOpen(true)}>
<p> Zobrazit QR kódy
<strong>{formatDateString(qr.date)}</strong> {qr.creator} ({qr.totalPrice / 100} ) </Button>
{qr.purpose && <><br /><span className="text-muted">{qr.purpose}</span></>} </p>
</p> {pendingQrs.map(renderQr)}
<img src={`/api/qr?login=${login}&id=${qr.id}`} alt='QR kód' />
<div className='mt-2'>
<Button variant="success" onClick={() => setDismissQrId(qr.id)}>
Zaplatil jsem
</Button>
</div>
</div>
))}
</div> </div>
<Modal show={modalOpen} onHide={() => setModalOpen(false)} centered scrollable>
<Modal.Header closeButton>
<Modal.Title>Nevyřízené platby</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Máte neuhrazené platby. Naskenujte QR kód pro zaplacení.</p>
{pendingQrs.map(renderQr)}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setModalOpen(false)}>
Zavřít
</Button>
</Modal.Footer>
</Modal>
<ConfirmModal <ConfirmModal
isOpen={dismissQrId !== null} isOpen={dismissQrId !== null}
title="Potvrzení platby" title="Potvrzení platby"
+15 -3
View File
@@ -152,12 +152,24 @@ export default function OrderGroupsPage() {
return () => { socket.off(EVENT_MESSAGE); socket.off(EVENT_PENDING_QR); }; return () => { socket.off(EVENT_MESSAGE); socket.off(EVENT_PENDING_QR); };
}, [socket]); }, [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:<login>)
useEffect(() => { useEffect(() => {
// Po znovupřipojení socketu načteme aktuálně zobrazený den (mohli jsme přijít o živé aktualizace) if (auth?.login) {
const onReconnect = () => fetchData(selectedDateRef.current); 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); socket.io.on('reconnect', onReconnect);
return () => { socket.io.off('reconnect', onReconnect); }; return () => { socket.io.off('reconnect', onReconnect); };
}, [socket]); }, [socket, auth?.login]);
// Navigace mezi dny pomocí klávesových šipek (←/→), obdobně jako na hlavní stránce // Navigace mezi dny pomocí klávesových šipek (←/→), obdobně jako na hlavní stránce
const handleKeyDown = useCallback((e: KeyboardEvent) => { const handleKeyDown = useCallback((e: KeyboardEvent) => {