Migrace na OpenAPI - TypeScript typy

This commit is contained in:
2025-03-05 21:05:21 +01:00
parent d144c55bf7
commit d69e09afee
40 changed files with 1295 additions and 550 deletions

View File

@@ -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<boolean>(false);
const [data, setData] = useState<ClientData>();
const [food, setFood] = useState<{ [key in Restaurants]?: DayMenu }>();
const [myOrder, setMyOrder] = useState<Order>();
const [food, setFood] = useState<RestaurantDayMenuMap>();
const [myOrder, setMyOrder] = useState<PizzaOrder>();
const [foodChoiceList, setFoodChoiceList] = useState<Food[]>();
const [closed, setClosed] = useState<boolean>(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<HTMLSelectElement>) => {
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<HTMLSelectElement>) => {
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 = <h3>Zavřeno</h3>
} else if (menu?.food?.length > 0) {
} else if (menu?.food?.length && menu.food.length > 0) {
const hideSoups = settings?.hideSoups;
content = <Table striped bordered hover>
<tbody style={{ cursor: 'pointer' }}>
@@ -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 &&
<div className='day-navigator'>
<FontAwesomeIcon title="Předchozí den" icon={faChevronLeft} style={{ cursor: "pointer", visibility: dayIndex > 0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} />
<h1 className='title' style={{ color: dayIndex === data.todayWeekIndex ? 'black' : 'gray' }}>{data.date}</h1>
<h1 className='title' style={{ color: dayIndex === data.todayDayIndex ? 'black' : 'gray' }}>{data.date}</h1>
<FontAwesomeIcon title="Následující den" icon={faChevronRight} style={{ cursor: "pointer", visibility: dayIndex < 4 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex + 1)} />
</div>
}
<Row className='food-tables'>
{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'])}
</Row>
<div className='content-wrapper'>
<div className='content'>
{canChangeChoice && <>
<p>{`Jak to ${dayIndex == null || dayIndex === data.todayWeekIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<p>{`Jak to ${dayIndex == null || dayIndex === data.todayDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<Form.Select ref={choiceRef} onChange={doAddChoice}>
<option></option>
{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 => <option key={entry[0]} value={entry[0]}>{entry[1]}</option>)}
</Form.Select>
@@ -460,8 +460,8 @@ function App() {
<Form.Select ref={departureChoiceRef} onChange={handleChangeDepartureTime}>
<option></option>
{Object.values(DepartureTime)
.filter(time => isInTheFuture(time))
.map(time => <option key={time} value={time}>{time}</option>)}
.filter(time => isInTheFuture(time))
.map(time => <option key={time} value={time}>{time}</option>)}
</Form.Select>
</>}
</>}
@@ -469,8 +469,8 @@ function App() {
<Table bordered className='mt-5'>
<tbody>
{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 (
<tr key={key}>
{(locationPickCount?? 0) > 1 ? (
{(locationPickCount ?? 0) > 1 ? (
<td>{locationName} ({locationPickCount})</td>
) : (
<td>{locationName}</td>)}
<td>{locationName}</td>)}
<td className='p-0'>
<Table>
<tbody>
{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 <tr key={index}>
<td>
@@ -503,20 +503,18 @@ function App() {
setNoteModalOpen(true);
}} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />}
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
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} />}
</td>
{userChoices?.length && food ? <td>
<ul>
{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 <li key={foodIndex}>
{foodName}
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
doRemoveFoodChoice(key as LocationKey, foodIndex);
doRemoveFoodChoice(restaurantKey, foodIndex);
}} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
</li>
})}
@@ -536,7 +534,7 @@ function App() {
: <div className='mt-5'><i>Zatím nikdo nehlasoval...</i></div>
}
</div>
{dayIndex === data.todayWeekIndex &&
{dayIndex === data.todayDayIndex &&
<div className='mt-5'>
{!data.pizzaDay &&
<div style={{ textAlign: 'center' }}>
@@ -646,13 +644,13 @@ function App() {
</Button>
</div>
}
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} creator={data.pizzaDay.creator} />
<PizzaOrderList state={data.pizzaDay.state!} orders={data.pizzaDay.orders!} onDelete={handlePizzaDelete} creator={data.pizzaDay.creator!} />
{
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
<div className='qr-code'>
<h3>QR platba</h3>
<img src={getQrUrl(auth.login)} alt='QR kód' />
</div>
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr ?
<div className='qr-code'>
<h3>QR platba</h3>
<img src={getQrUrl(auth.login)} alt='QR kód' />
</div> : null
}
</div>
}

View File

@@ -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);
}

View File

@@ -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;
}
/**

8
client/src/api/Client.ts Normal file
View File

@@ -0,0 +1,8 @@
import { client } from '../../../types/gen/client.gen';
import { getToken } from '../Utils';
client.setConfig({
auth: () => getToken(),
});
export default client

View File

@@ -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<AddChoiceRequest, void>(`${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<RemoveChoicesRequest, void>(`${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<RemoveChoiceRequest, void>(`${FOOD_API_PREFIX}/removeChoice`, { locationKey, foodIndex, dayIndex });
}

View File

@@ -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<AddPizzaRequest, void>(`${PIZZADAY_API_PREFIX}/add`, { pizzaIndex, pizzaSizeIndex });
}
export const removePizza = async (pizzaOrder: PizzaOrder) => {
export const removePizza = async (pizzaOrder: PizzaVariant) => {
return await api.post<RemovePizzaRequest, void>(`${PIZZADAY_API_PREFIX}/remove`, { pizzaOrder });
}

View File

@@ -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';

View File

@@ -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();

View File

@@ -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,
}

View File

@@ -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 <>
<td>{order.customer}</td>
<td>{order.pizzaList.map<React.ReactNode>((pizzaOrder, index) =>
<td>{order.pizzaList!.map<React.ReactNode>((pizzaOrder, index) =>
<span key={index}>
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
{auth?.login === order.customer && state === PizzaDayState.CREATED &&

View File

@@ -28,7 +28,7 @@ export const useAuth = () => {
function useProvideAuth(): AuthContextProps {
const [loginName, setLoginName] = useState<string | undefined>();
const [trusted, setTrusted] = useState<boolean | undefined>();
const [token, setToken] = useState<string | null>(getToken());
const [token, setToken] = useState<string | undefined>(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) {

View File

@@ -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 <Line key={location} name={location} type="monotone" dataKey={data => data.locations[key] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} />
}
@@ -111,7 +111,7 @@ export default function StatsPage() {
<FontAwesomeIcon title="Následující týden" icon={faChevronRight} style={{ cursor: "pointer" }} onClick={handleNextWeek} />
</div>
<LineChart width={CHART_WIDTH} height={CHART_HEIGHT} data={data}>
{Object.values(Locations).map(location => renderLine(location))}
{Object.values(LunchChoice).map(location => renderLine(location))}
<XAxis dataKey="date" />
<YAxis />
<Tooltip />