feat: zadávání cen
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 28s
CI / Build server (push) Successful in 37s
CI / Build client (push) Successful in 45s
CI / Playwright E2E tests (push) Successful in 2m15s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s

This commit is contained in:
Ondřej Anděl committed 2026-10-21 09:42:04 +02:00
1 parent 96dc3f0053
commit 9a3987a2fa
13 files changed
+244 -11

No files matched your search

@@ -49,9 +49,10 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
if (!isOpen) return;
const entries: DinerEntry[] = Object.entries(locationChoices).map(([login, choice]) => {
const selectedFoods = choice.selectedFoods ?? [];
let baseAmount = 0;
// Cena, kterou si strávník zadal sám, má přednost před cenami z menu
let baseAmount = choice.price ?? 0;
let baseAmountParseFailed = false;
if (menu) {
if (menu && choice.price == null) {
for (const idx of selectedFoods) {
const priceKc = parsePriceCzk(menu.food?.[idx]?.price);
if (priceKc === null) {
@@ -168,7 +169,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
<>
<p>Zaplatili jste za skupinu v restauraci. Nastavte příplatky a společné poplatky, poté vygenerujte QR kódy pro ostatní.</p>
{!hasMenu && (
{!hasMenu && !diners.some(d => d.baseAmount > 0) && (
<Alert variant="info">
Pro tuto skupinu nejsou k dispozici ceny jídel — vyplňte příplatky ručně.
</Alert>
@@ -219,7 +220,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
<td>
<small>
{foodNames || <span className="text-muted">—</span>}
{hasMenu && d.baseAmount > 0 && <span className="text-muted"> ({d.baseAmount / 100} Kč)</span>}
{d.baseAmount > 0 && <span className="text-muted"> ({d.baseAmount / 100} Kč)</span>}
{d.baseAmountParseFailed && <span className="text-warning"> ⚠</span>}
</small>
</td>
@@ -0,0 +1,74 @@
import { useEffect, useState } from "react";
import { Modal, Button, Form } from "react-bootstrap";
type Props = {
isOpen: boolean,
onClose: () => void,
/** Uložená cena v haléřích */
price?: number,
/** Cena v haléřích spočtená z vybraných jídel v menu, nabídne se, pokud cena ještě není uložená */
suggestedPrice?: number,
/** Cena v haléřích, undefined cenu odstraní */
onSave: (price?: number) => void,
}
const toCrownsText = (amount?: number) => amount ? String(amount / 100).replace('.', ',') : '';
/** Modální dialog pro zadání ceny oběda, která se promítne do přehledu stravování. */
const PriceModal = ({ isOpen, onClose, price, suggestedPrice, onSave }: Readonly<Props>) => {
const [value, setValue] = useState('');
useEffect(() => {
if (isOpen) {
setValue(toCrownsText(price ?? suggestedPrice));
}
}, [isOpen, price, suggestedPrice]);
const parsed = value.trim().length ? Number.parseFloat(value.replace(',', '.')) : undefined;
const invalid = parsed != null && (Number.isNaN(parsed) || parsed < 0);
const save = () => {
if (invalid) return;
onSave(parsed == null ? undefined : Math.round(parsed * 100));
}
return <Modal show={isOpen} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>Cena oběda</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className="text-muted small">Cena se promítne do vašeho měsíčního přehledu stravování (export).</p>
<div className="d-flex align-items-center gap-2">
<Form.Control
autoFocus
type="text"
inputMode="decimal"
placeholder="např. 145"
value={value}
isInvalid={invalid}
onChange={e => setValue(e.target.value.replace(/[^0-9.,]/g, ''))}
onKeyDown={event => {
if (event.key === 'Enter') {
save();
}
event.stopPropagation();
}}
/>
<span>Kč</span>
</div>
</Modal.Body>
<Modal.Footer>
{price != null && <Button variant="outline-danger" className="me-auto" onClick={() => onSave(undefined)}>
Odstranit cenu
</Button>}
<Button variant="secondary" onClick={() => onClose()}>
Storno
</Button>
<Button variant="primary" onClick={() => save()} disabled={invalid}>
Uložit
</Button>
</Modal.Footer>
</Modal>
}
export default PriceModal;