113 lines
4.3 KiB
PHP
113 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace Kanboard\Plugin\OrganonTweaks\Helper;
|
|
|
|
use Kanboard\Core\Base;
|
|
use Kanboard\Model\SubtaskModel;
|
|
|
|
/**
|
|
* Done/Todo badge helper.
|
|
*
|
|
* "Done" has a single source of truth PER MODE (no drift with native Close/Open):
|
|
* - close-mode on ("check done also closes tasks"): Done == the task is CLOSED (is_active == 0).
|
|
* The badge reads the task's own is_active, so however a task is closed/reopened (badge, native
|
|
* sidebar, bulk, API) the badge always matches. No metadata is stored in this mode.
|
|
* - close-mode off (marker only): Done == the task metadata DONE_KEY == 'on', independent of the
|
|
* open/closed status. Stored as the non-falsy string 'on' (never '0'/'1'): MetadataModel::get()
|
|
* uses `?:` and PHP treats "0" as falsy, so a stored "0" would read back as the default.
|
|
*
|
|
* Templates cannot read models directly, so the board/task-view badge templates call isDone() here,
|
|
* passing the task array (which carries both id and is_active) so the close-mode read needs no query.
|
|
*/
|
|
class OrganonDoneHelper extends Base
|
|
{
|
|
const DONE_KEY = 'organon_done';
|
|
const CLOSES_KEY = 'organon_tweaks_done_closes_task';
|
|
const AUTOSUB_KEY = 'organon_tweaks_done_autosubtasks';
|
|
const ALLDONE_KEY = 'organon_subtasks_alldone';
|
|
|
|
/**
|
|
* Is this task Done? (mode-aware: closed status in close-mode, else the metadata marker)
|
|
*
|
|
* @param array $task a task array carrying 'id' and 'is_active'
|
|
* @return bool
|
|
*/
|
|
public function isDone(array $task)
|
|
{
|
|
if ($this->closesTask()) {
|
|
return isset($task['is_active']) && (int) $task['is_active'] === 0;
|
|
}
|
|
|
|
return $this->taskMetadataModel->get((int) $task['id'], self::DONE_KEY, '') === 'on';
|
|
}
|
|
|
|
/**
|
|
* Should toggling Done also close (and reopen) the task? Global setting, default off.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function closesTask()
|
|
{
|
|
return (int) $this->configModel->get(self::CLOSES_KEY, 0) === 1;
|
|
}
|
|
|
|
/**
|
|
* Mark a task Done in the current mode. Idempotent -- a no-op if it is already Done. Close-mode
|
|
* closes the task; marker-mode sets the DONE_KEY metadata.
|
|
*
|
|
* @param int $task_id
|
|
*/
|
|
public function markDone($task_id)
|
|
{
|
|
$task = $this->taskFinderModel->getById((int) $task_id);
|
|
|
|
if (empty($task) || $this->isDone($task)) {
|
|
return;
|
|
}
|
|
|
|
if ($this->closesTask()) {
|
|
$this->taskStatusModel->close((int) $task_id);
|
|
} else {
|
|
$this->taskMetadataModel->save((int) $task_id, array(self::DONE_KEY => 'on'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recompute whether the task's subtasks are ALL done and, only on the up-transition
|
|
* (was-not-all-done -> now-all-done), auto-mark the task Done. The last-seen all-done state is kept
|
|
* in ALLDONE_KEY ('on'/'off') to detect that edge -- so a manual unmark is honored (nothing
|
|
* re-fires) and title edits / refreshes never re-mark. Never auto-unmarks.
|
|
*
|
|
* @param int $task_id
|
|
* @param int $exclude_id a subtask id to exclude from the counts (the row being deleted --
|
|
* EVENT_DELETE fires BEFORE the row is removed, so it still counts)
|
|
*/
|
|
public function syncSubtasksDone($task_id, $exclude_id = 0)
|
|
{
|
|
$task_id = (int) $task_id;
|
|
$exclude_id = (int) $exclude_id;
|
|
|
|
$totalQuery = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id);
|
|
$doneQuery = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->eq('status', SubtaskModel::STATUS_DONE);
|
|
|
|
if ($exclude_id > 0) {
|
|
$totalQuery->neq('id', $exclude_id);
|
|
$doneQuery->neq('id', $exclude_id);
|
|
}
|
|
|
|
$total = $totalQuery->count();
|
|
$done = $doneQuery->count();
|
|
$allDone = $total > 0 && $done === $total;
|
|
$prev = $this->taskMetadataModel->get($task_id, self::ALLDONE_KEY, 'off') === 'on';
|
|
|
|
// Save the new state BEFORE marking: in close-mode markDone() -> close() -> closeAll() re-fires
|
|
// subtask events into this method; with the marker already 'on', that re-entry sees no
|
|
// transition and is a no-op (no loop).
|
|
$this->taskMetadataModel->save($task_id, array(self::ALLDONE_KEY => $allDone ? 'on' : 'off'));
|
|
|
|
if ($allDone && ! $prev) {
|
|
$this->markDone($task_id);
|
|
}
|
|
}
|
|
}
|