gaas-relatorio-xlsx
Build, extend, or improve the in-app XLSX report exporters behind the GaaS "Relatório" tab (calendar-estrategico React/TS app). Covers adding a new export button + workbook, adding a new sheet/section
SKILL.md.claude/skills/gaas-relatorio-xlsx/SKILL.md
GitHubGaaS Relatório XLSX Exporter
Guide for building and extending the browser-side XLSX report exporters in calendar-estrategico. These run in the React app (not Python): a button in the Relatório tab fetches from Supabase, builds an ExcelJS workbook in-memory, and triggers a browser download. There is one canonical, validated implementation you mirror for everything new: the CRM monthly MoM report. Copy its shape rather than inventing a new one — consistency is the whole point.
Read references/architecture.md for the file map and the shared-helper API before writing code. Read references/patterns.md for copy-ready code (fetch, analysis, writers, reconciliation, download, button wiring). Read references/validation-harness.md to test a new/changed export headlessly (real Supabase data, no browser) before committing.
The reference implementations
src/utils/crmAquisicaoMonthlyReportExport.ts— the canonical MoM report and the home of every shared helper (REPORT_COLORS,reportDelta,reportPctText,styleReportDelta,setCellValue,writeReportHeader,writeSectionTitle,weekWindows,previousWeekWindows,monthStart/monthEnd/previousMonthStart,MONTHS_PT,dayOfMonthReport, typeWeekWindow). If you need a helper in a new file, import it from here. If it isn't exported yet, add theexportkeyword — never copy-paste a helper.src/utils/midiaPagaMonthlyReportExport.ts— the most complete worked example: cards + MoM, per-frente table, weekly pacing (spend & clicks), per-platform, per-creative, plus a validation gate and multi-sheet workbook. Best template for a brand-new report.src/utils/midiaPagaDiarizadoSheet.ts— awrite<Name>Sheet(workbook, ...)that appends one sheet to an existing workbook (daily pacing cockpit). Model for "add another sheet".src/utils/aquisicaoCrmExcelExport.ts— the diarized CRM export; ownsfetchSupabaseRows(paginatedactivitiesfetch) and thedownloadBufferbrowser helper pattern.src/components/RelatorioView.tsx— where the buttons anduseCallbackhandlers live (searchexportAquisicaoCrmMonthlyfor the pattern to clone).
When the task is: add a whole new report (new button + file)
- Create
src/utils/<name>ReportExport.ts. Import shared helpers fromcrmAquisicaoMonthlyReportExport. Structure the file top-to-bottom: types/consts → classification (if any) → derived-metric helpers → fetch →build<Name>Analysis()(aggregation + reconciliation throw) →write*section writers →write<Name>Sheet(workbook, analysis)→export async function export<Name>Xlsx(start, end)wrapper. - The wrapper mirrors
exportAquisicaoCrmMonthlyXlsx(see references/patterns.md): resolvemonthStart/previousMonthStart, fetch month M and M-1,await import('exceljs'), build the workbook,writeBuffer(),downloadBuffer(buffer,Report_${MONTHS_PT[m]}_<Tipo>.xlsx). - Wire the button in
RelatorioView.tsx: add anisExporting<Name>state, aexport<Name>useCallback(try/catch withwindow.alerton failure, deps[periodEnd, periodStart]), and a<button>next to the CRM monthly one using the same classes (pick a distinct accent color). - Validate headlessly (see below), then build.
When the task is: add a sheet to an existing workbook
Write a write<Sheet>Sheet(workbook, data, ...) in its own file that calls workbook.addWorksheet(...), and call it from the wrapper after the other sheets. Preserve sheet order the user expects. Note ExcelJS in the browser cannot render charts — a ported chart-heavy sheet becomes values/tables only; say so.
When the task is: add MoM / comparison to metrics
Fetch M-1 alongside M, aggregate both, and for each metric emit the value then a Δ cell: reportPctText(reportDelta(cur, prev)) for the text, styleReportDelta(cell, delta, invert) for the color. invert=false for volumes (up = green/good); invert=true for costs/efficiency like CPC/CPM/CPL/CAC (down = green/good). reportDelta returns null when the previous value is 0/undefined, which renders as a gray "—" — never a divide-by-zero or a throw.
Non-negotiables (these hold across every report)
- Reconciliation gate. Before returning the workbook, assert that section totals reconcile with the month total (e.g. Σ by-frente spend == Σ by-platform == Σ weekly == month total). Use a cents tolerance (
Math.abs(a-b) <= 0.01) for summed floats, exact===for integer counts. On mismatchthrow new Error('Validação falhou: ...')so a wrong file is never generated. - Classify by the campaign name tag, never by a dirty enum field (e.g.
objective). The Mídia Paga frente rules and their exact precedence live in references/patterns.md. Copy the precedence order verbatim. - Reuse the visual system. Freeze
C3(xSplit:2, ySplit:2), Arial,showGridLines:false; header rowREPORT_COLORS.slateon white; cardsREPORT_COLORS.card; warningsREPORT_COLORS.note/noteText; TOTAL rowREPORT_COLORS.total. Number formats: integers#,##0, currency"R$" #,##0.00, rates0.00%(store CTR as the raw fraction, not ×100). Everything flows throughsetCellValue. - Keep the caveats. GaaS data has known traps (platform conversions are events not cards; tracking breakages). If the source report carries a yellow warning box, the new one keeps it — don't silently drop it.
Validate before you commit
Always exercise a new or changed export against real Supabase data before declaring it done. The app path (button → download) can't run in a headless shell, but every builder is a pure function of its rows, and supabaseClient.ts has hardcoded credentials (no import.meta.env), so you can bundle a tiny harness with esbuild and run it in Node: fetch real rows, call build*Analysis + the write*Sheet functions, write the .xlsx, reopen it, and assert the headline totals reconcile. Full template in references/validation-harness.md. Delete the temp harness before committing. Then run npm run build and confirm tsc is clean on the touched files.
Committing / deploying (repo gotcha)
The real project repo is the nested calendar-estrategico/ repo (remote growthafinz/GaaS-Framework-Afinz), not the git repo rooted at the user's home folder — running git from the project dir resolves to the home repo, which does not track these files. Always run git with -C pointed at calendar-estrategico, and there are usually unrelated uncommitted changes from other work in that repo, so stage only your specific files by path (never git add -A). Deploy is a push to main (that triggers the GaaS deploy) — do it only when the user asks.