Skip to content

[IP-145]: tabular reports — five company-panel report pages#609

Open
nielsdrost7 wants to merge 8 commits into
InvoicePlane:developfrom
underdogg-forks:feature/145-tabular-reports
Open

[IP-145]: tabular reports — five company-panel report pages#609
nielsdrost7 wants to merge 8 commits into
InvoicePlane:developfrom
underdogg-forks:feature/145-tabular-reports

Conversation

@nielsdrost7

@nielsdrost7 nielsdrost7 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Built on the test-infra PR #607 — merge that first. Until it merges, this diff also shows #607's single commit.

Implements Track B of epic #506 — the tabular reports module per naui95's spec in #145. Independent of the Report Builder (no shared code path).

What's in here

Five read-only Filament pages in the company panel under a Reports navigation group, each with a date-range filter (defaults to the current month), an optional client filter, a totals summary line under the table, and a CSV export of the filtered rows:

Report Query
Payment History payments in range, with invoice + client
Invoicing History invoices in range with status badge; summary splits paid vs outstanding
Invoiced Amount by Client per-client count + sum via withCount/withSum
Sales by Date per-day revenue of paid invoices
Invoices per Client per-client count + average invoice value

All queries are tenant-scoped by the BelongsToCompany global scope. Shared behavior lives in BaseTabularReportPage (filters, CSV streaming, access control: client_admin + elevated roles).

