diff --git a/app/src/Lib/Traits/SearchFilterTrait.php b/app/src/Lib/Traits/SearchFilterTrait.php index c401034af..c6a55b714 100644 --- a/app/src/Lib/Traits/SearchFilterTrait.php +++ b/app/src/Lib/Traits/SearchFilterTrait.php @@ -51,6 +51,12 @@ public function getSearchableAttributes(): array { 'type' => $type, 'label' => (__d('field', $column) ?? Inflector::humanize($column)) ]; + + // For the date fields we search ranges + if($type === 'timestamp') { + $this->searchFilters[$column]['alias'][] = $column . '_starts_at'; + $this->searchFilters[$column]['alias'][] = $column . '_ends_at'; + } } return $this->searchFilters ?? []; @@ -60,14 +66,14 @@ public function getSearchableAttributes(): array { * Build a query where() clause for the configured attribute. * * @param \Cake\ORM\Query $query Cake ORM Query object - * @param string|array $attribute Attribute to filter on (database name) - * @param string $q Value to filter on + * @param string $attribute Attribute to filter on (database name) + * @param string|array $q Value to filter on * * @return \Cake\ORM\Query Cake ORM Query object * @since COmanage Registry v5.0.0 */ - public function whereFilter(\Cake\ORM\Query $query, string|array $attribute, string $q): object { + public function whereFilter(\Cake\ORM\Query $query, string $attribute, string|array $q): object { // not a permitted attribute if(empty($this->searchFilters[$attribute])) { return $query; @@ -83,9 +89,29 @@ public function whereFilter(\Cake\ORM\Query $query, string|array $attribute, str } elseif(in_array($this->searchFilters[$attribute]['type'], $search_types, true)) { return $query->where([$attribute => $search]); } elseif( $this->searchFilters[$attribute]['type'] == "timestamp") { - return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) { - return $exp->between($attribute, $search[0], $search[1]); - }); + // Date between dates + if(!empty($search[0]) + && !empty($search[1])) { + return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) { + return $exp->between($attribute, $search[0], $search[1]); + }); + // The starts at is non empty. So the data should be greater than the starts_at date + } elseif(!empty($search[0]) + && empty($search[1])) { + return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) { + return $exp->gte($attribute, $search[0]); + }); + // The ends at is non-empty. So the data should be less than the ends at date + } elseif(!empty($search[1]) + && empty($search[0])) { + return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) { + return $exp->lte($attribute, $search[1]); + }); + } else { + // We return everything + return $query; + } + } // String values diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index a828e16e1..5ed37a01d 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -217,7 +217,7 @@ function _column_key($modelsName, $c, $tz=null) { break; case 'datetime': // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); - print $this->Time->nice($entity->$col, $vv_tz) . $suffix; + print !empty($entity->$col) ? $this->Time->nice($entity->$col, $vv_tz) . $suffix : ""; break; case 'enum': if($entity->$col) { diff --git a/app/templates/element/filter.php b/app/templates/element/filter.php index 6ca492da8..f40fcad5b 100644 --- a/app/templates/element/filter.php +++ b/app/templates/element/filter.php @@ -24,16 +24,48 @@ * @since COmanage Registry v5.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ +use Cake\Collection\Collection; +use Cake\Utility\Inflector; + // $this->name = Models $modelsName = $this->name; // $modelName = Model -$modelName = \Cake\Utility\Inflector::singularize($modelsName); +$modelName = Inflector::singularize($modelsName); // Get the query string and separate the search params from the non-search params $query = $this->request->getQueryParams(); -$non_search_params = array_diff_key($query, $vv_searchable_attributes); -$search_params = array_intersect_key($query, $vv_searchable_attributes); +// Search attributes collection +$search_attributes_collection = new Collection($vv_searchable_attributes); +$alias_params = $search_attributes_collection->filter(fn ($val, $attr) => (is_array($val) && array_key_exists('alias', $val)) ) + ->extract('alias') + ->unfold() + ->toArray(); +// For the non search params we need to search the alias params as well +$searchable_parameters = [ + ...array_keys($vv_searchable_attributes), + ...$alias_params + ]; +$non_search_params = (new Collection($query))->filter( fn($value, $key) => !in_array($key, $searchable_parameters) ) + ->toArray(); + +// Filter the search params and take params with aliases into consideration +$search_params = []; +foreach ($vv_searchable_attributes as $attr => $value) { + if(isset($query[$attr])) { + $search_params[$attr] = $query[$attr]; + continue; + } + + if(isset($value['alias']) + && is_array($value['alias'])) { + foreach ($value['alias'] as $alias_key) { + if(isset($query[$alias_key])) { + $search_params[$attr][$alias_key] = $query[$alias_key]; + } + } + } +} // Begin the form print $this->Form->create(null, [ @@ -61,7 +93,6 @@ search - $params): ?> @@ -74,16 +105,18 @@ // The populated variables are in plural while the column names are singular // Convention: It is a prerequisite that the vvar should be the plural of the column name - $populated_vvar = Cake\Utility\Inflector::pluralize($key); - $pvalue = isset($$populated_vvar) ? $$populated_vvar[ $search_params[$key] ] : $search_params[$key]; + $populated_vvar = Inflector::pluralize($key); + $button_label = isset($$populated_vvar) ? + $$populated_vvar[ $search_params[$key] ] : + (is_array($search_params[$key]) ? 'Range' : $search_params[$key]); ?> @@ -97,7 +130,7 @@
-
+
- + $options): ?>
-
-
+
+
@@ -149,14 +182,16 @@ $starts_field = $key . "_starts_at"; $coptions['class'] = 'form-control datepicker'; $coptions['label'] = 'Starts at:'; - $coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; // TODO: test for date-only inputs and send only the date + $coptions['required'] = false; + $coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; $coptions['id'] = $starts_field; $pickerDate = ''; if(!empty($query[$starts_field])) { + $starts_date = \Cake\I18n\FrozenTime::parse($query[$starts_field]); // Adjust the time back to the user's timezone - $coptions['value'] = $query[$starts_field]->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->get('vv_tz')); - $pickerDate = $query[$starts_field]->i18nFormat("yyyy-MM-dd", $this->get('vv_tz')); + $coptions['value'] = $starts_date->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->get('vv_tz')); + $pickerDate = $starts_date->i18nFormat("yyyy-MM-dd", $this->get('vv_tz')); } $date_args = [ @@ -178,15 +213,17 @@ // accessibility purposes. $ends_field = $key . "_ends_at"; $coptions['class'] = 'form-control datepicker'; - $coptions['label'] = 'Ends at:'; + $coptions['required'] = false; $coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; // TODO: test for date-only inputs and send only the date + $coptions['label'] = 'Ends at:'; $coptions['id'] = $ends_field; $pickerDate = ''; if(!empty($query[$ends_field])) { // Adjust the time back to the user's timezone - $coptions['value'] = $query[$ends_field]->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->get('vv_tz')); - $pickerDate = $query[$ends_field]->i18nFormat("yyyy-MM-dd", $this->get('vv_tz')); + $ends_date = \Cake\I18n\FrozenTime::parse($query[$ends_field]); + $coptions['value'] = $ends_date->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->get('vv_tz')); + $pickerDate = $ends_date->i18nFormat("yyyy-MM-dd", $this->get('vv_tz')); } $date_args = [ @@ -215,7 +252,8 @@ print $this->Form->checkbox($key, [ 'class' => 'form-check-input', 'checked' => $query[$key] ?? 0, - 'hiddenField' => false + 'hiddenField' => false, + 'required' => false ]); ?>
diff --git a/app/webroot/css/co-responsive.css b/app/webroot/css/co-responsive.css index b80bc40d0..e036179b3 100644 --- a/app/webroot/css/co-responsive.css +++ b/app/webroot/css/co-responsive.css @@ -192,7 +192,7 @@ margin-bottom: 0; } /* TOP SEARCH */ - #top-filters-fields-subgroups { + .top-filters-fields-subgroups { display: grid; grid-template-columns: 1fr 1fr; column-gap: 1em;