From c4b14bdf6b8e489b801fab04fc7aaf120ca350c1 Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Sun, 6 Aug 2023 21:45:27 +0200 Subject: [PATCH] =?UTF-8?q?Ukl=C3=A1d=C3=A1n=C3=AD=20dat=20v=C3=BDhradn?= =?UTF-8?q?=C4=9B=20do=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 ++++++---- client/src/App.tsx | 15 ++++++++------ server/src/index.ts | 10 ++++----- server/src/restaurants.ts | 29 ++++++++------------------ server/src/service.ts | 43 +++++++++++++++++++++++++++++++++++++-- server/src/utils.ts | 7 +++++++ types/Types.ts | 7 +++++++ 7 files changed, 85 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 34a89b6..44447cf 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,7 @@ Aplikace sestává ze tří modulů. - `docker compose -f compose-traefik.yml up --build -d` ## TODO -- [ ] Vybraná jídla strávníků zobrazovat v samostatném sloupci -- [ ] Umožnit výběr/zadání preferovaného času odchodu na oběd - - Hodí se např. pokud má někdo schůzky +- [ ] Zbytečně nescrapovat každý den pizzy z Pizza Chefie, dokud není založen Pizza Day - [ ] Možnost úhrady celé útraty jednou osobou - Základní myšlenka: jedna osoba uhradí celou útratu (v zájmu rychlosti odbavení), ostatním se automaticky vygeneruje QR kód, kterým následně uhradí svoji část útraty - Obecně to bude problém např. pokud si někdo objedná něco navíc (pití apod.) @@ -90,4 +88,9 @@ Aplikace sestává ze tří modulů. - [x] Zavést .env.template a přidat .env do .gitignore - [x] Zkrášlit dialog pro vyplnění čísla účtu, vypadá mizerně - [x] Zbavit se Food API, potřebnou funkcionalitu zahrnout do serveru -- [x] Vyřešit API mezi serverem a klientem, aby nebyl v obou projektech duplicitní kód (viz types.ts a Types.tsx) \ No newline at end of file +- [x] Vyřešit API mezi serverem a klientem, aby nebyl v obou projektech duplicitní kód (viz types.ts a Types.tsx) +- [X] Vybraná jídla strávníků zobrazovat v samostatném sloupci +- [X] Umožnit výběr/zadání preferovaného času odchodu na oběd + - Hodí se např. pokud má někdo schůzky +- [X] Ukládat dostupné pizzy do DB místo souborů +- [X] Ukládat jídla do DB místo souborů \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index cc5a3d1..da427e5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -14,7 +14,7 @@ import './App.css'; import { SelectSearchOption } from 'react-select-search'; import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { useBank } from './context/bank'; -import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices } from './types'; +import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, Menu } from './types'; import Footer from './components/Footer'; const EVENT_CONNECT = "connect" @@ -41,7 +41,7 @@ function App() { const bank = useBank(); const [isConnected, setIsConnected] = useState(false); const [data, setData] = useState(); - const [food, setFood] = useState<{ [key in Restaurants]: Food[] }>(); + const [food, setFood] = useState<{ [key in Restaurants]: Menu }>(); const [myOrder, setMyOrder] = useState(); const [foodChoiceList, setFoodChoiceList] = useState(); const socket = useContext(SocketContext); @@ -116,7 +116,7 @@ function App() { const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); if (restaurantKey > -1 && food) { const restaurant = Object.values(Restaurants)[restaurantKey]; - setFoodChoiceList(food[restaurant]); + setFoodChoiceList(food[restaurant].food); } else { setFoodChoiceList(undefined); } @@ -242,12 +242,13 @@ function App() { } } - const renderFoodTable = (name: string, food: Food[]) => { + const renderFoodTable = (name: string, menu: Menu) => { return

{name}

