diff --git a/client/src/Api.ts b/client/src/Api.ts index 7f93665..48071a8 100644 --- a/client/src/Api.ts +++ b/client/src/Api.ts @@ -24,14 +24,13 @@ export const getQrUrl = (login: string) => { return `${getBaseUrl()}/api/qr?login=${login}`; } -export const getData = async () => { - return await api.get('/api/data'); +export const getData = async (dayIndex?: number) => { + let url = '/api/data'; + if (dayIndex != null) { + url += '?dayIndex=' + dayIndex; + } + return await api.get(url); } - -export const getFood = async () => { - return await api.get('/api/food'); -} - export const createPizzaDay = async () => { return await api.post('/api/createPizzaDay', undefined); } @@ -56,16 +55,16 @@ export const finishDelivery = async (bankAccount?: string, bankAccountHolder?: s return await api.post('/api/finishDelivery', JSON.stringify({ bankAccount, bankAccountHolder })); } -export const addChoice = async (locationIndex: number, foodIndex?: number) => { - return await api.post('/api/addChoice', JSON.stringify({ locationIndex, foodIndex })); +export const addChoice = async (locationIndex: number, foodIndex?: number, dayIndex?: number) => { + return await api.post('/api/addChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex })); } -export const removeChoices = async (locationIndex: number) => { - return await api.post('/api/removeChoices', JSON.stringify({ locationIndex })); +export const removeChoices = async (locationIndex: number, dayIndex?: number) => { + return await api.post('/api/removeChoices', JSON.stringify({ locationIndex, dayIndex })); } -export const removeChoice = async (locationIndex: number, foodIndex: number) => { - return await api.post('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex })); +export const removeChoice = async (locationIndex: number, foodIndex: number, dayIndex?: number) => { + return await api.post('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex })); } export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => { @@ -84,6 +83,6 @@ export const login = async (login?: string) => { return await api.post('/api/login', JSON.stringify({ login })); } -export const changeDepartureTime = async (login: string, time: string) => { - return await api.post('/api/changeDepartureTime', JSON.stringify({ login, time })); +export const changeDepartureTime = async (login: string, time: string, dayIndex?: number) => { + return await api.post('/api/changeDepartureTime', JSON.stringify({ login, time, dayIndex })); } \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css index d31b027..aa245db 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -56,7 +56,7 @@ } .title { - margin: 50px 0; + margin: 50px 30px; } .food-tables { @@ -116,4 +116,10 @@ .trusted-icon { color: rgb(0, 89, 255); margin-right: 10px; +} + +.day-navigator { + display: flex; + align-items: center; + font-size: xx-large; } \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index bdba286..2818963 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket'; -import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getFood, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updateNote } from './Api'; +import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updateNote } from './Api'; import { useAuth } from './context/auth'; import Login from './Login'; import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap'; @@ -16,7 +16,7 @@ import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { useBank } from './context/bank'; import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, Menu } from './types'; import Footer from './components/Footer'; -import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faChainBroken, faChevronLeft, faChevronRight, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import Loader from './components/Loader'; const EVENT_CONNECT = "connect" @@ -50,26 +50,42 @@ function App() { const socket = useContext(SocketContext); const choiceRef = useRef(null); const foodChoiceRef = useRef(null); + const departureChoiceRef = useRef(null); const poznamkaRef = useRef(null); const [failure, setFailure] = useState(false); + const [dayIndex, setDayIndex] = useState(); + // Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu + // https://medium.com/@kishorkrishna/cant-access-latest-state-inside-socket-io-listener-heres-how-to-fix-it-1522a5abebdb + const dayIndexRef = useRef(dayIndex); // Načtení dat po přihlášení useEffect(() => { if (!auth || !auth.login) { return } - getData().then(data => { + getData().then((data: ClientData) => { setData(data); - }).catch(e => { - setFailure(true); - }) - getFood().then(food => { - setFood(food); + setDayIndex(data.weekIndex); + dayIndexRef.current = data.weekIndex; + setFood(data.menus); }).catch(e => { setFailure(true); }) }, [auth, auth?.login]); + // Přenačtení pro zvolený den + useEffect(() => { + if (!auth || !auth.login) { + return + } + getData(dayIndex).then((data: ClientData) => { + setData(data); + setFood(data.menus); + }).catch(e => { + setFailure(true); + }) + }, [dayIndex]); + // Registrace socket eventů useEffect(() => { socket.on(EVENT_CONNECT, () => { @@ -82,7 +98,10 @@ function App() { }); socket.on(EVENT_MESSAGE, (newData: ClientData) => { // console.log("Přijata nová data ze socketu", newData); - setData(newData); + // Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený + if (dayIndexRef.current == null || newData.weekIndex === dayIndexRef.current) { + setData(newData); + } }); return () => { @@ -137,10 +156,16 @@ function App() { } }, [choiceRef.current?.value, food]) + // Index v týdnu dnešního dne (0-6) + // TODO tohle má posílat server, klient je nespolehlivý + const currentDayIndex = useMemo(() => { + return (((new Date().getDay() - 1) % 7) + 7) % 7; + }, []) + const doAddChoice = async (event: React.ChangeEvent) => { const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations); if (auth?.login) { - await addChoice(index); + await addChoice(index, undefined, dayIndex); if (foodChoiceRef.current?.value) { foodChoiceRef.current.value = ""; } @@ -152,14 +177,14 @@ function App() { const restaurantKey = choiceRef.current.value; if (auth?.login) { const locationIndex = Object.keys(Locations).indexOf(restaurantKey as unknown as Locations); - await addChoice(locationIndex, Number(event.target.value)); + await addChoice(locationIndex, Number(event.target.value), dayIndex); } } } const doRemoveChoices = async (locationKey: string) => { if (auth?.login) { - await removeChoices(Number(locationKey)); + await removeChoices(Number(locationKey), dayIndex); // Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo if (choiceRef?.current?.value) { choiceRef.current.value = ""; @@ -172,7 +197,7 @@ function App() { const doRemoveFoodChoice = async (locationKey: string, foodIndex: number) => { if (auth?.login) { - await removeChoice(Number(locationKey), foodIndex); + await removeChoice(Number(locationKey), foodIndex, dayIndex); if (choiceRef?.current?.value) { choiceRef.current.value = ""; } @@ -250,11 +275,25 @@ function App() { const handleChangeDepartureTime = async (event: React.ChangeEvent) => { if (foodChoiceList?.length && choiceRef.current?.value) { if (auth?.login) { - await changeDepartureTime(auth.login, event.target.value); + await changeDepartureTime(auth.login, event.target.value, dayIndex); } } } + const handleDayChange = async (dayIndex: number) => { + setDayIndex(dayIndex); + dayIndexRef.current = dayIndex; + if (choiceRef?.current?.value) { + choiceRef.current.value = ""; + } + if (foodChoiceRef?.current?.value) { + foodChoiceRef.current.value = ""; + } + if (departureChoiceRef?.current?.value) { + departureChoiceRef.current.value = ""; + } + } + const renderFoodTable = (name: string, menu: Menu) => { let content; if (menu?.closed) { @@ -319,11 +358,19 @@ function App() { Poslední změny:
    -
  • Lépe vypadající a více vypovídající načítací obrazovky (ztráta spojení, chyba načtení apod.)
  • -
  • (Již brzy) možnost náhledu na další dny v týdnu
  • +
  • Možnost náhledu na celý týden a výběru na následující dny v týdnu
  • +
      +
    • Pizza day je možno založit pouze pro aktuální den
    • +
-

Dnes je {data.date}

+ {dayIndex != null && +
+ {dayIndex > 0 && handleDayChange(dayIndex - 1)} />} +

{`${dayIndex === currentDayIndex ? "(Dnes) " : ""}${data.date}`}

+ {dayIndex < 4 && handleDayChange(dayIndex + 1)} />} +
+ } {renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])} {renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])} @@ -331,35 +378,37 @@ function App() {
-

Jak to dnes vidíš s obědem?

- - - {Object.entries(Locations) - .filter(entry => { - // TODO: wtf, cos pil, když jsi tohle psal? v2 - const key = entry[0]; - const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations); - const locationsKey = Object.keys(Locations)[locationIndex]; - const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); - const v = Object.values(Restaurants)[restaurantKey]; - return v == null || !food[v].closed; - }) - .map(entry => )} - - Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí. - {foodChoiceList && !closed && <> -

Na co dobrého? (nepovinné)

- + {dayIndex == null || dayIndex >= currentDayIndex && <> +

{`Jak to ${dayIndex == null || dayIndex === currentDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}

+ - {foodChoiceList.map((food, index) => )} - - } - {foodChoiceList && !closed && <> -

V kolik hodin preferuješ odchod?

- - - {DEPARTURE_TIMES.map(time => )} + {Object.entries(Locations) + .filter(entry => { + // TODO: wtf, cos pil, když jsi tohle psal? v2 + const key = entry[0]; + const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations); + const locationsKey = Object.keys(Locations)[locationIndex]; + const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); + const v = Object.values(Restaurants)[restaurantKey]; + return v == null || !food[v].closed; + }) + .map(entry => )} + Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí. + {foodChoiceList && !closed && <> +

Na co dobrého? (nepovinné)

+ + + {foodChoiceList.map((food, index) => )} + + } + {foodChoiceList && !closed && <> +

V kolik hodin preferuješ odchod?

+ + + {DEPARTURE_TIMES.map(time => )} + + } } {Object.keys(data.choices).length > 0 ? @@ -419,115 +468,117 @@ function App() { :
Zatím nikdo nehlasoval...
} -
- {!data.pizzaDay && -
-

Pro dnešní den není aktuálně založen Pizza day.

- -
- } - {data.pizzaDay && -
+ {dayIndex === currentDayIndex && +
+ {!data.pizzaDay &&
-

Pizza day

- { - data.pizzaDay.state === PizzaDayState.CREATED && -
-

- Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.
- Můžete upravovat své objednávky. -

- { - data.pizzaDay.creator === auth.login && - <> - - - - } -
- } - { - data.pizzaDay.state === PizzaDayState.LOCKED && -
-

Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}

- {data.pizzaDay.creator === auth.login && - <> - - {/* +
+ } + {data.pizzaDay && +
+
+

Pizza day

+ { + data.pizzaDay.state === PizzaDayState.CREATED && +
+

+ Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.
+ Můžete upravovat své objednávky. +

+ { + data.pizzaDay.creator === auth.login && + <> + + + + } +
+ } + { + data.pizzaDay.state === PizzaDayState.LOCKED && +
+

Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}

+ {data.pizzaDay.creator === auth.login && + <> + + {/* */} - - - } + + + } +
+ } + { + data.pizzaDay.state === PizzaDayState.ORDERED && +
+

Pizzy byly objednány uživatelem {data.pizzaDay.creator}

+ {data.pizzaDay.creator === auth.login && +
+ + +
+ } +
+ } + { + data.pizzaDay.state === PizzaDayState.DELIVERED && +
+

Pizzy byly doručeny. Objednávku můžete uhradit pomocí QR kódu níže.

+
+ } +
+ {data.pizzaDay.state === PizzaDayState.CREATED && +
+ + Poznámka: { + if (event.key === 'Enter') { + handlePoznamkaChange(); + } + }} /> +
} + { - data.pizzaDay.state === PizzaDayState.ORDERED && -
-

Pizzy byly objednány uživatelem {data.pizzaDay.creator}

- {data.pizzaDay.creator === auth.login && -
- - -
- } -
- } - { - data.pizzaDay.state === PizzaDayState.DELIVERED && -
-

Pizzy byly doručeny. Objednávku můžete uhradit pomocí QR kódu níže.

+ data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && +
+

QR platba

+
Částka: {myOrder.totalPrice} Kč
+ QR kód +

Generování QR kódů je v experimentální fázi - doporučujeme si překontrolovat údaje před odesláním platby.

}
- {data.pizzaDay.state === PizzaDayState.CREATED && -
- - Poznámka: { - if (event.key === 'Enter') { - handlePoznamkaChange(); - } - }} /> - -
- } - - { - data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && -
-

QR platba

-
Částka: {myOrder.totalPrice} Kč
- QR kód -

Generování QR kódů je v experimentální fázi - doporučujeme si překontrolovat údaje před odesláním platby.

-
- } -
- } -
+ } +
+ }
}
diff --git a/server/src/index.ts b/server/src/index.ts index 1bdf708..f47bcdc 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -2,13 +2,12 @@ import express from "express"; import { Server } from "socket.io"; import bodyParser from "body-parser"; import cors from 'cors'; -import { addChoice, addPizzaOrder, createPizzaDay, deletePizzaDay, finishPizzaDelivery, finishPizzaOrder, getData, getPizzaList, getRestaurantMenu, lockPizzaDay, removeChoice, removeChoices, removePizzaOrder, savePizzaList, unlockPizzaDay, updateDepartureTime, updateNote } from "./service"; +import { addChoice, addPizzaOrder, createPizzaDay, deletePizzaDay, finishPizzaDelivery, finishPizzaOrder, getData, getDateForWeekIndex, getPizzaList, getToday, lockPizzaDay, removeChoice, removeChoices, removePizzaOrder, unlockPizzaDay, updateDepartureTime, updateNote } from "./service"; import dotenv from 'dotenv'; import path from 'path'; import { getQr } from "./qr"; import { generateToken, getLogin, getTrusted, verify } from "./auth"; -import { Food, Locations, Restaurants } from "../../types"; -import { downloadPizzy } from "./chefie"; +import { getDayOfWeekIndex } from "./utils"; const ENVIRONMENT = process.env.NODE_ENV || 'production'; dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); @@ -51,6 +50,28 @@ const parseToken = (req: any) => { } } +/** + * Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň + * roven nebo vyšší indexu dnešního dne. + * + * @param req request + * @returns index dne v týdnu + */ +const parseValidateFutureDayIndex = (req: any) => { + if (!req.body.dayIndex) { + throw Error(`Nebyl předán index dne v týdnu.`); + } + const todayDayIndex = getDayOfWeekIndex(getToday()); + const dayIndex = parseInt(req.body.dayIndex); + if (isNaN(dayIndex)) { + throw Error(`Neplatný index dne v týdnu: ${req.body.dayIndex}`); + } + if (dayIndex < todayDayIndex) { + throw Error(`Předaný index dne v týdnu (${dayIndex}) nesmí být nižší než dnešní den (${todayDayIndex})`); + } + return dayIndex; +} + // ----------- Metody nevyžadující token -------------- app.get("/api/whoami", (req, res) => { @@ -110,19 +131,14 @@ app.use((req, res, next) => { /** Vrátí data pro aktuální den. */ app.get("/api/data", async (req, res) => { - res.status(200).json(await getData()); -}); - -/** Vrátí obědové menu pro dostupné podniky. */ -app.get("/api/food", async (req, res) => { - const mock = process.env.MOCK_DATA === 'true'; - const date = new Date(); - const data = { - [Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date, mock), - [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date, mock), - [Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date, mock), + let date = undefined; + if (req.query.dayIndex != null && typeof req.query.dayIndex === 'string') { + const index = parseInt(req.query.dayIndex); + if (!isNaN(index)) { + date = getDateForWeekIndex(parseInt(req.query.dayIndex)); + } } - res.status(200).json(data); + res.status(200).json(await getData(date)); }); /** Založí pizza day pro aktuální den, za předpokladu že dosud neexistuje. */ @@ -207,27 +223,58 @@ app.post("/api/addChoice", async (req, res) => { const login = getLogin(parseToken(req)); const trusted = getTrusted(parseToken(req)); if (req.body.locationIndex > -1) { - const data = await addChoice(login, trusted, req.body.locationIndex, req.body.foodIndex); + let date = undefined; + if (req.body.dayIndex != null) { + let dayIndex; + try { + dayIndex = parseValidateFutureDayIndex(req); + } catch (e: any) { + return res.status(400).json({ error: e.message }); + } + date = getDateForWeekIndex(dayIndex); + } + const data = await addChoice(login, trusted, req.body.locationIndex, req.body.foodIndex, date); io.emit("message", data); - res.status(200).json(data); + return res.status(200).json(data); } - res.status(400); // TODO přidat popis chyby + return res.status(400); // TODO přidat popis chyby }); app.post("/api/removeChoices", async (req, res) => { const login = getLogin(parseToken(req)); - const data = await removeChoices(login, req.body.locationIndex); + let date = undefined; + if (req.body.dayIndex != null) { + let dayIndex; + try { + dayIndex = parseValidateFutureDayIndex(req); + } catch (e: any) { + return res.status(400).json({ error: e.message }); + } + date = getDateForWeekIndex(dayIndex); + } + const data = await removeChoices(login, req.body.locationIndex, date); io.emit("message", data); res.status(200).json(data); }); app.post("/api/removeChoice", async (req, res) => { const login = getLogin(parseToken(req)); - const data = await removeChoice(login, req.body.locationIndex, req.body.foodIndex); + let date = undefined; + if (req.body.dayIndex != null) { + let dayIndex; + try { + dayIndex = parseValidateFutureDayIndex(req); + } catch (e: any) { + return res.status(400).json({ error: e.message }); + } + date = getDateForWeekIndex(dayIndex); + } + const data = await removeChoice(login, req.body.locationIndex, req.body.foodIndex, date); io.emit("message", data); res.status(200).json(data); }); +// TODO přejmenovat, ať je jasné, že to patří k Pizza day app.post("/api/updateNote", async (req, res) => { const login = getLogin(parseToken(req)); if (req.body.note && req.body.note.length > 100) { @@ -240,7 +287,17 @@ app.post("/api/updateNote", async (req, res) => { app.post("/api/changeDepartureTime", async (req, res) => { const login = getLogin(parseToken(req)); - const data = await updateDepartureTime(login, req.body?.time); + let date = undefined; + if (req.body.dayIndex != null) { + let dayIndex; + try { + dayIndex = parseValidateFutureDayIndex(req); + } catch (e: any) { + return res.status(400).json({ error: e.message }); + } + date = getDateForWeekIndex(dayIndex); + } + const data = await updateDepartureTime(login, req.body?.time, date); io.emit("message", data); res.status(200).json(data); }); diff --git a/server/src/mock.ts b/server/src/mock.ts new file mode 100644 index 0000000..9444a91 --- /dev/null +++ b/server/src/mock.ts @@ -0,0 +1,387 @@ +import { getDayOfWeekIndex } from "./utils"; + +// Mockovací data pro podporované podniky, na jeden týden +const MOCK_DATA = { + 'sladovnicka': [ + [ + { + amount: "0,25l", + name: "Kulajda", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "250g", + name: "Kuřecí křidélka s vařeným bramborem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Hovězí hamburger s BBQ omáčkou a hranolky", + price: "145\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Frankfurtská hovězí pečeně s jasmínovou rýží", + price: "135\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,25l", + name: "Hovězí vývar s kapáním", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "200g", + name: "Smažený karbanátek s bramborovou kaší", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Vepřová plec na smetaně s kynutým knedlíkem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Trhané kachní maso se zeleninovým kuskusem", + price: "135\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,25l", + name: "Zelná polévka s klobásou", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "150g", + name: "Hovězí na česneku s bramborovým knedlíkem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "250g", + name: "Přírodní holandský řízek s bramborovou kaší, rajčatový salát", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "350g", + name: "Bagel s vinnou klobásou, cibulový konfit, kysané zelí, slanina a hořčicová mayo, hranolky, curry omáčka", + price: "135\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,25l", + name: "Kuřecí vývar s nudlemi", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "150g", + name: "Kovbojské fazole s klobásou a chlebem", + price: "125\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Kuřecí rarášci s vařeným bramborem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Hovězí pečeně na slanině s jasmínovou rýží", + price: "135\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,25l", + name: "Dršťková polévka", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "150g", + name: "Tortilla s kuřecím masem, čedarem, zeleninou a papričkami jalapeňos", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Segedínský guláš s kynutým knedlíkem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Filet z krůtích prsou, omáčka z modrého sýra, pečené brambory", + price: "145\xA0Kč", + isSoup: false, + } + ] + ], + 'uMotliku': [ + [ + { + amount: "0,33l", + name: "Žampionový krém", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "250g", + name: "Halušky se zelím a uzeným masem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Kuřecí směs se zeleninou a arašídy, jasmínová rýže", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Smažený vepřový řízek, vařený brambor, okurka", + price: "135\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,33l", + name: "Zelňačka", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "250g", + name: "Lasagne s boloňskou omáčkou a sýrem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Fazolový guláš s párkem, bramborem a pečivem", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Grilovaná vepřová panenka s omáčkou z hrubozrnné hořčice, restované brambory se slaninou", + price: "145\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,33l", + name: "Kuřecí vývar s nudlemi", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "150g", + name: "Hovězí svíčková na smetaně, kynutý knedlík, brusinky", + price: "145\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Kuřecí roláda s mandlovou nádivkou, šťouchané brambory se slaninou", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Těstovinový salát s tuňákem", + price: "135\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,33l", + name: "Minestrone", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "150g", + name: "Hamburger s trhaným vepřovým pleckem v BBQ omáčce, karamelizovaná cibule, hranolky, jarní dip", + price: "145\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Kuřecí medailonky v sýrové omáčce, šťouchaný brambor s pažitkou", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Kofty z mletého masa, tzatziki, pita chléb", + price: "135\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "0,33l", + name: "Gulášová", + price: "35\xA0Kč", + isSoup: true, + }, + { + amount: "150g", + name: "Španělský hovězí ptáček, rýže (houskový knedlík)", + price: "145\xA0Kč", + isSoup: false, + }, + { + amount: "150g", + name: "Kuřecí prsa zapečená s rajčaty a mozarellou, šťouchaný brambor s jarní cibulkou", + price: "135\xA0Kč", + isSoup: false, + }, + { + amount: "3ks", + name: "Ovocné knedlíky s máslem, cukrem a tvarohem", + price: "135\xA0Kč", + isSoup: false, + } + ] + ], + 'techTower': [ + [ + { + amount: "-", + name: "Uzený vývar s kapustou", + price: "40\xA0Kč", + isSoup: true, + }, + { + amount: "-", + name: "Čočka na kyselo, opečená klobása, okurka, chléb", + price: "120\xA0Kč", + isSoup: false, + }, + { + amount: "-", + name: "Kuřecí medailonky se sýrovou omáčkou, hranolky", + price: "170\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "-", + name: "Slepičí s nudlemi", + price: "40\xA0Kč", + isSoup: true, + }, + { + amount: "-", + name: "Zvěřinový guláš, knedlík", + price: "120\xA0Kč", + isSoup: false, + }, + { + amount: "-", + name: "Smažený hermelín, brambory, tatarská omáčka", + price: "170\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "-", + name: "Dýňový krém se smetanou", + price: "40\xA0Kč", + isSoup: true, + }, + { + amount: "-", + name: "Kuřecí směs se zeleninou, rýže", + price: "120\xA0Kč", + isSoup: false, + }, + { + amount: "-", + name: "Hambuger Black Angus s čedarem a slaninou, cibulové kroužky", + price: "220\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "-", + name: "Zeleninová s jáhly", + price: "40\xA0Kč", + isSoup: true, + }, + { + amount: "-", + name: "Rizoto s vepřovým masem, okurka", + price: "120\xA0Kč", + isSoup: false, + }, + { + amount: "-", + name: "Steak z lososa, grilovaná zelenina", + price: "220\xA0Kč", + isSoup: false, + } + ], + [ + { + amount: "-", + name: "Fazolová s uzeninou", + price: "40\xA0Kč", + isSoup: true, + }, + { + amount: "-", + name: "Krůtí perkelt, těstoviny", + price: "120\xA0Kč", + isSoup: false, + }, + { + amount: "-", + name: "Grilovaná vepřová panenka, parmazánové pyré", + price: "170\xA0Kč", + isSoup: false, + } + ] + ] +} + +export const getTodayMock = () => { + return '2023-05-31'; // středa +} + +export const getMenuSladovnickaMock = (date: Date) => { + return MOCK_DATA['sladovnicka'][getDayOfWeekIndex(date)]; +} + +export const getMenuUMotlikuMock = (date: Date) => { + return MOCK_DATA['uMotliku'][getDayOfWeekIndex(date)]; +} + +export const getMenuTechTowerMock = (date: Date) => { + return MOCK_DATA['techTower'][getDayOfWeekIndex(date)]; +} \ No newline at end of file diff --git a/server/src/restaurants.ts b/server/src/restaurants.ts index 92ef5e4..906b57b 100644 --- a/server/src/restaurants.ts +++ b/server/src/restaurants.ts @@ -1,6 +1,8 @@ import axios from "axios"; import { load } from 'cheerio'; import { Food } from "../../types"; +import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock } from "./mock"; +import { getDayOfWeekIndex } from "./utils"; // Fráze v názvech jídel, které naznačují že se jedná o polévku const SOUP_NAMES = ['polévka', 'česnečka', 'česnekový krém', 'cibulačka', 'vývar'] @@ -35,17 +37,6 @@ const sanitizeText = (text: string): string => { return text.replace('\t', '').trim(); } -/** - * Vrátí index dne v týdnu, kde pondělí=0, neděle=6 - * - * @param date datum - * @returns index dne v týdnu - */ -const getDayOfWeekIndex = (date: Date) => { - // https://stackoverflow.com/a/4467559 - return (((date.getDay() - 1) % 7) + 7) % 7; -} - /** * Stáhne a vrátí aktuální HTML z dané URL. * @@ -65,32 +56,7 @@ const getHtml = async (url: string): Promise => { */ export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean = false): Promise => { if (mock) { - return [ - { - amount: "0,25l", - name: "Zelná polévka s klobásou", - price: "35\xA0Kč", - isSoup: true, - }, - { - amount: "150g", - name: "Hovězí na česneku s bramborovým knedlíkem", - price: "135\xA0Kč", - isSoup: false, - }, - { - amount: "250g", - name: "Přírodní holandský řízek s bramborovou kaší, rajčatový salát", - price: "135\xA0Kč", - isSoup: false, - }, - { - amount: "350g", - name: "Bagel s vinnou klobásou, cibulový konfit, kysané zelí, slanina a hořčicová mayo, hranolky, curry omáčka", - price: "135\xA0Kč", - isSoup: false, - } - ] + return getMenuSladovnickaMock(date); } const todayDayIndex = getDayOfWeekIndex(date); if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend @@ -185,33 +151,7 @@ export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean */ export const getMenuUMotliku = async (date: Date = new Date(), mock: boolean = false): Promise => { if (mock) { - return [ - { - amount: "0,33l", - name: "Hovězí vývar s nudlemi", - price: "35\xA0Kč", - isSoup: true, - }, - { - amount: "150g", - name: "Opečený párek, čočka, sázené vejce, okurka", - price: "135\xA0Kč", - isSoup: false, - }, - { - amount: "150g", - name: "Hovězí líčka na červeném víně, bramborová kaše", - price: "145\xA0Kč", - isSoup: false, - }, - { - amount: "150g", - name: "Tortilla s trhaným kuřecím masem, uzeným sýrem, dipem a kukuřicí, míchaný salát", - price: "135\xA0Kč", - isSoup: false, - }, - - ] + return getMenuUMotlikuMock(date); } const todayDayIndex = getDayOfWeekIndex(date); if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend @@ -275,26 +215,7 @@ export const getMenuUMotliku = async (date: Date = new Date(), mock: boolean = f */ export const getMenuTechTower = async (date: Date = new Date(), mock: boolean = false) => { if (mock) { - return [ - { - amount: "-", - name: "Bavorská gulášová polévka s kroupami", - price: "40\xA0Kč", - isSoup: true, - }, - { - amount: "-", - name: "Vepřové výpečky, kedlubnové zelí, bramborový knedlík", - price: "120\xA0Kč", - isSoup: false, - }, - { - amount: "-", - name: "Hambuger Black Angus s čedarem a slaninou, cibulové kroužky", - price: "220\xA0Kč", - isSoup: false, - } - ] + return getMenuTechTowerMock(date); } const todayDayIndex = getDayOfWeekIndex(date); if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend diff --git a/server/src/service.ts b/server/src/service.ts index 3af00dc..38957ea 100644 --- a/server/src/service.ts +++ b/server/src/service.ts @@ -1,31 +1,56 @@ -import { formatDate, getHumanDate, getHumanTime, getIsWeekend } from "./utils"; +import { formatDate, getDayOfWeekIndex, getHumanDate, getHumanTime, getIsWeekend } from "./utils"; import { callNotifikace } from "./notifikace"; import { generateQr } from "./qr"; import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, Locations, Restaurants, Food, Menu } from "../../types"; import getStorage from "./storage"; import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants"; import { downloadPizzy } from "./chefie"; +import { getTodayMock } from "./mock"; const storage = getStorage(); /** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */ -function getToday(): Date { +export function getToday(): Date { if (process.env.MOCK_DATA === 'true') { - return new Date('2023-05-31'); + return new Date(getTodayMock()); } return new Date(); } -/** Vrátí "prázdná" (implicitní) data, pokud ještě nikdo nehlasoval. */ -function getEmptyData(): ClientData { - return { date: getHumanDate(getToday()), isWeekend: getIsWeekend(getToday()), choices: {} }; +/** Vrátí datum v aktuálním týdnu na základě předaného indexu (0 = pondělí). */ +export const getDateForWeekIndex = (index: number) => { + if (index < 0 || index > 4) { + // Nechceme shodit server, vrátíme dnešek + console.log('Neplatný index dne v týdnu: ' + index); + return getToday(); + } + const date = getToday(); + date.setDate(date.getDate() - getDayOfWeekIndex(date) + index); + return date; +} + +/** Vrátí "prázdná" (implicitní) data pro předaný den. */ +function getEmptyData(date?: Date): ClientData { + const usedDate = date || getToday(); + return { date: getHumanDate(usedDate), isWeekend: getIsWeekend(usedDate), weekIndex: getDayOfWeekIndex(usedDate), choices: {} }; } /** - * Vrátí veškerá klientská data pro aktuální den. + * Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán. */ -export async function getData(): Promise { - return await storage.getData(formatDate(getToday())) || getEmptyData(); +export async function getData(date?: Date): Promise { + const dateString = formatDate(date ?? getToday()); + const data = await storage.getData(dateString) || getEmptyData(date); + // Dotažení jídel, pokud je ještě nemáme + if (!data.menus) { + data.menus = { + [Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date ?? getToday()), + [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date ?? getToday()), + [Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date ?? getToday()), + } + await storage.setData(dateString, data); + } + return data; } /** @@ -64,9 +89,9 @@ export async function savePizzaList(pizzaList: Pizza[]): Promise { * @param date datum * @param mock příznak, zda chceme pouze mock data */ -export async function getRestaurantMenu(restaurant: Restaurants, date?: Date, mock?: boolean): Promise { - await initIfNeeded(); - const today = formatDate(getToday()); +export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise { + await initIfNeeded(date); + const today = formatDate(date ?? getToday()); const clientData: ClientData = await storage.getData(today); if (!clientData.menus) { clientData.menus = {}; @@ -78,6 +103,7 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date, mo closed: false, food: [], }; + const mock = process.env.MOCK_DATA === 'true'; switch (restaurant) { case Restaurants.SLADOVNICKA: clientData.menus[restaurant].food = await getMenuSladovnicka(date, mock); @@ -307,11 +333,11 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b return clientData; } -export async function initIfNeeded() { - const today = formatDate(getToday()); - const hasData = await storage.hasData(today); +export async function initIfNeeded(date?: Date) { + const usedDate = formatDate(date ?? getToday()); + const hasData = await storage.hasData(usedDate); if (!hasData) { - await storage.setData(today, getEmptyData()); + await storage.setData(usedDate, getEmptyData(date || getToday())); } } @@ -320,11 +346,12 @@ export async function initIfNeeded() { * * @param login login uživatele * @param location vybrané "umístění" + * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoices(login: string, location: Locations) { - const today = formatDate(getToday()); - let data: ClientData = await storage.getData(today); +export async function removeChoices(login: string, location: Locations, date?: Date) { + const selectedDay = formatDate(date ?? getToday()); + let data: ClientData = await storage.getData(selectedDay); // TODO zajistit, že neověřený uživatel se stejným loginem nemůže mazat volby ověřeného if (location in data.choices) { if (login in data.choices[location]) { @@ -332,7 +359,7 @@ export async function removeChoices(login: string, location: Locations) { if (Object.keys(data.choices[location]).length === 0) { delete data.choices[location] } - await storage.setData(today, data); + await storage.setData(selectedDay, data); } } return data; @@ -345,18 +372,19 @@ export async function removeChoices(login: string, location: Locations) { * @param login login uživatele * @param location vybrané "umístění" * @param foodIndex index jídla v jídelním lístku daného umístění, pokud existuje + * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoice(login: string, location: Locations, foodIndex: number) { - const today = formatDate(getToday()); - let data: ClientData = await storage.getData(today); +export async function removeChoice(login: string, location: Locations, foodIndex: number, date?: Date) { + const selectedDay = formatDate(date ?? getToday()); + let data: ClientData = await storage.getData(selectedDay); // TODO řešit ověření uživatele if (location in data.choices) { if (login in data.choices[location]) { const index = data.choices[location][login].options.indexOf(foodIndex); if (index > -1) { data.choices[location][login].options.splice(index, 1) - await storage.setData(today, data); + await storage.setData(selectedDay, data); } } } @@ -368,16 +396,15 @@ export async function removeChoice(login: string, location: Locations, foodIndex * * @param login login uživatele */ -async function removeChoiceIfPresent(login: string) { - const today = formatDate(getToday()); - let data: ClientData = await storage.getData(today); +async function removeChoiceIfPresent(login: string, date: string) { + let data: ClientData = await storage.getData(date); for (const key of Object.keys(data.choices)) { if (login in data.choices[key]) { delete data.choices[key][login]; if (Object.keys(data.choices[key]).length === 0) { delete data.choices[key]; } - await storage.setData(today, data); + await storage.setData(date, data); } } return data; @@ -390,12 +417,13 @@ async function removeChoiceIfPresent(login: string) { * @param location vybrané "umístění" * @param foodIndex volitelný index jídla v daném umístění * @param trusted příznak, zda se jedná o ověřeného uživatele + * @param date datum, ke kterému se volba vztahuje * @returns aktuální data */ -export async function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number) { +export async function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number, date?: Date) { await initIfNeeded(); - const today = formatDate(getToday()); - let data: ClientData = await storage.getData(today); + const selectedDate = formatDate(date ?? getToday()); + let data: ClientData = await storage.getData(selectedDate); // Ověření, že se neověřený užívatel nepokouší přepsat údaje ověřeného const locations = Object.values(data?.choices); let found = false; @@ -411,7 +439,7 @@ export async function addChoice(login: string, trusted: boolean, location: Locat } // Pokud měníme pouze lokaci, mažeme případné předchozí if (foodIndex == null) { - data = await removeChoiceIfPresent(login); + data = await removeChoiceIfPresent(login, selectedDate); } if (!(location in data.choices)) { data.choices[location] = {}; @@ -425,7 +453,7 @@ export async function addChoice(login: string, trusted: boolean, location: Locat if (foodIndex != null && !data.choices[location][login].options.includes(foodIndex)) { data.choices[location][login].options.push(foodIndex); } - await storage.setData(today, data); + await storage.setData(selectedDate, data); return data; } @@ -453,10 +481,11 @@ export async function updateNote(login: string, note?: string) { * * @param login login uživatele * @param time preferovaný čas odchodu + * @param date datum, ke kterému se čas vztahuje */ -export async function updateDepartureTime(login: string, time?: string) { - const today = formatDate(getToday()); - let clientData: ClientData = await storage.getData(today); +export async function updateDepartureTime(login: string, time?: string, date?: Date) { + const selectedDate = formatDate(date ?? getToday()); + let clientData: ClientData = await storage.getData(selectedDate); const found = Object.values(clientData.choices).find(location => login in location); // TODO validace, že se jedná o restauraci if (found) { @@ -465,7 +494,7 @@ export async function updateDepartureTime(login: string, time?: string) { } else { found[login].departureTime = time; } - await storage.setData(today, clientData); + await storage.setData(selectedDate, clientData); } return clientData; } \ No newline at end of file diff --git a/server/src/utils.ts b/server/src/utils.ts index 3961b33..6e7893e 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -22,8 +22,19 @@ export function getHumanTime(time: Date) { return `${currentHours}:${currentMinutes}`; } +/** + * Vrátí index dne v týdnu, kde pondělí=0, neděle=6 + * + * @param date datum + * @returns index dne v týdnu + */ +export const getDayOfWeekIndex = (date: Date) => { + // https://stackoverflow.com/a/4467559 + return (((date.getDay() - 1) % 7) + 7) % 7; +} + /** Vrátí true, pokud je předané datum o víkendu. */ export function getIsWeekend(date: Date) { - const dayName = date.toLocaleDateString("CZ-cs", { weekday: 'long' }).toLowerCase() - return dayName === 'sobota' || dayName === 'neděle' + const index = getDayOfWeekIndex(date); + return index == 5 || index == 6; } \ No newline at end of file diff --git a/types/Types.ts b/types/Types.ts index 91480ed..ee8493c 100644 --- a/types/Types.ts +++ b/types/Types.ts @@ -70,6 +70,7 @@ interface PizzaDay { export interface ClientData { date: string, // dnešní datum pro zobrazení isWeekend: boolean, // příznak, zda je dnes víkend + weekIndex: number, // index aktuálního dne v týdnu (0-6) choices: Choices, // seznam voleb menus?: { [restaurant: string]: Menu }, // menu jednotlivých restaurací pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje