feat: vylepšení funkce objednávání
CI / Generate TypeScript types (push) Successful in 1m24s
CI / Build server (push) Successful in 26s
CI / Build client (push) Successful in 41s
CI / Server unit tests (push) Successful in 3m25s
CI / Playwright E2E tests (push) Has been cancelled
CI / Build and push Docker image (push) Has been cancelled
CI / Notify (push) Has been cancelled

This commit is contained in:
2026-05-10 08:24:01 +02:00
parent 03f4e438a3
commit 3ba5fdd086
8 changed files with 88 additions and 28 deletions
+31 -17
View File
@@ -18,6 +18,7 @@ import { useNavigate } from 'react-router-dom';
import Loader from './components/Loader';
import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils';
import NoteModal from './components/modals/NoteModal';
import ConfirmModal from './components/modals/ConfirmModal';
import PayForAllModal from './components/modals/PayForAllModal';
import { useEasterEgg } from './context/eggs';
import { ClientData, Food, MealSlot, PendingQr, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr, generateQr } from '../../types';
@@ -77,6 +78,7 @@ function App() {
const [dayIndex, setDayIndex] = useState<number>();
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
const [dismissQrId, setDismissQrId] = useState<string | null>(null);
const [payForAllLocationKey, setPayForAllLocationKey] = useState<LunchChoice | null>(null);
const [eggImage, setEggImage] = useState<Blob>();
const eggRef = useRef<HTMLImageElement>(null);
@@ -698,15 +700,15 @@ function App() {
{locationPickCount >= 2 && auth.login && loginObject[auth.login] !== undefined
&& locationKey !== LunchChoice.PIZZA && locationKey !== LunchChoice.NEOBEDVAM && locationKey !== LunchChoice.ROZHODUJI
&& settings?.bankAccount && settings?.holderName && (
<span title='Zaplatit za všechny a vygenerovat QR kódy ostatním' className="ms-2">
<FontAwesomeIcon
icon={faMoneyBillTransfer}
onClick={() => setPayForAllLocationKey(locationKey)}
className='action-icon'
style={{ cursor: 'pointer' }}
/>
</span>
)}
<span title='Zaplatit za všechny a vygenerovat QR kódy ostatním' className="ms-2">
<FontAwesomeIcon
icon={faMoneyBillTransfer}
onClick={() => setPayForAllLocationKey(locationKey)}
className='action-icon'
style={{ cursor: 'pointer' }}
/>
</span>
)}
</td>
<td className='p-0'>
<Table className="nested-table">
@@ -909,18 +911,12 @@ function App() {
{data.pendingQrs.map(qr => (
<div key={qr.id} className='qr-code mb-3'>
<p>
<strong>{formatDateString(qr.date)}</strong> {qr.creator} ({qr.totalPrice} )
<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=${auth.login}&id=${qr.id}`} alt='QR kód' />
<div className='mt-2'>
<Button variant="success" onClick={async () => {
await dismissQr({ body: { id: qr.id } });
const response = await getData({ query: { dayIndex } });
if (response.data) {
setData(response.data);
}
}}>
<Button variant="success" onClick={() => setDismissQrId(qr.id)}>
Zaplatil jsem
</Button>
</div>
@@ -936,6 +932,24 @@ function App() {
/> */}
<Footer />
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
<ConfirmModal
isOpen={dismissQrId !== null}
title="Potvrzení platby"
message="Opravdu jste zaplatili? QR kód bude odstraněn."
confirmLabel="Zaplatil jsem"
confirmVariant="success"
onClose={() => setDismissQrId(null)}
onConfirm={async () => {
if (!dismissQrId) return;
const id = dismissQrId;
setDismissQrId(null);
await dismissQr({ body: { id } });
const response = await getData({ query: { dayIndex } });
if (response.data) {
setData(response.data);
}
}}
/>
{payForAllLocationKey && data && (
<PayForAllModal
isOpen
@@ -0,0 +1,26 @@
import { Modal, Button } from "react-bootstrap";
type Props = {
isOpen: boolean;
title: string;
message: string;
confirmLabel?: string;
confirmVariant?: string;
onConfirm: () => void;
onClose: () => void;
};
export default function ConfirmModal({ isOpen, title, message, confirmLabel = "Potvrdit", confirmVariant = "primary", onConfirm, onClose }: Readonly<Props>) {
return (
<Modal show={isOpen} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>{message}</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onClose}>Zrušit</Button>
<Button variant={confirmVariant} onClick={onConfirm}>{confirmLabel}</Button>
</Modal.Footer>
</Modal>
);
}
@@ -5,6 +5,7 @@ import { generateQr, OrderGroup, OrderGroupMember, QrRecipient } from "../../../
type Props = {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
group: OrderGroup;
payerLogin: string;
bankAccount: string;
@@ -18,7 +19,7 @@ type DinerEntry = {
included: boolean;
};
export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, bankAccount, bankAccountHolder, groupId }: Readonly<Props>) {
export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, payerLogin, bankAccount, bankAccountHolder, groupId }: Readonly<Props>) {
const [diners, setDiners] = useState<DinerEntry[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@@ -73,9 +74,10 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
setError(`Celková částka pro ${d.login} musí být kladná`);
return;
}
const note = d.member.note?.trim();
recipients.push({
login: d.login,
purpose: `Objednávka ${group.name}`.substring(0, 60),
purpose: note ? note.replace(/[^\x00-\xff*]/g, '').replace(/\*/g, '').substring(0, 60) : `Objednávka ${group.name}`.substring(0, 60),
amount: total,
});
}
@@ -94,6 +96,7 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
setError((response.error as any).error || 'Nastala chyba při generování QR kódů');
} else {
setSuccess(true);
onSuccess?.();
setTimeout(() => onClose(), 2000);
}
} catch (e: any) {
+5 -5
View File
@@ -209,7 +209,7 @@ export default function OrderGroupsPage() {
)}
<div className="content-wrapper">
<div className="content" style={{ maxWidth: 960 }}>
<div className="content" style={{ maxWidth: 1200 }}>
{/* Vytvoření nové skupiny */}
<div className="choice-section fade-in mb-4">
<h5>Vytvořit skupinu</h5>
@@ -295,7 +295,7 @@ export default function OrderGroupsPage() {
)}
{isCreator && isOrdered && (
<>
{settings?.bankAccount && settings?.holderName && (
{settings?.bankAccount && settings?.holderName && !group.qrGenerated && (
<Button variant="primary" size="sm" onClick={() => setPayModal(group)}>
<FontAwesomeIcon icon={faBasketShopping} className="me-1" />
Generovat QR
@@ -319,10 +319,10 @@ export default function OrderGroupsPage() {
<thead>
<tr>
<th>Člen</th>
<th style={{ width: 140 }}>Částka ()</th>
<th style={{ width: 180 }}>Částka (bez slev)</th>
<th style={{ width: 220 }}>Příplatek</th>
<th>Poznámka</th>
<th style={{ width: 100 }}>Celkem</th>
<th style={{ width: 160 }}>Celkem (s poplatky)</th>
<th style={{ width: 40 }}></th>
</tr>
</thead>
@@ -537,7 +537,6 @@ export default function OrderGroupsPage() {
<small className="text-muted">
Doručení v: <strong>{group.deliveryAt ?? '—'}</strong>
</small>
{isCreator && <small className="text-muted fst-italic">(upravit)</small>}
</div>
)}
</div>
@@ -579,6 +578,7 @@ export default function OrderGroupsPage() {
<PayForGroupModal
isOpen={!!payModal}
onClose={() => setPayModal(null)}
onSuccess={fetchData}
group={payModal}
groupId={payModal.id}
payerLogin={auth.login}