feat: CNB exchange rates, multi-currency KPI stats, invoice PDF VAT in CZK

- ČNB exchange rate service with date-specific rates and caching
- Invoice/received invoice stats convert foreign currencies to CZK
- Dashboard revenue converts all currencies to CZK
- Invoice PDF: VAT recap table always in CZK with CNB rate footer
- Inline styles replaced with utility classes (step 4 cleanup)
- Spinner animation exempt from prefers-reduced-motion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-27 13:44:53 +01:00
parent a3ef37d0d2
commit 8cdf057ab3
13 changed files with 173 additions and 117 deletions

View File

@@ -270,7 +270,7 @@ function SortableInvoiceRow({
</select>
</td>
) : null}
<td style={{ textAlign: "right", fontWeight: 600, whiteSpace: "nowrap" }}>
<td className="text-right fw-600 whitespace-nowrap">
{formatCurrency(lineTotal, currency)}
</td>
<td>
@@ -354,12 +354,7 @@ function SortableInvoiceEditRow({
</svg>
</button>
</td>
<td
className="text-tertiary"
style={{ textAlign: "center", fontWeight: 500 }}
>
{index + 1}
</td>
<td className="text-tertiary text-center fw-500">{index + 1}</td>
<td>
<input
type="text"
@@ -1371,10 +1366,7 @@ export default function InvoiceDetail() {
))}
</select>
{form.due_date && (
<span
className="text-tertiary"
style={{ fontSize: "0.75rem", marginTop: "0.25rem" }}
>
<span className="text-tertiary text-xs mt-1">
Splatnost:{" "}
{new Date(form.due_date).toLocaleDateString("cs-CZ")}
</span>
@@ -1448,10 +1440,7 @@ export default function InvoiceDetail() {
</FormField>
<FormField label="DPH">
<div className="flex-row-gap">
<label
className="admin-form-checkbox"
style={{ whiteSpace: "nowrap" }}
>
<label className="admin-form-checkbox whitespace-nowrap">
<input
type="checkbox"
checked={!!form.apply_vat}
@@ -1809,7 +1798,7 @@ export default function InvoiceDetail() {
{invoice.paid_date && (
<div className="admin-form-row mt-2">
<FormField label="Datum úhrady">
<div style={{ color: "var(--success)", fontWeight: 500 }}>
<div className="fw-500" style={{ color: "var(--success)" }}>
{formatDate(invoice.paid_date)}
</div>
</FormField>
@@ -1954,16 +1943,13 @@ export default function InvoiceDetail() {
: 0;
return (
<tr key={item.id || index}>
<td
className="text-tertiary"
style={{ textAlign: "center", fontWeight: 500 }}
>
<td className="text-tertiary text-center fw-500">
{index + 1}
</td>
<td className="fw-500">
{item.description || "\u2014"}
</td>
<td style={{ textAlign: "center" }}>
<td className="text-center">
{item.quantity}{" "}
{item.unit && (
<span className="text-tertiary">
@@ -1971,7 +1957,7 @@ export default function InvoiceDetail() {
</span>
)}
</td>
<td style={{ textAlign: "center" }}>
<td className="text-center">
{item.unit || "\u2014"}
</td>
<td className="admin-mono text-right">
@@ -1980,16 +1966,13 @@ export default function InvoiceDetail() {
invoice.currency,
)}
</td>
<td style={{ textAlign: "center" }}>
<td className="text-center">
{Number(invoice.apply_vat)
? Number(item.vat_rate)
: 0}
%
</td>
<td
className="admin-mono"
style={{ textAlign: "right", fontWeight: 600 }}
>
<td className="admin-mono text-right fw-600">
{formatCurrency(
lineSubtotal + lineVat,
invoice.currency,