1 Commits

Author SHA1 Message Date
Michal Hájek
aa00542a03 Výběr obědu kliknutím
Výběr obědu kliknutím

Oprava možnosti zadat klikáním více podniků současně

Výběr obědu kliknutím
2025-02-18 10:05:19 +01:00
8 changed files with 96 additions and 150 deletions

View File

@@ -189,7 +189,7 @@ function App() {
} }
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]); }, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
const doAddClickFoodChoice = async (location: Locations, foodIndex?: number) => { const doAddClickFoodChoice = async (location: Locations, foodIndex: number) => {
const locationKey = Object.keys(Locations).find(key => Locations[key as keyof typeof Locations] === location) as LocationKey; const locationKey = Object.keys(Locations).find(key => Locations[key as keyof typeof Locations] === location) as LocationKey;
if (auth?.login) { if (auth?.login) {
await errorHandler(() => addChoice(locationKey, foodIndex, dayIndex)); await errorHandler(() => addChoice(locationKey, foodIndex, dayIndex));
@@ -206,9 +206,9 @@ function App() {
} }
} }
const doJdemeObed = async (locationKey: LocationKey) => { const doJdemeObed = async () => {
if (auth?.login) { if (auth?.login) {
await jdemeObed(locationKey); await jdemeObed();
} }
} }
@@ -338,16 +338,15 @@ function App() {
} }
} }
const renderFoodTable = (location: Locations, menu: DayMenu) => { const renderFoodTable = (name: string, location: Locations, menu: DayMenu) => {
let content; let content;
if (menu?.closed) { if (menu?.closed) {
content = <h3>Zavřeno</h3> content = <h3>Zavřeno</h3>
} else if (menu?.food?.length > 0) { } else if (menu?.food?.length > 0) {
const hideSoups = settings?.hideSoups;
content = <Table striped bordered hover> content = <Table striped bordered hover>
<tbody style={{ cursor: 'pointer' }}> <tbody style={{ cursor: 'pointer' }}>
{menu.food.filter(f => (hideSoups ? !f.isSoup : true)).map((f: any, index: number) => {menu.food.filter(f => (settings?.hideSoups ? !f.isSoup : true)).map((f: any, index: number) =>
<tr key={index} onClick={() => doAddClickFoodChoice(location, hideSoups ? index + 1 : index)}> <tr key={index} onClick={() => doAddClickFoodChoice(location, index)}>
<td>{f.amount}</td> <td>{f.amount}</td>
<td>{f.name}</td> <td>{f.name}</td>
<td>{f.price}</td> <td>{f.price}</td>
@@ -359,7 +358,7 @@ function App() {
content = <h3>Chyba načtení dat</h3> content = <h3>Chyba načtení dat</h3>
} }
return <Col md={12} lg={3} className='mt-3'> return <Col md={12} lg={3} className='mt-3'>
<h3 style={{ cursor: 'pointer' }} onClick={() => doAddClickFoodChoice(location, undefined)}>{location}</h3> <h3>{name}</h3>
{menu?.lastUpdate && <small>Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}</small>} {menu?.lastUpdate && <small>Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}</small>}
{content} {content}
</Col> </Col>
@@ -421,11 +420,11 @@ function App() {
</div> </div>
} }
<Row className='food-tables'> <Row className='food-tables'>
{food[Restaurants.SLADOVNICKA] && renderFoodTable(Locations.SLADOVNICKA, food[Restaurants.SLADOVNICKA])} {food[Restaurants.SLADOVNICKA] && renderFoodTable('Sladovnická', Locations.SLADOVNICKA, food[Restaurants.SLADOVNICKA])}
{/* {food[Restaurants.UMOTLIKU] && renderFoodTable(food[Restaurants.UMOTLIKU])} */} {/* {food[Restaurants.UMOTLIKU] && renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])} */}
{food[Restaurants.TECHTOWER] && renderFoodTable(Locations.TECHTOWER, food[Restaurants.TECHTOWER])} {food[Restaurants.TECHTOWER] && renderFoodTable('TechTower', Locations.TECHTOWER, food[Restaurants.TECHTOWER])}
{food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable(Locations.ZASTAVKAUMICHALA, food[Restaurants.ZASTAVKAUMICHALA])} {food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable('Zastávka u Michala', Locations.ZASTAVKAUMICHALA, food[Restaurants.ZASTAVKAUMICHALA])}
{food[Restaurants.SENKSERIKOVA] && renderFoodTable(Locations.SENKSERIKOVA, food[Restaurants.SENKSERIKOVA])} {food[Restaurants.SENKSERIKOVA] && renderFoodTable('Pivovarský šenk Šeříková', Locations.SENKSERIKOVA, food[Restaurants.SENKSERIKOVA])}
</Row> </Row>
<div className='content-wrapper'> <div className='content-wrapper'>
<div className='content'> <div className='content'>
@@ -471,7 +470,6 @@ function App() {
return; return;
} }
const locationLoginList = Object.entries(loginObject); const locationLoginList = Object.entries(loginObject);
const disabled = false;
return ( return (
<tr key={key}> <tr key={key}>
<td>{locationName}</td> <td>{locationName}</td>
@@ -520,9 +518,6 @@ function App() {
</tbody> </tbody>
</Table> </Table>
</td> </td>
<td>
<Button onClick={() => doJdemeObed(locationKey)} disabled={false}>Jdeme na oběd</Button>
</td>
</tr>) </tr>)
} }
)} )}
@@ -546,6 +541,7 @@ function App() {
setLoadingPizzaDay(true); setLoadingPizzaDay(true);
await createPizzaDay().then(() => setLoadingPizzaDay(false)); await createPizzaDay().then(() => setLoadingPizzaDay(false));
}}>Založit Pizza day</Button> }}>Založit Pizza day</Button>
<Button onClick={doJdemeObed} style={{ marginLeft: "14px" }}>Jdeme na oběd !</Button>
</> </>
} }
</div> </div>