+ {menu?.lastUpdate && Poslední aktualizace: {menu.lastUpdate}} - {food?.length > 0 ? food.map((f: any, index: number) => + {menu?.food?.length > 0 ? menu.food.map((f: any, index: number) => @@ -279,6 +280,8 @@ function App() {
  • Podpora Redis
  • Možnost výběru preferovaného času odchodu
  • +
  • Ukládání dostupných obědových menu a pizz do DB (zrušení dočasných souborů)
  • +
  • Zobrazení času poslední aktualizace každého menu

Dnes je {data.date}

@@ -349,7 +352,7 @@ function App() { const locationsKey = Object.keys(Locations)[Number(locationKey)] const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); const restaurant = Object.values(Restaurants)[restaurantKey]; - const foodName = food[restaurant][foodIndex].name; + const foodName = food[restaurant].food[foodIndex].name; return
  • {foodName} {login === auth.login && { diff --git a/server/src/index.ts b/server/src/index.ts index 8fe51d7..8e37c52 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -2,13 +2,13 @@ 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, lockPizzaDay, removeChoice, removeChoices, removePizzaOrder, savePizzaList, unlockPizzaDay, updateDepartureTime, updateNote } from "./service"; +import { addChoice, addPizzaOrder, createPizzaDay, deletePizzaDay, finishPizzaDelivery, finishPizzaOrder, getData, getPizzaList, getRestaurantMenu, lockPizzaDay, removeChoice, removeChoices, removePizzaOrder, savePizzaList, unlockPizzaDay, updateDepartureTime, updateNote } from "./service"; import dotenv from 'dotenv'; import path from 'path'; import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants"; import { getQr } from "./qr"; import { generateToken, getLogin, getTrusted, verify } from "./auth"; -import { Locations, Restaurants } from "../../types"; +import { Food, Locations, Restaurants } from "../../types"; import { downloadPizzy } from "./chefie"; const ENVIRONMENT = process.env.NODE_ENV || 'production'; @@ -114,9 +114,9 @@ app.get("/api/food", async (req, res) => { const mock = !!process.env.MOCK_DATA; const date = new Date(); const data = { - [Restaurants.SLADOVNICKA]: await getMenuSladovnicka(date, mock), - [Restaurants.UMOTLIKU]: await getMenuUMotliku(date, mock), - [Restaurants.TECHTOWER]: await getMenuTechTower(date, mock), + [Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date, mock), + [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date, mock), + [Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date, mock), } res.status(200).json(data); }); diff --git a/server/src/restaurants.ts b/server/src/restaurants.ts index 32600af..bca6e5a 100644 --- a/server/src/restaurants.ts +++ b/server/src/restaurants.ts @@ -1,9 +1,5 @@ import axios from "axios"; -import os from "os"; -import path from "path"; -import fs from "fs"; import { load } from 'cheerio'; -import { formatDate } from "./utils"; import { Food } from "../../types"; // Fráze v názvech jídel, které naznačují že se jedná o polévku @@ -51,22 +47,15 @@ const getDayOfWeekIndex = (date: Date) => { } /** - * Stáhne (v případě potřeby) a vrátí HTML z dané URL pro předané datum. - * Pokud je pro dané datum již staženo, vrátí jeho obsah ze souboru. + * Stáhne a vrátí aktuální HTML z dané URL. * * @param url URL pro stažení - * @param prefix prefix pro uložení do souboru - * @param date datum ke kterému stáhnout HTML - * @returns stažené HTML, nebo HTML ze souborové cache + * @returns stažené HTML */ -const getHtml = async (url: string, prefix: string, date: Date): Promise => { - const fileName = path.join(os.tmpdir(), `${prefix}_${formatDate(date)}.html`); - if (!fs.existsSync(fileName)) { - await axios.get(url).then(res => res.data).then(content => { - fs.writeFileSync(fileName, content); - }); - } - return fs.readFileSync(fileName); +const getHtml = async (url: string): Promise => { + await axios.get(url).then(res => res.data).then(content => { + return content + }); } /** @@ -109,7 +98,7 @@ export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend return []; } - const html = await getHtml(SLADOVNICKA_URL, 'sladovnicka', date); + const html = await getHtml(SLADOVNICKA_URL); const $ = load(html); // Najdeme index pro vstupní datum (např. při svátcích bude posunutý) // TODO validovat, že vstupní datum je v aktuálním týdnu @@ -230,7 +219,7 @@ export const getMenuUMotliku = async (date: Date = new Date(), mock: boolean = f if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend return []; } - const html = await getHtml(U_MOTLIKU_URL, 'umotliku', date); + const html = await getHtml(U_MOTLIKU_URL); const $ = load(html); const table = $('table.table.table-hover.Xtable-striped').first(); const body = table.children().first(); @@ -313,7 +302,7 @@ export const getMenuTechTower = async (date: Date = new Date(), mock: boolean = if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend return []; } - const html = await getHtml(TECHTOWER_URL, 'techtower', date); + const html = await getHtml(TECHTOWER_URL); const $ = load(html); const fonts = $('font.wsw-41'); let font = undefined; diff --git a/server/src/service.ts b/server/src/service.ts index b9eaf41..1e66c78 100644 --- a/server/src/service.ts +++ b/server/src/service.ts @@ -1,8 +1,9 @@ -import { formatDate, getHumanDate, getIsWeekend } from "./utils"; +import { formatDate, getHumanDate, getHumanTime, getIsWeekend } from "./utils"; import { callNotifikace } from "./notifikace"; import { generateQr } from "./qr"; -import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, Locations } from "../../types"; +import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, Locations, Restaurants, Food, Menu } from "../../types"; import getStorage from "./storage"; +import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants"; const storage = getStorage(); @@ -51,6 +52,44 @@ export async function savePizzaList(pizzaList: Pizza[]): Promise { return clientData; } +/** + * Vrátí menu dané restaurace pro předaný den. Pokud neexistuje, provede jeho stažení a uložení do DB. + * + * @param restaurant restaurace + * @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()); + const clientData: ClientData = await storage.getData(today); + if (!clientData.menus) { + clientData.menus = {}; + storage.setData(today, clientData); + } + if (!clientData?.menus?.[restaurant]) { + if (!clientData.menus[restaurant]) { + clientData.menus[restaurant] = { + lastUpdate: getHumanTime(new Date()), + food: [], + }; + } + switch (restaurant) { + case Restaurants.SLADOVNICKA: + clientData.menus[restaurant].food = await getMenuSladovnicka(date, mock); + break; + case Restaurants.UMOTLIKU: + clientData.menus[restaurant].food = await getMenuUMotliku(date, mock); + break; + case Restaurants.TECHTOWER: + clientData.menus[restaurant].food = await getMenuTechTower(date, mock); + break; + } + storage.setData(today, clientData); + } + return clientData?.menus?.[restaurant]; +} + /** * Vytvoří pizza day pro aktuální den a vrátí data pro klienta. */ diff --git a/server/src/utils.ts b/server/src/utils.ts index 7810019..3961b33 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -15,6 +15,13 @@ export function getHumanDate(date: Date) { return `${currentDay}.${currentMonth}.${currentYear} (${currentDayOfWeek})`; } +/** Vrátí human-readable reprezentaci předaného času pro zobrazení. */ +export function getHumanTime(time: Date) { + let currentHours = String(time.getHours()).padStart(2, '0'); + let currentMinutes = String(time.getMinutes()).padStart(2, "0"); + return `${currentHours}:${currentMinutes}`; +} + /** 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() diff --git a/types/Types.ts b/types/Types.ts index 2a33402..fe2cb87 100644 --- a/types/Types.ts +++ b/types/Types.ts @@ -71,11 +71,18 @@ export interface ClientData { date: string, // dnešní datum pro zobrazení isWeekend: boolean, // příznak, zda je dnes víkend choices: Choices, // seznam voleb + menus?: { [restaurant: string]: Menu }, // menu jednotlivých restaurací pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje pizzaList?: Pizza[], // seznam dostupných pizz pro dnešní den pizzaListLastUpdate?: Date, // datum a čas poslední aktualizace pizz } +/** Nabídka jídel jednoho podniku. */ +export interface Menu { + lastUpdate: string, // human-readable čas poslední aktualizace menu + food: Food[], // seznam jídel v menu +} + /** Jídlo z obědového menu restaurace. */ export interface Food { amount?: string, // množství standardní porce, např. 0,33l nebo 150g
  • {f.amount} {f.name}