Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions Modules/Core/Filament/Company/Pages/Reports/BaseTabularReportPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace Modules\Core\Filament\Company\Pages\Reports;

use Filament\Actions\Action;
use Filament\Pages\Page;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Modules\Clients\Models\Relation;
use Modules\Core\Enums\UserRole;

/**
* Shared shell for the tabular reports (#145): a date range (defaults to
* the current month), an optional client filter, a read-only Filament
* table with a summary line, and a CSV export of the filtered rows.
* All queries are tenant-scoped through the BelongsToCompany global scope.
*/
abstract class BaseTabularReportPage extends Page implements HasTable
{
use InteractsWithTable;

public ?string $dateFrom = null;

public ?string $dateTo = null;

public ?int $clientId = null;

protected string $view = 'core::filament.company.pages.reports.tabular-report';

/**
* The filtered query behind both the table and the CSV export.
*/
abstract public function reportQuery(): Builder;

/**
* @return array<int, \Filament\Tables\Columns\Column>
*/
abstract protected function reportColumns(): array;

/**
* @return array<int, string>
*/
abstract protected function csvHeaders(): array;

/**
* @return array<int, string|int|float>
*/
abstract protected function csvRow($record): array;

/**
* One-line totals summary rendered under the table.
*/
abstract public function summaryLine(): string;

public static function canAccess(): bool
{
return auth()->user()?->hasAnyRole([
...UserRole::elevated(),
UserRole::CUSTOMER_ADMIN->value,
]) ?? false;
}

public static function getNavigationGroup(): ?string
{
return trans('ip.reports');
}

public function mount(): void
{
$this->dateFrom ??= now()->startOfMonth()->toDateString();
$this->dateTo ??= now()->endOfMonth()->toDateString();
}

public function table(Table $table): Table
{
return $table
->query(fn (): Builder => $this->reportQuery())
->columns($this->reportColumns())
->paginated([25, 50, 100])
->headerActions([
$this->exportCsvAction(),
]);
}

public function exportCsvAction(): Action
{
return Action::make('exportCsv')
->label(trans('ip.export_csv'))
->icon('heroicon-o-arrow-down-tray')
->action(function () {
$filename = static::getSlug() . '-' . now()->toDateString() . '.csv';

return response()->streamDownload(function (): void {
$handle = fopen('php://output', 'wb');
fputcsv($handle, $this->csvHeaders());

foreach ($this->reportQuery()->get() as $record) {
fputcsv($handle, $this->csvRow($record));
}

fclose($handle);
}, $filename, ['Content-Type' => 'text/csv']);
});
}

/**
* @return array<int, array{id: int, name: string}>
*/
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])
->all();
}

protected function dateRange(): array
{
return [
$this->dateFrom ?? now()->startOfMonth()->toDateString(),
$this->dateTo ?? now()->endOfMonth()->toDateString(),
];
}

