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",