194 lines
9.8 KiB
PHP
194 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace Kanboard\Plugin\RecoReco\Controller;
|
|
|
|
use Kanboard\Controller\BaseController;
|
|
use Kanboard\Model\TaskModel;
|
|
|
|
/**
|
|
* The "Recurring schedule" modal: configure a card's calendar recurrence and store it in task
|
|
* metadata. The CLI engine (RecoRecoModel) does the actual spawning.
|
|
*
|
|
* Gate: a card can only be enabled when it has a due date (the recurrence anchor), is not already a
|
|
* native-recurring card (mutually exclusive), and the board has a column other than the card's own
|
|
* (the copy must land elsewhere).
|
|
*/
|
|
class RecurrenceController extends BaseController
|
|
{
|
|
public function edit(array $values = array(), array $errors = array())
|
|
{
|
|
$task = $this->getTask();
|
|
$columns = $this->targetColumns($task);
|
|
$meta = $this->taskMetadataModel->getAll($task['id']);
|
|
|
|
// The "Follow FinanceBuddy installments" option only appears when FinanceBuddy is enabled on
|
|
// this board (installed but not enabled means no installment data to follow).
|
|
$fb_enabled = (int) $this->projectMetadataModel->get($task['project_id'], 'financebuddy_enabled', 0) === 1;
|
|
|
|
if (empty($values)) {
|
|
$values = $this->getStoredValues($meta, $task, $columns, $fb_enabled);
|
|
}
|
|
|
|
$this->response->html($this->template->render('recoReco:recurrence/edit', array(
|
|
'task' => $task,
|
|
'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,
|
|
'has_target' => ! empty($columns),
|
|
'is_clone' => isset($meta['recoreco_clone']) && $meta['recoreco_clone'] == 1,
|
|
'source_id' => isset($meta['recoreco_source']) ? (int) $meta['recoreco_source'] : 0,
|
|
'fb_enabled' => $fb_enabled,
|
|
'fb_current' => isset($meta['financebuddy_installment_current']) ? $meta['financebuddy_installment_current'] : '',
|
|
'fb_total' => isset($meta['financebuddy_installment_total']) ? $meta['financebuddy_installment_total'] : '',
|
|
)));
|
|
}
|
|
|
|
public function save()
|
|
{
|
|
$task = $this->getTask();
|
|
$input = $this->request->getValues();
|
|
$columns = $this->targetColumns($task);
|
|
|
|
// A clone can never be made recurring (that would recurse). Guard even though the modal
|
|
// hides the form for clones.
|
|
$is_clone = (int) $this->taskMetadataModel->get($task['id'], 'recoreco_clone', 0) === 1;
|
|
|
|
// Only a plain card (a due date, not native-recurring, not a clone, a valid target) may enable.
|
|
$can_recur = ! empty($task['date_due'])
|
|
&& $task['recurrence_status'] == TaskModel::RECURRING_STATUS_NONE
|
|
&& ! $is_clone
|
|
&& ! empty($columns);
|
|
$enabled = ($can_recur && isset($input['recoreco_enabled']) && $input['recoreco_enabled'] == 1) ? 1 : 0;
|
|
|
|
$frequency = isset($input['recoreco_frequency']) && array_key_exists($input['recoreco_frequency'], $this->getFrequencyList())
|
|
? $input['recoreco_frequency']
|
|
: 'monthly_day';
|
|
|
|
// Keep the lead time under one period (else a copy's fire time crosses the prior occurrence).
|
|
$daysBefore = isset($input['recoreco_days_before']) && ctype_digit((string) $input['recoreco_days_before']) ? (int) $input['recoreco_days_before'] : 0;
|
|
$daysBefore = min($daysBefore, $this->maxDaysBefore($frequency));
|
|
|
|
$values = array(
|
|
'recoreco_enabled' => $enabled,
|
|
'recoreco_target_column' => $this->resolveTarget(isset($input['recoreco_target_column']) ? $input['recoreco_target_column'] : 0, $columns),
|
|
'recoreco_frequency' => $frequency,
|
|
'recoreco_last_day' => isset($input['recoreco_last_day']) ? 1 : 0,
|
|
'recoreco_days_before' => $daysBefore,
|
|
'recoreco_link_copies' => isset($input['recoreco_link_copies']) ? 1 : 0,
|
|
);
|
|
|
|
// Capture the anchor (the due date at enable time) -- the drift-free pattern.
|
|
if ($enabled) {
|
|
$values['recoreco_anchor'] = (int) $task['date_due'];
|
|
}
|
|
|
|
// Finite-plan limit. When FinanceBuddy is enabled on the board and "Follow" is on, RecoReco
|
|
// reads its installment total/current live at run time -- so nothing is stored here and the
|
|
// standalone limit/counter are left untouched (the modal greys them). Otherwise store the
|
|
// standalone limit (blank/0 = forever) and the 1-based progress counter (seedable).
|
|
$fb_enabled = (int) $this->projectMetadataModel->get($task['project_id'], 'financebuddy_enabled', 0) === 1;
|
|
$follow = $fb_enabled && isset($input['recoreco_follow_finance']) && $input['recoreco_follow_finance'] == 1;
|
|
$values['recoreco_follow_finance'] = $follow ? 1 : 0;
|
|
|
|
if (! $follow) {
|
|
$values['recoreco_limit'] = (isset($input['recoreco_limit']) && ctype_digit((string) $input['recoreco_limit'])) ? (int) $input['recoreco_limit'] : 0;
|
|
$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.
|
|
$model = new \Kanboard\Plugin\RecoReco\Model\RecoRecoModel($this->container);
|
|
$model->syncCloneLinks($task['id'], $values['recoreco_link_copies'] == 1);
|
|
|
|
$this->flash->success(t('Recurring schedule saved.'));
|
|
|
|
return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])), true);
|
|
}
|
|
|
|
// Target columns: ALL of the project's columns. This used to exclude the card's own column (a
|
|
// duplicate copy "must move"), but move-mode legitimately rests the card IN the target, so the
|
|
// target must be any column -- otherwise a moved card's own target vanishes from the dropdown and
|
|
// a re-save would silently reset it. `target != trigger` is enforced in the modal instead.
|
|
private function targetColumns(array $task)
|
|
{
|
|
return $this->columnModel->getList($task['project_id']);
|
|
}
|
|
|
|
// A stored/posted target, if still a valid non-current column; otherwise the first one (or 0).
|
|
private function resolveTarget($candidate, array $columns)
|
|
{
|
|
$candidate = (int) $candidate;
|
|
|
|
if (array_key_exists($candidate, $columns)) {
|
|
return $candidate;
|
|
}
|
|
|
|
$keys = array_keys($columns);
|
|
|
|
return empty($keys) ? 0 : (int) $keys[0];
|
|
}
|
|
|
|
private function getStoredValues(array $meta, array $task, array $columns, $fb_enabled)
|
|
{
|
|
return array(
|
|
'recoreco_enabled' => isset($meta['recoreco_enabled']) ? (int) $meta['recoreco_enabled'] : 0,
|
|
'recoreco_target_column' => $this->resolveTarget(isset($meta['recoreco_target_column']) ? $meta['recoreco_target_column'] : 0, $columns),
|
|
'recoreco_frequency' => isset($meta['recoreco_frequency']) ? $meta['recoreco_frequency'] : 'monthly_day',
|
|
'recoreco_last_day' => isset($meta['recoreco_last_day']) ? (int) $meta['recoreco_last_day'] : 0,
|
|
'recoreco_days_before' => isset($meta['recoreco_days_before']) ? (int) $meta['recoreco_days_before'] : 0,
|
|
'recoreco_link_copies' => isset($meta['recoreco_link_copies']) ? (int) $meta['recoreco_link_copies'] : 0,
|
|
// Follow defaults ON for a fresh card on a FinanceBuddy board; otherwise the stored choice.
|
|
'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,
|
|
);
|
|
}
|
|
|
|
private function getFrequencyList()
|
|
{
|
|
return array(
|
|
'yearly' => t('Yearly'),
|
|
'monthly_day' => t('Monthly by day'),
|
|
'monthly_dow' => t('Monthly by weekday'),
|
|
'weekly' => t('Weekly'),
|
|
'daily' => t('Daily'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The largest allowed "days before" for a frequency -- kept strictly under one period so a
|
|
* copy's fire time never crosses the previous occurrence. Daily = 0 (disabled).
|
|
*/
|
|
private function maxDaysBefore($frequency)
|
|
{
|
|
switch ($frequency) {
|
|
case 'daily':
|
|
return 0;
|
|
case 'weekly':
|
|
return 6;
|
|
case 'yearly':
|
|
return 364;
|
|
default: // monthly_day, monthly_dow
|
|
return 27;
|
|
}
|
|
}
|
|
}
|