fix: počítání částek v haléřích z důvodu přesnosti
CI / Generate TypeScript types (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Server unit tests (push) Successful in 55s
CI / Build client (push) Successful in 33s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 35s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Server unit tests (push) Successful in 55s
CI / Build client (push) Successful in 33s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 35s
CI / Notify (push) Successful in 2s
This commit is contained in:
@@ -48,7 +48,7 @@ export default function PizzaOrderList({ state, orders, onDelete, creator }: Rea
|
||||
borderTop: '2px solid var(--luncher-border)'
|
||||
}}>
|
||||
<td colSpan={4} style={{ padding: '16px 20px', border: 'none' }}>Celkem</td>
|
||||
<td style={{ padding: '16px 20px', border: 'none', textAlign: 'right', color: 'var(--luncher-primary)' }}>{`${total} Kč`}</td>
|
||||
<td style={{ padding: '16px 20px', border: 'none', textAlign: 'right', color: 'var(--luncher-primary)' }}>{`${total / 100} Kč`}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
|
||||
<td>{order.customer}</td>
|
||||
<td>{order.pizzaList!.map<React.ReactNode>(pizzaOrder =>
|
||||
<span key={pizzaOrder.name}>
|
||||
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
|
||||
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price / 100} Kč)`}
|
||||
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
|
||||
<span title='Odstranit'>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
@@ -38,10 +38,10 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
|
||||
.reduce((prev, curr, index) => [prev, <br key={`br-${index}`} />, curr])}
|
||||
</td>
|
||||
<td style={{ maxWidth: "200px" }}>{order.note ?? '-'}</td>
|
||||
<td style={{ maxWidth: "200px" }}>{order.fee?.price ? `${order.fee.price} Kč${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
|
||||
<td style={{ maxWidth: "200px" }}>{order.fee?.price ? `${order.fee.price / 100} Kč${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
|
||||
<td>
|
||||
{order.totalPrice} Kč{auth?.login === creator && state === PizzaDayState.CREATED && <span title='Nastavit příplatek'><FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} className='action-icon' icon={faMoneyBill1} /></span>}
|
||||
{order.totalPrice / 100} Kč{auth?.login === creator && state === PizzaDayState.CREATED && <span title='Nastavit příplatek'><FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} className='action-icon' icon={faMoneyBill1} /></span>}
|
||||
</td>
|
||||
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setIsFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price?.toString() }} />
|
||||
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setIsFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price != null ? String(order.fee.price / 100) : undefined }} />
|
||||
</>
|
||||
}
|
||||
@@ -9,18 +9,23 @@ type Props = {
|
||||
onSaved: (data: any) => void;
|
||||
};
|
||||
|
||||
function parseNum(s: string): number {
|
||||
function parseHal(s: string): number {
|
||||
const n = parseFloat(s.replace(',', '.'));
|
||||
return isNaN(n) || n < 0 ? 0 : Math.round(n * 100) / 100;
|
||||
return isNaN(n) || n < 0 ? 0 : Math.round(n * 100);
|
||||
}
|
||||
|
||||
function parsePercent(s: string): number {
|
||||
const n = parseFloat(s.replace(',', '.'));
|
||||
return isNaN(n) || n < 0 ? 0 : Math.round(n);
|
||||
}
|
||||
|
||||
function computeMemberTotal(member: OrderGroupMember, feeShare: number, discountType: string, discountValue: number, memberCount: number): number {
|
||||
const base = member.amount ?? 0;
|
||||
const surcharge = member.surchargeAmount ?? 0;
|
||||
const discount = discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountValue / 100 * 100) / 100
|
||||
: Math.round(discountValue / memberCount * 100) / 100;
|
||||
return Math.round((base + surcharge + feeShare - discount) * 100) / 100;
|
||||
? Math.round((base + surcharge) * discountValue / 100)
|
||||
: Math.round(discountValue / memberCount);
|
||||
return base + surcharge + feeShare - discount;
|
||||
}
|
||||
|
||||
export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }: Readonly<Props>) {
|
||||
@@ -34,23 +39,25 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setFees(group.fees ? String(group.fees) : '');
|
||||
setShipping(group.shipping ? String(group.shipping) : '');
|
||||
setTip(group.tip ? String(group.tip) : '');
|
||||
setFees(group.fees ? String(group.fees / 100) : '');
|
||||
setShipping(group.shipping ? String(group.shipping / 100) : '');
|
||||
setTip(group.tip ? String(group.tip / 100) : '');
|
||||
setDiscountType((group.discountType as 'percent' | 'fixed') ?? 'percent');
|
||||
setDiscountValue(group.discountValue ? String(group.discountValue) : '');
|
||||
setDiscountValue(group.discountValue
|
||||
? ((group.discountType as string) === 'fixed' ? String(group.discountValue / 100) : String(group.discountValue))
|
||||
: '');
|
||||
setError(null);
|
||||
}, [isOpen, group]);
|
||||
|
||||
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
|
||||
const memberCount = memberEntries.length;
|
||||
|
||||
const feesNum = parseNum(fees);
|
||||
const shippingNum = parseNum(shipping);
|
||||
const tipNum = parseNum(tip);
|
||||
const discountNum = parseNum(discountValue);
|
||||
const feesNum = parseHal(fees);
|
||||
const shippingNum = parseHal(shipping);
|
||||
const tipNum = parseHal(tip);
|
||||
const discountNum = discountType === 'percent' ? parsePercent(discountValue) : parseHal(discountValue);
|
||||
const totalFees = feesNum + shippingNum + tipNum;
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount * 100) / 100 : 0;
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
|
||||
|
||||
const handleSave = async () => {
|
||||
setError(null);
|
||||
@@ -64,7 +71,7 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
|
||||
tip: tipNum,
|
||||
discountType: discountNum > 0 ? discountType : undefined,
|
||||
discountValue: discountNum > 0 ? discountNum : undefined,
|
||||
},
|
||||
}
|
||||
});
|
||||
if (res.error) {
|
||||
setError((res.error as any).error || 'Nastala chyba');
|
||||
@@ -143,7 +150,7 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<h6>Náhled celkových částek ({memberCount} členů, {feeShare > 0 ? `poplatek ${feeShare} Kč/os.` : 'bez poplatku'})</h6>
|
||||
<h6>Náhled celkových částek ({memberCount} členů, {feeShare > 0 ? `poplatek ${feeShare / 100} Kč/os.` : 'bez poplatku'})</h6>
|
||||
<Table size="sm" bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -161,18 +168,18 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
|
||||
const surcharge = member.surchargeAmount ?? 0;
|
||||
const discount = discountNum > 0
|
||||
? (discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountNum / 100 * 100) / 100
|
||||
: Math.round(discountNum / memberCount * 100) / 100)
|
||||
? Math.round((base + surcharge) * discountNum / 100)
|
||||
: Math.round(discountNum / memberCount))
|
||||
: 0;
|
||||
const total = computeMemberTotal(member, feeShare, discountType, discountNum, memberCount);
|
||||
return (
|
||||
<tr key={login}>
|
||||
<td><strong>{login}</strong></td>
|
||||
<td className="text-end">{base > 0 ? `${base} Kč` : '—'}</td>
|
||||
<td className="text-end">{surcharge > 0 ? `${surcharge} Kč` : '—'}</td>
|
||||
<td className="text-end">{feeShare > 0 ? `${feeShare} Kč` : '—'}</td>
|
||||
<td className="text-end text-danger">{discount > 0 ? `-${discount} Kč` : '—'}</td>
|
||||
<td className="text-end fw-bold">{total > 0 ? `${total} Kč` : '—'}</td>
|
||||
<td className="text-end">{base > 0 ? `${base / 100} Kč` : '—'}</td>
|
||||
<td className="text-end">{surcharge > 0 ? `${surcharge / 100} Kč` : '—'}</td>
|
||||
<td className="text-end">{feeShare > 0 ? `${feeShare / 100} Kč` : '—'}</td>
|
||||
<td className="text-end text-danger">{discount > 0 ? `-${discount / 100} Kč` : '—'}</td>
|
||||
<td className="text-end fw-bold">{total > 0 ? `${total / 100} Kč` : '—'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -33,9 +33,7 @@ function parseAmount(s: string): number | null {
|
||||
if (!s || s.trim().length === 0) return null;
|
||||
const n = parseFloat(s);
|
||||
if (isNaN(n) || n < 0) return null;
|
||||
const parts = s.split('.');
|
||||
if (parts.length === 2 && parts[1].length > 2) return null;
|
||||
return Math.round(n * 100) / 100;
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
export default function PayForAllModal({ isOpen, onClose, locationName, locationChoices, menu, payerLogin, bankAccount, bankAccountHolder }: Readonly<Props>) {
|
||||
@@ -55,11 +53,11 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
let baseAmountParseFailed = false;
|
||||
if (menu) {
|
||||
for (const idx of selectedFoods) {
|
||||
const price = parsePriceCzk(menu.food?.[idx]?.price);
|
||||
if (price === null) {
|
||||
const priceKc = parsePriceCzk(menu.food?.[idx]?.price);
|
||||
if (priceKc === null) {
|
||||
baseAmountParseFailed = true;
|
||||
} else {
|
||||
baseAmount += price;
|
||||
baseAmount += Math.round(priceKc * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,19 +82,19 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
if (includedDiners.length === 0) return 0;
|
||||
const tip = parseAmount(tipTotal);
|
||||
if (tip === null || tip === 0) return 0;
|
||||
const totalPeople = includedDiners.length + 1; // +1 for payer
|
||||
return Math.round((tip / totalPeople) * 100) / 100;
|
||||
const totalPeople = includedDiners.length + 1;
|
||||
return Math.round(tip / totalPeople);
|
||||
})();
|
||||
const payerTipShare = (() => {
|
||||
const tip = parseAmount(tipTotal);
|
||||
if (!tip) return 0;
|
||||
return Math.round((tip - tipPerPerson * includedDiners.length) * 100) / 100;
|
||||
return tip - tipPerPerson * includedDiners.length;
|
||||
})();
|
||||
|
||||
const getTotal = (d: DinerEntry): number => {
|
||||
const surcharge = parseAmount(d.surchargeAmount) ?? 0;
|
||||
const tip = d.login === payerLogin ? payerTipShare : tipPerPerson;
|
||||
return Math.round((d.baseAmount + surcharge + tip) * 100) / 100;
|
||||
return d.baseAmount + surcharge + tip;
|
||||
};
|
||||
|
||||
const handleInclude = useCallback((login: string, checked: boolean) => {
|
||||
@@ -122,11 +120,6 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
setError(`Celková částka pro ${d.login} musí být kladná`);
|
||||
return;
|
||||
}
|
||||
const amountStr = total.toString();
|
||||
if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) {
|
||||
setError(`Částka pro ${d.login} má více než 2 desetinná místa`);
|
||||
return;
|
||||
}
|
||||
const foods = d.selectedFoods.map(i => menu?.food?.[i]?.name).filter(Boolean).join(', ');
|
||||
const purposeBase = `Oběd ${locationName}${foods ? ` — ${foods}` : ''}`;
|
||||
recipients.push({
|
||||
@@ -226,7 +219,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} Kč)</span>}
|
||||
{hasMenu && d.baseAmount > 0 && <span className="text-muted"> ({d.baseAmount / 100} Kč)</span>}
|
||||
{d.baseAmountParseFailed && <span className="text-warning"> ⚠</span>}
|
||||
</small>
|
||||
</td>
|
||||
@@ -254,10 +247,10 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-end">
|
||||
{(() => { const s = isPayer ? payerTipShare : tipPerPerson; return s > 0 ? `${s} Kč` : '—'; })()}
|
||||
{(() => { const s = isPayer ? payerTipShare : tipPerPerson; return s > 0 ? `${s / 100} Kč` : '—'; })()}
|
||||
</td>
|
||||
<td className="text-end fw-bold">
|
||||
{`${total} Kč`}
|
||||
{`${total / 100} Kč`}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -278,7 +271,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{includedDiners.length > 0 && tipPerPerson > 0
|
||||
? `(${tipPerPerson} Kč / osoba)`
|
||||
? `(${tipPerPerson / 100} Kč / osoba)`
|
||||
: ''}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
|
||||
const shipping = group.shipping ?? 0;
|
||||
const tip = group.tip ?? 0;
|
||||
const totalFees = fees + shipping + tip;
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount * 100) / 100 : 0;
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
|
||||
|
||||
const getMemberTotal = (entry: DinerEntry): number => {
|
||||
const base = entry.member.amount ?? 0;
|
||||
@@ -50,10 +50,10 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
|
||||
const discountValue = group.discountValue ?? 0;
|
||||
const discount = discountValue > 0
|
||||
? (discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountValue / 100 * 100) / 100
|
||||
: Math.round(discountValue / memberCount * 100) / 100)
|
||||
? Math.round((base + surcharge) * discountValue / 100)
|
||||
: Math.round(discountValue / memberCount))
|
||||
: 0;
|
||||
return Math.round((base + surcharge + feeShare - discount) * 100) / 100;
|
||||
return base + surcharge + feeShare - discount;
|
||||
};
|
||||
|
||||
const includedNonPayers = diners.filter(d => d.included && d.login !== payerLogin);
|
||||
@@ -73,11 +73,6 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
|
||||
setError(`Celková částka pro ${d.login} musí být kladná`);
|
||||
return;
|
||||
}
|
||||
const amountStr = total.toString();
|
||||
if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) {
|
||||
setError(`Částka pro ${d.login} má více než 2 desetinná místa`);
|
||||
return;
|
||||
}
|
||||
recipients.push({
|
||||
login: d.login,
|
||||
purpose: `Objednávka ${group.name}`.substring(0, 60),
|
||||
@@ -132,15 +127,15 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
|
||||
|
||||
{hasFees && (
|
||||
<div className="d-flex gap-3 mb-2 text-muted" style={{ fontSize: '0.9em' }}>
|
||||
{fees > 0 && <span>Poplatky: <strong>{fees} Kč</strong></span>}
|
||||
{shipping > 0 && <span>Doprava: <strong>{shipping} Kč</strong></span>}
|
||||
{tip > 0 && <span>Spropitné: <strong>{tip} Kč</strong></span>}
|
||||
<span>→ {feeShare} Kč/os.</span>
|
||||
{fees > 0 && <span>Poplatky: <strong>{fees / 100} Kč</strong></span>}
|
||||
{shipping > 0 && <span>Doprava: <strong>{shipping / 100} Kč</strong></span>}
|
||||
{tip > 0 && <span>Spropitné: <strong>{tip / 100} Kč</strong></span>}
|
||||
<span>→ {feeShare / 100} Kč/os.</span>
|
||||
</div>
|
||||
)}
|
||||
{group.discountValue != null && group.discountValue > 0 && (
|
||||
<div className="mb-2 text-success" style={{ fontSize: '0.9em' }}>
|
||||
Sleva: {group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue} Kč`}
|
||||
Sleva: {group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue / 100} Kč`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -180,18 +175,18 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">
|
||||
{(d.member.amount ?? 0) > 0 ? `${d.member.amount} Kč` : <span className="text-muted">—</span>}
|
||||
{(d.member.amount ?? 0) > 0 ? `${d.member.amount! / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</td>
|
||||
<td className="text-end">
|
||||
{surcharge > 0 ? `${surcharge} Kč` : <span className="text-muted">—</span>}
|
||||
{surcharge > 0 ? `${surcharge / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</td>
|
||||
{hasFees && (
|
||||
<td className="text-end">
|
||||
{feeShare > 0 ? `${feeShare} Kč` : '—'}
|
||||
{feeShare > 0 ? `${feeShare / 100} Kč` : '—'}
|
||||
</td>
|
||||
)}
|
||||
<td className="text-end fw-bold">
|
||||
{total > 0 ? `${total} Kč` : <span className="text-muted">—</span>}
|
||||
{total > 0 ? `${total / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -15,12 +15,12 @@ export default function PizzaAdditionalFeeModal({ customerName, isOpen, onClose,
|
||||
const priceRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const doSubmit = () => {
|
||||
onSave(customerName, textRef.current?.value, Number.parseInt(priceRef.current?.value ?? "0"));
|
||||
onSave(customerName, textRef.current?.value, Math.round(Number.parseFloat(priceRef.current?.value ?? "0") * 100));
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSave(customerName, textRef.current?.value, Number.parseInt(priceRef.current?.value ?? "0"));
|
||||
onSave(customerName, textRef.current?.value, Math.round(Number.parseFloat(priceRef.current?.value ?? "0") * 100));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function OrderGroupsPage() {
|
||||
setPageError('Zadejte platnou kladnou částku');
|
||||
return;
|
||||
}
|
||||
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, amount: n } }));
|
||||
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; });
|
||||
};
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function OrderGroupsPage() {
|
||||
setPageError('Zadejte platnou výši příplatku');
|
||||
return;
|
||||
}
|
||||
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, surchargeText, surchargeAmount: rawAmount === '' ? 0 : surchargeAmount } }));
|
||||
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; });
|
||||
};
|
||||
|
||||
@@ -254,17 +254,17 @@ export default function OrderGroupsPage() {
|
||||
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 * 100) / 100 : 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 * 100) / 100
|
||||
: Math.round(dv / memberCount * 100) / 100)
|
||||
? Math.round((base + surcharge) * dv / 100)
|
||||
: Math.round(dv / memberCount))
|
||||
: 0;
|
||||
return Math.round((base + surcharge + feeShare - discount) * 100) / 100;
|
||||
return base + surcharge + feeShare - discount;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -370,10 +370,10 @@ export default function OrderGroupsPage() {
|
||||
) : (
|
||||
<span
|
||||
style={{ cursor: canEdit ? 'pointer' : undefined }}
|
||||
onClick={() => canEdit && setEditAmounts(prev => ({ ...prev, [key]: String(member.amount ?? '') }))}
|
||||
onClick={() => canEdit && setEditAmounts(prev => ({ ...prev, [key]: member.amount != null ? String(member.amount / 100) : '' }))}
|
||||
title={canEdit ? 'Klikněte pro úpravu' : undefined}
|
||||
>
|
||||
{member.amount != null ? `${member.amount} Kč` : <span className="text-muted">—</span>}
|
||||
{member.amount != null ? `${member.amount / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -404,11 +404,11 @@ export default function OrderGroupsPage() {
|
||||
) : (
|
||||
<span
|
||||
style={{ cursor: canEdit ? 'pointer' : undefined }}
|
||||
onClick={() => canEdit && setEditSurcharges(prev => ({ ...prev, [key]: { text: member.surchargeText ?? '', amount: member.surchargeAmount != null ? String(member.surchargeAmount) : '' } }))}
|
||||
onClick={() => 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 ? (
|
||||
<small>{member.surchargeText ? `${member.surchargeText}: ` : ''}<strong>{member.surchargeAmount} Kč</strong></small>
|
||||
<small>{member.surchargeText ? `${member.surchargeText}: ` : ''}<strong>{member.surchargeAmount / 100} Kč</strong></small>
|
||||
) : (
|
||||
<small className="text-muted">—</small>
|
||||
)}
|
||||
@@ -440,7 +440,7 @@ export default function OrderGroupsPage() {
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<small className={memberTotal > 0 ? 'fw-bold' : 'text-muted'}>
|
||||
{memberTotal > 0 ? `${memberTotal} Kč` : '—'}
|
||||
{memberTotal > 0 ? `${memberTotal / 100} Kč` : '—'}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
@@ -464,13 +464,13 @@ export default function OrderGroupsPage() {
|
||||
{/* Souhrn poplatků a slevy */}
|
||||
{(totalFees > 0 || (group.discountValue != null && group.discountValue > 0)) && (
|
||||
<div className="px-3 py-2 border-top d-flex gap-3 flex-wrap" style={{ fontSize: '0.85em', color: 'var(--luncher-text-muted)' }}>
|
||||
{group.fees != null && group.fees > 0 && <span>Poplatky: <strong>{group.fees} Kč</strong></span>}
|
||||
{group.shipping != null && group.shipping > 0 && <span>Doprava: <strong>{group.shipping} Kč</strong></span>}
|
||||
{group.tip != null && group.tip > 0 && <span>Spropitné: <strong>{group.tip} Kč</strong></span>}
|
||||
{feeShare > 0 && <span>→ <strong>{feeShare} Kč</strong>/os.</span>}
|
||||
{group.fees != null && group.fees > 0 && <span>Poplatky: <strong>{group.fees / 100} Kč</strong></span>}
|
||||
{group.shipping != null && group.shipping > 0 && <span>Doprava: <strong>{group.shipping / 100} Kč</strong></span>}
|
||||
{group.tip != null && group.tip > 0 && <span>Spropitné: <strong>{group.tip / 100} Kč</strong></span>}
|
||||
{feeShare > 0 && <span>→ <strong>{feeShare / 100} Kč</strong>/os.</span>}
|
||||
{group.discountValue != null && group.discountValue > 0 && (
|
||||
<span className="text-success">
|
||||
Sleva: <strong>{group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue} Kč`}</strong>
|
||||
Sleva: <strong>{group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue / 100} Kč`}</strong>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,16 +28,16 @@ const buildPizzaUrl = (pizzaUrl: string) => {
|
||||
return `${baseUrl}/${pizzaUrl}`;
|
||||
}
|
||||
|
||||
// Ceny krabic dle velikosti
|
||||
// Ceny krabic dle velikosti v haléřích
|
||||
const boxPrices: { [key: string]: number } = {
|
||||
"30cm": 13,
|
||||
"35cm": 15,
|
||||
"40cm": 18,
|
||||
"50cm": 25
|
||||
"30cm": 1300,
|
||||
"35cm": 1500,
|
||||
"40cm": 1800,
|
||||
"50cm": 2500
|
||||
}
|
||||
|
||||
// Cena obalu pro salát
|
||||
const SALAT_BOX_PRICE = 13;
|
||||
// Cena obalu pro salát v haléřích
|
||||
const SALAT_BOX_PRICE = 1300;
|
||||
|
||||
/**
|
||||
* Stáhne a scrapne aktuální pizzy ze stránek Pizza Chefie.
|
||||
@@ -79,7 +79,7 @@ export async function downloadPizzy(mock: boolean): Promise<Pizza[]> {
|
||||
a.each((i, elm) => {
|
||||
const varId = Number.parseInt(elm.attribs.href.split('?varianta=')[1].trim());
|
||||
const size = $($(elm).contents().get(0)).text().trim();
|
||||
const price = Number.parseInt($($(elm).contents().get(1)).text().trim().split(" Kč")[0]);
|
||||
const price = Number.parseInt($($(elm).contents().get(1)).text().trim().split(" Kč")[0]) * 100;
|
||||
sizes.push({ varId, size, pizzaPrice: price, boxPrice: boxPrices[size], price: price + boxPrices[size] });
|
||||
})
|
||||
result.push({
|
||||
@@ -119,7 +119,7 @@ export async function downloadSalaty(mock: boolean): Promise<Salat[]> {
|
||||
ingredients.push($(elm).text());
|
||||
});
|
||||
const priceText = $('.cena > span', salatHtml).first().text().trim();
|
||||
const price = Number.parseInt(priceText.split(' Kč')[0]);
|
||||
const price = Number.parseInt(priceText.split(' Kč')[0]) * 100;
|
||||
result.push({ name, ingredients, price: price + SALAT_BOX_PRICE });
|
||||
}
|
||||
return result;
|
||||
|
||||
+220
-220
@@ -661,30 +661,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 1,
|
||||
size: "30cm",
|
||||
pizzaPrice: 138,
|
||||
boxPrice: 13,
|
||||
price: 151
|
||||
pizzaPrice: 13800,
|
||||
boxPrice: 1300,
|
||||
price: 15100
|
||||
},
|
||||
{
|
||||
varId: 2,
|
||||
size: "35cm",
|
||||
pizzaPrice: 166,
|
||||
boxPrice: 15,
|
||||
price: 181
|
||||
pizzaPrice: 16600,
|
||||
boxPrice: 1500,
|
||||
price: 18100
|
||||
},
|
||||
{
|
||||
varId: 3,
|
||||
size: "40cm",
|
||||
pizzaPrice: 223,
|
||||
boxPrice: 18,
|
||||
price: 241
|
||||
pizzaPrice: 22300,
|
||||
boxPrice: 1800,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 4,
|
||||
size: "50cm",
|
||||
pizzaPrice: 306,
|
||||
boxPrice: 25,
|
||||
price: 331
|
||||
pizzaPrice: 30600,
|
||||
boxPrice: 2500,
|
||||
price: 33100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -700,30 +700,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 6,
|
||||
size: "30cm",
|
||||
pizzaPrice: 142,
|
||||
boxPrice: 13,
|
||||
price: 155
|
||||
pizzaPrice: 14200,
|
||||
boxPrice: 1300,
|
||||
price: 15500
|
||||
},
|
||||
{
|
||||
varId: 7,
|
||||
size: "35cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 15,
|
||||
price: 187
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1500,
|
||||
price: 18700
|
||||
},
|
||||
{
|
||||
varId: 8,
|
||||
size: "40cm",
|
||||
pizzaPrice: 233,
|
||||
boxPrice: 18,
|
||||
price: 251
|
||||
pizzaPrice: 23300,
|
||||
boxPrice: 1800,
|
||||
price: 25100
|
||||
},
|
||||
{
|
||||
varId: 9,
|
||||
size: "50cm",
|
||||
pizzaPrice: 316,
|
||||
boxPrice: 25,
|
||||
price: 341
|
||||
pizzaPrice: 31600,
|
||||
boxPrice: 2500,
|
||||
price: 34100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -741,30 +741,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 10,
|
||||
size: "30cm",
|
||||
pizzaPrice: 142,
|
||||
boxPrice: 13,
|
||||
price: 155
|
||||
pizzaPrice: 14200,
|
||||
boxPrice: 1300,
|
||||
price: 15500
|
||||
},
|
||||
{
|
||||
varId: 11,
|
||||
size: "35cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 15,
|
||||
price: 187
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1500,
|
||||
price: 18700
|
||||
},
|
||||
{
|
||||
varId: 12,
|
||||
size: "40cm",
|
||||
pizzaPrice: 233,
|
||||
boxPrice: 18,
|
||||
price: 251
|
||||
pizzaPrice: 23300,
|
||||
boxPrice: 1800,
|
||||
price: 25100
|
||||
},
|
||||
{
|
||||
varId: 13,
|
||||
size: "50cm",
|
||||
pizzaPrice: 316,
|
||||
boxPrice: 25,
|
||||
price: 341
|
||||
pizzaPrice: 31600,
|
||||
boxPrice: 2500,
|
||||
price: 34100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -780,30 +780,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 14,
|
||||
size: "30cm",
|
||||
pizzaPrice: 142,
|
||||
boxPrice: 13,
|
||||
price: 155
|
||||
pizzaPrice: 14200,
|
||||
boxPrice: 1300,
|
||||
price: 15500
|
||||
},
|
||||
{
|
||||
varId: 15,
|
||||
size: "35cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 15,
|
||||
price: 187
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1500,
|
||||
price: 18700
|
||||
},
|
||||
{
|
||||
varId: 16,
|
||||
size: "40cm",
|
||||
pizzaPrice: 233,
|
||||
boxPrice: 18,
|
||||
price: 251
|
||||
pizzaPrice: 23300,
|
||||
boxPrice: 1800,
|
||||
price: 25100
|
||||
},
|
||||
{
|
||||
varId: 17,
|
||||
size: "50cm",
|
||||
pizzaPrice: 294,
|
||||
boxPrice: 25,
|
||||
price: 319
|
||||
pizzaPrice: 29400,
|
||||
boxPrice: 2500,
|
||||
price: 31900
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -821,30 +821,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 22,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 23,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 24,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 25,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -861,30 +861,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 26,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 27,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 28,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 29,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -902,30 +902,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 30,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 31,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 32,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 33,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -946,30 +946,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 34,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 35,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 36,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 37,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -987,30 +987,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 38,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 39,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 40,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 41,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1028,30 +1028,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 42,
|
||||
size: "30cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 13,
|
||||
price: 185
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1300,
|
||||
price: 18500
|
||||
},
|
||||
{
|
||||
varId: 43,
|
||||
size: "35cm",
|
||||
pizzaPrice: 212,
|
||||
boxPrice: 15,
|
||||
price: 227
|
||||
pizzaPrice: 21200,
|
||||
boxPrice: 1500,
|
||||
price: 22700
|
||||
},
|
||||
{
|
||||
varId: 44,
|
||||
size: "40cm",
|
||||
pizzaPrice: 293,
|
||||
boxPrice: 18,
|
||||
price: 311
|
||||
pizzaPrice: 29300,
|
||||
boxPrice: 1800,
|
||||
price: 31100
|
||||
},
|
||||
{
|
||||
varId: 45,
|
||||
size: "50cm",
|
||||
pizzaPrice: 376,
|
||||
boxPrice: 25,
|
||||
price: 401
|
||||
pizzaPrice: 37600,
|
||||
boxPrice: 2500,
|
||||
price: 40100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1069,30 +1069,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 46,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 47,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 48,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 49,
|
||||
size: "50cm",
|
||||
pizzaPrice: 386,
|
||||
boxPrice: 25,
|
||||
price: 411
|
||||
pizzaPrice: 38600,
|
||||
boxPrice: 2500,
|
||||
price: 41100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1114,30 +1114,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 50,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 51,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 52,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 53,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1156,30 +1156,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 54,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 55,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 56,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 57,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1199,30 +1199,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 58,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 59,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 60,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 61,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1241,30 +1241,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 62,
|
||||
size: "30cm",
|
||||
pizzaPrice: 188,
|
||||
boxPrice: 13,
|
||||
price: 201
|
||||
pizzaPrice: 18800,
|
||||
boxPrice: 1300,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 63,
|
||||
size: "35cm",
|
||||
pizzaPrice: 226,
|
||||
boxPrice: 15,
|
||||
price: 241
|
||||
pizzaPrice: 22600,
|
||||
boxPrice: 1500,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 64,
|
||||
size: "40cm",
|
||||
pizzaPrice: 313,
|
||||
boxPrice: 18,
|
||||
price: 331
|
||||
pizzaPrice: 31300,
|
||||
boxPrice: 1800,
|
||||
price: 33100
|
||||
},
|
||||
{
|
||||
varId: 65,
|
||||
size: "50cm",
|
||||
pizzaPrice: 426,
|
||||
boxPrice: 25,
|
||||
price: 451
|
||||
pizzaPrice: 42600,
|
||||
boxPrice: 2500,
|
||||
price: 45100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1283,30 +1283,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 66,
|
||||
size: "30cm",
|
||||
pizzaPrice: 188,
|
||||
boxPrice: 13,
|
||||
price: 201
|
||||
pizzaPrice: 18800,
|
||||
boxPrice: 1300,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 67,
|
||||
size: "35cm",
|
||||
pizzaPrice: 226,
|
||||
boxPrice: 15,
|
||||
price: 241
|
||||
pizzaPrice: 22600,
|
||||
boxPrice: 1500,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 68,
|
||||
size: "40cm",
|
||||
pizzaPrice: 313,
|
||||
boxPrice: 18,
|
||||
price: 331
|
||||
pizzaPrice: 31300,
|
||||
boxPrice: 1800,
|
||||
price: 33100
|
||||
},
|
||||
{
|
||||
varId: 69,
|
||||
size: "50cm",
|
||||
pizzaPrice: 426,
|
||||
boxPrice: 25,
|
||||
price: 451
|
||||
pizzaPrice: 42600,
|
||||
boxPrice: 2500,
|
||||
price: 45100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1327,30 +1327,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 309,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 310,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 311,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 312,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1369,30 +1369,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 394,
|
||||
size: "30cm",
|
||||
pizzaPrice: 188,
|
||||
boxPrice: 13,
|
||||
price: 201
|
||||
pizzaPrice: 18800,
|
||||
boxPrice: 1300,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 395,
|
||||
size: "35cm",
|
||||
pizzaPrice: 226,
|
||||
boxPrice: 15,
|
||||
price: 241
|
||||
pizzaPrice: 22600,
|
||||
boxPrice: 1500,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 396,
|
||||
size: "40cm",
|
||||
pizzaPrice: 313,
|
||||
boxPrice: 18,
|
||||
price: 331
|
||||
pizzaPrice: 31300,
|
||||
boxPrice: 1800,
|
||||
price: 33100
|
||||
},
|
||||
{
|
||||
varId: 397,
|
||||
size: "50cm",
|
||||
pizzaPrice: 426,
|
||||
boxPrice: 25,
|
||||
price: 451
|
||||
pizzaPrice: 42600,
|
||||
boxPrice: 2500,
|
||||
price: 45100
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1434,22 +1434,22 @@ const MOCK_SALAT_LIST = [
|
||||
{
|
||||
name: "Greek",
|
||||
ingredients: ["Salát", "Černé olivy", "Paprika mix", "Červená cibule", "Rajčata", "Okurka salátová", "Jogurtový dresing"],
|
||||
price: 174 + 13,
|
||||
price: (174 + 13) * 100,
|
||||
},
|
||||
{
|
||||
name: "Caesar",
|
||||
ingredients: ["Salát", "Rajčata", "Kuřecí maso", "Krutony", "Parmazán", "Caesar dresing", "Olivový olej"],
|
||||
price: 184 + 13,
|
||||
price: (184 + 13) * 100,
|
||||
},
|
||||
{
|
||||
name: "Šopský salát",
|
||||
ingredients: ["Salátová okurka", "Rajčata", "Paprika mix", "Červená cibule", "Balkánský sýr"],
|
||||
price: 164 + 13,
|
||||
price: (164 + 13) * 100,
|
||||
},
|
||||
{
|
||||
name: "Těstovinový salát",
|
||||
ingredients: ["Penne", "Okurka", "Rajčata", "Paprika mix", "Kuřecí maso", "Jogurtový dresing"],
|
||||
price: 184 + 13,
|
||||
price: (184 + 13) * 100,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
+1
-1
@@ -342,7 +342,7 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
|
||||
let message = order.pizzaList!.map(item =>
|
||||
item.category === 'salat' ? `Salát ${item.name}` : `Pizza ${item.name} (${item.size})`
|
||||
).join(', ');
|
||||
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message, id);
|
||||
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice / 100, message, id);
|
||||
order.hasQr = true;
|
||||
// Uložíme nevyřízený QR kód pro persistentní zobrazení
|
||||
await addPendingQr(order.customer, {
|
||||
|
||||
@@ -69,7 +69,7 @@ router.post("/updateMember", async (req: Request, res, next) => {
|
||||
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
|
||||
const patch: Record<string, any> = {};
|
||||
if (amount !== undefined) {
|
||||
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0) {
|
||||
if (!Number.isInteger(amount) || amount < 0) {
|
||||
return res.status(400).json({ error: 'Neplatná částka' });
|
||||
}
|
||||
patch.amount = amount;
|
||||
@@ -83,7 +83,7 @@ router.post("/updateMember", async (req: Request, res, next) => {
|
||||
patch.surchargeText = surchargeText;
|
||||
}
|
||||
if (surchargeAmount !== undefined) {
|
||||
if (typeof surchargeAmount !== 'number' || !Number.isFinite(surchargeAmount) || surchargeAmount < 0) {
|
||||
if (!Number.isInteger(surchargeAmount) || surchargeAmount < 0) {
|
||||
return res.status(400).json({ error: 'Neplatná výše příplatku' });
|
||||
}
|
||||
patch.surchargeAmount = surchargeAmount;
|
||||
@@ -113,19 +113,19 @@ router.post("/updateFees", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id, fees, shipping, tip, discountType, discountValue } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
if (fees !== undefined && (typeof fees !== 'number' || !Number.isFinite(fees) || fees < 0)) {
|
||||
if (fees !== undefined && (!Number.isInteger(fees) || fees < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše poplatků' });
|
||||
}
|
||||
if (shipping !== undefined && (typeof shipping !== 'number' || !Number.isFinite(shipping) || shipping < 0)) {
|
||||
if (shipping !== undefined && (!Number.isInteger(shipping) || shipping < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše dopravy' });
|
||||
}
|
||||
if (tip !== undefined && (typeof tip !== 'number' || !Number.isFinite(tip) || tip < 0)) {
|
||||
if (tip !== undefined && (!Number.isInteger(tip) || tip < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše spropitného' });
|
||||
}
|
||||
if (discountType !== undefined && discountType !== '' && !['percent', 'fixed'].includes(discountType)) {
|
||||
return res.status(400).json({ error: 'Neplatný typ slevy' });
|
||||
}
|
||||
if (discountValue !== undefined && (typeof discountValue !== 'number' || !Number.isFinite(discountValue) || discountValue < 0)) {
|
||||
if (discountValue !== undefined && (!Number.isInteger(discountValue) || discountValue < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše slevy' });
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -36,18 +36,13 @@ router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, r
|
||||
if (!recipient.purpose || recipient.purpose.trim().length === 0) {
|
||||
return res.status(400).json({ error: `Příjemce ${recipient.login} nemá vyplněný účel platby` });
|
||||
}
|
||||
if (typeof recipient.amount !== 'number' || recipient.amount <= 0) {
|
||||
if (!Number.isInteger(recipient.amount) || recipient.amount <= 0) {
|
||||
return res.status(400).json({ error: `Příjemce ${recipient.login} má neplatnou částku` });
|
||||
}
|
||||
// Validace max 2 desetinná místa
|
||||
const amountStr = recipient.amount.toString();
|
||||
if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) {
|
||||
return res.status(400).json({ error: `Částka pro ${recipient.login} má více než 2 desetinná místa` });
|
||||
}
|
||||
|
||||
// Vygenerovat QR kód
|
||||
const id = crypto.randomUUID();
|
||||
await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose, id);
|
||||
await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount / 100, recipient.purpose, id);
|
||||
|
||||
// Uložit jako nevyřízený QR kód a okamžitě doručit příjemci
|
||||
const pendingQr = {
|
||||
|
||||
@@ -31,8 +31,8 @@ test('saláty mají name a ingredients', async () => {
|
||||
|
||||
test('cena salátu zahrnuje pevný příplatek 13 Kč za obal', async () => {
|
||||
const salaty = await downloadSalaty(false);
|
||||
// Caesar sticker price = 129, box = 13
|
||||
expect(salaty[0].price).toBe(129 + 13);
|
||||
// Řecký sticker price = 119, box = 13
|
||||
expect(salaty[1].price).toBe(119 + 13);
|
||||
// Caesar sticker price = 129, box = 13 → (129 + 13) * 100 haléřů
|
||||
expect(salaty[0].price).toBe((129 + 13) * 100);
|
||||
// Řecký sticker price = 119, box = 13 → (119 + 13) * 100 haléřů
|
||||
expect(salaty[1].price).toBe((119 + 13) * 100);
|
||||
});
|
||||
|
||||
@@ -28,8 +28,8 @@ beforeEach(() => {
|
||||
|
||||
const VALID_BODY = {
|
||||
recipients: [
|
||||
{ login: 'uzivatel1', purpose: 'Pizza Margherita', amount: 149 },
|
||||
{ login: 'uzivatel2', purpose: 'Pizza Diavola', amount: 179 },
|
||||
{ login: 'uzivatel1', purpose: 'Pizza Margherita', amount: 14900 },
|
||||
{ login: 'uzivatel2', purpose: 'Pizza Diavola', amount: 17900 },
|
||||
],
|
||||
bankAccount: '19-2000145399/0800',
|
||||
bankAccountHolder: 'Jan Novák',
|
||||
@@ -76,17 +76,17 @@ test('POST /generate vrátí 400 pro zápornou částku', async () => {
|
||||
expect(res.body.error).toContain('částku');
|
||||
});
|
||||
|
||||
test('POST /generate vrátí 400 pro částku s více než 2 desetinnými místy', async () => {
|
||||
test('POST /generate vrátí 400 pro necelou částku', async () => {
|
||||
const body = {
|
||||
...VALID_BODY,
|
||||
recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: 149.999 }],
|
||||
recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: 149.5 }],
|
||||
};
|
||||
const res = await request(buildApp())
|
||||
.post('/api/qr/generate')
|
||||
.set('Authorization', TOKEN)
|
||||
.send(body);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain('desetinná');
|
||||
expect(res.body.error).toContain('částku');
|
||||
});
|
||||
|
||||
test('POST /generate vrátí 400 pro příjemce bez login', async () => {
|
||||
|
||||
@@ -14,21 +14,21 @@ post:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
fees:
|
||||
description: Poplatky (Kč)
|
||||
type: number
|
||||
description: Poplatky (haléře)
|
||||
type: integer
|
||||
shipping:
|
||||
description: Doprava (Kč)
|
||||
type: number
|
||||
description: Doprava (haléře)
|
||||
type: integer
|
||||
tip:
|
||||
description: Spropitné (Kč)
|
||||
type: number
|
||||
description: Spropitné (haléře)
|
||||
type: integer
|
||||
discountType:
|
||||
description: Typ slevy
|
||||
type: string
|
||||
enum: [percent, fixed]
|
||||
discountValue:
|
||||
description: Hodnota slevy
|
||||
type: number
|
||||
description: Hodnota slevy (procenta nebo haléře pro fixed)
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
|
||||
@@ -18,8 +18,8 @@ post:
|
||||
description: Login člena ke změně
|
||||
type: string
|
||||
amount:
|
||||
description: Částka k úhradě v Kč
|
||||
type: number
|
||||
description: Částka k úhradě v haléřích
|
||||
type: integer
|
||||
note:
|
||||
description: Poznámka
|
||||
type: string
|
||||
@@ -27,8 +27,8 @@ post:
|
||||
description: Popis příplatku
|
||||
type: string
|
||||
surchargeAmount:
|
||||
description: Výše příplatku v Kč
|
||||
type: number
|
||||
description: Výše příplatku v haléřích
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
|
||||
@@ -16,8 +16,8 @@ post:
|
||||
type: string
|
||||
description: Textový popis přirážky/slevy
|
||||
price:
|
||||
type: number
|
||||
description: Částka přirážky/slevy v Kč
|
||||
type: integer
|
||||
description: Částka přirážky/slevy v haléřích
|
||||
responses:
|
||||
"200":
|
||||
description: Nastavení přirážky/slevy proběhlo úspěšně.
|
||||
|
||||
+31
-31
@@ -420,14 +420,14 @@ PizzaSize:
|
||||
description: Velikost pizzy, např. "30cm"
|
||||
type: string
|
||||
pizzaPrice:
|
||||
description: Cena samotné pizzy v Kč
|
||||
type: number
|
||||
description: Cena samotné pizzy v haléřích
|
||||
type: integer
|
||||
boxPrice:
|
||||
description: Cena krabice pizzy v Kč
|
||||
type: number
|
||||
description: Cena krabice pizzy v haléřích
|
||||
type: integer
|
||||
price:
|
||||
description: Celková cena (pizza + krabice)
|
||||
type: number
|
||||
description: Celková cena (pizza + krabice) v haléřích
|
||||
type: integer
|
||||
Pizza:
|
||||
description: Údaje o konkrétní pizze.
|
||||
type: object
|
||||
@@ -470,8 +470,8 @@ PizzaVariant:
|
||||
description: Velikost pizzy (např. "30cm"), nebo "1 porce" pro salát
|
||||
type: string
|
||||
price:
|
||||
description: Cena v Kč, včetně krabice/obalu
|
||||
type: number
|
||||
description: Cena v haléřích, včetně krabice/obalu
|
||||
type: integer
|
||||
category:
|
||||
description: Kategorie položky (pizza nebo salat)
|
||||
type: string
|
||||
@@ -494,8 +494,8 @@ Salat:
|
||||
items:
|
||||
type: string
|
||||
price:
|
||||
description: Cena salátu v Kč (bez obalu)
|
||||
type: number
|
||||
description: Cena salátu v haléřích (bez obalu)
|
||||
type: integer
|
||||
PizzaOrder:
|
||||
description: Údaje o objednávce pizzy jednoho uživatele.
|
||||
type: object
|
||||
@@ -521,11 +521,11 @@ PizzaOrder:
|
||||
description: Popis příplatku (např. "kuřecí maso navíc")
|
||||
type: string
|
||||
price:
|
||||
description: Cena příplatku v Kč
|
||||
type: number
|
||||
description: Cena příplatku v haléřích
|
||||
type: integer
|
||||
totalPrice:
|
||||
description: Celková cena všech objednaných pizz daného uživatele, včetně krabic a příplatků
|
||||
type: number
|
||||
description: Celková cena všech objednaných pizz daného uživatele v haléřích, včetně krabic a příplatků
|
||||
type: integer
|
||||
hasQr:
|
||||
description: |
|
||||
Příznak, pokud je k této objednávce vygenerován QR kód pro platbu. To je typicky pravda, pokud:
|
||||
@@ -635,9 +635,9 @@ QrRecipient:
|
||||
description: Účel platby (např. "Pizza prosciutto")
|
||||
type: string
|
||||
amount:
|
||||
description: Částka v Kč (kladné číslo, max 2 desetinná místa)
|
||||
type: number
|
||||
minimum: 0.01
|
||||
description: Částka v haléřích (kladné celé číslo)
|
||||
type: integer
|
||||
minimum: 1
|
||||
GenerateQrRequest:
|
||||
description: Request pro generování QR kódů
|
||||
type: object
|
||||
@@ -704,8 +704,8 @@ OrderGroupMember:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
amount:
|
||||
description: Částka k úhradě v Kč
|
||||
type: number
|
||||
description: Částka k úhradě v haléřích
|
||||
type: integer
|
||||
note:
|
||||
description: Volitelná poznámka (např. co si objednává)
|
||||
type: string
|
||||
@@ -713,8 +713,8 @@ OrderGroupMember:
|
||||
description: Popis příplatku
|
||||
type: string
|
||||
surchargeAmount:
|
||||
description: Výše příplatku v Kč
|
||||
type: number
|
||||
description: Výše příplatku v haléřích
|
||||
type: integer
|
||||
paid:
|
||||
description: Příznak, zda člen uhradil svůj podíl objednávky
|
||||
type: boolean
|
||||
@@ -753,21 +753,21 @@ OrderGroup:
|
||||
description: Očekávaný čas doručení ve formátu HH:MM
|
||||
type: string
|
||||
fees:
|
||||
description: Poplatky (balení apod.) celkem v Kč
|
||||
type: number
|
||||
description: Poplatky (balení apod.) celkem v haléřích
|
||||
type: integer
|
||||
shipping:
|
||||
description: Doprava v Kč
|
||||
type: number
|
||||
description: Doprava v haléřích
|
||||
type: integer
|
||||
tip:
|
||||
description: Spropitné v Kč
|
||||
type: number
|
||||
description: Spropitné v haléřích
|
||||
type: integer
|
||||
discountType:
|
||||
description: Typ slevy aplikované na objednávku
|
||||
type: string
|
||||
enum: [percent, fixed]
|
||||
discountValue:
|
||||
description: Hodnota slevy (procenta nebo Kč)
|
||||
type: number
|
||||
description: Hodnota slevy (procenta pro typ 'percent', haléře pro typ 'fixed')
|
||||
type: integer
|
||||
|
||||
# --- NEVYŘÍZENÉ QR KÓDY ---
|
||||
PendingQr:
|
||||
@@ -790,8 +790,8 @@ PendingQr:
|
||||
description: Jméno uživatele, který QR vygeneroval (příjemce platby)
|
||||
type: string
|
||||
totalPrice:
|
||||
description: Celková cena objednávky v Kč
|
||||
type: number
|
||||
description: Celková cena objednávky v haléřích
|
||||
type: integer
|
||||
purpose:
|
||||
description: Účel platby (např. "Pizza prosciutto")
|
||||
type: string
|
||||
|
||||
Reference in New Issue
Block a user