From 8e285e91977190921e7e9f72e0f23d255b391c5c Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Wed, 27 Sep 2023 18:35:18 +0200 Subject: [PATCH] =?UTF-8?q?Mo=C5=BEnost=20hlasov=C3=A1n=C3=AD=20o=20nov?= =?UTF-8?q?=C3=BDch=20funkc=C3=ADch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/Api.ts | 10 +++- client/src/App.tsx | 1 + client/src/components/Header.tsx | 46 +++++++++++---- .../components/modals/FeaturesVotingModal.tsx | 45 +++++++++++++++ server/src/index.ts | 19 +++++++ server/src/voting.ts | 56 +++++++++++++++++++ types/Types.ts | 11 ++++ 7 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 client/src/components/modals/FeaturesVotingModal.tsx create mode 100644 server/src/voting.ts diff --git a/client/src/Api.ts b/client/src/Api.ts index 8706b69..c8f3a97 100644 --- a/client/src/Api.ts +++ b/client/src/Api.ts @@ -1,5 +1,5 @@ import { toast } from "react-toastify"; -import { PizzaOrder } from "./types"; +import { FeatureRequest, PizzaOrder } from "./types"; import { getBaseUrl, getToken } from "./Utils"; /** @@ -110,4 +110,12 @@ export const changeDepartureTime = async (time: string, dayIndex?: number) => { export const updatePizzaFee = async (login: string, text?: string, price?: number) => { return await api.post('/api/updatePizzaFee', JSON.stringify({ login, text, price })); +} + +export const getFeatureVotes = async () => { + return await api.get('/api/getFeatureVotes'); +} + +export const updateFeatureVote = async (option: FeatureRequest, active: boolean) => { + return await api.post('/api/updateFeatureVote', JSON.stringify({ option, active })); } \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 4ba14bf..6a79fbc 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -356,6 +356,7 @@ function App() {
  • Možnost ručního zadání příplatku k Pizza day objednávkám
  • Vylepšená detekce uzavření pro podniky Sladovnická a TechTower
  • Úprava zvýraznění aktuálního dne
  • +
  • Možnost hlasování o nových funkcích
  • {dayIndex != null && diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index c0aabc9..d3be02a 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,21 +1,34 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Navbar, Nav, NavDropdown } from "react-bootstrap"; import { useAuth } from "../context/auth"; import BankAccountModal from "./modals/BankAccountModal"; import { useBank } from "../context/bank"; +import FeaturesVotingModal from "./modals/FeaturesVotingModal"; +import { FeatureRequest } from "../types"; +import { errorHandler, getFeatureVotes, updateFeatureVote } from "../Api"; export default function Header() { const auth = useAuth(); const bank = useBank(); - const [modalOpen, setModalOpen] = useState(false); + const [bankModalOpen, setBankModalOpen] = useState(false); + const [votingModalOpen, setVotingModalOpen] = useState(false); + const [featureVotes, setFeatureVotes] = useState([]); - const openBankSettings = () => { - setModalOpen(true); + useEffect(() => { + if (auth?.login) { + getFeatureVotes().then(votes => { + setFeatureVotes(votes); + }) + } + }, [auth?.login]); + + const closeBankModal = () => { + setBankModalOpen(false); } - const closeModal = () => { - setModalOpen(false); + const closeVotingModal = () => { + setVotingModalOpen(false); } const isValidInteger = (str: string) => { @@ -28,7 +41,7 @@ export default function Header() { return n !== Infinity && String(n) === str && n >= 0; } - const save = (bankAccountNumber?: string, bankAccountHolderName?: string) => { + const saveBankAccount = (bankAccountNumber?: string, bankAccountHolderName?: string) => { if (bankAccountNumber) { try { // Validace kódu banky @@ -72,7 +85,18 @@ export default function Header() { } bank?.setBankAccountNumber(bankAccountNumber); bank?.setBankAccountHolderName(bankAccountHolderName); - closeModal(); + closeBankModal(); + } + + const saveFeatureVote = async (option: FeatureRequest, active: boolean) => { + await errorHandler(() => updateFeatureVote(option, active)); + const votes = [...featureVotes]; + if (active) { + votes.push(option); + } else { + votes.splice(votes.indexOf(option), 1); + } + setFeatureVotes(votes); } return @@ -81,11 +105,13 @@ export default function Header() { - + + } \ No newline at end of file diff --git a/client/src/components/modals/FeaturesVotingModal.tsx b/client/src/components/modals/FeaturesVotingModal.tsx new file mode 100644 index 0000000..9edf601 --- /dev/null +++ b/client/src/components/modals/FeaturesVotingModal.tsx @@ -0,0 +1,45 @@ +import { Modal, Button, Form } from "react-bootstrap" +import { FeatureRequest } from "../../types"; + +type Props = { + isOpen: boolean, + onClose: () => void, + onChange: (option: FeatureRequest, active: boolean) => void, + initialValues?: FeatureRequest[], +} + +/** Modální dialog pro hlasování o nových funkcích. */ +export default function FeaturesVotingModal({ isOpen, onClose, onChange, initialValues }: Props) { + + const handleChange = (e: React.ChangeEvent) => { + onChange(e.currentTarget.value as FeatureRequest, e.currentTarget.checked); + } + + return + + + Hlasujte pro nové funkce +

    Je možno vybrat maximálně 3 možnosti

    +
    +
    + + {(Object.keys(FeatureRequest) as Array).map(key => { + return + })} +

    Něco jiného? Dejte vědět.

    +
    + + + +
    +} \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index e6efd51..18bbc6b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -9,6 +9,7 @@ import path from 'path'; import { getQr } from "./qr"; import { generateToken, getLogin, getTrusted, verify } from "./auth"; import { InsufficientPermissions, getDayOfWeekIndex } from "./utils"; +import { getUserVotes, updateFeatureVote } from "./voting"; const ENVIRONMENT = process.env.NODE_ENV || 'production'; dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); @@ -324,6 +325,24 @@ app.post("/api/updatePizzaFee", async (req, res, next) => { } catch (e: any) { next(e) } }); +app.get("/api/getFeatureVotes", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await getUserVotes(login); + res.status(200).json(data); +}); + +app.post("/api/updateFeatureVote", async (req, res, next) => { + const login = getLogin(parseToken(req)); + if (req.body?.option == null || req.body?.active == null) { + res.status(400).json({ error: "Chybné parametry volání" }); + } + try { + const data = await updateFeatureVote(login, req.body.option, req.body.active); + io.emit("message", data); + res.status(200).json(data); + } catch (e: any) { next(e) } +}); + // Middleware pro zpracování chyb app.use((err: any, req: any, res: any, next: any) => { if (err instanceof InsufficientPermissions) { diff --git a/server/src/voting.ts b/server/src/voting.ts new file mode 100644 index 0000000..97dc9b8 --- /dev/null +++ b/server/src/voting.ts @@ -0,0 +1,56 @@ +import { FeatureRequest } from "../../types"; +import getStorage from "./storage"; + +interface VotingData { + [login: string]: FeatureRequest[], +} + +const storage = getStorage(); +const STORAGE_KEY = 'voting'; + +/** + * Vrátí pole voleb, pro které uživatel aktuálně hlasuje. + * + * @param login login uživatele + * @returns pole voleb + */ +export async function getUserVotes(login: string) { + const data: VotingData = await storage.getData(STORAGE_KEY); + return data[login]; +} + +/** + * Aktualizuje hlas uživatele pro konkrétní volbu. + * + * @param login login uživatele + * @param option volba + * @param active příznak, zda volbu přidat nebo odebrat + * @returns aktuální data + */ +export async function updateFeatureVote(login: string, option: FeatureRequest, active: boolean): Promise { + let data: VotingData = await storage.getData(STORAGE_KEY); + if (data == null) { + data = {}; + } + if (!(login in data)) { + data[login] = []; + } + const index = data[login].indexOf(option); + if (index > -1) { + if (active) { + throw Error('Pro tuto možnost jste již hlasovali'); + } else { + data[login].splice(index, 1); + if (data[login].length === 0) { + delete data[login]; + } + } + } else if (active) { + if (data[login].length == 3) { + throw Error('Je možné hlasovat pro maximálně 3 možnosti'); + } + data[login].push(option); + } + await storage.setData(STORAGE_KEY, data); + return data; +} \ No newline at end of file diff --git a/types/Types.ts b/types/Types.ts index 7ffadb1..ccc2c44 100644 --- a/types/Types.ts +++ b/types/Types.ts @@ -143,4 +143,15 @@ export enum DepartureTime { T12_30 = "12:30", T12_45 = "12:45", T13_00 = "13:00", +} + +export enum FeatureRequest { + SINGLE_PAYMENT = "Možnost úhrady v podniku jednou osobou a generování QR pro ostatní", + NOTIFICATIONS = "Podpora push notifikací na mobil", + STATISTICS = "Statistiky (nejoblíbenější podnik, nejpopulárnější jídla, nejobjednávanější pizzy, ...)", + 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" } \ No newline at end of file