Možnost hlasování o nových funkcích

This commit is contained in:
Martin Berka 2023-09-27 18:35:18 +02:00
parent 401833f763
commit 8e285e9197
7 changed files with 177 additions and 11 deletions

View File

@ -1,5 +1,5 @@
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { PizzaOrder } from "./types"; import { FeatureRequest, PizzaOrder } from "./types";
import { getBaseUrl, getToken } from "./Utils"; import { getBaseUrl, getToken } from "./Utils";
/** /**
@ -111,3 +111,11 @@ export const changeDepartureTime = async (time: string, dayIndex?: number) => {
export const updatePizzaFee = async (login: string, text?: string, price?: number) => { export const updatePizzaFee = async (login: string, text?: string, price?: number) => {
return await api.post<any, any>('/api/updatePizzaFee', JSON.stringify({ login, text, price })); return await api.post<any, any>('/api/updatePizzaFee', JSON.stringify({ login, text, price }));
} }
export const getFeatureVotes = async () => {
return await api.get<any>('/api/getFeatureVotes');
}
export const updateFeatureVote = async (option: FeatureRequest, active: boolean) => {
return await api.post<any, any>('/api/updateFeatureVote', JSON.stringify({ option, active }));
}

View File

@ -356,6 +356,7 @@ function App() {
<li>Možnost ručního zadání příplatku k Pizza day objednávkám</li> <li>Možnost ručního zadání příplatku k Pizza day objednávkám</li>
<li>Vylepšená detekce uzavření pro podniky Sladovnická a TechTower</li> <li>Vylepšená detekce uzavření pro podniky Sladovnická a TechTower</li>
<li>Úprava zvýraznění aktuálního dne</li> <li>Úprava zvýraznění aktuálního dne</li>
<li>Možnost hlasování o nových funkcích</li>
</ul> </ul>
</Alert> </Alert>
{dayIndex != null && {dayIndex != null &&

View File

@ -1,21 +1,34 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Navbar, Nav, NavDropdown } from "react-bootstrap"; import { Navbar, Nav, NavDropdown } from "react-bootstrap";
import { useAuth } from "../context/auth"; import { useAuth } from "../context/auth";
import BankAccountModal from "./modals/BankAccountModal"; import BankAccountModal from "./modals/BankAccountModal";
import { useBank } from "../context/bank"; import { useBank } from "../context/bank";
import FeaturesVotingModal from "./modals/FeaturesVotingModal";
import { FeatureRequest } from "../types";
import { errorHandler, getFeatureVotes, updateFeatureVote } from "../Api";
export default function Header() { export default function Header() {
const auth = useAuth(); const auth = useAuth();
const bank = useBank(); const bank = useBank();
const [modalOpen, setModalOpen] = useState<boolean>(false); const [bankModalOpen, setBankModalOpen] = useState<boolean>(false);
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[]>([]);
const openBankSettings = () => { useEffect(() => {
setModalOpen(true); if (auth?.login) {
getFeatureVotes().then(votes => {
setFeatureVotes(votes);
})
}
}, [auth?.login]);
const closeBankModal = () => {
setBankModalOpen(false);
} }
const closeModal = () => { const closeVotingModal = () => {
setModalOpen(false); setVotingModalOpen(false);
} }
const isValidInteger = (str: string) => { const isValidInteger = (str: string) => {
@ -28,7 +41,7 @@ export default function Header() {
return n !== Infinity && String(n) === str && n >= 0; return n !== Infinity && String(n) === str && n >= 0;
} }
const save = (bankAccountNumber?: string, bankAccountHolderName?: string) => { const saveBankAccount = (bankAccountNumber?: string, bankAccountHolderName?: string) => {
if (bankAccountNumber) { if (bankAccountNumber) {
try { try {
// Validace kódu banky // Validace kódu banky
@ -72,7 +85,18 @@ export default function Header() {
} }
bank?.setBankAccountNumber(bankAccountNumber); bank?.setBankAccountNumber(bankAccountNumber);
bank?.setBankAccountHolderName(bankAccountHolderName); 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 <Navbar variant='dark' expand="lg"> return <Navbar variant='dark' expand="lg">
@ -81,11 +105,13 @@ export default function Header() {
<Navbar.Collapse id="basic-navbar-nav"> <Navbar.Collapse id="basic-navbar-nav">
<Nav className="nav"> <Nav className="nav">
<NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown"> <NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown">
<NavDropdown.Item onClick={openBankSettings}>Nastavit číslo účtu</NavDropdown.Item> <NavDropdown.Item onClick={() => setBankModalOpen(true)}>Nastavit číslo účtu</NavDropdown.Item>
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item> <NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
</NavDropdown> </NavDropdown>
</Nav> </Nav>
</Navbar.Collapse> </Navbar.Collapse>
<BankAccountModal isOpen={modalOpen} onClose={closeModal} onSave={save} /> <BankAccountModal isOpen={bankModalOpen} onClose={closeBankModal} onSave={saveBankAccount} />
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
</Navbar> </Navbar>
} }

View File

@ -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<HTMLInputElement>) => {
onChange(e.currentTarget.value as FeatureRequest, e.currentTarget.checked);
}
return <Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton>
<Modal.Title>
Hlasujte pro nové funkce
<p style={{ fontSize: '12px' }}>Je možno vybrat maximálně 3 možnosti</p>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{(Object.keys(FeatureRequest) as Array<keyof typeof FeatureRequest>).map(key => {
return <Form.Check
key={key}
type='checkbox'
id={key}
label={FeatureRequest[key]}
onChange={handleChange}
value={key}
defaultChecked={initialValues && initialValues.includes(key as FeatureRequest)}
/>
})}
<p className="mt-3" style={{ fontSize: '12px' }}>Něco jiného? Dejte vědět.</p>
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={onClose}>
Zavřít
</Button>
</Modal.Footer>
</Modal>
}

View File

@ -9,6 +9,7 @@ import path from 'path';
import { getQr } from "./qr"; import { getQr } from "./qr";
import { generateToken, getLogin, getTrusted, verify } from "./auth"; import { generateToken, getLogin, getTrusted, verify } from "./auth";
import { InsufficientPermissions, getDayOfWeekIndex } from "./utils"; import { InsufficientPermissions, getDayOfWeekIndex } from "./utils";
import { getUserVotes, updateFeatureVote } from "./voting";
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}`) });
@ -324,6 +325,24 @@ app.post("/api/updatePizzaFee", async (req, res, next) => {
} catch (e: any) { next(e) } } 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 // Middleware pro zpracování chyb
app.use((err: any, req: any, res: any, next: any) => { app.use((err: any, req: any, res: any, next: any) => {
if (err instanceof InsufficientPermissions) { if (err instanceof InsufficientPermissions) {

56
server/src/voting.ts Normal file
View File

@ -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<VotingData> {
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;
}

View File

@ -144,3 +144,14 @@ export enum DepartureTime {
T12_45 = "12:45", T12_45 = "12:45",
T13_00 = "13:00", 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"
}