Compare commits

...

1 Commits

Author SHA1 Message Date
6a79e7989b Zbavení se duplicitních Typescript typů 2023-07-09 19:17:20 +02:00
25 changed files with 10282 additions and 130 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ node_modules
# production
/build
dist
# misc
.DS_Store

View File

@ -1,7 +1,9 @@
# Luncher
Aplikace pro profesionální management obědů.
Aplikace sestává ze dvou (tří) modulů.
Aplikace sestává ze tří (čtyř) modulů.
- api
- společné Typescript definice, pro objekty posílané mezi serverem a klientem
- server
- backend psaný v [node.js](https://nodejs.dev)
- client

1
api/README.md Normal file
View File

@ -0,0 +1 @@
# Společné Typescript definice serveru a klienta

14
api/package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "@luncher/api",
"version": "1.0.0",
"main": "src/index.ts",
"scripts": {
"build": "yarn clean && tsc -p .",
"build:watch": "tsc -p . --watch",
"clean": "rm -rf dist"
},
"types": "dist/index.d.ts",
"devDependencies": {
"typescript": "^5.0.2"
}
}

View File

@ -1,4 +1,9 @@
import exp from "constants";
/** 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',
}
export interface Choices {
[location: string]: string[],
@ -69,13 +74,6 @@ export interface Food {
isSoup: boolean, // příznak, zda se jedná o polévku
}
/** 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',
}
export enum Locations {
SLADOVNICKA = 'Sladovnická',
UMOTLIKU = 'U Motlíků',

1
api/src/index.ts Normal file
View File

@ -0,0 +1 @@
export * as Types from './Types';

9
api/tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"declaration": true,
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}

8
api/yarn.lock Normal file
View File

@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
typescript@^5.0.2:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==

View File

@ -16,6 +16,7 @@
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"bootstrap": "^5.2.3",
"@luncher/api": "1.0.0",
"react": "^18.2.0",
"react-bootstrap": "^2.7.2",
"react-dom": "^18.2.0",

View File

@ -1,14 +1,12 @@
import { PizzaOrder } from "./Types";
import { PizzaOrder } from "@luncher/api/dist/Types";
import { getBaseUrl, getToken } from "./Utils";
async function request<TResponse>(
url: string,
config: RequestInit = {}
): Promise<TResponse> {
if (!config.headers) {
config.headers = {};
}
config.headers["Authorization"] = `Bearer ${getToken()}`;
config.headers = config?.headers ? new Headers(config.headers) : new Headers();
config.headers.set("Authorization", `Bearer ${getToken()}`);
return fetch(getBaseUrl() + url, config).then(response => {
if (!response.ok) {
throw new Error(response.statusText);
@ -58,7 +56,7 @@ export const finishOrder = async () => {
return await api.post<any, any>('/api/finishOrder', undefined);
}
export const finishDelivery = async (bankAccount, bankAccountHolder) => {
export const finishDelivery = async (bankAccount?: string, bankAccountHolder?: string) => {
return await api.post<any, any>('/api/finishDelivery', JSON.stringify({ bankAccount, bankAccountHolder }));
}

View File

@ -4,17 +4,17 @@ import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket
import { addPizza, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getFood, getPizzy, getQrUrl, lockPizzaDay, removePizza, unlockPizzaDay, updateChoice, updateNote } from './Api';
import { useAuth } from './context/auth';
import Login from './Login';
import { Locations, ClientData, Pizza, PizzaOrder, State, Order, Food, Restaurants } from './Types';
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 from 'react-select-search';
import SelectSearch, { SelectedOptionValue } from 'react-select-search';
import 'react-select-search/style.css';
import './App.css';
import { SelectSearchOption } from 'react-select-search';
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { useBank } from './context/bank';
import { ClientData, Restaurants, Food, Pizza, Order, Locations, PizzaOrder, PizzaDayState } from '@luncher/api/dist/Types';
const EVENT_CONNECT = "connect"
@ -77,7 +77,8 @@ function App() {
if (data?.choices && choiceRef.current) {
for (let entry of Object.entries(data.choices)) {
if (entry[1].includes(auth.login)) {
choiceRef.current.value = Object.values(Locations)[entry[0]]
const value = entry[0] as any as number; // TODO tohle je absurdní
choiceRef.current.value = Object.values(Locations)[value];
}
}
}
@ -124,8 +125,11 @@ function App() {
return suggestions;
}, [pizzy]);
const handlePizzaChange = async (value) => {
const handlePizzaChange = async (value: SelectedOptionValue | SelectedOptionValue[]) => {
if (auth?.login && pizzy) {
if (!(value instanceof String)) {
throw Error('Nepodporovaný typ hodnoty');
}
const s = value.split('|');
const pizzaIndex = Number.parseInt(s[0]);
const pizzaSizeIndex = Number.parseInt(s[1]);
@ -169,7 +173,7 @@ function App() {
// }
// }
const renderFoodTable = (name, food) => {
const renderFoodTable = (name: string, food: Food[]) => {
return <Col md={12} lg={4}>
<h3>{name}</h3>
<Table striped bordered hover>
@ -268,7 +272,7 @@ function App() {
<div style={{ textAlign: 'center' }}>
<h3>Pizza day</h3>
{
data.pizzaDay.state === State.CREATED &&
data.pizzaDay.state === PizzaDayState.CREATED &&
<div>
<p>
Pizza Day je založen a spravován uživatelem {data.pizzaDay.creator}.<br />
@ -288,7 +292,7 @@ function App() {
</div>
}
{
data.pizzaDay.state === State.LOCKED &&
data.pizzaDay.state === PizzaDayState.LOCKED &&
<div>
<p>Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}</p>
{data.pizzaDay.creator === auth.login &&
@ -307,7 +311,7 @@ function App() {
</div>
}
{
data.pizzaDay.state === State.ORDERED &&
data.pizzaDay.state === PizzaDayState.ORDERED &&
<div>
<p>Pizzy byly objednány uživatelem {data.pizzaDay.creator}</p>
{data.pizzaDay.creator === auth.login &&
@ -323,13 +327,13 @@ function App() {
</div>
}
{
data.pizzaDay.state === State.DELIVERED &&
data.pizzaDay.state === PizzaDayState.DELIVERED &&
<div>
<p>Pizzy byly doručeny. Objednávku můžete uhradit pomocí QR kódu níže.</p>
</div>
}
</div>
{data.pizzaDay.state === State.CREATED &&
{data.pizzaDay.state === PizzaDayState.CREATED &&
<div style={{ textAlign: 'center' }}>
<SelectSearch
search={true}
@ -352,7 +356,7 @@ function App() {
}
<PizzaOrderList state={data.pizzaDay.state} orders={data.pizzaDay.orders} onDelete={handlePizzaDelete} />
{
data.pizzaDay.state === State.DELIVERED && myOrder?.hasQr &&
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
<div className='qr-code'>
<h3>QR platba</h3>
<div>Částka: {myOrder.totalPrice} </div>

View File

@ -1,84 +0,0 @@
// TODO všechno v tomto souboru jsou duplicity se serverem, ale aktuálně nevím jaký je nejlepší způsob jejich sdílení
export interface 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 interface Pizza {
name: string, // název pizzy
ingredients: string[], // seznam ingrediencí
sizes: PizzaSize[], // dostupné velikosti pizzy
}
/** Objednávka jedné konkrétní pizzy */
export interface 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 interface Order {
customer: string, // jméno objednatele
pizzaList: PizzaOrder[], // seznam objednaných pizz
totalPrice: number, // celková cena všech objednaných pizz a krabic
hasQr?: boolean, // zda je pro objednávku vygenerován QR kód pro platbu
note?: string, // volitelná poznámka uživatele k objednávce
}
export interface Choices {
[location: string]: string[],
}
/** Údaje o Pizza day. */
export interface PizzaDay {
state: State,
creator: string,
orders: Order[]
}
export interface ClientData {
date: string, // dnešní datum pro zobrazení
isWeekend: boolean, // příznak zda je dnešní den víkend
choices: Choices, // seznam voleb
pizzaDay?: PizzaDay, // údaje o pizza day, pokud je pro dnešek založen
}
export enum Locations {
SLADOVNICKA = 'Sladovnická',
UMOTLIKU = 'U Motlíků',
TECHTOWER = 'TechTower',
SPSE = 'SPŠE',
PIZZA = 'Pizza day',
OBJEDNAVAM = 'Budu objednávat',
NEOBEDVAM = 'Mám vlastní/neobědvám',
}
/** Jídlo z obědového menu restaurace. */
export interface 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
}
/** 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',
}
export enum State {
NOT_CREATED, // Pizza day nebyl založen
CREATED, // Pizza day je založen
LOCKED, // Objednávky uzamčeny
ORDERED, // Objednáno
DELIVERED, // Doručeno
}

View File

@ -19,7 +19,7 @@ export default function Header() {
setModalOpen(false);
}
const isValidInteger = (str) => {
const isValidInteger = (str: string) => {
str = str.trim();
if (!str) {
return false;
@ -66,7 +66,7 @@ export default function Header() {
if (sum % 11 !== 0) {
throw Error("Číslo účtu je neplatné")
}
} catch (e) {
} catch (e: any) {
alert(e.message)
return
}

View File

@ -1,11 +1,11 @@
import React from "react";
import { Table } from "react-bootstrap";
import { Order, PizzaOrder, State } from "../Types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { useAuth } from "../context/auth";
import { Order, PizzaDayState, PizzaOrder } from "@luncher/api/dist/Types";
export default function PizzaOrderList({ state, orders, onDelete }: { state: State, orders: Order[], onDelete: (pizzaOrder: PizzaOrder) => void }) {
export default function PizzaOrderList({ state, orders, onDelete }: { state: PizzaDayState, orders: Order[], onDelete: (pizzaOrder: PizzaOrder) => void }) {
const auth = useAuth();
if (!orders?.length) {
@ -29,7 +29,7 @@ export default function PizzaOrderList({ state, orders, onDelete }: { state: Sta
<td>{order.pizzaList.map<React.ReactNode>((pizzaOrder, index) =>
<span key={index}>
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
{auth?.login === order.customer && state === State.CREATED &&
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
<FontAwesomeIcon onClick={() => {
onDelete(pizzaOrder);
}} title='Odstranit' className='trash-icon' icon={faTrashCan} />

View File

@ -19,8 +19,5 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"client/src"
]
}
}

View File

@ -6427,6 +6427,9 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
"luncher-api@file:./../api":
version "1.0.0"
lz-string@^1.4.4:
version "1.5.0"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"private": true,
"workspaces": [
"api",
"server",
"client"
]
}

View File

@ -1,4 +1,6 @@
export NODE_ENV=development
cd server && yarn install && yarn start &
cd client && yarn install && yarn start &
yarn install
cd api && yarn build &
cd server && yarn start &
cd client && yarn start &
wait

View File

@ -1,5 +1,5 @@
{
"name": "luncher-server",
"name": "@luncher/server",
"version": "1.0.0",
"main": "src/index.ts",
"license": "MIT",
@ -25,7 +25,8 @@
"dotenv": "^16.1.3",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"@luncher/api": "1.0.0",
"simple-json-db": "^2.0.0",
"socket.io": "^4.6.1"
}
}
}

View File

@ -8,10 +8,10 @@ import dotenv from 'dotenv';
import path from 'path';
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
import { getQr } from "./qr";
import { Restaurants } from "./types";
import { generateToken, getLogin, verify } from "./auth";
import { Restaurants } from "@luncher/api/dist/Types";
const ENVIRONMENT = process.env.NODE_ENV || 'production'
const ENVIRONMENT = process.env.NODE_ENV || 'production';
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
const app = express();
@ -95,7 +95,7 @@ app.get("/api/data", (req, res) => {
/** Vrátí obědové menu pro dostupné podniky. */
app.get("/api/food", async (req, res) => {
const mock = !!req.query?.mock;
const mock = !!process.env.MOCK_DATA;
const date = new Date();
const data = {
[Restaurants.SLADOVNICKA]: await getMenuSladovnicka(date, mock),

View File

@ -1,6 +1,6 @@
/** Notifikace pro gotify*/
import axios, { AxiosError, AxiosResponse } from 'axios';
import { GotifyServer, NotififaceInput, NotifikaceData, UdalostEnum } from "./types";
import { GotifyServer, NotififaceInput, NotifikaceData } from '@luncher/api/dist/Types';
import axios, { AxiosError } from 'axios';
import dotenv from 'dotenv';
import path from 'path';

View File

@ -4,7 +4,7 @@ import path from "path";
import fs from "fs";
import { load } from 'cheerio';
import { formatDate } from "./utils";
import { Food } from "./types";
import { Food } from "@luncher/api/dist/Types";
// Fráze v názvech jídel, které naznačují že se jedná o polévku
const SOUP_NAMES = ['polévka', 'česnečka', 'česnekový krém', 'cibulačka', 'vývar']

View File

@ -1,8 +1,8 @@
import { ClientData, Locations, Order, Pizza, PizzaDayState, PizzaOrder, PizzaSize, UdalostEnum } from "./types";
import { db } from "./database";
import { formatDate, getHumanDate, getIsWeekend } from "./utils";
import { callNotifikace } from "./notifikace";
import { generateQr } from "./qr";
import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, Locations } from "@luncher/api/dist/Types";
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
function getToday(): Date {

View File

@ -788,6 +788,9 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
"luncher-api@file:./../api":
version "1.0.0"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"

10185
yarn.lock Normal file

File diff suppressed because it is too large Load Diff