@@ -110,7 +110,7 @@ class="sr-only ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">
You have selected {{ count($selectedRows) }} {{ Str::of('row')->plural(count($selectedRows)) }} . Do you want to select all {{ $this->rows->total() }}?
-
Select all
@@ -142,6 +142,7 @@ class="ml-2 text-blue-500 hover:text-blue-600">
diff --git a/src/GroupsBuilder.php b/src/GroupsBuilder.php
new file mode 100755
index 0000000..0574add
--- /dev/null
+++ b/src/GroupsBuilder.php
@@ -0,0 +1,21 @@
+orderBy($this->sortBy, $this->sortDirection);
});
+ $query->when($this->sortBy !== '', function ($query) {
+ if (Str::contains($this->sortBy, '.')) {
+ // If it's a relationship, use orderByRelated
+ $this->orderByRelated($query, $this->sortBy, $this->sortDirection);
+ } else {
+ // Otherwise, use regular orderBy
+ $query->orderBy($this->sortBy, $this->sortDirection);
+ }
+ });
+
return $query;
}
diff --git a/src/Support/Concerns/WithFilters.php b/src/Support/Concerns/WithFilters.php
index bf06312..dedc2e0 100644
--- a/src/Support/Concerns/WithFilters.php
+++ b/src/Support/Concerns/WithFilters.php
@@ -2,12 +2,13 @@
namespace ACTTraining\QueryBuilder\Support\Concerns;
-use Livewire\Attributes\Session;
+use Livewire\Attributes\Url;
trait WithFilters
{
protected bool $displayFilters = false;
+ #[Url(as: 'f')]
public array $filterValues = [];
public function areFiltersAvailable(): bool
diff --git a/src/Support/Concerns/WithGroupBuilder.php b/src/Support/Concerns/WithGroupBuilder.php
new file mode 100644
index 0000000..dfc60a4
--- /dev/null
+++ b/src/Support/Concerns/WithGroupBuilder.php
@@ -0,0 +1,166 @@
+findElementByKey($value, $targetValue);
+ if ($result !== null) {
+ return $result;
+ }
+ }
+ }
+
+ return null; // Return null if no match is found
+ }
+
+ public function updatedGroupBy(): void
+ {
+ $this->resetPage();
+ $this->dispatch('refreshTable')->self();
+ }
+
+ public function availableColumns(): array
+ {
+ return [];
+ }
+
+ public function configuredColumns(): array
+ {
+ $columns = [];
+
+ foreach ($this->selectedColumns as $column) {
+ $columns[] = $this->findElementByKey($this->availableColumns(), $column);
+ }
+
+ return $columns;
+ }
+
+ public function buildColumns(): array
+ {
+ $columns = [];
+
+ $counter = 0;
+
+ foreach ($this->configuredColumns() as $column) {
+ $columnToAdd = match ($column['type'] ?? null) {
+ 'number' => Column::make($column['label'], $column['key'])->justify('right'),
+ 'boolean' => BooleanColumn::make($column['label'], $column['key'])->justify('center')->hideIf(false),
+ 'date' => DateColumn::make($column['label'], $column['key'])->format(config('settings.date.short-format'))->justify('right'),
+ 'view' => ViewColumn::make($column['label'])->justify($column['justify'] ?? 'left'),
+ default => Column::make($column['label'], $column['key'])
+ };
+
+ if ($column['view'] ?? false) {
+ $columnToAdd->component($column['view']);
+ } elseif ($counter === 0) {
+ $columnToAdd->component('columns.common.title');
+ }
+
+ if ($column['sortable'] ?? false) {
+ $columnToAdd->sortable();
+ }
+
+ if ($column['justify'] ?? false) {
+ $columnToAdd->justify($column['justify']);
+ }
+
+ $columns[] = $columnToAdd;
+
+ $counter++;
+ }
+
+ return $columns;
+ }
+
+ public function buildConditions(): array
+ {
+ $conditions = [];
+
+ foreach ($this->configuredColumns() as $column) {
+ if ($column['skipCondition'] ?? false) {
+ continue;
+ }
+
+ $conditions[] = match ($column['type'] ?? null) {
+ 'number' => NumberCondition::make($column['label'], $column['key']),
+ 'enum' => EnumCondition::make($column['label'], $column['key'], $column['options'] ?? []),
+ 'float' => FloatCondition::make($column['label'], $column['key']),
+ 'boolean' => BooleanCondition::make($column['label'], $column['key']),
+ 'date' => DateCondition::make($column['label'], $column['key']),
+ default => TextCondition::make($column['label'], $column['key'])
+ };
+
+ }
+
+ return $conditions;
+ }
+
+ public function removeCriteria($index): void
+ {
+ unset($this->criteria[$index]);
+ $this->criteria = array_values($this->criteria);
+
+ $this->saveToSession();
+ }
+
+ public function resetReportBuilder(): void
+ {
+ $this->criteria = [];
+ $this->selectedColumns = [];
+ $this->saveToSession();
+ }
+
+ public function exportReportBuilder(): Response|BinaryFileResponse|null
+ {
+ return null;
+ }
+
+ public function saveReportBuilder(): void
+ {
+ //
+ }
+
+ public function loadReportBuilder(): void
+ {
+ //
+ }
+
+ public function saveToSession(): void
+ {
+ //
+ }
+}
diff --git a/src/Support/Concerns/WithPagination.php b/src/Support/Concerns/WithPagination.php
index 825dc9b..2f83ae0 100644
--- a/src/Support/Concerns/WithPagination.php
+++ b/src/Support/Concerns/WithPagination.php
@@ -12,7 +12,7 @@ trait WithPagination
private bool $paginate = true;
- private bool|string $scrollTo = false; //false disables scroll on pagination
+ private bool|string $scrollTo = false; // false disables scroll on pagination
public function usePagination($usePagination = true): static
{
diff --git a/src/Support/Concerns/WithReportBuilder.php b/src/Support/Concerns/WithReportBuilder.php
index ef384a9..6b465c3 100644
--- a/src/Support/Concerns/WithReportBuilder.php
+++ b/src/Support/Concerns/WithReportBuilder.php
@@ -5,10 +5,12 @@
use ACTTraining\QueryBuilder\Support\Columns\BooleanColumn;
use ACTTraining\QueryBuilder\Support\Columns\Column;
use ACTTraining\QueryBuilder\Support\Columns\DateColumn;
+use ACTTraining\QueryBuilder\Support\Columns\ViewColumn;
use ACTTraining\QueryBuilder\Support\Conditions\BooleanCondition;
use ACTTraining\QueryBuilder\Support\Conditions\DateCondition;
use ACTTraining\QueryBuilder\Support\Conditions\EnumCondition;
use ACTTraining\QueryBuilder\Support\Conditions\FloatCondition;
+use ACTTraining\QueryBuilder\Support\Conditions\NullCondition;
use ACTTraining\QueryBuilder\Support\Conditions\NumberCondition;
use ACTTraining\QueryBuilder\Support\Conditions\TextCondition;
use Illuminate\Http\Response;
@@ -66,33 +68,41 @@ public function configuredColumns(): array
public function buildColumns(): array
{
$columns = [];
-
$counter = 0;
- foreach ($this->configuredColumns() as $column) {
- $columnToAdd = match ($column['type'] ?? null) {
+ foreach (($this->configuredColumns() ?? []) as $column) {
+ // Skip nulls/invalid items early
+ if (! is_array($column) || ! isset($column['label'], $column['key'])) {
+ continue;
+ }
+
+ $type = $column['type'] ?? null;
+
+ $columnToAdd = match ($type) {
'number' => Column::make($column['label'], $column['key'])->justify('right'),
'boolean' => BooleanColumn::make($column['label'], $column['key'])->justify('center')->hideIf(false),
- 'date' => DateColumn::make($column['label'], $column['key'])->format(config('settings.date.short-format'))->justify('right'),
- default => Column::make($column['label'], $column['key'])
+ 'date' => DateColumn::make($column['label'], $column['key'])
+ ->format(config('settings.date.short-format'))
+ ->justify('right'),
+ 'view' => ViewColumn::make($column['label']),
+ default => Column::make($column['label'], $column['key']),
};
- if ($column['view'] ?? false) {
+ if (! empty($column['view'])) {
$columnToAdd->component($column['view']);
} elseif ($counter === 0) {
$columnToAdd->component('columns.common.title');
}
- if ($column['sortable'] ?? false) {
+ if (! empty($column['sortable'])) {
$columnToAdd->sortable();
}
- if ($column['justify'] ?? false) {
+ if (! empty($column['justify'])) {
$columnToAdd->justify($column['justify']);
}
$columns[] = $columnToAdd;
-
$counter++;
}
@@ -103,20 +113,33 @@ public function buildConditions(): array
{
$conditions = [];
- foreach ($this->configuredColumns() as $column) {
- if ($column['skipCondition'] ?? false) {
+ foreach (($this->configuredColumns() ?? []) as $column) {
+ // Skip nulls/garbage early
+ if (! is_array($column) || ! isset($column['label'], $column['key'])) {
continue;
}
- $conditions[] = match ($column['type'] ?? null) {
+ if (! empty($column['skipCondition'])) {
+ continue;
+ }
+
+ $type = $column['type'] ?? null;
+
+ // Normalise enum options
+ $options = $column['options'] ?? [];
+ if (! is_array($options)) {
+ $options = [];
+ }
+
+ $conditions[] = match ($type) {
'number' => NumberCondition::make($column['label'], $column['key']),
- 'enum' => EnumCondition::make($column['label'], $column['key'], $column['options'] ?? []),
+ 'enum' => EnumCondition::make($column['label'], $column['key'], $options),
'float' => FloatCondition::make($column['label'], $column['key']),
'boolean' => BooleanCondition::make($column['label'], $column['key']),
'date' => DateCondition::make($column['label'], $column['key']),
- default => TextCondition::make($column['label'], $column['key'])
+ 'null' => NullCondition::make($column['label'], $column['key']),
+ default => TextCondition::make($column['label'], $column['key']),
};
-
}
return $conditions;
diff --git a/src/Support/Concerns/WithSearch.php b/src/Support/Concerns/WithSearch.php
index b0555e0..10791da 100644
--- a/src/Support/Concerns/WithSearch.php
+++ b/src/Support/Concerns/WithSearch.php
@@ -2,7 +2,7 @@
namespace ACTTraining\QueryBuilder\Support\Concerns;
-use Livewire\Attributes\Session;
+use Livewire\Attributes\Url;
trait WithSearch
{
@@ -12,6 +12,7 @@ trait WithSearch
protected bool $displaySearchableIcon = true;
+ #[Url(as: 's')]
public string $searchBy = '';
protected array $additionalSearchables = [];
diff --git a/src/Support/Concerns/WithSelecting.php b/src/Support/Concerns/WithSelecting.php
index ce1ef62..e632d96 100644
--- a/src/Support/Concerns/WithSelecting.php
+++ b/src/Support/Concerns/WithSelecting.php
@@ -23,17 +23,24 @@ public function updatedSelectPage($value): void
}
$this->dispatch('refreshTable')->self();
+
}
public function updatedSelectedRows(): void
{
$this->selectAll = false;
$this->selectPage = false;
+ $this->dispatch('refreshTable')->self();
}
- public function selectAll(): void
+ public function toggleSelectAll(): void
{
$this->selectAll = ! $this->selectAll;
+
+ $this->selectedRows = $this->selectAll
+ ? $this->rowsQuery->pluck('id')->toArray()
+ : [];
+ $this->dispatch('refreshTable')->self();
}
public function clearSelection(): void
@@ -41,5 +48,7 @@ public function clearSelection(): void
$this->selectAll = false;
$this->selectPage = false;
$this->selectedRows = [];
+
+ $this->dispatch('refreshTable')->self();
}
}
diff --git a/src/Support/Concerns/WithSorting.php b/src/Support/Concerns/WithSorting.php
index 4bd96d0..03e127d 100644
--- a/src/Support/Concerns/WithSorting.php
+++ b/src/Support/Concerns/WithSorting.php
@@ -2,6 +2,9 @@
namespace ACTTraining\QueryBuilder\Support\Concerns;
+use Illuminate\Database\Eloquent\Builder;
+use InvalidArgumentException;
+
trait WithSorting
{
use WithPagination;
@@ -10,7 +13,7 @@ trait WithSorting
public string $sortDirection = 'asc';
- protected $sortable = false;
+ protected bool $sortable = false;
public function isSortable(): bool
{
@@ -24,13 +27,32 @@ public function sortable(bool $sortable = true): static
return $this;
}
+ public function setSort($key, $direction = 'asc'): static
+ {
+ $this->sortBy = $key;
+ $this->sortDirection = $direction;
+
+ return $this;
+ }
+
public function sort($key): void
{
$this->resetPage();
if ($this->sortBy === $key) {
- $direction = $this->sortDirection === 'asc' ? 'desc' : 'asc';
- $this->sortDirection = $direction;
+
+ $direction = match ($this->sortDirection) {
+ 'asc' => 'desc',
+ 'desc' , => null,
+ default => 'asc',
+ };
+
+ if ($direction === null) {
+ $this->sortDirection = 'asc';
+ $this->sortBy = '';
+ } else {
+ $this->sortDirection = $direction;
+ }
return;
}
@@ -38,4 +60,26 @@ public function sort($key): void
$this->sortBy = $key;
$this->sortDirection = 'asc';
}
+
+ protected function orderByRelated(Builder $query, string $relationColumn, string $direction = 'asc'): void
+ {
+ // Ensure only one dot is present
+ if (substr_count($relationColumn, '.') !== 1) {
+ throw new InvalidArgumentException("Invalid relation column: '{$relationColumn}'. Only single-level relationships are supported.");
+ }
+
+ [$relation, $column] = explode('.', $relationColumn, 2);
+
+ // Get the related model instance correctly
+ $relatedModel = $query->getModel()->{$relation}()->getRelated();
+
+ $query->orderBy(
+ $relatedModel::select($column)
+ ->whereColumn(
+ "{$relatedModel->getTable()}.id",
+ $query->getModel()->{$relation}()->getForeignKeyName()
+ ),
+ $direction
+ );
+ }
}
diff --git a/src/Support/Conditions/NullCondition.php b/src/Support/Conditions/NullCondition.php
new file mode 100644
index 0000000..3a46202
--- /dev/null
+++ b/src/Support/Conditions/NullCondition.php
@@ -0,0 +1,16 @@
+ 'is set',
+ 'is_not_set' => 'is not set',
+ ];
+ }
+}
diff --git a/src/Support/Criteria/DateCriteria.php b/src/Support/Criteria/DateCriteria.php
index 08f3f99..dbe3fbb 100644
--- a/src/Support/Criteria/DateCriteria.php
+++ b/src/Support/Criteria/DateCriteria.php
@@ -17,23 +17,8 @@ class DateCriteria extends BaseCriteria implements CriteriaInterface
public function __construct(string $field, string|array $value, string $operator = '=')
{
- // Check if the date is in the 'd/m/Y' format and convert it to 'd-m-Y'
- $date = \DateTime::createFromFormat('d/m/Y', $field);
-
- if ($date) {
- // If the date is successfully parsed in the 'd/m/Y' format, convert it to 'd-m-Y'
- $this->field = $date->format('d-m-Y');
- } else {
- // If the date was not in 'd/m/Y', assume it's already in 'd-m-Y' format
- // Optionally, you could check again if it's in the correct format
- $date = \DateTime::createFromFormat('d-m-Y', $field);
-
- if ($date) {
- $this->field = $field; // Already in 'd-m-Y', so just assign it
- }
- }
-
- $this->value = $value;
+ $this->field = $field;
+ $this->value = Carbon::createFromFormat('Y-m-d', $value)->format('d-m-Y');
$this->operator = $operator;
}
diff --git a/src/Support/Criteria/NullCriteria.php b/src/Support/Criteria/NullCriteria.php
index 5140532..0faa6d6 100644
--- a/src/Support/Criteria/NullCriteria.php
+++ b/src/Support/Criteria/NullCriteria.php
@@ -8,9 +8,9 @@ class NullCriteria extends BaseCriteria implements CriteriaInterface
{
public string $inputType = 'boolean';
- private $field;
+ private string $field;
- private $operation;
+ private string $operation;
public function __construct(string $field, string $operation)
{
@@ -22,9 +22,11 @@ public function apply($query): void
{
$this->applyWhereCondition($query, $this->field, function ($query, $field) {
if ($this->operation === 'is_not_set') {
- $query->whereNull($field);
+ $query->whereNull($field)
+ ->orWhere($field, '');
} else {
- $query->whereNotNull($field);
+ $query->whereNotNull($field)
+ ->orWhere($field, '!=', '');
}
});
}
diff --git a/src/Support/Filters/NullFilter.php b/src/Support/Filters/NullFilter.php
new file mode 100644
index 0000000..15f2998
--- /dev/null
+++ b/src/Support/Filters/NullFilter.php
@@ -0,0 +1,43 @@
+ 'Is set',
+ 'isNotSet' => 'Is not set',
+ ];
+ }
+
+ public function prompt(): string
+ {
+ return $this->prompt;
+ }
+
+ public function apply($query, $value)
+ {
+ $key = $this->key();
+ $isNegation = $value === 'isNotSet';
+
+ return $query->where(function ($q) use ($key, $isNegation) {
+ if ($isNegation) {
+ $q->whereNull($key)
+ ->orWhere($key, '')
+ ->orWhereJsonLength($key, 0);
+ } else {
+ $q->whereNotNull($key)
+ ->where($key, '!=', '')
+ ->whereJsonLength($key, '>', 0);
+ }
+ });
+ }
+}
diff --git a/src/Support/Filters/SelectFilter.php b/src/Support/Filters/SelectFilter.php
index 15e5473..f14f96e 100644
--- a/src/Support/Filters/SelectFilter.php
+++ b/src/Support/Filters/SelectFilter.php
@@ -4,20 +4,20 @@
class SelectFilter extends BaseFilter
{
- public $options = [];
+ public array $options = [];
- public $optionsInGroups = [];
+ public array $optionsInGroups = [];
public string $component = 'select';
private string $prompt = 'Select an option';
- public function options()
+ public function options(): array
{
return $this->options;
}
- public function optionsInGroups()
+ public function optionsInGroups(): array
{
return $this->optionsInGroups;
}
diff --git a/src/TableBuilder.php b/src/TableBuilder.php
index b3d33ea..6b5cf8b 100755
--- a/src/TableBuilder.php
+++ b/src/TableBuilder.php
@@ -17,6 +17,7 @@
use ACTTraining\QueryBuilder\Support\Concerns\WithSelecting;
use ACTTraining\QueryBuilder\Support\Concerns\WithSorting;
use ACTTraining\QueryBuilder\Support\Concerns\WithViews;
+use ACTTraining\QueryBuilder\Support\Filters\NullFilter;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
@@ -78,7 +79,13 @@ public function config(): void
public function getRowsQueryProperty()
{
$query = $this->query()->when($this->sortBy !== '', function ($query) {
- $query->orderBy($this->sortBy, $this->sortDirection);
+ if (Str::contains($this->sortBy, '.')) {
+ // If it's a relationship, use orderByRelated
+ $this->orderByRelated($query, $this->sortBy, $this->sortDirection);
+ } else {
+ // Otherwise, use regular orderBy
+ $query->orderBy($this->sortBy, $this->sortDirection);
+ }
});
if ($this->searchBy && $this->searchBy !== '') {
@@ -111,13 +118,55 @@ public function getRowsQueryProperty()
$value = $dottedFilterValue[$filter->code()] ?? null;
$query->when($value !== null, function ($query) use ($filter, $value) {
+ // Special case for NullFilter with isSet / isNotSet values
+ if ($filter instanceof NullFilter && in_array($value, ['isSet', 'isNotSet'])) {
+ $isNegation = $value === 'isNotSet';
+
+ if (Str::contains($filter->key(), '.')) {
+ $relation = Str::beforeLast($filter->key(), '.');
+ $column = Str::afterLast($filter->key(), '.');
+
+ $query->whereHas($relation, function ($q) use ($column, $isNegation) {
+ $q->where(function ($q) use ($column, $isNegation) {
+ if ($isNegation) {
+ $q->whereNull($column)
+ ->orWhere($column, '')
+ ->orWhereJsonLength($column, 0);
+ } else {
+ $q->whereNotNull($column)
+ ->where($column, '!=', '')
+ ->whereJsonLength($column, '>', 0);
+ }
+ });
+ });
+ } else {
+ $query->where(function ($q) use ($filter, $isNegation) {
+ $key = $filter->key();
+
+ if ($isNegation) {
+ $q->whereNull($key)
+ ->orWhere($key, '')
+ ->orWhereJsonLength($key, 0);
+ } else {
+ $q->whereNotNull($key)
+ ->where($key, '!=', '')
+ ->whereJsonLength($key, '>', 0);
+ }
+ });
+ }
+
+ return;
+ }
+
+ // Normal filters
$value = $filter->parseValue($value);
+
if (Str::contains($filter->key(), '.')) {
$relation = Str::beforeLast($filter->key(), '.');
$column = Str::afterLast($filter->key(), '.');
- $query->whereHas($relation, function ($query) use ($filter, $value, $column) {
- $query->where($column, $filter->operator(), $value);
+ $query->whereHas($relation, function ($q) use ($filter, $value, $column) {
+ $q->where($column, $filter->operator(), $value);
});
} else {
$query->where($filter->key(), $filter->operator(), $value);