This commit is contained in:
+22
-10
@@ -428,31 +428,43 @@ function App() {
|
||||
}
|
||||
|
||||
const pizzaSuggestions = useMemo(() => {
|
||||
if (!data?.pizzaList) {
|
||||
if (!data?.pizzaList && !data?.salatList) {
|
||||
return [];
|
||||
}
|
||||
const suggestions: SelectSearchOption[] = [];
|
||||
data.pizzaList.forEach((pizza, index) => {
|
||||
data.pizzaList?.forEach((pizza, index) => {
|
||||
const group: SelectSearchOption = { name: pizza.name, type: "group", items: [] }
|
||||
pizza.sizes.forEach((size, sizeIndex) => {
|
||||
const name = `${size.size} (${size.price} Kč)`;
|
||||
const value = `${index}|${sizeIndex}`;
|
||||
const value = `pizza|${index}|${sizeIndex}`;
|
||||
group.items?.push({ name, value });
|
||||
})
|
||||
suggestions.push(group);
|
||||
})
|
||||
});
|
||||
if (data.salatList?.length) {
|
||||
const salatGroup: SelectSearchOption = { name: "Saláty", type: "group", items: [] }
|
||||
data.salatList.forEach((salat, index) => {
|
||||
salatGroup.items?.push({ name: `${salat.name} (${salat.price} Kč)`, value: `salat|${index}` });
|
||||
});
|
||||
suggestions.push(salatGroup);
|
||||
}
|
||||
return suggestions;
|
||||
}, [data?.pizzaList]);
|
||||
}, [data?.pizzaList, data?.salatList]);
|
||||
|
||||
const handlePizzaChange = async (value: SelectedOptionValue | SelectedOptionValue[]) => {
|
||||
if (auth?.login && data?.pizzaList) {
|
||||
if (auth?.login) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError('Nepodporovaný typ hodnoty: ' + typeof value);
|
||||
}
|
||||
const s = value.split('|');
|
||||
const pizzaIndex = Number.parseInt(s[0]);
|
||||
const pizzaSizeIndex = Number.parseInt(s[1]);
|
||||
await addPizza({ body: { pizzaIndex, pizzaSizeIndex } });
|
||||
if (s[0] === 'salat') {
|
||||
const salatIndex = Number.parseInt(s[1]);
|
||||
await addPizza({ body: { salatIndex } });
|
||||
} else {
|
||||
const pizzaIndex = Number.parseInt(s[1]);
|
||||
const pizzaSizeIndex = Number.parseInt(s[2]);
|
||||
await addPizza({ body: { pizzaIndex, pizzaSizeIndex } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,7 +833,7 @@ function App() {
|
||||
<SelectSearch
|
||||
search={true}
|
||||
options={pizzaSuggestions}
|
||||
placeholder='Vyhledat pizzu...'
|
||||
placeholder='Vyhledat pizzu nebo salát...'
|
||||
onChange={handlePizzaChange}
|
||||
onBlur={_ => { }}
|
||||
onFocus={_ => { }}
|
||||
|
||||
+42
-3
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { load } from 'cheerio';
|
||||
import { getPizzaListMock } from './mock';
|
||||
import { getPizzaListMock, getSalatListMock } from './mock';
|
||||
import { Salat } from '../../types/gen/types.gen';
|
||||
|
||||
// TODO přesunout do types
|
||||
type PizzaSize = {
|
||||
@@ -20,7 +21,8 @@ type Pizza = {
|
||||
|
||||
// TODO mělo by být konfigurovatelné proměnnou z prostředí s tímhle jako default
|
||||
const baseUrl = 'https://www.pizzachefie.cz';
|
||||
const pizzyUrl = `${baseUrl}/pizzy.html?pobocka=plzen`;
|
||||
const pizzyUrl = `${baseUrl}/pizzy.html`;
|
||||
const salayUrl = `${baseUrl}/salaty.html`;
|
||||
|
||||
const buildPizzaUrl = (pizzaUrl: string) => {
|
||||
return `${baseUrl}/${pizzaUrl}`;
|
||||
@@ -34,9 +36,12 @@ const boxPrices: { [key: string]: number } = {
|
||||
"50cm": 25
|
||||
}
|
||||
|
||||
// Cena obalu pro salát
|
||||
const SALAT_BOX_PRICE = 13;
|
||||
|
||||
/**
|
||||
* Stáhne a scrapne aktuální pizzy ze stránek Pizza Chefie.
|
||||
*
|
||||
*
|
||||
* @param mock zda vrátit pouze mock data
|
||||
*/
|
||||
export async function downloadPizzy(mock: boolean): Promise<Pizza[]> {
|
||||
@@ -84,4 +89,38 @@ export async function downloadPizzy(mock: boolean): Promise<Pizza[]> {
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stáhne a scrapne aktuální saláty ze stránek Pizza Chefie.
|
||||
* Příplatek za obal je pro každý salát pevně 13 Kč.
|
||||
*
|
||||
* @param mock zda vrátit pouze mock data
|
||||
*/
|
||||
export async function downloadSalaty(mock: boolean): Promise<Salat[]> {
|
||||
if (mock) {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(getSalatListMock()), 1000));
|
||||
}
|
||||
const html = await axios.get(salayUrl).then(res => res.data);
|
||||
const $ = load(html);
|
||||
const links = $('.vypisproduktu > div > h4 > a');
|
||||
const urls = [];
|
||||
for (const element of links) {
|
||||
if (element.name === 'a' && element.attribs?.href) {
|
||||
urls.push(buildPizzaUrl(element.attribs.href));
|
||||
}
|
||||
}
|
||||
const result: Salat[] = [];
|
||||
for (const url of urls) {
|
||||
const salatHtml = await axios.get(url).then(res => res.data);
|
||||
const name = $('.produkt > h2', salatHtml).first().text().trim();
|
||||
const ingredients: string[] = [];
|
||||
$('.prisady > li', salatHtml).each((i, elm) => {
|
||||
ingredients.push($(elm).text());
|
||||
});
|
||||
const priceText = $('.cena > span', salatHtml).first().text().trim();
|
||||
const price = Number.parseInt(priceText.split(' Kč')[0]);
|
||||
result.push({ name, ingredients, price: price + SALAT_BOX_PRICE });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1429,6 +1429,34 @@ export const getPizzaListMock = () => {
|
||||
return MOCK_PIZZA_LIST;
|
||||
}
|
||||
|
||||
// Mockovací data pro saláty
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "Caesar",
|
||||
ingredients: ["Salát", "Rajčata", "Kuřecí maso", "Krutony", "Parmazán", "Caesar dresing", "Olivový olej"],
|
||||
price: 184 + 13,
|
||||
},
|
||||
{
|
||||
name: "Šopský salát",
|
||||
ingredients: ["Salátová okurka", "Rajčata", "Paprika mix", "Červená cibule", "Balkánský sýr"],
|
||||
price: 164 + 13,
|
||||
},
|
||||
{
|
||||
name: "Těstovinový salát",
|
||||
ingredients: ["Penne", "Okurka", "Rajčata", "Paprika mix", "Kuřecí maso", "Jogurtový dresing"],
|
||||
price: 184 + 13,
|
||||
},
|
||||
]
|
||||
|
||||
export const getSalatListMock = () => {
|
||||
return MOCK_SALAT_LIST;
|
||||
}
|
||||
|
||||
export const getStatsMock = (): WeeklyStats => {
|
||||
return [
|
||||
{
|
||||
|
||||
+76
-6
@@ -2,9 +2,9 @@ import { formatDate } from "./utils";
|
||||
import { callNotifikace } from "./notifikace";
|
||||
import { generateQr } from "./qr";
|
||||
import getStorage from "./storage";
|
||||
import { downloadPizzy } from "./chefie";
|
||||
import { downloadPizzy, downloadSalaty } from "./chefie";
|
||||
import { getClientData, getToday, initIfNeeded } from "./service";
|
||||
import { Pizza, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum, PendingQr } from "../../types/gen/types.gen";
|
||||
import { Pizza, Salat, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum, PendingQr } from "../../types/gen/types.gen";
|
||||
|
||||
const storage = getStorage();
|
||||
const PENDING_QR_PREFIX = 'pending_qr';
|
||||
@@ -25,7 +25,7 @@ export async function getPizzaList(): Promise<Pizza[] | undefined> {
|
||||
|
||||
/**
|
||||
* Uloží seznam dostupných pizz pro dnešní den.
|
||||
*
|
||||
*
|
||||
* @param pizzaList seznam dostupných pizz
|
||||
*/
|
||||
export async function savePizzaList(pizzaList: Pizza[]): Promise<ClientData> {
|
||||
@@ -38,6 +38,34 @@ export async function savePizzaList(pizzaList: Pizza[]): Promise<ClientData> {
|
||||
return clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrátí seznam dostupných salátů pro dnešní den.
|
||||
* Stáhne je, pokud je pro dnešní den nemá.
|
||||
*/
|
||||
export async function getSalatList(): Promise<Salat[] | undefined> {
|
||||
await initIfNeeded();
|
||||
let clientData = await getClientData(getToday());
|
||||
if (!clientData.salatList) {
|
||||
const mock = process.env.MOCK_DATA === 'true';
|
||||
clientData = await saveSalatList(await downloadSalaty(mock));
|
||||
}
|
||||
return Promise.resolve(clientData.salatList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uloží seznam dostupných salátů pro dnešní den.
|
||||
*
|
||||
* @param salatList seznam dostupných salátů
|
||||
*/
|
||||
export async function saveSalatList(salatList: Salat[]): Promise<ClientData> {
|
||||
await initIfNeeded();
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
clientData.salatList = salatList;
|
||||
await storage.setData(today, clientData);
|
||||
return clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vytvoří pizza day pro aktuální den a vrátí data pro klienta.
|
||||
*/
|
||||
@@ -48,8 +76,8 @@ export async function createPizzaDay(creator: string): Promise<ClientData> {
|
||||
throw Error("Pizza day pro dnešní den již existuje");
|
||||
}
|
||||
// TODO berka rychlooprava, vyřešit lépe - stahovat jednou, na jediném místě!
|
||||
const pizzaList = await getPizzaList();
|
||||
const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, ...clientData };
|
||||
const [pizzaList, salatList] = await Promise.all([getPizzaList(), getSalatList()]);
|
||||
const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, salatList, ...clientData };
|
||||
const today = formatDate(getToday());
|
||||
await storage.setData(today, data);
|
||||
callNotifikace({ input: { udalost: UdalostEnum.ZAHAJENA_PIZZA, user: creator } })
|
||||
@@ -113,6 +141,46 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize
|
||||
return clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Přidá objednávku salátu uživateli.
|
||||
*
|
||||
* @param login login uživatele
|
||||
* @param salat zvolený salát
|
||||
*/
|
||||
export async function addSalatOrder(login: string, salat: Salat) {
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED);
|
||||
}
|
||||
let order: PizzaOrder | undefined = clientData.pizzaDay?.orders?.find(o => o.customer === login);
|
||||
if (!order) {
|
||||
order = {
|
||||
customer: login,
|
||||
pizzaList: [],
|
||||
totalPrice: 0,
|
||||
hasQr: false,
|
||||
}
|
||||
clientData.pizzaDay.orders ??= [];
|
||||
clientData.pizzaDay.orders.push(order);
|
||||
}
|
||||
const salatOrder: PizzaVariant = {
|
||||
varId: 0,
|
||||
name: salat.name,
|
||||
size: "1 porce",
|
||||
price: salat.price,
|
||||
category: 'salat',
|
||||
}
|
||||
order.pizzaList ??= [];
|
||||
order.pizzaList.push(salatOrder);
|
||||
order.totalPrice += salatOrder.price;
|
||||
await storage.setData(today, clientData);
|
||||
return clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Odstraní všechny pizzy uživatele (celou jeho objednávku).
|
||||
* Pokud Pizza day neexistuje nebo není ve stavu CREATED, neudělá nic.
|
||||
@@ -269,7 +337,9 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
|
||||
if (bankAccount?.length && bankAccountHolder?.length) {
|
||||
for (const order of clientData.pizzaDay.orders!) {
|
||||
if (order.customer !== login) { // zatím platí creator = objednávající, a pro toho nemá QR kód smysl
|
||||
let message = order.pizzaList!.map(pizza => `Pizza ${pizza.name} (${pizza.size})`).join(', ');
|
||||
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);
|
||||
order.hasQr = true;
|
||||
// Uložíme nevyřízený QR kód pro persistentní zobrazení
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express, { Request } from "express";
|
||||
import { getLogin } from "../auth";
|
||||
import { createPizzaDay, deletePizzaDay, getPizzaList, addPizzaOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee, dismissPendingQr } from "../pizza";
|
||||
import { createPizzaDay, deletePizzaDay, getPizzaList, getSalatList, addPizzaOrder, addSalatOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee, dismissPendingQr } from "../pizza";
|
||||
import { parseToken } from "../utils";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { AddPizzaData, DismissQrData, FinishDeliveryData, RemovePizzaData, UpdatePizzaDayNoteData, UpdatePizzaFeeData } from "../../../types";
|
||||
@@ -24,27 +24,43 @@ router.post("/delete", async (req: Request<{}, any, undefined>, res) => {
|
||||
|
||||
router.post("/add", async (req: Request<{}, any, AddPizzaData["body"]>, res) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
if (isNaN(req.body?.pizzaIndex)) {
|
||||
throw Error("Nebyl předán index pizzy");
|
||||
if (req.body?.salatIndex !== undefined && !isNaN(req.body.salatIndex)) {
|
||||
// Přidání salátu
|
||||
const salatIndex = req.body.salatIndex;
|
||||
const salaty = await getSalatList();
|
||||
if (!salaty) {
|
||||
throw Error("Selhalo získání seznamu dostupných salátů.");
|
||||
}
|
||||
if (!salaty[salatIndex]) {
|
||||
throw Error("Neplatný index salátu: " + salatIndex);
|
||||
}
|
||||
const data = await addSalatOrder(login, salaty[salatIndex]);
|
||||
getWebsocket().emit("message", data);
|
||||
res.status(200).json({});
|
||||
} else {
|
||||
// Přidání pizzy
|
||||
if (req.body?.pizzaIndex === undefined || isNaN(req.body.pizzaIndex)) {
|
||||
throw Error("Nebyl předán index pizzy ani salátu");
|
||||
}
|
||||
const pizzaIndex = req.body.pizzaIndex;
|
||||
if (req.body?.pizzaSizeIndex === undefined || isNaN(req.body.pizzaSizeIndex)) {
|
||||
throw Error("Nebyl předán index velikosti pizzy");
|
||||
}
|
||||
const pizzaSizeIndex = req.body.pizzaSizeIndex;
|
||||
let pizzy = await getPizzaList();
|
||||
if (!pizzy) {
|
||||
throw Error("Selhalo získání seznamu dostupných pizz.");
|
||||
}
|
||||
if (!pizzy[pizzaIndex]) {
|
||||
throw Error("Neplatný index pizzy: " + pizzaIndex);
|
||||
}
|
||||
if (!pizzy[pizzaIndex].sizes[pizzaSizeIndex]) {
|
||||
throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex);
|
||||
}
|
||||
const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]);
|
||||
getWebsocket().emit("message", data);
|
||||
res.status(200).json({});
|
||||
}
|
||||
const pizzaIndex = req.body.pizzaIndex;
|
||||
if (isNaN(req.body?.pizzaSizeIndex)) {
|
||||
throw Error("Nebyl předán index velikosti pizzy");
|
||||
}
|
||||
const pizzaSizeIndex = req.body.pizzaSizeIndex;
|
||||
let pizzy = await getPizzaList();
|
||||
if (!pizzy) {
|
||||
throw Error("Selhalo získání seznamu dostupných pizz.");
|
||||
}
|
||||
if (!pizzy[pizzaIndex]) {
|
||||
throw Error("Neplatný index pizzy: " + pizzaIndex);
|
||||
}
|
||||
if (!pizzy[pizzaIndex].sizes[pizzaSizeIndex]) {
|
||||
throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex);
|
||||
}
|
||||
const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]);
|
||||
getWebsocket().emit("message", data);
|
||||
res.status(200).json({});
|
||||
});
|
||||
|
||||
router.post("/remove", async (req: Request<{}, any, RemovePizzaData["body"]>, res) => {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
post:
|
||||
operationId: addPizza
|
||||
summary: Přidání pizzy do objednávky.
|
||||
summary: Přidání pizzy nebo salátu do objednávky.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- pizzaIndex
|
||||
- pizzaSizeIndex
|
||||
properties:
|
||||
pizzaIndex:
|
||||
description: Index pizzy v nabídce
|
||||
description: Index pizzy v nabídce (pro přidání pizzy)
|
||||
type: integer
|
||||
pizzaSizeIndex:
|
||||
description: Index velikosti pizzy v nabídce variant
|
||||
description: Index velikosti pizzy v nabídce variant (pro přidání pizzy)
|
||||
type: integer
|
||||
salatIndex:
|
||||
description: Index salátu v nabídce (pro přidání salátu)
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: Přidání pizzy do objednávky proběhlo úspěšně.
|
||||
description: Přidání pizzy nebo salátu do objednávky proběhlo úspěšně.
|
||||
|
||||
@@ -53,6 +53,11 @@ ClientData:
|
||||
description: Datum a čas poslední aktualizace pizz
|
||||
type: string
|
||||
format: date-time
|
||||
salatList:
|
||||
description: Seznam dostupných salátů pro předaný den
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/Salat"
|
||||
pendingQrs:
|
||||
description: Nevyřízené QR kódy pro platbu z předchozích pizza day
|
||||
type: array
|
||||
@@ -426,7 +431,7 @@ Pizza:
|
||||
items:
|
||||
$ref: "#/PizzaSize"
|
||||
PizzaVariant:
|
||||
description: Konkrétní varianta (velikost) jedné pizzy.
|
||||
description: Konkrétní varianta (velikost) jedné pizzy nebo salátu.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
@@ -436,16 +441,40 @@ PizzaVariant:
|
||||
- price
|
||||
properties:
|
||||
varId:
|
||||
description: Unikátní identifikátor varianty pizzy
|
||||
description: Unikátní identifikátor varianty
|
||||
type: integer
|
||||
name:
|
||||
description: Název pizzy
|
||||
description: Název pizzy nebo salátu
|
||||
type: string
|
||||
size:
|
||||
description: Velikost pizzy (např. "30cm")
|
||||
description: Velikost pizzy (např. "30cm"), nebo "1 porce" pro salát
|
||||
type: string
|
||||
price:
|
||||
description: Cena pizzy v Kč, včetně krabice
|
||||
description: Cena v Kč, včetně krabice/obalu
|
||||
type: number
|
||||
category:
|
||||
description: Kategorie položky (pizza nebo salat)
|
||||
type: string
|
||||
enum: [pizza, salat]
|
||||
Salat:
|
||||
description: Salát z nabídky Pizza Chefie
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- name
|
||||
- ingredients
|
||||
- price
|
||||
properties:
|
||||
name:
|
||||
description: Název salátu
|
||||
type: string
|
||||
ingredients:
|
||||
description: Seznam obsažených ingrediencí
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
price:
|
||||
description: Cena salátu v Kč (bez obalu)
|
||||
type: number
|
||||
PizzaOrder:
|
||||
description: Údaje o objednávce pizzy jednoho uživatele.
|
||||
|
||||
Reference in New Issue
Block a user