feat: Základ generování QR kódů

This commit is contained in:
2025-12-02 11:46:52 +01:00
parent 0179afca75
commit 2e8774900f
11 changed files with 430 additions and 4 deletions

View File

@@ -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'}>

View File

@@ -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>

View 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 ()</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>
);
}

View File

@@ -103,7 +103,7 @@ export default function StatsPage() {
return (
<>
<Header />
<Header dayIndex={undefined} />
<div className="stats-page">
<h1>Statistiky</h1>
<div className="week-navigator">