Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc1f38fb30 | |||
| 9b486970d3 |
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,17 +121,21 @@ class RecoRecoModel extends Base
|
||||
// Default off: only keep the duplicate link when explicitly turned on.
|
||||
$linkCopies = isset($meta['recoreco_link_copies']) && $meta['recoreco_link_copies'] == 1;
|
||||
|
||||
// Finite-plan stop. RecoReco caps its own recurrence: either by its own limit (standalone),
|
||||
// or by following FinanceBuddy's installment total. In follow mode both numbers are read LIVE
|
||||
// from FinanceBuddy (never copied into RecoReco), so a mid-plan start, a re-purchase, or an
|
||||
// extend all just work by editing FinanceBuddy. A blank/0 limit means recur forever. The
|
||||
// spawn loop below stops when counter > limit (1-based; the counter ends on limit+1).
|
||||
// Finite-plan stop. RecoReco caps its own recurrence, either by its own limit (standalone) or
|
||||
// by following FinanceBuddy. When follow is on, FinanceBuddy governs: the plan is finite only
|
||||
// if the card has a numeric installment total, so an ordinary recurring bill (money but no
|
||||
// installments) recurs forever -- and any stale standalone limit is ignored. In follow mode
|
||||
// the numbers are read LIVE from FinanceBuddy (never copied), so a mid-plan start, a
|
||||
// re-purchase, or an extend all just work by editing FinanceBuddy. A blank/0 limit means
|
||||
// recur forever. The spawn loop below stops when counter > limit (1-based; counter ends on
|
||||
// limit+1).
|
||||
$followIntent = isset($meta['recoreco_follow_finance']) && $meta['recoreco_follow_finance'] == 1;
|
||||
$fbTotal = isset($meta['financebuddy_installment_total']) ? (string) $meta['financebuddy_installment_total'] : '';
|
||||
$follow = isset($meta['recoreco_follow_finance']) && $meta['recoreco_follow_finance'] == 1
|
||||
&& $fbTotal !== '' && ctype_digit($fbTotal);
|
||||
$fbHasTotal = $fbTotal !== '' && ctype_digit($fbTotal);
|
||||
$follow = $followIntent && $fbHasTotal;
|
||||
|
||||
if ($follow) {
|
||||
$limit = (int) $fbTotal;
|
||||
if ($followIntent) {
|
||||
$limit = $fbHasTotal ? (int) $fbTotal : 0; // following FinanceBuddy; no total => forever
|
||||
} else {
|
||||
$limitRaw = isset($meta['recoreco_limit']) ? (string) $meta['recoreco_limit'] : '';
|
||||
$limit = ($limitRaw !== '' && ctype_digit($limitRaw)) ? (int) $limitRaw : 0;
|
||||
@@ -141,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.
|
||||
|
||||
@@ -62,7 +62,7 @@ class Plugin extends Base
|
||||
|
||||
public function getPluginVersion()
|
||||
{
|
||||
return '1.6.1';
|
||||
return '1.9.0';
|
||||
}
|
||||
|
||||
public function getPluginHomepage()
|
||||
|
||||
24
README.md
24
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
|
||||
|
||||
|
||||
@@ -46,9 +46,21 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<?= $this->form->label(t('Target column (where the copy appears)'), 'recoreco_target_column') ?>
|
||||
<?= $this->form->label(t('Target column'), 'recoreco_target_column') ?>
|
||||
<?= $this->form->select('recoreco_target_column', $columns_list, $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Trigger column'), 'recoreco_trigger_column') ?>
|
||||
<div class="recoreco-trigger" style="display:flex; align-items:center; gap:8px;">
|
||||
<?= $this->form->select('recoreco_trigger_column', array('any' => t('Any')) + $all_columns, $values) ?>
|
||||
<label style="white-space:nowrap;">
|
||||
<input type="checkbox" name="recoreco_trigger_invert" value="1" <?= $values['recoreco_trigger_invert'] == 1 ? 'checked="checked"' : '' ?>> <?= t('any but selected') ?>
|
||||
</label>
|
||||
</div>
|
||||
<p class="form-help"><?= t('Recurrence fires only while the card sits in a triggering column; the target column never triggers.') ?></p>
|
||||
|
||||
<?= $this->form->checkbox('recoreco_move', t('Move the card instead of creating a copy'), 1, $values['recoreco_move'] == 1) ?>
|
||||
<p class="form-help"><?= t('Relocate this one card to the target column each period instead of spawning a copy.') ?></p>
|
||||
|
||||
<?= $this->form->label(t('Frequency'), 'recoreco_frequency') ?>
|
||||
<?= $this->form->select('recoreco_frequency', $frequency_list, $values) ?>
|
||||
|
||||
@@ -63,27 +75,30 @@
|
||||
<p class="form-help"><?= t('Links each copy back to the template (a count and quick navigation). Off by default.') ?></p>
|
||||
|
||||
<?php $following = $fb_enabled && $values['recoreco_follow_finance'] == 1 ?>
|
||||
<?php $limit_display = (int) $values['recoreco_limit'] > 0 ? (int) $values['recoreco_limit'] : '' ?>
|
||||
<div class="recoreco-limit-group"
|
||||
data-fb-current="<?= $this->text->e($fb_current === '' ? '1' : $fb_current) ?>"
|
||||
data-fb-total="<?= $this->text->e($fb_total === '' ? '0' : $fb_total) ?>"
|
||||
data-fb-current="<?= $this->text->e($fb_current) ?>"
|
||||
data-fb-total="<?= $this->text->e($fb_total) ?>"
|
||||
data-rr-count="<?= (int) $values['recoreco_count'] ?>"
|
||||
data-rr-limit="<?= (int) $values['recoreco_limit'] ?>">
|
||||
data-rr-limit="<?= $this->text->e($limit_display) ?>">
|
||||
|
||||
<?php if ($fb_enabled): ?>
|
||||
<?= $this->form->checkbox('recoreco_follow_finance', t('Follow FinanceBuddy installments'), 1, $following) ?>
|
||||
<p class="form-help"><?= t('Stop recurring when the installment plan ends, following the card total and current installment.') ?></p>
|
||||
<?php endif ?>
|
||||
|
||||
<?= $this->form->label(t('Current recurrence'), 'recoreco_count') ?>
|
||||
<input type="number" name="recoreco_count" min="1"
|
||||
value="<?= $this->text->e($following ? ($fb_current === '' ? '1' : $fb_current) : $values['recoreco_count']) ?>"
|
||||
<?= $this->form->label(t('Recurrence'), 'recoreco_count') ?>
|
||||
<div class="recoreco-limit-inline" style="display:flex; align-items:center; gap:6px;">
|
||||
<input type="number" name="recoreco_count" min="1" style="width:5em;"
|
||||
value="<?= $this->text->e($following ? $fb_current : $values['recoreco_count']) ?>"
|
||||
<?= $following ? 'disabled="disabled"' : '' ?>>
|
||||
|
||||
<?= $this->form->label(t('Stop after this many recurrences (0 = never)'), 'recoreco_limit') ?>
|
||||
<input type="number" name="recoreco_limit" min="0"
|
||||
value="<?= $this->text->e($following ? ($fb_total === '' ? '0' : $fb_total) : $values['recoreco_limit']) ?>"
|
||||
<span><?= t('of') ?></span>
|
||||
<input type="number" name="recoreco_limit" min="1" style="width:5em;"
|
||||
value="<?= $this->text->e($following ? $fb_total : $limit_display) ?>"
|
||||
<?= $following ? 'disabled="disabled"' : '' ?>>
|
||||
</div>
|
||||
<p class="form-help"><?= t('Current recurrence and how many to run. Leave the second box empty to recur forever.') ?></p>
|
||||
</div>
|
||||
|
||||
<?= $this->modal->submitButtons() ?>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user