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

This commit is contained in:
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}`;
}
export const getData = async () => {
return await api.get<any>('/api/data');
export const getData = async (dayIndex?: number) => {
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 () => {
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 }));
}
export const addChoice = async (locationIndex: number, foodIndex?: number) => {
return await api.post<any, any>('/api/addChoice', JSON.stringify({ locationIndex, foodIndex }));
export const addChoice = async (locationIndex: number, foodIndex?: number, dayIndex?: number) => {
return await api.post<any, any>('/api/addChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex }));
}
export const removeChoices = async (locationIndex: number) => {
return await api.post<any, any>('/api/removeChoices', JSON.stringify({ locationIndex }));
export const removeChoices = async (locationIndex: number, dayIndex?: number) => {
return await api.post<any, any>('/api/removeChoices', JSON.stringify({ locationIndex, dayIndex }));
}
export const removeChoice = async (locationIndex: number, foodIndex: number) => {
return await api.post<any, any>('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex }));
export const removeChoice = async (locationIndex: number, foodIndex: number, dayIndex?: number) => {
return await api.post<any, any>('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex }));
}
export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => {
@@ -84,6 +83,6 @@ export const login = async (login?: string) => {
return await api.post<any, any>('/api/login', JSON.stringify({ login }));
}
export const changeDepartureTime = async (login: string, time: string) => {
return await api.post<any, any>('/api/changeDepartureTime', JSON.stringify({ login, time }));
export const changeDepartureTime = async (login: string, time: string, dayIndex?: number) => {
return await api.post<any, any>('/api/changeDepartureTime', JSON.stringify({ login, time, dayIndex }));
}

View File

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

View File

@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket';
import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getFood, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updateNote } from './Api';
import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updateNote } from './Api';
import { useAuth } from './context/auth';
import Login from './Login';
import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
@@ -16,7 +16,7 @@ import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { useBank } from './context/bank';
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, Menu } from './types';
import Footer from './components/Footer';
import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
import { faChainBroken, faChevronLeft, faChevronRight, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
import Loader from './components/Loader';
const EVENT_CONNECT = "connect"
@@ -50,26 +50,42 @@ function App() {
const socket = useContext(SocketContext);
const choiceRef = useRef<HTMLSelectElement>(null);
const foodChoiceRef = useRef<HTMLSelectElement>(null);
const departureChoiceRef = useRef<HTMLSelectElement>(null);
const poznamkaRef = useRef<HTMLInputElement>(null);
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í
useEffect(() => {
if (!auth || !auth.login) {
return
}
getData().then(data => {
getData().then((data: ClientData) => {
setData(data);
}).catch(e => {
setFailure(true);
})
getFood().then(food => {
setFood(food);
setDayIndex(data.weekIndex);
dayIndexRef.current = data.weekIndex;
setFood(data.menus);
}).catch(e => {
setFailure(true);
})
}, [auth, auth?.login]);
// Přenačtení pro zvolený den
useEffect(() => {
if (!auth || !auth.login) {
return
}
getData(dayIndex).then((data: ClientData) => {
setData(data);
setFood(data.menus);
}).catch(e => {
setFailure(true);
})
}, [dayIndex]);
// Registrace socket eventů
useEffect(() => {
socket.on(EVENT_CONNECT, () => {
@@ -82,7 +98,10 @@ function App() {
});
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
// console.log("Přijata nová data ze socketu", newData);
setData(newData);
// Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený
if (dayIndexRef.current == null || newData.weekIndex === dayIndexRef.current) {
setData(newData);
}
});
return () => {
@@ -137,10 +156,16 @@ function App() {
}
}, [choiceRef.current?.value, food])
// Index v týdnu dnešního dne (0-6)
// TODO tohle má posílat server, klient je nespolehlivý
const currentDayIndex = useMemo(() => {
return (((new Date().getDay() - 1) % 7) + 7) % 7;
}, [])
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations);
if (auth?.login) {
await addChoice(index);
await addChoice(index, undefined, dayIndex);
if (foodChoiceRef.current?.value) {
foodChoiceRef.current.value = "";
}
@@ -152,14 +177,14 @@ function App() {
const restaurantKey = choiceRef.current.value;
if (auth?.login) {
const locationIndex = Object.keys(Locations).indexOf(restaurantKey as unknown as Locations);
await addChoice(locationIndex, Number(event.target.value));
await addChoice(locationIndex, Number(event.target.value), dayIndex);
}
}
}
const doRemoveChoices = async (locationKey: string) => {
if (auth?.login) {
await removeChoices(Number(locationKey));
await removeChoices(Number(locationKey), dayIndex);
// Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo
if (choiceRef?.current?.value) {
choiceRef.current.value = "";
@@ -172,7 +197,7 @@ function App() {
const doRemoveFoodChoice = async (locationKey: string, foodIndex: number) => {
if (auth?.login) {
await removeChoice(Number(locationKey), foodIndex);
await removeChoice(Number(locationKey), foodIndex, dayIndex);
if (choiceRef?.current?.value) {
choiceRef.current.value = "";
}
@@ -250,11 +275,25 @@ function App() {
const handleChangeDepartureTime = async (event: React.ChangeEvent<HTMLSelectElement>) => {
if (foodChoiceList?.length && choiceRef.current?.value) {
if (auth?.login) {
await changeDepartureTime(auth.login, event.target.value);
await changeDepartureTime(auth.login, event.target.value, dayIndex);
}
}
}
const handleDayChange = async (dayIndex: number) => {
setDayIndex(dayIndex);
dayIndexRef.current = dayIndex;
if (choiceRef?.current?.value) {
choiceRef.current.value = "";
}
if (foodChoiceRef?.current?.value) {
foodChoiceRef.current.value = "";
}
if (departureChoiceRef?.current?.value) {
departureChoiceRef.current.value = "";
}
}
const renderFoodTable = (name: string, menu: Menu) => {
let content;
if (menu?.closed) {
@@ -319,11 +358,19 @@ function App() {
<Alert variant={'primary'}>
Poslední změny:
<ul>
<li>Lépe vypadající a více vypovídající načítací obrazovky (ztráta spojení, chyba načtení apod.)</li>
<li>(Již brzy) možnost náhledu na další dny v týdnu</li>
<li>Možnost náhledu na celý týden a výběru na následující dny v týdnu</li>
<ul>
<li>Pizza day je možno založit pouze pro aktuální den</li>
</ul>
</ul>
</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'>
{renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])}
{renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])}
@@ -331,35 +378,37 @@ function App() {
</Row>
<div className='content-wrapper'>
<div className='content'>
<p>Jak to dnes vidíš s obědem?</p>
<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}>
{dayIndex == null || dayIndex >= currentDayIndex && <>
<p>{`Jak to ${dayIndex == null || dayIndex === currentDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<Form.Select ref={choiceRef} onChange={doAddChoice}>
<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={foodChoiceRef} onChange={handleChangeDepartureTime}>
<option></option>
{DEPARTURE_TIMES.map(time => <option key={time} value={time}>{time}</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>
{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 ?
<Table bordered className='mt-5'>
@@ -419,115 +468,117 @@ function App() {
: <div className='mt-5'><i>Zatím nikdo nehlasoval...</i></div>
}
</div>
<div className='mt-5'>
{!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>
{dayIndex === currentDayIndex &&
<div className='mt-5'>
{!data.pizzaDay &&
<div style={{ textAlign: 'center' }}>
<h3>Pizza day</h3>
{
data.pizzaDay.state === PizzaDayState.CREATED &&
<div>
<p>
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
Můžete upravovat své objednávky.
</p>
{
data.pizzaDay.creator === auth.login &&
<>
<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>
<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 () => {
await lockPizzaDay();
}}>Uzamknout</Button>
</>
}
</div>
}
{
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 () => {
<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' }}>
<h3>Pizza day</h3>
{
data.pizzaDay.state === PizzaDayState.CREATED &&
<div>
<p>
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
Můžete upravovat své objednávky.
</p>
{
data.pizzaDay.creator === auth.login &&
<>
<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>
<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 () => {
await lockPizzaDay();
}}>Uzamknout</Button>
</>
}
</div>
}
{
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();
}}>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 () => {
await finishOrder();
}}>Objednáno</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 () => {
await finishOrder();
}}>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>
}
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} />
{
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>
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>
{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>
}
<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>