protected function money(mixed $amount): string
{
return number_format((float) $amount, 2, '.', '');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Modules\Core\Filament\Company\Pages\Reports;

use Filament\Tables\Columns\TextColumn;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Modules\Clients\Models\Relation;

class InvoicedByClientReport extends BaseTabularReportPage
{
public static function getNavigationLabel(): string
{
return trans('ip.report_invoiced_by_client');
}

public function getTitle(): string
{
return trans('ip.report_invoiced_by_client');
}

public function reportQuery(): Builder
{
$range = fn (Builder $query) => $query->whereBetween('invoiced_at', $this->dateRange());

return Relation::query()
->when($this->clientId, fn (Builder $query) => $query->whereKey($this->clientId))
->whereHas('invoices', $range)
->withCount(['invoices as invoices_count' => $range])
->withSum(['invoices as invoiced_total' => $range], 'invoice_total')
->orderByDesc('invoiced_total');
}

public function summaryLine(): string
{
$rows = $this->reportQuery()->get();

return trans('ip.report_summary_invoiced_by_client', [
'clients' => $rows->count(),
'total' => $this->money($rows->sum('invoiced_total')),
]);
}

protected function reportColumns(): array
{
return [
TextColumn::make('company_name')->label(trans('ip.client')),
TextColumn::make('invoices_count')->label(trans('ip.invoices'))->alignRight(),
TextColumn::make('invoiced_total')->label(trans('ip.total'))->numeric(2)->alignRight(),
];
}

protected function csvHeaders(): array
{
return ['Client', 'Invoices', 'Invoiced total'];
}

protected function csvRow($record): array
{
return [
$record->company_name,
$record->invoices_count,
$this->money($record->invoiced_total),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Modules\Core\Filament\Company\Pages\Reports;

use Filament\Tables\Columns\TextColumn;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Modules\Clients\Models\Relation;

class InvoicesPerClientReport extends BaseTabularReportPage
{
public static function getNavigationLabel(): string
{
return trans('ip.report_invoices_per_client');
}

public function getTitle(): string
{
return trans('ip.report_invoices_per_client');
}

public function reportQuery(): Builder
{
$range = fn (Builder $query) => $query->whereBetween('invoiced_at', $this->dateRange());

return Relation::query()
->when($this->clientId, fn (Builder $query) => $query->whereKey($this->clientId))
->whereHas('invoices', $range)
->withCount(['invoices as invoices_count' => $range])
->withAvg(['invoices as average_value' => $range], 'invoice_total')
->orderByDesc('invoices_count');
}

public function summaryLine(): string
{
$rows = $this->reportQuery()->get();

return trans('ip.report_summary_invoices_per_client', [
'clients' => $rows->count(),
'invoices' => (int) $rows->sum('invoices_count'),
]);
}

protected function reportColumns(): array
{
return [
TextColumn::make('company_name')->label(trans('ip.client')),
TextColumn::make('invoices_count')->label(trans('ip.invoices'))->alignRight(),
TextColumn::make('average_value')->label(trans('ip.average_value'))->numeric(2)->alignRight(),
];
}

protected function csvHeaders(): array
{
return ['Client', 'Invoices', 'Average value'];
}

protected function csvRow($record): array
{
return [
$record->company_name,
$record->invoices_count,
$this->money($record->average_value),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Modules\Core\Filament\Company\Pages\Reports;

use Filament\Tables\Columns\TextColumn;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Modules\Invoices\Enums\InvoiceStatus;
use Modules\Invoices\Models\Invoice;

class InvoicingHistoryReport extends BaseTabularReportPage
{
public static function getNavigationLabel(): string
{
return trans('ip.report_invoicing_history');
}

public function getTitle(): string
{
return trans('ip.report_invoicing_history');
}

public function reportQuery(): Builder
{
return Invoice::query()
->with('customer')
->whereBetween('invoiced_at', $this->dateRange())
->when($this->clientId, fn (Builder $query) => $query->where('customer_id', $this->clientId))
->orderBy('invoiced_at');
}

public function summaryLine(): string
{
$query = $this->reportQuery();
$paid = (clone $query)->where('invoice_status', InvoiceStatus::PAID->value)->sum('invoice_total');

return trans('ip.report_summary_invoicing', [
'count' => $query->count(),
'total' => $this->money($query->sum('invoice_total')),
'paid' => $this->money($paid),
'unpaid' => $this->money($query->sum('invoice_total') - $paid),
]);
}

protected function reportColumns(): array
{
return [
TextColumn::make('invoice_number')->label(trans('ip.invoice_number')),
TextColumn::make('invoiced_at')->label(trans('ip.invoice_date'))->date(),
TextColumn::make('invoice_status')->label(trans('ip.invoice_status'))->badge(),
TextColumn::make('customer.company_name')->label(trans('ip.client')),
TextColumn::make('invoice_total')->label(trans('ip.total'))->numeric(2)->alignRight(),
];
}

protected function csvHeaders(): array
{
return ['Invoice number', 'Date', 'Status', 'Client', 'Total'];
}

protected function csvRow($record): array
{
return [
$record->invoice_number,
$record->invoiced_at?->toDateString(),
$record->invoice_status?->value,
$record->customer?->company_name,
$this->money($record->invoice_total),
];
}
}
Loading
Loading