feat: vylepšení Pizza day
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
This commit is contained in:
@@ -175,6 +175,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [auth?.login, data?.pizzaDay?.orders])
|
}, [auth?.login, data?.pizzaDay?.orders])
|
||||||
|
|
||||||
|
// Kontrola, zda má uživatel vybranou volbu PIZZA
|
||||||
|
const userHasPizzaChoice = useMemo(() => {
|
||||||
|
return auth?.login ? data?.choices?.PIZZA?.[auth.login] != null : false;
|
||||||
|
}, [data?.choices?.PIZZA, auth?.login]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (choiceRef?.current?.value && choiceRef.current.value !== "") {
|
if (choiceRef?.current?.value && choiceRef.current.value !== "") {
|
||||||
const locationKey = choiceRef.current.value as LunchChoice;
|
const locationKey = choiceRef.current.value as LunchChoice;
|
||||||
@@ -226,10 +231,65 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
|
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
|
||||||
|
|
||||||
|
// Pomocná funkce pro kontrolu a potvrzení změny volby při existujícím Pizza day
|
||||||
|
const checkPizzaDayBeforeChange = async (newLocationKey: LunchChoice): Promise<boolean> => {
|
||||||
|
if (!auth?.login || !data) return false;
|
||||||
|
|
||||||
|
// Kontrola, zda uživatel má vybranou PIZZA a mění na něco jiného
|
||||||
|
const hasPizzaChoice = data.choices?.PIZZA?.[auth.login] != null;
|
||||||
|
const isCreator = data.pizzaDay?.creator === auth.login;
|
||||||
|
const isPizzaDayCreated = data.pizzaDay?.state === PizzaDayState.CREATED;
|
||||||
|
|
||||||
|
// Pokud není vybraná PIZZA nebo přepínáme na PIZZA, není potřeba kontrolovat
|
||||||
|
if (!hasPizzaChoice || newLocationKey === LunchChoice.PIZZA) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pokud uživatel není zakladatel Pizza day, není potřeba dialogu
|
||||||
|
if (!isCreator || !data?.pizzaDay) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uživatel je zakladatel Pizza day a mění volbu z PIZZA
|
||||||
|
if (!isPizzaDayCreated) {
|
||||||
|
// Pizza day není ve stavu CREATED, nelze změnit volbu
|
||||||
|
alert(`Nelze změnit volbu. Pizza day je ve stavu "${data.pizzaDay.state}" a musí být nejprve dokončen.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pizza day je CREATED, zobrazit potvrzovací dialog
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
'Jsi zakladatel aktivního Pizza day. Změna volby smaže celý Pizza day včetně všech objednávek. Pokračovat?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uživatel potvrdil, smazat Pizza day
|
||||||
|
try {
|
||||||
|
await deletePizzaDay();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(`Chyba při mazání Pizza day: ${error.message || error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const doAddClickFoodChoice = async (location: LunchChoice, foodIndex?: number) => {
|
const doAddClickFoodChoice = async (location: LunchChoice, foodIndex?: number) => {
|
||||||
if (document.getSelection()?.type !== 'Range') { // pouze pokud se nejedná o výběr textu
|
if (document.getSelection()?.type !== 'Range') { // pouze pokud se nejedná o výběr textu
|
||||||
if (canChangeChoice && auth?.login) {
|
if (canChangeChoice && auth?.login) {
|
||||||
|
// Kontrola Pizza day před změnou volby
|
||||||
|
const canProceed = await checkPizzaDayBeforeChange(location);
|
||||||
|
if (!canProceed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await addChoice({ body: { locationKey: location, foodIndex, dayIndex } });
|
await addChoice({ body: { locationKey: location, foodIndex, dayIndex } });
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(`Chyba při změně volby: ${error.message || error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,11 +297,32 @@ function App() {
|
|||||||
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const locationKey = event.target.value as LunchChoice;
|
const locationKey = event.target.value as LunchChoice;
|
||||||
if (canChangeChoice && auth?.login) {
|
if (canChangeChoice && auth?.login) {
|
||||||
|
// Kontrola Pizza day před změnou volby
|
||||||
|
const canProceed = await checkPizzaDayBeforeChange(locationKey);
|
||||||
|
if (!canProceed) {
|
||||||
|
// Uživatel zrušil akci nebo došlo k chybě, reset výběru zpět na PIZZA
|
||||||
|
if (choiceRef.current) {
|
||||||
|
choiceRef.current.value = LunchChoice.PIZZA;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await addChoice({ body: { locationKey, dayIndex } });
|
await addChoice({ body: { locationKey, dayIndex } });
|
||||||
if (foodChoiceRef.current?.value) {
|
if (foodChoiceRef.current?.value) {
|
||||||
foodChoiceRef.current.value = "";
|
foodChoiceRef.current.value = "";
|
||||||
}
|
}
|
||||||
choiceRef.current?.blur();
|
choiceRef.current?.blur();
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(`Chyba při změně volby: ${error.message || error}`);
|
||||||
|
// Reset výběru zpět
|
||||||
|
const hasPizzaChoice = data?.choices?.PIZZA?.[auth.login] != null;
|
||||||
|
if (choiceRef.current && hasPizzaChoice) {
|
||||||
|
choiceRef.current.value = LunchChoice.PIZZA;
|
||||||
|
} else if (choiceRef.current) {
|
||||||
|
choiceRef.current.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +694,7 @@ function App() {
|
|||||||
: <div className='no-votes mt-4'>Zatím nikdo nehlasoval...</div>
|
: <div className='no-votes mt-4'>Zatím nikdo nehlasoval...</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{dayIndex === data.todayDayIndex &&
|
{dayIndex === data.todayDayIndex && userHasPizzaChoice &&
|
||||||
<div className='pizza-section fade-in'>
|
<div className='pizza-section fade-in'>
|
||||||
{!data.pizzaDay &&
|
{!data.pizzaDay &&
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dotenv from 'dotenv';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getQr } from "./qr";
|
import { getQr } from "./qr";
|
||||||
import { generateToken, getLogin, verify } from "./auth";
|
import { generateToken, getLogin, verify } from "./auth";
|
||||||
import { getIsWeekend, InsufficientPermissions, parseToken } from "./utils";
|
import { getIsWeekend, InsufficientPermissions, PizzaDayConflictError, parseToken } from "./utils";
|
||||||
import { getPendingQrs } from "./pizza";
|
import { getPendingQrs } from "./pizza";
|
||||||
import { initWebsocket } from "./websocket";
|
import { initWebsocket } from "./websocket";
|
||||||
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
||||||
@@ -168,6 +168,8 @@ app.use(express.static('public'));
|
|||||||
app.use((err: any, req: any, res: any, next: any) => {
|
app.use((err: any, req: any, res: any, next: any) => {
|
||||||
if (err instanceof InsufficientPermissions) {
|
if (err instanceof InsufficientPermissions) {
|
||||||
res.status(403).send({ error: err.message })
|
res.status(403).send({ error: err.message })
|
||||||
|
} else if (err instanceof PizzaDayConflictError) {
|
||||||
|
res.status(409).send({ error: err.message })
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send({ error: err.message })
|
res.status(500).send({ error: err.message })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,36 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize
|
|||||||
return clientData;
|
return clientData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Odstraní všechny pizzy uživatele (celou jeho objednávku).
|
||||||
|
* Pokud Pizza day neexistuje nebo není ve stavu CREATED, neudělá nic.
|
||||||
|
*
|
||||||
|
* @param login login uživatele
|
||||||
|
* @param date datum, pro které se objednávka odstraňuje (výchozí je dnešek)
|
||||||
|
* @returns aktuální data pro klienta
|
||||||
|
*/
|
||||||
|
export async function removeAllUserPizzas(login: string, date?: Date) {
|
||||||
|
const usedDate = date ?? getToday();
|
||||||
|
const today = formatDate(usedDate);
|
||||||
|
const clientData = await getClientData(usedDate);
|
||||||
|
|
||||||
|
if (!clientData.pizzaDay) {
|
||||||
|
return clientData; // Pizza day neexistuje, není co mazat
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientData.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||||
|
return clientData; // Pizza day není ve stavu CREATED, nelze mazat
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderIndex = clientData.pizzaDay.orders!.findIndex(o => o.customer === login);
|
||||||
|
if (orderIndex >= 0) {
|
||||||
|
clientData.pizzaDay.orders!.splice(orderIndex, 1);
|
||||||
|
await storage.setData(today, clientData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Odstraní danou objednávku pizzy.
|
* Odstraní danou objednávku pizzy.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
|
import { InsufficientPermissions, PizzaDayConflictError, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
|
||||||
import getStorage from "./storage";
|
import getStorage from "./storage";
|
||||||
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
|
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
|
||||||
import { getTodayMock } from "./mock";
|
import { getTodayMock } from "./mock";
|
||||||
import { ClientData, DepartureTime, LunchChoice, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
|
import { removeAllUserPizzas } from "./pizza";
|
||||||
|
import { ClientData, DepartureTime, LunchChoice, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
|
||||||
|
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
const MENU_PREFIX = 'menu';
|
const MENU_PREFIX = 'menu';
|
||||||
@@ -405,6 +406,29 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lu
|
|||||||
let data = await getClientData(usedDate);
|
let data = await getClientData(usedDate);
|
||||||
validateTrusted(data, login, trusted);
|
validateTrusted(data, login, trusted);
|
||||||
await validateFoodIndex(locationKey, foodIndex, date);
|
await validateFoodIndex(locationKey, foodIndex, date);
|
||||||
|
|
||||||
|
// Pokud uživatel měl vybranou PIZZA a mění na něco jiného
|
||||||
|
const hadPizzaChoice = data.choices.PIZZA && login in data.choices.PIZZA;
|
||||||
|
if (hadPizzaChoice && locationKey !== LunchChoice.PIZZA && foodIndex == null) {
|
||||||
|
// Kontrola, zda existuje Pizza day a uživatel je jeho zakladatel
|
||||||
|
if (data.pizzaDay && data.pizzaDay.creator === login) {
|
||||||
|
// Pokud Pizza day není ve stavu CREATED, nelze změnit volbu
|
||||||
|
if (data.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||||
|
throw new PizzaDayConflictError(
|
||||||
|
`Nelze změnit volbu. Pizza day je ve stavu "${data.pizzaDay.state}" a musí být nejprve dokončen nebo smazán.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Pizza day je ve stavu CREATED - bude smazán frontendem po potvrzení uživatelem
|
||||||
|
// (frontend volá nejprve deletePizzaDay, pak teprve addChoice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smažeme pizzy uživatele (pokud Pizza day nebyl založen tímto uživatelem,
|
||||||
|
// nebo byl již smazán frontendem)
|
||||||
|
await removeAllUserPizzas(login, usedDate);
|
||||||
|
// Znovu načteme data, protože removeAllUserPizzas je upravila
|
||||||
|
data = await getClientData(usedDate);
|
||||||
|
}
|
||||||
|
|
||||||
// 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, usedDate);
|
data = await removeChoiceIfPresent(login, usedDate);
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ export const checkBodyParams = (req: any, paramNames: string[]) => {
|
|||||||
// TODO umístit do samostatného souboru
|
// TODO umístit do samostatného souboru
|
||||||
export class InsufficientPermissions extends Error { }
|
export class InsufficientPermissions extends Error { }
|
||||||
|
|
||||||
|
export class PizzaDayConflictError extends Error { }
|
||||||
|
|
||||||
export const getUsersByLocation = (choices: LunchChoices, login?: string): string[] => {
|
export const getUsersByLocation = (choices: LunchChoices, login?: string): string[] => {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user