import React, { useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket'; import { useAuth } from './context/auth'; import Login from './Login'; import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap'; import Header from './components/Header'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import PizzaOrderList from './components/PizzaOrderList'; import SelectSearch, { SelectedOptionValue, SelectSearchOption } from 'react-select-search'; import 'react-select-search/style.css'; import './App.scss'; import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { useSettings } from './context/settings'; import Footer from './components/Footer'; import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import Loader from './components/Loader'; import { getHumanDateTime, isInTheFuture } from './Utils'; import NoteModal from './components/modals/NoteModal'; import { useEasterEgg } from './context/eggs'; import { Link } from 'react-router'; import { STATS_URL } from './AppRoutes'; import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime } from '../../types'; import { getLunchChoiceName } from './enums'; const EVENT_CONNECT = "connect" // Fixní styl pro všechny easter egg obrázky const EASTER_EGG_STYLE = { zIndex: 1, animationName: "bounce-in", animationTimingFunction: "ease" } // Výchozí doba trvání animace v sekundách, pokud není přetíženo v konfiguračním JSONu const EASTER_EGG_DEFAULT_DURATION = 0.75; function App() { const auth = useAuth(); const settings = useSettings(); const [easterEgg, _] = useEasterEgg(auth); const [isConnected, setIsConnected] = useState(false); const [data, setData] = useState(); const [food, setFood] = useState(); const [myOrder, setMyOrder] = useState(); const [foodChoiceList, setFoodChoiceList] = useState(); const [closed, setClosed] = useState(false); const socket = useContext(SocketContext); const choiceRef = useRef(null); const foodChoiceRef = useRef(null); const departureChoiceRef = useRef(null); const pizzaPoznamkaRef = useRef(null); const [failure, setFailure] = useState(false); const [dayIndex, setDayIndex] = useState(); const [loadingPizzaDay, setLoadingPizzaDay] = useState(false); const [noteModalOpen, setNoteModalOpen] = useState(false); const [eggImage, setEggImage] = useState(); const eggRef = useRef(null); // Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu // https://medium.com/@kishorkrishna/cant-access-latest-state-inside-socket-io-listener-heres-how-to-fix-it-1522a5abebdb const dayIndexRef = useRef(dayIndex); // Načtení dat po přihlášení useEffect(() => { if (!auth?.login) { return } getData().then(response => { const data = response.data if (data) { setData(data); setDayIndex(data.dayIndex); dayIndexRef.current = data.dayIndex; setFood(data.menus); } }).catch(e => { setFailure(true); }) }, [auth, auth?.login]); // Přenačtení pro zvolený den useEffect(() => { if (!auth?.login) { return } getData({ query: { dayIndex: dayIndex } }).then(response => { const data = response.data; setData(data); if (data) { setFood(data.menus); } }).catch(e => { setFailure(true); }) }, [dayIndex, auth]); // Registrace socket eventů useEffect(() => { socket.on(EVENT_CONNECT, () => { setIsConnected(true); }); socket.on(EVENT_DISCONNECT, () => { setIsConnected(false); }); socket.on(EVENT_MESSAGE, (newData: ClientData) => { // console.log("Přijata nová data ze socketu", newData); // Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený if (dayIndexRef.current == null || newData.dayIndex === dayIndexRef.current) { setData(newData); } }); return () => { socket.off(EVENT_CONNECT); socket.off(EVENT_DISCONNECT); socket.off(EVENT_MESSAGE); } }, [socket]); useEffect(() => { if (!auth?.login) { return } // TODO tohle občas náhodně nezafunguje, nutno přepsat, viz https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780 // TODO nutno opravit // if (data?.choices && choiceRef.current) { // for (let entry of Object.entries(data.choices)) { // if (entry[1].includes(auth.login)) { // const value = entry[0] as any as number; // TODO tohle je absurdní // choiceRef.current.value = Object.values(Locations)[value]; // } // } // } }, [auth, auth?.login, data?.choices]) // Reference na mojí objednávku useEffect(() => { if (data?.pizzaDay?.orders) { const myOrder = data.pizzaDay.orders.find(o => o.customer === auth?.login); setMyOrder(myOrder); } }, [auth?.login, data?.pizzaDay?.orders]) useEffect(() => { if (choiceRef?.current?.value && choiceRef.current.value !== "") { const locationKey = choiceRef.current.value as LunchChoice; const restaurantKey = Object.keys(Restaurant).indexOf(locationKey); if (restaurantKey > -1 && food) { const restaurant = Object.keys(Restaurant)[restaurantKey] as Restaurant; setFoodChoiceList(food[restaurant]?.food); setClosed(food[restaurant]?.closed ?? false); } else { setFoodChoiceList(undefined); setClosed(false); } } else { setFoodChoiceList(undefined); setClosed(false); } }, [choiceRef.current?.value, food]) // Navigace mezi dny pomocí klávesových šípek const handleKeyDown = useCallback((e: any) => { if (e.keyCode === 37 && dayIndex != null && dayIndex > 0) { handleDayChange(dayIndex - 1); } else if (e.keyCode === 39 && dayIndex != null && dayIndex < 4) { handleDayChange(dayIndex + 1); } }, [dayIndex]); useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); } }, [handleKeyDown]); // Stažení a nastavení easter egg obrázku useEffect(() => { if (auth?.login && easterEgg?.url && !eggImage) { getEasterEggImage({ path: { url: easterEgg.url } }).then(response => { if (response.data) { setEggImage(response.data); // Smazání obrázku z DOMu po animaci setTimeout(() => { if (eggRef?.current) { eggRef.current.remove(); } }, (easterEgg.duration || EASTER_EGG_DEFAULT_DURATION) * 1000); } }); } }, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]); const doAddClickFoodChoice = async (location: LunchChoice, foodIndex?: number) => { if (document.getSelection()?.type !== 'Range') { // pouze pokud se nejedná o výběr textu if (auth?.login) { await addChoice({ body: { locationKey: location, foodIndex, dayIndex } }); } } } const doAddChoice = async (event: React.ChangeEvent) => { const locationKey = event.target.value as LunchChoice; if (auth?.login) { await addChoice({ body: { locationKey, dayIndex } }); if (foodChoiceRef.current?.value) { foodChoiceRef.current.value = ""; } } } const doJdemeObed = async () => { if (auth?.login) { await jdemeObed(); } } const doAddFoodChoice = async (event: React.ChangeEvent) => { if (event.target.value && foodChoiceList?.length && choiceRef.current?.value) { const locationKey = choiceRef.current.value as LunchChoice; if (auth?.login) { await addChoice({ body: { locationKey, foodIndex: Number(event.target.value), dayIndex } }); } } } const doRemoveChoices = async (locationKey: LunchChoice) => { if (auth?.login) { await removeChoices({ body: { locationKey, dayIndex } }); // Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo if (choiceRef?.current?.value) { choiceRef.current.value = ""; } if (foodChoiceRef?.current?.value) { foodChoiceRef.current.value = ""; } } } const doRemoveFoodChoice = async (locationKey: LunchChoice, foodIndex: number) => { if (auth?.login) { await removeChoice({ body: { locationKey, foodIndex, dayIndex } }); if (choiceRef?.current?.value) { choiceRef.current.value = ""; } if (foodChoiceRef?.current?.value) { foodChoiceRef.current.value = ""; } } } const saveNote = async (note?: string) => { if (auth?.login) { await updateNote({ body: { note, dayIndex } }); setNoteModalOpen(false); } } const pizzaSuggestions = useMemo(() => { if (!data?.pizzaList) { return []; } const suggestions: SelectSearchOption[] = []; data.pizzaList.forEach((pizza, index) => { const group: SelectSearchOption = { name: pizza.name, type: "group", items: [] } pizza.sizes.forEach((size, sizeIndex) => { const name = `${size.size} (${size.price} Kč)`; const value = `${index}|${sizeIndex}`; group.items?.push({ name, value }); }) suggestions.push(group); }) return suggestions; }, [data?.pizzaList]); const handlePizzaChange = async (value: SelectedOptionValue | SelectedOptionValue[]) => { if (auth?.login && data?.pizzaList) { if (typeof value !== 'string') { throw Error('Nepodporovaný typ hodnoty'); } const s = value.split('|'); const pizzaIndex = Number.parseInt(s[0]); const pizzaSizeIndex = Number.parseInt(s[1]); await addPizza({ body: { pizzaIndex, pizzaSizeIndex } }); } } const handlePizzaDelete = async (pizzaOrder: PizzaVariant) => { await removePizza({ body: { pizzaOrder } }); } const handlePizzaPoznamkaChange = async () => { if (pizzaPoznamkaRef.current?.value && pizzaPoznamkaRef.current.value.length > 70) { alert("Poznámka může mít maximálně 70 znaků"); return; } updatePizzaDayNote({ body: { note: pizzaPoznamkaRef.current?.value } }); } // const addToCart = async () => { // TODO aktuálně nefunkční - nedokážeme poslat PHPSESSIONID cookie // if (data?.pizzaDay?.orders) { // for (const order of data?.pizzaDay?.orders) { // for (const pizzaOrder of order.pizzaList) { // const url = 'https://www.pizzachefie.cz/pridat.html'; // const payload = new URLSearchParams(); // payload.append('varId', pizzaOrder.varId.toString()); // await fetch(url, { // method: "POST", // mode: "no-cors", // cache: "no-cache", // credentials: "same-origin", // headers: { // 'Content-Type': 'application/x-www-form-urlencoded', // }, // body: payload, // }) // } // } // // TODO otevřít košík v nové záložce // } // } const handleChangeDepartureTime = async (event: React.ChangeEvent) => { if (foodChoiceList?.length && choiceRef.current?.value) { await changeDepartureTime({ body: { time: event.target.value as DepartureTime, dayIndex } }); } } const handleDayChange = async (dayIndex: number) => { setDayIndex(dayIndex); dayIndexRef.current = dayIndex; if (choiceRef?.current?.value) { choiceRef.current.value = ""; } if (foodChoiceRef?.current?.value) { foodChoiceRef.current.value = ""; } if (departureChoiceRef?.current?.value) { departureChoiceRef.current.value = ""; } } const renderFoodTable = (location: Restaurant, menu: RestaurantDayMenu) => { let content; if (menu?.closed) { content =

