mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
+241
-6
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+241
-6
@@ -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
@@ -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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
+4
@@ -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()
|
||||
|
||||
+8
@@ -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
@@ -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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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('-'),
|
||||
|
||||
+3
@@ -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()
|
||||
|
||||
+3
@@ -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()
|
||||
|
||||
+3
@@ -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
Reference in New Issue
Block a user