keizah7/custom-date-filter
A Laravel package that adds a configurable date filter to your admin panels, letting users narrow records by custom ranges, presets, or single dates. Simple to register, easy to style, and flexible enough for any resource.
This is a small, single-purpose Nova filter package with no stars, no dependents and a score of 0 — so treat the README as a hint, not a contract, and read the source before you build on it.
1. Install (verify the tag first)
composer show keizah7/custom-date-filter --all
composer require keizah7/custom-date-filter
If Packagist has no tagged release you'll need to pin a branch or add a VCS repository entry:
"repositories": [{ "type": "vcs", "url": "https://github.com/keizah7/nova-date-filter" }],
"require": { "keizah7/custom-date-filter": "dev-main" }
2. Look here first, in this order
composer.json — the illuminate/* and laravel/nova constraints tell you which Nova major it targets. A Nova 3 constraint on a Nova 4/5 app is the most common reason this kind of package "doesn't work".src/ — the filter class. Confirm whether it extends Laravel\Nova\Filters\Filter and what shape $value arrives in (apply() signature).resources/js/ — if there's a Vue component, the filter has a custom UI and you'll need npm run build / nova:publish semantics. If there's no JS, it's a plain select-based filter and Nova renders it for you.README.md — usually a screenshot and one snippet.3. First use case: date-range filter on a resource index
Register it like any Nova filter:
// app/Nova/Order.php
public function filters(NovaRequest $request)
{
return [
\Keizah7\CustomDateFilter\CustomDateFilter::make(),
];
}
Then load /nova/resources/orders, open the filter dropdown, pick a date, and confirm the resulting URL contains a filters query param. That URL is the contract — copy it into a failing test if the results look wrong.
Preset ranges via options() — the highest-value pattern for day-to-day Nova work, and the one users actually ask for. Most date filter packages expose this; if this one doesn't, add it in a subclass:
public function options(NovaRequest $request)
{
return [
'Today' => 'today',
'Last 7 days' => 'last_7',
'This month' => 'this_month',
'Last month' => 'last_month',
'This year' => 'this_year',
];
}
public function apply(NovaRequest $request, $query, $value)
{
return match ($value) {
'today' => $query->whereDate('created_at', today()),
'last_7' => $query->where('created_at', '>=', now()->subDays(7)),
'this_month' => $query->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()]),
'last_month' => $query->whereBetween('created_at', [
now()->subMonthNoOverflow()->startOfMonth(),
now()->subMonthNoOverflow()->endOfMonth(),
]),
'this_year' => $query->whereYear('created_at', now()->year),
default => $query,
};
}
Explicit from/to range with two inputs. When the filter exposes a range, $value is typically an array of strings from the frontend:
public function apply(NovaRequest $request, $query, $value)
{
[$from, $to] = [$value['from'] ?? null, $value['to'] ?? null];
return $query
->when($from, fn ($q) => $q->where('created_at', '>=', Carbon::parse($from)->startOfDay()))
->when($to, fn ($q) => $q->where('created_at', '<=', Carbon::parse($to)->endOfDay()));
}
Sensible default so the index isn't a full table scan — default() pre-populates the filter, which matters once the table is large:
public function default()
{
return 'last_7';
}
Reuse across many resources. Rather than registering the package class on fifteen resources, extract the date logic into a base class or trait and let each resource supply the column:
abstract class DateColumnFilter extends Filter
{
abstract protected function column(): string;
public function apply(NovaRequest $request, $query, $value)
{
return $query->whereBetween($this->column(), [
Carbon::parse($value['from'])->startOfDay(),
Carbon::parse($value['to'])->endOfDay(),
]);
}
}
Qualify the column when the index query joins. Nova lens and relationship queries often introduce joins; unqualified created_at will throw ambiguous column errors. Use $query->qualifyColumn('created_at') or orders.created_at.
Test the filter directly. You don't need a browser to verify behaviour:
$filter = new CreatedAtRange;
$query = $filter->apply(NovaRequest::create('/'), Order::query(), ['from' => '2024-01-01', 'to' => '2024-01-31']);
$this->assertSame(42, $query->count());
Guard with canSee() when only certain roles should filter by date — cheaper than a policy and keeps the dropdown tidy.
whereBetween('created_at', ['2024-01-01', '2024-01-31']) silently drops everything after midnight on the 31st. Always ->endOfDay() the upper bound, or the bug report will read "records are missing for the last day."whereDate kills your index. On MySQL, whereDate('created_at', $d) compiles to DATE(created_at) = ?, which can't use the created_at index. On a large table prefer an explicit half-open range: >= startOfDay() and < startOfDay()->addDay(). Reach for whereDate only on small tables or where correctness beats speed.Carbon::parse($value, config('app.timezone'))->utc(). A filter that's off by a day for users east of UTC is the classic symptom.$value shape is not guaranteed. Depending on whether the filter renders as a select, a date input, or a multi-select, $value can be a string, an array, a JSON-encoded string, or null when the user clicks Reset. Always null-guard and return $query unchanged on empty input.laravel/nova constraint in composer.json against your installed Nova. Nova 4 removed a lot of the frontend scaffolding Nova 3 packages depend on; a filter with a bundled Vue component from the Nova 3 era will usually render blank in Nova 4/5.src/ ships a .vue file, Nova needs the compiled JS. Run php artisan nova:publish and rebuild assets; a filter that appears in the dropdown but never fires is nearly always a missing or stale bundle.?filters=base64... means bookmarks and shared links carry the date range. Useful, but it also means a user's "Today" link shows stale results tomorrow — prefer preset keys over resolved dates for anything you expect people to share.null/empty for every filter; confirm apply() handles it before shipping, or you'll get a Carbon::parse(null) exception on the reset click.src/, copy the ~40 lines into app/Nova/Filters/, and drop the dependency — you keep full control over timezone and end-of-day behaviour, and you're not exposed if the package stops working with a Nova upgrade. If you do keep it, pin the exact version in composer.json and add a feature test around the filter so an upgrade can't break it silently.How can I help you explore Laravel packages today?