dpsoft/nova-persian-date-filter
A Laravel Nova filter that lets you filter date fields using the Persian (Jalali) calendar. Pick dates with a familiar Shamsi datepicker while Nova queries the underlying Gregorian values, making admin panels friendlier for Persian-speaking users.
This is a small Nova filter package that lets you put a Jalali (Persian) date picker in front of a Nova resource index and have the chosen dates translated into Gregorian ranges for the underlying query. It is a thin wrapper — there is no service provider magic to learn, no migrations, and no UI beyond the filter menu.
Install it next to Nova:
composer require dpsoft/nova-persian-date-filter
Before you install, read composer.json in the package. The last tagged release is February 2020, so the Nova constraint is almost certainly ^1.0|^2.0|^3.0. If you are on Nova 4 or 5, expect to either fork it or lift the apply() logic into a filter of your own (see Gotchas).
Look at src/ first. The package is a single base filter class (something like PersianDateFilter / DateFilter) extending Laravel\Nova\Filters\Filter. Read its apply() method — that is the entire contract you inherit, and it tells you exactly which $value shape the front-end sends (usually an array of two Jalali date strings, or a single string depending on whether it is a range or a single-date filter).
Create your own filter:
php artisan nova:filter CreatedAtFilter
Then change the parent class from Laravel\Nova\Filters\Filter to the package's base filter, and point it at your column.
Register it in the resource and you have your first use case — a Jalali date-range filter on created_at:
public function filters(Request $request)
{
return [new CreatedAtFilter];
}
If the package ships a config file (config/persian-date-filter.php or similar), publish it with php artisan vendor:publish --provider="Dpsoft\...\FilterServiceProvider" — check vendor/dpsoft/nova-persian-date-filter/ for the exact provider name rather than guessing.
Subclass-and-configure (the intended path). The package is designed so you extend its filter and only declare the column and label. Typical shape:
class CreatedAtFilter extends PersianDateFilter
{
public $name = 'تاریخ ایجاد';
public function apply(Request $request, $query, $value)
{
// $value is Jalali input from the picker
return parent::apply($request, $query, $value);
}
}
If the parent already handles conversion, your override should only add scoping (e.g. a where('tenant_id', ...)) rather than re-parsing dates.
Do the conversion explicitly when you own the query. The most maintainable pattern with a package this small is to let it own only the input widget and keep the query logic in your filter:
public function apply(Request $request, $query, $value)
{
if (blank($value)) {
return $query;
}
[$from, $to] = array_pad((array) $value, 2, null);
$from = $from ? Jalalian::fromFormat('Y-m-d', $from)->toCarbon()->startOfDay() : null;
$to = $to ? Jalalian::fromFormat('Y-m-d', $to)->toCarbon()->endOfDay() : null;
return $query
->when($from, fn ($q) => $q->where('created_at', '>=', $from))
->when($to, fn ($q) => $q->where('created_at', '<=', $to));
}
This is what you fall back to on Nova 4+ or if the package's apply() hard-codes a column name.
Reuse across resources. Because filters are instantiated per-resource, extract your conversion into a trait (ConvertsJalaliDates) or an abstract JalaliDateRangeFilter in app/Nova/Filters/. Then each resource filter is three lines: $name, $column, done. This also gives you one place to fix when the package is abandoned.
Pair it with a quick-select. Nova filters support ->default() and options; a common day-to-day combo is the Jalali range picker plus a preset menu ("امروز / این هفته / این ماه") that sets the same $value shape. Keep the value shape identical so apply() does not branch.
Test the filter, not the UI. Nova filters are easy to unit test without a browser:
$filter = new CreatedAtFilter;
$query = User::query();
$filter->apply(request(), $query, ['1402-01-01', '1402-01-31']);
$this->assertStringContainsString('where', $query->toSql());
Assert on the bound dates (Gregorian) rather than the SQL string — it documents the conversion contract and catches off-by-one-day bugs on range ends.
apply(Request $request, $query, $value) signature and the switch to nova:filter generating Inertia/Vue components that differ from Nova 2/3's). If composer require conflicts, don't fight it — vendor the ~50 lines of apply() into your own filter. With 6 stars and no dependents, assume you are the maintainer.Asia/Tehran, but your column is almost certainly UTC. startOfDay() on a Carbon date built from a Jalali string uses the app timezone, not the user's. Set the timezone explicitly (->setTimezone('Asia/Tehran') for display, UTC for the query) or your "today" filter will silently drop the first and last few hours of records.۱۴۰۲-۰۱-۰۱ while Jalalian::fromFormat('Y-m-d', ...) expects ASCII. Normalize on the way in (strtr($value, ['۰'=>'0', ...])) or you get a null date and a query that matches everything.$filters, so apply() runs with null/[] on every index load. Always early-return $query on blank input, otherwise you'll append whereBetween with nulls and empty the table.1402-01-31 should be <= 1402-01-31 23:59:59, not <= 1402-01-31 00:00:00. Use endOfDay(), or better, use an exclusive upper bound (< nextDay) to avoid microsecond precision surprises.dist/ and expect it via nova.mix or a script()/fields() method. If the picker renders as a plain text input, the package's JS wasn't loaded — check whether it needs php artisan vendor:publish --tag=nova-persian-date-filter-assets or a ->withMeta() call.dd($value, $query->toSql(), $query->getBindings()) at the top of apply() and hit the index once. It answers, in one shot, what shape the picker sends and whether your conversion produced the dates you expected.$name/$column properties, the apply() override, and Nova's ->default() / ->canSee() for permission-gating. There is nothing deeper to hook into, so treat the class as a template rather than a dependency — copy it into app/Nova/Filters/ and delete the composer requirement if you want long-term stability.How can I help you explore Laravel packages today?