diff --git a/app/Http/Controllers/Api/DeliveryController.php b/app/Http/Controllers/Api/DeliveryController.php index dd454c3..11acee7 100644 --- a/app/Http/Controllers/Api/DeliveryController.php +++ b/app/Http/Controllers/Api/DeliveryController.php @@ -6,6 +6,7 @@ use App\Models\Delivery; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; @@ -52,13 +53,15 @@ public function index(Request $request): JsonResponse $query->where('event_id', $request->input('event_id')); } - // Filter by date range + // Filter by date range. Comparing against a plain timestamp range + // (rather than wrapping created_at in whereDate()) keeps the query + // sargable so it can use the index on created_at. if ($request->has('from_date')) { - $query->whereDate('created_at', '>=', $request->input('from_date')); + $query->where('created_at', '>=', Carbon::parse($request->input('from_date'))->startOfDay()); } if ($request->has('to_date')) { - $query->whereDate('created_at', '<=', $request->input('to_date')); + $query->where('created_at', '<', Carbon::parse($request->input('to_date'))->addDay()->startOfDay()); } $deliveries = $query->orderBy('created_at', 'desc') diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 05c8277..5f1f3fc 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -5,6 +5,7 @@ use App\Models\Delivery; use App\Models\Event; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Inertia\Inertia; use Inertia\Response; @@ -133,12 +134,15 @@ public function deliveries(Request $request): Response $query->whereHas('event', fn ($q) => $q->where('name', 'like', '%'.$request->event_name.'%')); } + // Comparing against a plain timestamp range (rather than wrapping + // created_at in whereDate()) keeps the query sargable so it can use + // the index on created_at. if ($request->has('from_date') && $request->from_date) { - $query->whereDate('created_at', '>=', $request->from_date); + $query->where('created_at', '>=', Carbon::parse($request->from_date)->startOfDay()); } if ($request->has('to_date') && $request->to_date) { - $query->whereDate('created_at', '<=', $request->to_date); + $query->where('created_at', '<', Carbon::parse($request->to_date)->addDay()->startOfDay()); } $statusCounts = (clone $query) diff --git a/database/migrations/2026_09_21_000000_add_created_at_index_to_deliveries_table.php b/database/migrations/2026_09_21_000000_add_created_at_index_to_deliveries_table.php new file mode 100644 index 0000000..ba6913e --- /dev/null +++ b/database/migrations/2026_09_21_000000_add_created_at_index_to_deliveries_table.php @@ -0,0 +1,27 @@ +index('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('deliveries', function (Blueprint $table) { + $table->dropIndex(['created_at']); + }); + } +}; diff --git a/tests/Feature/DeliveryDateRangeFilterBoundaryTest.php b/tests/Feature/DeliveryDateRangeFilterBoundaryTest.php new file mode 100644 index 0000000..df8883c --- /dev/null +++ b/tests/Feature/DeliveryDateRangeFilterBoundaryTest.php @@ -0,0 +1,93 @@ +pluck('columns') + ->flatten() + ->all(); + + $this->assertContains('created_at', $indexedColumns); + } + + public function test_api_date_range_filter_includes_the_full_first_and_last_day(): void + { + $user = User::factory()->withPersonalTeam()->create(); + $endpoint = Endpoint::factory()->for($user)->create(); + $event = Event::factory()->for($user)->create(); + + $startOfFirstDay = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-06-01 00:00:00', + ]); + $endOfLastDay = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-06-30 23:59:59', + ]); + $beforeRange = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-05-31 23:59:59', + ]); + $afterRange = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-07-01 00:00:00', + ]); + + $response = $this->actingAs($user)->getJson( + '/api/v1/deliveries?from_date=2026-06-01&to_date=2026-06-30' + ); + + $response->assertOk(); + $ids = collect($response->json('data'))->pluck('id'); + + $this->assertTrue($ids->contains($startOfFirstDay->id)); + $this->assertTrue($ids->contains($endOfLastDay->id)); + $this->assertFalse($ids->contains($beforeRange->id)); + $this->assertFalse($ids->contains($afterRange->id)); + } + + public function test_dashboard_date_range_filter_includes_the_full_first_and_last_day(): void + { + $user = User::factory()->withPersonalTeam()->create(); + $endpoint = Endpoint::factory()->for($user)->create(); + $event = Event::factory()->for($user)->create(); + + $startOfFirstDay = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-06-01 00:00:00', + ]); + $endOfLastDay = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-06-30 23:59:59', + ]); + $beforeRange = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-05-31 23:59:59', + ]); + $afterRange = Delivery::factory()->for($event)->for($endpoint)->create([ + 'created_at' => '2026-07-01 00:00:00', + ]); + + $response = $this->actingAs($user)->get(route('deliveries', [ + 'from_date' => '2026-06-01', + 'to_date' => '2026-06-30', + ])); + + $response->assertOk(); + $response->assertInertia( + fn ($page) => $page + ->component('Deliveries/Index') + ->has('deliveries.data', 2) + // Ordered by latest() first, so the end-of-range delivery sorts before the start-of-range one. + ->where('deliveries.data.0.id', $endOfLastDay->id) + ->where('deliveries.data.1.id', $startOfFirstDay->id) + ); + } +}