From 0e55e36ceace96aef31cc4ea2f1006281d06807e Mon Sep 17 00:00:00 2001
From: Nyan Lin Paing <117423022+LinPaing21@users.noreply.github.com>
Date: Sun, 23 Aug 2026 22:32:49 +0700
Subject: [PATCH] Add Bookings & Revenue reporting module
New modules/reporting Filament page: filterable bookings table (travel
date range, status, route, channel) with CSV/Excel export. Report
columns include booking ref, route, passenger name/count, price,
best-payment status/amount, and driver info, plus a TOTAL row summing
passenger count, price, and payment amount in both export formats.
- BookingsRevenueExport backs both CSV and XLSX via maatwebsite/excel
^4.0 (the only version compatible with PHP 8.5; 3.1.x caps
phpoffice/phpspreadsheet below 8.5).
- CSV export writes a UTF-8 BOM so non-Latin passenger names (Burmese)
open correctly in Excel.
- New view_reports permission (super_admin/admin/support) gates the
page; new indexes on bookings.travel_date/status/created_by_channel
and payments.completed_at support the report's filters.
---
.../database/seeders/RolePermissionSeeder.php | 3 +
app-modules/reporting/composer.json | 26 ++
...ndexes_to_bookings_and_payments_tables.php | 40 ++
.../pages/bookings-revenue-report.blade.php | 5 +
.../src/Exports/BookingsRevenueExport.php | 134 ++++++
.../Filament/Pages/BookingsRevenueReport.php | 195 ++++++++
.../Providers/ReportingServiceProvider.php | 12 +
app-modules/reporting/src/ReportingPlugin.php | 29 ++
.../Feature/BookingsRevenueReportTest.php | 89 ++++
app/Providers/Filament/AdminPanelProvider.php | 3 +
composer.json | 1 +
composer.lock | 420 +++++++++++++++++-
12 files changed, 956 insertions(+), 1 deletion(-)
create mode 100644 app-modules/reporting/composer.json
create mode 100644 app-modules/reporting/database/migrations/2026_08_23_000000_add_reporting_indexes_to_bookings_and_payments_tables.php
create mode 100644 app-modules/reporting/resources/views/filament/pages/bookings-revenue-report.blade.php
create mode 100644 app-modules/reporting/src/Exports/BookingsRevenueExport.php
create mode 100644 app-modules/reporting/src/Filament/Pages/BookingsRevenueReport.php
create mode 100644 app-modules/reporting/src/Providers/ReportingServiceProvider.php
create mode 100644 app-modules/reporting/src/ReportingPlugin.php
create mode 100644 app-modules/reporting/tests/Feature/BookingsRevenueReportTest.php
diff --git a/app-modules/identity/database/seeders/RolePermissionSeeder.php b/app-modules/identity/database/seeders/RolePermissionSeeder.php
index d40d413..71591e2 100644
--- a/app-modules/identity/database/seeders/RolePermissionSeeder.php
+++ b/app-modules/identity/database/seeders/RolePermissionSeeder.php
@@ -25,6 +25,7 @@ class RolePermissionSeeder extends Seeder
'manage_roles',
'view_customers',
'manage_settings',
+ 'view_reports',
];
/**
@@ -44,6 +45,7 @@ class RolePermissionSeeder extends Seeder
'manage_roles',
'view_customers',
'manage_settings',
+ 'view_reports',
],
'admin' => [
'manage_catalog',
@@ -56,6 +58,7 @@ class RolePermissionSeeder extends Seeder
'view_audit_log',
'view_customers',
'manage_settings',
+ 'view_reports',
],
'support' => [
'view_bookings',
diff --git a/app-modules/reporting/composer.json b/app-modules/reporting/composer.json
new file mode 100644
index 0000000..ed153dd
--- /dev/null
+++ b/app-modules/reporting/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "modules/reporting",
+ "description": "",
+ "type": "library",
+ "version": "1.0",
+ "license": "proprietary",
+ "require": {
+ "maatwebsite/excel": "^4.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Modules\\Reporting\\": "src/",
+ "Modules\\Reporting\\Tests\\": "tests/",
+ "Modules\\Reporting\\Database\\Factories\\": "database/factories/",
+ "Modules\\Reporting\\Database\\Seeders\\": "database/seeders/"
+ }
+ },
+ "minimum-stability": "stable",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Modules\\Reporting\\Providers\\ReportingServiceProvider"
+ ]
+ }
+ }
+}
diff --git a/app-modules/reporting/database/migrations/2026_08_23_000000_add_reporting_indexes_to_bookings_and_payments_tables.php b/app-modules/reporting/database/migrations/2026_08_23_000000_add_reporting_indexes_to_bookings_and_payments_tables.php
new file mode 100644
index 0000000..875eb50
--- /dev/null
+++ b/app-modules/reporting/database/migrations/2026_08_23_000000_add_reporting_indexes_to_bookings_and_payments_tables.php
@@ -0,0 +1,40 @@
+index('travel_date');
+ $table->index('status');
+ $table->index('created_by_channel');
+ });
+
+ Schema::table('payments', function (Blueprint $table) {
+ $table->index('completed_at');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('bookings', function (Blueprint $table) {
+ $table->dropIndex(['travel_date']);
+ $table->dropIndex(['status']);
+ $table->dropIndex(['created_by_channel']);
+ });
+
+ Schema::table('payments', function (Blueprint $table) {
+ $table->dropIndex(['completed_at']);
+ });
+ }
+};
diff --git a/app-modules/reporting/resources/views/filament/pages/bookings-revenue-report.blade.php b/app-modules/reporting/resources/views/filament/pages/bookings-revenue-report.blade.php
new file mode 100644
index 0000000..e19ba1b
--- /dev/null
+++ b/app-modules/reporting/resources/views/filament/pages/bookings-revenue-report.blade.php
@@ -0,0 +1,5 @@
+
+ {{ $this->filtersForm }}
+
+ {{ $this->table }}
+
diff --git a/app-modules/reporting/src/Exports/BookingsRevenueExport.php b/app-modules/reporting/src/Exports/BookingsRevenueExport.php
new file mode 100644
index 0000000..ee1038a
--- /dev/null
+++ b/app-modules/reporting/src/Exports/BookingsRevenueExport.php
@@ -0,0 +1,134 @@
+query;
+ }
+
+ /**
+ * @return array
+ */
+ public function headings(): array
+ {
+ return [
+ 'Booking Ref', 'Travel Date', 'Route', 'Channel', 'Status',
+ 'Passenger Name', 'Passenger Count', 'Price', 'Payment Status',
+ 'Payment Amount', 'Driver Name', 'Driver Phone', 'Car Plate',
+ 'Car Model', 'Vehicle Options',
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function map($booking): array
+ {
+ /** @var Booking $booking */
+ $payment = $this->bestPayment($booking);
+
+ return [
+ $booking->booking_ref,
+ $booking->travel_date?->toDateString(),
+ $booking->route ? $booking->route->name : '',
+ $booking->created_by_channel?->value,
+ $booking->status->value,
+ $booking->passenger_name,
+ $booking->vehicleOptions->sum('passenger_count'),
+ (float) $booking->price,
+ $payment?->status?->value ?? '',
+ $payment ? (float) $payment->amount : null,
+ $booking->driver_name,
+ $booking->driver_phone,
+ $booking->car_plate_number,
+ $booking->car_model,
+ $booking->vehicleOptions
+ ->map(fn ($v) => str($v->vehicle_option->value)->headline().' x'.$v->passenger_count)
+ ->implode('; '),
+ ];
+ }
+
+ /**
+ * Appends a bold TOTAL row (passenger count, price, payment amount)
+ * below the last data row. Re-fetches the already-filtered query rather
+ * than accumulating during map() — FromQuery streams rows in chunks, so
+ * there's no single point with the full result set to total as it's
+ * written; report-sized result sets make a second fetch cheap enough to
+ * trade for keeping the chunked write untouched.
+ *
+ * @return array
+ */
+ public function registerEvents(): array
+ {
+ return [
+ AfterSheet::class => function (AfterSheet $event): void {
+ $bookings = (clone $this->query)->get();
+
+ $totalPassengers = $bookings->sum(fn (Booking $b) => $b->vehicleOptions->sum('passenger_count'));
+ $totalPrice = $bookings->sum('price');
+ $totalPaid = $bookings->sum(fn (Booking $b) => $this->bestPayment($b)?->amount ?? 0);
+
+ $worksheet = $event->getDelegate();
+ $row = $worksheet->getHighestRow() + 1;
+ $lastColumn = Coordinate::stringFromColumnIndex(count($this->headings()));
+
+ $worksheet->setCellValue("A{$row}", 'TOTAL');
+ $worksheet->setCellValue($this->columnFor('Passenger Count').$row, $totalPassengers);
+ $worksheet->setCellValue($this->columnFor('Price').$row, $totalPrice);
+ $worksheet->setCellValue($this->columnFor('Payment Amount').$row, $totalPaid);
+ $worksheet->getStyle("A{$row}:{$lastColumn}{$row}")->getFont()->setBold(true);
+ },
+ ];
+ }
+
+ private function columnFor(string $heading): string
+ {
+ return Coordinate::stringFromColumnIndex(array_search($heading, $this->headings(), true) + 1);
+ }
+
+ private function bestPayment(Booking $booking): ?Payment
+ {
+ return $booking->payments->sortByDesc(
+ fn (Payment $p) => $p->status === PaymentStatus::Completed ? 1 : 0
+ )->first();
+ }
+
+ /**
+ * Excel's CSV import guesses encoding from the system locale unless a
+ * UTF-8 BOM is present, so passenger names/routes containing Burmese
+ * (or other non-Latin) text open correctly instead of as mojibake.
+ *
+ * @return array
+ */
+ public function getCsvSettings(): array
+ {
+ return [
+ 'use_bom' => true,
+ ];
+ }
+}
diff --git a/app-modules/reporting/src/Filament/Pages/BookingsRevenueReport.php b/app-modules/reporting/src/Filament/Pages/BookingsRevenueReport.php
new file mode 100644
index 0000000..113ac01
--- /dev/null
+++ b/app-modules/reporting/src/Filament/Pages/BookingsRevenueReport.php
@@ -0,0 +1,195 @@
+|null
+ */
+ public ?array $filters = [];
+
+ public static function canAccess(): bool
+ {
+ return auth()->user()?->can('view_reports') ?? false;
+ }
+
+ public function mount(): void
+ {
+ $this->filtersForm->fill();
+ }
+
+ public function filtersForm(Schema $schema): Schema
+ {
+ return $schema
+ ->components([
+ DatePicker::make('travel_date_from')
+ ->label('Travel date from')
+ ->live(),
+ DatePicker::make('travel_date_to')
+ ->label('Travel date to')
+ ->afterOrEqual('travel_date_from')
+ ->live(),
+ Select::make('status')
+ ->label('Status')
+ ->options(BookingStatus::class)
+ ->native(false)
+ ->placeholder('All statuses')
+ ->live(),
+ Select::make('ev_route_id')
+ ->label('Route')
+ ->options(fn () => EvRoute::with(['fromDestination', 'toDestination'])->get()
+ ->mapWithKeys(fn (EvRoute $route) => [$route->id => $route->name]))
+ ->searchable()
+ ->placeholder('All routes')
+ ->live(),
+ Select::make('created_by_channel')
+ ->label('Channel')
+ ->options(BookingChannel::class)
+ ->native(false)
+ ->placeholder('All channels')
+ ->live(),
+ ])
+ ->columns(3)
+ ->statePath('filters');
+ }
+
+ public function table(Table $table): Table
+ {
+ return $table
+ ->query(fn (): Builder => $this->reportQuery())
+ ->columns([
+ TextColumn::make('booking_ref')
+ ->label('Ref')
+ ->sortable(),
+ TextColumn::make('travel_date')
+ ->date()
+ ->sortable(),
+ TextColumn::make('route.name')
+ ->label('Route'),
+ TextColumn::make('created_by_channel')
+ ->badge(),
+ TextColumn::make('status')
+ ->badge(),
+ TextColumn::make('passenger_name')
+ ->label('Passenger'),
+ TextColumn::make('passenger_count')
+ ->label('Pax')
+ ->state(fn (Booking $record) => $record->vehicleOptions->sum('passenger_count'))
+ ->summarize(Summarizer::make()
+ ->label('Total')
+ ->using(fn (QueryBuilder $query) => Booking::query()
+ ->with('vehicleOptions')
+ ->whereIn('id', (clone $query)->pluck('id'))
+ ->get()
+ ->sum(fn (Booking $b) => $b->vehicleOptions->sum('passenger_count')))),
+ TextColumn::make('price')
+ ->numeric(2)
+ ->sortable()
+ ->summarize(Sum::make()->label('Total')),
+ TextColumn::make('payment_status')
+ ->label('Payment')
+ ->state(fn (Booking $record) => $this->bestPayment($record)?->status?->value ?? '—'),
+ TextColumn::make('payment_amount')
+ ->label('Paid')
+ ->state(fn (Booking $record) => $this->bestPayment($record)?->amount)
+ ->summarize(Summarizer::make()
+ ->label('Total')
+ ->using(fn (QueryBuilder $query) => Booking::query()
+ ->with('payments')
+ ->whereIn('id', (clone $query)->pluck('id'))
+ ->get()
+ ->sum(fn (Booking $b) => $this->bestPayment($b)?->amount ?? 0))),
+ TextColumn::make('driver_name')
+ ->label('Driver')
+ ->placeholder('—'),
+ ])
+ ->defaultSort('travel_date', 'desc')
+ ->paginated([25, 50, 100]);
+ }
+
+ public function reportQuery(): Builder
+ {
+ $data = $this->filters ?? [];
+
+ return Booking::query()
+ ->with(['route.fromDestination', 'route.toDestination', 'payments', 'vehicleOptions'])
+ ->when($data['travel_date_from'] ?? null, fn (Builder $q, $d) => $q->whereDate('travel_date', '>=', $d))
+ ->when($data['travel_date_to'] ?? null, fn (Builder $q, $d) => $q->whereDate('travel_date', '<=', $d))
+ ->when($data['status'] ?? null, fn (Builder $q, $s) => $q->where('status', $s))
+ ->when($data['ev_route_id'] ?? null, fn (Builder $q, $id) => $q->where('ev_route_id', $id))
+ ->when($data['created_by_channel'] ?? null, fn (Builder $q, $c) => $q->where('created_by_channel', $c))
+ // A unique tie-breaker after travel_date — required for FromQuery's
+ // chunked export to paginate deterministically (see its docblock);
+ // the table's own defaultSort() applies on top of this for display.
+ ->orderBy('travel_date', 'desc')
+ ->orderBy('id');
+ }
+
+ protected function bestPayment(Booking $record): ?Payment
+ {
+ return $record->payments->sortByDesc(
+ fn (Payment $p) => $p->status === PaymentStatus::Completed ? 1 : 0
+ )->first();
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ Action::make('exportCsv')
+ ->label('Export CSV')
+ ->icon(Heroicon::OutlinedArrowDownTray)
+ ->action(fn () => Excel::download(
+ new BookingsRevenueExport($this->reportQuery()),
+ 'bookings-revenue-'.now()->format('Y-m-d').'.csv',
+ ExcelFormat::CSV,
+ )),
+ Action::make('exportXlsx')
+ ->label('Export Excel')
+ ->icon(Heroicon::OutlinedArrowDownTray)
+ ->action(fn () => Excel::download(
+ new BookingsRevenueExport($this->reportQuery()),
+ 'bookings-revenue-'.now()->format('Y-m-d').'.xlsx',
+ ExcelFormat::XLSX,
+ )),
+ ];
+ }
+}
diff --git a/app-modules/reporting/src/Providers/ReportingServiceProvider.php b/app-modules/reporting/src/Providers/ReportingServiceProvider.php
new file mode 100644
index 0000000..022ca2d
--- /dev/null
+++ b/app-modules/reporting/src/Providers/ReportingServiceProvider.php
@@ -0,0 +1,12 @@
+discoverPages(
+ in: __DIR__.'/Filament/Pages',
+ for: 'Modules\Reporting\Filament\Pages',
+ );
+ }
+
+ public function boot(Panel $panel): void {}
+
+ public static function make(): static
+ {
+ return app(static::class);
+ }
+}
diff --git a/app-modules/reporting/tests/Feature/BookingsRevenueReportTest.php b/app-modules/reporting/tests/Feature/BookingsRevenueReportTest.php
new file mode 100644
index 0000000..2c284a8
--- /dev/null
+++ b/app-modules/reporting/tests/Feature/BookingsRevenueReportTest.php
@@ -0,0 +1,89 @@
+admin = User::factory()->create()->givePermissionTo(['view_reports']);
+ $this->actingAs($this->admin);
+});
+
+test('it renders for a user with view_reports', function () {
+ Livewire::test(BookingsRevenueReport::class)->assertOk();
+});
+
+test('a user without view_reports cannot access it', function () {
+ $this->actingAs(User::factory()->create());
+
+ expect(BookingsRevenueReport::canAccess())->toBeFalse();
+});
+
+test('it narrows results by travel date range and status', function () {
+ $inRange = Booking::factory()->create(['travel_date' => today(), 'status' => BookingStatus::Confirmed]);
+ $outOfRange = Booking::factory()->create(['travel_date' => today()->addMonths(2), 'status' => BookingStatus::Confirmed]);
+ $wrongStatus = Booking::factory()->create(['travel_date' => today(), 'status' => BookingStatus::Cancelled]);
+
+ Livewire::test(BookingsRevenueReport::class)
+ ->fillForm([
+ 'travel_date_from' => today()->toDateString(),
+ 'travel_date_to' => today()->toDateString(),
+ 'status' => BookingStatus::Confirmed->value,
+ ], 'filtersForm')
+ ->assertCanSeeTableRecords([$inRange])
+ ->assertCanNotSeeTableRecords([$outOfRange, $wrongStatus]);
+});
+
+test('exporting csv triggers a download', function () {
+ Excel::fake();
+
+ Booking::factory()->create();
+
+ Livewire::test(BookingsRevenueReport::class)->callAction('exportCsv');
+
+ Excel::assertDownloaded('bookings-revenue-'.now()->format('Y-m-d').'.csv');
+});
+
+test('exporting excel triggers a download', function () {
+ Excel::fake();
+
+ Booking::factory()->create();
+
+ Livewire::test(BookingsRevenueReport::class)->callAction('exportXlsx');
+
+ Excel::assertDownloaded('bookings-revenue-'.now()->format('Y-m-d').'.xlsx');
+});
+
+test('the export includes passenger name/count columns and a total row', function () {
+ $a = Booking::factory()->create(['passenger_name' => 'Jane Doe', 'price' => 10000]);
+ $a->vehicleOptions()->create(['vehicle_option' => 'back_seat', 'passenger_count' => 2, 'unit_price' => 5000, 'line_total' => 10000]);
+
+ $b = Booking::factory()->create(['passenger_name' => 'John Roe', 'price' => 15000]);
+ $b->vehicleOptions()->create(['vehicle_option' => 'back_seat', 'passenger_count' => 3, 'unit_price' => 5000, 'line_total' => 15000]);
+
+ $export = new BookingsRevenueExport(Booking::query()->with(['route.fromDestination', 'route.toDestination', 'payments', 'vehicleOptions']));
+
+ $path = storage_path('app/test-bookings-revenue.xlsx');
+ file_put_contents($path, Excel::raw($export, ExcelFormat::XLSX));
+
+ $sheet = IOFactory::load($path)->getActiveSheet();
+ unlink($path);
+
+ expect($sheet->getCell('F1')->getValue())->toBe('Passenger Name')
+ ->and($sheet->getCell('G1')->getValue())->toBe('Passenger Count')
+ ->and([$sheet->getCell('F2')->getValue(), $sheet->getCell('F3')->getValue()])->toContain('Jane Doe', 'John Roe');
+
+ $totalRow = $sheet->getHighestRow();
+ expect($sheet->getCell("A{$totalRow}")->getValue())->toBe('TOTAL')
+ ->and((int) $sheet->getCell("G{$totalRow}")->getValue())->toBe(5) // 2 + 3 passengers
+ ->and((float) $sheet->getCell("H{$totalRow}")->getValue())->toBe(25000.0); // 10000 + 15000 price
+});
diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php
index 19c8794..e387ad3 100644
--- a/app/Providers/Filament/AdminPanelProvider.php
+++ b/app/Providers/Filament/AdminPanelProvider.php
@@ -25,6 +25,7 @@ use Modules\Booking\BookingPlugin;
use Modules\Catalog\CatalogPlugin;
use Modules\Identity\IdentityPlugin;
use Modules\Payment\PaymentPlugin;
+use Modules\Reporting\ReportingPlugin;
use Modules\Routing\RoutingPlugin;
class AdminPanelProvider extends PanelProvider
@@ -47,6 +48,7 @@ class AdminPanelProvider extends PanelProvider
NavigationGroup::make()->label('Catalog'),
NavigationGroup::make()->label('Routing'),
NavigationGroup::make()->label('Operations'),
+ NavigationGroup::make()->label('Reports'),
])
->plugins([
CatalogPlugin::make(),
@@ -54,6 +56,7 @@ class AdminPanelProvider extends PanelProvider
BookingPlugin::make(),
PaymentPlugin::make(),
IdentityPlugin::make(),
+ ReportingPlugin::make(),
// T6.5 — ops convenience for browsing storage/logs/*.log
// in-browser; distinct from the structured, per-model audit
// trail (AuditLogResource, T6.2). No extra permission gate:
diff --git a/composer.json b/composer.json
index 71d6f18..30ae070 100644
--- a/composer.json
+++ b/composer.json
@@ -18,6 +18,7 @@
"modules/catalog": "*",
"modules/identity": "*",
"modules/payment": "*",
+ "modules/reporting": "*",
"modules/routing": "*",
"modules/shared": "*",
"spatie/laravel-activitylog": "^5.0",
diff --git a/composer.lock b/composer.lock
index efedaa9..0d76063 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "323e86b8a07e30ef4c9ed6f5a2f01b8f",
+ "content-hash": "b3018ca42fa6d16d0da6113b3aa6b1a8",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -4316,6 +4316,173 @@
],
"time": "2026-08-10T15:24:05+00:00"
},
+ {
+ "name": "maatwebsite/excel",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SpartnerNL/Laravel-Excel.git",
+ "reference": "5d1c617c9fea810d0c547d69d4dfddd3f0a9fea8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/5d1c617c9fea810d0c547d69d4dfddd3f0a9fea8",
+ "reference": "5d1c617c9fea810d0c547d69d4dfddd3f0a9fea8",
+ "shasum": ""
+ },
+ "require": {
+ "composer/semver": "^3.4",
+ "illuminate/support": "^12.0 || ^13.0",
+ "php": "^8.3",
+ "phpoffice/phpspreadsheet": "^5.8",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.20",
+ "driftingly/rector-laravel": "^2.5",
+ "ext-sqlite3": "*",
+ "larastan/larastan": "^3.10",
+ "laravel/pint": "^1.29",
+ "laravel/scout": "^10.25 || ^11.2",
+ "orchestra/testbench": "^10.11 || ^11.1",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpunit/phpunit": "^12.5 || ~13.1.14",
+ "predis/predis": "^2.3 || ^3.0",
+ "rector/rector": "^2.4.2"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Excel": "Maatwebsite\\Excel\\Facades\\Excel"
+ },
+ "providers": [
+ "Maatwebsite\\Excel\\ExcelServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Maatwebsite\\Excel\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Patrick Brouwers",
+ "email": "patrick@spartner.nl"
+ }
+ ],
+ "description": "Supercharged Excel exports and imports in Laravel",
+ "keywords": [
+ "PHPExcel",
+ "batch",
+ "csv",
+ "excel",
+ "export",
+ "import",
+ "laravel",
+ "php",
+ "phpspreadsheet"
+ ],
+ "support": {
+ "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
+ "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://laravel-excel.com/commercial-support",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/patrickbrouwers",
+ "type": "github"
+ }
+ ],
+ "time": "2026-08-18T12:32:09+00:00"
+ },
+ {
+ "name": "maennchen/zipstream-php",
+ "version": "3.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maennchen/ZipStream-PHP.git",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "ext-zlib": "*",
+ "php-64bit": "^8.3"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.7",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^3.86",
+ "guzzlehttp/guzzle": "^7.5",
+ "mikey179/vfsstream": "^1.6",
+ "php-coveralls/php-coveralls": "^2.5",
+ "phpunit/phpunit": "^12.0",
+ "vimeo/psalm": "^6.0"
+ },
+ "suggest": {
+ "guzzlehttp/psr7": "^2.4",
+ "psr/http-message": "^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ZipStream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paul Duncan",
+ "email": "pabs@pablotron.org"
+ },
+ {
+ "name": "Jonatan Männchen",
+ "email": "jonatan@maennchen.ch"
+ },
+ {
+ "name": "Jesse Donat",
+ "email": "donatj@gmail.com"
+ },
+ {
+ "name": "András Kolesár",
+ "email": "kolesar@kolesar.hu"
+ }
+ ],
+ "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
+ "keywords": [
+ "stream",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/maennchen",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-11T18:38:28+00:00"
+ },
{
"name": "marc-mabe/php-enum",
"version": "v4.7.2",
@@ -4389,6 +4556,113 @@
},
"time": "2025-09-14T11:18:39+00:00"
},
+ {
+ "name": "markbaker/complex",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
+ },
+ "time": "2022-12-06T16:21:08+00:00"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "sebastian/phpcpd": "^4.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@demon-angel.eu"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
+ },
+ "time": "2022-12-02T22:17:43+00:00"
+ },
{
"name": "modules/booking",
"version": "1.0",
@@ -4517,6 +4791,41 @@
"relative": true
}
},
+ {
+ "name": "modules/reporting",
+ "version": "1.0",
+ "dist": {
+ "type": "path",
+ "url": "app-modules/reporting",
+ "reference": "39c235e3c324b47b1c890e9aedd07044b670aad8"
+ },
+ "require": {
+ "maatwebsite/excel": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Modules\\Reporting\\Providers\\ReportingServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Modules\\Reporting\\": "src/",
+ "Modules\\Reporting\\Tests\\": "tests/",
+ "Modules\\Reporting\\Database\\Factories\\": "database/factories/",
+ "Modules\\Reporting\\Database\\Seeders\\": "database/seeders/"
+ }
+ },
+ "license": [
+ "proprietary"
+ ],
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
{
"name": "modules/routing",
"version": "1.0",
@@ -5327,6 +5636,115 @@
},
"time": "2025-09-24T15:06:41+00:00"
},
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "5.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/05e99ebf61238a70227b4d9cc02d0030d34f6339",
+ "reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1||^2||^3",
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-filter": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "maennchen/zipstream-php": "^2.1 || ^3.0",
+ "markbaker/complex": "^3.0",
+ "markbaker/matrix": "^3.0",
+ "php": "^8.2",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
+ "dompdf/dompdf": "^2.0 || ^3.0",
+ "ext-intl": "*",
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "mitoteam/jpgraph": "^10.5",
+ "mpdf/mpdf": "^8.1.1",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpstan/phpstan": "^1.1 || ^2.0",
+ "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
+ "phpunit/phpunit": "^10.5 || ^11.0",
+ "squizlabs/php_codesniffer": "^3.7",
+ "tecnickcom/tcpdf": "^6.5"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+ "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
+ "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ },
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ },
+ {
+ "name": "Owen Leibman"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.9.0"
+ },
+ "time": "2026-07-12T19:17:39+00:00"
+ },
{
"name": "phpoption/phpoption",
"version": "1.9.5",