Možnost zadání obecné poznámky k volbě

This commit is contained in:
Martin Berka 2024-03-04 23:35:58 +01:00
parent 44187bc316
commit 1e280e9d05
7 changed files with 122 additions and 17 deletions

View File

@ -12,15 +12,16 @@ import SelectSearch, { SelectedOptionValue } from 'react-select-search';
import 'react-select-search/style.css'; import 'react-select-search/style.css';
import './App.css'; import './App.css';
import { SelectSearchOption } from 'react-select-search'; import { SelectSearchOption } from 'react-select-search';
import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { useSettings } from './context/settings'; import { useSettings } from './context/settings';
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime } from './types'; import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime } 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, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
import Loader from './components/Loader'; import Loader from './components/Loader';
import { getData, errorHandler, getQrUrl } from './api/Api'; import { getData, errorHandler, getQrUrl } from './api/Api';
import { addChoice, removeChoices, removeChoice, changeDepartureTime, jdemeObed } from './api/FoodApi'; import { addChoice, removeChoices, removeChoice, changeDepartureTime, jdemeObed, updateNote } from './api/FoodApi';
import { getHumanDateTime } from './Utils'; import { getHumanDateTime } from './Utils';
import NoteModal from './components/modals/NoteModal';
const EVENT_CONNECT = "connect" const EVENT_CONNECT = "connect"
@ -37,10 +38,11 @@ function App() {
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 departureChoiceRef = useRef<HTMLSelectElement>(null);
const poznamkaRef = useRef<HTMLInputElement>(null); const pizzaPoznamkaRef = useRef<HTMLInputElement>(null);
const [failure, setFailure] = useState<boolean>(false); const [failure, setFailure] = useState<boolean>(false);
const [dayIndex, setDayIndex] = useState<number>(); const [dayIndex, setDayIndex] = useState<number>();
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false); const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
// Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu // 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 // https://medium.com/@kishorkrishna/cant-access-latest-state-inside-socket-io-listener-heres-how-to-fix-it-1522a5abebdb
const dayIndexRef = useRef<number | undefined>(dayIndex); const dayIndexRef = useRef<number | undefined>(dayIndex);
@ -210,6 +212,17 @@ function App() {
} }
} }
const saveNote = async (note?: string) => {
if (auth?.login) {
if (note != null && note.length > 70) {
alert("Poznámka může mít maximálně 70 znaků");
return;
}
await errorHandler(() => updateNote(note, dayIndex));
setNoteModalOpen(false);
}
}
const pizzaSuggestions = useMemo(() => { const pizzaSuggestions = useMemo(() => {
if (!data?.pizzaList) { if (!data?.pizzaList) {
return []; return [];
@ -243,12 +256,12 @@ function App() {
await removePizza(pizzaOrder); await removePizza(pizzaOrder);
} }
const handlePoznamkaChange = async () => { const handlePizzaPoznamkaChange = async () => {
if (poznamkaRef.current?.value && poznamkaRef.current.value.length > 100) { if (pizzaPoznamkaRef.current?.value && pizzaPoznamkaRef.current.value.length > 70) {
alert("Poznámka může mít maximálně 100 znaků"); alert("Poznámka může mít maximálně 70 znaků");
return; return;
} }
updatePizzaDayNote(poznamkaRef.current?.value); updatePizzaDayNote(pizzaPoznamkaRef.current?.value);
} }
// const addToCart = async () => { // const addToCart = async () => {
@ -360,10 +373,7 @@ function App() {
<Alert variant={'primary'}> <Alert variant={'primary'}>
Poslední změny: Poslední změny:
<ul> <ul>
<li>Možnost skrytí polévek</li> <li>Anděloviny</li>
<li>Možnost "Rozhoduji se"</li>
<li>Neuskakování data vlevo při přechodu na pondělí</li>
<li>Mělo tu toho být mnohem víc, ale zasekl jsem se snahou nahradit pizza search box</li>
</ul> </ul>
</Alert> </Alert>
{dayIndex != null && {dayIndex != null &&
@ -436,6 +446,10 @@ function App() {
</span>} </span>}
{login} {login}
{userPayload.departureTime && <small> ({userPayload.departureTime})</small>} {userPayload.departureTime && <small> ({userPayload.departureTime})</small>}
{userPayload.note && <small> ({userPayload.note})</small>}
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
setNoteModalOpen(true);
}} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />}
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => { {login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
doRemoveChoices(locationKey); doRemoveChoices(locationKey);
}} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />} }} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
@ -564,15 +578,15 @@ function App() {
placeholder='Vyhledat pizzu...' placeholder='Vyhledat pizzu...'
onChange={handlePizzaChange} onChange={handlePizzaChange}
/> />
Poznámka: <input ref={poznamkaRef} className='mt-3' type="text" onKeyDown={event => { Poznámka: <input ref={pizzaPoznamkaRef} className='mt-3' type="text" onKeyDown={event => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
handlePoznamkaChange(); handlePizzaPoznamkaChange();
} }
}} /> }} />
<Button <Button
style={{ marginLeft: '20px' }} style={{ marginLeft: '20px' }}
disabled={!myOrder?.pizzaList?.length} disabled={!myOrder?.pizzaList?.length}
onClick={handlePoznamkaChange}> onClick={handlePizzaPoznamkaChange}>
Uložit Uložit
</Button> </Button>
</div> </div>
@ -593,6 +607,7 @@ function App() {
</>} </>}
</div> </div>
<Footer /> <Footer />
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
</> </>
); );
} }

