Compare commits

..

No commits in common. "832d3089ece2a09efdc30308503b4d1872a9b3e4" and "700a6980cabc398a323e16fb8692494ea975ad65" have entirely different histories.

10 changed files with 313 additions and 777 deletions

View File

@ -24,13 +24,14 @@ export const getQrUrl = (login: string) => {
return `${getBaseUrl()}/api/qr?login=${login}`; return `${getBaseUrl()}/api/qr?login=${login}`;
} }
export const getData = async (dayIndex?: number) => { export const getData = async () => {
let url = '/api/data'; return await api.get<any>('/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);
} }
@ -55,16 +56,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, dayIndex?: number) => { export const addChoice = async (locationIndex: number, foodIndex?: number) => {
return await api.post<any, any>('/api/addChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex })); return await api.post<any, any>('/api/addChoice', JSON.stringify({ locationIndex, foodIndex }));
} }
export const removeChoices = async (locationIndex: number, dayIndex?: number) => { export const removeChoices = async (locationIndex: number) => {
return await api.post<any, any>('/api/removeChoices', JSON.stringify({ locationIndex, dayIndex })); return await api.post<any, any>('/api/removeChoices', JSON.stringify({ locationIndex }));
} }
export const removeChoice = async (locationIndex: number, foodIndex: number, dayIndex?: number) => { export const removeChoice = async (locationIndex: number, foodIndex: number) => {
return await api.post<any, any>('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex })); return await api.post<any, any>('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex }));
} }
export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => { export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => {
@ -83,6 +84,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, dayIndex?: number) => { export const changeDepartureTime = async (login: string, time: string) => {
return await api.post<any, any>('/api/changeDepartureTime', JSON.stringify({ login, time, dayIndex })); return await api.post<any, any>('/api/changeDepartureTime', JSON.stringify({ login, time }));
} }

View File

@ -56,7 +56,7 @@
} }
.title { .title {
margin: 50px 30px; margin: 50px 0;
} }
.food-tables { .food-tables {
@ -116,10 +116,4 @@
.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, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updateNote } from './Api'; import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getFood, 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, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import { faChainBroken, faChevronLeft, faChevronRight, faGear, 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,42 +50,26 @@ 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: ClientData) => { getData().then(data => {
setData(data); setData(data);
setDayIndex(data.weekIndex); }).catch(e => {
dayIndexRef.current = data.weekIndex; setFailure(true);
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, () => {
@ -98,10 +82,7 @@ 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);
// Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený setData(newData);
if (dayIndexRef.current == null || newData.weekIndex === dayIndexRef.current) {
setData(newData);
}
}); });
return () => { return () => {
@ -156,16 +137,10 @@ 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, undefined, dayIndex); await addChoice(index);
if (foodChoiceRef.current?.value) { if (foodChoiceRef.current?.value) {
foodChoiceRef.current.value = ""; foodChoiceRef.current.value = "";
} }
@ -177,14 +152,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), dayIndex); await addChoice(locationIndex, Number(event.target.value));
} }
} }
} }
const doRemoveChoices = async (locationKey: string) => { const doRemoveChoices = async (locationKey: string) => {
if (auth?.login) { if (auth?.login) {
await removeChoices(Number(locationKey), dayIndex); await removeChoices(Number(locationKey));
// 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 = "";
@ -197,7 +172,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, dayIndex); await removeChoice(Number(locationKey), foodIndex);
if (choiceRef?.current?.value) { if (choiceRef?.current?.value) {
choiceRef.current.value = ""; choiceRef.current.value = "";
} }
@ -275,25 +250,11 @@ 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, dayIndex); await changeDepartureTime(auth.login, event.target.value);
} }
} }
} }
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) {
@ -358,19 +319,11 @@ function App() {
<Alert variant={'primary'}> <Alert variant={'primary'}>
Poslední změny: Poslední změny:
<ul> <ul>
<li>Možnost náhledu na celý týden a výběru na následující dny v týdnu</li> <li>Lépe vypadající a více vypovídající načítací obrazovky (ztráta spojení, chyba načtení apod.)</li>
<ul> <li>(Již brzy) možnost náhledu na další dny v týdnu</li>
<li>Pizza day je možno založit pouze pro aktuální den</li>
</ul>
</ul> </ul>
</Alert> </Alert>
{dayIndex != null && <h1 className='title'>Dnes je {data.date}</h1>
<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])}
@ -378,37 +331,35 @@ function App() {
</Row> </Row>
<div className='content-wrapper'> <div className='content-wrapper'>
<div className='content'> <div className='content'>
{dayIndex == null || dayIndex >= currentDayIndex && <> <p>Jak to dnes vidíš s obědem?</p>
<p>{`Jak to ${dayIndex == null || dayIndex === currentDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p> <Form.Select ref={choiceRef} onChange={doAddChoice}>
<Form.Select ref={choiceRef} onChange={doAddChoice}> <option></option>
{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>
{Object.entries(Locations) {foodChoiceList.map((food, index) => <option key={index} value={index}>{food.name}</option>)}
.filter(entry => { </Form.Select>
// TODO: wtf, cos pil, když jsi tohle psal? v2 </>}
const key = entry[0]; {foodChoiceList && !closed && <>
const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations); <p style={{ marginTop: "10px" }}>V kolik hodin preferuješ odchod?</p>
const locationsKey = Object.keys(Locations)[locationIndex]; <Form.Select ref={foodChoiceRef} onChange={handleChangeDepartureTime}>
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); <option></option>
const v = Object.values(Restaurants)[restaurantKey]; {DEPARTURE_TIMES.map(time => <option key={time} value={time}>{time}</option>)}
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'>
@ -468,117 +419,115 @@ 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>
{dayIndex === currentDayIndex && <div className='mt-5'>
<div className='mt-5'> {!data.pizzaDay &&
{!data.pizzaDay && <div style={{ textAlign: 'center' }}>
<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' }}>
<p>Pro dnešní den není aktuálně založen Pizza day.</p> <h3>Pizza day</h3>
<Button onClick={async () => { {
await createPizzaDay(); data.pizzaDay.state === PizzaDayState.CREATED &&
}}>Založit Pizza day</Button> <div>
</div> <p>
} Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
{data.pizzaDay && Můžete upravovat své objednávky.
<div> </p>
<div style={{ textAlign: 'center' }}> {
<h3>Pizza day</h3> data.pizzaDay.creator === auth.login &&
{ <>
data.pizzaDay.state === PizzaDayState.CREATED && <Button className='danger mb-3' title="Smaže kompletně pizza day, včetně dosud zadaných objednávek." onClick={async () => {
<div> await deletePizzaDay();
<p> }}>Smazat Pizza day</Button>
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br /> <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 () => {
Můžete upravovat své objednávky. await lockPizzaDay();
</p> }}>Uzamknout</Button>
{ </>
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(); {
}}>Smazat Pizza day</Button> data.pizzaDay.state === PizzaDayState.LOCKED &&
<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 () => { <div>
await lockPizzaDay(); <p>Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}</p>
}}>Uzamknout</Button> {data.pizzaDay.creator === auth.login &&
</> <>
} <Button className='danger mb-3' title="Umožní znovu editovat objednávky." onClick={async () => {
</div> await unlockPizzaDay();
} }}>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.DELIVERED && myOrder?.hasQr && data.pizzaDay.state === PizzaDayState.ORDERED &&
<div className='qr-code'> <div>
<h3>QR platba</h3> <p>Pizzy byly objednány uživatelem {data.pizzaDay.creator}</p>
<div>Částka: {myOrder.totalPrice} </div> {data.pizzaDay.creator === auth.login &&
<img src={getQrUrl(auth.login)} alt='QR kód' /> <div>
<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> <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>
} }
</div> </div>
} {data.pizzaDay.state === PizzaDayState.CREATED &&
</div> <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>
}
<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

@ -12,17 +12,15 @@ export default function Login() {
const loginRef = useRef<HTMLInputElement>(null); const loginRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
if (auth && !auth.login) { // Vyzkoušíme přihlášení "naprázdno", pokud projde, přihlásili nás trusted headers
// Vyzkoušíme přihlášení "naprázdno", pokud projde, přihlásili nás trusted headers login().then(token => {
login().then(token => { if (token) {
if (token) { auth?.setToken(token);
auth?.setToken(token); }
} }).catch(error => {
}).catch(error => { // nezajímá nás
// nezajímá nás });
}); }, []);
}
}, [auth]);
const doLogin = useCallback(async () => { const doLogin = useCallback(async () => {
const length = loginRef?.current?.value && loginRef?.current?.value.length && loginRef.current.value.replace(/\s/g, '').length const length = loginRef?.current?.value && loginRef?.current?.value.length && loginRef.current.value.replace(/\s/g, '').length

View File

@ -2,12 +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, getDateForWeekIndex, getPizzaList, getToday, lockPizzaDay, removeChoice, removeChoices, removePizzaOrder, 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 { getQr } from "./qr"; import { getQr } from "./qr";
import { generateToken, getLogin, getTrusted, verify } from "./auth"; import { generateToken, getLogin, getTrusted, verify } from "./auth";
import { getDayOfWeekIndex } from "./utils"; import { Food, Locations, Restaurants } from "../../types";
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}`) });
@ -50,28 +51,6 @@ 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) => {
@ -131,14 +110,19 @@ 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) => {
let date = undefined; res.status(200).json(await getData());
if (req.query.dayIndex != null && typeof req.query.dayIndex === 'string') { });
const index = parseInt(req.query.dayIndex);
if (!isNaN(index)) { /** Vrátí obědové menu pro dostupné podniky. */
date = getDateForWeekIndex(parseInt(req.query.dayIndex)); 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),
} }
res.status(200).json(await getData(date)); res.status(200).json(data);
}); });
/** 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. */
@ -223,58 +207,27 @@ 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) {
let date = undefined; const data = await addChoice(login, trusted, req.body.locationIndex, req.body.foodIndex);
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);
return res.status(200).json(data); res.status(200).json(data);
} }
return res.status(400); // TODO přidat popis chyby 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));
let date = undefined; const data = await removeChoices(login, req.body.locationIndex);
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));
let date = undefined; const data = await removeChoice(login, req.body.locationIndex, req.body.foodIndex);
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) {
@ -287,17 +240,7 @@ 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));
let date = undefined; const data = await updateDepartureTime(login, req.body?.time);
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);
}); });

