diff --git a/.gitignore b/.gitignore index 3c3629e..6516e78 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +types/gen \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ff60305..938bd8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,12 @@ FROM ${NODE_VERSION} AS builder WORKDIR /build +# Zkopírování závislostí - OpenAPI generátor +COPY types/package.json ./types/ +COPY types/yarn.lock ./types/ +COPY types/api.yml ./types/ +COPY types/openapi-ts.config.ts ./types/ + # Zkopírování závislostí - server COPY server/package.json ./server/ COPY server/yarn.lock ./server/ @@ -13,6 +19,10 @@ COPY server/yarn.lock ./server/ COPY client/package.json ./client/ COPY client/yarn.lock ./client/ +# Instalace závislostí - OpenAPI generátor +WORKDIR /build/types +RUN yarn install --frozen-lockfile + # Instalace závislostí - server WORKDIR /build/server RUN yarn install --frozen-lockfile @@ -36,7 +46,12 @@ COPY client/src ./client/src COPY client/public ./client/public # Zkopírování společných typů -COPY types ./types/ +COPY types/RequestTypes.ts ./types/ +COPY types/index.ts ./types/ + +# Vygenerování společných typů z OpenAPI +WORKDIR /build/types +RUN yarn openapi-ts # Sestavení serveru WORKDIR /build/server diff --git a/README.md b/README.md index 4daefe3..df17590 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Luncher Aplikace pro profesionální management obědů. -Aplikace sestává ze dvou modulů + společných TypeScript definic (adresář `types`). +Aplikace sestává ze tří modulů. +- types + - OpenAPI definice společných typů, generované přes [openapi-ts](https://github.com/hey-api/openapi-ts) - server - backend psaný v [node.js](https://nodejs.dev) - client @@ -10,19 +12,27 @@ Aplikace sestává ze dvou modulů + společných TypeScript definic (adresář ## Spuštění pro vývoj ### Závislosti #### Klient/server -- [Node.js 22.x](https://nodejs.dev) +- [Node.js 22.x (>= 22.11)](https://nodejs.dev) - [Yarn 1.22.x (Classic)](https://classic.yarnpkg.com) ### Spuštění na *nix platformách - Nainstalovat závislosti viz předchozí bod - Zkopírovat `server/.env.template` do `server/.env.development` a upravit dle potřeby -- Spustit `./run_dev.sh`. Na jiných platformách se lze inspirovat jeho obsahem, postup by měl být víceméně stejný. +- Vygenerovat společné TypeScript typy + - `cd types && yarn install && yarn openapi-ts` +- Server + - `cd server && yarn install && export NODE_ENV=development && yarn startReload` +- Klient + - `cd client && yarn install && yarn start` ## Sestavení a spuštění produkční verze v Docker ### Závislosti - [Docker](https://www.docker.com) - [Docker Compose](https://docs.docker.com/compose) +### Spuštění +- `docker compose up --build -d` + ### Spuštení s traefik - `docker compose -f compose-traefik.yml up --build -d` diff --git a/client/src/App.tsx b/client/src/App.tsx index 5e3e947..ed8df66 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,12 +8,11 @@ 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 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 { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime, LocationKey } from '../../types'; import Footer from './components/Footer'; import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import Loader from './components/Loader'; @@ -25,6 +24,7 @@ import { useEasterEgg } from './context/eggs'; import { getImage } from './api/EasterEggApi'; import { Link } from 'react-router'; import { STATS_URL } from './AppRoutes'; +import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LunchChoices, UserLunchChoice, PizzaVariant } from '../../types'; const EVENT_CONNECT = "connect" @@ -44,8 +44,8 @@ function App() { const [easterEgg, easterEggLoading] = useEasterEgg(auth); const [isConnected, setIsConnected] = useState(false); const [data, setData] = useState(); - const [food, setFood] = useState<{ [key in Restaurants]?: DayMenu }>(); - const [myOrder, setMyOrder] = useState(); + const [food, setFood] = useState(); + const [myOrder, setMyOrder] = useState(); const [foodChoiceList, setFoodChoiceList] = useState(); const [closed, setClosed] = useState(false); const socket = useContext(SocketContext); @@ -68,11 +68,13 @@ function App() { if (!auth || !auth.login) { return } - getData().then((data: ClientData) => { - setData(data); - setDayIndex(data.weekIndex); - dayIndexRef.current = data.weekIndex; - setFood(data.menus); + getData().then(({ data }) => { + if (data) { + setData(data); + setDayIndex(data.weekIndex); + dayIndexRef.current = data.weekIndex; + setFood(data.menus); + } }).catch(e => { setFailure(true); }) @@ -104,7 +106,7 @@ function App() { 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.weekIndex === dayIndexRef.current) { + if (dayIndexRef.current == null || newData.dayIndex === dayIndexRef.current) { setData(newData); } }); @@ -142,10 +144,10 @@ function App() { useEffect(() => { if (choiceRef?.current?.value && choiceRef.current.value !== "") { - const locationKey = choiceRef.current.value as LocationKey; - const restaurantKey = Object.keys(Restaurants).indexOf(locationKey); + const locationKey = choiceRef.current.value as keyof typeof LunchChoice; + const restaurantKey = Object.keys(Restaurant).indexOf(locationKey); if (restaurantKey > -1 && food) { - const restaurant = Object.values(Restaurants)[restaurantKey]; + const restaurant = Object.keys(Restaurant)[restaurantKey] as keyof typeof Restaurant; setFoodChoiceList(food[restaurant]?.food); setClosed(food[restaurant]?.closed ?? false); } else { @@ -191,17 +193,16 @@ function App() { } }, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]); - const doAddClickFoodChoice = async (location: Locations, foodIndex?: number) => { + const doAddClickFoodChoice = async (location: keyof typeof LunchChoice, foodIndex?: number) => { if (document.getSelection()?.type !== 'Range') { // pouze pokud se nejedná o výběr textu - const locationKey = Object.keys(Locations).find(key => Locations[key as keyof typeof Locations] === location) as LocationKey; if (auth?.login) { - await errorHandler(() => addChoice(locationKey, foodIndex, dayIndex)); + await errorHandler(() => addChoice(location, foodIndex, dayIndex)); } } } const doAddChoice = async (event: React.ChangeEvent) => { - const locationKey = event.target.value as LocationKey; + const locationKey = event.target.value as keyof typeof LunchChoice; if (auth?.login) { await errorHandler(() => addChoice(locationKey, undefined, dayIndex)); if (foodChoiceRef.current?.value) { @@ -218,14 +219,14 @@ function App() { const doAddFoodChoice = async (event: React.ChangeEvent) => { if (event.target.value && foodChoiceList?.length && choiceRef.current?.value) { - const locationKey = choiceRef.current.value as LocationKey; + const locationKey = choiceRef.current.value as keyof typeof LunchChoice; if (auth?.login) { await errorHandler(() => addChoice(locationKey, Number(event.target.value), dayIndex)); } } } - const doRemoveChoices = async (locationKey: LocationKey) => { + const doRemoveChoices = async (locationKey: keyof typeof LunchChoice) => { if (auth?.login) { await errorHandler(() => removeChoices(locationKey, dayIndex)); // Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo @@ -238,7 +239,7 @@ function App() { } } - const doRemoveFoodChoice = async (locationKey: LocationKey, foodIndex: number) => { + const doRemoveFoodChoice = async (locationKey: keyof typeof LunchChoice, foodIndex: number) => { if (auth?.login) { await errorHandler(() => removeChoice(locationKey, foodIndex, dayIndex)); if (choiceRef?.current?.value) { @@ -286,7 +287,7 @@ function App() { } } - const handlePizzaDelete = async (pizzaOrder: PizzaOrder) => { + const handlePizzaDelete = async (pizzaOrder: PizzaVariant) => { await removePizza(pizzaOrder); } @@ -342,11 +343,11 @@ function App() { } } - const renderFoodTable = (location: Locations, menu: DayMenu) => { + const renderFoodTable = (location: keyof typeof Restaurant, menu: RestaurantDayMenu) => { let content; if (menu?.closed) { content =

Zavřeno

- } else if (menu?.food?.length > 0) { + } else if (menu?.food?.length && menu.food.length > 0) { const hideSoups = settings?.hideSoups; content = @@ -399,7 +400,7 @@ function App() { } const noOrders = data?.pizzaDay?.orders?.length === 0; - const canChangeChoice = dayIndex == null || data.todayWeekIndex == null || dayIndex >= data.todayWeekIndex; + const canChangeChoice = dayIndex == null || data.todayDayIndex == null || dayIndex >= data.todayDayIndex; const { path, url, startOffset, endOffset, duration, ...style } = easterEgg || {}; @@ -421,29 +422,28 @@ function App() { {dayIndex != null &&
0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} /> -

{data.date}

+

{data.date}

handleDayChange(dayIndex + 1)} />
} - {food[Restaurants.SLADOVNICKA] && renderFoodTable(Locations.SLADOVNICKA, food[Restaurants.SLADOVNICKA])} - {/* {food[Restaurants.UMOTLIKU] && renderFoodTable(food[Restaurants.UMOTLIKU])} */} - {food[Restaurants.TECHTOWER] && renderFoodTable(Locations.TECHTOWER, food[Restaurants.TECHTOWER])} - {food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable(Locations.ZASTAVKAUMICHALA, food[Restaurants.ZASTAVKAUMICHALA])} - {food[Restaurants.SENKSERIKOVA] && renderFoodTable(Locations.SENKSERIKOVA, food[Restaurants.SENKSERIKOVA])} + {/* TODO zjednodušit, stačí iterovat klíče typu Restaurant */} + {food['SLADOVNICKA'] && renderFoodTable('SLADOVNICKA', food['SLADOVNICKA'])} + {/* {food['UMOTLIKU'] && renderFoodTable('UMOTLIKU', food['UMOTLIKU'])} */} + {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.todayWeekIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}

+

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

