Možnost hlasování o nových funkcích
This commit is contained in:
parent
401833f763
commit
8e285e9197
@ -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<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 }));
|
||||
}
|
@ -356,6 +356,7 @@ function App() {
|
||||
<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>Úprava zvýraznění aktuálního dne</li>
|
||||
<li>Možnost hlasování o nových funkcích</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
{dayIndex != null &&
|
||||
|
@ -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<boolean>(false);
|
||||
const [bankModalOpen, setBankModalOpen] = useState<boolean>(false);
|
||||
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
||||
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[]>([]);
|
||||
|
||||
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 <Navbar variant='dark' expand="lg">
|
||||
@ -81,11 +105,13 @@ export default function Header() {
|
||||
<Navbar.Collapse id="basic-navbar-nav">
|
||||
<Nav className="nav">
|
||||
<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>
|
||||
</Nav>
|
||||
</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>
|
||||
}
|
45
client/src/components/modals/FeaturesVotingModal.tsx
Normal file
45
client/src/components/modals/FeaturesVotingModal.tsx
Normal 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>
|
||||
}
|
@ -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) {
|
||||
|
56
server/src/voting.ts
Normal file
56
server/src/voting.ts
Normal 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;
|
||||
}
|
@ -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"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user