View File

@ -1,387 +0,0 @@
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,8 +1,6 @@
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']
@ -37,6 +35,17 @@ 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.
* *
@ -56,7 +65,32 @@ 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 getMenuSladovnickaMock(date); 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,
}
]
} }
const todayDayIndex = getDayOfWeekIndex(date); const todayDayIndex = getDayOfWeekIndex(date);
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
@ -151,7 +185,33 @@ 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 getMenuUMotlikuMock(date); 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,
},
]
} }
const todayDayIndex = getDayOfWeekIndex(date); const todayDayIndex = getDayOfWeekIndex(date);
if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
@ -215,7 +275,26 @@ 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 getMenuTechTowerMock(date); 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,
}
]
} }
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,56 +1,31 @@
import { formatDate, getDayOfWeekIndex, getHumanDate, getHumanTime, 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, 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í. */
export function getToday(): Date { function getToday(): Date {
if (process.env.MOCK_DATA === 'true') { if (process.env.MOCK_DATA === 'true') {
return new Date(getTodayMock()); return new Date('2023-05-31');
} }
return new Date(); return new Date();
} }
/** Vrátí datum v aktuálním týdnu na základě předaného indexu (0 = pondělí). */ /** Vrátí "prázdná" (implicitní) data, pokud ještě nikdo nehlasoval. */
export const getDateForWeekIndex = (index: number) => { function getEmptyData(): ClientData {
if (index < 0 || index > 4) { return { date: getHumanDate(getToday()), isWeekend: getIsWeekend(getToday()), choices: {} };
// 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 předaný den, nebo aktuální den, pokud není předán. * Vrátí veškerá klientská data pro aktuální den.
*/ */
export async function getData(date?: Date): Promise<ClientData> { export async function getData(): Promise<ClientData> {
const dateString = formatDate(date ?? getToday()); return await storage.getData(formatDate(getToday())) || getEmptyData();
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;
} }
/** /**
@ -89,9 +64,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): Promise<Menu> { export async function getRestaurantMenu(restaurant: Restaurants, date?: Date, mock?: boolean): Promise<Menu> {
await initIfNeeded(date); await initIfNeeded();
const today = formatDate(date ?? getToday()); const today = formatDate(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 = {};
@ -103,7 +78,6 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P
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);
@ -333,11 +307,11 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
return clientData; return clientData;
} }
export async function initIfNeeded(date?: Date) { export async function initIfNeeded() {
const usedDate = formatDate(date ?? getToday()); const today = formatDate(getToday());
const hasData = await storage.hasData(usedDate); const hasData = await storage.hasData(today);
if (!hasData) { if (!hasData) {
await storage.setData(usedDate, getEmptyData(date || getToday())); await storage.setData(today, getEmptyData());
} }
} }
@ -346,12 +320,11 @@ export async function initIfNeeded(date?: Date) {
* *
* @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, date?: Date) { export async function removeChoices(login: string, location: Locations) {
const selectedDay = formatDate(date ?? getToday()); const today = formatDate(getToday());
let data: ClientData = await storage.getData(selectedDay); let data: ClientData = await storage.getData(today);
// 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]) {
@ -359,7 +332,7 @@ export async function removeChoices(login: string, location: Locations, date?: D
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(selectedDay, data); await storage.setData(today, data);
} }
} }
return data; return data;
@ -372,19 +345,18 @@ export async function removeChoices(login: string, location: Locations, date?: D
* @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, date?: Date) { export async function removeChoice(login: string, location: Locations, foodIndex: number) {
const selectedDay = formatDate(date ?? getToday()); const today = formatDate(getToday());
let data: ClientData = await storage.getData(selectedDay); let data: ClientData = await storage.getData(today);
// 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(selectedDay, data); await storage.setData(today, data);
} }
} }
} }
@ -396,15 +368,16 @@ export async function removeChoice(login: string, location: Locations, foodIndex
* *
* @param login login uživatele * @param login login uživatele
*/ */
async function removeChoiceIfPresent(login: string, date: string) { async function removeChoiceIfPresent(login: string) {
let data: ClientData = await storage.getData(date); const today = formatDate(getToday());
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(date, data); await storage.setData(today, data);
} }
} }
return data; return data;
@ -417,13 +390,12 @@ async function removeChoiceIfPresent(login: string, date: 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, date?: Date) { export async function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number) {
await initIfNeeded(); await initIfNeeded();
const selectedDate = formatDate(date ?? getToday()); const today = formatDate(getToday());
let data: ClientData = await storage.getData(selectedDate); let data: ClientData = await storage.getData(today);
// 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;
@ -439,7 +411,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, selectedDate); data = await removeChoiceIfPresent(login);
} }
if (!(location in data.choices)) { if (!(location in data.choices)) {
data.choices[location] = {}; data.choices[location] = {};
@ -453,7 +425,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(selectedDate, data); await storage.setData(today, data);
return data; return data;
} }
@ -481,11 +453,10 @@ 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, date?: Date) { export async function updateDepartureTime(login: string, time?: string) {
const selectedDate = formatDate(date ?? getToday()); const today = formatDate(getToday());
let clientData: ClientData = await storage.getData(selectedDate); let clientData: ClientData = await storage.getData(today);
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) {
@ -494,7 +465,7 @@ export async function updateDepartureTime(login: string, time?: string, date?: D
} else { } else {
found[login].departureTime = time; found[login].departureTime = time;
} }
await storage.setData(selectedDate, clientData); await storage.setData(today, clientData);
} }
return clientData; return clientData;
} }

View File

@ -22,19 +22,8 @@ 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 index = getDayOfWeekIndex(date); const dayName = date.toLocaleDateString("CZ-cs", { weekday: 'long' }).toLowerCase()
return index == 5 || index == 6; return dayName === 'sobota' || dayName === 'neděle'
} }

View File

@ -70,7 +70,6 @@ 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