CI / Generate TypeScript types (push) Successful in 13s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 26s
CI / Build client (push) Successful in 38s
CI / Playwright E2E tests (push) Successful in 1m29s
CI / Build and push Docker image (push) Successful in 43s
CI / Notify (push) Successful in 2s
626 lines
25 KiB
TypeScript
626 lines
25 KiB
TypeScript
import { InsufficientPermissions, PizzaDayConflictError, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
|
||
import getStorage from "./storage";
|
||
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova, StaleWeekError } from "./restaurants";
|
||
import { getTodayMock } from "./mock";
|
||
import { removeAllUserPizzas } from "./pizza";
|
||
import { getStores } from "./stores";
|
||
import { ClientData, DepartureTime, LunchChoice, LunchChoices, MealSlot, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, WeekMenu } from "../../types/gen/types.gen";
|
||
|
||
const storage = getStorage();
|
||
const MENU_PREFIX = 'menu';
|
||
|
||
function getDataKey(date: Date, slot?: MealSlot): string {
|
||
const base = formatDate(date);
|
||
return slot === MealSlot.EXTRA ? `${base}_extra` : base;
|
||
}
|
||
|
||
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
|
||
export function getToday(): Date {
|
||
if (process.env.MOCK_DATA === 'true') {
|
||
return getTodayMock();
|
||
}
|
||
return new Date();
|
||
}
|
||
|
||
/** Vrátí datum v aktuálním týdnu na základě předaného indexu (0 = pondělí). */
|
||
export const getDateForWeekIndex = (index: number) => {
|
||
if (index < 0 || index > 4) {
|
||
// Nechceme shodit server, vrátíme dnešek
|
||
console.log('Neplatný index dne v týdnu: ' + index);
|
||
return getToday();
|
||
}
|
||
const date = getToday();
|
||
date.setDate(date.getDate() - getDayOfWeekIndex(date) + index);
|
||
return date;
|
||
}
|
||
|
||
/** Vrátí "prázdná" (implicitní) data pro předaný den. */
|
||
export function getEmptyData(date?: Date): ClientData {
|
||
const usedDate = date || getToday();
|
||
return {
|
||
todayDayIndex: getDayOfWeekIndex(getToday()),
|
||
date: getHumanDate(usedDate),
|
||
isoDate: formatDate(usedDate),
|
||
isWeekend: getIsWeekend(usedDate),
|
||
dayIndex: getDayOfWeekIndex(usedDate),
|
||
choices: {},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán.
|
||
*/
|
||
export async function getData(date?: Date, slot?: MealSlot): Promise<ClientData> {
|
||
const clientData = await getClientData(date, slot);
|
||
if (slot === MealSlot.EXTRA) {
|
||
clientData.stores = await getStores();
|
||
} else {
|
||
clientData.menus = {
|
||
SLADOVNICKA: await getRestaurantMenu('SLADOVNICKA', date),
|
||
// UMOTLIKU: await getRestaurantMenu('UMOTLIKU', date),
|
||
TECHTOWER: await getRestaurantMenu('TECHTOWER', date),
|
||
ZASTAVKAUMICHALA: await getRestaurantMenu('ZASTAVKAUMICHALA', date),
|
||
SENKSERIKOVA: await getRestaurantMenu('SENKSERIKOVA', date),
|
||
}
|
||
// Očistí volby jídel odkazující na indexy, které v aktuálním menu již neexistují.
|
||
// Pokud se v odpovědi něco změnilo, promítne očistu i do uložených dat (self-heal).
|
||
if (pruneStaleFoodChoices(clientData.choices, clientData.menus)) {
|
||
await storage.updateData<ClientData>(getDataKey(date ?? getToday(), slot), (current) => {
|
||
const data = current ?? getEmptyData(date);
|
||
pruneStaleFoodChoices(data.choices, clientData.menus!);
|
||
return data;
|
||
});
|
||
}
|
||
}
|
||
return clientData;
|
||
}
|
||
|
||
/**
|
||
* Odstraní z voleb uživatelů indexy jídel, které v aktuálním menu daného podniku již neexistují.
|
||
*
|
||
* Nastává typicky poté, co podnik ze svého webu odebere menu předchozího týdne: aplikace pak
|
||
* pro daný den vyparsuje méně jídel než dříve a dříve uložený index (např. z „budoucího“ týdne)
|
||
* se stane neplatným. Bez očištění by klient spadl při přístupu na neexistující jídlo a data
|
||
* by zůstala nekonzistentní.
|
||
*
|
||
* Očista se provádí pouze u podniků, které mají v menu alespoň jedno jídlo – prázdné menu
|
||
* (zavřeno / neúspěšné načtení) volby záměrně nemaže, aby nedošlo ke ztrátě platných voleb.
|
||
*
|
||
* @param choices volby uživatelů
|
||
* @param menus menu podniků pro daný den
|
||
* @returns true, pokud došlo k odebrání alespoň jednoho indexu
|
||
*/
|
||
export function pruneStaleFoodChoices(choices: LunchChoices, menus: RestaurantDayMenuMap): boolean {
|
||
let mutated = false;
|
||
for (const restaurant of Object.keys(Restaurant) as Restaurant[]) {
|
||
const foodCount = menus[restaurant]?.food?.length ?? 0;
|
||
if (foodCount === 0) continue; // prázdné menu volby nemažeme
|
||
const locationChoices = choices[restaurant];
|
||
if (!locationChoices) continue;
|
||
for (const userChoice of Object.values(locationChoices)) {
|
||
const selected = userChoice.selectedFoods;
|
||
if (!selected?.length) continue;
|
||
const filtered = selected.filter(index => index < foodCount);
|
||
if (filtered.length !== selected.length) {
|
||
userChoice.selectedFoods = filtered;
|
||
mutated = true;
|
||
}
|
||
}
|
||
}
|
||
return mutated;
|
||
}
|
||
|
||
/**
|
||
* Vrátí klíč, pod kterým je uloženo menu pro týden příslušící předanému datu.
|
||
*
|
||
* @param date datum
|
||
* @returns databázový klíč
|
||
*/
|
||
export function getMenuKey(date: Date) {
|
||
const weekNumber = getWeekNumber(date);
|
||
return `${MENU_PREFIX}_${date.getFullYear()}_${weekNumber}`;
|
||
}
|
||
|
||
/**
|
||
* Vrátí menu všech podniků pro celý týden do kterého spadá předané datum, pokud již existují.
|
||
*
|
||
* @param date datum
|
||
* @returns menu restaurací pro týden příslušící předanému datu
|
||
*/
|
||
async function getMenu(date: Date): Promise<WeekMenu | undefined> {
|
||
return await storage.getData<WeekMenu | undefined>(getMenuKey(date));
|
||
}
|
||
|
||
// TODO přesun do restaurants.ts
|
||
/**
|
||
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
|
||
* Používá se pro validaci dat před uložením.
|
||
*
|
||
* @param restaurant restaurace
|
||
* @param firstDay první pracovní den týdne
|
||
* @returns pole menu pro jednotlivé dny týdne
|
||
*/
|
||
export async function fetchRestaurantWeekMenuData(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
|
||
return await fetchRestaurantWeekMenu(restaurant, firstDay);
|
||
}
|
||
|
||
/**
|
||
* Uloží týdenní menu restaurace do storage.
|
||
*
|
||
* @param restaurant restaurace
|
||
* @param date datum z týdne, pro který ukládat menu
|
||
* @param weekData data týdenního menu
|
||
*/
|
||
export async function saveRestaurantWeekMenu(restaurant: Restaurant, date: Date, weekData: any[]): Promise<void> {
|
||
const now = new Date().getTime();
|
||
let weekMenu = await getMenu(date);
|
||
weekMenu ??= [{}, {}, {}, {}, {}];
|
||
|
||
// Inicializace struktury pro restauraci
|
||
for (let i = 0; i < 5; i++) {
|
||
weekMenu[i] ??= {};
|
||
weekMenu[i][restaurant] ??= {
|
||
lastUpdate: now,
|
||
closed: false,
|
||
food: [],
|
||
};
|
||
}
|
||
|
||
// Uložení dat pro všechny dny
|
||
for (let i = 0; i < weekData.length && i < weekMenu.length; i++) {
|
||
weekMenu[i][restaurant]!.food = weekData[i];
|
||
weekMenu[i][restaurant]!.lastUpdate = now;
|
||
|
||
// Detekce uzavření pro každou restauraci
|
||
switch (restaurant) {
|
||
case 'SLADOVNICKA':
|
||
if (weekData[i].length === 1 && weekData[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
case 'TECHTOWER':
|
||
if (weekData[i]?.length === 1 && weekData[i][0].name.toLowerCase() === 'svátek') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
case 'ZASTAVKAUMICHALA':
|
||
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
case 'SENKSERIKOVA':
|
||
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Uložení do storage
|
||
await storage.setData(getMenuKey(date), weekMenu);
|
||
}
|
||
|
||
/**
|
||
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
|
||
*
|
||
* @param restaurant restaurace
|
||
* @param firstDay první pracovní den týdne
|
||
* @returns pole menu pro jednotlivé dny týdne
|
||
*/
|
||
async function fetchRestaurantWeekMenu(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
|
||
const mock = process.env.MOCK_DATA === 'true';
|
||
|
||
switch (restaurant) {
|
||
case 'SLADOVNICKA':
|
||
return await getMenuSladovnicka(firstDay, mock);
|
||
case 'TECHTOWER':
|
||
return await getMenuTechTower(firstDay, mock);
|
||
case 'ZASTAVKAUMICHALA':
|
||
return await getMenuZastavkaUmichala(firstDay, mock);
|
||
case 'SENKSERIKOVA':
|
||
return await getMenuSenkSerikova(firstDay, mock);
|
||
default:
|
||
throw new Error(`Nepodporovaná restaurace: ${restaurant}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vrátí menu dané restaurace pro předaný den.
|
||
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
|
||
*
|
||
* @param restaurant restaurace
|
||
* @param date datum, ke kterému získat menu
|
||
* @param forceRefresh příznak vynuceného obnovení
|
||
*/
|
||
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, forceRefresh = false): Promise<RestaurantDayMenu> {
|
||
const usedDate = date ?? getToday();
|
||
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
|
||
const now = new Date().getTime();
|
||
if (getIsWeekend(usedDate)) {
|
||
return {
|
||
lastUpdate: now,
|
||
closed: true,
|
||
food: [],
|
||
};
|
||
}
|
||
|
||
let weekMenu = await getMenu(usedDate);
|
||
weekMenu ??= [{}, {}, {}, {}, {}];
|
||
for (let i = 0; i < 5; i++) {
|
||
weekMenu[i] ??= {};
|
||
weekMenu[i][restaurant] ??= {
|
||
lastUpdate: now,
|
||
closed: false,
|
||
food: [],
|
||
};
|
||
}
|
||
const MENU_REFETCH_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||
const existingMenu = weekMenu[dayOfWeekIndex][restaurant];
|
||
const lastFetchExpired = !existingMenu?.lastUpdate ||
|
||
existingMenu.lastUpdate === now || // freshly initialized, never fetched
|
||
(now - existingMenu.lastUpdate) > MENU_REFETCH_TTL_MS;
|
||
const shouldFetch = forceRefresh ||
|
||
(!existingMenu?.food?.length && !existingMenu?.closed && lastFetchExpired);
|
||
if (shouldFetch) {
|
||
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
||
|
||
try {
|
||
const restaurantWeekFood = await fetchRestaurantWeekMenu(restaurant, firstDay);
|
||
|
||
// Aktualizace menu pro všechny dny
|
||
for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) {
|
||
weekMenu[i][restaurant]!.food = restaurantWeekFood[i];
|
||
weekMenu[i][restaurant]!.lastUpdate = now;
|
||
weekMenu[i][restaurant]!.isStale = false;
|
||
|
||
// Detekce uzavření pro každou restauraci
|
||
switch (restaurant) {
|
||
case 'SLADOVNICKA':
|
||
if (restaurantWeekFood[i].length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
case 'TECHTOWER':
|
||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'svátek') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
case 'ZASTAVKAUMICHALA':
|
||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
case 'SENKSERIKOVA':
|
||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
||
weekMenu[i][restaurant]!.closed = true;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Uložení do storage
|
||
await storage.setData(getMenuKey(usedDate), weekMenu);
|
||
} catch (e: any) {
|
||
if (e instanceof StaleWeekError) {
|
||
for (let i = 0; i < e.food.length && i < weekMenu.length; i++) {
|
||
weekMenu[i][restaurant]!.food = e.food[i];
|
||
weekMenu[i][restaurant]!.lastUpdate = now;
|
||
weekMenu[i][restaurant]!.isStale = true;
|
||
}
|
||
await storage.setData(getMenuKey(usedDate), weekMenu);
|
||
} else {
|
||
console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e);
|
||
}
|
||
}
|
||
}
|
||
const result = weekMenu[dayOfWeekIndex][restaurant]!;
|
||
result.warnings = generateMenuWarnings(result);
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Generuje varování o kvalitě/úplnosti dat menu restaurace.
|
||
*/
|
||
function generateMenuWarnings(menu: RestaurantDayMenu): string[] {
|
||
const warnings: string[] = [];
|
||
if (!menu.food?.length || menu.closed) {
|
||
return warnings;
|
||
}
|
||
if (menu.isStale) {
|
||
warnings.push('Data jsou z minulého týdne');
|
||
}
|
||
const hasSoup = menu.food.some(f => f.isSoup);
|
||
if (!hasSoup) {
|
||
warnings.push('Chybí polévka');
|
||
}
|
||
const missingPrice = menu.food.some(f => !f.isSoup && (!f.price || f.price.trim() === ''));
|
||
if (missingPrice) {
|
||
warnings.push('U některých jídel chybí cena');
|
||
}
|
||
return warnings;
|
||
}
|
||
|
||
/**
|
||
* Inicializuje výchozí data pro předané datum, nebo dnešek, pokud není datum předáno.
|
||
*
|
||
* @param date datum
|
||
*/
|
||
export async function initIfNeeded(date?: Date, slot?: MealSlot) {
|
||
const usedDate = getDataKey(date ?? getToday(), slot);
|
||
const hasData = await storage.hasData(usedDate);
|
||
if (!hasData) {
|
||
await storage.setData(usedDate, getEmptyData(date || getToday()));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Odstraní kompletně volbu uživatele (včetně případných podřízených jídel).
|
||
*
|
||
* @param login login uživatele
|
||
* @param trusted příznak, zda se jedná o ověřeného uživatele
|
||
* @param locationKey vybrané "umístění"
|
||
* @param date datum, ke kterému se volba vztahuje
|
||
* @returns
|
||
*/
|
||
export async function removeChoices(login: string, trusted: boolean, locationKey: LunchChoice, date?: Date, slot?: MealSlot) {
|
||
const selectedDay = getDataKey(date ?? getToday(), slot);
|
||
// Validate trusted flag against current data before atomic update
|
||
const snapshot = await getClientData(date, slot);
|
||
validateTrusted(snapshot, login, trusted);
|
||
return storage.updateData<ClientData>(selectedDay, (current) => {
|
||
const data = current ?? getEmptyData(date);
|
||
if (locationKey in data.choices && data.choices[locationKey] && login in data.choices[locationKey]) {
|
||
delete data.choices[locationKey][login];
|
||
if (Object.keys(data.choices[locationKey]).length === 0) delete data.choices[locationKey];
|
||
}
|
||
return data;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Odstraní konkrétní volbu jídla uživatele.
|
||
* Neodstraňuje volbu samotnou, k tomu slouží {@link removeChoices}.
|
||
*
|
||
* @param login login uživatele
|
||
* @param trusted příznak, zda se jedná o ověřeného uživatele
|
||
* @param locationKey vybrané "umístění"
|
||
* @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
|
||
*/
|
||
export async function removeChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex: number, date?: Date, slot?: MealSlot) {
|
||
const selectedDay = getDataKey(date ?? getToday(), slot);
|
||
const snapshot = await getClientData(date, slot);
|
||
validateTrusted(snapshot, login, trusted);
|
||
return storage.updateData<ClientData>(selectedDay, (current) => {
|
||
const data = current ?? getEmptyData(date);
|
||
if (locationKey in data.choices && data.choices[locationKey] && login in data.choices[locationKey]) {
|
||
const index = data.choices[locationKey][login].selectedFoods?.indexOf(foodIndex);
|
||
if (index != null && index > -1) data.choices[locationKey][login].selectedFoods?.splice(index, 1);
|
||
}
|
||
return data;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Odstraní kompletně volbu uživatele, vyjma ignoredLocationKey (pokud byla předána a existuje).
|
||
*
|
||
* @param login login uživatele
|
||
* @param date datum, ke kterému se volby vztahují
|
||
* @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje
|
||
*/
|
||
async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: LunchChoice, slot?: MealSlot) {
|
||
const usedDate = date ?? getToday();
|
||
let data = await getClientData(usedDate, slot);
|
||
for (const key of Object.keys(data.choices)) {
|
||
const locationKey = key as LunchChoice;
|
||
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
|
||
continue;
|
||
}
|
||
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
||
delete data.choices[locationKey][login];
|
||
if (Object.keys(data.choices[locationKey]).length === 0) {
|
||
delete data.choices[locationKey];
|
||
}
|
||
await storage.setData(getDataKey(usedDate, slot), data);
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
|
||
/**
|
||
* Ověří, zda se neověřený uživatel nepokouší přepsat údaje ověřeného a případně vyhodí chybu.
|
||
*
|
||
* @param data aktuální klientská data
|
||
* @param login přihlašovací jméno uživatele
|
||
* @param trusted příznak, zda se jedná o ověřeného uživatele
|
||
*/
|
||
function validateTrusted(data: ClientData, login: string, trusted: boolean) {
|
||
const locations = Object.values(data?.choices);
|
||
let found = false;
|
||
if (!trusted) {
|
||
for (const location of locations) {
|
||
if (Object.keys(location).includes(login) && location[login].trusted) {
|
||
found = true;
|
||
}
|
||
}
|
||
}
|
||
if (!trusted && found) {
|
||
throw new InsufficientPermissions("Nelze změnit volbu ověřeného uživatele");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Přidá volbu uživatele.
|
||
*
|
||
* @param login login uživatele
|
||
* @param trusted příznak, zda se jedná o ověřeného uživatele
|
||
* @param locationKey vybrané "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 date datum, ke kterému se volba vztahuje
|
||
* @returns aktuální data
|
||
*/
|
||
export async function addChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex?: number, date?: Date, slot?: MealSlot) {
|
||
const usedDate = date ?? getToday();
|
||
await initIfNeeded(usedDate, slot);
|
||
let data = await getClientData(usedDate, slot);
|
||
validateTrusted(data, login, trusted);
|
||
await validateFoodIndex(locationKey, foodIndex, date);
|
||
|
||
if (!slot || slot === MealSlot.OBED) {
|
||
// 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, slot);
|
||
}
|
||
}
|
||
|
||
// Pokud měníme pouze lokaci, mažeme případné předchozí
|
||
if (foodIndex == null) {
|
||
data = await removeChoiceIfPresent(login, usedDate, undefined, slot);
|
||
} else {
|
||
// Mažeme případné ostatní volby (měla by být maximálně jedna)
|
||
data = await removeChoiceIfPresent(login, usedDate, locationKey, slot);
|
||
}
|
||
// TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
|
||
data.choices[locationKey] ??= {};
|
||
if (!(login in data.choices[locationKey])) {
|
||
if (!data.choices[locationKey]) {
|
||
data.choices[locationKey] = {}
|
||
}
|
||
data.choices[locationKey][login] = {
|
||
trusted,
|
||
selectedFoods: []
|
||
};
|
||
}
|
||
if (foodIndex != null && !data.choices[locationKey][login].selectedFoods?.includes(foodIndex)) {
|
||
data.choices[locationKey][login].selectedFoods?.push(foodIndex);
|
||
}
|
||
await storage.setData(getDataKey(usedDate, slot), data);
|
||
return data;
|
||
}
|
||
|
||
/**
|
||
* Zvaliduje platnost indexu jídla pro vybranou lokalitu a datum.
|
||
*
|
||
* @param locationKey vybraná lokalita
|
||
* @param foodIndex index jídla pro danou lokalitu
|
||
* @param date datum, pro které je validace prováděna
|
||
*/
|
||
async function validateFoodIndex(locationKey: LunchChoice, foodIndex?: number, date?: Date) {
|
||
if (foodIndex != null) {
|
||
if (typeof foodIndex !== 'number') {
|
||
throw new Error(`Neplatný index ${foodIndex} typu ${typeof foodIndex}`);
|
||
}
|
||
if (foodIndex < 0) {
|
||
throw new Error(`Neplatný index ${foodIndex}`);
|
||
}
|
||
if (!Object.keys(Restaurant).includes(locationKey)) {
|
||
throw new Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey} nepodporující indexy`);
|
||
}
|
||
const usedDate = date ?? getToday();
|
||
const menu = await getRestaurantMenu(locationKey as Restaurant, usedDate);
|
||
if (menu.food?.length && foodIndex > (menu.food.length - 1)) {
|
||
throw new Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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, slot?: MealSlot) {
|
||
const usedDate = date ?? getToday();
|
||
await initIfNeeded(usedDate, slot);
|
||
const snapshot = await getClientData(usedDate, slot);
|
||
validateTrusted(snapshot, login, trusted);
|
||
return storage.updateData<ClientData>(getDataKey(usedDate, slot), (current) => {
|
||
const data = current ?? getEmptyData(date);
|
||
const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null);
|
||
if (userEntry) {
|
||
if (!note?.length) delete userEntry[1][login].note;
|
||
else userEntry[1][login].note = note;
|
||
}
|
||
return data;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Aktualizuje preferovaný čas odchodu strávníka.
|
||
*
|
||
* @param login login uživatele
|
||
* @param time preferovaný čas odchodu
|
||
* @param date datum, ke kterému se čas vztahuje
|
||
*/
|
||
export async function updateDepartureTime(login: string, time?: string, date?: Date) {
|
||
const usedDate = date ?? getToday();
|
||
if (time?.length && !Object.values<string>(DepartureTime).includes(time)) {
|
||
throw Error(`Neplatný čas odchodu ${time}`);
|
||
}
|
||
return storage.updateData<ClientData>(getDataKey(usedDate), (current) => {
|
||
const data = current ?? getEmptyData(date);
|
||
const found = Object.values(data.choices).find(location => login in location);
|
||
if (found) {
|
||
if (!time?.length) delete found[login].departureTime;
|
||
else found[login].departureTime = time;
|
||
}
|
||
return data;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Nastaví/odnastaví uživatele jako objednatele pro dnešní den.
|
||
* Objednatelů může být více.
|
||
*
|
||
* @param login přihlašovací jméno uživatele
|
||
*/
|
||
export async function updateBuyer(login: string, slot?: MealSlot) {
|
||
const usedDate = getToday();
|
||
return storage.updateData<ClientData>(getDataKey(usedDate, slot), (current) => {
|
||
const data = current ?? getEmptyData();
|
||
const userEntry = data.choices?.['OBJEDNAVAM']?.[login];
|
||
if (!userEntry) throw new Error("Nelze nastavit objednatele pro uživatele s jinou volbou než \"Budu objednávat\"");
|
||
userEntry.isBuyer = !(userEntry.isBuyer || false);
|
||
return data;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Vrátí data pro klienta pro předaný nebo aktuální den.
|
||
*
|
||
* @param date datum pro který vrátit data, pokud není vyplněno, je použit dnešní den
|
||
* @returns data pro klienta
|
||
*/
|
||
export async function getClientData(date?: Date, slot?: MealSlot): Promise<ClientData> {
|
||
const targetDate = date ?? getToday();
|
||
const dateString = getDataKey(targetDate, slot);
|
||
const clientData = await storage.getData<ClientData>(dateString) || getEmptyData(date);
|
||
return {
|
||
...clientData,
|
||
todayDayIndex: getDayOfWeekIndex(getToday()),
|
||
isoDate: formatDate(targetDate),
|
||
...(slot === MealSlot.EXTRA ? { slot: MealSlot.EXTRA } : {}),
|
||
}
|
||
} |