feat: vylepšení objednávek
CI / Generate TypeScript types (pull_request) Successful in 20s
CI / Server unit tests (pull_request) Failing after 20s
CI / Build client (pull_request) Failing after 30s
CI / Build server (pull_request) Successful in 3m13s
CI / Playwright E2E tests (pull_request) Has been skipped
CI / Build and push Docker image (pull_request) Has been skipped
CI / Notify (pull_request) Has been skipped
CI / Build client (push) Failing after 10m5s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Failing after 22s
CI / Build server (push) Successful in 41s
CI / Playwright E2E tests (push) Has been skipped
CI / Build and push Docker image (push) Has been skipped
CI / Notify (push) Successful in 4s
CI / Generate TypeScript types (pull_request) Successful in 20s
CI / Server unit tests (pull_request) Failing after 20s
CI / Build client (pull_request) Failing after 30s
CI / Build server (pull_request) Successful in 3m13s
CI / Playwright E2E tests (pull_request) Has been skipped
CI / Build and push Docker image (pull_request) Has been skipped
CI / Notify (pull_request) Has been skipped
CI / Build client (push) Failing after 10m5s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Failing after 22s
CI / Build server (push) Successful in 41s
CI / Playwright E2E tests (push) Has been skipped
CI / Build and push Docker image (push) Has been skipped
CI / Notify (push) Successful in 4s
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
||||
import { updateGroupFees, OrderGroup, OrderGroupMember } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
group: OrderGroup;
|
||||
onSaved: (data: any) => void;
|
||||
};
|
||||
|
||||
function parseNum(s: string): number {
|
||||
const n = parseFloat(s.replace(',', '.'));
|
||||
return isNaN(n) || n < 0 ? 0 : Math.round(n * 100) / 100;
|
||||
}
|
||||
|
||||
function computeMemberTotal(member: OrderGroupMember, feeShare: number, discountType: string, discountValue: number, memberCount: number): number {
|
||||
const base = member.amount ?? 0;
|
||||
const surcharge = member.surchargeAmount ?? 0;
|
||||
const discount = discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountValue / 100 * 100) / 100
|
||||
: Math.round(discountValue / memberCount * 100) / 100;
|
||||
return Math.round((base + surcharge + feeShare - discount) * 100) / 100;
|
||||
}
|
||||
|
||||
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) : '');
|
||||
setShipping(group.shipping ? String(group.shipping) : '');
|
||||
setTip(group.tip ? String(group.tip) : '');
|
||||
setDiscountType((group.discountType as 'percent' | 'fixed') ?? 'percent');
|
||||
setDiscountValue(group.discountValue ? String(group.discountValue) : '');
|
||||
setError(null);
|
||||
}, [isOpen, group]);
|
||||
|
||||
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
|
||||
const memberCount = memberEntries.length;
|
||||
|
||||
const feesNum = parseNum(fees);
|
||||
const shippingNum = parseNum(shipping);
|
||||
const tipNum = parseNum(tip);
|
||||
const discountNum = parseNum(discountValue);
|
||||
const totalFees = feesNum + shippingNum + tipNum;
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount * 100) / 100 : 0;
|
||||
|
||||
const handleSave = async () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
const body: Record<string, any> = { id: group.id };
|
||||
body.fees = feesNum;
|
||||
body.shipping = shippingNum;
|
||||
body.tip = tipNum;
|
||||
if (discountNum > 0) {
|
||||
body.discountType = discountType;
|
||||
body.discountValue = discountNum;
|
||||
} else {
|
||||
body.discountType = '';
|
||||
body.discountValue = 0;
|
||||
}
|
||||
const res = await updateGroupFees({ body });
|
||||
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 (Kč)</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 (Kč)</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é (Kč)</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 (Kč)</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 ({memberCount} členů, {feeShare > 0 ? `poplatek ${feeShare} 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 discount = discountNum > 0
|
||||
? (discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountNum / 100 * 100) / 100
|
||||
: Math.round(discountNum / memberCount * 100) / 100)
|
||||
: 0;
|
||||
const total = computeMemberTotal(member, feeShare, discountType, discountNum, memberCount);
|
||||
return (
|
||||
<tr key={login}>
|
||||
<td><strong>{login}</strong></td>
|
||||
<td className="text-end">{base > 0 ? `${base} Kč` : '—'}</td>
|
||||
<td className="text-end">{surcharge > 0 ? `${surcharge} Kč` : '—'}</td>
|
||||
<td className="text-end">{feeShare > 0 ? `${feeShare} Kč` : '—'}</td>
|
||||
<td className="text-end text-danger">{discount > 0 ? `-${discount} Kč` : '—'}</td>
|
||||
<td className="text-end fw-bold">{total > 0 ? `${total} 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user