Also ships the AbstractCompanyPanelTestCase::testLivewire() fix (broken Livewire import + withSession chained on the wrong object) — identical fix is on the report-builder branch (underdogg-forks#2); whichever merges second resolves cleanly.

Testing

Full suite green: 314 tests, 1,033 assertions, 0 failures (develop baseline: 307). The three scenarios drafted in the issue body (in-range vs out-of-range payments, per-client sum shows 1500, cross-company invisibility) plus client filtering, paid-only sales-by-date, page smoke for all five, and the CSV download assertion.

Follow-up

The issue's "later enhancement" (print/PDF export of these reports through builder templates) stays out of scope, as specified in #145.

Summary by CodeRabbit

  • New Features

    • Added company reports for invoicing history, payment history, sales by date, invoices per client, and invoiced totals by client.
    • Added date-range and client filters with summary totals.
    • Added CSV export for filtered report results.
    • Added a dedicated Reports navigation section.
    • Added localized labels and report summaries.
  • Bug Fixes

    • Reports are restricted to the selected company and authorized users.

@nielsdrost7 nielsdrost7 changed the title feat: tabular reports — five company-panel report pages (IP-145) [IP-145]: tabular reports — five company-panel report pages Jul 17, 2026
Ran vendor/bin/pint on develop: import ordering, binary-operator
spacing, and empty-body brace style. Purely cosmetic, no behavior
change.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@nielsdrost7, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f3997181-2211-47a4-8886-93c3ffee8351

📥 Commits

Reviewing files that changed from the base of the PR and between 9cfeb72 and 4eac422.

📒 Files selected for processing (34)
  • .github/DOCKER.md
  • .gitignore
  • AGENTS.md
  • CLAUDE.md
  • Makefile
  • Modules/Core/Database/Factories/CompanyUserFactory.php
  • Modules/Core/Database/Factories/CustomFieldValueFactory.php
  • Modules/Core/Database/Factories/NoteFactory.php
  • Modules/Core/Filament/Company/Pages/Reports/BaseTabularReportPage.php
  • Modules/Core/Filament/Company/Pages/Reports/InvoicedByClientReport.php
  • Modules/Core/Filament/Company/Pages/Reports/InvoicesPerClientReport.php
  • Modules/Core/Filament/Company/Pages/Reports/InvoicingHistoryReport.php
  • Modules/Core/Filament/Company/Pages/Reports/PaymentHistoryReport.php
  • Modules/Core/Filament/Company/Pages/Reports/SalesByDateReport.php
  • Modules/Core/Providers/CompanyPanelProvider.php
  • Modules/Core/Tests/Feature/CompanyTabularReportsTest.php
  • Modules/Core/Tests/Feature/Seeders/RoleHasPermissionsSeederTest.php
  • Modules/Core/resources/views/filament/company/pages/reports/tabular-report.blade.php
  • Modules/Expenses/Filament/Company/Widgets/RecentExpensesWidget.php
  • Modules/Expenses/Observers/ExpenseObserver.php
  • Modules/Invoices/Tests/Feature/InvoiceDuplicateNumberPreventionTest.php
  • Modules/Invoices/Tests/Feature/RecurringInvoicesTest.php
  • Modules/Payments/Filament/Company/Widgets/RecentPaymentsWidget.php
  • Modules/Payments/Observers/PaymentObserver.php
  • Modules/Projects/Filament/Company/Widgets/RecentProjectsWidget.php
  • Modules/Projects/Filament/Company/Widgets/RecentTasksWidget.php
  • Modules/Quotes/Tests/Feature/QuoteDuplicateNumberPreventionTest.php
  • Modules/Quotes/Tests/Unit/QuoteModelTest.php
  • README.md
  • composer.json
  • docker-compose.yml
  • docker-resources/mariadb/init/01-create-test-db.sql
  • docker-resources/php-cli/Dockerfile
  • resources/lang/en/ip.php
📝 Walkthrough

Walkthrough

Introduces five company-scoped Filament reports with shared date/client filters, tabular views, summaries, CSV export, navigation registration, translations, and feature tests covering filtering, aggregation, scoping, rendering, and export.

Changes

Company tabular reports

Layer / File(s) Summary
Shared report framework and presentation
Modules/Core/Filament/Company/Pages/Reports/BaseTabularReportPage.php, Modules/Core/resources/views/filament/company/pages/reports/tabular-report.blade.php, resources/lang/en/ip.php
Adds shared filter state, access control, table configuration, CSV streaming, client options, date defaults, formatting helpers, report controls, and translations.
Report queries and tabular outputs
Modules/Core/Filament/Company/Pages/Reports/*Report.php
Adds payment, invoice history, client aggregation, invoice-per-client, and sales-by-date reports with filtered queries, summaries, table columns, and CSV mappings.
Panel registration and navigation
Modules/Core/Providers/CompanyPanelProvider.php
Registers the five report pages and groups their navigation entries under the reports section.
Feature coverage for report behavior
Modules/Core/Tests/Feature/CompanyTabularReportsTest.php
Tests rendering, date and client filters, company scoping, aggregation, paid-invoice sales, and CSV export.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CompanyUser
  participant FilamentReportPage
  participant EloquentQuery
  participant CsvDownload
  CompanyUser->>FilamentReportPage: Select date and client filters
  FilamentReportPage->>EloquentQuery: Execute filtered report query
  EloquentQuery-->>FilamentReportPage: Return report rows
  CompanyUser->>FilamentReportPage: Trigger CSV export
  FilamentReportPage->>CsvDownload: Stream headers and rows
  CsvDownload-->>CompanyUser: Download CSV
Loading

Possibly related issues

  • InvoicePlane-v2 issue 145 — This PR implements the five report pages, filters, CSV export, navigation, translations, and tests described by the issue.
  • InvoicePlane-v2 issue 680 — This PR establishes the tabular report pages and existing CSV export that the issue extends with PDF export.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: five new company-panel tabular report pages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
Modules/Core/Filament/Company/Pages/Reports/BaseTabularReportPage.php (1)

111-118: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider using pluck instead of map for performance.

When generating key-value options from the database, using pluck directly on the query builder is more efficient than fetching full models and mapping them in memory.

♻️ Proposed fix
     public function getClientOptions(): array
     {
         return Relation::query()
             ->orderBy('company_name')
-            ->get(['id', 'company_name'])
-            ->map(fn (Relation $relation): array => ['id' => $relation->id, 'name' => $relation->company_name])
+            ->pluck('company_name', 'id')
+            ->map(fn (string $name, int $id): array => ['id' => $id, 'name' => $name])
+            ->values()
             ->all();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Core/Filament/Company/Pages/Reports/BaseTabularReportPage.php` around
lines 111 - 118, Update getClientOptions to use the query builder’s pluck
operation instead of fetching Relation models with get and transforming them via
map. Preserve the company_name ordering and return the same id-to-name option
structure expected by callers.
Modules/Core/Filament/Company/Pages/Reports/InvoicedByClientReport.php (1)

33-41: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid fetching all records into memory for the summary.

Calling ->get() on the full query loads all matching records into memory, which can lead to memory exhaustion and slow performance for companies with many clients. It is more efficient to calculate the summary values directly at the database level.

♻️ Proposed fix
     public function summaryLine(): string
     {
-        $rows = $this->reportQuery()->get();
+        $query = $this->reportQuery();
+
+        $totalQuery = \Modules\Invoices\Models\Invoice::query()
+            ->whereBetween('invoiced_at', $this->dateRange())
+            ->when($this->clientId, fn ($q) => $q->where('customer_id', $this->clientId));

         return trans('ip.report_summary_invoiced_by_client', [
-            'clients' => $rows->count(),
-            'total'   => $this->money($rows->sum('invoiced_total')),
+            'clients' => $query->count(),
+            'total'   => $this->money($totalQuery->sum('invoice_total')),
         ]);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Core/Filament/Company/Pages/Reports/InvoicedByClientReport.php`
around lines 33 - 41, Update summaryLine() to compute the client count and
invoiced_total sum through database-level aggregate queries instead of calling
get() and materializing all rows. Preserve the existing translation keys and
money formatting, using the reportQuery() builder while ensuring the count and
sum operate on the full matching dataset.
Modules/Core/Filament/Company/Pages/Reports/InvoicesPerClientReport.php (1)

33-41: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid fetching all records into memory for the summary.

Loading all rows into memory via ->get() just to compute the total invoice count is inefficient and can cause memory issues for large datasets. Compute the totals at the database level instead.

♻️ Proposed fix
     public function summaryLine(): string
     {
-        $rows = $this->reportQuery()->get();
+        $query = $this->reportQuery();
+
+        $invoicesQuery = \Modules\Invoices\Models\Invoice::query()
+            ->whereBetween('invoiced_at', $this->dateRange())
+            ->when($this->clientId, fn ($q) => $q->where('customer_id', $this->clientId));

         return trans('ip.report_summary_invoices_per_client', [
-            'clients'  => $rows->count(),
-            'invoices' => (int) $rows->sum('invoices_count'),
+            'clients'  => $query->count(),
+            'invoices' => $invoicesQuery->count(),
         ]);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Core/Filament/Company/Pages/Reports/InvoicesPerClientReport.php`
around lines 33 - 41, Update summaryLine() to avoid reportQuery()->get() and
calculate both the client count and total invoices_count in the database using
aggregate queries. Preserve the existing translation key and summary values,
ensuring clients reflects the row count and invoices remains an integer total.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Modules/Core/Filament/Company/Pages/Reports/BaseTabularReportPage.php`:
- Around line 111-118: Update getClientOptions to use the query builder’s pluck
operation instead of fetching Relation models with get and transforming them via
map. Preserve the company_name ordering and return the same id-to-name option
structure expected by callers.

In `@Modules/Core/Filament/Company/Pages/Reports/InvoicedByClientReport.php`:
- Around line 33-41: Update summaryLine() to compute the client count and
invoiced_total sum through database-level aggregate queries instead of calling
get() and materializing all rows. Preserve the existing translation keys and
money formatting, using the reportQuery() builder while ensuring the count and
sum operate on the full matching dataset.

In `@Modules/Core/Filament/Company/Pages/Reports/InvoicesPerClientReport.php`:
- Around line 33-41: Update summaryLine() to avoid reportQuery()->get() and
calculate both the client count and total invoices_count in the database using
aggregate queries. Preserve the existing translation key and summary values,
ensuring clients reflects the row count and invoices remains an integer total.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3eaf4be9-cd6a-429e-bf3b-2814ea042776

📥 Commits

Reviewing files that changed from the base of the PR and between 16c779b and 9cfeb72.

📒 Files selected for processing (10)
  • Modules/Core/Filament/Company/Pages/Reports/BaseTabularReportPage.php
  • Modules/Core/Filament/Company/Pages/Reports/InvoicedByClientReport.php
  • Modules/Core/Filament/Company/Pages/Reports/InvoicesPerClientReport.php
  • Modules/Core/Filament/Company/Pages/Reports/InvoicingHistoryReport.php
  • Modules/Core/Filament/Company/Pages/Reports/PaymentHistoryReport.php
  • Modules/Core/Filament/Company/Pages/Reports/SalesByDateReport.php
  • Modules/Core/Providers/CompanyPanelProvider.php
  • Modules/Core/Tests/Feature/CompanyTabularReportsTest.php
  • Modules/Core/resources/views/filament/company/pages/reports/tabular-report.blade.php
  • resources/lang/en/ip.php

nielsdrost7 and others added 3 commits July 24, 2026 07:57
…ed_at

Expense, Payment, Project, and Task all set $timestamps = false and have
no created_at column, but the RecentExpensesWidget/RecentPaymentsWidget/
RecentProjectsWidget/RecentTasksWidget dashboard widgets called ->latest()
with no argument, which defaults to ordering by created_at regardless of
$timestamps. MySQL (CI) raises a hard 1054 Unknown column error; SQLite
silently reinterprets the double-quoted "created_at" identifier as a
string literal when it can't resolve it, so the bug never surfaced in
local sqlite-backed test runs.
Local tests silently diverging from CI's MariaDB (via a documented SQLite
.env.testing fallback) has repeatedly masked real bugs this session —
->latest() defaulting to a nonexistent created_at column, and identifier
quoting differences, both passed locally on SQLite and only failed on CI.

- docker-compose.yml: cli service now injects DB_CONNECTION=mysql/DB_HOST=db
  etc. itself and depends_on db, so `docker compose run --rm cli php artisan
  test` works against real MariaDB with zero per-developer .env.testing edits
- Add docker-resources/mariadb/init/01-create-test-db.sql to provision a
  dedicated invoiceplane_test database alongside the dev one on first boot
- Fix db service: the named `database` volume was declared but never
  mounted, so all local dev/test data was lost on every container recreate
- docker-resources/php-cli/Dockerfile: rebuild on Debian (php:8.4-cli) with
  the minimal proven extension set, matching the ip2-test-php:8.4 image this
  session used successfully throughout — see InvoicePlane#689 for a still-open false-
  failure issue found with a fresh cli image build, flagged in the docs
- Update AGENTS.md/CLAUDE.md/README.md/.github/DOCKER.md/Makefile to point
  at the compose db/cli path instead of the SQLite instructions
- Note throughout: use `php artisan test`, not raw vendor/bin/phpunit — the
  two were observed to behave differently for this app's Livewire form tests
…ages (Phase 5)

Track B of epic InvoicePlane#506, per naui95's spec: Payment History, Invoicing
History, Invoiced Amount by Client, Sales by Date (paid invoices), and
Invoices per Client. Each is a read-only Filament page under a Reports
navigation group with a date-range filter (defaults to the current
month), an optional client filter, a totals summary line, and a CSV
export streaming the filtered rows. Tenant scoping comes from the
BelongsToCompany global scope and is covered by a cross-company test.

Includes the AbstractCompanyPanelTestCase::testLivewire fix (wrong
Livewire import + withSession chained on the wrong object) — the same
fix ships in the report-builder branch; whichever merges second
resolves cleanly to identical content.

Test scenarios follow the issue body: in-range vs out-of-range
payments, per-client invoiced sum, company scoping, plus client
filtering, paid-only sales, and the CSV download.

Refs InvoicePlane#145 InvoicePlane#506

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nielsdrost7
nielsdrost7 force-pushed the feature/145-tabular-reports branch from a1b9fc4 to 4eac422 Compare July 24, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Reports]: PDF export for tabular report pages

1 participant