Files
Luncher/client/src/components/modals/EditGroupFeesModal.tsx
T
mates c85842267a
CI / Generate TypeScript types (push) Successful in 14s
CI / Server unit tests (push) Successful in 20s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 33s
CI / Playwright E2E tests (push) Successful in 1m17s
CI / Build and push Docker image (push) Successful in 39s
CI / Notify (push) Successful in 2s
feat: podpora neplatících osob u objednávek
2026-06-05 11:31:55 +02:00

194 lines
9.3 KiB
TypeScript

import { useState, useEffect } from "react";
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
import { updateGroupFees, OrderGroup, OrderGroupMember } from "../../../../types";
import { computeFeeShare, computeMemberTotal, countActiveMembers, isActiveMember } from "../../utils/groupFees";
type Props = {
isOpen: boolean;
onClose: () => void;
group: OrderGroup;
onSaved: (data: any) => void;
};
function parseHal(s: string): number {
const n = parseFloat(s.replace(',', '.'));
return isNaN(n) || n < 0 ? 0 : Math.round(n * 100);
}
function parsePercent(s: string): number {
const n = parseFloat(s.replace(',', '.'));
return isNaN(n) || n < 0 ? 0 : Math.round(n);
}
export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }: Readonly<Props>) {
const [fees, setFees] = useState('');
const [shipping, setShipping] = useState('');
const [tip, setTip] = useState('');
const [discountType, setDiscountType] = useState<'percent' | 'fixed'>('percent');
const [discountValue, setDiscountValue] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!isOpen) return;
setFees(group.fees ? String(group.fees / 100) : '');
setShipping(group.shipping ? String(group.shipping / 100) : '');
setTip(group.tip ? String(group.tip / 100) : '');
setDiscountType((group.discountType as 'percent' | 'fixed') ?? 'percent');
setDiscountValue(group.discountValue
? ((group.discountType as string) === 'fixed' ? String(group.discountValue / 100) : String(group.discountValue))
: '');
setError(null);
}, [isOpen, group]);
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
// Poplatky se dělí jen mezi aktivní strávníky (kdo si reálně něco objednal).
const activeCount = countActiveMembers(group.members);
const feesNum = parseHal(fees);
const shippingNum = parseHal(shipping);
const tipNum = parseHal(tip);
const discountNum = discountType === 'percent' ? parsePercent(discountValue) : parseHal(discountValue);
const totalFees = feesNum + shippingNum + tipNum;
const feeShare = computeFeeShare(totalFees, activeCount);
const feeParams = { totalFees, discountType, discountValue: discountNum };
const handleSave = async () => {
setError(null);
setLoading(true);
try {
const res = await updateGroupFees({
body: {
id: group.id,
fees: feesNum,
shipping: shippingNum,
tip: tipNum,
discountType: discountNum > 0 ? discountType : undefined,
discountValue: discountNum > 0 ? discountNum : undefined,
}
});
if (res.error) {
setError((res.error as any).error || 'Nastala chyba');
} else {
onSaved(res.data);
onClose();
}
} catch (e: any) {
setError(e.message || 'Nastala chyba');
} finally {
setLoading(false);
}
};
return (
<Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton>
<Modal.Title><h2>Poplatky skupiny {group.name}</h2></Modal.Title>
</Modal.Header>
<Modal.Body>
{error && (
<Alert variant="danger" onClose={() => setError(null)} dismissible>{error}</Alert>
)}
<div className="d-flex gap-3 flex-wrap mb-3">
<Form.Group>
<Form.Label>Poplatky ()</Form.Label>
<Form.Control
type="number" min={0} step={0.01}
value={fees} onChange={e => setFees(e.target.value)}
placeholder="0" style={{ width: 110 }}
onKeyDown={e => e.stopPropagation()}
/>
</Form.Group>
<Form.Group>
<Form.Label>Doprava ()</Form.Label>
<Form.Control
type="number" min={0} step={0.01}
value={shipping} onChange={e => setShipping(e.target.value)}
placeholder="0" style={{ width: 110 }}
onKeyDown={e => e.stopPropagation()}
/>
</Form.Group>
<Form.Group>
<Form.Label>Spropitné ()</Form.Label>
<Form.Control
type="number" min={0} step={0.01}
value={tip} onChange={e => setTip(e.target.value)}
placeholder="0" style={{ width: 110 }}
onKeyDown={e => e.stopPropagation()}
/>
</Form.Group>
</div>
<div className="d-flex gap-3 align-items-end flex-wrap mb-3">
<Form.Group>
<Form.Label>Sleva</Form.Label>
<div className="d-flex gap-2 align-items-center">
<Form.Select
value={discountType}
onChange={e => setDiscountType(e.target.value as 'percent' | 'fixed')}
style={{ width: 160 }}
>
<option value="percent">Procentuální (%)</option>
<option value="fixed">Pevná částka ()</option>
</Form.Select>
<Form.Control
type="number" min={0} step={discountType === 'percent' ? 1 : 0.01}
value={discountValue} onChange={e => setDiscountValue(e.target.value)}
placeholder="0" style={{ width: 100 }}
onKeyDown={e => e.stopPropagation()}
/>
<span className="text-muted">{discountType === 'percent' ? '%' : 'Kč'}</span>
</div>
</Form.Group>
</div>
<hr />
<h6>Náhled celkových částek ({activeCount} {activeCount === 1 ? 'strávník' : 'strávníků'} s objednávkou, {feeShare > 0 ? `poplatek ${feeShare / 100} Kč/os.` : 'bez poplatku'})</h6>
<Table size="sm" bordered>
<thead>
<tr>
<th>Člen</th>
<th className="text-end">Základ</th>
<th className="text-end">Příplatek</th>
<th className="text-end">Poplatek</th>
<th className="text-end">Sleva</th>
<th className="text-end fw-bold">Celkem</th>
</tr>
</thead>
<tbody>
{memberEntries.map(([login, member]) => {
const base = member.amount ?? 0;
const surcharge = member.surchargeAmount ?? 0;
const active = isActiveMember(member);
const total = computeMemberTotal(member, feeParams, feeShare, activeCount);
// Sleva i poplatek se týkají jen aktivních strávníků.
const discount = active && discountNum > 0
? (discountType === 'percent'
? Math.round((base + surcharge) * discountNum / 100)
: Math.round(discountNum / activeCount))
: 0;
return (
<tr key={login} className={active ? '' : 'text-muted'}>
<td><strong>{login}</strong>{!active && <small className="ms-1">(jen objednává)</small>}</td>
<td className="text-end">{base > 0 ? `${base / 100} Kč` : '—'}</td>
<td className="text-end">{surcharge > 0 ? `${surcharge / 100} Kč` : '—'}</td>
<td className="text-end">{active && feeShare > 0 ? `${feeShare / 100} Kč` : '—'}</td>
<td className="text-end text-danger">{discount > 0 ? `-${discount / 100} Kč` : '—'}</td>
<td className="text-end fw-bold">{total > 0 ? `${total / 100} Kč` : '—'}</td>
</tr>
);
})}
</tbody>
</Table>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onClose} disabled={loading}>Storno</Button>
<Button variant="primary" onClick={handleSave} disabled={loading}>
{loading ? 'Ukládám...' : 'Uložit'}
</Button>
</Modal.Footer>
</Modal>
);
}