feat: Základ generování QR kódů
This commit is contained in:
@@ -452,7 +452,7 @@ function App() {
|
||||
return (
|
||||
<div className="app-container">
|
||||
{easterEgg && eggImage && <img ref={eggRef} alt='' src={URL.createObjectURL(eggImage)} style={{ position: 'absolute', ...EASTER_EGG_STYLE, ...style, animationDuration: `${duration ?? EASTER_EGG_DEFAULT_DURATION}s` }} />}
|
||||
<Header />
|
||||
<Header dayIndex={dayIndex} />
|
||||
<div className='wrapper'>
|
||||
{isTodayWeekend ? <h4>Užívejte víkend :)</h4> : <>
|
||||
<Alert variant={'primary'}>
|
||||
|
||||
@@ -6,11 +6,16 @@ import { useSettings } from "../context/settings";
|
||||
import FeaturesVotingModal from "./modals/FeaturesVotingModal";
|
||||
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
||||
import RefreshMenuModal from "./modals/RefreshMenuModal";
|
||||
import GenerateQRModal from "./modals/GenerateQRModal";
|
||||
import { useNavigate } from "react-router";
|
||||
import { STATS_URL } from "../AppRoutes";
|
||||
import { FeatureRequest, getVotes, updateVote } from "../../../types";
|
||||
|
||||
export default function Header() {
|
||||
type Props = {
|
||||
dayIndex?: number;
|
||||
}
|
||||
|
||||
export default function Header({ dayIndex }: Readonly<Props>) {
|
||||
const auth = useAuth();
|
||||
const settings = useSettings();
|
||||
const navigate = useNavigate();
|
||||
@@ -18,6 +23,7 @@ export default function Header() {
|
||||
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
||||
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
||||
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
|
||||
const [generateQRModalOpen, setGenerateQRModalOpen] = useState<boolean>(false);
|
||||
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,6 +50,10 @@ export default function Header() {
|
||||
setRefreshMenuModalOpen(false);
|
||||
}
|
||||
|
||||
const closeGenerateQRModal = () => {
|
||||
setGenerateQRModalOpen(false);
|
||||
}
|
||||
|
||||
const isValidInteger = (str: string) => {
|
||||
str = str.trim();
|
||||
if (!str) {
|
||||
@@ -121,6 +131,7 @@ export default function Header() {
|
||||
<NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown">
|
||||
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setRefreshMenuModalOpen(true)}>Přenačtení menu</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setGenerateQRModalOpen(true)}>Generování QR</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||
@@ -131,6 +142,7 @@ export default function Header() {
|
||||
</Navbar.Collapse>
|
||||
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
|
||||
<RefreshMenuModal isOpen={refreshMenuModalOpen} onClose={closeRefreshMenuModal} />
|
||||
<GenerateQRModal isOpen={generateQRModalOpen} onClose={closeGenerateQRModal} dayIndex={dayIndex} bankAccount={settings?.bankAccount} bankAccountHolder={settings?.holderName} />
|
||||
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
|
||||
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
|
||||
</Navbar>
|
||||
|
||||
176
client/src/components/modals/GenerateQRModal.tsx
Normal file
176
client/src/components/modals/GenerateQRModal.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal, Button, Table, Form, Alert } from "react-bootstrap";
|
||||
import { ClientData, generateQr, getData } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean,
|
||||
onClose: () => void,
|
||||
dayIndex?: number,
|
||||
bankAccount?: string,
|
||||
bankAccountHolder?: string,
|
||||
}
|
||||
|
||||
type UserQRData = {
|
||||
login: string;
|
||||
selected: boolean;
|
||||
note: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
/** Modální dialog pro generování QR kódů. */
|
||||
export default function GenerateQRModal({ isOpen, onClose, dayIndex, bankAccount, bankAccountHolder }: Readonly<Props>) {
|
||||
const [users, setUsers] = useState<UserQRData[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const isBankDataValid = bankAccount && bankAccountHolder;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLoading(true);
|
||||
getData({ query: { dayIndex } }).then(response => {
|
||||
const data: ClientData = response.data;
|
||||
const userList: UserQRData[] = [];
|
||||
|
||||
// Projdeme všechny volby stravování a získáme uživatele
|
||||
if (data.choices) {
|
||||
Object.entries(data.choices).forEach(([locationKey, locationUsers]) => {
|
||||
Object.keys(locationUsers).forEach(login => {
|
||||
// Přidáme uživatele pouze pokud tam ještě není
|
||||
if (!userList.find(u => u.login === login)) {
|
||||
userList.push({
|
||||
login,
|
||||
selected: false,
|
||||
note: '',
|
||||
amount: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setUsers(userList);
|
||||
setLoading(false);
|
||||
}).catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [isOpen, dayIndex]);
|
||||
|
||||
const handleCheckboxChange = (login: string) => {
|
||||
setUsers(users.map(u =>
|
||||
u.login === login ? { ...u, selected: !u.selected } : u
|
||||
));
|
||||
};
|
||||
|
||||
const handleNoteChange = (login: string, note: string) => {
|
||||
setUsers(users.map(u =>
|
||||
u.login === login ? { ...u, note } : u
|
||||
));
|
||||
};
|
||||
|
||||
const handleAmountChange = (login: string, amount: string) => {
|
||||
setUsers(users.map(u =>
|
||||
u.login === login ? { ...u, amount } : u
|
||||
));
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const selectedUsers = users.filter(u => u.selected);
|
||||
// TODO: Implementovat generování QR kódů
|
||||
console.log('Generování QR pro:', selectedUsers);
|
||||
alert('Funkce generování QR bude implementována');
|
||||
await generateQr({
|
||||
body: {
|
||||
bankAccount: bankAccount!,
|
||||
bankAccountHolder: bankAccountHolder!,
|
||||
qrCodes: selectedUsers.map(u => ({
|
||||
login: u.login,
|
||||
des: u.note,
|
||||
amount: Number.parseFloat(u.amount)
|
||||
}))
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setUsers([]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={handleClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>Generování QR kódů</h2></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{!isBankDataValid && (
|
||||
<Alert variant="warning">
|
||||
<strong>Upozornění:</strong> Pro generování QR kódů je nutné mít v nastavení vyplněné číslo bankovního účtu a jméno majitele účtu.
|
||||
</Alert>
|
||||
)}
|
||||
{loading ? (
|
||||
<p>Načítání uživatelů...</p>
|
||||
) : users.length === 0 ? (
|
||||
<p>Pro aktuální den nemá žádný uživatel vybranou volbu stravování.</p>
|
||||
) : (
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '50px' }}></th>
|
||||
<th>Uživatel</th>
|
||||
<th>Poznámka</th>
|
||||
<th style={{ width: '120px' }}>Částka (Kč)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map(user => (
|
||||
<tr key={user.login}>
|
||||
<td className="text-center">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
checked={user.selected}
|
||||
onChange={() => handleCheckboxChange(user.login)}
|
||||
/>
|
||||
</td>
|
||||
<td>{user.login}</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={user.note}
|
||||
onChange={(e) => handleNoteChange(user.login, e.target.value)}
|
||||
placeholder="Poznámka"
|
||||
disabled={!user.selected}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={user.amount}
|
||||
onChange={(e) => handleAmountChange(user.login, e.target.value)}
|
||||
placeholder="0.00"
|
||||
disabled={!user.selected}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Zavřít
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleGenerate}
|
||||
disabled={users.filter(u => u.selected).length === 0 || !isBankDataValid}
|
||||
>
|
||||
Generovat
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export default function StatsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Header dayIndex={undefined} />
|
||||
<div className="stats-page">
|
||||
<h1>Statistiky</h1>
|
||||
<div className="week-navigator">
|
||||
|
||||
Reference in New Issue
Block a user