View File

@@ -1,12 +1,4 @@
import { import { AddChoiceRequest, ChangeDepartureTimeRequest, LocationKey, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types";
AddChoiceRequest,
ChangeDepartureTimeRequest,
JdemeObedRequest,
LocationKey,
RemoveChoiceRequest,
RemoveChoicesRequest,
UpdateNoteRequest
} from "../../../types";
import { api } from "./Api"; import { api } from "./Api";
const FOOD_API_PREFIX = '/api/food'; const FOOD_API_PREFIX = '/api/food';
@@ -31,6 +23,6 @@ export const changeDepartureTime = async (time: string, dayIndex?: number) => {
return await api.post<ChangeDepartureTimeRequest, void>(`${FOOD_API_PREFIX}/changeDepartureTime`, { time, dayIndex }); return await api.post<ChangeDepartureTimeRequest, void>(`${FOOD_API_PREFIX}/changeDepartureTime`, { time, dayIndex });
} }
export const jdemeObed = async (locationKey: LocationKey) => { export const jdemeObed = async () => {
return await api.post<JdemeObedRequest, void>(`${FOOD_API_PREFIX}/jdemeObed`, { locationKey }); return await api.post<undefined, void>(`${FOOD_API_PREFIX}/jdemeObed`);
} }

View File

