Možnost náhledu a výběru na další dny v týdnu

This commit is contained in:
Martin Berka 2023-09-06 19:22:19 +02:00
parent 5379c21203
commit 832d3089ec
9 changed files with 766 additions and 304 deletions

View File

@ -24,14 +24,13 @@ export const getQrUrl = (login: string) => {
return `${getBaseUrl()}/api/qr?login=${login}`; return `${getBaseUrl()}/api/qr?login=${login}`;
} }
export const getData = async () => { export const getData = async (dayIndex?: number) => {
return await api.get<any>('/api/data'); let url = '/api/data';
if (dayIndex != null) {
url += '?dayIndex=' + dayIndex;
}
return await api.get<any>(url);
} }
export const getFood = async () => {
return await api.get<any>('/api/food');
}
export const createPizzaDay = async () => { export const createPizzaDay = async () => {
return await api.post<any, any>('/api/createPizzaDay', undefined); return await api.post<any, any>('/api/createPizzaDay', undefined);
} }
@ -56,16 +55,16 @@ export const finishDelivery = async (bankAccount?: string, bankAccountHolder?: s
return await api.post<any, any>('/api/finishDelivery', JSON.stringify({ bankAccount, bankAccountHolder })); return await api.post<any, any>('/api/finishDelivery', JSON.stringify({ bankAccount, bankAccountHolder }));
} }
export const addChoice = async (locationIndex: number, foodIndex?: number) => { export const addChoice = async (locationIndex: number, foodIndex?: number, dayIndex?: number) => {
return await api.post<any, any>('/api/addChoice', JSON.stringify({ locationIndex, foodIndex })); return await api.post<any, any>('/api/addChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex }));
} }
export const removeChoices = async (locationIndex: number) => { export const removeChoices = async (locationIndex: number, dayIndex?: number) => {
return await api.post<any, any>('/api/removeChoices', JSON.stringify({ locationIndex })); return await api.post<any, any>('/api/removeChoices', JSON.stringify({ locationIndex, dayIndex }));
} }
export const removeChoice = async (locationIndex: number, foodIndex: number) => { export const removeChoice = async (locationIndex: number, foodIndex: number, dayIndex?: number) => {
return await api.post<any, any>('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex })); return await api.post<any, any>('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex }));
} }
export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => { export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => {
@ -84,6 +83,6 @@ export const login = async (login?: string) => {
return await api.post<any, any>('/api/login', JSON.stringify({ login })); return await api.post<any, any>('/api/login', JSON.stringify({ login }));
} }
export const changeDepartureTime = async (login: string, time: string) => { export const changeDepartureTime = async (login: string, time: string, dayIndex?: number) => {
return await api.post<any, any>('/api/changeDepartureTime', JSON.stringify({ login, time })); return await api.post<any, any>('/api/changeDepartureTime', JSON.stringify({ login, time, dayIndex }));
} }

View File

@ -56,7 +56,7 @@
} }
.title { .title {
margin: 50px 0; margin: 50px 30px;
} }
.food-tables { .food-tables {
@ -116,4 +116,10 @@
.trusted-icon { .trusted-icon {
color: rgb(0, 89, 255); color: rgb(0, 89, 255);
margin-right: 10px; margin-right: 10px;
}
.day-navigator {
display: flex;
align-items: center;
font-size: xx-large;
} }

View File

@ -1,7 +1,7 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket'; 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 { useAuth } from './context/auth';
import Login from './Login'; import Login from './Login';
import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap'; 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 { useBank } from './context/bank';
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, Menu } from './types'; import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, Menu } from './types';
import Footer from './components/Footer'; 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'; import Loader from './components/Loader';
const EVENT_CONNECT = "connect" const EVENT_CONNECT = "connect"
@ -50,26 +50,42 @@ function App() {
const socket = useContext(SocketContext); const socket = useContext(SocketContext);
const choiceRef = useRef<HTMLSelectElement>(null); const choiceRef = useRef<HTMLSelectElement>(null);
const foodChoiceRef = useRef<HTMLSelectElement>(null); const foodChoiceRef = useRef<HTMLSelectElement>(null);
const departureChoiceRef = useRef<HTMLSelectElement>(null);
const poznamkaRef = useRef<HTMLInputElement>(null); const poznamkaRef = useRef<HTMLInputElement>(null);
const [failure, setFailure] = useState<boolean>(false); const [failure, setFailure] = useState<boolean>(false);
const [dayIndex, setDayIndex] = useState<number>();
// 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<number | undefined>(dayIndex);
// Načtení dat po přihlášení // Načtení dat po přihlášení
useEffect(() => { useEffect(() => {
if (!auth || !auth.login) { if (!auth || !auth.login) {
return return
} }
getData().then(data => { getData().then((data: ClientData) => {
setData(data); setData(data);
}).catch(e => { setDayIndex(data.weekIndex);
setFailure(true); dayIndexRef.current = data.weekIndex;
}) setFood(data.menus);
getFood().then(food => {
setFood(food);
}).catch(e => { }).catch(e => {
setFailure(true); setFailure(true);
}) })
}, [auth, auth?.login]); }, [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ů // Registrace socket eventů
useEffect(() => { useEffect(() => {
socket.on(EVENT_CONNECT, () => { socket.on(EVENT_CONNECT, () => {
@ -82,7 +98,10 @@ function App() {
}); });
socket.on(EVENT_MESSAGE, (newData: ClientData) => { socket.on(EVENT_MESSAGE, (newData: ClientData) => {
// console.log("Přijata nová data ze socketu", newData); // 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 () => { return () => {
@ -137,10 +156,16 @@ function App() {
} }
}, [choiceRef.current?.value, food]) }, [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<HTMLSelectElement>) => { const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations); const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations);
if (auth?.login) { if (auth?.login) {
await addChoice(index); await addChoice(index, undefined, dayIndex);
if (foodChoiceRef.current?.value) { if (foodChoiceRef.current?.value) {
foodChoiceRef.current.value = ""; foodChoiceRef.current.value = "";
} }
@ -152,14 +177,14 @@ function App() {
const restaurantKey = choiceRef.current.value; const restaurantKey = choiceRef.current.value;
if (auth?.login) { if (auth?.login) {
const locationIndex = Object.keys(Locations).indexOf(restaurantKey as unknown as Locations); 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) => { const doRemoveChoices = async (locationKey: string) => {
if (auth?.login) { 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 // Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo
if (choiceRef?.current?.value) { if (choiceRef?.current?.value) {
choiceRef.current.value = ""; choiceRef.current.value = "";
@ -172,7 +197,7 @@ function App() {
const doRemoveFoodChoice = async (locationKey: string, foodIndex: number) => { const doRemoveFoodChoice = async (locationKey: string, foodIndex: number) => {
if (auth?.login) { if (auth?.login) {
await removeChoice(Number(locationKey), foodIndex); await removeChoice(Number(locationKey), foodIndex, dayIndex);
if (choiceRef?.current?.value) { if (choiceRef?.current?.value) {
choiceRef.current.value = ""; choiceRef.current.value = "";
} }
@ -250,11 +275,25 @@ function App() {
const handleChangeDepartureTime = async (event: React.ChangeEvent<HTMLSelectElement>) => { const handleChangeDepartureTime = async (event: React.ChangeEvent<HTMLSelectElement>) => {
if (foodChoiceList?.length && choiceRef.current?.value) { if (foodChoiceList?.length && choiceRef.current?.value) {
if (auth?.login) { 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) => { const renderFoodTable = (name: string, menu: Menu) => {
let content; let content;
if (menu?.closed) { if (menu?.closed) {
@ -319,11 +358,19 @@ function App() {
<Alert variant={'primary'}> <Alert variant={'primary'}>
Poslední změny: Poslední změny:
<ul> <ul>
<li>Lépe vypadající a více vypovídající načítací obrazovky (ztráta spojení, chyba načtení apod.)</li> <li>Možnost náhledu na celý týden a výběru na následující dny v týdnu</li>
<li>(Již brzy) možnost náhledu na další dny v týdnu</li> <ul>
<li>Pizza day je možno založit pouze pro aktuální den</li>
</ul>
</ul> </ul>
</Alert> </Alert>
<h1 className='title'>Dnes je {data.date}</h1> {dayIndex != null &&
<div className='day-navigator'>
{dayIndex > 0 && <FontAwesomeIcon title="Předchozí den" icon={faChevronLeft} style={{ cursor: "pointer" }} onClick={() => handleDayChange(dayIndex - 1)} />}
<h1 className='title'>{`${dayIndex === currentDayIndex ? "(Dnes) " : ""}${data.date}`}</h1>
{dayIndex < 4 && <FontAwesomeIcon title="Následující den" icon={faChevronRight} style={{ cursor: "pointer" }} onClick={() => handleDayChange(dayIndex + 1)} />}
</div>
}
<Row className='food-tables'> <Row className='food-tables'>
{renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])} {renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])}
{renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])} {renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])}
@ -331,35 +378,37 @@ function App() {
</Row> </Row>
<div className='content-wrapper'> <div className='content-wrapper'>
<div className='content'> <div className='content'>
<p>Jak to dnes vidíš s obědem?</p> {dayIndex == null || dayIndex >= currentDayIndex && <>
<Form.Select ref={choiceRef} onChange={doAddChoice}> <p>{`Jak to ${dayIndex == null || dayIndex === currentDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<option></option> <Form.Select ref={choiceRef} onChange={doAddChoice}>
{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 => <option key={entry[0]} value={entry[0]}>{entry[1]}</option>)}
</Form.Select>
<small>Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí.</small>
{foodChoiceList && !closed && <>
<p style={{ marginTop: "10px" }}>Na co dobrého? <small>(nepovinné)</small></p>
<Form.Select ref={foodChoiceRef} onChange={doAddFoodChoice}>
<option></option> <option></option>
{foodChoiceList.map((food, index) => <option key={index} value={index}>{food.name}</option>)} {Object.entries(Locations)
</Form.Select> .filter(entry => {
</>} // TODO: wtf, cos pil, když jsi tohle psal? v2
{foodChoiceList && !closed && <> const key = entry[0];
<p style={{ marginTop: "10px" }}>V kolik hodin preferuješ odchod?</p> const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations);
<Form.Select ref={foodChoiceRef} onChange={handleChangeDepartureTime}> const locationsKey = Object.keys(Locations)[locationIndex];
<option></option> const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
{DEPARTURE_TIMES.map(time => <option key={time} value={time}>{time}</option>)} const v = Object.values(Restaurants)[restaurantKey];
return v == null || !food[v].closed;
})
.map(entry => <option key={entry[0]} value={entry[0]}>{entry[1]}</option>)}
</Form.Select> </Form.Select>
<small>Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí.</small>
{foodChoiceList && !closed && <>
<p style={{ marginTop: "10px" }}>Na co dobrého? <small>(nepovinné)</small></p>
<Form.Select ref={foodChoiceRef} onChange={doAddFoodChoice}>
<option></option>
{foodChoiceList.map((food, index) => <option key={index} value={index}>{food.name}</option>)}
</Form.Select>
</>}
{foodChoiceList && !closed && <>
<p style={{ marginTop: "10px" }}>V kolik hodin preferuješ odchod?</p>
<Form.Select ref={departureChoiceRef} onChange={handleChangeDepartureTime}>
<option></option>
{DEPARTURE_TIMES.map(time => <option key={time} value={time}>{time}</option>)}
</Form.Select>
</>}
</>} </>}
{Object.keys(data.choices).length > 0 ? {Object.keys(data.choices).length > 0 ?
<Table bordered className='mt-5'> <Table bordered className='mt-5'>
@ -419,115 +468,117 @@ function App() {
: <div className='mt-5'><i>Zatím nikdo nehlasoval...</i></div> : <div className='mt-5'><i>Zatím nikdo nehlasoval...</i></div>
} }
</div> </div>
<div className='mt-5'> {dayIndex === currentDayIndex &&
{!data.pizzaDay && <div className='mt-5'>
<div style={{ textAlign: 'center' }}> {!data.pizzaDay &&
<p>Pro dnešní den není aktuálně založen Pizza day.</p>
<Button onClick={async () => {
await createPizzaDay();
}}>Založit Pizza day</Button>
</div>
}
{data.pizzaDay &&
<div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<h3>Pizza day</h3> <p>Pro dnešní den není aktuálně založen Pizza day.</p>
{ <Button onClick={async () => {
data.pizzaDay.state === PizzaDayState.CREATED && await createPizzaDay();
<div> }}>Založit Pizza day</Button>
<p> </div>
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br /> }
Můžete upravovat své objednávky. {data.pizzaDay &&
</p> <div>
{ <div style={{ textAlign: 'center' }}>
data.pizzaDay.creator === auth.login && <h3>Pizza day</h3>
<> {
<Button className='danger mb-3' title="Smaže kompletně pizza day, včetně dosud zadaných objednávek." onClick={async () => { data.pizzaDay.state === PizzaDayState.CREATED &&
await deletePizzaDay(); <div>
}}>Smazat Pizza day</Button> <p>
<Button className='mb-3' style={{ marginLeft: '20px' }} title={noOrders ? "Nelze uzamknout - neexistuje žádná objednávka" : "Zamezí přidávat/odebírat objednávky. Použij před samotným objednáním, aby již nemohlo docházet ke změnám."} disabled={noOrders} onClick={async () => { Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
await lockPizzaDay(); Můžete upravovat své objednávky.
}}>Uzamknout</Button> </p>
</> {
} data.pizzaDay.creator === auth.login &&
</div> <>
} <Button className='danger mb-3' title="Smaže kompletně pizza day, včetně dosud zadaných objednávek." onClick={async () => {
{ await deletePizzaDay();
data.pizzaDay.state === PizzaDayState.LOCKED && }}>Smazat Pizza day</Button>
<div> <Button className='mb-3' style={{ marginLeft: '20px' }} title={noOrders ? "Nelze uzamknout - neexistuje žádná objednávka" : "Zamezí přidávat/odebírat objednávky. Použij před samotným objednáním, aby již nemohlo docházet ke změnám."} disabled={noOrders} onClick={async () => {
<p>Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}</p> await lockPizzaDay();
{data.pizzaDay.creator === auth.login && }}>Uzamknout</Button>
<> </>
<Button className='danger mb-3' title="Umožní znovu editovat objednávky." onClick={async () => { }
await unlockPizzaDay(); </div>
}}>Odemknout</Button> }
{/* <Button className='danger mb-3' style={{ marginLeft: '20px' }} onClick={async () => { {
data.pizzaDay.state === PizzaDayState.LOCKED &&
<div>
<p>Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}</p>
{data.pizzaDay.creator === auth.login &&
<>
<Button className='danger mb-3' title="Umožní znovu editovat objednávky." onClick={async () => {
await unlockPizzaDay();
}}>Odemknout</Button>
{/* <Button className='danger mb-3' style={{ marginLeft: '20px' }} onClick={async () => {
await addToCart(); await addToCart();
}}>Přidat vše do košíku</Button> */} }}>Přidat vše do košíku</Button> */}
<Button className='danger mb-3' style={{ marginLeft: '20px' }} title={noOrders ? "Nelze objednat - neexistuje žádná objednávka" : "Použij po objednání. Objednávky zůstanou zamčeny."} disabled={noOrders} onClick={async () => { <Button className='danger mb-3' style={{ marginLeft: '20px' }} title={noOrders ? "Nelze objednat - neexistuje žádná objednávka" : "Použij po objednání. Objednávky zůstanou zamčeny."} disabled={noOrders} onClick={async () => {
await finishOrder(); await finishOrder();
}}>Objednáno</Button> }}>Objednáno</Button>
</> </>
} }
</div>
}
{
data.pizzaDay.state === PizzaDayState.ORDERED &&
<div>
<p>Pizzy byly objednány uživatelem {data.pizzaDay.creator}</p>
{data.pizzaDay.creator === auth.login &&
<div>
<Button className='danger mb-3' title="Vrátí stav do předchozího kroku (před objednáním)." onClick={async () => {
await lockPizzaDay();
}}>Vrátit do "uzamčeno"</Button>
<Button className='danger mb-3' style={{ marginLeft: '20px' }} title="Nastaví stav na 'Doručeno' - koncový stav." onClick={async () => {
await finishDelivery(bank?.bankAccount, bank?.holderName);
}}>Doručeno</Button>
</div>
}
</div>
}
{
data.pizzaDay.state === PizzaDayState.DELIVERED &&
<div>
<p>Pizzy byly doručeny. Objednávku můžete uhradit pomocí QR kódu níže.</p>
</div>
}
</div>
{data.pizzaDay.state === PizzaDayState.CREATED &&
<div style={{ textAlign: 'center' }}>
<SelectSearch
search={true}
options={pizzaSuggestions}
placeholder='Vyhledat pizzu...'
onChange={handlePizzaChange}
/>
Poznámka: <input ref={poznamkaRef} className='mt-3' type="text" onKeyDown={event => {
if (event.key === 'Enter') {
handlePoznamkaChange();
}
}} />
<Button
style={{ marginLeft: '20px' }}
disabled={!myOrder?.pizzaList?.length}
onClick={handlePoznamkaChange}>
Uložit
</Button>
</div> </div>
} }
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} />
{ {
data.pizzaDay.state === PizzaDayState.ORDERED && data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
<div> <div className='qr-code'>
<p>Pizzy byly objednány uživatelem {data.pizzaDay.creator}</p> <h3>QR platba</h3>
{data.pizzaDay.creator === auth.login && <div>Částka: {myOrder.totalPrice} </div>
<div> <img src={getQrUrl(auth.login)} alt='QR kód' />
<Button className='danger mb-3' title="Vrátí stav do předchozího kroku (před objednáním)." onClick={async () => { <p>Generování QR kódů je v experimentální fázi - doporučujeme si překontrolovat údaje před odesláním platby.</p>
await lockPizzaDay();
}}>Vrátit do "uzamčeno"</Button>
<Button className='danger mb-3' style={{ marginLeft: '20px' }} title="Nastaví stav na 'Doručeno' - koncový stav." onClick={async () => {
await finishDelivery(bank?.bankAccount, bank?.holderName);
}}>Doručeno</Button>
</div>
}
</div>
}
{
data.pizzaDay.state === PizzaDayState.DELIVERED &&
<div>
<p>Pizzy byly doručeny. Objednávku můžete uhradit pomocí QR kódu níže.</p>
</div> </div>
} }
</div> </div>
{data.pizzaDay.state === PizzaDayState.CREATED && }
<div style={{ textAlign: 'center' }}> </div>
<SelectSearch }
search={true}
options={pizzaSuggestions}
placeholder='Vyhledat pizzu...'
onChange={handlePizzaChange}
/>
Poznámka: <input ref={poznamkaRef} className='mt-3' type="text" onKeyDown={event => {
if (event.key === 'Enter') {
handlePoznamkaChange();
}
}} />
<Button
style={{ marginLeft: '20px' }}
disabled={!myOrder?.pizzaList?.length}
onClick={handlePoznamkaChange}>
Uložit
</Button>
</div>
}
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} />
{
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
<div className='qr-code'>
<h3>QR platba</h3>
<div>Částka: {myOrder.totalPrice} </div>
<img src={getQrUrl(auth.login)} alt='QR kód' />
<p>Generování QR kódů je v experimentální fázi - doporučujeme si překontrolovat údaje před odesláním platby.</p>
</div>
}
</div>
}
</div>
</div> </div>
</>} </>}
</div> </div>

View File

@ -2,13 +2,12 @@ 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, 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 dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { getQr } from "./qr"; import { getQr } from "./qr";
import { generateToken, getLogin, getTrusted, verify } from "./auth"; import { generateToken, getLogin, getTrusted, verify } from "./auth";
import { Food, Locations, Restaurants } from "../../types"; import { getDayOfWeekIndex } from "./utils";
import { downloadPizzy } from "./chefie";
const ENVIRONMENT = process.env.NODE_ENV || 'production'; const ENVIRONMENT = process.env.NODE_ENV || 'production';
dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); 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 -------------- // ----------- Metody nevyžadující token --------------
app.get("/api/whoami", (req, res) => { app.get("/api/whoami", (req, res) => {
@ -110,19 +131,14 @@ app.use((req, res, next) => {
/** Vrátí data pro aktuální den. */ /** Vrátí data pro aktuální den. */
app.get("/api/data", async (req, res) => { app.get("/api/data", async (req, res) => {
res.status(200).json(await getData()); let date = undefined;
}); if (req.query.dayIndex != null && typeof req.query.dayIndex === 'string') {
const index = parseInt(req.query.dayIndex);
/** Vrátí obědové menu pro dostupné podniky. */ if (!isNaN(index)) {
app.get("/api/food", async (req, res) => { date = getDateForWeekIndex(parseInt(req.query.dayIndex));
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),
} }
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. */ /** 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 login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req)); const trusted = getTrusted(parseToken(req));
if (req.body.locationIndex > -1) { 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); 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) => { app.post("/api/removeChoices", async (req, res) => {
const login = getLogin(parseToken(req)); 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); io.emit("message", data);
res.status(200).json(data); res.status(200).json(data);
}); });
app.post("/api/removeChoice", async (req, res) => { app.post("/api/removeChoice", async (req, res) => {
const login = getLogin(parseToken(req)); 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); io.emit("message", data);
res.status(200).json(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) => { app.post("/api/updateNote", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
if (req.body.note && req.body.note.length > 100) { 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) => { app.post("/api/changeDepartureTime", async (req, res) => {
const login = getLogin(parseToken(req)); 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); io.emit("message", data);
res.status(200).json(data); res.status(200).json(data);
}); });

387
server/src/mock.ts Normal file
View File

@ -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)];
}

View File

@ -1,6 +1,8 @@
import axios from "axios"; import axios from "axios";
import { load } from 'cheerio'; import { load } from 'cheerio';
import { Food } from "../../types"; 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 // 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'] 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(); 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. * Stáhne a vrátí aktuální HTML z dané URL.
* *
@ -65,32 +56,7 @@ const getHtml = async (url: string): Promise<any> => {
*/ */
export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean = false): Promise<Food[]> => { export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean = false): Promise<Food[]> => {
if (mock) { if (mock) {
return [ return getMenuSladovnickaMock(date);
{
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,
}
]
} }
const todayDayIndex = getDayOfWeekIndex(date); const todayDayIndex = getDayOfWeekIndex(date);
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend 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<Food[]> => { export const getMenuUMotliku = async (date: Date = new Date(), mock: boolean = false): Promise<Food[]> => {
if (mock) { if (mock) {
return [ return getMenuUMotlikuMock(date);
{
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,
},
]
} }
const todayDayIndex = getDayOfWeekIndex(date); const todayDayIndex = getDayOfWeekIndex(date);
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend 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) => { export const getMenuTechTower = async (date: Date = new Date(), mock: boolean = false) => {
if (mock) { if (mock) {
return [ return getMenuTechTowerMock(date);
{
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,
}
]
} }
const todayDayIndex = getDayOfWeekIndex(date); const todayDayIndex = getDayOfWeekIndex(date);
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend

View File

@ -1,31 +1,56 @@
import { formatDate, getHumanDate, getHumanTime, getIsWeekend } from "./utils"; import { formatDate, getDayOfWeekIndex, 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, Restaurants, Food, Menu } 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"; import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
import { downloadPizzy } from "./chefie"; import { downloadPizzy } from "./chefie";
import { getTodayMock } from "./mock";
const storage = getStorage(); const storage = getStorage();
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */ /** 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') { if (process.env.MOCK_DATA === 'true') {
return new Date('2023-05-31'); return new Date(getTodayMock());
} }
return new Date(); return new Date();
} }
/** Vrátí "prázdná" (implicitní) data, pokud ještě nikdo nehlasoval. */ /** Vrátí datum v aktuálním týdnu na základě předaného indexu (0 = pondělí). */
function getEmptyData(): ClientData { export const getDateForWeekIndex = (index: number) => {
return { date: getHumanDate(getToday()), isWeekend: getIsWeekend(getToday()), choices: {} }; 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<ClientData> { export async function getData(date?: Date): Promise<ClientData> {
return await storage.getData(formatDate(getToday())) || getEmptyData(); 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<ClientData> {
* @param date datum * @param date datum
* @param mock příznak, zda chceme pouze mock data * @param mock příznak, zda chceme pouze mock data
*/ */
export async function getRestaurantMenu(restaurant: Restaurants, date?: Date, mock?: boolean): Promise<Menu> { export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise<Menu> {
await initIfNeeded(); await initIfNeeded(date);
const today = formatDate(getToday()); const today = formatDate(date ?? getToday());
const clientData: ClientData = await storage.getData(today); const clientData: ClientData = await storage.getData(today);
if (!clientData.menus) { if (!clientData.menus) {
clientData.menus = {}; clientData.menus = {};
@ -78,6 +103,7 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date, mo
closed: false, closed: false,
food: [], food: [],
}; };
const mock = process.env.MOCK_DATA === 'true';
switch (restaurant) { switch (restaurant) {
case Restaurants.SLADOVNICKA: case Restaurants.SLADOVNICKA:
clientData.menus[restaurant].food = await getMenuSladovnicka(date, mock); clientData.menus[restaurant].food = await getMenuSladovnicka(date, mock);
@ -307,11 +333,11 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
return clientData; return clientData;
} }
export async function initIfNeeded() { export async function initIfNeeded(date?: Date) {
const today = formatDate(getToday()); const usedDate = formatDate(date ?? getToday());
const hasData = await storage.hasData(today); const hasData = await storage.hasData(usedDate);
if (!hasData) { 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 login login uživatele
* @param location vybrané "umístění" * @param location vybrané "umístění"
* @param date datum, ke kterému se volba vztahuje
* @returns * @returns
*/ */
export async function removeChoices(login: string, location: Locations) { export async function removeChoices(login: string, location: Locations, date?: Date) {
const today = formatDate(getToday()); const selectedDay = formatDate(date ?? getToday());
let data: ClientData = await storage.getData(today); 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 // TODO zajistit, že neověřený uživatel se stejným loginem nemůže mazat volby ověřeného
if (location in data.choices) { if (location in data.choices) {
if (login in data.choices[location]) { 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) { if (Object.keys(data.choices[location]).length === 0) {
delete data.choices[location] delete data.choices[location]
} }
await storage.setData(today, data); await storage.setData(selectedDay, data);
} }
} }
return data; return data;
@ -345,18 +372,19 @@ export async function removeChoices(login: string, location: Locations) {
* @param login login uživatele * @param login login uživatele
* @param location vybrané "umístění" * @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 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 * @returns
*/ */
export async function removeChoice(login: string, location: Locations, foodIndex: number) { export async function removeChoice(login: string, location: Locations, foodIndex: number, date?: Date) {
const today = formatDate(getToday()); const selectedDay = formatDate(date ?? getToday());
let data: ClientData = await storage.getData(today); let data: ClientData = await storage.getData(selectedDay);
// TODO řešit ověření uživatele // TODO řešit ověření uživatele
if (location in data.choices) { if (location in data.choices) {
if (login in data.choices[location]) { if (login in data.choices[location]) {
const index = data.choices[location][login].options.indexOf(foodIndex); const index = data.choices[location][login].options.indexOf(foodIndex);
if (index > -1) { if (index > -1) {
data.choices[location][login].options.splice(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 * @param login login uživatele
*/ */
async function removeChoiceIfPresent(login: string) { async function removeChoiceIfPresent(login: string, date: string) {
const today = formatDate(getToday()); let data: ClientData = await storage.getData(date);
let data: ClientData = await storage.getData(today);
for (const key of Object.keys(data.choices)) { for (const key of Object.keys(data.choices)) {
if (login in data.choices[key]) { if (login in data.choices[key]) {
delete data.choices[key][login]; delete data.choices[key][login];
if (Object.keys(data.choices[key]).length === 0) { if (Object.keys(data.choices[key]).length === 0) {
delete data.choices[key]; delete data.choices[key];
} }
await storage.setData(today, data); await storage.setData(date, data);
} }
} }
return data; return data;
@ -390,12 +417,13 @@ async function removeChoiceIfPresent(login: string) {
* @param location vybrané "umístění" * @param location vybrané "umístění"
* @param foodIndex volitelný index jídla v daném 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 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 * @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(); await initIfNeeded();
const today = formatDate(getToday()); const selectedDate = formatDate(date ?? getToday());
let data: ClientData = await storage.getData(today); let data: ClientData = await storage.getData(selectedDate);
// Ověření, že se neověřený užívatel nepokouší přepsat údaje ověřeného // Ověření, že se neověřený užívatel nepokouší přepsat údaje ověřeného
const locations = Object.values(data?.choices); const locations = Object.values(data?.choices);
let found = false; 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í // Pokud měníme pouze lokaci, mažeme případné předchozí
if (foodIndex == null) { if (foodIndex == null) {
data = await removeChoiceIfPresent(login); data = await removeChoiceIfPresent(login, selectedDate);
} }
if (!(location in data.choices)) { if (!(location in data.choices)) {
data.choices[location] = {}; 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)) { if (foodIndex != null && !data.choices[location][login].options.includes(foodIndex)) {
data.choices[location][login].options.push(foodIndex); data.choices[location][login].options.push(foodIndex);
} }
await storage.setData(today, data); await storage.setData(selectedDate, data);
return data; return data;
} }
@ -453,10 +481,11 @@ export async function updateNote(login: string, note?: string) {
* *
* @param login login uživatele * @param login login uživatele
* @param time preferovaný čas odchodu * @param time preferovaný čas odchodu
* @param date datum, ke kterému se čas vztahuje
*/ */
export async function updateDepartureTime(login: string, time?: string) { export async function updateDepartureTime(login: string, time?: string, date?: Date) {
const today = formatDate(getToday()); const selectedDate = formatDate(date ?? getToday());
let clientData: ClientData = await storage.getData(today); let clientData: ClientData = await storage.getData(selectedDate);
const found = Object.values(clientData.choices).find(location => login in location); const found = Object.values(clientData.choices).find(location => login in location);
// TODO validace, že se jedná o restauraci // TODO validace, že se jedná o restauraci
if (found) { if (found) {
@ -465,7 +494,7 @@ export async function updateDepartureTime(login: string, time?: string) {
} else { } else {
found[login].departureTime = time; found[login].departureTime = time;
} }
await storage.setData(today, clientData); await storage.setData(selectedDate, clientData);
} }
return clientData; return clientData;
} }

View File

@ -22,8 +22,19 @@ export function getHumanTime(time: Date) {
return `${currentHours}:${currentMinutes}`; 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. */ /** 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 index = getDayOfWeekIndex(date);
return dayName === 'sobota' || dayName === 'neděle' return index == 5 || index == 6;
} }

View File

@ -70,6 +70,7 @@ interface PizzaDay {
export interface ClientData { 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
weekIndex: number, // index aktuálního dne v týdnu (0-6)
choices: Choices, // seznam voleb choices: Choices, // seznam voleb
menus?: { [restaurant: string]: Menu }, // menu jednotlivých restaurací 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