All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
|
import { generateQr, LunchChoices, QrRecipient } from "../../../../types";
|
|
|
|
type UserEntry = {
|
|
login: string;
|
|
selected: boolean;
|
|
purpose: string;
|
|
amount: string;
|
|
};
|
|
|
|
type Props = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
choices: LunchChoices;
|
|
bankAccount: string;
|
|
bankAccountHolder: string;
|
|
};
|
|
|
|
/** Modální dialog pro generování QR kódů pro platbu. */
|
|
export default function GenerateQrModal({ isOpen, onClose, choices, bankAccount, bankAccountHolder }: Readonly<Props>) {
|
|
const [users, setUsers] = useState<UserEntry[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
// Při otevření modálu načteme seznam uživatelů z choices
|
|
useEffect(() => {
|
|
if (isOpen && choices) {
|
|
const userLogins = new Set<string>();
|
|
// Projdeme všechny lokace a získáme unikátní loginy
|
|
Object.values(choices).forEach(locationChoices => {
|
|
if (locationChoices) {
|
|
Object.keys(locationChoices).forEach(login => {
|
|
userLogins.add(login);
|
|
});
|
|
}
|
|
});
|
|
// Vytvoříme seznam uživatelů
|
|
const userList: UserEntry[] = Array.from(userLogins)
|
|
.sort((a, b) => a.localeCompare(b, 'cs'))
|
|
.map(login => ({
|
|
login,
|
|
selected: false,
|
|
purpose: '',
|
|
amount: '',
|
|
}));
|
|
setUsers(userList);
|
|
setError(null);
|
|
setSuccess(false);
|
|
}
|
|
}, [isOpen, choices]);
|
|
|
|
const handleCheckboxChange = useCallback((login: string, checked: boolean) => {
|
|
setUsers(prev => prev.map(u =>
|
|
u.login === login ? { ...u, selected: checked } : u
|
|
));
|
|
}, []);
|
|
|
|
const handlePurposeChange = useCallback((login: string, value: string) => {
|
|
setUsers(prev => prev.map(u =>
|
|
u.login === login ? { ...u, purpose: value } : u
|
|
));
|
|
}, []);
|
|
|
|
const handleAmountChange = useCallback((login: string, value: string) => {
|
|
// Povolíme pouze čísla, tečku a čárku
|
|
const sanitized = value.replace(/[^0-9.,]/g, '').replace(',', '.');
|
|
setUsers(prev => prev.map(u =>
|
|
u.login === login ? { ...u, amount: sanitized } : u
|
|
));
|
|
}, []);
|
|
|
|
const validateAmount = (amountStr: string): number | null => {
|
|
if (!amountStr || amountStr.trim().length === 0) {
|
|
return null;
|
|
}
|
|
const amount = parseFloat(amountStr);
|
|
if (isNaN(amount) || amount <= 0) {
|
|
return null;
|
|
}
|
|
// Max 2 desetinná místa
|
|
const parts = amountStr.split('.');
|
|
if (parts.length === 2 && parts[1].length > 2) {
|
|
return null;
|
|
}
|
|
return Math.round(amount * 100) / 100; // Zaokrouhlíme na 2 desetinná místa
|
|
};
|
|
|
|
const handleGenerate = async () => {
|
|
setError(null);
|
|
const selectedUsers = users.filter(u => u.selected);
|
|
|
|
if (selectedUsers.length === 0) {
|
|
setError("Nebyl vybrán žádný uživatel");
|
|
return;
|
|
}
|
|
|
|
// Validace
|
|
const recipients: QrRecipient[] = [];
|
|
for (const user of selectedUsers) {
|
|
if (!user.purpose || user.purpose.trim().length === 0) {
|
|
setError(`Uživatel ${user.login} nemá vyplněný účel platby`);
|
|
return;
|
|
}
|
|
const amount = validateAmount(user.amount);
|
|
if (amount === null) {
|
|
setError(`Uživatel ${user.login} má neplatnou částku (musí být kladné číslo s max. 2 desetinnými místy)`);
|
|
return;
|
|
}
|
|
recipients.push({
|
|
login: user.login,
|
|
purpose: user.purpose.trim(),
|
|
amount,
|
|
});
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const response = await generateQr({
|
|
body: {
|
|
recipients,
|
|
bankAccount,
|
|
bankAccountHolder,
|
|
}
|
|
});
|
|
if (response.error) {
|
|
setError((response.error as any).error || 'Nastala chyba při generování QR kódů');
|
|
} else {
|
|
setSuccess(true);
|
|
// Po 2 sekundách zavřeme modal
|
|
setTimeout(() => {
|
|
onClose();
|
|
}, 2000);
|
|
}
|
|
} catch (e: any) {
|
|
setError(e.message || 'Nastala chyba při generování QR kódů');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setError(null);
|
|
setSuccess(false);
|
|
onClose();
|
|
};
|
|
|
|
const selectedCount = users.filter(u => u.selected).length;
|
|
|
|
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>
|
|
{success ? (
|
|
<Alert variant="success">
|
|
QR kódy byly úspěšně vygenerovány! Uživatelé je uvidí v sekci "Nevyřízené platby".
|
|
</Alert>
|
|
) : (
|
|
<>
|
|
<p>
|
|
Vyberte uživatele, kterým chcete vygenerovat QR kód pro platbu.
|
|
QR kódy se uživatelům zobrazí v sekci "Nevyřízené platby".
|
|
</p>
|
|
|
|
{error && (
|
|
<Alert variant="danger" onClose={() => setError(null)} dismissible>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{users.length === 0 ? (
|
|
<Alert variant="info">
|
|
V tento den nemá žádný uživatel zvolenou možnost stravování.
|
|
</Alert>
|
|
) : (
|
|
<Table striped bordered hover responsive>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: '50px' }}></th>
|
|
<th>Uživatel</th>
|
|
<th>Účel platby</th>
|
|
<th style={{ width: '120px' }}>Částka (Kč)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map(user => (
|
|
<tr key={user.login} className={user.selected ? '' : 'text-muted'}>
|
|
<td className="text-center">
|
|
<Form.Check
|
|
type="checkbox"
|
|
checked={user.selected}
|
|
onChange={e => handleCheckboxChange(user.login, e.target.checked)}
|
|
/>
|
|
</td>
|
|
<td>{user.login}</td>
|
|
<td>
|
|
<Form.Control
|
|
type="text"
|
|
placeholder="např. Pizza prosciutto"
|
|
value={user.purpose}
|
|
onChange={e => handlePurposeChange(user.login, e.target.value)}
|
|
disabled={!user.selected}
|
|
size="sm"
|
|
onKeyDown={e => e.stopPropagation()}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<Form.Control
|
|
type="text"
|
|
placeholder="0.00"
|
|
value={user.amount}
|
|
onChange={e => handleAmountChange(user.login, e.target.value)}
|
|
disabled={!user.selected}
|
|
size="sm"
|
|
onKeyDown={e => e.stopPropagation()}
|
|
/>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</Table>
|
|
)}
|
|
</>
|
|
)}
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
{!success && (
|
|
<>
|
|
<span className="me-auto text-muted">
|
|
Vybráno: {selectedCount} / {users.length}
|
|
</span>
|
|
<Button variant="secondary" onClick={handleClose} disabled={loading}>
|
|
Storno
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleGenerate}
|
|
disabled={loading || selectedCount === 0}
|
|
>
|
|
{loading ? 'Generuji...' : 'Generovat'}
|
|
</Button>
|
|
</>
|
|
)}
|
|
{success && (
|
|
<Button variant="secondary" onClick={handleClose}>
|
|
Zavřít
|
|
</Button>
|
|
)}
|
|
</Modal.Footer>
|
|
</Modal>
|
|
);
|
|
}
|