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
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:
@@ -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} Kč)
|
||||||
|
{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} Kč)
|
</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"
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user