feat: podpora ručního generování QR kódů pro platby
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
This commit is contained in:
@@ -15,6 +15,8 @@ import votingRoutes from "./routes/votingRoutes";
|
||||
import easterEggRoutes from "./routes/easterEggRoutes";
|
||||
import statsRoutes from "./routes/statsRoutes";
|
||||
import notificationRoutes from "./routes/notificationRoutes";
|
||||
import qrRoutes from "./routes/qrRoutes";
|
||||
import devRoutes from "./routes/devRoutes";
|
||||
|
||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
@@ -160,6 +162,8 @@ app.use("/api/voting", votingRoutes);
|
||||
app.use("/api/easterEggs", easterEggRoutes);
|
||||
app.use("/api/stats", statsRoutes);
|
||||
app.use("/api/notifications", notificationRoutes);
|
||||
app.use("/api/qr", qrRoutes);
|
||||
app.use("/api/dev", devRoutes);
|
||||
|
||||
app.use('/stats', express.static('public'));
|
||||
app.use(express.static('public'));
|
||||
|
||||
@@ -277,6 +277,7 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
|
||||
date: today,
|
||||
creator: login,
|
||||
totalPrice: order.totalPrice,
|
||||
purpose: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -356,7 +357,7 @@ function getPendingQrKey(login: string): string {
|
||||
/**
|
||||
* Přidá nevyřízený QR kód pro uživatele.
|
||||
*/
|
||||
async function addPendingQr(login: string, pendingQr: PendingQr): Promise<void> {
|
||||
export async function addPendingQr(login: string, pendingQr: PendingQr): Promise<void> {
|
||||
const key = getPendingQrKey(login);
|
||||
const existing = await storage.getData<PendingQr[]>(key) ?? [];
|
||||
// Nepřidáváme duplicity pro stejný den
|
||||
|
||||
157
server/src/routes/devRoutes.ts
Normal file
157
server/src/routes/devRoutes.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import express, { Request } from "express";
|
||||
import { getDateForWeekIndex, getData, getRestaurantMenu, getToday, initIfNeeded } from "../service";
|
||||
import { formatDate, getDayOfWeekIndex } from "../utils";
|
||||
import getStorage from "../storage";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { GenerateMockDataData, ClearMockDataData, LunchChoice, Restaurant } from "../../../types";
|
||||
|
||||
const router = express.Router();
|
||||
const storage = getStorage();
|
||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||
|
||||
// Seznam náhodných jmen pro generování mock dat
|
||||
const MOCK_NAMES = [
|
||||
'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Filip', 'Gita', 'Honza',
|
||||
'Ivana', 'Jakub', 'Kamila', 'Lukáš', 'Markéta', 'Nikola', 'Ondřej',
|
||||
'Petra', 'Quido', 'Radek', 'Simona', 'Tomáš', 'Ursula', 'Viktor',
|
||||
'Wanda', 'Xaver', 'Yvona', 'Zdeněk', 'Aneta', 'Boris', 'Cecílie', 'Daniel'
|
||||
];
|
||||
|
||||
// Volby stravování pro mock data
|
||||
const LUNCH_CHOICES: LunchChoice[] = [
|
||||
LunchChoice.SLADOVNICKA,
|
||||
LunchChoice.TECHTOWER,
|
||||
LunchChoice.ZASTAVKAUMICHALA,
|
||||
LunchChoice.SENKSERIKOVA,
|
||||
LunchChoice.OBJEDNAVAM,
|
||||
LunchChoice.NEOBEDVAM,
|
||||
LunchChoice.ROZHODUJI,
|
||||
];
|
||||
|
||||
// Restaurace s menu
|
||||
const RESTAURANTS_WITH_MENU: LunchChoice[] = [
|
||||
LunchChoice.SLADOVNICKA,
|
||||
LunchChoice.TECHTOWER,
|
||||
LunchChoice.ZASTAVKAUMICHALA,
|
||||
LunchChoice.SENKSERIKOVA,
|
||||
];
|
||||
|
||||
/**
|
||||
* Middleware pro kontrolu DEV režimu
|
||||
*/
|
||||
function requireDevMode(req: any, res: any, next: any) {
|
||||
if (ENVIRONMENT !== 'development') {
|
||||
return res.status(403).json({ error: 'Tento endpoint je dostupný pouze ve vývojovém režimu' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(requireDevMode);
|
||||
|
||||
/**
|
||||
* Vygeneruje mock data pro testování.
|
||||
*/
|
||||
router.post("/generate", async (req: Request<{}, any, GenerateMockDataData["body"]>, res, next) => {
|
||||
try {
|
||||
const dayIndex = req.body?.dayIndex ?? getDayOfWeekIndex(getToday());
|
||||
const count = req.body?.count ?? Math.floor(Math.random() * 16) + 5; // 5-20
|
||||
|
||||
if (dayIndex < 0 || dayIndex > 4) {
|
||||
return res.status(400).json({ error: 'Neplatný index dne (0-4)' });
|
||||
}
|
||||
|
||||
const date = getDateForWeekIndex(dayIndex);
|
||||
await initIfNeeded(date);
|
||||
|
||||
const dateKey = formatDate(date);
|
||||
const data = await storage.getData<any>(dateKey);
|
||||
|
||||
// Získání menu restaurací pro vybraný den
|
||||
const menus: { [key: string]: any } = {};
|
||||
for (const restaurant of RESTAURANTS_WITH_MENU) {
|
||||
const menu = await getRestaurantMenu(restaurant as Restaurant, date);
|
||||
if (menu?.food?.length) {
|
||||
menus[restaurant] = menu.food;
|
||||
}
|
||||
}
|
||||
|
||||
// Vygenerování náhodných uživatelů
|
||||
const usedNames = new Set<string>();
|
||||
for (let i = 0; i < count && usedNames.size < MOCK_NAMES.length; i++) {
|
||||
// Vybereme náhodné jméno, které ještě nebylo použito
|
||||
let name: string;
|
||||
do {
|
||||
name = MOCK_NAMES[Math.floor(Math.random() * MOCK_NAMES.length)];
|
||||
} while (usedNames.has(name));
|
||||
usedNames.add(name);
|
||||
|
||||
// Vybereme náhodnou volbu stravování
|
||||
const choice = LUNCH_CHOICES[Math.floor(Math.random() * LUNCH_CHOICES.length)];
|
||||
|
||||
// Inicializace struktury pro volbu
|
||||
data.choices[choice] ??= {};
|
||||
|
||||
const userChoice: any = {
|
||||
trusted: false,
|
||||
selectedFoods: [],
|
||||
};
|
||||
|
||||
// Pokud má restaurace menu, vybereme náhodné jídlo
|
||||
if (RESTAURANTS_WITH_MENU.includes(choice) && menus[choice]?.length) {
|
||||
const foods = menus[choice];
|
||||
// Vybereme náhodné jídlo (ne polévku)
|
||||
const mainFoods = foods.filter((f: any) => !f.isSoup);
|
||||
if (mainFoods.length > 0) {
|
||||
const randomFoodIndex = foods.indexOf(mainFoods[Math.floor(Math.random() * mainFoods.length)]);
|
||||
userChoice.selectedFoods = [randomFoodIndex];
|
||||
}
|
||||
}
|
||||
|
||||
data.choices[choice][name] = userChoice;
|
||||
}
|
||||
|
||||
await storage.setData(dateKey, data);
|
||||
|
||||
// Odeslat aktualizovaná data přes WebSocket
|
||||
const clientData = await getData(date);
|
||||
getWebsocket().emit("message", clientData);
|
||||
|
||||
res.status(200).json({ success: true, count: usedNames.size, dayIndex });
|
||||
} catch (e: any) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Smaže všechny volby pro daný den.
|
||||
*/
|
||||
router.post("/clear", async (req: Request<{}, any, ClearMockDataData["body"]>, res, next) => {
|
||||
try {
|
||||
const dayIndex = req.body?.dayIndex ?? getDayOfWeekIndex(getToday());
|
||||
|
||||
if (dayIndex < 0 || dayIndex > 4) {
|
||||
return res.status(400).json({ error: 'Neplatný index dne (0-4)' });
|
||||
}
|
||||
|
||||
const date = getDateForWeekIndex(dayIndex);
|
||||
await initIfNeeded(date);
|
||||
|
||||
const dateKey = formatDate(date);
|
||||
const data = await storage.getData<any>(dateKey);
|
||||
|
||||
// Vymažeme všechny volby
|
||||
data.choices = {};
|
||||
|
||||
await storage.setData(dateKey, data);
|
||||
|
||||
// Odeslat aktualizovaná data přes WebSocket
|
||||
const clientData = await getData(date);
|
||||
getWebsocket().emit("message", clientData);
|
||||
|
||||
res.status(200).json({ success: true, dayIndex });
|
||||
} catch (e: any) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
64
server/src/routes/qrRoutes.ts
Normal file
64
server/src/routes/qrRoutes.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import express, { Request } from "express";
|
||||
import { getLogin } from "../auth";
|
||||
import { parseToken, formatDate } from "../utils";
|
||||
import { generateQr } from "../qr";
|
||||
import { addPendingQr } from "../pizza";
|
||||
import { GenerateQrData } from "../../../types";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Vygeneruje QR kódy pro platbu vybraným uživatelům.
|
||||
*/
|
||||
router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
try {
|
||||
const { recipients, bankAccount, bankAccountHolder } = req.body;
|
||||
|
||||
if (!recipients || !Array.isArray(recipients) || recipients.length === 0) {
|
||||
return res.status(400).json({ error: "Nebyl předán seznam příjemců" });
|
||||
}
|
||||
if (!bankAccount) {
|
||||
return res.status(400).json({ error: "Nebylo předáno číslo účtu" });
|
||||
}
|
||||
if (!bankAccountHolder) {
|
||||
return res.status(400).json({ error: "Nebylo předáno jméno držitele účtu" });
|
||||
}
|
||||
|
||||
const today = formatDate(new Date());
|
||||
|
||||
for (const recipient of recipients) {
|
||||
if (!recipient.login) {
|
||||
return res.status(400).json({ error: "Příjemce nemá vyplněný login" });
|
||||
}
|
||||
if (!recipient.purpose || recipient.purpose.trim().length === 0) {
|
||||
return res.status(400).json({ error: `Příjemce ${recipient.login} nemá vyplněný účel platby` });
|
||||
}
|
||||
if (typeof recipient.amount !== 'number' || recipient.amount <= 0) {
|
||||
return res.status(400).json({ error: `Příjemce ${recipient.login} má neplatnou částku` });
|
||||
}
|
||||
// Validace max 2 desetinná místa
|
||||
const amountStr = recipient.amount.toString();
|
||||
if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) {
|
||||
return res.status(400).json({ error: `Částka pro ${recipient.login} má více než 2 desetinná místa` });
|
||||
}
|
||||
|
||||
// Vygenerovat QR kód
|
||||
await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose);
|
||||
|
||||
// Uložit jako nevyřízený QR kód
|
||||
await addPendingQr(recipient.login, {
|
||||
date: today,
|
||||
creator: login,
|
||||
totalPrice: recipient.amount,
|
||||
purpose: recipient.purpose,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, count: recipients.length });
|
||||
} catch (e: any) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user