= 'on' (hidden) or 'off' (shown, the default). Literal // on/off, never 1/0, so the flag is never dropped by the metadata save (PHP empty("0") === true). const HIDE_TOTAL_PREFIX = 'financebuddy_hide_total_'; const HIDE_TOTAL_ON = 'on'; const HIDE_TOTAL_OFF = 'off'; // Hidden marker present only when the task form was submitted with the finance fieldset, so // non-finance task updates (moves, API calls) are ignored by the persistence hooks. const FORM_MARKER = 'financebuddy_form'; // Memoized per project_id: the board hooks below call isEnabled() once per card. private static $enabledCache = array(); // Memoized per project_id: currency() is likewise resolved once per card/column render. private static $currencyCache = array(); /** * Is FinanceBuddy enabled for this board? * * @param integer $project_id * @return bool */ public function isEnabled($project_id) { $project_id = (int) $project_id; if (! isset(self::$enabledCache[$project_id])) { self::$enabledCache[$project_id] = (int) $this->projectMetadataModel->get($project_id, self::ENABLED_KEY, 0) === 1; } return self::$enabledCache[$project_id]; } /** * Per-board currency symbol shown before amounts: the stored value trimmed and capped to * CURRENCY_MAX_LENGTH, or CURRENCY_DEFAULT ("R$") when empty. Memoized per project. * * @param integer $project_id * @return string */ public function currency($project_id) { $project_id = (int) $project_id; if (! isset(self::$currencyCache[$project_id])) { $symbol = trim((string) $this->projectMetadataModel->get($project_id, self::CURRENCY_KEY, '')); $symbol = mb_substr($symbol, 0, self::CURRENCY_MAX_LENGTH); self::$currencyCache[$project_id] = $symbol === '' ? self::CURRENCY_DEFAULT : $symbol; } return self::$currencyCache[$project_id]; } /** * Stored finance values for a task, with defaults. One query (getAll) since the board renders * this per card. * * @param integer $task_id * @return array */ public function get($task_id) { $all = $this->taskMetadataModel->getAll($task_id); return array( 'amount' => isset($all[self::AMOUNT_KEY]) ? $all[self::AMOUNT_KEY] : '', 'direction' => (isset($all[self::DIRECTION_KEY]) && $all[self::DIRECTION_KEY] === 'credit') ? 'credit' : 'debit', 'installment_current' => isset($all[self::INSTALLMENT_CURRENT_KEY]) ? $all[self::INSTALLMENT_CURRENT_KEY] : '', 'installment_total' => isset($all[self::INSTALLMENT_TOTAL_KEY]) ? $all[self::INSTALLMENT_TOTAL_KEY] : '', ); } /** * Net money for a board column: credits positive, debits negative, summed over the cards in * the column. One grouped query over the column's task ids (the task rows carry no metadata). * * @param array $column A board column as passed to template:board:column:header ('tasks'). * @return float */ public function columnNet(array $column) { if (empty($column['tasks'])) { return 0.0; } $task_ids = array(); foreach ($column['tasks'] as $task) { $task_ids[] = (int) $task['id']; } $rows = $this->db->table('task_has_metadata') ->columns('task_id', 'name', 'value') ->in('task_id', $task_ids) ->in('name', array(self::AMOUNT_KEY, self::DIRECTION_KEY)) ->findAll(); $amounts = array(); $directions = array(); foreach ($rows as $row) { if ($row['name'] === self::AMOUNT_KEY) { $amounts[$row['task_id']] = (float) $row['value']; } else { $directions[$row['task_id']] = $row['value']; } } $net = 0.0; foreach ($amounts as $task_id => $amount) { if ($amount <= 0) { continue; } $is_credit = isset($directions[$task_id]) && $directions[$task_id] === 'credit'; $net += $is_credit ? $amount : -$amount; } return $net; } /** * Has this column's header total been hidden (per-column opt-out)? * * @param integer $project_id * @param integer $column_id * @return bool */ public function isTotalHidden($project_id, $column_id) { return $this->projectMetadataModel->get((int) $project_id, self::HIDE_TOTAL_PREFIX.(int) $column_id, self::HIDE_TOTAL_OFF) === self::HIDE_TOTAL_ON; } /** * Values to prefill the task-form fields: a re-post (after a validation error) wins, then the * stored metadata for an existing task, otherwise blanks for a new task. * * @param array $values * @return array */ public function formValues(array $values) { if (isset($values[self::FORM_MARKER])) { return array( 'amount' => isset($values[self::AMOUNT_KEY]) ? $values[self::AMOUNT_KEY] : '', 'direction' => (isset($values[self::DIRECTION_KEY]) && $values[self::DIRECTION_KEY] === 'credit') ? 'credit' : 'debit', 'installment_current' => isset($values[self::INSTALLMENT_CURRENT_KEY]) ? $values[self::INSTALLMENT_CURRENT_KEY] : '', 'installment_total' => isset($values[self::INSTALLMENT_TOTAL_KEY]) ? $values[self::INSTALLMENT_TOTAL_KEY] : '', ); } if (! empty($values['id'])) { return $this->get($values['id']); } return array('amount' => '', 'direction' => 'debit', 'installment_current' => '', 'installment_total' => ''); } /** * Parse a user-typed amount. Accepts a dot or a comma as the decimal separator and an optional * "R$"; there is no thousand separator. Returns a non-negative float (0.0 when unparseable). * * @param string $raw * @return float */ public static function parseAmount($raw) { $raw = str_replace(array('R$', ' '), '', trim((string) $raw)); $raw = str_replace(',', '.', $raw); if ($raw === '' || ! is_numeric($raw)) { return 0.0; } $value = round((float) $raw, 2); return $value > 0 ? $value : 0.0; } /** * Format a money value for display: two decimals, comma decimal separator, no thousand * separator (e.g. 20.9 -> "20,90"). Accepts a float or a canonical dot-string. * * @param float|string $amount * @return string */ public static function money($amount) { return number_format((float) $amount, 2, ',', ''); } }