Relative date and datetime constraint for query builder (#18635)

* feature: Relative date constraint filtering

* clean up

* time

* clean up

* Update 04-query-builder.md

* add new column to snapshots
This commit is contained in:
Dan Harrin
2025-12-02 19:32:37 +00:00
committed by GitHub
parent f72f079287
commit 79dda2e1ff
18 changed files with 2409 additions and 12 deletions
@@ -146,6 +146,50 @@ return [
],
'unit_labels' => [
'second' => 'Seconds',
'minute' => 'Minutes',
'hour' => 'Hours',
'day' => 'Days',
'week' => 'Weeks',
'month' => 'Months',
'quarter' => 'Quarters',
'year' => 'Years',
],
'presets' => [
'past_decade' => 'Past decade',
'past_5_years' => 'Past 5 years',
'past_2_years' => 'Past 2 years',
'past_year' => 'Past year',
'past_6_months' => 'Past 6 months',
'past_quarter' => 'Past quarter',
'past_month' => 'Past month',
'past_2_weeks' => 'Past 2 weeks',
'past_week' => 'Past week',
'past_hour' => 'Past hour',
'past_minute' => 'Past minute',
'this_decade' => 'This decade',
'this_year' => 'This year',
'this_quarter' => 'This quarter',
'this_month' => 'This month',
'today' => 'Today',
'this_hour' => 'This hour',
'this_minute' => 'This minute',
'next_minute' => 'Next minute',
'next_hour' => 'Next hour',
'next_week' => 'Next week',
'next_2_weeks' => 'Next 2 weeks',
'next_month' => 'Next month',
'next_quarter' => 'Next quarter',
'next_6_months' => 'Next 6 months',
'next_year' => 'Next year',
'next_2_years' => 'Next 2 years',
'next_5_years' => 'Next 5 years',
'next_decade' => 'Next decade',
'custom' => 'Custom',
],
'form' => [
'date' => [
@@ -160,6 +204,34 @@ return [
'label' => 'Year',
],
'mode' => [
'label' => 'Date type',
'options' => [
'absolute' => 'Specific date',
'relative' => 'Rolling window',
],
],
'preset' => [
'label' => 'Time period',
],
'relative_value' => [
'label' => 'How many',
],
'relative_unit' => [
'label' => 'Time unit',
],
'tense' => [
'label' => 'Tense',
'options' => [
'past' => 'Past',
'future' => 'Future',
],
],
],
],
@@ -2,6 +2,7 @@
namespace Filament\QueryBuilder\Constraints;
use Closure;
use Filament\QueryBuilder\Constraints\DateConstraint\Operators\IsAfterOperator;
use Filament\QueryBuilder\Constraints\DateConstraint\Operators\IsBeforeOperator;
use Filament\QueryBuilder\Constraints\DateConstraint\Operators\IsDateOperator;
@@ -16,6 +17,20 @@ class DateConstraint extends Constraint
{
use Concerns\CanBeNullable;
protected bool | Closure $hasTime = false;
public function time(bool | Closure $condition = true): static
{
$this->hasTime = $condition;
return $this;
}
public function hasTime(): bool
{
return (bool) $this->evaluate($this->hasTime);
}
protected function setUp(): void
{
parent::setUp();
@@ -0,0 +1,36 @@
<?php
namespace Filament\QueryBuilder\Constraints\DateConstraint;
use Filament\Support\Contracts\HasLabel;
enum DateUnit: string implements HasLabel
{
case Second = 'second';
case Minute = 'minute';
case Hour = 'hour';
case Day = 'day';
case Week = 'week';
case Month = 'month';
case Quarter = 'quarter';
case Year = 'year';
public function getLabel(): string
{
return match ($this) {
self::Second => __('filament-query-builder::query-builder.operators.date.unit_labels.second'),
self::Minute => __('filament-query-builder::query-builder.operators.date.unit_labels.minute'),
self::Hour => __('filament-query-builder::query-builder.operators.date.unit_labels.hour'),
self::Day => __('filament-query-builder::query-builder.operators.date.unit_labels.day'),
self::Week => __('filament-query-builder::query-builder.operators.date.unit_labels.week'),
self::Month => __('filament-query-builder::query-builder.operators.date.unit_labels.month'),
self::Quarter => __('filament-query-builder::query-builder.operators.date.unit_labels.quarter'),
self::Year => __('filament-query-builder::query-builder.operators.date.unit_labels.year'),
};
}
public function isTimeUnit(): bool
{
return in_array($this, [self::Second, self::Minute, self::Hour], strict: true);
}
}
@@ -4,9 +4,15 @@ namespace Filament\QueryBuilder\Constraints\DateConstraint\Operators;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\QueryBuilder\Constraints\DateConstraint;
use Filament\QueryBuilder\Constraints\DateConstraint\DateUnit;
use Filament\QueryBuilder\Constraints\Operators\Operator;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\FusedGroup;
use Filament\Schemas\Components\Utilities\Get;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
@@ -28,13 +34,36 @@ class IsAfterOperator extends Operator
public function getSummary(): string
{
$settings = $this->getSettings();
$constraint = $this->getConstraint();
$hasTime = $constraint instanceof DateConstraint && $constraint->hasTime();
// Check if using relative mode
if (($settings['mode'] ?? null) === 'relative') {
$resolvedDate = $this->resolveRelativeDate($settings, $hasTime);
$parsedDate = Carbon::parse($resolvedDate);
$isTimeBasedFilter = $this->isTimeBasedFilter($settings);
return __(
$this->isInverse() ?
'filament-query-builder::query-builder.operators.date.is_after.summary.inverse' :
'filament-query-builder::query-builder.operators.date.is_after.summary.direct',
[
'attribute' => $constraint->getAttributeLabel(),
'date' => $hasTime && $isTimeBasedFilter ? $parsedDate->toFormattedDayDateString() . ' ' . $parsedDate->format('H:i:s') : $parsedDate->toFormattedDateString(),
],
);
}
$parsedDate = Carbon::parse($settings['date']);
return __(
$this->isInverse() ?
'filament-query-builder::query-builder.operators.date.is_after.summary.inverse' :
'filament-query-builder::query-builder.operators.date.is_after.summary.direct',
[
'attribute' => $this->getConstraint()->getAttributeLabel(),
'date' => Carbon::parse($this->getSettings()['date'])->toFormattedDateString(),
'attribute' => $constraint->getAttributeLabel(),
'date' => $hasTime ? $parsedDate->toFormattedDayDateString() . ' ' . $parsedDate->format('H:i:s') : $parsedDate->toFormattedDateString(),
],
);
}
@@ -44,15 +73,221 @@ class IsAfterOperator extends Operator
*/
public function getFormSchema(): array
{
$constraint = $this->getConstraint();
$hasTime = $constraint instanceof DateConstraint && $constraint->hasTime();
return [
DatePicker::make('date')
Select::make('mode')
->label(__('filament-query-builder::query-builder.operators.date.form.mode.label'))
->selectablePlaceholder(false)
->live()
->options([
'absolute' => __('filament-query-builder::query-builder.operators.date.form.mode.options.absolute'),
'relative' => __('filament-query-builder::query-builder.operators.date.form.mode.options.relative'),
])
->default('absolute'),
DateTimePicker::make('date')
->label(__('filament-query-builder::query-builder.operators.date.form.date.label'))
->required(),
->time($hasTime)
->hidden(fn (Get $get): bool => $get('mode') === 'relative')
->required(fn (Get $get): bool => $get('mode') !== 'relative'),
Select::make('preset')
->label(__('filament-query-builder::query-builder.operators.date.form.preset.label'))
->selectablePlaceholder(false)
->live()
->options($this->getPresetOptions($hasTime))
->default('past_month')
->hidden(fn (Get $get): bool => $get('mode') !== 'relative')
->required(fn (Get $get): bool => $get('mode') === 'relative'),
FusedGroup::make([
Select::make('tense')
->label(__('filament-query-builder::query-builder.operators.date.form.tense.label'))
->selectablePlaceholder(false)
->options([
'past' => __('filament-query-builder::query-builder.operators.date.form.tense.options.past'),
'future' => __('filament-query-builder::query-builder.operators.date.form.tense.options.future'),
])
->default('past')
->required(),
TextInput::make('relative_value')
->label(__('filament-query-builder::query-builder.operators.date.form.relative_value.label'))
->numeric()
->minValue(1)
->default(1)
->required(),
Select::make('relative_unit')
->label(__('filament-query-builder::query-builder.operators.date.form.relative_unit.label'))
->options(
collect(DateUnit::cases())
->reject(fn (DateUnit $unit): bool => (! $hasTime) && $unit->isTimeUnit())
->mapWithKeys(fn (DateUnit $unit): array => [$unit->value => $unit->getLabel()])
->all()
)
->default(DateUnit::Day->value)
->required(),
])
->columns(3)
->columnSpanFull()
->hidden(fn (Get $get): bool => $get('mode') !== 'relative' || $get('preset') !== 'custom'),
];
}
/**
* @return array<string, string>
*/
protected function getPresetOptions(bool $hasTime): array
{
$options = [
'past_decade' => __('filament-query-builder::query-builder.operators.date.presets.past_decade'),
'past_5_years' => __('filament-query-builder::query-builder.operators.date.presets.past_5_years'),
'past_2_years' => __('filament-query-builder::query-builder.operators.date.presets.past_2_years'),
'past_year' => __('filament-query-builder::query-builder.operators.date.presets.past_year'),
'past_6_months' => __('filament-query-builder::query-builder.operators.date.presets.past_6_months'),
'past_quarter' => __('filament-query-builder::query-builder.operators.date.presets.past_quarter'),
'past_month' => __('filament-query-builder::query-builder.operators.date.presets.past_month'),
'past_2_weeks' => __('filament-query-builder::query-builder.operators.date.presets.past_2_weeks'),
'past_week' => __('filament-query-builder::query-builder.operators.date.presets.past_week'),
];
if ($hasTime) {
$options['past_hour'] = __('filament-query-builder::query-builder.operators.date.presets.past_hour');
$options['past_minute'] = __('filament-query-builder::query-builder.operators.date.presets.past_minute');
}
$options += [
'this_decade' => __('filament-query-builder::query-builder.operators.date.presets.this_decade'),
'this_year' => __('filament-query-builder::query-builder.operators.date.presets.this_year'),
'this_quarter' => __('filament-query-builder::query-builder.operators.date.presets.this_quarter'),
'this_month' => __('filament-query-builder::query-builder.operators.date.presets.this_month'),
'today' => __('filament-query-builder::query-builder.operators.date.presets.today'),
];
if ($hasTime) {
$options['this_hour'] = __('filament-query-builder::query-builder.operators.date.presets.this_hour');
$options['this_minute'] = __('filament-query-builder::query-builder.operators.date.presets.this_minute');
$options['next_minute'] = __('filament-query-builder::query-builder.operators.date.presets.next_minute');
$options['next_hour'] = __('filament-query-builder::query-builder.operators.date.presets.next_hour');
}
$options += [
'next_week' => __('filament-query-builder::query-builder.operators.date.presets.next_week'),
'next_2_weeks' => __('filament-query-builder::query-builder.operators.date.presets.next_2_weeks'),
'next_month' => __('filament-query-builder::query-builder.operators.date.presets.next_month'),
'next_quarter' => __('filament-query-builder::query-builder.operators.date.presets.next_quarter'),
'next_6_months' => __('filament-query-builder::query-builder.operators.date.presets.next_6_months'),
'next_year' => __('filament-query-builder::query-builder.operators.date.presets.next_year'),
'next_2_years' => __('filament-query-builder::query-builder.operators.date.presets.next_2_years'),
'next_5_years' => __('filament-query-builder::query-builder.operators.date.presets.next_5_years'),
'next_decade' => __('filament-query-builder::query-builder.operators.date.presets.next_decade'),
'custom' => __('filament-query-builder::query-builder.operators.date.presets.custom'),
];
return $options;
}
public function apply(Builder $query, string $qualifiedColumn): Builder
{
return $query->whereDate($qualifiedColumn, $this->isInverse() ? '<' : '>=', $this->getSettings()['date']);
$settings = $this->getSettings();
$constraint = $this->getConstraint();
$hasTime = $constraint instanceof DateConstraint && $constraint->hasTime();
// Check if using relative mode - only if mode is explicitly set to 'relative'
if (($settings['mode'] ?? null) === 'relative') {
$dateTime = $this->resolveRelativeDate($settings, $hasTime);
if ($hasTime && $this->isTimeBasedFilter($settings)) {
return $query->where($qualifiedColumn, $this->isInverse() ? '<' : '>=', $dateTime);
}
return $query->whereDate($qualifiedColumn, $this->isInverse() ? '<' : '>=', $dateTime);
}
return $query->whereDate($qualifiedColumn, $this->isInverse() ? '<' : '>=', $settings['date']);
}
/**
* @param array<string, mixed> $settings
*/
protected function isTimeBasedFilter(array $settings): bool
{
$preset = $settings['preset'] ?? 'custom';
if (in_array($preset, ['this_minute', 'this_hour', 'past_minute', 'past_hour', 'next_minute', 'next_hour'], true)) {
return true;
}
if ($preset === 'custom') {
$unit = $settings['relative_unit'] ?? DateUnit::Day->value;
return in_array($unit, [DateUnit::Second->value, DateUnit::Minute->value, DateUnit::Hour->value], true);
}
return false;
}
/**
* @param array<string, mixed> $settings
*/
protected function resolveRelativeDate(array $settings, bool $hasTime = false): string
{
$preset = $settings['preset'] ?? 'custom';
return match ($preset) {
'past_decade' => Carbon::now()->subYears(10)->toDateString(),
'past_5_years' => Carbon::now()->subYears(5)->toDateString(),
'past_2_years' => Carbon::now()->subYears(2)->toDateString(),
'past_year' => Carbon::now()->subYear()->toDateString(),
'past_6_months' => Carbon::now()->subMonths(6)->toDateString(),
'past_quarter' => Carbon::now()->subQuarter()->toDateString(),
'past_month' => Carbon::now()->subMonth()->toDateString(),
'past_2_weeks' => Carbon::now()->subWeeks(2)->toDateString(),
'past_week' => Carbon::now()->subWeek()->toDateString(),
'past_hour' => Carbon::now()->subHour()->toDateTimeString(),
'past_minute' => Carbon::now()->subMinute()->toDateTimeString(),
'this_decade' => Carbon::now()->startOfDecade()->toDateString(),
'this_year' => Carbon::now()->startOfYear()->toDateString(),
'this_quarter' => Carbon::now()->startOfQuarter()->toDateString(),
'this_month' => Carbon::now()->startOfMonth()->toDateString(),
'today' => Carbon::today()->toDateString(),
'this_hour' => Carbon::now()->startOfHour()->toDateTimeString(),
'this_minute' => Carbon::now()->startOfMinute()->toDateTimeString(),
'next_minute' => Carbon::now()->addMinute()->toDateTimeString(),
'next_hour' => Carbon::now()->addHour()->toDateTimeString(),
'next_week' => Carbon::now()->addWeek()->toDateString(),
'next_2_weeks' => Carbon::now()->addWeeks(2)->toDateString(),
'next_month' => Carbon::now()->addMonth()->toDateString(),
'next_quarter' => Carbon::now()->addQuarter()->toDateString(),
'next_6_months' => Carbon::now()->addMonths(6)->toDateString(),
'next_year' => Carbon::now()->addYear()->toDateString(),
'next_2_years' => Carbon::now()->addYears(2)->toDateString(),
'next_5_years' => Carbon::now()->addYears(5)->toDateString(),
'next_decade' => Carbon::now()->addYears(10)->toDateString(),
'custom' => $this->resolveCustomRelativeDate($settings, $hasTime),
default => Carbon::today()->toDateString(),
};
}
/**
* @param array<string, mixed> $settings
*/
protected function resolveCustomRelativeDate(array $settings, bool $hasTime = false): string
{
$value = (int) ($settings['relative_value'] ?? 1);
$unit = $settings['relative_unit'] ?? DateUnit::Day->value;
$tense = $settings['tense'] ?? 'past';
$method = $tense === 'future' ? 'add' : 'sub';
return match ($unit) {
DateUnit::Second->value => Carbon::now()->{$method . 'Seconds'}($value)->toDateTimeString(),
DateUnit::Minute->value => Carbon::now()->{$method . 'Minutes'}($value)->toDateTimeString(),
DateUnit::Hour->value => Carbon::now()->{$method . 'Hours'}($value)->toDateTimeString(),
DateUnit::Day->value => Carbon::now()->{$method . 'Days'}($value)->toDateString(),
DateUnit::Week->value => Carbon::now()->{$method . 'Weeks'}($value)->toDateString(),
DateUnit::Month->value => Carbon::now()->{$method . 'Months'}($value)->toDateString(),
DateUnit::Quarter->value => Carbon::now()->{$method . 'Quarters'}($value)->toDateString(),
DateUnit::Year->value => Carbon::now()->{$method . 'Years'}($value)->toDateString(),
default => Carbon::today()->toDateString(),
};
}
}
@@ -4,9 +4,15 @@ namespace Filament\QueryBuilder\Constraints\DateConstraint\Operators;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\QueryBuilder\Constraints\DateConstraint;
use Filament\QueryBuilder\Constraints\DateConstraint\DateUnit;
use Filament\QueryBuilder\Constraints\Operators\Operator;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\FusedGroup;
use Filament\Schemas\Components\Utilities\Get;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
@@ -28,13 +34,36 @@ class IsBeforeOperator extends Operator
public function getSummary(): string
{
$settings = $this->getSettings();
$constraint = $this->getConstraint();
$hasTime = $constraint instanceof DateConstraint && $constraint->hasTime();
// Check if using relative mode
if (($settings['mode'] ?? null) === 'relative') {
$resolvedDate = $this->resolveRelativeDate($settings, $hasTime);
$parsedDate = Carbon::parse($resolvedDate);
$isTimeBasedFilter = $this->isTimeBasedFilter($settings);
return __(
$this->isInverse() ?
'filament-query-builder::query-builder.operators.date.is_before.summary.inverse' :
'filament-query-builder::query-builder.operators.date.is_before.summary.direct',
[
'attribute' => $constraint->getAttributeLabel(),
'date' => $hasTime && $isTimeBasedFilter ? $parsedDate->toFormattedDayDateString() . ' ' . $parsedDate->format('H:i:s') : $parsedDate->toFormattedDateString(),
],
);
}
$parsedDate = Carbon::parse($settings['date']);
return __(
$this->isInverse() ?
'filament-query-builder::query-builder.operators.date.is_before.summary.inverse' :
'filament-query-builder::query-builder.operators.date.is_before.summary.direct',
[
'attribute' => $this->getConstraint()->getAttributeLabel(),
'date' => Carbon::parse($this->getSettings()['date'])->toFormattedDateString(),
'attribute' => $constraint->getAttributeLabel(),
'date' => $hasTime ? $parsedDate->toFormattedDayDateString() . ' ' . $parsedDate->format('H:i:s') : $parsedDate->toFormattedDateString(),
],
);
}
@@ -44,15 +73,221 @@ class IsBeforeOperator extends Operator
*/
public function getFormSchema(): array
{
$constraint = $this->getConstraint();
$hasTime = $constraint instanceof DateConstraint && $constraint->hasTime();
return [
DatePicker::make('date')
Select::make('mode')
->label(__('filament-query-builder::query-builder.operators.date.form.mode.label'))
->selectablePlaceholder(false)
->live()
->options([
'absolute' => __('filament-query-builder::query-builder.operators.date.form.mode.options.absolute'),
'relative' => __('filament-query-builder::query-builder.operators.date.form.mode.options.relative'),
])
->default('absolute'),
DateTimePicker::make('date')
->label(__('filament-query-builder::query-builder.operators.date.form.date.label'))
->required(),
->time($hasTime)
->hidden(fn (Get $get): bool => $get('mode') === 'relative')
->required(fn (Get $get): bool => $get('mode') !== 'relative'),
Select::make('preset')
->label(__('filament-query-builder::query-builder.operators.date.form.preset.label'))
->selectablePlaceholder(false)
->live()
->options($this->getPresetOptions($hasTime))
->default('past_month')
->hidden(fn (Get $get): bool => $get('mode') !== 'relative')
->required(fn (Get $get): bool => $get('mode') === 'relative'),
FusedGroup::make([
Select::make('tense')
->label(__('filament-query-builder::query-builder.operators.date.form.tense.label'))
->selectablePlaceholder(false)
->options([
'past' => __('filament-query-builder::query-builder.operators.date.form.tense.options.past'),
'future' => __('filament-query-builder::query-builder.operators.date.form.tense.options.future'),
])
->default('past')
->required(),
TextInput::make('relative_value')
->label(__('filament-query-builder::query-builder.operators.date.form.relative_value.label'))
->numeric()
->minValue(1)
->default(1)
->required(),
Select::make('relative_unit')
->label(__('filament-query-builder::query-builder.operators.date.form.relative_unit.label'))
->options(
collect(DateUnit::cases())
->reject(fn (DateUnit $unit): bool => (! $hasTime) && $unit->isTimeUnit())
->mapWithKeys(fn (DateUnit $unit): array => [$unit->value => $unit->getLabel()])
->all()
)
->default(DateUnit::Day->value)
->required(),
])
->columns(3)
->columnSpanFull()
->hidden(fn (Get $get): bool => $get('mode') !== 'relative' || $get('preset') !== 'custom'),
];
}
/**
* @return array<string, string>
*/
protected function getPresetOptions(bool $hasTime): array
{
$options = [
'past_decade' => __('filament-query-builder::query-builder.operators.date.presets.past_decade'),
'past_5_years' => __('filament-query-builder::query-builder.operators.date.presets.past_5_years'),
'past_2_years' => __('filament-query-builder::query-builder.operators.date.presets.past_2_years'),
'past_year' => __('filament-query-builder::query-builder.operators.date.presets.past_year'),
'past_6_months' => __('filament-query-builder::query-builder.operators.date.presets.past_6_months'),
'past_quarter' => __('filament-query-builder::query-builder.operators.date.presets.past_quarter'),
'past_month' => __('filament-query-builder::query-builder.operators.date.presets.past_month'),
'past_2_weeks' => __('filament-query-builder::query-builder.operators.date.presets.past_2_weeks'),
'past_week' => __('filament-query-builder::query-builder.operators.date.presets.past_week'),
];
if ($hasTime) {
$options['past_hour'] = __('filament-query-builder::query-builder.operators.date.presets.past_hour');
$options['past_minute'] = __('filament-query-builder::query-builder.operators.date.presets.past_minute');
}
$options += [
'this_decade' => __('filament-query-builder::query-builder.operators.date.presets.this_decade'),
'this_year' => __('filament-query-builder::query-builder.operators.date.presets.this_year'),
'this_quarter' => __('filament-query-builder::query-builder.operators.date.presets.this_quarter'),
'this_month' => __('filament-query-builder::query-builder.operators.date.presets.this_month'),
'today' => __('filament-query-builder::query-builder.operators.date.presets.today'),
];
if ($hasTime) {
$options['this_hour'] = __('filament-query-builder::query-builder.operators.date.presets.this_hour');
$options['this_minute'] = __('filament-query-builder::query-builder.operators.date.presets.this_minute');
$options['next_minute'] = __('filament-query-builder::query-builder.operators.date.presets.next_minute');
$options['next_hour'] = __('filament-query-builder::query-builder.operators.date.presets.next_hour');
}
$options += [
'next_week' => __('filament-query-builder::query-builder.operators.date.presets.next_week'),
'next_2_weeks' => __('filament-query-builder::query-builder.operators.date.presets.next_2_weeks'),
'next_month' => __('filament-query-builder::query-builder.operators.date.presets.next_month'),
'next_quarter' => __('filament-query-builder::query-builder.operators.date.presets.next_quarter'),
'next_6_months' => __('filament-query-builder::query-builder.operators.date.presets.next_6_months'),
'next_year' => __('filament-query-builder::query-builder.operators.date.presets.next_year'),
'next_2_years' => __('filament-query-builder::query-builder.operators.date.presets.next_2_years'),
'next_5_years' => __('filament-query-builder::query-builder.operators.date.presets.next_5_years'),
'next_decade' => __('filament-query-builder::query-builder.operators.date.presets.next_decade'),
'custom' => __('filament-query-builder::query-builder.operators.date.presets.custom'),
];
return $options;
}
public function apply(Builder $query, string $qualifiedColumn): Builder
{
return $query->whereDate($qualifiedColumn, $this->isInverse() ? '>' : '<=', $this->getSettings()['date']);
$settings = $this->getSettings();
$constraint = $this->getConstraint();
$hasTime = $constraint instanceof DateConstraint && $constraint->hasTime();
// Check if using relative mode - only if mode is explicitly set to 'relative'
if (($settings['mode'] ?? null) === 'relative') {
$dateTime = $this->resolveRelativeDate($settings, $hasTime);
if ($hasTime && $this->isTimeBasedFilter($settings)) {
return $query->where($qualifiedColumn, $this->isInverse() ? '>' : '<=', $dateTime);
}
return $query->whereDate($qualifiedColumn, $this->isInverse() ? '>' : '<=', $dateTime);
}
return $query->whereDate($qualifiedColumn, $this->isInverse() ? '>' : '<=', $settings['date']);
}
/**
* @param array<string, mixed> $settings
*/
protected function isTimeBasedFilter(array $settings): bool
{
$preset = $settings['preset'] ?? 'custom';
if (in_array($preset, ['this_minute', 'this_hour', 'past_minute', 'past_hour', 'next_minute', 'next_hour'], true)) {
return true;
}
if ($preset === 'custom') {
$unit = $settings['relative_unit'] ?? DateUnit::Day->value;
return in_array($unit, [DateUnit::Second->value, DateUnit::Minute->value, DateUnit::Hour->value], true);
}
return false;
}
/**
* @param array<string, mixed> $settings
*/
protected function resolveRelativeDate(array $settings, bool $hasTime = false): string
{
$preset = $settings['preset'] ?? 'custom';
return match ($preset) {
'past_decade' => Carbon::now()->subYears(10)->toDateString(),
'past_5_years' => Carbon::now()->subYears(5)->toDateString(),
'past_2_years' => Carbon::now()->subYears(2)->toDateString(),
'past_year' => Carbon::now()->subYear()->toDateString(),
'past_6_months' => Carbon::now()->subMonths(6)->toDateString(),
'past_quarter' => Carbon::now()->subQuarter()->toDateString(),
'past_month' => Carbon::now()->subMonth()->toDateString(),
'past_2_weeks' => Carbon::now()->subWeeks(2)->toDateString(),
'past_week' => Carbon::now()->subWeek()->toDateString(),
'past_hour' => Carbon::now()->subHour()->toDateTimeString(),
'past_minute' => Carbon::now()->subMinute()->toDateTimeString(),
'this_decade' => Carbon::now()->startOfDecade()->toDateString(),
'this_year' => Carbon::now()->startOfYear()->toDateString(),
'this_quarter' => Carbon::now()->startOfQuarter()->toDateString(),
'this_month' => Carbon::now()->startOfMonth()->toDateString(),
'today' => Carbon::today()->toDateString(),
'this_hour' => Carbon::now()->startOfHour()->toDateTimeString(),
'this_minute' => Carbon::now()->startOfMinute()->toDateTimeString(),
'next_minute' => Carbon::now()->addMinute()->toDateTimeString(),
'next_hour' => Carbon::now()->addHour()->toDateTimeString(),
'next_week' => Carbon::now()->addWeek()->toDateString(),
'next_2_weeks' => Carbon::now()->addWeeks(2)->toDateString(),
'next_month' => Carbon::now()->addMonth()->toDateString(),
'next_quarter' => Carbon::now()->addQuarter()->toDateString(),
'next_6_months' => Carbon::now()->addMonths(6)->toDateString(),
'next_year' => Carbon::now()->addYear()->toDateString(),
'next_2_years' => Carbon::now()->addYears(2)->toDateString(),
'next_5_years' => Carbon::now()->addYears(5)->toDateString(),
'next_decade' => Carbon::now()->addYears(10)->toDateString(),
'custom' => $this->resolveCustomRelativeDate($settings, $hasTime),
default => Carbon::today()->toDateString(),
};
}
/**
* @param array<string, mixed> $settings
*/
protected function resolveCustomRelativeDate(array $settings, bool $hasTime = false): string
{
$value = (int) ($settings['relative_value'] ?? 1);
$unit = $settings['relative_unit'] ?? DateUnit::Day->value;
$tense = $settings['tense'] ?? 'past';
$method = $tense === 'future' ? 'add' : 'sub';
return match ($unit) {
DateUnit::Second->value => Carbon::now()->{$method . 'Seconds'}($value)->toDateTimeString(),
DateUnit::Minute->value => Carbon::now()->{$method . 'Minutes'}($value)->toDateTimeString(),
DateUnit::Hour->value => Carbon::now()->{$method . 'Hours'}($value)->toDateTimeString(),
DateUnit::Day->value => Carbon::now()->{$method . 'Days'}($value)->toDateString(),
DateUnit::Week->value => Carbon::now()->{$method . 'Weeks'}($value)->toDateString(),
DateUnit::Month->value => Carbon::now()->{$method . 'Months'}($value)->toDateString(),
DateUnit::Quarter->value => Carbon::now()->{$method . 'Quarters'}($value)->toDateString(),
DateUnit::Year->value => Carbon::now()->{$method . 'Years'}($value)->toDateString(),
default => Carbon::today()->toDateString(),
};
}
}
@@ -179,6 +179,17 @@ By default, the following operators are available:
- Is year - filters a column to be in the same year as the searched year
- Is not year - filters a column to not be in the same year as the searched year
#### Datetime constraints
By default, date constraints only filter by date. If you have a datetime column and want to enable time-based filtering, you can use the `time()` method:
```php
use Filament\QueryBuilder\Constraints\DateConstraint;
DateConstraint::make('published_at')
->time()
```
### Select constraints
Select constraints allow you to filter fields using a select field. They can be used to filter any field, including via relationships.
@@ -2,6 +2,7 @@
namespace App\Filament\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
@@ -39,6 +40,7 @@ class PostForm
->columnSpanFull(),
Select::make('string_backed_enum')
->options(StringBackedEnum::class),
DateTimePicker::make('published_at'),
]);
}
}
@@ -4,6 +4,7 @@ namespace App\Livewire;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
@@ -55,6 +56,7 @@ class CreatePostWithFields extends Component implements HasActions, HasSchemas
->columnSpanFull(),
Select::make('string_backed_enum')
->options(StringBackedEnum::class),
DateTimePicker::make('published_at'),
])
->statePath('data')
->model(Post::class);
@@ -47,6 +47,7 @@ class PostResource extends Resource
->columnSpanFull(),
Forms\Components\Select::make('string_backed_enum')
->options(StringBackedEnum::class),
Forms\Components\DateTimePicker::make('published_at'),
]);
}
@@ -68,6 +69,9 @@ class PostResource extends Resource
Tables\Columns\TextColumn::make('string_backed_enum')
->badge()
->searchable(),
Tables\Columns\TextColumn::make('published_at')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -11,6 +11,7 @@ use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
@@ -59,6 +60,7 @@ class PostResource extends Resource
->columnSpanFull(),
Select::make('string_backed_enum')
->options(StringBackedEnum::class),
DateTimePicker::make('published_at'),
]);
}
@@ -94,6 +96,9 @@ class PostResource extends Resource
TextEntry::make('string_backed_enum')
->badge()
->placeholder('-'),
TextEntry::make('published_at')
->dateTime()
->placeholder('-'),
TextEntry::make('created_at')
->dateTime()
->placeholder('-'),
@@ -124,6 +129,9 @@ class PostResource extends Resource
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
TextColumn::make('published_at')
->dateTime()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -2,6 +2,7 @@
namespace App\Filament\Resources\Posts\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
@@ -38,6 +39,7 @@ class PostForm
->columnSpanFull(),
Select::make('string_backed_enum')
->options(StringBackedEnum::class),
DateTimePicker::make('published_at'),
]);
}
}
@@ -41,6 +41,9 @@ class PostInfolist
TextEntry::make('string_backed_enum')
->badge()
->placeholder('-'),
TextEntry::make('published_at')
->dateTime()
->placeholder('-'),
TextEntry::make('created_at')
->dateTime()
->placeholder('-'),
@@ -29,6 +29,9 @@ class PostsTable
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
TextColumn::make('published_at')
->dateTime()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -42,6 +42,9 @@ class ListPostsWithColumns extends Component implements HasActions, HasSchemas,
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
TextColumn::make('published_at')
->dateTime()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -42,6 +42,9 @@ class ListPostsWithColumns extends Component implements HasActions, HasSchemas,
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
TextColumn::make('published_at')
->dateTime()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -21,6 +21,7 @@ return new class extends Migration
$table->json('json')->nullable();
$table->json('json_array_of_objects')->nullable();
$table->string('string_backed_enum')->nullable();
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->softDeletes();
});
@@ -52,6 +52,8 @@ class PostsQueryBuilderTable extends Component implements HasActions, HasSchemas
->attribute('rating')
->integer(),
DateConstraint::make('created_at'),
DateConstraint::make('published_at')
->time(),
SelectConstraint::make('rating_select')
->attribute('rating')
->options([
File diff suppressed because it is too large Load Diff