diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2027cce..dc9c5aa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,12 @@ "Bash(vendor/bin/pest:*)", "Bash(vendor/bin/pint:*)", "Bash(git push:*)", - "Bash(gh pr:*)" + "Bash(gh pr:*)", + "Bash(composer analyse:*)", + "Bash(composer test:*)", + "Bash(composer format:*)", + "Bash(git stash:*)", + "Bash(git commit:*)" ] } } diff --git a/README.md b/README.md index c995a34..6080b5c 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,62 @@ -# A drop in query builder for Laravel models. +# Query Builder for Laravel -Add a query builder and report table for youir models. +Drop-in Livewire components for building query builders, tables, and reports on Eloquent models. ## Installation -You can install the package via composer: - ```bash composer require act-training/query-builder ``` -Optionally, you can publish the views using +Optionally, publish the views: ```bash php artisan vendor:publish --tag="query-builder-views" ``` -## Usage -Models should use the AppliesCriteria trait. +## Model Setup + +Models should use the `AppliesCriteria` trait: ```php -class User extends Authenticatable +use ACTTraining\QueryBuilder\Support\Criteria\AppliesCriteria; + +class Employee extends Model { use AppliesCriteria; - - ... - } ``` -Create a Livewire component and make sure it extends QueryBuilder. +## Components + +The package provides three main abstract Livewire components to extend: + +| Component | Purpose | +|---|---| +| `QueryBuilder` | Full query builder with criteria-based AND/OR filtering | +| `TableBuilder` | Simpler table with URL-persisted filters and search | +| `ReportBuilder` | Dynamic column selection, save/load/export, optional groupBy | + +### QueryBuilder + +Create a Livewire component that extends `QueryBuilder`. Define `query()`, `columns()`, and `conditions()`: ```php rowClickable(false); } + public function query(): Builder + { + return Employee::query()->with(['contract', 'contract.job', 'contract.location']); + } + public function columns(): array { return [ @@ -74,20 +97,218 @@ class EmployeesReport extends QueryBuilder DateCondition::make('Start Date', 'contract.start_date'), ]; } +} +``` + +### TableBuilder + +A simpler alternative using filters instead of criteria. Define `query()`, `columns()`, and `filters()`: + +```php +sortable()->searchable(), + Column::make('Department', 'department')->sortable(), + ]; + } + + public function filters(): array + { + return [ + TextFilter::make('Name', 'full_name'), + SelectFilter::make('Department', 'department') + ->options(['HR' => 'HR', 'IT' => 'IT', 'Finance' => 'Finance']), + ]; + } +} +``` + +### ReportBuilder + +Extends `QueryBuilder` with dynamic column selection. Users pick columns at runtime from `availableColumns()`, and the component builds columns and conditions automatically: + +```php +with(['contract', 'contract.job', 'contract.location']); } - public function rowClick($row): void + public function availableColumns(): array { - $this->dispatchBrowserEvent('notify', ['content' => 'The row was clicked', 'type' => 'success']); + return [ + 'Employee' => [ + ['label' => 'Name', 'key' => 'full_name'], + ['label' => 'Email', 'key' => 'email'], + ], + 'Contract' => [ + ['label' => 'Job Title', 'key' => 'contract.job.name'], + ['label' => 'Location', 'key' => 'contract.location.name'], + ['label' => 'Start Date', 'key' => 'contract.start_date', 'type' => 'date'], + ['label' => 'Salary', 'key' => 'contract.salary', 'type' => 'number'], + ['label' => 'Line Manager', 'key' => 'contract.line_manager', 'type' => 'boolean'], + ], + ]; + } +} +``` + +Column definitions in `availableColumns()` support these keys: + +| Key | Description | +|---|---| +| `label` | Display label (required) | +| `key` | Column key, supports dot-notation for relationships (required) | +| `type` | Column type: `text` (default), `number`, `float`, `boolean`, `date`, `enum`, `null`, `view` | +| `sortable` | Enable sorting on this column | +| `justify` | Alignment: `left`, `center`, `right` | +| `view` | Custom Blade component for rendering | +| `options` | Options array for `enum` type | +| `skipCondition` | Exclude from query builder conditions | + +### ReportBuilder with GroupBy + +Enable groupBy to allow users to group results by a column with aggregate functions (COUNT, SUM, AVG, MIN, MAX). Set `$enableGroupBy = true`: + +```php +with(['contract', 'contract.job', 'contract.location']); } + public function availableColumns(): array + { + return [ + 'Employee' => [ + ['label' => 'Name', 'key' => 'full_name'], + ['label' => 'Department', 'key' => 'department'], + ], + 'Contract' => [ + ['label' => 'Job Title', 'key' => 'contract.job.name'], + ['label' => 'Location', 'key' => 'contract.location.name'], + ['label' => 'Salary', 'key' => 'contract.salary', 'type' => 'number'], + ], + ]; + } } ``` +When `enableGroupBy` is true, the report editor UI shows additional controls: + +- **Group By Column** - select which column to group by +- **Function** - aggregate function (COUNT, SUM, AVG, MIN, MAX) +- **Aggregate Column** - which column to aggregate (defaults to `id`, numeric columns from `availableColumns()` are included automatically) + +An "Aggregate" column is automatically appended to the table when grouping is active. + +You can customise the available aggregate functions and groupable/aggregatable columns by overriding: + +```php +public function aggregateFunctions(): array +{ + return ['COUNT', 'SUM', 'AVG']; +} + +public function groupableColumns(): array +{ + // Return a flat array of ['label' => ..., 'key' => ...] items + return [ + ['label' => 'Department', 'key' => 'department'], + ['label' => 'Location', 'key' => 'contract.location.name'], + ]; +} + +public function aggregatableColumns(): array +{ + return [ + ['label' => 'ID', 'key' => 'id'], + ['label' => 'Salary', 'key' => 'contract.salary'], + ]; +} +``` + +## Columns + +All column types use a fluent `make($label, $key)` constructor. If `$key` is omitted, it defaults to `Str::snake($label)`. + +| Column Type | Description | +|---|---| +| `Column` | General text column | +| `BooleanColumn` | Boolean/checkbox display | +| `DateColumn` | Date formatting with `->format()` and `->humanDiff()` | +| `ViewColumn` | Custom Blade view rendering | + +Columns support: `->sortable()`, `->searchable()`, `->justify('right')`, `->component('view.name')`, `->reformatUsing(callable)`, `->withSubTitle(callable)`, `->hideIf(condition)`, `->hideHeader()`. + +## Conditions (QueryBuilder) + +Used with `QueryBuilder` and `ReportBuilder` for criteria-based filtering: + +| Condition Type | Operations | +|---|---| +| `TextCondition` | equals, not_equals, contains, starts_with, ends_with | +| `NumberCondition` | equals, not_equals, greater_than, less_than, is_between | +| `FloatCondition` | equals, not_equals, greater_than, less_than, is_between | +| `BooleanCondition` | is_true, is_false | +| `DateCondition` | equals, before, after, is_between | +| `EnumCondition` | equals, not_equals (with defined options) | +| `NullCondition` | is_set, is_not_set | + +## Filters (TableBuilder) + +Used with `TableBuilder` for simpler key/operator/value filtering: + +`TextFilter`, `NumberFilter`, `DateFilter`, `BooleanFilter`, `SelectFilter`, `NullFilter` + +## Relationship Support + +Dot-notation keys work throughout columns, conditions, and filters to traverse Eloquent relationships: + +```php +Column::make('Job Title', 'contract.job.name') +TextCondition::make('Location', 'contract.location.name') +``` + ## Testing ```bash diff --git a/resources/views/group-table.blade.php b/resources/views/group-table.blade.php index 5959b7c..efc313e 100644 --- a/resources/views/group-table.blade.php +++ b/resources/views/group-table.blade.php @@ -1,220 +1,2 @@ -
|
-
-
-
-
- |
- @endif
-
- @foreach ($this->columns() as $column)
- @if(in_array($column->key, $displayColumns))
- isSortable()) wire:click="sort('{{ $column->key }}')" @endif>
- @if ($column->showHeader)
- justify,
- 'cursor-pointer' => $column->isSortable(),
- ])>
- {{ $column->label }}
-
- @if ($sortBy === $column->key)
- @if ($sortDirection === 'desc')
-
- @else
-
- @endif
- @endif
-
- @if($this->isSearchableIconVisible() && $column->isSearchable())
-
- @endif
- |
- @endif
- @endforeach
- ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
- @unless($selectAll)
-
- You have selected {{ count($selectedRows) }} {{ Str::of('row')->plural(count($selectedRows)) }}. Do you want to select all {{ $this->rows->total() }}?
-
-
- @else
- You have selected all {{ $this->rows->total() }} {{ Str::of('row')->plural(count($selectedRows)) }}.
- @endif
- |
- |||||||||||||||||||
|
-
-
-
-
- |
- @endif
-
- @foreach ($this->columns() as $column)
- @if(in_array($column->key, $displayColumns))
-
-
-
- |
- @endif
- @endforeach
- ||||||||||||||||||