Stage 1: Příprava pro týdenní parser
This commit is contained in:
		
							parent
							
								
									74893c38eb
								
							
						
					
					
						commit
						546ee52ca0
					
				
							
								
								
									
										6
									
								
								server/babel.config.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										6
									
								
								server/babel.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| module.exports = { | ||||
|     presets: [ | ||||
|         ['@babel/preset-env', { targets: { node: 'current' } }], | ||||
|         '@babel/preset-typescript', | ||||
|     ], | ||||
| }; | ||||
| @ -7,13 +7,19 @@ | ||||
|   "scripts": { | ||||
|     "start": "ts-node src/index.ts", | ||||
|     "startReload": "nodemon src/index.ts", | ||||
|     "build": "tsc -p ." | ||||
|     "build": "tsc -p .", | ||||
|     "test": "jest" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "^7.23.0", | ||||
|     "@babel/preset-env": "^7.22.20", | ||||
|     "@babel/preset-typescript": "^7.23.0", | ||||
|     "@types/express": "^4.17.17", | ||||
|     "@types/jsonwebtoken": "^9.0.2", | ||||
|     "@types/node": "^20.2.5", | ||||
|     "@types/request-promise": "^4.1.48", | ||||
|     "babel-jest": "^29.7.0", | ||||
|     "jest": "^29.7.0", | ||||
|     "nodemon": "^2.0.22", | ||||
|     "ts-node": "^10.9.1", | ||||
|     "typescript": "^5.0.2" | ||||
|  | ||||
| @ -1124,16 +1124,16 @@ export const getTodayMock = () => { | ||||
|     return '2023-05-31'; // středa
 | ||||
| } | ||||
| 
 | ||||
| export const getMenuSladovnickaMock = (date: Date) => { | ||||
|     return MOCK_DATA['sladovnicka'][getDayOfWeekIndex(date)]; | ||||
| export const getMenuSladovnickaMock = () => { | ||||
|     return MOCK_DATA['sladovnicka']; | ||||
| } | ||||
| 
 | ||||
| export const getMenuUMotlikuMock = (date: Date) => { | ||||
|     return MOCK_DATA['uMotliku'][getDayOfWeekIndex(date)]; | ||||
| export const getMenuUMotlikuMock = () => { | ||||
|     return MOCK_DATA['uMotliku']; | ||||
| } | ||||
| 
 | ||||
| export const getMenuTechTowerMock = (date: Date) => { | ||||
|     return MOCK_DATA['techTower'][getDayOfWeekIndex(date)]; | ||||
| export const getMenuTechTowerMock = () => { | ||||
|     return MOCK_DATA['techTower']; | ||||
| } | ||||
| 
 | ||||
| export const getPizzaListMock = () => { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { formatDate } from "./utils"; | ||||
| import { callNotifikace } from "./notifikace"; | ||||
| import { generateQr } from "./qr"; | ||||
| import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder } from "../../types"; | ||||
| import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, DayData } from "../../types"; | ||||
| import getStorage from "./storage"; | ||||
| import { downloadPizzy } from "./chefie"; | ||||
| import { getToday, initIfNeeded } from "./service"; | ||||
| @ -15,7 +15,7 @@ const storage = getStorage(); | ||||
| export async function getPizzaList(): Promise<Pizza[] | undefined> { | ||||
|     await initIfNeeded(); | ||||
|     const today = formatDate(getToday()); | ||||
|     let clientData: ClientData = await storage.getData(today); | ||||
|     let clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaList) { | ||||
|         const mock = process.env.MOCK_DATA === 'true'; | ||||
|         clientData = await savePizzaList(await downloadPizzy(mock)); | ||||
| @ -31,7 +31,7 @@ export async function getPizzaList(): Promise<Pizza[] | undefined> { | ||||
| export async function savePizzaList(pizzaList: Pizza[]): Promise<ClientData> { | ||||
|     await initIfNeeded(); | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     clientData.pizzaList = pizzaList; | ||||
|     clientData.pizzaListLastUpdate = new Date(); | ||||
|     await storage.setData(today, clientData); | ||||
| @ -44,7 +44,7 @@ export async function savePizzaList(pizzaList: Pizza[]): Promise<ClientData> { | ||||
| export async function createPizzaDay(creator: string): Promise<ClientData> { | ||||
|     await initIfNeeded(); | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den již existuje"); | ||||
|     } | ||||
| @ -61,7 +61,7 @@ export async function createPizzaDay(creator: string): Promise<ClientData> { | ||||
|  */ | ||||
| export async function deletePizzaDay(login: string): Promise<ClientData> { | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -82,7 +82,7 @@ export async function deletePizzaDay(login: string): Promise<ClientData> { | ||||
|  */ | ||||
| export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize) { | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -118,7 +118,7 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize | ||||
|  */ | ||||
| export async function removePizzaOrder(login: string, pizzaOrder: PizzaOrder) { | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -149,7 +149,7 @@ export async function removePizzaOrder(login: string, pizzaOrder: PizzaOrder) { | ||||
|  */ | ||||
| export async function lockPizzaDay(login: string) { | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -172,7 +172,7 @@ export async function lockPizzaDay(login: string) { | ||||
|  */ | ||||
| export async function unlockPizzaDay(login: string) { | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -195,7 +195,7 @@ export async function unlockPizzaDay(login: string) { | ||||
|  */ | ||||
| export async function finishPizzaOrder(login: string) { | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -220,7 +220,7 @@ export async function finishPizzaOrder(login: string) { | ||||
|  */ | ||||
| export async function finishPizzaDelivery(login: string, bankAccount?: string, bankAccountHolder?: string) { | ||||
|     const today = formatDate(getToday()); | ||||
|     const clientData: ClientData = await storage.getData(today); | ||||
|     const clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -255,7 +255,7 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b | ||||
|  */ | ||||
| export async function updatePizzaDayNote(login: string, note?: string) { | ||||
|     const today = formatDate(getToday()); | ||||
|     let clientData: ClientData = await storage.getData(today); | ||||
|     let clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
| @ -282,7 +282,7 @@ export async function updatePizzaDayNote(login: string, note?: string) { | ||||
|  */ | ||||
| export async function updatePizzaFee(login: string, targetLogin: string, text?: string, price?: number) { | ||||
|     const today = formatDate(getToday()); | ||||
|     let clientData: ClientData = await storage.getData(today); | ||||
|     let clientData: DayData = await storage.getData(today); | ||||
|     if (!clientData.pizzaDay) { | ||||
|         throw Error("Pizza day pro dnešní den neexistuje"); | ||||
|     } | ||||
|  | ||||
| @ -48,27 +48,29 @@ const getHtml = async (url: string): Promise<any> => { | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Získá obědovou nabídku Sladovnické pro předané datum. | ||||
|  * Získá obědovou nabídku Sladovnické pro jeden týden. | ||||
|  *  | ||||
|  * @param date datum, pro které získat menu | ||||
|  * @param firstDayOfWeek první den v týdnu, pro který získat menu | ||||
|  * @param mock zda vrátit mock data | ||||
|  * @returns seznam jídel pro dané datum | ||||
|  * @returns seznam jídel pro daný týden | ||||
|  */ | ||||
| export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean = false): Promise<Food[]> => { | ||||
| export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => { | ||||
|     if (mock) { | ||||
|         return getMenuSladovnickaMock(date); | ||||
|     } | ||||
|     const todayDayIndex = getDayOfWeekIndex(date); | ||||
|     if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
 | ||||
|         return []; | ||||
|         return getMenuSladovnickaMock(); | ||||
|     } | ||||
| 
 | ||||
|     const html = await getHtml(SLADOVNICKA_URL); | ||||
|     const $ = load(html); | ||||
| 
 | ||||
|     const list = $('ul.tab-links').children(); | ||||
|     const result: Food[][] = []; | ||||
|     for (let dayIndex = 0; dayIndex < 5; dayIndex++) { | ||||
|         const currentDate = new Date(firstDayOfWeek); | ||||
|         currentDate.setDate(firstDayOfWeek.getDate() + dayIndex); | ||||
|         const searchedDayText = `${currentDate.getDate()}.${currentDate.getMonth() + 1}.${capitalize(DAYS_IN_WEEK[dayIndex])}`; | ||||
|         // Najdeme index pro vstupní datum (např. při svátcích bude posunutý)
 | ||||
|         // TODO validovat, že vstupní datum je v aktuálním týdnu
 | ||||
|         // TODO tenhle způsob je zbytečně komplikovaný - stačilo by hledat rovnou v div.tab-content, protože každý den tam má datum taky (akorát je print-only)
 | ||||
|     const list = $('ul.tab-links').children(); | ||||
|     const searchedDayText = `${date.getDate()}.${date.getMonth() + 1}.${capitalize(DAYS_IN_WEEK[todayDayIndex])}`; | ||||
|         let index = undefined; | ||||
|         list.each((i, dayRow) => { | ||||
|             const rowText = $(dayRow).first().text().trim(); | ||||
| @ -79,12 +81,13 @@ export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean | ||||
|         }) | ||||
|         if (index === undefined) { | ||||
|             // Pravděpodobně svátek, nebo je zavřeno
 | ||||
|         return [{ | ||||
|             result[dayIndex] = [{ | ||||
|                 amount: undefined, | ||||
|                 name: "Pro daný den nebyla nalezena denní nabídka", | ||||
|                 price: "", | ||||
|                 isSoup: false, | ||||
|             }]; | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         // Dle dohledaného indexu najdeme správný tabpanel
 | ||||
| @ -109,13 +112,13 @@ export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean | ||||
|         if (tables.length !== 2) { | ||||
|             throw Error("Neočekávaný počet tabulek na řádce nalezeného dne: " + tables.length + ", ale očekávány byly 2"); | ||||
|         } | ||||
|     const results: Food[] = []; | ||||
|         const currentDayFood: Food[] = []; | ||||
|         // Polévka - div -> table -> tbody -> tr -> 3x td
 | ||||
|         const soupCells = $(tables.get(0)).children().first().children().first().children(); | ||||
|         if (soupCells.length !== 3) { | ||||
|             throw Error("Neočekávaný počet buněk v tabulce polévky: " + soupCells.length + ", ale očekávány byly 3"); | ||||
|         } | ||||
|     results.push({ | ||||
|         currentDayFood.push({ | ||||
|             amount: sanitizeText($(soupCells.get(0)).text()), | ||||
|             name: sanitizeText($(soupCells.get(1)).text()), | ||||
|             price: sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0')), | ||||
| @ -123,60 +126,61 @@ export const getMenuSladovnicka = async (date: Date = new Date(), mock: boolean | ||||
|         }); | ||||
|         // Hlavní jídla - div -> table -> tbody -> 3x tr
 | ||||
|         const mainCourseRows = $(tables.get(1)).children().first().children(); | ||||
|     // Záměrně zakomentováno - občas je ve Sladovnické jídel méně
 | ||||
|     // if (mainCourseRows.length !== 3) {
 | ||||
|     //     throw Error("Neočekávaný počet řádek jídel: " + mainCourseRows.length + ", ale očekávány byly 3");
 | ||||
|     // }
 | ||||
|         mainCourseRows.each((i, foodRow) => { | ||||
|             const foodCells = $(foodRow).children(); | ||||
|             if (foodCells.length !== 3) { | ||||
|                 throw Error("Neočekávaný počet buněk v řádku jídla: " + foodCells.length + ", ale očekávány byly 3"); | ||||
|             } | ||||
|         results.push({ | ||||
|             currentDayFood.push({ | ||||
|                 amount: sanitizeText($(foodCells.get(0)).text()), | ||||
|                 name: sanitizeText($(foodCells.get(1)).text()), | ||||
|                 price: sanitizeText($(foodCells.get(2)).text().replace(' ', '\xA0')), | ||||
|                 isSoup: false, | ||||
|             }); | ||||
|         }) | ||||
|     return results; | ||||
|         result[index] = currentDayFood; | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Získá obědovou nabídku restaurace U Motlíků pro předané datum. | ||||
|  * Získá obědovou nabídku restaurace U Motlíků pro jeden týden. | ||||
|  *  | ||||
|  * @param date datum, pro které získat menu | ||||
|  * @param firstDayOfWeek první den v týdnu, pro který získat menu | ||||
|  * @param mock zda vrátit mock data | ||||
|  * @returns seznam jídel pro dané datum | ||||
|  */ | ||||
| export const getMenuUMotliku = async (date: Date = new Date(), mock: boolean = false): Promise<Food[]> => { | ||||
| export const getMenuUMotliku = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => { | ||||
|     if (mock) { | ||||
|         return getMenuUMotlikuMock(date); | ||||
|     } | ||||
|     const todayDayIndex = getDayOfWeekIndex(date); | ||||
|     if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
 | ||||
|         return []; | ||||
|         return getMenuUMotlikuMock(); | ||||
|     } | ||||
| 
 | ||||
|     const html = await getHtml(U_MOTLIKU_URL); | ||||
|     const $ = load(html); | ||||
| 
 | ||||
|     const table = $('table.table.table-hover.Xtable-striped').first(); | ||||
|     const body = table.children().first(); | ||||
|     const rows = body.children(); | ||||
|     const results: Food[] = []; | ||||
| 
 | ||||
|     const result: Food[][] = []; | ||||
|     for (let dayIndex = 0; dayIndex < 5; dayIndex++) { | ||||
|         if (!(dayIndex in result)) { | ||||
|             result[dayIndex] = []; | ||||
|         } | ||||
|         let parsing = false; | ||||
|         let isSoup = false; | ||||
|         rows.each((i, row) => { | ||||
|             const firstChild = $(row).children().get(0); | ||||
|             if (firstChild?.name == 'th') { | ||||
|                 const childText = $(firstChild).text(); | ||||
|             if (capitalize(DAYS_IN_WEEK[todayDayIndex]) === childText) { // Našli jsme dnešek
 | ||||
|                 if (capitalize(DAYS_IN_WEEK[dayIndex]) === childText) { | ||||
|                     parsing = true; | ||||
|                 } else if (parsing) { | ||||
|                     // Narazili jsme na další den - konec parsování
 | ||||
|                     parsing = false; | ||||
|                     return; | ||||
|                 } | ||||
|         } else if (parsing) { // Jsme aktuálně na dnešním dni
 | ||||
|             } else if (parsing) { | ||||
|                 const children = $(row).children(); | ||||
|                 if (children.length === 1) { // Nadpis "Polévka" nebo "Hlavní jídlo"
 | ||||
|                     const foodType = children.first().text(); | ||||
| @ -194,7 +198,7 @@ export const getMenuUMotliku = async (date: Date = new Date(), mock: boolean = f | ||||
|                     const amount = sanitizeText($(children.get(0)).text()); | ||||
|                     const name = sanitizeText($(children.get(1)).text()); | ||||
|                     const price = sanitizeText($(children.get(2)).text()).replace(',-', '').replace(' ', '\xA0'); | ||||
|                 results.push({ | ||||
|                     result[dayIndex].push({ | ||||
|                         amount, | ||||
|                         name, | ||||
|                         price, | ||||
| @ -203,26 +207,25 @@ export const getMenuUMotliku = async (date: Date = new Date(), mock: boolean = f | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|     return results; | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Získá obědovou nabídku TechTower pro předané datum. | ||||
|  * Získá obědovou nabídku TechTower pro jeden týden. | ||||
|  *  | ||||
|  * @param date datum, pro které získat menu | ||||
|  * @param firstDayOfWeek první den v týdnu, pro který získat menu | ||||
|  * @param mock zda vrátit mock data | ||||
|  * @returns seznam jídel pro dané datum | ||||
|  */ | ||||
| export const getMenuTechTower = async (date: Date = new Date(), mock: boolean = false) => { | ||||
| export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => { | ||||
|     if (mock) { | ||||
|         return getMenuTechTowerMock(date); | ||||
|     } | ||||
|     const todayDayIndex = getDayOfWeekIndex(date); | ||||
|     if (todayDayIndex == 5 || todayDayIndex == 6) { // Víkend
 | ||||
|         return []; | ||||
|         return getMenuTechTowerMock(); | ||||
|     } | ||||
| 
 | ||||
|     const html = await getHtml(TECHTOWER_URL); | ||||
|     const $ = load(html); | ||||
| 
 | ||||
|     const fonts = $('font.wsw-41'); | ||||
|     let font = undefined; | ||||
|     fonts.each((i, f) => { | ||||
| @ -236,11 +239,16 @@ export const getMenuTechTower = async (date: Date = new Date(), mock: boolean = | ||||
|     // TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
 | ||||
|     const siblings = $(font).parent().parent().siblings(); | ||||
|     let parsing = false; | ||||
|     const results: Food[] = []; | ||||
|     const result: Food[][] = []; | ||||
|     // TODO toto je kvůli poslednímu "línému" refaktoru neoptimální, stačilo by to projít jedním cyklem 
 | ||||
|     for (let dayIndex = 0; dayIndex < 5; dayIndex++) { | ||||
|         if (!(dayIndex in result)) { | ||||
|             result[dayIndex] = []; | ||||
|         } | ||||
|         for (let i = 0; i < siblings.length; i++) { | ||||
|             const text = $(siblings.get(i)).text().trim().replace('\t', '').replace('\n', ' '); | ||||
|             if (DAYS_IN_WEEK.includes(text)) { | ||||
|             if (text === DAYS_IN_WEEK[todayDayIndex]) { | ||||
|                 if (text === DAYS_IN_WEEK[dayIndex]) { | ||||
|                     // Našli jsme dnešní den, odtud začínáme parsovat jídla
 | ||||
|                     parsing = true; | ||||
|                     continue | ||||
| @ -261,7 +269,7 @@ export const getMenuTechTower = async (date: Date = new Date(), mock: boolean = | ||||
|                     price = `${split.slice(1)[0]}\xA0Kč` | ||||
|                     name = split[0] | ||||
|                 } | ||||
|             results.push({ | ||||
|                 result[dayIndex].push({ | ||||
|                     amount: '-', | ||||
|                     name, | ||||
|                     price, | ||||
| @ -269,5 +277,6 @@ export const getMenuTechTower = async (date: Date = new Date(), mock: boolean = | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|     return results; | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
| @ -1,10 +1,12 @@ | ||||
| import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getHumanDate, getHumanTime, getIsWeekend } from "./utils"; | ||||
| import { ClientData, Locations, Restaurants, Menu, DepartureTime } from "../../types"; | ||||
| import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getHumanTime, getIsWeekend, getLastWorkDayOfWeek, getWeekNumber } from "./utils"; | ||||
| import { ClientData, Locations, Restaurants, Menu, DepartureTime, DayData, WeekMenu } from "../../types"; | ||||
| import getStorage from "./storage"; | ||||
| import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants"; | ||||
| import { getTodayMock } from "./mock"; | ||||
| import { first } from "cheerio/lib/api/traversing"; | ||||
| 
 | ||||
| const storage = getStorage(); | ||||
| const MENU_PREFIX = 'menu'; | ||||
| 
 | ||||
| /** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */ | ||||
| export function getToday(): Date { | ||||
| @ -34,7 +36,7 @@ function getEmptyData(date?: Date): ClientData { | ||||
|         isWeekend: getIsWeekend(usedDate), | ||||
|         weekIndex: getDayOfWeekIndex(usedDate), | ||||
|         choices: {}, | ||||
|         departureTimes: Object.values(DepartureTime), | ||||
|         departureTimes: Object.values(DepartureTime), // TODO tohle zmizí, bude se přidávat do dat dynamicky
 | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| @ -42,21 +44,42 @@ function getEmptyData(date?: Date): ClientData { | ||||
|  * 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): Promise<ClientData> { | ||||
|     const dateString = formatDate(date ?? getToday()); | ||||
|     const data: ClientData = await storage.getData(dateString) || getEmptyData(date); | ||||
|     data.todayWeekIndex = getDayOfWeekIndex(getToday()); | ||||
|     const targetDate = date ?? getToday(); | ||||
|     const dateString = formatDate(targetDate); | ||||
|     const data: DayData = await storage.getData(dateString) || getEmptyData(date); | ||||
|     const clientData: ClientData = { ...data }; | ||||
|     clientData.todayWeekIndex = getDayOfWeekIndex(getToday()); | ||||
|     // 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()), | ||||
|             [Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, targetDate), | ||||
|             [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, targetDate), | ||||
|             [Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, targetDate), | ||||
|         } | ||||
|         await storage.setData(dateString, data); | ||||
|         clientData.menus = data.menus; | ||||
|     } | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Vrátí menu restaurací pro předané datum, pokud již existují. | ||||
|  * Jinak založí a vrátí prázdný objekt. | ||||
|  *  | ||||
|  * @param date datum | ||||
|  * @returns menu restaurací pro předané datum | ||||
|  */ | ||||
| async function getMenu(date: Date): Promise<WeekMenu> { | ||||
|     const weekNumber = getWeekNumber(date); | ||||
|     const menuKey = `${MENU_PREFIX}_${date.getFullYear()}_${weekNumber}`; | ||||
|     const menus: WeekMenu = await storage.getData(menuKey); | ||||
|     if (!menus) { | ||||
|         storage.setData(menuKey, {}); | ||||
|         return {}; | ||||
|     } | ||||
|     return menus; | ||||
| } | ||||
| 
 | ||||
| // TODO přesun do restaurants.ts
 | ||||
| /** | ||||
|  * Vrátí menu dané restaurace pro předaný den. Pokud neexistuje, provede jeho stažení a uložení do DB. | ||||
| @ -66,24 +89,27 @@ export async function getData(date?: Date): Promise<ClientData> { | ||||
|  * @param mock příznak, zda chceme pouze mock data | ||||
|  */ | ||||
| export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise<Menu> { | ||||
|     await initIfNeeded(date); | ||||
|     const selectedDay = formatDate(date ?? getToday()); | ||||
|     const clientData: ClientData = await storage.getData(selectedDay); | ||||
|     if (!clientData.menus) { | ||||
|         clientData.menus = {}; | ||||
|         storage.setData(selectedDay, clientData); | ||||
|     const usedDate = date ?? getToday(); | ||||
|     await initIfNeeded(usedDate); | ||||
|     const selectedDay = formatDate(usedDate); | ||||
|     const clientData: DayData = await storage.getData(selectedDay); | ||||
|     const weekNumber = getWeekNumber(usedDate); | ||||
|     const menus = await getMenu(usedDate); | ||||
|     if (!menus[weekNumber]) { | ||||
|         menus[weekNumber] = {}; | ||||
|     } | ||||
|     if (!clientData.menus[restaurant]) { | ||||
|         clientData.menus[restaurant] = { | ||||
|     if (!menus[weekNumber][restaurant]) { | ||||
|         menus[weekNumber][restaurant] = { | ||||
|             lastUpdate: getHumanTime(new Date()), | ||||
|             closed: false, | ||||
|             food: [], | ||||
|         }; | ||||
|         const firstDay = getFirstWorkDayOfWeek(usedDate); | ||||
|         const mock = process.env.MOCK_DATA === 'true'; | ||||
|         switch (restaurant) { | ||||
|             case Restaurants.SLADOVNICKA: | ||||
|                 const sladovnickaFood = await getMenuSladovnicka(date, mock); | ||||
|                 clientData.menus[restaurant]!.food = sladovnickaFood; | ||||
|                 const sladovnickaFood = await getMenuSladovnicka(firstDay, mock); | ||||
|                 menus[weekNumber][restaurant]!.food = sladovnickaFood; | ||||
|                 // Velice chatrný a nespolehlivý způsob detekce uzavření...
 | ||||
|                 if (sladovnickaFood.length === 1 && sladovnickaFood[0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') { | ||||
|                     clientData.menus[restaurant]!.closed = true; | ||||
| @ -106,7 +132,7 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P | ||||
|         } | ||||
|         storage.setData(selectedDay, clientData); | ||||
|     } | ||||
|     return clientData.menus[restaurant]!; | ||||
|     return menus[restaurant]!; | ||||
| } | ||||
| 
 | ||||
| export async function initIfNeeded(date?: Date) { | ||||
| @ -128,7 +154,7 @@ export async function initIfNeeded(date?: Date) { | ||||
|  */ | ||||
| export async function removeChoices(login: string, trusted: boolean, location: Locations, date?: Date) { | ||||
|     const selectedDay = formatDate(date ?? getToday()); | ||||
|     let data: ClientData = await storage.getData(selectedDay); | ||||
|     let data: DayData = await storage.getData(selectedDay); | ||||
|     validateTrusted(data, login, trusted); | ||||
|     if (location in data.choices) { | ||||
|         if (login in data.choices[location]) { | ||||
| @ -155,7 +181,7 @@ export async function removeChoices(login: string, trusted: boolean, location: L | ||||
|  */ | ||||
| export async function removeChoice(login: string, trusted: boolean, location: Locations, foodIndex: number, date?: Date) { | ||||
|     const selectedDay = formatDate(date ?? getToday()); | ||||
|     let data: ClientData = await storage.getData(selectedDay); | ||||
|     let data: DayData = await storage.getData(selectedDay); | ||||
|     validateTrusted(data, login, trusted); | ||||
|     if (location in data.choices) { | ||||
|         if (login in data.choices[location]) { | ||||
| @ -175,7 +201,7 @@ export async function removeChoice(login: string, trusted: boolean, location: Lo | ||||
|  * @param login login uživatele | ||||
|  */ | ||||
| async function removeChoiceIfPresent(login: string, date: string) { | ||||
|     let data: ClientData = await storage.getData(date); | ||||
|     let data: DayData = await storage.getData(date); | ||||
|     for (const key of Object.keys(data.choices)) { | ||||
|         if (login in data.choices[key]) { | ||||
|             delete data.choices[key][login]; | ||||
| @ -224,7 +250,7 @@ function validateTrusted(data: ClientData, login: string, trusted: boolean) { | ||||
| export async function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number, date?: Date) { | ||||
|     await initIfNeeded(); | ||||
|     const selectedDate = formatDate(date ?? getToday()); | ||||
|     let data: ClientData = await storage.getData(selectedDate); | ||||
|     let data: DayData = await storage.getData(selectedDate); | ||||
|     validateTrusted(data, login, trusted); | ||||
|     // Pokud měníme pouze lokaci, mažeme případné předchozí
 | ||||
|     if (foodIndex == null) { | ||||
| @ -255,7 +281,7 @@ export async function addChoice(login: string, trusted: boolean, location: Locat | ||||
|  */ | ||||
| export async function updateDepartureTime(login: string, time?: string, date?: Date) { | ||||
|     const selectedDate = formatDate(date ?? getToday()); | ||||
|     let clientData: ClientData = await storage.getData(selectedDate); | ||||
|     let clientData: DayData = await storage.getData(selectedDate); | ||||
|     const found = Object.values(clientData.choices).find(location => login in location); | ||||
|     // TODO validace, že se jedná o restauraci
 | ||||
|     if (found) { | ||||
|  | ||||
							
								
								
									
										159
									
								
								server/src/tests/dates.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										159
									
								
								server/src/tests/dates.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,159 @@ | ||||
| import { formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getLastWorkDayOfWeek, getWeekNumber } from "../utils"; | ||||
| 
 | ||||
| test('získání indexu dne v týdnu', () => { | ||||
|     let date = new Date("2023-10-01"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(6); | ||||
|     date = new Date("2023-10-02"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(0); | ||||
|     date = new Date("2023-10-03"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(1); | ||||
|     date = new Date("2023-10-04"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(2); | ||||
|     date = new Date("2023-10-05"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(3); | ||||
|     date = new Date("2023-10-06"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(4); | ||||
|     date = new Date("2023-10-07"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(5); | ||||
|     date = new Date("2023-10-08"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(6); | ||||
|     date = new Date("2023-10-09"); | ||||
|     expect(getDayOfWeekIndex(date)).toBe(0); | ||||
| }); | ||||
| 
 | ||||
| test('získání data prvního/posledního dne v týdnu', () => { | ||||
|     let date = new Date("2023-10-02"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-10-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-10-06"); | ||||
|     date = new Date("2023-10-03"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-10-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-10-06"); | ||||
|     date = new Date("2023-10-04"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-10-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-10-06"); | ||||
|     date = new Date("2023-10-05"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-10-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-10-06"); | ||||
|     date = new Date("2023-10-06"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-10-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-10-06"); | ||||
|     date = new Date("2023-10-07"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-10-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-10-06"); | ||||
|     date = new Date("2023-10-08"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-10-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-10-06"); | ||||
| 
 | ||||
|     date = new Date("2023-01-01"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2022-12-26"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2022-12-30"); | ||||
|     date = new Date("2023-01-02"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-01-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-01-06"); | ||||
|     date = new Date("2023-01-03"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-01-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-01-06"); | ||||
|     date = new Date("2023-01-04"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-01-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-01-06"); | ||||
|     date = new Date("2023-01-05"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-01-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-01-06"); | ||||
|     date = new Date("2023-01-06"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-01-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-01-06"); | ||||
|     date = new Date("2023-01-07"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-01-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-01-06"); | ||||
|     date = new Date("2023-01-08"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-01-02"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-01-06"); | ||||
| 
 | ||||
|     date = new Date("2023-12-25"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-12-25"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-12-29"); | ||||
|     date = new Date("2023-12-26"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-12-25"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-12-29"); | ||||
|     date = new Date("2023-12-27"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-12-25"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-12-29"); | ||||
|     date = new Date("2023-12-28"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-12-25"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-12-29"); | ||||
|     date = new Date("2023-12-29"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-12-25"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-12-29"); | ||||
|     date = new Date("2023-12-30"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-12-25"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-12-29"); | ||||
|     date = new Date("2023-12-31"); | ||||
|     expect(formatDate(getFirstWorkDayOfWeek(date))).toBe("2023-12-25"); | ||||
|     expect(formatDate(getLastWorkDayOfWeek(date))).toBe("2023-12-29"); | ||||
| }); | ||||
| 
 | ||||
| test('získání čísla týdne v roce', () => { | ||||
|     let date = new Date("2023-10-01"); | ||||
|     expect(getWeekNumber(date)).toBe(39); | ||||
|     date = new Date("2023-10-02"); | ||||
|     expect(getWeekNumber(date)).toBe(40); | ||||
|     date = new Date("2023-10-03"); | ||||
|     expect(getWeekNumber(date)).toBe(40); | ||||
|     date = new Date("2023-10-04"); | ||||
|     expect(getWeekNumber(date)).toBe(40); | ||||
|     date = new Date("2023-10-05"); | ||||
|     expect(getWeekNumber(date)).toBe(40); | ||||
|     date = new Date("2023-10-06"); | ||||
|     expect(getWeekNumber(date)).toBe(40); | ||||
|     date = new Date("2023-10-07"); | ||||
|     expect(getWeekNumber(date)).toBe(40); | ||||
|     date = new Date("2023-10-08"); | ||||
|     expect(getWeekNumber(date)).toBe(40); | ||||
|     date = new Date("2023-10-09"); | ||||
|     expect(getWeekNumber(date)).toBe(41); | ||||
| 
 | ||||
|     date = new Date("2022-01-01"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
| 
 | ||||
|     date = new Date("2022-12-30"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2022-12-31"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-01-01"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-01-02"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
|     date = new Date("2023-01-03"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
|     date = new Date("2023-01-04"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
|     date = new Date("2023-01-05"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
|     date = new Date("2023-01-06"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
|     date = new Date("2023-01-07"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
|     date = new Date("2023-01-08"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
|     date = new Date("2023-01-09"); | ||||
|     expect(getWeekNumber(date)).toBe(2); | ||||
| 
 | ||||
|     date = new Date("2023-12-24"); | ||||
|     expect(getWeekNumber(date)).toBe(51); | ||||
|     date = new Date("2023-12-25"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-12-26"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-12-27"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-12-28"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-12-29"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-12-30"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2023-12-31"); | ||||
|     expect(getWeekNumber(date)).toBe(52); | ||||
|     date = new Date("2024-01-01"); | ||||
|     expect(getWeekNumber(date)).toBe(1); | ||||
| }); | ||||
| @ -39,6 +39,29 @@ export function getIsWeekend(date: Date) { | ||||
|     return index == 5 || index == 6; | ||||
| } | ||||
| 
 | ||||
| /** Vrátí první pracovní den v týdnu předaného data. */ | ||||
| export function getFirstWorkDayOfWeek(date: Date) { | ||||
|     const firstDay = new Date(date.getTime()); | ||||
|     firstDay.setDate(date.getDate() - getDayOfWeekIndex(date)); | ||||
|     return firstDay; | ||||
| } | ||||
| 
 | ||||
| /** Vrátí poslední pracovní den v týdnu předaného data. */ | ||||
| export function getLastWorkDayOfWeek(date: Date) { | ||||
|     const lastDay = new Date(date.getTime()); | ||||
|     lastDay.setDate(date.getDate() + (4 - getDayOfWeekIndex(date))); | ||||
|     return lastDay; | ||||
| } | ||||
| 
 | ||||
| /** Vrátí pořadové číslo týdne předaného data v roce dle ISO 8601. */ | ||||
| export function getWeekNumber(inputDate: Date) { | ||||
|     var date = new Date(inputDate.getTime()); | ||||
|     date.setHours(0, 0, 0, 0); | ||||
|     date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); | ||||
|     var week1 = new Date(date.getFullYear(), 0, 4); | ||||
|     return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Vrátí JWT token z hlaviček, pokud ho obsahují. | ||||
|  *  | ||||
|  | ||||
| @ -67,13 +67,20 @@ interface PizzaDay { | ||||
|     orders: Order[], // seznam objednávek jednotlivých lidí
 | ||||
| } | ||||
| 
 | ||||
| /** Veškerá data pro zobrazení na klientovi */ | ||||
| export interface ClientData { | ||||
|     date: string, // datum vybraného dne pro zobrazení
 | ||||
|     isWeekend: boolean, // příznak, zda je zvolené datum víkend
 | ||||
|     weekIndex: number, // index zvoleného dne v týdnu (0-6)
 | ||||
|     todayWeekIndex?: number, // index dnešního dne v týdnu (0-6)
 | ||||
|     choices: Choices, // seznam voleb
 | ||||
| /** Týdenní menu jednotlivých restaurací. */ | ||||
| export interface WeekMenu { | ||||
|     [dayIndex: number]: { | ||||
|         [restaurant in Restaurants]?: Menu | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** Data vztahující se k jednomu konkrétnímu dni. */ | ||||
| export interface DayData { | ||||
|     date: string, // datum dne
 | ||||
|     isWeekend: boolean, // příznak, zda je datum víkend
 | ||||
|     weekIndex: number, // index dne v týdnu (0-6)
 | ||||
|     choices: Choices, // seznam voleb uživatelů
 | ||||
|     // TODO smazat
 | ||||
|     departureTimes: DepartureTime[], // seznam možných časů odchodu
 | ||||
|     menus?: { [restaurant in Restaurants]?: Menu }, // menu jednotlivých restaurací
 | ||||
|     pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje
 | ||||
| @ -81,6 +88,11 @@ export interface ClientData { | ||||
|     pizzaListLastUpdate?: Date, // datum a čas poslední aktualizace pizz
 | ||||
| } | ||||
| 
 | ||||
| /** Veškerá data pro zobrazení na klientovi. */ | ||||
| export interface ClientData extends DayData { | ||||
|     todayWeekIndex?: number, // index dnešního dne v týdnu (0-6)
 | ||||
| } | ||||
| 
 | ||||
| /** Nabídka jídel jednoho podniku. */ | ||||
| export interface Menu { | ||||
|     lastUpdate: string, // human-readable čas poslední aktualizace menu
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user