Možnost příplatků u Pizza day objednávek

This commit is contained in:
2023-09-24 20:15:04 +02:00
parent c3d35ccc9c
commit eb27591727
11 changed files with 233 additions and 74 deletions

View File

@@ -106,4 +106,8 @@ export const login = async (login?: string) => {
export const changeDepartureTime = async (login: string, time: string, dayIndex?: number) => {
return await api.post<any, any>('/api/changeDepartureTime', JSON.stringify({ login, time, dayIndex }));
}
export const updatePizzaFee = async (login: string, text?: string, price?: number) => {
return await api.post<any, any>('/api/updatePizzaFee', JSON.stringify({ login, text, price }));
}

View File

@@ -84,7 +84,7 @@
margin-bottom: 0;
}
.table> :not(caption) .trash-icon {
.table> :not(caption) .action-icon {
color: rgb(0, 89, 255);
cursor: pointer;
margin-left: 10px;

View File

@@ -355,6 +355,7 @@ function App() {
<li>Oprava generování QR kódů pro Pizza day</li>
<li>Serverová validace času odchodu</li>
<li>Loader při zakládání Pizza day</li>
<li>Možnost ručního zadání příplatku k Pizza day objednávkám</li>
</ul>
</Alert>
{dayIndex != null &&
@@ -429,7 +430,7 @@ function App() {
{userPayload.departureTime && <small> ({userPayload.departureTime})</small>}
{login === auth.login && <FontAwesomeIcon onClick={() => {
doRemoveChoices(locationKey);
}} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='trash-icon' icon={faTrashCan} />}
}} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
</td>
{userChoices?.length && food ? <td className='w-100'>
<ul>
@@ -442,7 +443,7 @@ function App() {
{foodName}
{login === auth.login && <FontAwesomeIcon onClick={() => {
doRemoveFoodChoice(locationKey, foodIndex);
}} title={`Odstranit ${foodName}`} className='trash-icon' icon={faTrashCan} />}
}} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
</li>
})}
</ul>
@@ -565,13 +566,12 @@ function App() {
</Button>
</div>
}
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} />
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} creator={data.pizzaDay.creator} />
{
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
<div className='qr-code'>
<h3>QR platba</h3>
<img src={getQrUrl(auth.login)} alt='QR kód' />
<p>Pozor, QR kód nezohledňuje případné přidané ingredience.</p>
</div>
}
</div>

View File

