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

@@ -5,6 +5,8 @@ import { requirePermission } from "../../middleware/auth";
import { localDateCzStr } from "../../utils/date";
import { nasFinancialsManager } from "../../services/nas-financials-manager";
import { htmlToPdf } from "../../utils/html-to-pdf";
import { getRate } from "../../services/exchange-rates";
import { localDateStr } from "../../utils/date";
/* ── Helpers ─────────────────────────────────────────────────────── */
@@ -358,9 +360,12 @@ export default async function invoicesPdfRoutes(
// QR generation failed — leave empty
}
// VAT recapitulation (always in CZK)
// VAT recapitulation (always in CZK — Czech tax requirement)
const isForeign = currency.toUpperCase() !== "CZK";
const cnbRate = 1.0; // Skip CNB rate conversion
const issueDateStr = invoice.issue_date
? localDateStr(new Date(invoice.issue_date))
: undefined;
const cnbRate = isForeign ? await getRate(currency, issueDateStr) : 1.0;
const vatRates = [21, 12, 0];
const vatRecap: Array<{
rate: number;
@@ -1007,6 +1012,17 @@ ${indentCSS}
<tbody>
${vatRecapHtml}
</tbody>
${
isForeign
? `<tfoot>
<tr>
<td colspan="4" style="font-size:0.7em; color:#666; padding-top:6px; text-align:left;">
Přepočet kurzem ČNB ke dni ${formatDate(invoice.issue_date)}: 1 ${escapeHtml(currency)} = ${cnbRate.toFixed(3).replace(".", ",")} CZK
</td>
</tr>
</tfoot>`
: ""
}
</table>
</div>