Zavřeno

} else if (menu?.food?.length && menu.food.length > 0) { const hideSoups = settings?.hideSoups; content = {menu.food.map((f: Food, index: number) => (!hideSoups || !f.isSoup) && doAddClickFoodChoice(location, index)}> )}
{f.amount} {f.name} {f.price}
} else { content =

Chyba načtení dat

} return

doAddClickFoodChoice(location)}>{getLunchChoiceName(location)}

{menu?.lastUpdate && Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}} {content} } if (!auth?.login) { return ; } if (!isConnected) { return } if (failure) { return } if (!data || !food) { return } const noOrders = data?.pizzaDay?.orders?.length === 0; const canChangeChoice = dayIndex == null || data.todayDayIndex == null || dayIndex >= data.todayDayIndex; const { path, url, startOffset, endOffset, duration, ...style } = easterEgg || {}; return ( <> {easterEgg && eggImage && }
{data.isWeekend ?

Užívejte víkend :)

: <> Poslední změny:
  • Možnost výběru restaurace a jídel kliknutím v tabulce
  • Statistiky
{dayIndex != null &&
0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} />

{data.date}

handleDayChange(dayIndex + 1)} />
} {/* TODO zjednodušit, stačí iterovat klíče typu Restaurant */} {food['SLADOVNICKA'] && renderFoodTable('SLADOVNICKA', food['SLADOVNICKA'])} {food['TECHTOWER'] && renderFoodTable('TECHTOWER', food['TECHTOWER'])} {food['ZASTAVKAUMICHALA'] && renderFoodTable('ZASTAVKAUMICHALA', food['ZASTAVKAUMICHALA'])} {food['SENKSERIKOVA'] && renderFoodTable('SENKSERIKOVA', food['SENKSERIKOVA'])}
{canChangeChoice && <>

{`Jak to ${dayIndex == null || dayIndex === data.todayDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}

{Object.entries(LunchChoice) .filter(entry => { const locationKey = entry[0] as Restaurant; return !food[locationKey]?.closed; }) .map(entry => )} Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí. {foodChoiceList && !closed && <>

Na co dobrého? (nepovinné)

{foodChoiceList.map((food, index) => )} } {foodChoiceList && !closed && <>

V kolik hodin preferuješ odchod?

{Object.values(DepartureTime) .filter(time => isInTheFuture(time)) .map(time => )} } } {Object.keys(data.choices).length > 0 ? {Object.keys(data.choices).map(key => { const locationKey = key as LunchChoice; const locationName = getLunchChoiceName(locationKey); const loginObject = data.choices[locationKey]; if (!loginObject) { return; } const locationLoginList = Object.entries(loginObject); const locationPickCount = locationLoginList.length return ( {(locationPickCount ?? 0) > 1 ? ( ) : ( )} ) } )}
{locationName} ({locationPickCount}){locationName} {locationLoginList.map((entry: [string, UserLunchChoice], index) => { const login = entry[0]; const userPayload = entry[1]; const userChoices = userPayload?.selectedFoods; const trusted = userPayload?.trusted || false; return {userChoices?.length && food ? : null} } )}
{trusted && } {login} {userPayload.departureTime && ({userPayload.departureTime})} {userPayload.note && ({userPayload.note})} {login === auth.login && canChangeChoice && { setNoteModalOpen(true); }} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />} {login === auth.login && canChangeChoice && { doRemoveChoices(key as LunchChoice); }} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
    {userChoices?.map(foodIndex => { const restaurantKey = key as Restaurant; const foodName = food[restaurantKey]?.food?.[foodIndex].name; return
  • {foodName} {login === auth.login && canChangeChoice && { doRemoveFoodChoice(restaurantKey, foodIndex); }} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
  • })}
:
Zatím nikdo nehlasoval...
}
{dayIndex === data.todayDayIndex &&
{!data.pizzaDay &&

Pro dnešní den není aktuálně založen Pizza day.

{loadingPizzaDay ? Zjišťujeme dostupné pizzy : <> }
} {data.pizzaDay &&

Pizza day

{ data.pizzaDay.state === PizzaDayState.CREATED &&

Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.
Můžete upravovat své objednávky.

{ data.pizzaDay.creator === auth.login && <> }
} { data.pizzaDay.state === PizzaDayState.LOCKED &&

Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}

{data.pizzaDay.creator === auth.login && <> }
} { data.pizzaDay.state === PizzaDayState.ORDERED &&

Pizzy byly objednány uživatelem {data.pizzaDay.creator}

{data.pizzaDay.creator === auth.login &&
}
} { data.pizzaDay.state === PizzaDayState.DELIVERED &&

{`Pizzy byly doručeny.${myOrder?.hasQr ? ` Objednávku můžete uživateli ${data.pizzaDay.creator} uhradit pomocí QR kódu níže.` : ''}`}

}
{data.pizzaDay.state === PizzaDayState.CREATED &&
{ }} onFocus={_ => { }} /> Poznámka: { if (event.key === 'Enter') { handlePizzaPoznamkaChange(); } event.stopPropagation(); }} />
} { data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr ?

QR platba

QR kód
: null }
}
}
|| "Jejda, něco se nám nepovedlo :("}