shemyart/filament-date-range
A Filament plugin that adds a date range picker component for admin panels and forms. Let users select start and end dates through a clean, intuitive interface, ideal for filtering reports, dashboards, and resource tables by custom time periods within Laravel Filament apps.
Install and publish
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config
The service provider is auto-discovered; only publish the config if you need to change defaults (date format, default range, timezone behaviour). Don't publish assets you won't edit — it makes future upgrades harder to diff.
Where to look first
config/<package>.php — the default format, fallback range, and the list of accepted query keys live here.src/Filters or similar). This is the real API surface; the config is just wiring.First use case
The canonical 10-minute win: an admin index page with a "From / To" date picker that filters a listing by created_at.
// routes/web.php
Route::get('/orders', OrderController::class);
// app/Http/Controllers/OrderController.php
public function __invoke(Request $request)
{
$orders = Order::query()
->dateRange($request->only(['from', 'to'])) // registered scope/trait
->latest()
->paginate(25);
return view('orders.index', compact('orders'));
}
Wire the form inputs to the query keys the package expects (from / to by default — confirm in config) and you have a working filter. Reach for the package's own validation rule or form request before hand-rolling date parsing.
Scope-first, not controller-first. Apply the filter as a query scope or trait method so the same logic serves HTTP, queued exports, and Artisan commands. Keeping parsing in the request layer and application in the model layer means your export job can pass a CarbonPeriod directly without faking a request.
Compose rather than branch. Most implementations accept an array of filters, which lets you build the parameter set conditionally:
$filters = array_filter([
'from' => $request->date('from'),
'to' => $request->date('to'),
'status' => $request->input('status'),
'customer' => $request->input('customer'),
]);
$orders = Order::query()->filters($filters)->paginate();
This scales better than if ($request->filled('from')) chains, and a single empty-array call is a harmless no-op.
Validate at the boundary, once. Put date validation in a FormRequest so both the index page and any JSON endpoint reject malformed input identically:
'from' => ['nullable', 'date', 'before_or_equal:to'],
'to' => ['nullable', 'date', 'after_or_equal:from'],
Without before_or_equal, an inverted range silently returns zero rows and looks like a bug in the filter.
Sensible defaults beat empty screens. For dashboards, default to a rolling window (now()->subDays(30) to now()) and reflect it back into the form inputs. Users read an unfiltered table as "all data" and then misread totals.
Preserve filters across pagination and sorting. $orders->appends($request->query()) on the paginator, and make sort links carry the current date params. This is the single most common "the filter is broken" report and it's never the filter — it's the links.
Exports and reports. Reuse the same scope in an ExportsOrders job and pass an explicit CarbonPeriod so the export captures the range at dispatch time rather than resolving now() when the worker picks it up.
Testing. Use Carbon::setTestNow() for relative defaults, and cover these four cases explicitly: only from, only to, both, neither.
Inclusive vs. exclusive end dates. A to of 2024-03-31 sent as a bare date parses to midnight and silently drops that entire day. Either use endOfDay() on the bound or filter created_at < $to->copy()->addDay()->startOfDay(). The second form is index-friendly; the first can be too if your column is a DATETIME. Verify which one the package does rather than assuming.
Never use whereDate() on a large table. WHERE DATE(created_at) = ? is non-sargable and turns a range scan into a full scan. If you see that in a generated query, that's your performance problem. Check with ->toSql() and EXPLAIN before shipping.
Timezone drift. Laravel stores created_at in the app timezone (usually UTC) but users pick dates in their own. Parse input in the user's timezone, convert to the app timezone before querying. A filter that "misses the last 8 hours" for a European team is almost always this.
Ambiguous date formats. 03/04/2024 is March 4th or April 3rd depending on locale. Pass an explicit format (or accept ISO-8601 only) — never rely on strtotime heuristics. This bites hardest when a numeric-looking string parses "successfully" into the wrong month.
Empty strings vs. nulls. An HTML form submits from="" when cleared, which is not null. Normalise with array_filter() or $request->date() (which returns null for empty) before the filter sees it, otherwise you may get a validation failure or a 1970-01-01 bound.
Config caching. After changing the config, php artisan config:clear in local dev. In production, remember the deploy step — a cached config will ignore your new default range and there is no error message for this.
Extension points. Most packages of this shape let you register a custom filter class (implement the package's filter interface, then add it to the config array). Prefer that over subclassing the package's built-in filter — subclassing couples you to internal method signatures that change between minors.
Debugging checklist when a filter returns the wrong rows, in order: dump $request->query(); dump the parsed Carbon bounds; ->ddRawSql() the query; confirm the column's actual type (DATE vs DATETIME vs TIMESTAMP changes boundary behaviour); confirm the app timezone.
Upgrade discipline. Pin the major version. Date handling is exactly the kind of thing that gets "fixed" in a minor release (boundary semantics, default format), and such a change is silent — no exception, just different row counts. Re-run your four boundary tests after every update.
How can I help you explore Laravel packages today?