@@ -1,6 +1,7 @@
import React, { useRef, useState } from "react";
import { Navbar, Nav, NavDropdown, Modal, Button } from "react-bootstrap";
import { useState } from "react";
import { Navbar, Nav, NavDropdown } from "react-bootstrap";
import { useAuth } from "../context/auth";
import BankAccountModal from "./modals/BankAccountModal";
import { useBank } from "../context/bank";
@@ -8,8 +9,6 @@ export default function Header() {
const auth = useAuth();
const bank = useBank();
const [modalOpen, setModalOpen] = useState<boolean>(false);
const bankAccountRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const openBankSettings = () => {
setModalOpen(true);
@@ -29,14 +28,14 @@ export default function Header() {
return n !== Infinity && String(n) === str && n >= 0;
}
const save = () => {
if (bankAccountRef.current?.value) {
const save = (bankAccountNumber?: string, bankAccountHolderName?: string) => {
if (bankAccountNumber) {
try {
// Validace kódu banky
if (bankAccountRef.current?.value.indexOf('/') < 0) {
if (bankAccountNumber.indexOf('/') < 0) {
throw Error("Číslo účtu neobsahuje lomítko/kód banky")
}
const split = bankAccountRef.current?.value.split("/");
const split = bankAccountNumber.split("/");
if (split[1].length !== 4) {
throw Error("Kód banky musí být 4 číslice")
}
@@ -71,8 +70,8 @@ export default function Header() {
return
}
}
bank?.setBankAccountNumber(bankAccountRef.current?.value);
bank?.setBankAccountHolderName(nameRef.current?.value);
bank?.setBankAccountNumber(bankAccountNumber);
bank?.setBankAccountHolderName(bankAccountHolderName);
closeModal();
}
@@ -87,23 +86,6 @@ export default function Header() {
</NavDropdown>
</Nav>
</Navbar.Collapse>
<Modal show={modalOpen} onHide={closeModal} size="lg">
<Modal.Header closeButton>
<Modal.Title>Bankovní účet</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p>
Číslo účtu: <input className="mb-3" ref={bankAccountRef} type="text" placeholder="123456-1234567890/1234" defaultValue={bank?.bankAccount} /> <br />
Název příjemce (jméno majitele účtu): <input ref={nameRef} type="text" placeholder="Jan Novák" defaultValue={bank?.holderName} />
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={closeModal}>
Storno
</Button>
<Button variant="primary" onClick={save}>
Uložit
</Button>
</Modal.Footer>
</Modal>
<BankAccountModal isOpen={modalOpen} onClose={closeModal} onSave={save} />
</Navbar>
}

View File

@@ -1,49 +1,49 @@
import React from "react";
import { Table } from "react-bootstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { useAuth } from "../context/auth";
import { Order, PizzaDayState, PizzaOrder } from "../types";
import { updatePizzaFee } from "../Api";
import PizzaOrderRow from "./PizzaOrderRow";
export default function PizzaOrderList({ state, orders, onDelete }: { state: PizzaDayState, orders: Order[], onDelete: (pizzaOrder: PizzaOrder) => void }) {
type Props = {
state: PizzaDayState,
orders: Order[],
onDelete: (pizzaOrder: PizzaOrder) => void,
creator: string,
}
export default function PizzaOrderList({ state, orders, onDelete, creator }: Props) {
const auth = useAuth();
const saveFees = async (customer: string, text?: string, price?: number) => {
await updatePizzaFee(customer, text, price);
}
if (!orders?.length) {
return <p className="mt-3"><i>Zatím žádné objednávky...</i></p>
}
const total = orders.map(order => order.pizzaList.map(o => o.price).reduce((total, i) => total + i)).reduce((total, i) => total + i);
const total = orders.reduce((total, order) => total + order.totalPrice, 0);
return <Table className="mt-3" striped bordered hover>
<thead>
<tr>
<th>Jméno</th>
<th>Objednávka</th>
<th>Pozmka</th>
<th>Cena</th>
</tr>
</thead>
<tbody>
{orders.map(order => <tr key={order.customer}>
<td>{order.customer}</td>
<td>{order.pizzaList.map<React.ReactNode>((pizzaOrder, index) =>
<span key={index}>
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
<FontAwesomeIcon onClick={() => {
onDelete(pizzaOrder);
}} title='Odstranit' className='trash-icon' icon={faTrashCan} />
}
</span>)
.reduce((prev, curr, index) => [prev, <br key={`br-${index}`} />, curr])}
</td>
<td>{order.note || '-'}</td>
<td>{order.totalPrice} </td>
</tr>)}
<tr style={{ fontWeight: 'bold' }}>
<td colSpan={3}>Celkem</td>
<td>{`${total}`}</td>
</tr>
</tbody>
</Table>
return <>
<Table className="mt-3" striped bordered hover>
<thead>
<tr>
<th>Jméno</th>
<th>Objedvka</th>
<th>Poznámka</th>
<th>Příplatek</th>
<th>Cena</th>
</tr>
</thead>
<tbody>
{orders.map(order => <tr key={order.customer}>
<PizzaOrderRow creator={creator} state={state} order={order} onDelete={onDelete} onFeeModalSave={saveFees} />
</tr>)}
<tr style={{ fontWeight: 'bold' }}>
<td colSpan={4}>Celkem</td>
<td>{`${total}`}</td>
</tr>
</tbody>
</Table>
</>
}

View File

@@ -0,0 +1,45 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMoneyBill1, faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { useAuth } from "../context/auth";
import { Order, PizzaDayState, PizzaOrder } from "../types";
import PizzaAdditionalFeeModal from "./modals/PizzaAdditionalFeeModal";
type Props = {
creator: string,
order: Order,
state: PizzaDayState,
onDelete: (order: PizzaOrder) => void,
onFeeModalSave: (customer: string, name?: string, price?: number) => void,
}
export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeModalSave }: Props) {
const auth = useAuth();
const [isFeeModalOpen, setFeeModalOpen] = useState<boolean>(false);
const saveFees = (customer: string, text?: string, price?: number) => {
onFeeModalSave(customer, text, price);
setFeeModalOpen(false);
}
return <>
<td>{order.customer}</td>
<td>{order.pizzaList.map<React.ReactNode>((pizzaOrder, index) =>
<span key={index}>
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
<FontAwesomeIcon onClick={() => {
onDelete(pizzaOrder);
}} title='Odstranit' className='action-icon' icon={faTrashCan} />
}
</span>)
.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}${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
<td>
{order.totalPrice} {auth?.login === creator && state === PizzaDayState.CREATED && <FontAwesomeIcon onClick={() => { setFeeModalOpen(true) }} title='Nastavit příplatek' className='action-icon' icon={faMoneyBill1} />}
</td>
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price?.toString() }} />
</>
}

View File

@@ -0,0 +1,35 @@
import { useRef } from "react";
import { Modal, Button } from "react-bootstrap"
import { useBank } from "../../context/bank";
type Props = {
isOpen: boolean,
onClose: () => void,
onSave: (bankAccountNumber?: string, bankAccountHolderName?: string) => void,
}
/** Modální dialog pro nastavení čísla účtu a jména majitele. */
export default function BankAccountModal({ isOpen, onClose, onSave }: Props) {
const bank = useBank();
const bankAccountRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
return <Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton>
<Modal.Title>Bankovní účet</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p>
Číslo účtu: <input className="mb-3" ref={bankAccountRef} type="text" placeholder="123456-1234567890/1234" defaultValue={bank?.bankAccount} /> <br />
Název příjemce (jméno majitele účtu): <input ref={nameRef} type="text" placeholder="Jan Novák" defaultValue={bank?.holderName} />
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onClose}>
Storno
</Button>
<Button variant="primary" onClick={() => onSave(bankAccountRef.current?.value, nameRef.current?.value)}>
Uložit
</Button>
</Modal.Footer>
</Modal>
}

View File

@@ -0,0 +1,45 @@
import { useRef } from "react";
import { Modal, Button } from "react-bootstrap"
type Props = {
customerName: string,
isOpen: boolean,
onClose: () => void,
onSave: (customer: string, name?: string, price?: number) => void,
initialValues?: { text?: string, price?: string },
}
/** Modální dialog pro nastavení příplatků za pizzu. */
export default function PizzaAdditionalFeeModal({ customerName, isOpen, onClose, onSave, initialValues }: Props) {
const textRef = useRef<HTMLInputElement>(null);
const priceRef = useRef<HTMLInputElement>(null);
const doSubmit = () => {
onSave(customerName, textRef.current?.value, parseInt(priceRef.current?.value || "0"));
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onSave(customerName, textRef.current?.value, parseInt(priceRef.current?.value || "0"));
}
}
return <Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton>
<Modal.Title>Příplatky za objednávku pro {customerName}</Modal.Title>
</Modal.Header>
<Modal.Body>
Popis: <input className="mb-3" ref={textRef} type="text" placeholder="např. kuřecí maso" defaultValue={initialValues?.text} onKeyDown={handleKeyDown} /> <br />
Cena v : <input ref={priceRef} type="number" placeholder="0" defaultValue={initialValues?.price} onKeyDown={handleKeyDown} /> <br />
<div className="mt-3" style={{ fontSize: 'small' }}>Je možné zadávat i záporné částky (např. v případě slev)</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onClose}>
Storno
</Button>
<Button variant="primary" onClick={doSubmit}>
Uložit
</Button>
</Modal.Footer>
</Modal>
}