diff --git a/Controller/ConfigController.php b/Controller/ConfigController.php index a35c829..55d8d24 100644 --- a/Controller/ConfigController.php +++ b/Controller/ConfigController.php @@ -20,6 +20,7 @@ class ConfigController extends BaseController 'organon_tweaks_enhance_recurrence' => (int) $this->configModel->get('organon_tweaks_enhance_recurrence', 1), 'organon_tweaks_emphasize_duedate_default' => (int) $this->configModel->get('organon_tweaks_emphasize_duedate_default', 0), 'organon_tweaks_show_all_filter' => (int) $this->configModel->get('organon_tweaks_show_all_filter', 1), + 'organon_tweaks_month_filters' => (int) $this->configModel->get('organon_tweaks_month_filters', 1), ), 'errors' => array(), ))); @@ -35,6 +36,7 @@ class ConfigController extends BaseController $enhanceRecurrence = isset($values['organon_tweaks_enhance_recurrence']) ? 1 : 0; $emphasizeDueDateDefault = isset($values['organon_tweaks_emphasize_duedate_default']) ? 1 : 0; $showAllFilter = isset($values['organon_tweaks_show_all_filter']) ? 1 : 0; + $monthFilters = isset($values['organon_tweaks_month_filters']) ? 1 : 0; if ($this->configModel->save(array( 'organon_tweaks_always_comment_icon' => $alwaysCommentIcon, @@ -43,20 +45,27 @@ class ConfigController extends BaseController 'organon_tweaks_enhance_recurrence' => $enhanceRecurrence, 'organon_tweaks_emphasize_duedate_default' => $emphasizeDueDateDefault, 'organon_tweaks_show_all_filter' => $showAllFilter, + 'organon_tweaks_month_filters' => $monthFilters, ))) { $this->flash->success(t('Settings saved successfully.')); } else { $this->flash->failure(t('Unable to save your settings.')); } - // Apply the "Show all tasks" toggle across all boards immediately: on -> (re)create the - // filter everywhere, off -> remove every one (it is a plugin-managed, reserved-name filter). + // Apply each filter-group toggle across all boards immediately: on -> (re)create the group's + // reserved-name filters everywhere, off -> remove them (they are plugin-managed). $projectHelper = new \Kanboard\Plugin\OrganonTweaks\Helper\OrganonProjectHelper($this->container); if ($showAllFilter === 1) { - $projectHelper->addShowAllFilterToAllProjects(); + $projectHelper->addGroupToAllProjects($projectHelper::GROUP_SHOW_ALL); } else { - $projectHelper->removeAllShowAllFilters(); + $projectHelper->removeGroup($projectHelper::GROUP_SHOW_ALL); + } + + if ($monthFilters === 1) { + $projectHelper->addGroupToAllProjects($projectHelper::GROUP_MONTH); + } else { + $projectHelper->removeGroup($projectHelper::GROUP_MONTH); } $this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'OrganonTweaks'))); diff --git a/Filter/TaskMonthRangeFilter.php b/Filter/TaskMonthRangeFilter.php new file mode 100644 index 0000000..bc7c317 --- /dev/null +++ b/Filter/TaskMonthRangeFilter.php @@ -0,0 +1,92 @@ +extend('taskLexer', ...)): + * - `duemonth:VALUE` -- padDays 0: due within the target month. + * - `aroundmonth:VALUE` -- padDays 10: the month minus 10 days .. plus 10 days (i.e. the last + * 10 days of the previous month, the whole month, and the first 10 days of the next month). + * + * VALUE is `this` (also `next` / `last`) or an explicit month such as `2026-01` or `january-2026` + * (anything strtotime() can parse). Bounds are computed with mktime (correct for any month length) + * at search time, so the filter auto-rolls. + * + * Both bounds (gte start, lte end) are added inside this single apply() on the single-value path, + * so they AND -- the range Kanboard's saved-filter string syntax cannot express (repeating the + * same attribute ORs via OrCriteria, and there is no AND keyword). + */ +class TaskMonthRangeFilter extends BaseFilter implements FilterInterface +{ + private $attribute = 'duemonth'; + private $padDays = 0; + + public function setAttribute($attribute) + { + $this->attribute = $attribute; + return $this; + } + + public function setPadDays($days) + { + $this->padDays = (int) $days; + return $this; + } + + public function getAttributes() + { + return array($this->attribute); + } + + public function apply() + { + $reference = $this->resolveMonthTimestamp((string) $this->value); + + if ($reference === false) { + return $this; // unknown/typo value -> no-op (matches nothing added) + } + + $month = (int) date('n', $reference); + $year = (int) date('Y', $reference); + $first = mktime(0, 0, 0, $month, 1, $year); + $last = mktime(23, 59, 59, $month + 1, 0, $year); // day 0 of next month = last day of this + $pad = $this->padDays * 86400; + + $this->query->neq(TaskModel::TABLE.'.date_due', 0); + $this->query->gte(TaskModel::TABLE.'.date_due', $first - $pad); + $this->query->lte(TaskModel::TABLE.'.date_due', $last + $pad); + + return $this; + } + + /** + * Resolve the filter value to any timestamp inside the target month (or false if unparseable). + * + * @param string $value + * @return int|false + */ + private function resolveMonthTimestamp($value) + { + $value = strtolower(trim($value)); + + if ($value === '' || $value === 'this') { + return strtotime('first day of this month'); + } + + if ($value === 'next') { + return strtotime('first day of next month'); + } + + if ($value === 'last' || $value === 'prev' || $value === 'previous') { + return strtotime('first day of last month'); + } + + return strtotime($value); // 2026-01, january-2026, "last day of january 2026", ... + } +} diff --git a/Helper/OrganonProjectHelper.php b/Helper/OrganonProjectHelper.php index 35ed748..3862af2 100644 --- a/Helper/OrganonProjectHelper.php +++ b/Helper/OrganonProjectHelper.php @@ -16,19 +16,29 @@ use Kanboard\Model\CustomFilterModel; * MetadataModel::get() uses `?:`, and PHP treats the string "0" as falsy, so a stored "0" would * come back as the 'default' fallback and "Always off" would never be honored. * - * Also maintains a shared "Show all tasks" custom filter (an empty query) on every board, so there - * is always a one-click way to clear the board filter and reveal open + closed tasks. + * Also maintains shared, reserved-name custom filters on every board (v1.5 "Show all tasks", v1.6 + * "Due this month" / "Around this month"), each group gated by its own config toggle. */ class OrganonProjectHelper extends Base { const EMPHASIZE_KEY = 'organon_tweaks_emphasize_duedate'; const EMPHASIZE_DEFAULT_KEY = 'organon_tweaks_emphasize_duedate_default'; + // Config toggles for the auto-managed custom-filter groups. const SHOW_ALL_FILTER_KEY = 'organon_tweaks_show_all_filter'; + const MONTH_FILTERS_KEY = 'organon_tweaks_month_filters'; - // The plugin RESERVES this custom-filter name: a fixed (non-translated) string so the dedup is - // stable across locales. A user's own filter with this name is absorbed into the canonical form. - const SHOW_ALL_FILTER_NAME = 'Show all tasks'; + // Reserved custom-filter groups: each row is [name, query, append]. The NAME is reserved by the + // plugin (fixed, non-translated strings so dedup is stable across locales); a user's own filter + // with a reserved name is absorbed into the canonical form. "Show all tasks" is a REPLACE filter + // (append 0) so it clears; the rest are APPEND (1) so clicks stack (distinct attributes AND). + const GROUP_SHOW_ALL = array( + array('Show all tasks', '', 0), + ); + const GROUP_MONTH = array( + array('Due this month', 'duemonth:this', 1), + array('Around this month', 'aroundmonth:this', 1), + ); /** * Should the current board emphasize the due date? Board pages only (project_id in the route). @@ -58,15 +68,17 @@ class OrganonProjectHelper extends Base } /** - * Ensure the board has exactly one canonical "Show all tasks" filter (empty query, shared). - * Idempotent: create it when absent, force a same-named one back to the canonical form (reserved - * name), and drop any duplicates. Called lazily on every project-header render, so it covers old - * and new boards alike. No-op without a real project and a real owner (custom_filters.user_id has - * a FK to users, so a system id like 0 would fail the insert). + * Ensure one canonical reserved custom filter on a board (shared). Idempotent: create when + * absent, force a same-named one back to the canonical form (query + shared + append) since the + * name is reserved, and drop duplicates. No-op without a real project and a real owner + * (custom_filters.user_id has a FK to users, so a system id like 0 would fail the insert). * - * @param int $project_id + * @param int $project_id + * @param string $name + * @param string $query + * @param int $append */ - public function ensureShowAllFilter($project_id) + public function ensureReservedFilter($project_id, $name, $query, $append) { $project_id = (int) $project_id; $user_id = (int) $this->userSession->getId(); @@ -75,30 +87,32 @@ class OrganonProjectHelper extends Base return; } + $append = $append ? 1 : 0; + $existing = $this->db->table(CustomFilterModel::TABLE) ->eq('project_id', $project_id) - ->eq('name', self::SHOW_ALL_FILTER_NAME) + ->eq('name', $name) ->findAll(); if (empty($existing)) { $this->db->table(CustomFilterModel::TABLE)->insert(array( - 'filter' => '', + 'filter' => $query, 'project_id' => $project_id, 'user_id' => $user_id, - 'name' => self::SHOW_ALL_FILTER_NAME, + 'name' => $name, 'is_shared' => 1, - 'append' => 0, + 'append' => $append, )); return; } - // Reserved name: keep one, force it to the canonical empty + shared form, drop the rest. + // Reserved name: keep one, force it to the canonical form, drop the rest. $keep = array_shift($existing); - if ($keep['filter'] !== '' || (int) $keep['is_shared'] !== 1) { + if ($keep['filter'] !== $query || (int) $keep['is_shared'] !== 1 || (int) $keep['append'] !== $append) { $this->db->table(CustomFilterModel::TABLE) ->eq('id', $keep['id']) - ->update(array('filter' => '', 'is_shared' => 1)); + ->update(array('filter' => $query, 'is_shared' => 1, 'append' => $append)); } foreach ($existing as $duplicate) { @@ -107,23 +121,55 @@ class OrganonProjectHelper extends Base } /** - * Toggle turned ON: eagerly (re)create the "Show all tasks" filter on every existing board. - * ensureShowAllFilter() keeps covering boards created afterwards. + * Ensure every ENABLED group's filters on a board. Called lazily on each project-header render, + * so it covers old and new boards alike. + * + * @param int $project_id */ - public function addShowAllFilterToAllProjects() + public function ensureBoardFilters($project_id) { - foreach ($this->projectModel->getAll() as $project) { - $this->ensureShowAllFilter((int) $project['id']); + if ((int) $this->configModel->get(self::SHOW_ALL_FILTER_KEY, 1) === 1) { + $this->ensureGroup($project_id, self::GROUP_SHOW_ALL); + } + + if ((int) $this->configModel->get(self::MONTH_FILTERS_KEY, 1) === 1) { + $this->ensureGroup($project_id, self::GROUP_MONTH); + } + } + + private function ensureGroup($project_id, array $rows) + { + foreach ($rows as $row) { + $this->ensureReservedFilter($project_id, $row[0], $row[1], $row[2]); } } /** - * Toggle turned OFF: remove every "Show all tasks" filter across all boards (reserved name). + * Toggle turned ON: eagerly (re)create a group's filters on every existing board. + * ensureBoardFilters() keeps covering boards created afterwards. + * + * @param array $rows */ - public function removeAllShowAllFilters() + public function addGroupToAllProjects(array $rows) { - $this->db->table(CustomFilterModel::TABLE) - ->eq('name', self::SHOW_ALL_FILTER_NAME) - ->remove(); + foreach ($this->projectModel->getAll() as $project) { + $this->ensureGroup((int) $project['id'], $rows); + } + } + + /** + * Toggle turned OFF: remove a group's filters across all boards (by their reserved names). + * + * @param array $rows + */ + public function removeGroup(array $rows) + { + $names = array(); + + foreach ($rows as $row) { + $names[] = $row[0]; + } + + $this->db->table(CustomFilterModel::TABLE)->in('name', $names)->remove(); } } diff --git a/Plugin.php b/Plugin.php index fda3f36..ed0a194 100644 --- a/Plugin.php +++ b/Plugin.php @@ -58,11 +58,27 @@ class Plugin extends Base )); } - // Tweak: keep a shared "Show all tasks" custom filter (empty query -> open + closed) on every - // board as a one-click "clear the filter". Ensured lazily when a project header renders, so - // it covers old and new boards. The config toggle bulk-adds/removes them (ConfigController). - if ((int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1) { - $this->template->hook->attach('template:project:header:before', 'organonTweaks:project_header/show_all_filter'); + // Auto-managed shared custom filters (v1.5 "Show all tasks", v1.6 month filters). Ensured + // lazily whenever a project header renders (covers old + new boards); each group is gated by + // its own config toggle inside OrganonProjectHelper::ensureBoardFilters(). The toggles also + // bulk add/remove the filters (ConfigController). + $showAllFilters = (int) $this->configModel->get('organon_tweaks_show_all_filter', 1) === 1; + $monthFilters = (int) $this->configModel->get('organon_tweaks_month_filters', 1) === 1; + if ($showAllFilters || $monthFilters) { + $this->template->hook->attach('template:project:header:before', 'organonTweaks:project_header/board_filters'); + } + + // Custom board-search keywords, registered into the task lexer by extending its factory + // service (Pimple preserves factory status on extend, so the lexer stays per-request fresh). + // `duemonth:VALUE` = a whole calendar month; `aroundmonth:VALUE` = that month padded by 10 + // days on each side. Always available to type, independent of the bookmark toggles above. + // Guarded so a future Kanboard service rename no-ops (keyword absent) instead of breaking. + if (isset($this->container['taskLexer'])) { + $this->container->extend('taskLexer', function ($builder, $c) { + return $builder + ->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskMonthRangeFilter())->setAttribute('duemonth')->setPadDays(0)) + ->withFilter((new \Kanboard\Plugin\OrganonTweaks\Filter\TaskMonthRangeFilter())->setAttribute('aroundmonth')->setPadDays(10)); + }); } } @@ -90,7 +106,7 @@ class Plugin extends Base public function getPluginVersion() { - return '1.5.0'; + return '1.6.0'; } public function getPluginHomepage() diff --git a/Template/config/show.php b/Template/config/show.php index c2eae37..6ab8608 100644 --- a/Template/config/show.php +++ b/Template/config/show.php @@ -47,6 +47,13 @@
+ +