Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.89% covered (success)
97.89%
278 / 284
90.91% covered (success)
90.91%
10 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateTableBlockCompiler
97.88% covered (success)
97.88%
277 / 283
90.91% covered (success)
90.91%
10 / 11
26
0.00% covered (danger)
0.00%
0 / 1
 compileTableStandard
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
1 / 1
7
 compileTableSummary
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
1 / 1
4
 compileTableSimple
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 compileRecordsTable
84.62% covered (warning)
84.62%
33 / 39
0.00% covered (danger)
0.00%
0 / 1
6.13
 compileLineItemsTable
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 compileFinancialSummary
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 compileTaxSummaryBox
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 compileListHeader
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 compileListSummary
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 compileListKpiBar
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
1
 compileKpiMetric
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Template\Application\Service\BlockCompiler;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Template\Domain\Model\TemplateBlock;
12
13/**
14 * Table, record listing, and financial summary block compiler.
15 *
16 * Compiles tabular data layouts, line items tables, tax breakdowns,
17 * and KPI summary bars into HTML/Twig markup.
18 *
19 * @package App\Core\Template\Application\Service\BlockCompiler
20 */
21final class TemplateTableBlockCompiler
22{
23    /**
24     * Compiles standard data table primitive with headers.
25     *
26     * @param TemplateBlock $block Block instance.
27     * @return string Compiled HTML.
28     */
29    public function compileTableStandard(TemplateBlock $block): string
30    {
31        $props = $block->props;
32        $collection = htmlspecialchars((string)($props['collection'] ?? 'records'), ENT_QUOTES, 'UTF-8');
33        $title = htmlspecialchars((string)($props['title'] ?? ''), ENT_QUOTES, 'UTF-8');
34
35        /** @var array<int, array<string, string>> $cols */
36        $cols = !empty($props['columns']) && is_array($props['columns'])
37            ? $props['columns']
38            : [
39                ['key' => 'id', 'label' => 'ID', 'width' => '50px'],
40                ['key' => 'name', 'label' => 'Nazwa / Opis', 'width' => 'auto'],
41                ['key' => 'created_at', 'label' => 'Data', 'width' => '100px'],
42            ];
43
44        $html = '<div class="template-table-standard" style="margin:10px 0;width:100%;">';
45        if ($title !== '') {
46            $html .= sprintf(
47                '<div style="font-size:10pt;font-weight:bold;margin-bottom:6px;color:#1e293b;">%s</div>',
48                $title
49            );
50        }
51
52        $html .= '<table style="width:100%;border-collapse:collapse;font-size:8.5pt;">'
53            . '<thead><tr style="background:#f1f5f9;">';
54        foreach ($cols as $c) {
55            $lbl = htmlspecialchars((string)($c['label'] ?? $c['key']), ENT_QUOTES, 'UTF-8');
56            $w = htmlspecialchars((string)($c['width'] ?? 'auto'), ENT_QUOTES, 'UTF-8');
57            $html .= sprintf(
58                '<th style="border:1px solid #cbd5e1;padding:6px;text-align:left;width:%s;">%s</th>',
59                $w,
60                $lbl
61            );
62        }
63        $html .= '</tr></thead><tbody>{% for row in ' . $collection . '|default([]) %}<tr>';
64        foreach ($cols as $c) {
65            $k = htmlspecialchars((string)($c['key'] ?? 'name'), ENT_QUOTES, 'UTF-8');
66            $html .= sprintf(
67                '<td style="border:1px solid #cbd5e1;padding:5px 6px;">{{ row.%s|default(\'\')|e }}</td>',
68                $k
69            );
70        }
71        $html .= '</tr>{% endfor %}</tbody>';
72
73        if (!empty($props['show_summary'])) {
74            $colspan = max(1, count($cols) - 1);
75            $html .= '<tfoot><tr style="background:#f8fafc;font-weight:bold;border-top:2px solid #cbd5e1;">'
76                . sprintf(
77                    '<td colspan="%d" style="border:1px solid #cbd5e1;padding:6px;text-align:right;">Razem:</td>',
78                    $colspan
79                )
80                . '<td style="border:1px solid #cbd5e1;padding:6px;text-align:right;">'
81                . '{{ record.amount_gross|default(record.total|default(\'0,00\'))|format_currency }}</td>'
82                . '</tr></tfoot>';
83        }
84
85        $html .= '</table></div>';
86
87        return $html;
88    }
89
90    /**
91     * Compiles table primitive with full financial summary footer (Net, VAT, Gross).
92     *
93     * @param TemplateBlock $block Block instance.
94     * @return string Compiled HTML.
95     */
96    public function compileTableSummary(TemplateBlock $block): string
97    {
98        $props = $block->props;
99        $title = htmlspecialchars((string)($props['title'] ?? 'Zestawienie z Podsumowaniem'), ENT_QUOTES, 'UTF-8');
100        $currency = htmlspecialchars((string)($props['currency'] ?? 'PLN'), ENT_QUOTES, 'UTF-8');
101        $showTax = !isset($props['show_tax_breakdown']) || (bool)$props['show_tax_breakdown'];
102        $collection = htmlspecialchars((string)($props['collection'] ?? 'record.items'), ENT_QUOTES, 'UTF-8');
103
104        $html = '<div class="template-table-summary" style="margin:12px 0;width:100%;">';
105        if ($title !== '') {
106            $html .= sprintf(
107                '<div style="font-size:10.5pt;font-weight:bold;margin-bottom:6px;color:#1e293b;">%s</div>',
108                $title
109            );
110        }
111
112        $currExport = var_export($currency, true);
113        $html .= '<table style="width:100%;border-collapse:collapse;font-size:8.5pt;border:1px solid #cbd5e1;">'
114            . '<thead><tr style="background:#f1f5f9;border-bottom:2px solid #cbd5e1;font-weight:600;color:#334155;">'
115            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:center;width:35px;">#</th>'
116            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:left;">Item / Description</th>'
117            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:center;width:55px;">Qty</th>'
118            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:right;width:80px;">Net Price</th>'
119            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:right;width:85px;">Net Value</th>'
120            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:center;width:55px;">Tax %</th>'
121            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:right;width:75px;">Tax Amount</th>'
122            . '<th style="border:1px solid #cbd5e1;padding:6px;text-align:right;width:95px;">Gross Value</th>'
123            . '</tr></thead><tbody>'
124            . '{% for row in ' . $collection . '|default([]) %}'
125            . '<tr style="border-bottom:1px solid #e2e8f0;background:'
126            . '{% if loop.index is odd %}#ffffff{% else %}#f8fafc{% endif %};">'
127            . '<td style="border:1px solid #cbd5e1;padding:5px;text-align:center;color:#64748b;">{{ loop.index }}</td>'
128            . '<td style="border:1px solid #cbd5e1;padding:5px;">'
129            . '{{ row.name|default(row.description|default("Item"))|e }}</td>'
130            . '<td style="border:1px solid #cbd5e1;padding:5px;text-align:center;">'
131            . '{{ row.quantity|default(1) }} {{ row.unit|default("pcs") }}</td>'
132            . '<td style="border:1px solid #cbd5e1;padding:5px;text-align:right;">'
133            . '{{ row.price_net|default(row.price|default(0))|format_currency(' . $currExport . ') }}</td>'
134            . '<td style="border:1px solid #cbd5e1;padding:5px;text-align:right;">'
135            . '{{ row.amount_net|default(row.total_net|default(0))|format_currency(' . $currExport . ') }}</td>'
136            . '<td style="border:1px solid #cbd5e1;padding:5px;text-align:center;">'
137            . '{{ row.vat_rate|default("23%") }}</td>'
138            . '<td style="border:1px solid #cbd5e1;padding:5px;text-align:right;">'
139            . '{{ row.amount_vat|default(0)|format_currency(' . $currExport . ') }}</td>'
140            . '<td style="border:1px solid #cbd5e1;padding:5px;text-align:right;font-weight:600;">'
141            . '{{ row.amount_gross|default(row.total_gross|default(0))|format_currency(' . $currExport . ') }}</td>'
142            . '</tr>{% else %}'
143            . '<tr><td colspan="8" style="padding:12px;text-align:center;color:#94a3b8;font-style:italic;">'
144            . 'No items to display</td></tr>'
145            . '{% endfor %}</tbody>'
146            . '<tfoot>'
147            . '<tr style="background:#f8fafc;font-weight:bold;border-top:2px solid #cbd5e1;">'
148            . '<td colspan="4" style="border:1px solid #cbd5e1;padding:6px 8px;text-align:right;">Total:</td>'
149            . '<td style="border:1px solid #cbd5e1;padding:6px 8px;text-align:right;">'
150            . '{{ record.amount_net|default(sum_net|default("0,00"))|format_currency(' . $currExport . ') }}</td>'
151            . '<td style="border:1px solid #cbd5e1;padding:6px 8px;text-align:center;">-</td>'
152            . '<td style="border:1px solid #cbd5e1;padding:6px 8px;text-align:right;">'
153            . '{{ record.amount_vat|default(sum_vat|default("0,00"))|format_currency(' . $currExport . ') }}</td>'
154            . '<td style="border:1px solid #cbd5e1;padding:6px 8px;text-align:right;color:#0f172a;">'
155            . '{{ record.amount_gross|default(sum_gross|default("0,00"))|format_currency(' . $currExport . ') }}</td>'
156            . '</tr>';
157
158        if ($showTax) {
159            $html .= '<tr style="background:#ffffff;font-size:8pt;color:#475569;">'
160                . '<td colspan="4" style="border:1px solid #cbd5e1;padding:4px 8px;text-align:right;">'
161                . 'Including tax rate {{ record.vat_rate|default("23%") }}:</td>'
162                . '<td style="border:1px solid #cbd5e1;padding:4px 8px;text-align:right;">'
163                . '{{ record.amount_net|default("0,00")|format_currency(' . $currExport . ') }}</td>'
164                . '<td style="border:1px solid #cbd5e1;padding:4px 8px;text-align:center;">'
165                . '{{ record.vat_rate|default("23%") }}</td>'
166                . '<td style="border:1px solid #cbd5e1;padding:4px 8px;text-align:right;">'
167                . '{{ record.amount_vat|default("0,00")|format_currency(' . $currExport . ') }}</td>'
168                . '<td style="border:1px solid #cbd5e1;padding:4px 8px;text-align:right;">'
169                . '{{ record.amount_gross|default("0,00")|format_currency(' . $currExport . ') }}</td>'
170                . '</tr>';
171        }
172
173        $html .= '<tr style="background:#f1f5f9;font-size:9.5pt;font-weight:bold;border-top:2px solid #0f172a;">'
174            . '<td colspan="7" style="border:1px solid #cbd5e1;padding:8px 10px;text-align:right;'
175            . 'text-transform:uppercase;color:#0f172a;">Total due:</td>'
176            . '<td style="border:1px solid #cbd5e1;padding:8px 10px;text-align:right;color:#2563eb;font-size:10.5pt;">'
177            . '{{ record.amount_gross|default("0,00")|format_currency(' . $currExport . ') }}'
178            . '</td></tr></tfoot></table></div>';
179
180        return $html;
181    }
182
183    /**
184     * Compiles simple matrix table without heavy headers.
185     *
186     * @param TemplateBlock $block Block instance.
187     * @return string Compiled HTML.
188     */
189    public function compileTableSimple(TemplateBlock $block): string
190    {
191        $props = $block->props;
192        $content = (string)($props['content'] ?? '');
193        if ($content !== '') {
194            return '<div class="template-table-simple" style="margin:8px 0;">' . $content . '</div>';
195        }
196
197        return '<table class="template-table-simple" style="width:100%;border-collapse:collapse;font-size:9pt;">'
198            . '<tr><td style="border:1px solid #e2e8f0;padding:6px;background:#f8fafc;width:30%;">'
199            . '<strong>Document</strong></td>'
200            . '<td style="border:1px solid #e2e8f0;padding:6px;">{{ record.prefix_number|default(\'FV/2026\') }}</td>'
201            . '</tr><tr><td style="border:1px solid #e2e8f0;padding:6px;background:#f8fafc;">'
202            . '<strong>Status</strong></td>'
203            . '<td style="border:1px solid #e2e8f0;padding:6px;">{{ record.status|default(\'Approved\') }}</td>'
204            . '</tr></table>';
205    }
206
207    /**
208     * Compiles Records Table block for list scope templates ({% for row in records %}).
209     *
210     * @param TemplateBlock $block Block instance.
211     * @return string Compiled HTML.
212     */
213    public function compileRecordsTable(TemplateBlock $block): string
214    {
215        $columns = isset($block->props['columns']) && is_array($block->props['columns'])
216            ? $block->props['columns']
217            : [
218                ['key' => 'id', 'label' => 'ID', 'width' => '60px'],
219                ['key' => 'name', 'label' => 'Name / Title', 'width' => 'auto'],
220                ['key' => 'created_at', 'label' => 'Date', 'width' => '110px'],
221                ['key' => 'status', 'label' => 'Status', 'width' => '90px'],
222            ];
223
224        $html = '<table class="template-records-table" style="width:100%;border-collapse:collapse;'
225            . 'margin:12px 0;font-size:9pt;border:1px solid #cbd5e1;">' . "\n"
226            . '<thead><tr style="background:#f1f5f9;font-weight:600;color:#334155;'
227            . 'border-bottom:2px solid #cbd5e1;">' . "\n"
228            . '<th style="padding:6px 8px;border:1px solid #cbd5e1;width:35px;text-align:center;">#</th>' . "\n";
229
230        foreach ($columns as $col) {
231            $colLabel = htmlspecialchars((string)($col['label'] ?? $col['key'] ?? ''), ENT_QUOTES, 'UTF-8');
232            $widthStyle = isset($col['width'])
233                ? 'width:' . htmlspecialchars((string)$col['width'], ENT_QUOTES, 'UTF-8') . ';'
234                : '';
235            $html .= '<th style="padding:6px 8px;border:1px solid #cbd5e1;' . $widthStyle . '">'
236                . $colLabel . '</th>' . "\n";
237        }
238        $html .= '</tr></thead>' . "\n"
239            . '<tbody>' . "\n"
240            . '{% for row in records %}' . "\n"
241            . '<tr style="border-bottom:1px solid #e2e8f0;background:'
242            . '{% if loop.index is odd %}#ffffff{% else %}#f8fafc{% endif %};">' . "\n"
243            . '<td style="padding:6px 8px;border:1px solid #cbd5e1;text-align:center;color:#64748b;">'
244            . '{{ loop.index }}</td>' . "\n";
245
246        foreach ($columns as $col) {
247            $key = (string)($col['key'] ?? 'id');
248            $html .= '<td style="padding:6px 8px;border:1px solid #cbd5e1;">'
249                . '{{ attribute(row, ' . var_export($key, true) . ')|default("-") }}</td>' . "\n";
250        }
251
252        $colSpan = count($columns) + 1;
253        $html .= '</tr>' . "\n"
254            . '{% else %}' . "\n"
255            . '<tr><td colspan="' . $colSpan . '" style="padding:16px;text-align:center;color:#94a3b8;">'
256            . 'No records to display</td></tr>' . "\n"
257            . '{% endfor %}' . "\n"
258            . '</tbody></table>';
259
260        return $html;
261    }
262
263    /**
264     * Compiles Line Items Table block.
265     *
266     * @param TemplateBlock $block Block instance.
267     * @return string Compiled HTML.
268     */
269    public function compileLineItemsTable(TemplateBlock $block): string
270    {
271        $relation = (string)($block->props['relation'] ?? 'invoice_lines');
272        $title = (string)($block->props['title'] ?? 'Zestawienie pozycji');
273        $currency = (string)($block->props['currency'] ?? 'PLN');
274
275        return sprintf(
276            '{{ related_table(%s, ["name", "quantity", "price_net", "amount_net", "vat_rate", '
277            . '"amount_vat", "amount_gross"], {"title": %s, "show_total": true, '
278            . '"show_financial_summary": true, "currency": %s, "items": record.items|default([])}) }}',
279            var_export($relation, true),
280            var_export($title, true),
281            var_export($currency, true)
282        );
283    }
284
285    /**
286     * Compiles Financial Summary block.
287     *
288     * @param TemplateBlock $block Block instance.
289     * @return string Compiled HTML.
290     */
291    public function compileFinancialSummary(TemplateBlock $block): string
292    {
293        $currency = (string)($block->props['currency'] ?? 'PLN');
294        $width = (string)($block->props['width'] ?? '280px');
295
296        return '<div style="margin:16px 0;width:100%;text-align:right;">'
297            . '<table style="width:' . htmlspecialchars($width, ENT_QUOTES, 'UTF-8')
298            . ';margin-left:auto;border-collapse:collapse;">'
299            . '<tr><td style="padding:4px 8px;color:#64748b;">Razem netto:</td>'
300            . '<td style="padding:4px 8px;font-weight:600;">{{ record.amount_net|format_currency("'
301            . htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') . '") }}</td></tr>'
302            . '<tr><td style="padding:4px 8px;color:#64748b;">Podatek VAT:</td>'
303            . '<td style="padding:4px 8px;font-weight:600;">{{ record.amount_vat|format_currency("'
304            . htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') . '") }}</td></tr>'
305            . '<tr style="border-top:2px solid #0f172a;"><td style="padding:6px 8px;font-weight:bold;">Brutto:</td>'
306            . '<td style="padding:6px 8px;font-weight:bold;font-size:12pt;color:#0f172a;">'
307            . '{{ record.amount_gross|format_currency("'
308            . htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') . '") }}</td></tr>'
309            . '</table></div>';
310    }
311
312    /**
313     * Compiles VAT Tax Summary breakdown block.
314     *
315     * @param TemplateBlock $block Block instance.
316     * @return string Compiled HTML.
317     */
318    public function compileTaxSummaryBox(TemplateBlock $block): string
319    {
320        $rate = (string)($block->props['rate'] ?? '23%');
321
322        return '<div style="margin:16px 0;width:100%;">'
323            . '<table style="width:100%;border-collapse:collapse;font-size:9pt;border:1px solid #cbd5e1;">'
324            . '<thead><tr style="background:#f1f5f9;font-weight:600;color:#334155;border-bottom:1px solid #cbd5e1;">'
325            . '<th style="padding:6px;text-align:left;">Tax Rate</th>'
326            . '<th style="padding:6px;text-align:right;">Net Value</th>'
327            . '<th style="padding:6px;text-align:right;">Tax Amount</th>'
328            . '<th style="padding:6px;text-align:right;">Gross Value</th>'
329            . '</tr></thead><tbody>'
330            . '<tr><td style="padding:6px;">' . htmlspecialchars($rate, ENT_QUOTES, 'UTF-8') . '</td>'
331            . '<td style="padding:6px;text-align:right;">{{ record.amount_net|format_currency }}</td>'
332            . '<td style="padding:6px;text-align:right;">{{ record.amount_vat|format_currency }}</td>'
333            . '<td style="padding:6px;text-align:right;">{{ record.amount_gross|format_currency }}</td></tr>'
334            . '</tbody></table></div>';
335    }
336
337    /**
338     * Compiles List Header block for report/list scope templates.
339     *
340     * @param TemplateBlock $block Block instance.
341     * @return string Compiled HTML.
342     */
343    public function compileListHeader(TemplateBlock $block): string
344    {
345        $title = (string)($block->props['title'] ?? 'RECORDS SUMMARY');
346        $subtitle = (string)($block->props['subtitle'] ?? 'Summary report generated by Ammonly system');
347
348        return '<div class="template-list-header" style="border-bottom:2px solid #0f172a;'
349            . 'padding-bottom:12px;margin-bottom:16px;">'
350            . '<div style="display:flex;justify-content:space-between;align-items:flex-end;">'
351            . '<div>'
352            . '<h1 style="margin:0;font-size:18pt;color:#0f172a;">'
353            . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</h1>'
354            . '<p style="margin:4px 0 0 0;font-size:9pt;color:#64748b;">'
355            . htmlspecialchars($subtitle, ENT_QUOTES, 'UTF-8') . '</p>'
356            . '</div>'
357            . '<div style="text-align:right;font-size:8.5pt;color:#64748b;">'
358            . '<div>Generated at: <strong>{{ context.now|default("now"|date("Y-m-d H:i")) }}</strong></div>'
359            . '<div>Total records: <strong>{{ records|length|default(0) }}</strong></div>'
360            . '</div></div></div>';
361    }
362
363    /**
364     * Compiles List Summary block.
365     *
366     * @param TemplateBlock $block Block instance.
367     * @return string Compiled HTML.
368     */
369    public function compileListSummary(TemplateBlock $block): string
370    {
371        $title = (string)($block->props['title'] ?? 'List Summary');
372
373        return '<div class="template-list-summary" style="margin:14px 0;padding:12px;background:#f8fafc;'
374            . 'border:1px solid #cbd5e1;border-radius:4px;">'
375            . '<div style="font-size:10pt;font-weight:bold;color:#1e293b;margin-bottom:8px;">'
376            . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</div>'
377            . '<div style="display:flex;justify-content:space-between;font-size:9pt;margin-bottom:4px;">'
378            . '<span style="color:#64748b;">Total records:</span>'
379            . '<strong style="color:#0f172a;">{{ records|length|default(0) }}</strong>'
380            . '</div>'
381            . '<div style="display:flex;justify-content:space-between;font-size:9pt;">'
382            . '<span style="color:#64748b;">Processing status:</span>'
383            . '<strong style="color:#10b981;">Complete</strong>'
384            . '</div></div>';
385    }
386
387    /**
388     * Compiles List KPI Bar block.
389     *
390     * @param TemplateBlock $block Block instance.
391     * @return string Compiled HTML.
392     */
393    public function compileListKpiBar(TemplateBlock $block): string
394    {
395        $allLabel = htmlspecialchars(
396            (string)($block->props['all_label'] ?? 'Wszystkie Pozycje'),
397            ENT_QUOTES,
398            'UTF-8'
399        );
400        $activeLabel = htmlspecialchars(
401            (string)($block->props['active_label'] ?? 'Aktywne / Zrealizowane'),
402            ENT_QUOTES,
403            'UTF-8'
404        );
405        $formatLabel = htmlspecialchars(
406            (string)($block->props['format_label'] ?? 'Format Zestawienia'),
407            ENT_QUOTES,
408            'UTF-8'
409        );
410
411        return '<div class="template-list-kpi-bar" style="display:flex;gap:12px;margin:12px 0;width:100%;">'
412            . '<div style="flex:1;padding:10px;background:#f1f5f9;border-radius:6px;border-left:4px solid #2563eb;">'
413            . '<div style="font-size:8pt;color:#64748b;text-transform:uppercase;">' . $allLabel . '</div>'
414            . '<div style="font-size:14pt;font-weight:bold;color:#0f172a;">{{ records|length|default(0) }}</div>'
415            . '</div>'
416            . '<div style="flex:1;padding:10px;background:#f1f5f9;border-radius:6px;border-left:4px solid #10b981;">'
417            . '<div style="font-size:8pt;color:#64748b;text-transform:uppercase;">' . $activeLabel . '</div>'
418            . '<div style="font-size:14pt;font-weight:bold;color:#10b981;">100%</div>'
419            . '</div>'
420            . '<div style="flex:1;padding:10px;background:#f1f5f9;border-radius:6px;border-left:4px solid #f59e0b;">'
421            . '<div style="font-size:8pt;color:#64748b;text-transform:uppercase;">' . $formatLabel . '</div>'
422            . '<div style="font-size:14pt;font-weight:bold;color:#0f172a;">A4 ZBIORCZY</div>'
423            . '</div></div>';
424    }
425
426    /**
427     * Compiles KPI Metric card primitive.
428     *
429     * @param TemplateBlock $block Block instance.
430     * @param callable(string, string): string $exprResolver Value expression resolver callback.
431     * @return string Compiled HTML.
432     */
433    public function compileKpiMetric(TemplateBlock $block, callable $exprResolver): string
434    {
435        $props = $block->props;
436        $title = htmlspecialchars((string)($props['title'] ?? 'Metric'), ENT_QUOTES, 'UTF-8');
437        $fieldKey = htmlspecialchars((string)($props['field_key'] ?? 'total_gross'), ENT_QUOTES, 'UTF-8');
438        $format = (string)($props['format'] ?? 'currency');
439        $valExpr = $exprResolver($fieldKey, $format);
440
441        return sprintf(
442            '<div class="template-kpi-metric" style="padding:10px 14px;background:#f8fafc;border:1px solid #cbd5e1;'
443            . 'border-radius:6px;border-left:4px solid #2563eb;margin:6px 0;">'
444            . '<div style="font-size:8pt;text-transform:uppercase;color:#64748b;font-weight:600;">%s</div>'
445            . '<div style="font-size:16pt;font-weight:bold;color:#0f172a;">%s</div></div>',
446            $title,
447            $valExpr
448        );
449    }
450}