- {Object.entries(Locations) + {Object.keys(Restaurant) .filter(entry => { - const locationKey = entry[0] as LocationKey; - const restaurantKey = Object.keys(Restaurants).indexOf(locationKey); - const v = Object.values(Restaurants)[restaurantKey]; - return v == null || !food[v]?.closed; + const locationKey = entry as keyof typeof Restaurant; + return !food[locationKey]?.closed; }) .map(entry => )} @@ -460,8 +460,8 @@ function App() { {Object.values(DepartureTime) - .filter(time => isInTheFuture(time)) - .map(time => )} + .filter(time => isInTheFuture(time)) + .map(time => )} } } @@ -469,8 +469,8 @@ function App() {
{Object.keys(data.choices).map(key => { - const locationKey = key as LocationKey; - const locationName = Locations[locationKey]; + const locationKey = key as keyof typeof LunchChoice; + const locationName = LunchChoice[locationKey]; const loginObject = data.choices[locationKey]; if (!loginObject) { return; @@ -479,17 +479,17 @@ function App() { const locationPickCount = locationLoginList.length return ( - {(locationPickCount?? 0) > 1 ? ( + {(locationPickCount ?? 0) > 1 ? ( ) : ( - )} + )}
{locationName} ({locationPickCount}){locationName}{locationName} - {locationLoginList.map((entry: [string, FoodChoices], index) => { + {locationLoginList.map((entry: [string, UserLunchChoice], index) => { const login = entry[0]; const userPayload = entry[1]; - const userChoices = userPayload?.options; + const userChoices = userPayload?.selectedFoods; const trusted = userPayload?.trusted || false; return {userChoices?.length && food ? -
@@ -503,20 +503,18 @@ function App() { setNoteModalOpen(true); }} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />} {login === auth.login && canChangeChoice && { - doRemoveChoices(key as LocationKey); + doRemoveChoices(key as keyof typeof LunchChoice); }} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
    {userChoices?.map(foodIndex => { - // TODO narovnat, tohle je zbytečně složité - const restaurantKey = Object.keys(Restaurants).indexOf(key); - const restaurant = Object.values(Restaurants)[restaurantKey]; - const foodName = food[restaurant]?.food[foodIndex].name; + const restaurantKey = key as keyof typeof Restaurant; + const foodName = food[restaurantKey]?.food?.[foodIndex].name; return
  • {foodName} {login === auth.login && canChangeChoice && { - doRemoveFoodChoice(key as LocationKey, foodIndex); + doRemoveFoodChoice(restaurantKey, foodIndex); }} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
  • })} @@ -536,7 +534,7 @@ function App() { :
    Zatím nikdo nehlasoval...
    } - {dayIndex === data.todayWeekIndex && + {dayIndex === data.todayDayIndex &&
    {!data.pizzaDay &&
    @@ -646,13 +644,13 @@ function App() {
    } - + { - data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && -
    -

    QR platba

    - QR kód -
    + data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr ? +
    +

    QR platba

    + QR kód +
    : null }
    } diff --git a/client/src/Login.tsx b/client/src/Login.tsx index 59e1241..8b178bf 100644 --- a/client/src/Login.tsx +++ b/client/src/Login.tsx @@ -28,7 +28,7 @@ export default function Login() { const length = loginRef?.current?.value && loginRef?.current?.value.length && loginRef.current.value.replace(/\s/g, '').length if (length) { // TODO odchytávat cokoliv mimo 200 - const token = await login(loginRef.current.value); + const token = await login(loginRef.current?.value); if (token) { auth?.setToken(token); } diff --git a/client/src/Utils.tsx b/client/src/Utils.tsx index e79e77e..524e8e0 100644 --- a/client/src/Utils.tsx +++ b/client/src/Utils.tsx @@ -1,4 +1,4 @@ -import {DepartureTime} from "../../types"; +import { DepartureTime } from "../../types"; const TOKEN_KEY = "token"; @@ -16,8 +16,8 @@ export const storeToken = (token: string) => { * * @returns token nebo null */ -export const getToken = (): string | null => { - return localStorage.getItem(TOKEN_KEY); +export const getToken = (): string | undefined => { + return localStorage.getItem(TOKEN_KEY) ?? undefined; } /** diff --git a/client/src/api/Client.ts b/client/src/api/Client.ts new file mode 100644 index 0000000..2c5d5c6 --- /dev/null +++ b/client/src/api/Client.ts @@ -0,0 +1,8 @@ +import { client } from '../../../types/gen/client.gen'; +import { getToken } from '../Utils'; + +client.setConfig({ + auth: () => getToken(), +}); + +export default client \ No newline at end of file diff --git a/client/src/api/FoodApi.ts b/client/src/api/FoodApi.ts index ef2f865..aa74827 100644 --- a/client/src/api/FoodApi.ts +++ b/client/src/api/FoodApi.ts @@ -1,17 +1,18 @@ -import { AddChoiceRequest, ChangeDepartureTimeRequest, LocationKey, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types"; +import { AddChoiceRequest, ChangeDepartureTimeRequest, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types"; +import { LunchChoice } from "../../../types"; import { api } from "./Api"; const FOOD_API_PREFIX = '/api/food'; -export const addChoice = async (locationKey: LocationKey, foodIndex?: number, dayIndex?: number) => { +export const addChoice = async (locationKey: keyof typeof LunchChoice, foodIndex?: number, dayIndex?: number) => { return await api.post(`${FOOD_API_PREFIX}/addChoice`, { locationKey, foodIndex, dayIndex }); } -export const removeChoices = async (locationKey: LocationKey, dayIndex?: number) => { +export const removeChoices = async (locationKey: keyof typeof LunchChoice, dayIndex?: number) => { return await api.post(`${FOOD_API_PREFIX}/removeChoices`, { locationKey, dayIndex }); } -export const removeChoice = async (locationKey: LocationKey, foodIndex: number, dayIndex?: number) => { +export const removeChoice = async (locationKey: keyof typeof LunchChoice, foodIndex: number, dayIndex?: number) => { return await api.post(`${FOOD_API_PREFIX}/removeChoice`, { locationKey, foodIndex, dayIndex }); } diff --git a/client/src/api/PizzaDayApi.ts b/client/src/api/PizzaDayApi.ts index 153d763..46f4564 100644 --- a/client/src/api/PizzaDayApi.ts +++ b/client/src/api/PizzaDayApi.ts @@ -1,4 +1,5 @@ -import { AddPizzaRequest, FinishDeliveryRequest, PizzaOrder, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types"; +import { AddPizzaRequest, FinishDeliveryRequest, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types"; +import { PizzaVariant } from "../../../types"; import { api } from "./Api"; const PIZZADAY_API_PREFIX = '/api/pizzaDay'; @@ -31,7 +32,7 @@ export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => { return await api.post(`${PIZZADAY_API_PREFIX}/add`, { pizzaIndex, pizzaSizeIndex }); } -export const removePizza = async (pizzaOrder: PizzaOrder) => { +export const removePizza = async (pizzaOrder: PizzaVariant) => { return await api.post(`${PIZZADAY_API_PREFIX}/remove`, { pizzaOrder }); } diff --git a/client/src/api/VotingApi.ts b/client/src/api/VotingApi.ts index a1f7341..a424736 100644 --- a/client/src/api/VotingApi.ts +++ b/client/src/api/VotingApi.ts @@ -1,4 +1,5 @@ -import { FeatureRequest, UpdateFeatureVoteRequest } from "../../../types"; +import { UpdateFeatureVoteRequest } from "../../../types"; +import { FeatureRequest } from "../../../types"; import { api } from "./Api"; const VOTING_API_PREFIX = '/api/voting'; diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index c45377e..d5ba480 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -4,13 +4,12 @@ import { useAuth } from "../context/auth"; import SettingsModal from "./modals/SettingsModal"; import { useSettings } from "../context/settings"; import FeaturesVotingModal from "./modals/FeaturesVotingModal"; -import { FeatureRequest } from "../../../types"; import { errorHandler } from "../api/Api"; import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi"; import PizzaCalculatorModal from "./modals/PizzaCalculatorModal"; import { useNavigate } from "react-router"; import { STATS_URL } from "../AppRoutes"; - +import { FeatureRequest } from "../../../types"; export default function Header() { const auth = useAuth(); diff --git a/client/src/components/PizzaOrderList.tsx b/client/src/components/PizzaOrderList.tsx index fa1a200..3c67eef 100644 --- a/client/src/components/PizzaOrderList.tsx +++ b/client/src/components/PizzaOrderList.tsx @@ -1,12 +1,12 @@ import { Table } from "react-bootstrap"; -import { Order, PizzaDayState, PizzaOrder } from "../../../types"; import PizzaOrderRow from "./PizzaOrderRow"; import { updatePizzaFee } from "../api/PizzaDayApi"; +import { PizzaDayState, PizzaOrder, PizzaVariant } from "../../../types"; type Props = { state: PizzaDayState, - orders: Order[], - onDelete: (pizzaOrder: PizzaOrder) => void, + orders: PizzaOrder[], + onDelete: (pizzaOrder: PizzaVariant) => void, creator: string, } diff --git a/client/src/components/PizzaOrderRow.tsx b/client/src/components/PizzaOrderRow.tsx index 9e229f3..07493aa 100644 --- a/client/src/components/PizzaOrderRow.tsx +++ b/client/src/components/PizzaOrderRow.tsx @@ -2,14 +2,14 @@ import React, { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faMoneyBill1, faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { useAuth } from "../context/auth"; -import { Order, PizzaDayState, PizzaOrder } from "../../../types"; import PizzaAdditionalFeeModal from "./modals/PizzaAdditionalFeeModal"; +import { PizzaDayState, PizzaOrder, PizzaVariant } from "../../../types"; type Props = { creator: string, - order: Order, + order: PizzaOrder, state: PizzaDayState, - onDelete: (order: PizzaOrder) => void, + onDelete: (order: PizzaVariant) => void, onFeeModalSave: (customer: string, name?: string, price?: number) => void, } @@ -24,7 +24,7 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo return <>
{order.customer}{order.pizzaList.map((pizzaOrder, index) => + {order.pizzaList!.map((pizzaOrder, index) => {`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`} {auth?.login === order.customer && state === PizzaDayState.CREATED && diff --git a/client/src/context/auth.tsx b/client/src/context/auth.tsx index a9f86c1..a22afd5 100644 --- a/client/src/context/auth.tsx +++ b/client/src/context/auth.tsx @@ -28,7 +28,7 @@ export const useAuth = () => { function useProvideAuth(): AuthContextProps { const [loginName, setLoginName] = useState(); const [trusted, setTrusted] = useState(); - const [token, setToken] = useState(getToken()); + const [token, setToken] = useState(getToken()); const { decodedToken } = useJwt(token || ''); useEffect(() => { @@ -52,7 +52,7 @@ function useProvideAuth(): AuthContextProps { function logout() { const trusted = (decodedToken as any).trusted; const logoutUrl = (decodedToken as any).logoutUrl; - setToken(null); + setToken(undefined); setLoginName(undefined); setTrusted(undefined); if (trusted && logoutUrl?.length) { diff --git a/client/src/pages/StatsPage.tsx b/client/src/pages/StatsPage.tsx index 6648c46..0b589ea 100644 --- a/client/src/pages/StatsPage.tsx +++ b/client/src/pages/StatsPage.tsx @@ -5,7 +5,7 @@ import { useAuth } from "../context/auth"; import Login from "../Login"; import { formatDate, getFirstWorkDayOfWeek, getHumanDate, getLastWorkDayOfWeek } from "../Utils"; import { getStats } from "../api/StatsApi"; -import { WeeklyStats, LocationKey, Locations } from "../../../types"; +import { WeeklyStats, LunchChoice } from "../../../types"; import Loader from "../components/Loader"; import { faChevronLeft, faChevronRight, faGear } from "@fortawesome/free-solid-svg-icons"; import { Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts"; @@ -47,9 +47,9 @@ export default function StatsPage() { } }, [dateRange]); - const renderLine = (location: Locations) => { - const index = Object.values(Locations).indexOf(location); - const key = Object.keys(Locations)[index]; + const renderLine = (location: LunchChoice) => { + const index = Object.values(LunchChoice).indexOf(location); + const key = Object.keys(LunchChoice)[index]; return data.locations[key] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} /> } @@ -111,7 +111,7 @@ export default function StatsPage() { - {Object.values(Locations).map(location => renderLine(location))} + {Object.values(LunchChoice).map(location => renderLine(location))} diff --git a/run_dev.sh b/run_dev.sh index bc75bbc..2d39c77 100755 --- a/run_dev.sh +++ b/run_dev.sh @@ -1,5 +1,5 @@ export NODE_ENV=development -yarn install -cd server && yarn start & -cd client && yarn start & +cd types && yarn install && yarn openapi-ts +cd server && yarn install && yarn start & +cd client && yarn install && yarn start & wait \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index bf85c2b..0ef7a74 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,5 +1,6 @@ /dist /resources/easterEggs +/src/gen .env.production .env.development .easter-eggs.json diff --git a/server/src/index.ts b/server/src/index.ts index f451d8b..4d68368 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -101,7 +101,7 @@ app.use("/api/", (req, res, next) => { const emailHeader = req.header('remote-email'); if (userHeader !== undefined && nameHeader !== undefined) { const remoteName = Buffer.from(nameHeader, 'latin1').toString(); - if (ENVIRONMENT !== "production"){ + if (ENVIRONMENT !== "production") { console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader); } } diff --git a/server/src/mock.ts b/server/src/mock.ts index af8f970..8723fb9 100644 --- a/server/src/mock.ts +++ b/server/src/mock.ts @@ -1,4 +1,4 @@ -import { WeeklyStats, Locations } from "../../types"; +import { WeeklyStats, LunchChoice } from "../../types"; // Mockovací data pro podporované podniky, na jeden týden const MOCK_DATA = { @@ -1386,77 +1386,26 @@ export const getPizzaListMock = () => { } export const getStatsMock = (): WeeklyStats => { - // TODO stačilo by iterovat ten enum, jako to už děláme jinde return [ { date: '24.02.', - locations: { - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), - } + locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } }, { date: '25.02.', - locations: { - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), - } + locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } }, { date: '26.02.', - locations: { - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), - } + locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } }, { date: '27.02.', - locations: { - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), - } + locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } }, { date: '28.02.', - locations: { - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), - [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), - } + locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } } ]; } \ No newline at end of file diff --git a/server/src/notifikace.ts b/server/src/notifikace.ts index 435f1fe..c06181a 100644 --- a/server/src/notifikace.ts +++ b/server/src/notifikace.ts @@ -1,11 +1,11 @@ /** Notifikace */ -import { ClientData, NotififaceInput, NotifikaceData } from '../../types'; import axios from 'axios'; import dotenv from 'dotenv'; import path from 'path'; -import { getToday } from "./service"; -import { formatDate, getUsersByLocation, getHumanTime } from "./utils"; +import { getClientData, getToday } from "./service"; +import { getUsersByLocation, getHumanTime } from "./utils"; import getStorage from "./storage"; +import { NotifikaceData, NotifikaceInput } from '../../types'; const storage = getStorage(); const ENVIRONMENT = process.env.NODE_ENV || 'production' @@ -54,7 +54,7 @@ dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) }); // return promises; // }; -export const ntfyCall = async (data: NotififaceInput) => { +export const ntfyCall = async (data: NotifikaceInput) => { const url = process.env.NTFY_HOST const username = process.env.NTFY_USERNAME; const password = process.env.NTFY_PASSWD; @@ -70,8 +70,7 @@ export const ntfyCall = async (data: NotififaceInput) => { console.log("NTFY_PASSWD není definován v env") return } - const today = formatDate(getToday()); - let clientData: ClientData = await storage.getData(today); + let clientData = await getClientData(getToday()); const userByCLocation = getUsersByLocation(clientData.choices, data.user) const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64'); @@ -98,7 +97,7 @@ export const ntfyCall = async (data: NotififaceInput) => { } -export const teamsCall = async (data: NotififaceInput) => { +export const teamsCall = async (data: NotifikaceInput) => { const url = process.env.TEAMS_WEBHOOK_URL; const title = data.udalost; let time = new Date(); diff --git a/server/src/pizza.ts b/server/src/pizza.ts index 6dc0129..ada27b0 100644 --- a/server/src/pizza.ts +++ b/server/src/pizza.ts @@ -1,10 +1,10 @@ import { formatDate } from "./utils"; import { callNotifikace } from "./notifikace"; import { generateQr } from "./qr"; -import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, DayData } from "../../types"; import getStorage from "./storage"; import { downloadPizzy } from "./chefie"; -import { getToday, initIfNeeded } from "./service"; +import { getClientData, getToday, initIfNeeded } from "./service"; +import { Pizza, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum } from "../../types"; const storage = getStorage(); @@ -14,8 +14,7 @@ const storage = getStorage(); */ export async function getPizzaList(): Promise { await initIfNeeded(); - const today = formatDate(getToday()); - let clientData: DayData = await storage.getData(today); + let clientData = await getClientData(getToday()); if (!clientData.pizzaList) { const mock = process.env.MOCK_DATA === 'true'; clientData = await savePizzaList(await downloadPizzy(mock)); @@ -31,9 +30,9 @@ export async function getPizzaList(): Promise { export async function savePizzaList(pizzaList: Pizza[]): Promise { await initIfNeeded(); const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); clientData.pizzaList = pizzaList; - clientData.pizzaListLastUpdate = new Date(); + clientData.pizzaListLastUpdate = formatDate(new Date()); await storage.setData(today, clientData); return clientData; } @@ -43,14 +42,14 @@ export async function savePizzaList(pizzaList: Pizza[]): Promise { */ export async function createPizzaDay(creator: string): Promise { await initIfNeeded(); - const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (clientData.pizzaDay) { throw Error("Pizza day pro dnešní den již existuje"); } // TODO berka rychlooprava, vyřešit lépe - stahovat jednou, na jediném místě! const pizzaList = await getPizzaList(); const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, ...clientData }; + const today = formatDate(getToday()); await storage.setData(today, data); callNotifikace({ input: { udalost: UdalostEnum.ZAHAJENA_PIZZA, user: creator } }) return data; @@ -60,8 +59,7 @@ export async function createPizzaDay(creator: string): Promise { * Smaže pizza day pro aktuální den. */ export async function deletePizzaDay(login: string): Promise { - const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } @@ -69,6 +67,7 @@ export async function deletePizzaDay(login: string): Promise { throw Error("Login uživatele se neshoduje se zakladatelem Pizza Day"); } delete clientData.pizzaDay; + const today = formatDate(getToday()); await storage.setData(today, clientData); return clientData; } @@ -82,28 +81,35 @@ export async function deletePizzaDay(login: string): Promise { */ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize) { const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } if (clientData.pizzaDay.state !== PizzaDayState.CREATED) { throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED); } - let order: Order | undefined = clientData.pizzaDay.orders.find(o => o.customer === login); + let order: PizzaOrder | undefined = clientData.pizzaDay?.orders?.find(o => o.customer === login); if (!order) { order = { customer: login, pizzaList: [], totalPrice: 0, + hasQr: false, + } + if (!clientData.pizzaDay.orders) { + clientData.pizzaDay.orders = []; } clientData.pizzaDay.orders.push(order); } - const pizzaOrder: PizzaOrder = { + const pizzaOrder: PizzaVariant = { varId: size.varId, name: pizza.name, size: size.size, price: size.price, } + if (!order.pizzaList) { + order.pizzaList = []; + } order.pizzaList.push(pizzaOrder); order.totalPrice += pizzaOrder.price; await storage.setData(today, clientData); @@ -116,26 +122,26 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize * @param login login uživatele * @param pizzaOrder objednávka pizzy */ -export async function removePizzaOrder(login: string, pizzaOrder: PizzaOrder) { +export async function removePizzaOrder(login: string, pizzaOrder: PizzaVariant) { const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } - const orderIndex = clientData.pizzaDay.orders.findIndex(o => o.customer === login); + const orderIndex = clientData.pizzaDay!.orders!.findIndex(o => o.customer === login); if (orderIndex < 0) { throw Error("Nebyly nalezeny žádné objednávky pro uživatele " + login); } - const order = clientData.pizzaDay.orders[orderIndex]; - const index = order.pizzaList.findIndex(o => o.name === pizzaOrder.name && o.size === pizzaOrder.size); + const order = clientData.pizzaDay!.orders![orderIndex]; + const index = order.pizzaList!.findIndex(o => o.name === pizzaOrder.name && o.size === pizzaOrder.size); if (index < 0) { throw Error("Objednávka s danými parametry nebyla nalezena"); } - const price = order.pizzaList[index].price; - order.pizzaList.splice(index, 1); + const price = order.pizzaList![index].price; + order.pizzaList!.splice(index, 1); order.totalPrice -= price; - if (order.pizzaList.length == 0) { - clientData.pizzaDay.orders.splice(orderIndex, 1); + if (order.pizzaList!.length == 0) { + clientData.pizzaDay.orders!.splice(orderIndex, 1); } await storage.setData(today, clientData); return clientData; @@ -149,7 +155,7 @@ export async function removePizzaOrder(login: string, pizzaOrder: PizzaOrder) { */ export async function lockPizzaDay(login: string) { const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } @@ -172,7 +178,7 @@ export async function lockPizzaDay(login: string) { */ export async function unlockPizzaDay(login: string) { const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } @@ -195,7 +201,7 @@ export async function unlockPizzaDay(login: string) { */ export async function finishPizzaOrder(login: string) { const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } @@ -220,7 +226,7 @@ export async function finishPizzaOrder(login: string) { */ export async function finishPizzaDelivery(login: string, bankAccount?: string, bankAccountHolder?: string) { const today = formatDate(getToday()); - const clientData: DayData = await storage.getData(today); + const clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } @@ -234,9 +240,9 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b // Vygenerujeme QR kód, pokud k tomu máme data if (bankAccount?.length && bankAccountHolder?.length) { - for (const order of clientData.pizzaDay.orders) { + for (const order of clientData.pizzaDay.orders!) { if (order.customer !== login) { // zatím platí creator = objednávající, a pro toho nemá QR kód smysl - let message = order.pizzaList.map(pizza => `Pizza ${pizza.name} (${pizza.size})`).join(', '); + let message = order.pizzaList!.map(pizza => `Pizza ${pizza.name} (${pizza.size})`).join(', '); await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message); order.hasQr = true; } @@ -255,15 +261,15 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b */ export async function updatePizzaDayNote(login: string, note?: string) { const today = formatDate(getToday()); - let clientData: DayData = await storage.getData(today); + let clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } if (clientData.pizzaDay.state !== PizzaDayState.CREATED) { throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED); } - const myOrder = clientData.pizzaDay.orders.find(o => o.customer === login); - if (!myOrder || !myOrder.pizzaList.length) { + const myOrder = clientData.pizzaDay.orders!.find(o => o.customer === login); + if (!myOrder?.pizzaList?.length) { throw Error("Pizza day neobsahuje žádné objednávky uživatele " + login); } myOrder.note = note; @@ -282,7 +288,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: DayData = await storage.getData(today); + let clientData = await getClientData(getToday()); if (!clientData.pizzaDay) { throw Error("Pizza day pro dnešní den neexistuje"); } @@ -292,8 +298,8 @@ export async function updatePizzaFee(login: string, targetLogin: string, text?: if (clientData.pizzaDay.creator !== login) { throw Error("Příplatky může měnit pouze zakladatel Pizza day"); } - const targetOrder = clientData.pizzaDay.orders.find(o => o.customer === targetLogin); - if (!targetOrder || !targetOrder.pizzaList.length) { + const targetOrder = clientData.pizzaDay.orders!.find(o => o.customer === targetLogin); + if (!targetOrder?.pizzaList?.length) { throw Error(`Pizza day neobsahuje žádné objednávky uživatele ${targetLogin}`); } if (!price) { diff --git a/server/src/restaurants.ts b/server/src/restaurants.ts index aed651a..580c819 100644 --- a/server/src/restaurants.ts +++ b/server/src/restaurants.ts @@ -1,8 +1,8 @@ import axios from "axios"; import { load } from 'cheerio'; +import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getMenuZastavkaUmichalaMock, getMenuSenkSerikovaMock } from "./mock"; +import { formatDate } from "./utils"; import { Food } from "../../types"; -import {getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getMenuZastavkaUmichalaMock, getMenuSenkSerikovaMock} from "./mock"; -import {formatDate} from "./utils"; // Fráze v názvech jídel, které naznačují že se jedná o polévku const SOUP_NAMES = [ @@ -407,7 +407,7 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean = const currentDate = new Date(firstDayOfWeek); const result: Food[][] = []; let dayIndex = 0; - while(currentDate.getDate() < nowDate) { + while (currentDate.getDate() < nowDate) { result[dayIndex] = [{ amount: undefined, name: "Pro tento den není uveřejněna nabídka jídel", @@ -417,7 +417,7 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean = dayIndex = dayIndex + 1; currentDate.setDate(firstDayOfWeek.getDate() + dayIndex); } - + $('.menicka').each((i, element) => { const currentDayFood: Food[] = []; $(element).find('.popup-gallery li').each((j, element) => { diff --git a/server/src/routes/foodRoutes.ts b/server/src/routes/foodRoutes.ts index 571b874..bb0d20a 100644 --- a/server/src/routes/foodRoutes.ts +++ b/server/src/routes/foodRoutes.ts @@ -1,10 +1,11 @@ import express, { Request } from "express"; import { getLogin, getTrusted } from "../auth"; -import { addChoice, addVolatileData, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service"; +import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service"; import { getDayOfWeekIndex, parseToken } from "../utils"; import { getWebsocket } from "../websocket"; import { callNotifikace } from "../notifikace"; -import { AddChoiceRequest, ChangeDepartureTimeRequest, IDayIndex, RemoveChoiceRequest, RemoveChoicesRequest, UdalostEnum, UpdateNoteRequest } from "../../../types"; +import { AddChoiceRequest, ChangeDepartureTimeRequest, IDayIndex, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types"; +import { UdalostEnum } from "../../../types"; /** * Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň @@ -45,7 +46,7 @@ router.post("/addChoice", async (req: Request<{}, any, AddChoiceRequest>, res, n } try { const data = await addChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); return res.status(200).json(data); } catch (e: any) { next(e) } }); @@ -65,7 +66,7 @@ router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesRequest> } try { const data = await removeChoices(login, trusted, req.body.locationKey, date); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); @@ -85,7 +86,7 @@ router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceRequest>, } try { const data = await removeChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); @@ -109,7 +110,7 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteRequest>, res, date = getDateForWeekIndex(dayIndex); } const data = await updateNote(login, trusted, note, date); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); @@ -128,7 +129,7 @@ router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDeparture } try { const data = await updateDepartureTime(login, req.body?.time, date); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); diff --git a/server/src/routes/pizzaDayRoutes.ts b/server/src/routes/pizzaDayRoutes.ts index 1f1627c..dcd5e42 100644 --- a/server/src/routes/pizzaDayRoutes.ts +++ b/server/src/routes/pizzaDayRoutes.ts @@ -3,7 +3,6 @@ import { getLogin } from "../auth"; import { createPizzaDay, deletePizzaDay, getPizzaList, addPizzaOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee } from "../pizza"; import { parseToken } from "../utils"; import { getWebsocket } from "../websocket"; -import { addVolatileData } from "../service"; import { AddPizzaRequest, FinishDeliveryRequest, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types"; const router = express.Router(); @@ -13,14 +12,14 @@ router.post("/create", async (req: Request<{}, any, undefined>, res) => { const login = getLogin(parseToken(req)); const data = await createPizzaDay(login); res.status(200).json(data); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); }); /** Smaže pizza day pro aktuální den, za předpokladu že existuje. */ router.post("/delete", async (req: Request<{}, any, undefined>, res) => { const login = getLogin(parseToken(req)); const data = await deletePizzaDay(login); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); }); router.post("/add", async (req: Request<{}, any, AddPizzaRequest>, res) => { @@ -44,7 +43,7 @@ router.post("/add", async (req: Request<{}, any, AddPizzaRequest>, res) => { throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex); } const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json({}); }); @@ -54,35 +53,35 @@ router.post("/remove", async (req: Request<{}, any, RemovePizzaRequest>, res) => throw Error("Nebyla předána objednávka"); } const data = await removePizzaOrder(login, req.body?.pizzaOrder); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json({}); }); router.post("/lock", async (req: Request<{}, any, undefined>, res) => { const login = getLogin(parseToken(req)); const data = await lockPizzaDay(login); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json({}); }); router.post("/unlock", async (req: Request<{}, any, undefined>, res) => { const login = getLogin(parseToken(req)); const data = await unlockPizzaDay(login); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json({}); }); router.post("/finishOrder", async (req: Request<{}, any, undefined>, res) => { const login = getLogin(parseToken(req)); const data = await finishPizzaOrder(login); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json({}); }); router.post("/finishDelivery", async (req: Request<{}, any, FinishDeliveryRequest>, res) => { const login = getLogin(parseToken(req)); const data = await finishPizzaDelivery(login, req.body.bankAccount, req.body.bankAccountHolder); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json({}); }); @@ -93,7 +92,7 @@ router.post("/updatePizzaDayNote", async (req: Request<{}, any, UpdatePizzaDayNo throw Error("Poznámka může mít maximálně 70 znaků"); } const data = await updatePizzaDayNote(login, req.body.note); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); @@ -105,7 +104,7 @@ router.post("/updatePizzaFee", async (req: Request<{}, any, UpdatePizzaFeeReques } try { const data = await updatePizzaFee(login, req.body.login, req.body.text, req.body.price); - getWebsocket().emit("message", await addVolatileData(data)); + getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); diff --git a/server/src/routes/statsRoutes.ts b/server/src/routes/statsRoutes.ts index aa20eb0..38c6354 100644 --- a/server/src/routes/statsRoutes.ts +++ b/server/src/routes/statsRoutes.ts @@ -1,8 +1,8 @@ import express, { Request, Response } from "express"; import { getLogin } from "../auth"; import { parseToken } from "../utils"; -import { WeeklyStats } from "../../../types"; import { getStats } from "../stats"; +import { WeeklyStats } from "../../../types"; const router = express.Router(); diff --git a/server/src/routes/votingRoutes.ts b/server/src/routes/votingRoutes.ts index 2aa6672..618b8b7 100644 --- a/server/src/routes/votingRoutes.ts +++ b/server/src/routes/votingRoutes.ts @@ -2,7 +2,8 @@ import express, { Request, Response } from "express"; import { getLogin } from "../auth"; import { parseToken } from "../utils"; import { getUserVotes, updateFeatureVote } from "../voting"; -import { FeatureRequest, UpdateFeatureVoteRequest } from "../../../types"; +import { UpdateFeatureVoteRequest } from "../../../types"; +import { FeatureRequest } from "../../../types"; const router = express.Router(); diff --git a/server/src/service.ts b/server/src/service.ts index ecdd74b..9a73e55 100644 --- a/server/src/service.ts +++ b/server/src/service.ts @@ -1,8 +1,8 @@ import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils"; -import { ClientData, Restaurants, DayMenu, DepartureTime, DayData, WeekMenu, LocationKey } from "../../types"; import getStorage from "./storage"; import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants"; import { getTodayMock } from "./mock"; +import { ClientData, DepartureTime, LunchChoice, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types"; const storage = getStorage(); const MENU_PREFIX = 'menu'; @@ -31,45 +31,31 @@ export const getDateForWeekIndex = (index: number) => { function getEmptyData(date?: Date): ClientData { const usedDate = date || getToday(); return { + todayDayIndex: getDayOfWeekIndex(getToday()), date: getHumanDate(usedDate), isWeekend: getIsWeekend(usedDate), - weekIndex: getDayOfWeekIndex(usedDate), + dayIndex: getDayOfWeekIndex(usedDate), choices: {}, }; } -/** - * Přidá k datům "dopočítaná" data, která nejsou přímo uložena v databázi. - * - * @param data data z databáze - * @returns obohacená data - */ -export async function addVolatileData(data: ClientData): Promise { - data.todayWeekIndex = getDayOfWeekIndex(getToday()); - return data; -} - /** * 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 { - const targetDate = date ?? getToday(); - const dateString = formatDate(targetDate); - const data: DayData = await storage.getData(dateString) || getEmptyData(date); - let clientData: ClientData = { ...data }; + const clientData = await getClientData(date); clientData.menus = { - [Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date), - // [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date), - [Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date), - [Restaurants.ZASTAVKAUMICHALA]: await getRestaurantMenu(Restaurants.ZASTAVKAUMICHALA, date), - [Restaurants.SENKSERIKOVA]: await getRestaurantMenu(Restaurants.SENKSERIKOVA, date), + 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), } - clientData = await addVolatileData(clientData); return clientData; } /** - * Vrátí klíč, pod kterým je uloženo menu pro předané datum. + * 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íč @@ -80,13 +66,13 @@ function getMenuKey(date: Date) { } /** - * Vrátí menu restaurací pro předané datum, pokud již existují. + * 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 předané datum + * @returns menu restaurací pro týden příslušící předanému datu */ async function getMenu(date: Date): Promise { - return await storage.getData(getMenuKey(date)); + return await storage.getData(getMenuKey(date)); } // TODO přesun do restaurants.ts @@ -98,7 +84,7 @@ async function getMenu(date: Date): Promise { * @param date datum, ke kterému získat menu * @param mock příznak, zda chceme pouze mock data */ -export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise { +export async function getRestaurantMenu(restaurant: keyof typeof Restaurant, date?: Date): Promise { const usedDate = date ?? getToday(); const dayOfWeekIndex = getDayOfWeekIndex(usedDate); const now = new Date().getTime(); @@ -110,41 +96,41 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P }; } - let menus = await getMenu(usedDate); - if (menus == null) { - menus = []; + let weekMenu = await getMenu(usedDate); + if (weekMenu == null) { + weekMenu = [{}, {}, {}, {}, {}]; } for (let i = 0; i < 5; i++) { - if (menus[i] == null) { - menus[i] = {}; + if (weekMenu[i] == null) { + weekMenu[i] = {}; } - if (menus[i][restaurant] == null) { - menus[i][restaurant] = { + if (weekMenu[i][restaurant] == null) { + weekMenu[i][restaurant] = { lastUpdate: now, closed: false, food: [], }; } } - if (!menus[dayOfWeekIndex][restaurant]?.food?.length) { + if (!weekMenu[dayOfWeekIndex][restaurant]?.food?.length) { const firstDay = getFirstWorkDayOfWeek(usedDate); const mock = process.env.MOCK_DATA === 'true'; switch (restaurant) { - case Restaurants.SLADOVNICKA: + case 'SLADOVNICKA': try { const sladovnickaFood = await getMenuSladovnicka(firstDay, mock); for (let i = 0; i < sladovnickaFood.length; i++) { - menus[i][restaurant]!.food = sladovnickaFood[i]; + weekMenu[i][restaurant]!.food = sladovnickaFood[i]; // Velice chatrný a nespolehlivý způsob detekce uzavření... if (sladovnickaFood[i].length === 1 && sladovnickaFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') { - menus[i][restaurant]!.closed = true; + weekMenu[i][restaurant]!.closed = true; } } } catch (e: any) { console.error("Selhalo načtení jídel pro podnik Sladovnická", e); } break; - // case Restaurants.UMOTLIKU: + // case 'UMOTLIKU': // try { // const uMotlikuFood = await getMenuUMotliku(firstDay, mock); // for (let i = 0; i < uMotlikuFood.length; i++) { @@ -157,39 +143,39 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P // console.error("Selhalo načtení jídel pro podnik U Motlíků", e); // } // break; - case Restaurants.TECHTOWER: + case 'TECHTOWER': try { const techTowerFood = await getMenuTechTower(firstDay, mock); for (let i = 0; i < techTowerFood.length; i++) { - menus[i][restaurant]!.food = techTowerFood[i]; + weekMenu[i][restaurant]!.food = techTowerFood[i]; if (techTowerFood[i]?.length === 1 && techTowerFood[i][0].name.toLowerCase() === 'svátek') { - menus[i][restaurant]!.closed = true; + weekMenu[i][restaurant]!.closed = true; } } break; } catch (e: any) { console.error("Selhalo načtení jídel pro podnik TechTower", e); } - case Restaurants.ZASTAVKAUMICHALA: + case 'ZASTAVKAUMICHALA': try { const zastavkaUmichalaFood = await getMenuZastavkaUmichala(firstDay, mock); for (let i = 0; i < zastavkaUmichalaFood.length; i++) { - menus[i][restaurant]!.food = zastavkaUmichalaFood[i]; + weekMenu[i][restaurant]!.food = zastavkaUmichalaFood[i]; if (zastavkaUmichalaFood[i]?.length === 1 && zastavkaUmichalaFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') { - menus[i][restaurant]!.closed = true; + weekMenu[i][restaurant]!.closed = true; } } break; } catch (e: any) { console.error("Selhalo načtení jídel pro podnik Zastávka u Michala", e); } - case Restaurants.SENKSERIKOVA: + case 'SENKSERIKOVA': try { const senkSerikovaFood = await getMenuSenkSerikova(firstDay, mock); for (let i = 0; i < senkSerikovaFood.length; i++) { - menus[i][restaurant]!.food = senkSerikovaFood[i]; + weekMenu[i][restaurant]!.food = senkSerikovaFood[i]; if (senkSerikovaFood[i]?.length === 1 && senkSerikovaFood[i][0].name === 'Pro tento den nebylo zadáno menu.') { - menus[i][restaurant]!.closed = true; + weekMenu[i][restaurant]!.closed = true; } } break; @@ -197,9 +183,9 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P console.error("Selhalo načtení jídel pro podnik Pivovarský šenk Šeříková", e); } } - await storage.setData(getMenuKey(usedDate), menus); + await storage.setData(getMenuKey(usedDate), weekMenu); } - return menus[dayOfWeekIndex][restaurant]!; + return weekMenu[dayOfWeekIndex][restaurant]!; } /** @@ -224,9 +210,9 @@ export async function initIfNeeded(date?: Date) { * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoices(login: string, trusted: boolean, locationKey: LocationKey, date?: Date) { +export async function removeChoices(login: string, trusted: boolean, locationKey: keyof typeof LunchChoice, date?: Date) { const selectedDay = formatDate(date ?? getToday()); - let data: DayData = await storage.getData(selectedDay); + let data = await getClientData(date); validateTrusted(data, login, trusted); if (locationKey in data.choices) { if (data.choices[locationKey] && login in data.choices[locationKey]) { @@ -251,15 +237,15 @@ export async function removeChoices(login: string, trusted: boolean, locationKey * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoice(login: string, trusted: boolean, locationKey: LocationKey, foodIndex: number, date?: Date) { +export async function removeChoice(login: string, trusted: boolean, locationKey: keyof typeof LunchChoice, foodIndex: number, date?: Date) { const selectedDay = formatDate(date ?? getToday()); - let data: DayData = await storage.getData(selectedDay); + let data = await getClientData(date); validateTrusted(data, login, trusted); if (locationKey in data.choices) { if (data.choices[locationKey] && login in data.choices[locationKey]) { - const index = data.choices[locationKey][login].options.indexOf(foodIndex); - if (index > -1) { - data.choices[locationKey][login].options.splice(index, 1) + const index = data.choices[locationKey][login].selectedFoods?.indexOf(foodIndex); + if (index && index > -1) { + data.choices[locationKey][login].selectedFoods?.splice(index, 1); await storage.setData(selectedDay, data); } } @@ -274,10 +260,11 @@ export async function removeChoice(login: string, trusted: boolean, locationKey: * @param date datum, ke kterému se volby vztahují * @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje */ -async function removeChoiceIfPresent(login: string, date: string, ignoredLocationKey?: LocationKey) { - let data: DayData = await storage.getData(date); +async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: keyof typeof LunchChoice) { + const usedDate = date ?? getToday(); + let data = await getClientData(usedDate); for (const key of Object.keys(data.choices)) { - const locationKey = key as LocationKey; + const locationKey = key as keyof typeof LunchChoice; if (ignoredLocationKey != null && ignoredLocationKey == locationKey) { continue; } @@ -286,7 +273,7 @@ async function removeChoiceIfPresent(login: string, date: string, ignoredLocatio if (Object.keys(data.choices[locationKey]).length === 0) { delete data.choices[locationKey]; } - await storage.setData(date, data); + await storage.setData(formatDate(usedDate), data); } } return data; @@ -325,19 +312,18 @@ function validateTrusted(data: ClientData, login: string, trusted: boolean) { * @param date datum, ke kterému se volba vztahuje * @returns aktuální data */ -export async function addChoice(login: string, trusted: boolean, locationKey: LocationKey, foodIndex?: number, date?: Date) { +export async function addChoice(login: string, trusted: boolean, locationKey: keyof typeof LunchChoice, foodIndex?: number, date?: Date) { const usedDate = date ?? getToday(); await initIfNeeded(usedDate); - const selectedDate = formatDate(usedDate); - let data: DayData = await storage.getData(selectedDate); + let data = await getClientData(usedDate); validateTrusted(data, login, trusted); await validateFoodIndex(locationKey, foodIndex, date); // Pokud měníme pouze lokaci, mažeme případné předchozí if (foodIndex == null) { - data = await removeChoiceIfPresent(login, selectedDate); + data = await removeChoiceIfPresent(login, usedDate); } else { // Mažeme případné ostatní volby (měla by být maximálně jedna) - removeChoiceIfPresent(login, selectedDate, locationKey); + removeChoiceIfPresent(login, usedDate, locationKey); } // TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce if (!(data.choices[locationKey])) { @@ -349,12 +335,13 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lo } data.choices[locationKey][login] = { trusted, - options: [] + selectedFoods: [] }; } - if (foodIndex != null && !data.choices[locationKey][login].options.includes(foodIndex)) { - data.choices[locationKey][login].options.push(foodIndex); + if (foodIndex != null && !data.choices[locationKey][login].selectedFoods?.includes(foodIndex)) { + data.choices[locationKey][login].selectedFoods?.push(foodIndex); } + const selectedDate = formatDate(usedDate); await storage.setData(selectedDate, data); return data; } @@ -366,7 +353,7 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lo * @param foodIndex index jídla pro danou lokalitu * @param date datum, pro které je validace prováděna */ -async function validateFoodIndex(locationKey: LocationKey, foodIndex?: number, date?: Date) { +async function validateFoodIndex(locationKey: keyof typeof LunchChoice, foodIndex?: number, date?: Date) { if (foodIndex != null) { if (typeof foodIndex !== 'number') { throw Error(`Neplatný index ${foodIndex} typu ${typeof foodIndex}`); @@ -374,13 +361,12 @@ async function validateFoodIndex(locationKey: LocationKey, foodIndex?: number, d if (foodIndex < 0) { throw Error(`Neplatný index ${foodIndex}`); } - if (!(locationKey in Restaurants)) { + if (!Object.keys(Restaurant).includes(locationKey)) { throw Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey} nepodporující indexy`); } const usedDate = date ?? getToday(); - const restaurantKey = Restaurants[locationKey as keyof typeof Restaurants] - const menu = await getRestaurantMenu(restaurantKey, usedDate); - if (foodIndex > (menu.food.length - 1)) { + const menu = await getRestaurantMenu(locationKey as keyof typeof Restaurant, usedDate); + if (menu.food?.length && foodIndex > (menu.food.length - 1)) { throw new Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey}`); } } @@ -397,16 +383,16 @@ async function validateFoodIndex(locationKey: LocationKey, foodIndex?: number, d export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date) { const usedDate = date ?? getToday(); await initIfNeeded(usedDate); - const selectedDate = formatDate(usedDate); - let data: DayData = await storage.getData(selectedDate); + let data = await getClientData(usedDate); validateTrusted(data, login, trusted); const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null); if (userEntry) { - if (!note || !note.length) { + if (!note?.length) { delete userEntry[1][login].note; } else { userEntry[1][login].note = note; } + const selectedDate = formatDate(usedDate); await storage.setData(selectedDate, data); } return data; @@ -420,8 +406,8 @@ export async function updateNote(login: string, trusted: boolean, note?: string, * @param date datum, ke kterému se čas vztahuje */ export async function updateDepartureTime(login: string, time?: string, date?: Date) { - const selectedDate = formatDate(date ?? getToday()); - let clientData: DayData = await storage.getData(selectedDate); + const usedDate = date ?? getToday(); + let clientData = await getClientData(usedDate); const found = Object.values(clientData.choices).find(location => login in location); // TODO validace, že se jedná o restauraci if (found) { @@ -433,7 +419,23 @@ export async function updateDepartureTime(login: string, time?: string, date?: D } found[login].departureTime = time; } - await storage.setData(selectedDate, clientData); + await storage.setData(formatDate(usedDate), clientData); } return clientData; +} + +/** + * 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): Promise { + const targetDate = date ?? getToday(); + const dateString = formatDate(targetDate); + const clientData = await storage.getData(dateString) || getEmptyData(date); + return { + ...clientData, + todayDayIndex: getDayOfWeekIndex(getToday()), + } } \ No newline at end of file diff --git a/server/src/stats.ts b/server/src/stats.ts index 97b657a..6f9d2df 100644 --- a/server/src/stats.ts +++ b/server/src/stats.ts @@ -1,5 +1,6 @@ -import { WeeklyStats, DayData, Locations, DailyStats, LocationKey } from "../../types"; +import { DailyStats, LunchChoice, WeeklyStats } from "../../types"; import { getStatsMock } from "./mock"; +import { getClientData } from "./service"; import getStorage from "./storage"; import { formatDate } from "./utils"; @@ -32,10 +33,14 @@ export async function getStats(startDate: string, endDate: string): Promise { - locationsStats.locations[locationKey as LocationKey] = Object.keys(data.choices[locationKey as LocationKey]!).length; + if (!locationsStats.locations) { + locationsStats.locations = {} + } + // TODO dořešit, tohle je zmatek a té hlášce Sonaru nerozumím + locationsStats.locations[locationKey as keyof typeof LunchChoice] = Object.keys(data.choices[locationKey as keyof typeof LunchChoice]!).length; }) } result.push(locationsStats); diff --git a/server/src/storage/StorageInterface.ts b/server/src/storage/StorageInterface.ts index cc4d6ec..ba034d5 100644 --- a/server/src/storage/StorageInterface.ts +++ b/server/src/storage/StorageInterface.ts @@ -1,5 +1,3 @@ -import { ClientData } from "../../../types"; - /** * Interface pro úložiště dat. * @@ -17,7 +15,7 @@ export interface StorageInterface { * Vrátí veškerá data pro předaný klíč. * @param key klíč, pro který vrátit data (typicky datum) */ - getData(key: string): Promise; + getData(key: string): Promise; /** * Uloží data pod předaný klíč. diff --git a/server/src/utils.ts b/server/src/utils.ts index aadacba..3c29a90 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -1,6 +1,6 @@ -import { Choices, LocationKey } from "../../types"; +import { LunchChoice, LunchChoices } from "../../types"; -const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat(undefined, {weekday: 'long'}); +const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat(undefined, { weekday: 'long' }); /** Vrátí datum v ISO formátu. */ export function formatDate(date: Date, format?: string) { @@ -114,13 +114,13 @@ export const checkBodyParams = (req: any, paramNames: string[]) => { // TODO umístit do samostatného souboru export class InsufficientPermissions extends Error { } -export const getUsersByLocation = (choices: Choices, login: string): string[] => { +export const getUsersByLocation = (choices: LunchChoices, login?: string): string[] => { const result: string[] = []; for (const location of Object.entries(choices)) { - const locationKey = location[0] as LocationKey; + const locationKey = location[0] as keyof typeof LunchChoice; const locationValue = location[1]; - if (locationValue[login]) { + if (login && locationValue[login]) { for (const username in choices[locationKey]) { if (choices[locationKey].hasOwnProperty(username)) { result.push(username); diff --git a/server/src/voting.ts b/server/src/voting.ts index 9a470dc..3bec667 100644 --- a/server/src/voting.ts +++ b/server/src/voting.ts @@ -15,7 +15,7 @@ const STORAGE_KEY = 'voting'; * @returns pole voleb */ export async function getUserVotes(login: string) { - const data: VotingData = await storage.getData(STORAGE_KEY); + const data = await storage.getData(STORAGE_KEY); return data?.[login] || []; } @@ -28,7 +28,7 @@ export async function getUserVotes(login: string) { * @returns aktuální data */ export async function updateFeatureVote(login: string, option: FeatureRequest, active: boolean): Promise { - let data: VotingData = await storage.getData(STORAGE_KEY); + let data = await storage.getData(STORAGE_KEY); if (data == null) { data = {}; } diff --git a/server/yarn.lock b/server/yarn.lock index 2529ff3..2b6b170 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -3264,9 +3264,9 @@ node-releases@^2.0.18: integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== nodemon@^3.1.0: - version "3.1.7" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.7.tgz#07cb1f455f8bece6a499e0d72b5e029485521a54" - integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== + version "3.1.9" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.9.tgz#df502cdc3b120e1c3c0c6e4152349019efa7387b" + integrity sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg== dependencies: chokidar "^3.5.2" debug "^4" diff --git a/types/RequestTypes.ts b/types/RequestTypes.ts index 7e15862..fc517c7 100644 --- a/types/RequestTypes.ts +++ b/types/RequestTypes.ts @@ -1,7 +1,7 @@ -import { FeatureRequest, LocationKey, PizzaOrder } from "./Types"; +import { FeatureRequest, LunchChoice, PizzaVariant } from "../types"; export type ILocationKey = { - locationKey: LocationKey, + locationKey: keyof typeof LunchChoice, } export type IDayIndex = { @@ -37,7 +37,7 @@ export type AddPizzaRequest = { } export type RemovePizzaRequest = { - pizzaOrder: PizzaOrder, + pizzaOrder: PizzaVariant, } export type UpdatePizzaDayNoteRequest = { diff --git a/types/Types.ts b/types/Types.ts deleted file mode 100644 index 106601c..0000000 --- a/types/Types.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** Výčtový typ pro restaurace, pro které umíme získat a parsovat obědové menu. */ -export enum Restaurants { - SLADOVNICKA = 'sladovnicka', - // UMOTLIKU = 'uMotliku', - TECHTOWER = 'techTower', - ZASTAVKAUMICHALA = 'zastavkaUmichala', - SENKSERIKOVA = 'senkSerikova', -} - -export type FoodChoices = { - trusted: boolean, - options: number[], - departureTime?: string, - note?: string, -} - -// TODO okomentovat / rozdělit -export type Choices = { - [location in LocationKey]?: { - [login: string]: FoodChoices - } -} - -/** Velikost konkrétní pizzy */ -export type PizzaSize = { - varId: number, // unikátní ID varianty pizzy - size: string, // velikost pizzy, např. "30cm" - pizzaPrice: number, // cena samotné pizzy - boxPrice: number, // cena krabice - price: number, // celková cena (pizza + krabice) -} - -/** Jedna konkrétní pizza */ -export type Pizza = { - name: string, // název pizzy - ingredients: string[], // seznam ingrediencí - sizes: PizzaSize[], // dostupné velikosti pizzy -} - -/** Objednávka jedné konkrétní pizzy */ -export type PizzaOrder = { - varId: number, // unikátní ID varianty pizzy - name: string, // název pizzy - size: string, // velikost pizzy jako string (30cm) - price: number, // cena pizzy v Kč, včetně krabice -} - -/** Celková objednávka jednoho člověka */ -export type Order = { - customer: string, // jméno objednatele - pizzaList: PizzaOrder[], // seznam objednaných pizz - fee?: { text?: string, price: number }, // příplatek (např. za extra ingredience) - totalPrice: number, // celková cena všech objednaných pizz, krabic a příplatků - hasQr?: boolean, // true, pokud je k objednávce vygenerován QR kód pro platbu - note?: string, // volitelná uživatelská poznámka k objednávce -} - -/** Stav pizza dne */ -export enum PizzaDayState { - NOT_CREATED, // Pizza day nebyl založen - CREATED, // Pizza day je založen - LOCKED, // Objednávky uzamčeny - ORDERED, // Pizzy objednány - DELIVERED // Pizzy doručeny -} - -/** Informace o pizza day pro dnešní den */ -interface PizzaDay { - state: PizzaDayState, // stav pizza dne - creator: string, // jméno zakladatele - orders: Order[], // seznam objednávek jednotlivých lidí -} - -/** Týdenní menu jednotlivých restaurací. */ -export type WeekMenu = { - [dayIndex: number]: { - [restaurant in Restaurants]?: DayMenu - } -} - -/** Data vztahující se k jednomu konkrétnímu dni. */ -export type 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ů - menus?: { [restaurant in Restaurants]?: DayMenu }, // menu jednotlivých restaurací - pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje - pizzaList?: Pizza[], // seznam dostupných pizz pro dnešní den - pizzaListLastUpdate?: Date, // datum a čas poslední aktualizace pizz -} - -/** Veškerá data pro zobrazení na klientovi. */ -export type ClientData = DayData & { - todayWeekIndex?: number, // index dnešního dne v týdnu (0-6) -} - -/** Nabídka jídel jednoho podniku pro jeden konkrétní den. */ -export type DayMenu = { - lastUpdate: number, // UNIX timestamp poslední aktualizace menu - closed: boolean, // příznak, zda je daný podnik v tento den zavřený - food: Food[], // seznam jídel v menu -} - -/** Jídlo z obědového menu restaurace. */ -export type Food = { - amount?: string, // množství standardní porce, např. 0,33l nebo 150g - name: string, // název/popis jídla - price: string, // cena ve formátu '135 Kč' - isSoup: boolean, // příznak, zda se jedná o polévku -} - -// TODO tohle je dost špatné pojmenování, vzhledem k tomu co to obsahuje -// TODO pokud by se použilo ovládáni výběru obědu kliknutím, pak bych restaurace z tohoto výčtu vyhodil -export enum Locations { - SLADOVNICKA = 'Sladovnická', - // UMOTLIKU = 'U Motlíků', - TECHTOWER = 'TechTower', - ZASTAVKAUMICHALA = 'Zastávka u Michala', - SENKSERIKOVA = 'Pivovarský šenk Šeříková', - SPSE = 'SPŠE', - PIZZA = 'Pizza day', - OBJEDNAVAM = 'Budu objednávat', - NEOBEDVAM = 'Mám vlastní/neobědvám', - ROZHODUJI = 'Rozhoduji se', -} - -// TODO totéž -export type LocationKey = keyof typeof Locations; - -export enum UdalostEnum { - ZAHAJENA_PIZZA = "Zahájen pizza day", - OBJEDNANA_PIZZA = "Objednána pizza", - JDEME_NA_OBED = "Jdeme na oběd", -} - -export type NotififaceInput = { - udalost: UdalostEnum, - user: string, -} - -export type NotifikaceData = { - input: NotififaceInput, - gotify?: boolean, - teams?: boolean, - ntfy?: boolean, -} - -export type GotifyServer = { - server: string; - api_keys: string[]; -} - -/** Čas preferovaného odchodu na oběd. */ -export enum DepartureTime { - T10_00 = "10:00", - T10_15 = "10:15", - T10_30 = "10:30", - T10_45 = "10:45", - T11_00 = "11:00", - T11_15 = "11:15", - T11_30 = "11:30", - T11_45 = "11:45", - T12_00 = "12:00", - T12_15 = "12:15", - T12_30 = "12:30", - T12_45 = "12:45", - T13_00 = "13:00", -} - -export enum FeatureRequest { - CUSTOM_QR = "Ruční generování QR kódů mimo Pizza day (např. při objednávání)", - FAVORITES = "Možnost označovat si jídla jako oblíbená (taková jídla by se uživateli následně zvýrazňovala)", - SINGLE_PAYMENT = "Možnost úhrady v podniku za všechny jednou osobou a následné generování QR ostatním", - NO_WEEKENDS = "Zrušení \"užívejte víkend\", místo toho umožnit zpětně náhled na uplynulý týden", - QR_FOREVER = "Umožnění zobrazení vygenerovaného QR kódu i po následující dny (dokud ho uživatel ručně \"nezavře\", např. tlačítkem \"Zaplatil jsem\")", - PIZZA_PICTURES = "Zobrazování náhledů (fotografií) pizz v rámci Pizza day", - STATISTICS = "Statistiky (nejoblíbenější podnik, nejpopulárnější jídla, nejobjednávanější pizzy, nejčastější uživatelé, ...)", - RESPONSIVITY = "Vylepšení responzivního designu", - SECURITY = "Zvýšení zabezpečení aplikace", - SAFETY = "Zvýšená ochrana proti chybám uživatele (potvrzovací dialogy, překliky, ...)", - UI = "Celkové vylepšení UI/UX", - DEVELOPMENT = "Zlepšení dokumentace/postupů pro ostatní vývojáře" -} - -export type EasterEgg = { - path: string; - url: string; - startOffset: number; - endOffset: number; - duration: number; - width?: string; - zIndex?: number; - position?: "absolute"; - animationName?: string; - animationDuration?: string; - animationTimingFunction?: string; -} - -// TODO aktuálně se k ničemu nepoužívá -export type AnimationPosition = { - left?: string, - startLeft?: string, - "--start-left"?: string, - right?: string, - startRight?: string, - "--start-right"?: string, - top?: string, - startTop?: string, - "--start-top"?: string, - bottom?: string, - startBottom?: string, - "--start-bottom"?: string, - endLeft?: string, - "--end-left"?: string, - endRight?: string, - "--end-right"?: string, - endTop?: string, - "--end-top"?: string, - endBottom?: string, - "--end-bottom"?: string, - rotate?: string, -} - -/** Statistiky pro jeden konkrétní den. */ -export type DailyStats = { - date: string, // zkrácené datum v human-readable formátu (např. 24.02.) - locations: { [location in LocationKey]?: number } -} - -/** Statistiky pro jeden konkrétní týden (pondělí-pátek) */ -export type WeeklyStats = [DailyStats, DailyStats, DailyStats, DailyStats, DailyStats]; \ No newline at end of file diff --git a/types/api.yml b/types/api.yml new file mode 100644 index 0000000..ddcd6ce --- /dev/null +++ b/types/api.yml @@ -0,0 +1,650 @@ +openapi: 3.0.4 +info: + title: Luncher API + version: 1.0.0 +servers: + - url: /api +paths: + /login: + post: + summary: Přihlášení uživatele + security: [] # Nevyžaduje autentizaci + requestBody: + content: + application/json: + schema: + type: object + properties: + login: + type: string + description: Přihlašovací jméno uživatele. Vyžadováno pouze pokud není předáno pomocí hlaviček. + responses: + "200": + description: Přihlášení bylo úspěšné + content: + application/json: + schema: + $ref: "#/components/schemas/JWTToken" + /qr: + get: + summary: Získání QR kódu pro platbu za Pizza day + security: [] # Nevyžaduje autentizaci + parameters: + - in: query + name: login + schema: + type: string + required: true + description: Přihlašovací jméno uživatele, pro kterého bude vrácen QR kód + responses: + "200": + description: Vygenerovaný QR kód pro platbu + content: + image/png: + schema: + type: string + format: binary + /data: + get: + summary: Načtení klientských dat pro aktuální nebo předaný den + parameters: + - in: query + name: dayIndex + description: Index dne v týdnu. Pokud není předán, je použit aktuální den. + schema: + type: integer + minimum: 0 + maximum: 4 + responses: + "200": + $ref: "#/components/responses/ClientDataResponse" + /addChoice: + post: + summary: Přidání či nahrazení volby uživatele pro zvolený den/podnik + requestBody: + required: true + content: + application/json: + schema: + required: + - locationKey + allOf: + - locationKey: + $ref: "#/components/schemas/LunchChoice" + - dayIndex: + $ref: "#/components/schemas/DayIndex" + - foodIndex: + $ref: "#/components/schemas/FoodIndex" + responses: + "200": + $ref: "#/components/responses/ClientDataResponse" + /removeChoices: + post: + summary: Odstranění volby uživatele pro zvolený den/podnik, včetně případných jídel + requestBody: + required: true + content: + application/json: + schema: + required: + - locationKey + allOf: + - locationKey: + $ref: "#/components/schemas/LunchChoice" + - dayIndex: + $ref: "#/components/schemas/DayIndex" + responses: + "200": + $ref: "#/components/responses/ClientDataResponse" +components: + schemas: + # --- OBECNÉ --- + JWTToken: + type: object + description: Klientský JWT token pro autentizaci a autorizaci + required: + - login + - trusted + - iat + properties: + login: + type: string + description: Přihlašovací jméno uživatele + trusted: + type: boolean + description: Příznak, zda se jedná o uživatele ověřeného doménovým přihlášením + iat: + type: number + description: Časové razítko vydání tokenu + ClientData: + description: Klientská data pro jeden konkrétní den. Obsahuje menu všech načtených podniků a volby jednotlivých uživatelů. + type: object + additionalProperties: false + required: + - todayDayIndex + - date + - isWeekend + - choices + properties: + todayDayIndex: + description: Index dnešního dne v týdnu + $ref: "#/components/schemas/DayIndex" + date: + description: Human-readable datum dne + type: string + isWeekend: + description: Příznak, zda je tento den víkend + type: boolean + dayIndex: + description: Index dne v týdnu, ke kterému se vztahují tato data + $ref: "#/components/schemas/DayIndex" + choices: + $ref: "#/components/schemas/LunchChoices" + menus: + $ref: "#/components/schemas/RestaurantDayMenuMap" + pizzaDay: + $ref: "#/components/schemas/PizzaDay" + pizzaList: + description: Seznam dostupných pizz pro předaný den + type: array + items: + $ref: "#/components/schemas/Pizza" + pizzaListLastUpdate: + description: Datum a čas poslední aktualizace pizz + type: string + format: date-time + + # --- OBĚDY --- + UserLunchChoice: + description: Konkrétní volba stravování jednoho uživatele v konkrétní den. Může se jednat jak o stravovací podnik, tak možnosti "budu objednávat", "neobědvám" apod. + additionalProperties: false + properties: + # TODO toto je tu z dost špatného důvodu, viz použití - mělo by se místo toho z loginu zjišťovat zda je uživatel trusted + trusted: + description: Příznak, zda byla tato volba provedena uživatelem ověřeným doménovým přihlášením + type: boolean + selectedFoods: + description: Pole indexů vybraných jídel v rámci dané restaurace. Index představuje pořadí jídla v menu dané restaurace. + type: array + items: + type: integer + departureTime: + description: Čas preferovaného odchodu do dané restaurace v human-readable formátu (např. 12:00) + type: string + note: + description: Volitelná, veřejně viditelná uživatelská poznámka k vybrané volbě + type: string + LocationLunchChoicesMap: + description: Objekt, kde klíčem je možnost stravování ((#/components/schemas/LunchChoice)) a hodnotou množina uživatelů s touto volbou ((#/components/schemas/LunchChoices)). + type: object + additionalProperties: + $ref: "#/components/schemas/UserLunchChoice" + LunchChoices: + description: Objekt, představující volby všech uživatelů pro konkrétní den. Klíčem je (#/components/schemas/LunchChoice). + type: object + properties: + SLADOVNICKA: + $ref: "#/components/schemas/LocationLunchChoicesMap" + TECHTOWER: + $ref: "#/components/schemas/LocationLunchChoicesMap" + ZASTAVKAUMICHALA: + $ref: "#/components/schemas/LocationLunchChoicesMap" + SENKSERIKOVA: + $ref: "#/components/schemas/LocationLunchChoicesMap" + SPSE: + $ref: "#/components/schemas/LocationLunchChoicesMap" + PIZZA: + $ref: "#/components/schemas/LocationLunchChoicesMap" + OBJEDNAVAM: + $ref: "#/components/schemas/LocationLunchChoicesMap" + NEOBEDVAM: + $ref: "#/components/schemas/LocationLunchChoicesMap" + ROZHODUJI: + $ref: "#/components/schemas/LocationLunchChoicesMap" + Restaurant: + description: Stravovací zařízení (restaurace, jídelna, hospoda, ...) + type: string + enum: + - Sladovnická + - TechTower + - Zastávka u Michala + - Šenk Šeříková + x-enum-varnames: + - SLADOVNICKA + - TECHTOWER + - ZASTAVKAUMICHALA + - SENKSERIKOVA + LunchChoice: + description: Konkrétní možnost stravování (konkrétní restaurace, pizza day, objednání, neobědvání, rozhodování se, ...) + type: string + enum: + - Sladovnická + - TechTower + - Zastávka u Michala + - Šenk Šeříková + - SPŠE + - Pizza day + - Budu objednávat + - Neobědvám + - Rozhoduji se + x-enum-varnames: + - SLADOVNICKA + - TECHTOWER + - ZASTAVKAUMICHALA + - SENKSERIKOVA + - SPSE + - PIZZA + - OBJEDNAVAM + - NEOBEDVAM + - ROZHODUJI + DayIndex: + description: Index dne v týdnu (0 = pondělí, 4 = pátek) + type: integer + minimum: 0 + maximum: 4 + FoodIndex: + description: Pořadový index jídla v menu konkrétní restaurace + type: integer + minimum: 0 + Food: + description: Konkrétní jídlo z menu restaurace + type: object + additionalProperties: false + required: + - name + - isSoup + properties: + amount: + description: Množství standardní porce, např. 0,33l nebo 150g + type: string + name: + description: Název/popis jídla + type: string + price: + description: Cena ve formátu '135 Kč' + type: string + isSoup: + description: Příznak, zda se jedná o polévku + type: boolean + RestaurantDayMenu: + description: Menu restaurace na konkrétní den + type: object + additionalProperties: false + properties: + lastUpdate: + description: UNIX timestamp poslední aktualizace menu + type: integer + closed: + description: Příznak, zda je daný podnik v daný den zavřený + type: boolean + food: + description: Seznam jídel pro daný den + type: array + items: + $ref: "#/components/schemas/Food" + RestaurantDayMenuMap: + description: Objekt, kde klíčem je podnik ((#/components/schemas/Restaurant)) a hodnotou denní menu daného podniku ((#/components/schemas/RestaurantDayMenu)) + type: object + additionalProperties: false + properties: + SLADOVNICKA: + $ref: "#/components/schemas/RestaurantDayMenu" + TECHTOWER: + $ref: "#/components/schemas/RestaurantDayMenu" + ZASTAVKAUMICHALA: + $ref: "#/components/schemas/RestaurantDayMenu" + SENKSERIKOVA: + $ref: "#/components/schemas/RestaurantDayMenu" + WeekMenu: + description: Pole týdenních menu jednotlivých podniků. Indexem je den v týdnu (0 = pondělí, 4 = pátek), hodnotou denní menu daného podniku. + type: array + minItems: 5 + maxItems: 5 + items: + $ref: "#/components/schemas/RestaurantDayMenuMap" + DepartureTime: + description: Preferovaný čas odchodu na oběd + type: string + enum: + - "10:00" + - "10:15" + - "10:30" + - "10:45" + - "11:00" + - "11:15" + - "11:30" + - "11:45" + - "12:00" + - "12:15" + - "12:30" + - "12:45" + - "13:00" + x-enum-varnames: + - T10_00 + - T10_15 + - T10_30 + - T10_45 + - T11_00 + - T11_15 + - T11_30 + - T11_45 + - T12_00 + - T12_15 + - T12_30 + - T12_45 + - T13_00 + + # --- HLASOVÁNÍ --- + FeatureRequest: + type: string + enum: + - Ruční generování QR kódů mimo Pizza day (např. při objednávání) + - Možnost označovat si jídla jako oblíbená (taková jídla by se uživateli následně zvýrazňovala) + - Možnost úhrady v podniku za všechny jednou osobou a následné generování QR ostatním + - Zrušení \"užívejte víkend\", místo toho umožnit zpětně náhled na uplynulý týden + - Umožnění zobrazení vygenerovaného QR kódu i po následující dny (dokud ho uživatel ručně \"nezavře\", např. tlačítkem \"Zaplatil jsem\") + - Zobrazování náhledů (fotografií) pizz v rámci Pizza day + - Statistiky (nejoblíbenější podnik, nejpopulárnější jídla, nejobjednávanější pizzy, nejčastější uživatelé, ...) + - Vylepšení responzivního designu + - Zvýšení zabezpečení aplikace + - Zvýšená ochrana proti chybám uživatele (potvrzovací dialogy, překliky, ...) + - Celkové vylepšení UI/UX + - Zlepšení dokumentace/postupů pro ostatní vývojáře + x-enum-varnames: + - CUSTOM_QR + - FAVORITES + - SINGLE_PAYMENT + - NO_WEEKENDS + - QR_FOREVER + - PIZZA_PICTURES + - STATISTICS + - RESPONSIVITY + - SECURITY + - SAFETY + - UI + - DEVELOPMENT + + # --- EASTER EGGS --- + EasterEgg: + description: Data pro zobrazení easter eggů + type: object + additionalProperties: false + required: + - path + - url + - startOffset + - endOffset + - duration + properties: + path: + type: string + url: + type: string + startOffset: + type: number + endOffset: + type: number + duration: + type: number + width: + type: string + zIndex: + type: integer + position: + type: string + enum: + - absolute + animationName: + type: string + animationDuration: + type: string + animationTimingFunction: + type: string + + # --- STATISTIKY --- + LocationStats: + description: Objekt, kde klíčem je zvolená možnost a hodnotou počet uživatelů, kteří tuto možnosti zvolili + type: object + additionalProperties: false + properties: + # Bohužel OpenAPI neumí nadefinovat objekt, kde klíčem může být pouze hodnota existujícího enumu :( + SLADOVNICKA: + type: number + TECHTOWER: + type: number + ZASTAVKAUMICHALA: + type: number + SENKSERIKOVA: + type: number + SPSE: + type: number + PIZZA: + type: number + OBJEDNAVAM: + type: number + NEOBEDVAM: + type: number + ROZHODUJI: + type: number + DailyStats: + description: Statistika vybraných možností pro jeden konkrétní den + type: object + additionalProperties: false + required: + - date + - locations + properties: + date: + description: Datum v human-readable formátu + type: string + locations: + $ref: "#/components/schemas/LocationStats" + WeeklyStats: + description: Pole statistik vybraných možností pro jeden konkrétní týden. Index představuje den v týdnu (0 = pondělí, 4 = pátek) + type: array + minItems: 5 + maxItems: 5 + items: + $ref: "#/components/schemas/DailyStats" + + # --- PIZZA DAY --- + PizzaDayState: + description: Stav pizza day + type: string + enum: + - Pizza day nebyl založen + - Pizza day je založen + - Objednávky uzamčeny + - Pizzy objednány + - Pizzy doručeny + x-enum-varnames: + - NOT_CREATED + - CREATED + - LOCKED + - ORDERED + - DELIVERED + # TODO toto je jen rozšířená varianta PizzaVariant - sloučit do jednoho objektu + PizzaSize: + description: Údaje o konkrétní variantě pizzy + type: object + additionalProperties: false + required: + - varId + - size + - pizzaPrice + - boxPrice + - price + properties: + varId: + description: Unikátní identifikátor varianty pizzy + type: integer + size: + description: Velikost pizzy, např. "30cm" + type: string + pizzaPrice: + description: Cena samotné pizzy v Kč + type: number + boxPrice: + description: Cena krabice pizzy v Kč + type: number + price: + description: Celková cena (pizza + krabice) + type: number + Pizza: + description: Údaje o konkrétní pizze. + type: object + additionalProperties: false + required: + - name + - ingredients + - sizes + properties: + name: + description: Název pizzy + type: string + ingredients: + description: Seznam obsažených ingrediencí + type: array + items: + type: string + sizes: + description: Dostupné velikosti pizzy + type: array + items: + $ref: "#/components/schemas/PizzaSize" + PizzaVariant: + description: Konkrétní varianta (velikost) jedné pizzy. + type: object + additionalProperties: false + required: + - varId + - name + - size + - price + properties: + varId: + description: Unikátní identifikátor varianty pizzy + type: integer + name: + description: Název pizzy + type: string + size: + description: Velikost pizzy (např. "30cm") + type: string + price: + description: Cena pizzy v Kč, včetně krabice + type: number + PizzaOrder: + description: Údaje o objednávce pizzy jednoho uživatele. + type: object + additionalProperties: false + required: + - customer + - totalPrice + - hasQr + properties: + customer: + description: Jméno objednávajícího uživatele + type: string + pizzaList: + description: Seznam variant pizz k objednání (typicky bývá jen jedna) + type: array + items: + $ref: "#/components/schemas/PizzaVariant" + fee: + description: Příplatek (např. za extra ingredience) + type: object + properties: + text: + description: Popis příplatku (např. "kuřecí maso navíc") + type: string + price: + description: Cena příplatku v Kč + type: number + totalPrice: + description: Celková cena všech objednaných pizz daného uživatele, včetně krabic a příplatků + type: number + hasQr: + description: | + Příznak, pokud je k této objednávce vygenerován QR kód pro platbu. To je typicky pravda, pokud: + - objednávající má v nastavení vyplněno číslo účtu + - pizza day je ve stavu DELIVERED (Pizzy byly doručeny) + note: + description: Volitelná uživatelská poznámka pro objednávajícího (např. "bez oliv") + type: string + PizzaDay: + description: Data o Pizza day pro konkrétní den + type: object + additionalProperties: false + properties: + state: + $ref: "#/components/schemas/PizzaDayState" + creator: + description: "Jméno zakladatele pizza day" + type: string + orders: + description: Pole objednávek jednotlivých uživatelů + type: array + items: + $ref: "#/components/schemas/PizzaOrder" + + # --- NOTIFIKACE --- + UdalostEnum: + type: string + enum: + - Zahájen pizza day + - Objednána pizza + - Jdeme na oběd + x-enum-varnames: + - ZAHAJENA_PIZZA + - OBJEDNANA_PIZZA + - JDEME_NA_OBED + NotifikaceInput: + type: object + required: + - udalost + - user + properties: + udalost: + $ref: "#/components/schemas/UdalostEnum" + user: + type: string + NotifikaceData: + type: object + required: + - input + properties: + input: + $ref: "#/components/schemas/NotifikaceInput" + gotify: + type: boolean + teams: + type: boolean + ntfy: + type: boolean + GotifyServer: + type: object + required: + - server + - api_keys + properties: + server: + type: string + api_keys: + type: array + items: + type: string + responses: + ClientDataResponse: + description: Aktuální data pro klienta + content: + application/json: + schema: + $ref: "#/components/schemas/ClientData" + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT +security: + - bearerAuth: [] diff --git a/types/index.ts b/types/index.ts index 7401e5b..1f3874c 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,2 +1,2 @@ -export * from './Types'; -export * from './RequestTypes'; \ No newline at end of file +export * from './RequestTypes'; +export * from './gen'; \ No newline at end of file diff --git a/types/openapi-ts.config.ts b/types/openapi-ts.config.ts new file mode 100644 index 0000000..b63c998 --- /dev/null +++ b/types/openapi-ts.config.ts @@ -0,0 +1,14 @@ +import { defaultPlugins } from '@hey-api/openapi-ts'; + +export default { + input: 'api.yml', + output: 'gen', + plugins: [ + ...defaultPlugins, + '@hey-api/client-fetch', + { + enums: 'javascript', + name: '@hey-api/typescript', + }, + ], +}; \ No newline at end of file diff --git a/types/package.json b/types/package.json new file mode 100644 index 0000000..40ec203 --- /dev/null +++ b/types/package.json @@ -0,0 +1,11 @@ +{ + "name": "@luncher/types", + "version": "1.0.0", + "license": "MIT", + "private": true, + "devDependencies": { + "@hey-api/client-fetch": "^0.8.2", + "@hey-api/openapi-ts": "^0.64.7", + "typescript": "^5.0.2" + } +} diff --git a/types/tsconfig.json b/types/tsconfig.json index 9e6febb..e825d4d 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,8 +1,6 @@ { "compilerOptions": { "declaration": true, - // "emitDeclarationOnly": true, - // "outDir": "./dist", "noEmit": true }, "include": [ diff --git a/types/yarn.lock b/types/yarn.lock new file mode 100644 index 0000000..3a83ce1 --- /dev/null +++ b/types/yarn.lock @@ -0,0 +1,309 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@hey-api/client-fetch@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.8.2.tgz#675aadfbc9478bb8eef5679f11a9334258dff4c8" + integrity sha512-61T4UGfAzY5345vMxWDX8qnSTNRJcOpWuZyvNu3vNebCTLPwMQAM85mhEuBoACdWeRtLhNoUjU0UR5liRyD1bA== + +"@hey-api/json-schema-ref-parser@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.2.tgz#c3824c5d9d531eeb5c2b2557857a8ad20b5c75a7" + integrity sha512-F6LSkttZcT/XiX3ydeDqTY3uRN3BLJMwyMTk4kg/ichZlKUp3+3Odv0WokSmXGSoZGTW/N66FROMYAm5NPdJlA== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.15" + js-yaml "^4.1.0" + +"@hey-api/openapi-ts@^0.64.7": + version "0.64.7" + resolved "https://registry.yarnpkg.com/@hey-api/openapi-ts/-/openapi-ts-0.64.7.tgz#f239d268b4a35b91f5ff25479d15578feb01f365" + integrity sha512-xpaBzdGAKz7cPuGah1GZWl3zTZquOXRnwmpVCQKEUpvDtSYWBLjSxtAYIqDBjwJkuNHcakZBIWLcappx7slc2g== + dependencies: + "@hey-api/json-schema-ref-parser" "1.0.2" + c12 "2.0.1" + commander "13.0.0" + handlebars "4.7.8" + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +c12@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/c12/-/c12-2.0.1.tgz#5702d280b31a08abba39833494c9b1202f0f5aec" + integrity sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A== + dependencies: + chokidar "^4.0.1" + confbox "^0.1.7" + defu "^6.1.4" + dotenv "^16.4.5" + giget "^1.2.3" + jiti "^2.3.0" + mlly "^1.7.1" + ohash "^1.1.4" + pathe "^1.1.2" + perfect-debounce "^1.0.0" + pkg-types "^1.2.0" + rc9 "^2.1.2" + +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +citty@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== + dependencies: + consola "^3.2.3" + +commander@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-13.0.0.tgz#1b161f60ee3ceb8074583a0f95359a4f8701845c" + integrity sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ== + +confbox@^0.1.7, confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +consola@^3.2.3, consola@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.0.tgz#4cfc9348fd85ed16a17940b3032765e31061ab88" + integrity sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA== + +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + +destr@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449" + integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ== + +dotenv@^16.4.5: + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +giget@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.5.tgz#0bd4909356a0da75cc1f2b33538f93adec0d202f" + integrity sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug== + dependencies: + citty "^0.1.6" + consola "^3.4.0" + defu "^6.1.4" + node-fetch-native "^1.6.6" + nypm "^0.5.4" + pathe "^2.0.3" + tar "^6.2.1" + +handlebars@4.7.8: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +jiti@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560" + integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mlly@^1.7.1, mlly@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f" + integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw== + dependencies: + acorn "^8.14.0" + pathe "^2.0.1" + pkg-types "^1.3.0" + ufo "^1.5.4" + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-fetch-native@^1.6.6: + version "1.6.6" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz#ae1d0e537af35c2c0b0de81cbff37eedd410aa37" + integrity sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ== + +nypm@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.5.4.tgz#a5ab0d8d37f96342328479f88ef58699f29b3051" + integrity sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA== + dependencies: + citty "^0.1.6" + consola "^3.4.0" + pathe "^2.0.3" + pkg-types "^1.3.1" + tinyexec "^0.3.2" + ufo "^1.5.4" + +ohash@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" + integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== + +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +pkg-types@^1.2.0, pkg-types@^1.3.0, pkg-types@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== + dependencies: + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +rc9@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.1.2.tgz#6282ff638a50caa0a91a31d76af4a0b9cbd1080d" + integrity sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg== + dependencies: + defu "^6.1.4" + destr "^2.0.3" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +tar@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +typescript@^5.0.2: + version "5.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" + integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== + +ufo@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + +uglify-js@^3.1.4: + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==