Ukládání dat výhradně do DB
This commit is contained in:
parent
18cb172e06
commit
c4b14bdf6b
@ -29,9 +29,7 @@ Aplikace sestává ze tří modulů.
|
|||||||
- `docker compose -f compose-traefik.yml up --build -d`
|
- `docker compose -f compose-traefik.yml up --build -d`
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- [ ] Vybraná jídla strávníků zobrazovat v samostatném sloupci
|
- [ ] Zbytečně nescrapovat každý den pizzy z Pizza Chefie, dokud není založen Pizza Day
|
||||||
- [ ] Umožnit výběr/zadání preferovaného času odchodu na oběd
|
|
||||||
- Hodí se např. pokud má někdo schůzky
|
|
||||||
- [ ] Možnost úhrady celé útraty jednou osobou
|
- [ ] 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
|
- 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.)
|
- Obecně to bude problém např. pokud si někdo objedná něco navíc (pití apod.)
|
||||||
@ -91,3 +89,8 @@ Aplikace sestává ze tří modulů.
|
|||||||
- [x] Zkrášlit dialog pro vyplnění čísla účtu, vypadá mizerně
|
- [x] Zkrášlit dialog pro vyplnění čísla účtu, vypadá mizerně
|
||||||
- [x] Zbavit se Food API, potřebnou funkcionalitu zahrnout do serveru
|
- [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)
|
- [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ů
|
@ -14,7 +14,7 @@ import './App.css';
|
|||||||
import { SelectSearchOption } from 'react-select-search';
|
import { SelectSearchOption } from 'react-select-search';
|
||||||
import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { useBank } from './context/bank';
|
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';
|
import Footer from './components/Footer';
|
||||||
|
|
||||||
const EVENT_CONNECT = "connect"
|
const EVENT_CONNECT = "connect"
|
||||||
@ -41,7 +41,7 @@ function App() {
|
|||||||
const bank = useBank();
|
const bank = useBank();
|
||||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||||
const [data, setData] = useState<ClientData>();
|
const [data, setData] = useState<ClientData>();
|
||||||
const [food, setFood] = useState<{ [key in Restaurants]: Food[] }>();
|
const [food, setFood] = useState<{ [key in Restaurants]: Menu }>();
|
||||||
const [myOrder, setMyOrder] = useState<Order>();
|
const [myOrder, setMyOrder] = useState<Order>();
|
||||||
const [foodChoiceList, setFoodChoiceList] = useState<Food[]>();
|
const [foodChoiceList, setFoodChoiceList] = useState<Food[]>();
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
@ -116,7 +116,7 @@ function App() {
|
|||||||
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
|
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
|
||||||
if (restaurantKey > -1 && food) {
|
if (restaurantKey > -1 && food) {
|
||||||
const restaurant = Object.values(Restaurants)[restaurantKey];
|
const restaurant = Object.values(Restaurants)[restaurantKey];
|
||||||
setFoodChoiceList(food[restaurant]);
|
setFoodChoiceList(food[restaurant].food);
|
||||||
} else {
|
} else {
|
||||||
setFoodChoiceList(undefined);
|
setFoodChoiceList(undefined);
|
||||||
}
|
}
|
||||||
@ -242,12 +242,13 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderFoodTable = (name: string, food: Food[]) => {
|
const renderFoodTable = (name: string, menu: Menu) => {
|
||||||
return <Col md={12} lg={4}>
|
return <Col md={12} lg={4}>
|
||||||
<h3>{name}</h3>
|
<h3>{name}</h3>
|
||||||
|
{menu?.lastUpdate && <small>Poslední aktualizace: {menu.lastUpdate}</small>}
|
||||||
<Table striped bordered hover>
|
<Table striped bordered hover>
|
||||||
<tbody>
|
<tbody>
|
||||||
{food?.length > 0 ? food.map((f: any, index: number) =>
|
{menu?.food?.length > 0 ? menu.food.map((f: any, index: number) =>
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{f.amount}</td>
|
<td>{f.amount}</td>
|
||||||
<td>{f.name}</td>
|
<td>{f.name}</td>
|
||||||
@ -279,6 +280,8 @@ function App() {
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Podpora <a href="https://redis.io">Redis</a></li>
|
<li>Podpora <a href="https://redis.io">Redis</a></li>
|
||||||
<li>Možnost výběru preferovaného času odchodu</li>
|
<li>Možnost výběru preferovaného času odchodu</li>
|
||||||
|
<li>Ukládání dostupných obědových menu a pizz do DB (zrušení dočasných souborů)</li>
|
||||||
|
<li>Zobrazení času poslední aktualizace každého menu</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Alert>
|
</Alert>
|
||||||
<h1 className='title'>Dnes je {data.date}</h1>
|
<h1 className='title'>Dnes je {data.date}</h1>
|
||||||
@ -349,7 +352,7 @@ function App() {
|
|||||||
const locationsKey = Object.keys(Locations)[Number(locationKey)]
|
const locationsKey = Object.keys(Locations)[Number(locationKey)]
|
||||||
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
|
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
|
||||||
const restaurant = Object.values(Restaurants)[restaurantKey];
|
const restaurant = Object.values(Restaurants)[restaurantKey];
|
||||||
const foodName = food[restaurant][foodIndex].name;
|
const foodName = food[restaurant].food[foodIndex].name;
|
||||||
return <li key={foodIndex}>
|
return <li key={foodIndex}>
|
||||||
{foodName}
|
{foodName}
|
||||||
{login === auth.login && <FontAwesomeIcon onClick={() => {
|
{login === auth.login && <FontAwesomeIcon onClick={() => {
|
||||||
|
@ -2,13 +2,13 @@ import express from "express";
|
|||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import bodyParser from "body-parser";
|
import bodyParser from "body-parser";
|
||||||
import cors from 'cors';
|
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 dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
|
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
|
||||||
import { getQr } from "./qr";
|
import { getQr } from "./qr";
|
||||||
import { generateToken, getLogin, getTrusted, verify } from "./auth";
|
import { generateToken, getLogin, getTrusted, verify } from "./auth";
|
||||||
import { Locations, Restaurants } from "../../types";
|
import { Food, Locations, Restaurants } from "../../types";
|
||||||
import { downloadPizzy } from "./chefie";
|
import { downloadPizzy } from "./chefie";
|
||||||
|
|
||||||
const ENVIRONMENT = process.env.NODE_ENV || 'production';
|
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 mock = !!process.env.MOCK_DATA;
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const data = {
|
const data = {
|
||||||
[Restaurants.SLADOVNICKA]: await getMenuSladovnicka(date, mock),
|
[Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date, mock),
|
||||||
[Restaurants.UMOTLIKU]: await getMenuUMotliku(date, mock),
|
[Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date, mock),
|
||||||
[Restaurants.TECHTOWER]: await getMenuTechTower(date, mock),
|
[Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date, mock),
|
||||||
}
|
}
|
||||||
res.status(200).json(data);
|
res.status(200).json(data);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import os from "os";
|
|
||||||
import path from "path";
|
|
||||||
import fs from "fs";
|
|
||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
import { formatDate } from "./utils";
|
|
||||||
import { Food } from "../../types";
|
import { Food } from "../../types";
|
||||||
|
|
||||||
// Fráze v názvech jídel, které naznačují že se jedná o polévku
|
// Fráze v názvech jídel, které naznačují že se jedná o polévku
|
||||||
@ -51,23 +47,16 @@ const getDayOfWeekIndex = (date: Date) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stáhne (v případě potřeby) a vrátí HTML z dané URL pro předané datum.
|
* Stáhne a vrátí aktuální HTML z dané URL.
|
||||||
* Pokud je pro dané datum již staženo, vrátí jeho obsah ze souboru.
|
|
||||||
*
|
*
|
||||||
* @param url URL pro stažení
|
* @param url URL pro stažení
|
||||||
* @param prefix prefix pro uložení do souboru
|
* @returns stažené HTML
|
||||||
* @param date datum ke kterému stáhnout HTML
|
|
||||||
* @returns stažené HTML, nebo HTML ze souborové cache
|
|
||||||
*/
|
*/
|
||||||
const getHtml = async (url: string, prefix: string, date: Date): Promise<Buffer> => {
|
const getHtml = async (url: string): Promise<any> => {
|
||||||
const fileName = path.join(os.tmpdir(), `${prefix}_${formatDate(date)}.html`);
|
|
||||||
if (!fs.existsSync(fileName)) {
|
|
||||||
await axios.get(url).then(res => res.data).then(content => {
|
await axios.get(url).then(res => res.data).then(content => {
|
||||||
fs.writeFileSync(fileName, content);
|
return content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return fs.readFileSync(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Získá obědovou nabídku Sladovnické pro předané datum.
|
* Získá obědovou nabídku Sladovnické pro předané datum.
|
||||||
@ -109,7 +98,7 @@ export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean
|
|||||||
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
|
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const html = await getHtml(SLADOVNICKA_URL, 'sladovnicka', date);
|
const html = await getHtml(SLADOVNICKA_URL);
|
||||||
const $ = load(html);
|
const $ = load(html);
|
||||||
// Najdeme index pro vstupní datum (např. při svátcích bude posunutý)
|
// 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
|
// 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
|
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const html = await getHtml(U_MOTLIKU_URL, 'umotliku', date);
|
const html = await getHtml(U_MOTLIKU_URL);
|
||||||
const $ = load(html);
|
const $ = load(html);
|
||||||
const table = $('table.table.table-hover.Xtable-striped').first();
|
const table = $('table.table.table-hover.Xtable-striped').first();
|
||||||
const body = table.children().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
|
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const html = await getHtml(TECHTOWER_URL, 'techtower', date);
|
const html = await getHtml(TECHTOWER_URL);
|
||||||
const $ = load(html);
|
const $ = load(html);
|
||||||
const fonts = $('font.wsw-41');
|
const fonts = $('font.wsw-41');
|
||||||
let font = undefined;
|
let font = undefined;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { formatDate, getHumanDate, getIsWeekend } from "./utils";
|
import { formatDate, getHumanDate, getHumanTime, getIsWeekend } from "./utils";
|
||||||
import { callNotifikace } from "./notifikace";
|
import { callNotifikace } from "./notifikace";
|
||||||
import { generateQr } from "./qr";
|
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 getStorage from "./storage";
|
||||||
|
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
|
||||||
|
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
|
|
||||||
@ -51,6 +52,44 @@ export async function savePizzaList(pizzaList: Pizza[]): Promise<ClientData> {
|
|||||||
return clientData;
|
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<Menu> {
|
||||||
|
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.
|
* Vytvoří pizza day pro aktuální den a vrátí data pro klienta.
|
||||||
*/
|
*/
|
||||||
|
@ -15,6 +15,13 @@ export function getHumanDate(date: Date) {
|
|||||||
return `${currentDay}.${currentMonth}.${currentYear} (${currentDayOfWeek})`;
|
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. */
|
/** Vrátí true, pokud je předané datum o víkendu. */
|
||||||
export function getIsWeekend(date: Date) {
|
export function getIsWeekend(date: Date) {
|
||||||
const dayName = date.toLocaleDateString("CZ-cs", { weekday: 'long' }).toLowerCase()
|
const dayName = date.toLocaleDateString("CZ-cs", { weekday: 'long' }).toLowerCase()
|
||||||
|
@ -71,11 +71,18 @@ export interface ClientData {
|
|||||||
date: string, // dnešní datum pro zobrazení
|
date: string, // dnešní datum pro zobrazení
|
||||||
isWeekend: boolean, // příznak, zda je dnes víkend
|
isWeekend: boolean, // příznak, zda je dnes víkend
|
||||||
choices: Choices, // seznam voleb
|
choices: Choices, // seznam voleb
|
||||||
|
menus?: { [restaurant: string]: Menu }, // menu jednotlivých restaurací
|
||||||
pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje
|
pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje
|
||||||
pizzaList?: Pizza[], // seznam dostupných pizz pro dnešní den
|
pizzaList?: Pizza[], // seznam dostupných pizz pro dnešní den
|
||||||
pizzaListLastUpdate?: Date, // datum a čas poslední aktualizace pizz
|
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. */
|
/** Jídlo z obědového menu restaurace. */
|
||||||
export interface Food {
|
export interface Food {
|
||||||
amount?: string, // množství standardní porce, např. 0,33l nebo 150g
|
amount?: string, // množství standardní porce, např. 0,33l nebo 150g
|
||||||
|
Loading…
x
Reference in New Issue
Block a user