diff --git a/Asset/js/recoreco-modal.js b/Asset/js/recoreco-modal.js index 788c5c7..4a800f1 100644 --- a/Asset/js/recoreco-modal.js +++ b/Asset/js/recoreco-modal.js @@ -61,6 +61,23 @@ limit.disabled = checkbox.checked; } + // Trigger column: it can never be the target, so hide+disable that option and reset to Any if it + // was selected; the invert checkbox only means something for a specific column. + function applyTrigger() { + var target = document.querySelector('select[name="recoreco_target_column"]'); + var trigger = document.querySelector('select[name="recoreco_trigger_column"]'); + var invert = document.querySelector('input[name="recoreco_trigger_invert"]'); + if (!target || !trigger) { return; } + + for (var i = 0; i < trigger.options.length; i++) { + var isTarget = trigger.options[i].value === target.value; + trigger.options[i].hidden = isTarget; + trigger.options[i].disabled = isTarget; + } + if (trigger.value === target.value) { trigger.value = "any"; } + if (invert) { invert.disabled = (trigger.value === "any"); } + } + document.addEventListener("change", function (e) { if (e.target && e.target.name === "recoreco_frequency") { apply(e.target); @@ -68,6 +85,9 @@ if (e.target && e.target.name === "recoreco_follow_finance") { applyFollow(e.target); } + if (e.target && (e.target.name === "recoreco_target_column" || e.target.name === "recoreco_trigger_column")) { + applyTrigger(); + } }); if (window.MutationObserver) { @@ -76,6 +96,7 @@ if (select) { select.setAttribute("data-recoreco-init", "1"); apply(select); + applyTrigger(); } }).observe(document.body, { childList: true, subtree: true }); } diff --git a/Controller/RecurrenceController.php b/Controller/RecurrenceController.php index 7630ebf..896528c 100644 --- a/Controller/RecurrenceController.php +++ b/Controller/RecurrenceController.php @@ -34,6 +34,7 @@ class RecurrenceController extends BaseController 'values' => $values, 'errors' => $errors, 'columns_list' => $columns, + 'all_columns' => $this->columnModel->getList($task['project_id']), 'frequency_list' => $this->getFrequencyList(), 'has_due_date' => ! empty($task['date_due']), 'is_native' => $task['recurrence_status'] != TaskModel::RECURRING_STATUS_NONE, @@ -98,6 +99,16 @@ class RecurrenceController extends BaseController $values['recoreco_count'] = (isset($input['recoreco_count']) && ctype_digit((string) $input['recoreco_count']) && (int) $input['recoreco_count'] >= 1) ? (int) $input['recoreco_count'] : 1; } + // Trigger column + move-mode (v1.9). + $allColumns = $this->columnModel->getList($task['project_id']); + $trig = isset($input['recoreco_trigger_column']) ? (string) $input['recoreco_trigger_column'] : 'any'; + $values['recoreco_trigger_column'] = ($trig !== 'any' && array_key_exists((int) $trig, $allColumns)) ? (int) $trig : 'any'; + $values['recoreco_trigger_invert'] = isset($input['recoreco_trigger_invert']) ? 1 : 0; + $values['recoreco_move'] = isset($input['recoreco_move']) ? 1 : 0; + if ($values['recoreco_move'] === 1) { + $values['recoreco_follow_finance'] = 0; // Follow-FinanceBuddy is N/A in move-mode + } + $this->taskMetadataModel->save($task['id'], $values); // Reconcile the template's duplicate links with the setting right away. @@ -145,6 +156,9 @@ class RecurrenceController extends BaseController 'recoreco_follow_finance' => isset($meta['recoreco_follow_finance']) ? (int) $meta['recoreco_follow_finance'] : ($fb_enabled ? 1 : 0), 'recoreco_limit' => isset($meta['recoreco_limit']) ? (int) $meta['recoreco_limit'] : 0, 'recoreco_count' => isset($meta['recoreco_count']) ? (int) $meta['recoreco_count'] : 1, + 'recoreco_move' => isset($meta['recoreco_move']) ? (int) $meta['recoreco_move'] : 0, + 'recoreco_trigger_column' => isset($meta['recoreco_trigger_column']) ? $meta['recoreco_trigger_column'] : 'any', + 'recoreco_trigger_invert' => isset($meta['recoreco_trigger_invert']) ? (int) $meta['recoreco_trigger_invert'] : 0, ); } diff --git a/Model/RecoRecoModel.php b/Model/RecoRecoModel.php index a3ce992..0516d07 100644 --- a/Model/RecoRecoModel.php +++ b/Model/RecoRecoModel.php @@ -145,6 +145,49 @@ class RecoRecoModel extends Base // counter is FinanceBuddy's live installment_current instead. $count = isset($meta['recoreco_count']) && ctype_digit((string) $meta['recoreco_count']) ? (int) $meta['recoreco_count'] : 1; + // Trigger gate (cross-cutting, both modes): fire only while the card is in a triggering column, + // and never while it sits in the target (Gate 2, uniform). + $col = (int) $task['column_id']; + $trig = isset($meta['recoreco_trigger_column']) ? $meta['recoreco_trigger_column'] : 'any'; + $invert = isset($meta['recoreco_trigger_invert']) && $meta['recoreco_trigger_invert'] == 1; + $base = ($trig === 'any') ? true : ($invert ? ($col !== (int) $trig) : ($col === (int) $trig)); + + if (! ($base && $col !== $targetColumn)) { + return 0; + } + + // Move-mode: relocate the single card to the target instead of spawning a clone. No back-fill, + // no clone metadata. Honors the standalone limit (save() forced follow=0, so $limit is it). + if (isset($meta['recoreco_move']) && $meta['recoreco_move'] == 1) { + $cursor = (int) $task['date_due']; + + if ($cursor - $daysBefore * 86400 >= $horizon) { + return 0; // not due yet + } + + if ($limit > 0 && $count > $limit) { + $this->completePlan($task, $task_id); // safety: re-armed after completion + return 0; + } + + $this->taskPositionModel->movePosition($task['project_id'], $task_id, $targetColumn, 1, $task['swimlane_id'], false); + + $next = $this->calculator()->occurrenceFrom($anchor, $frequency, $lastDay, $cursor, false); + if ($next !== null && $next > $cursor) { + $this->setDue($task_id, $next); // advance one period + } + + if ($limit > 0) { + $count++; + $this->setCount($task_id, $count); + if ($count > $limit) { + $this->completePlan($task, $task_id); + } + } + + return 1; + } + // Phase 1 -- walk forward from the cursor, collecting every occurrence whose fire time has // arrived. The loop exits on the first occurrence still ahead of the horizon, which is then // the "next future" one to park on. diff --git a/Plugin.php b/Plugin.php index 4a3e2d7..5933a61 100644 --- a/Plugin.php +++ b/Plugin.php @@ -62,7 +62,7 @@ class Plugin extends Base public function getPluginVersion() { - return '1.6.2'; + return '1.9.0'; } public function getPluginHomepage() diff --git a/README.md b/README.md index 158de94..cdfb380 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,32 @@ When a followed plan ends, the parked template is left one past the last install three-installment plan) as a visible "spent" marker, its recurrence off, with an activity-stream entry recording the completion. +## Trigger column and move-mode + +Two options in the Recurring schedule dialog change *where* and *how* a card recurs: + +- **Trigger column.** By default (`Any`) a card fires wherever it sits. Set a **Trigger column** and + it only fires while parked there -- so a template spawns only from its "active" column, and moving + it elsewhere quietly pauses it. The **`any but selected`** checkbox inverts the choice (fire in + every column *except* the selected one, e.g. "any but Archive"). The **target column never + triggers**, in any mode -- this is what lets a moved card come to rest in the target without + re-firing. (One edge to know: a *duplicate* template dragged into its own target column no longer + spawns; previously it fired from any column. Templates normally sit elsewhere, so this rarely + bites.) +- **Move instead of copy.** Tick **"Move the card instead of creating a copy"** and RecoReco + relocates the *one* card to the target column each period instead of spawning a clone -- made for a + recurring chore you don't want piling up copies. Pair it with a trigger column: e.g. trigger = + `Done`, target = `Todo` -- cron moves the card `Done -> Todo` when it's due; you do the task and + drag it back to `Done`; repeat. Move-mode honors the recurrence limit (stop after N moves) but not + the FinanceBuddy follow (there is no copy to advance an installment on). Moves are silent (no + "moved to column" notification each period). + ## Status All five frequencies work (yearly, monthly by day, monthly by weekday, weekly, daily) with the last-day rule, board recurrence icons, backfill with the 12-occurrence cap, the Run-now button, a -recurrence limit (standalone or following FinanceBuddy installments), and the FinanceBuddy -installment hand-off. +recurrence limit (standalone or following FinanceBuddy installments), the FinanceBuddy installment +hand-off, a per-card trigger column, and move-mode (relocate instead of copy). ## Requirements diff --git a/Template/recurrence/edit.php b/Template/recurrence/edit.php index 4b8b966..45f421a 100644 --- a/Template/recurrence/edit.php +++ b/Template/recurrence/edit.php @@ -46,9 +46,21 @@ - form->label(t('Target column (where the copy appears)'), 'recoreco_target_column') ?> + form->label(t('Target column'), 'recoreco_target_column') ?> form->select('recoreco_target_column', $columns_list, $values) ?> + form->label(t('Trigger column'), 'recoreco_trigger_column') ?> +
+ form->select('recoreco_trigger_column', array('any' => t('Any')) + $all_columns, $values) ?> + +
+

+ + form->checkbox('recoreco_move', t('Move the card instead of creating a copy'), 1, $values['recoreco_move'] == 1) ?> +

+ form->label(t('Frequency'), 'recoreco_frequency') ?> form->select('recoreco_frequency', $frequency_list, $values) ?> diff --git a/VERSION b/VERSION index a8bed69..52016fc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -RecoReco v1.6.2 +RecoReco v1.9