@@ -1,58 +1,58 @@
/** Notifikace */ /** Notifikace pro gotify*/
import {ClientData, Locations, NotififaceInput} from '../../types'; import { ClientData, GotifyServer, NotififaceInput, NotifikaceData } from '../../types';
import axios from 'axios'; import axios, { AxiosError } from 'axios';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { getToday } from "./service"; import { getToday } from "./service";
import {formatDate, getUsersByLocation, getHumanTime} from "./utils"; import { formatDate, getUsersByLocation } from "./utils";
import getStorage from "./storage"; import getStorage from "./storage";
const storage = getStorage(); const storage = getStorage();
const ENVIRONMENT = process.env.NODE_ENV || 'production' const ENVIRONMENT = process.env.NODE_ENV || 'production'
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) }); dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
// const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}"; const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}";
// const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw); const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw);
// export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => { export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
// if (!Array.isArray(gotifyServers)) { if (!Array.isArray(gotifyServers)) {
// return [] return []
// } }
// const urls = gotifyServers.flatMap(gotifyServer => const urls = gotifyServers.flatMap(gotifyServer =>
// gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`)); gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
//
// const dataPayload = { const dataPayload = {
// title: "Luncher", title: "Luncher",
// message: `${data.udalost} - spustil:${data.user}`, message: `${data.udalost} - spustil:${data.user}`,
// priority: 7, priority: 7,
// }; };
//
// const headers = { "Content-Type": "application/json" }; const headers = { "Content-Type": "application/json" };
//
// const promises = urls.map(url => const promises = urls.map(url =>
// axios.post(url, dataPayload, { headers }).then(response => { axios.post(url, dataPayload, { headers }).then(response => {
// response.data = { response.data = {
// success: true, success: true,
// message: "Notifikace doručena", message: "Notifikace doručena",
// }; };
// return response; return response;
// }).catch(error => { }).catch(error => {
// if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
// const axiosError = error as AxiosError; const axiosError = error as AxiosError;
// if (axiosError.response) { if (axiosError.response) {
// axiosError.response.data = { axiosError.response.data = {
// success: false, success: false,
// message: "fail", message: "fail",
// }; };
// console.log(error) console.log(error)
// return axiosError.response; return axiosError.response;
// } }
// } }
// // Handle unknown error without a response // Handle unknown error without a response
// console.log(error, "unknown error"); console.log(error, "unknown error");
// }) })
// ); );
// return promises; return promises;
// }; };
export const ntfyCall = async (data: NotififaceInput) => { export const ntfyCall = async (data: NotififaceInput) => {
const url = process.env.NTFY_HOST const url = process.env.NTFY_HOST
@@ -72,10 +72,10 @@ export const ntfyCall = async (data: NotififaceInput) => {
} }
const today = formatDate(getToday()); const today = formatDate(getToday());
let clientData: ClientData = await storage.getData(today); let clientData: ClientData = await storage.getData(today);
const usersByLocation = getUsersByLocation(clientData.choices, data.user) const userByCLocation = getUsersByLocation(clientData.choices, data.user)
const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64'); const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64');
const promises = usersByLocation.map(async user => { const promises = userByCLocation.map(async user => {
try { try {
// Odstraníme mezery a diakritiku a převedeme na lowercase // Odstraníme mezery a diakritiku a převedeme na lowercase
const topic = user.normalize('NFD').replace(' ', '').replace(/[\u0300-\u036f]/g, '').toLowerCase(); const topic = user.normalize('NFD').replace(' ', '').replace(/[\u0300-\u036f]/g, '').toLowerCase();
@@ -97,64 +97,27 @@ export const ntfyCall = async (data: NotififaceInput) => {
return promises; return promises;
} }
export const teamsCall = async (data: NotififaceInput) => {
const url = process.env.TEAMS_WEBHOOK_URL;
if (!url) {
console.log("TEAMS_WEBHOOK_URL není definován v env")
return
}
const title = data.udalost;
// const today = formatDate(getToday());
// let clientData: ClientData = await storage.getData(today);
// const usersByLocation = getUsersByLocation(clientData.choices, data.user)
let location = data.locationKey ? ` odcházíme do ${Locations[data.locationKey] ?? ''}` : ' jdeme na oběd';
const message = 'V ' + getHumanTime(getToday()) + location;
const card = {
'@type': 'MessageCard',
'@context': 'http://schema.org/extensions',
'themeColor': "0072C6", // light blue
summary: 'Summary description',
sections: [
{
activityTitle: title,
text: message,
},
],
};
if (!url) {
console.log("TEAMS_WEBHOOK_URL není definován v env")
return
}
try {
const response = await axios.post(url, card, {
headers: {
'content-type': 'application/vnd.microsoft.teams.card.o365connector'
},
});
return `${response.status} - ${response.statusText}`;
} catch (err) {
return err;
}
}
/** Zavolá notifikace na všechny konfigurované způsoby notifikace, přetížení proměných na false pro jednotlivé způsoby je vypne*/ /** Zavolá notifikace na všechny konfigurované způsoby notifikace, přetížení proměných na false pro jednotlivé způsoby je vypne*/
export const callNotifikace = async (input: NotififaceInput) => { export const callNotifikace = async ({ input, teams = true, gotify = false, ntfy = true }: NotifikaceData) => {
const notifications = []; const notifications = [];
if (ntfy) {
const ntfyPromises = await ntfyCall(input);
if (ntfyPromises) {
notifications.push(...ntfyPromises);
}
}
/* Zatím není
if (teams) {
notifications.push(teamsCall(input));
}*/
const ntfyPromises = await ntfyCall(input); // Add more notifications as necessary
if (ntfyPromises) {
notifications.push(...ntfyPromises); //gotify bych řekl, že už je deprecated
if (gotify) {
const gotifyPromises = await gotifyCall(input, gotifyData);
notifications.push(...gotifyPromises);
} }
const teamsPromises = await teamsCall(input);
if (teamsPromises) {
notifications.push(teamsPromises);
}
// gotify bych řekl, že už je deprecated
// const gotifyPromises = await gotifyCall(input, gotifyData);
// notifications.push(...gotifyPromises);
try { try {
const results = await Promise.all(notifications); const results = await Promise.all(notifications);

View File

@@ -52,7 +52,7 @@ export async function createPizzaDay(creator: string): Promise<ClientData> {
const pizzaList = await getPizzaList(); const pizzaList = await getPizzaList();
const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, ...clientData }; const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, ...clientData };
await storage.setData(today, data); await storage.setData(today, data);
callNotifikace({ udalost: UdalostEnum.ZAHAJENA_PIZZA, user: creator }) callNotifikace({ input: { udalost: UdalostEnum.ZAHAJENA_PIZZA, user: creator } })
return data; return data;
} }
@@ -207,7 +207,7 @@ export async function finishPizzaOrder(login: string) {
} }
clientData.pizzaDay.state = PizzaDayState.ORDERED; clientData.pizzaDay.state = PizzaDayState.ORDERED;
await storage.setData(today, clientData); await storage.setData(today, clientData);
callNotifikace({ udalost: UdalostEnum.OBJEDNANA_PIZZA, user: clientData?.pizzaDay?.creator }) callNotifikace({ input: { udalost: UdalostEnum.OBJEDNANA_PIZZA, user: clientData?.pizzaDay?.creator } })
return clientData; return clientData;
} }

View File

@@ -4,16 +4,7 @@ import { addChoice, addVolatileData, getDateForWeekIndex, getToday, removeChoice
import { getDayOfWeekIndex, parseToken } from "../utils"; import { getDayOfWeekIndex, parseToken } from "../utils";
import { getWebsocket } from "../websocket"; import { getWebsocket } from "../websocket";
import { callNotifikace } from "../notifikace"; import { callNotifikace } from "../notifikace";
import { import { AddChoiceRequest, ChangeDepartureTimeRequest, IDayIndex, RemoveChoiceRequest, RemoveChoicesRequest, UdalostEnum, UpdateNoteRequest } from "../../../types";
AddChoiceRequest,
ChangeDepartureTimeRequest,
IDayIndex,
JdemeObedRequest,
RemoveChoiceRequest,
RemoveChoicesRequest,
UdalostEnum,
UpdateNoteRequest
} 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ň * Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň
@@ -142,10 +133,10 @@ router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDeparture
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });
router.post("/jdemeObed", async (req: Request<{}, any, JdemeObedRequest>, res, next) => { router.post("/jdemeObed", async (req, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
try { try {
await callNotifikace({ user: login, udalost: UdalostEnum.JDEME_OBED, locationKey: req.body.locationKey }); await callNotifikace({ input: { user: login, udalost: UdalostEnum.JDEME_OBED }, gotify: false })
res.status(200).json({}); res.status(200).json({});
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });

View File

@@ -1,4 +1,4 @@
import {Choices, LocationKey } from "../../types"; import { Choices, LocationKey } from "../../types";
/** Vrátí datum v ISO formátu. */ /** Vrátí datum v ISO formátu. */
export function formatDate(date: Date, format?: string) { export function formatDate(date: Date, format?: string) {

View File

@@ -18,8 +18,6 @@ export type RemoveChoiceRequest = IDayIndex & ILocationKey & {
foodIndex: number, foodIndex: number,
} }
export type JdemeObedRequest = ILocationKey;
export type UpdateNoteRequest = IDayIndex & { export type UpdateNoteRequest = IDayIndex & {
note?: string, note?: string,
} }

View File

@@ -131,15 +131,21 @@ export type LocationKey = keyof typeof Locations;
export enum UdalostEnum { export enum UdalostEnum {
ZAHAJENA_PIZZA = "Zahájen pizza day", ZAHAJENA_PIZZA = "Zahájen pizza day",
OBJEDNANA_PIZZA = "Objednána pizza", OBJEDNANA_PIZZA = "Objednána pizza",
JDEME_OBED = "Jdeme na oběd", JDEME_OBED = "Jdeme oběd",
} }
export type NotififaceInput = { export type NotififaceInput = {
locationKey?: LocationKey,
udalost: UdalostEnum, udalost: UdalostEnum,
user: string, user: string,
} }
export type NotifikaceData = {
input: NotififaceInput,
gotify?: boolean,
teams?: boolean,
ntfy?: boolean,
}
export type GotifyServer = { export type GotifyServer = {
server: string; server: string;
api_keys: string[]; api_keys: string[];