View File

@ -14,6 +14,10 @@ export const removeChoice = async (locationIndex: number, foodIndex: number, day
return await api.post<any, any>(`${FOOD_API_PREFIX}/removeChoice`, JSON.stringify({ locationIndex, foodIndex, dayIndex })); return await api.post<any, any>(`${FOOD_API_PREFIX}/removeChoice`, JSON.stringify({ locationIndex, foodIndex, dayIndex }));
} }
export const updateNote = async (note?: string, dayIndex?: number) => {
return await api.post<any, any>(`${FOOD_API_PREFIX}/updateNote`, JSON.stringify({ note, dayIndex }));
}
export const changeDepartureTime = async (time: string, dayIndex?: number) => { export const changeDepartureTime = async (time: string, dayIndex?: number) => {
return await api.post<any, any>(`${FOOD_API_PREFIX}/changeDepartureTime`, JSON.stringify({ time, dayIndex })); return await api.post<any, any>(`${FOOD_API_PREFIX}/changeDepartureTime`, JSON.stringify({ time, dayIndex }));
} }

View File

@ -0,0 +1,35 @@
import { useRef } from "react";
import { Modal, Button, Form } from "react-bootstrap"
type Props = {
isOpen: boolean,
onClose: () => void,
onSave: (note?: string) => void,
}
/** Modální dialog pro úpravu obecné poznámky. */
export default function NoteModal({ isOpen, onClose, onSave }: Props) {
const note = useRef<HTMLInputElement>(null);
const save = () => {
onSave(note?.current?.value);
}
return <Modal show={isOpen} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>Úprava poznámky</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Control ref={note} autoFocus={true} type="text" id="note" onKeyDown={event => {
if (event.key === 'Enter') {
save();
}
}} />
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={save}>
Uložit
</Button>
</Modal.Footer>
</Modal>
}

View File

@ -1,6 +1,6 @@
import express from "express"; import express from "express";
import { getLogin, getTrusted } from "../auth"; import { getLogin, getTrusted } from "../auth";
import { addChoice, addVolatileData, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime } from "../service"; import { addChoice, addVolatileData, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
import { getDayOfWeekIndex, parseToken } from "../utils"; import { getDayOfWeekIndex, parseToken } from "../utils";
import { getWebsocket } from "../websocket"; import { getWebsocket } from "../websocket";
import { callNotifikace } from "../notifikace"; import { callNotifikace } from "../notifikace";
@ -93,6 +93,30 @@ router.post("/removeChoice", async (req, res, next) => {
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });
router.post("/updateNote", async (req, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
const note = req.body.note;
if (note && note.length > 70) {
throw Error("Poznámka může mít maximálně 70 znaků");
}
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);
}
try {
const data = await updateNote(login, trusted, note, date);
getWebsocket().emit("message", await addVolatileData(data));
res.status(200).json(data);
} catch (e: any) { next(e) }
});
router.post("/changeDepartureTime", async (req, res, next) => { router.post("/changeDepartureTime", async (req, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
let date = undefined; let date = undefined;

View File

@ -87,8 +87,8 @@ router.post("/finishDelivery", async (req, res) => {
router.post("/updatePizzaDayNote", async (req, res) => { router.post("/updatePizzaDayNote", 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 > 70) {
throw Error("Poznámka může mít maximálně 100 znaků"); throw Error("Poznámka může mít maximálně 70 znaků");
} }
const data = await updatePizzaDayNote(login, req.body.note); const data = await updatePizzaDayNote(login, req.body.note);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", await addVolatileData(data));

View File

@ -317,6 +317,32 @@ export async function addChoice(login: string, trusted: boolean, location: Locat
return data; return data;
} }
/**
* Aktualizuje poznámku k aktuálně vybrané možnosti.
*
* @param login login uživatele
* @param trusted příznak, zda se jedná o ověřeného uživatele
* @param note poznámka
* @param date datum, ke kterému se volba vztahuje
*/
export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date) {
const usedDate = date ?? getToday();
await initIfNeeded(usedDate);
const selectedDate = formatDate(usedDate);
let data: DayData = await storage.getData(selectedDate);
validateTrusted(data, login, trusted);
const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null);
if (userEntry) {
if (!note || !note.length) {
delete userEntry[1][login].note;
} else {
userEntry[1][login].note = note;
}
await storage.setData(selectedDate, data);
}
return data;
}
/** /**
* Aktualizuje preferovaný čas odchodu strávníka. * Aktualizuje preferovaný čas odchodu strávníka.
* *

View File

@ -9,6 +9,7 @@ export interface FoodChoices {
trusted: boolean, trusted: boolean,
options: number[], options: number[],
departureTime?: string, departureTime?: string,
note?: string,
} }
export interface Choices { export interface Choices {