'required|valid_date[Y-m-d]', 'amount' => 'required|decimal|greater_than[0]', 'type' => 'required|in_list[income,expense]', 'category_id' => 'permit_empty|is_natural_no_zero', 'description' => 'permit_empty|max_length[255]', ]; /** * Lista operacji z nazwa kategorii, z filtrami. * $f: from, to, category_id, type (wszystkie opcjonalne). */ public function filtered(array $f): array { $b = $this->db->table('operations o') ->select('o.*, c.name AS category_name, p.name AS parent_name') ->join('categories c', 'c.id = o.category_id', 'left') ->join('categories p', 'p.id = c.parent_id', 'left'); if (! empty($f['from'])) { $b->where('o.date >=', $f['from']); } if (! empty($f['to'])) { $b->where('o.date <=', $f['to']); } if (! empty($f['category_id'])) { $b->where('o.category_id', (int) $f['category_id']); } if (! empty($f['type'])) { $b->where('o.type', $f['type']); } return $b->orderBy('o.date', 'DESC')->orderBy('o.id', 'DESC')->get()->getResultArray(); } /** * Sumy przychodow/wydatkow/salda dla zakresu (i opcjonalnych filtrow). */ public function totals(array $f): array { $b = $this->db->table('operations o') ->select("SUM(CASE WHEN o.type='income' THEN o.amount ELSE 0 END) AS income") ->select("SUM(CASE WHEN o.type='expense' THEN o.amount ELSE 0 END) AS expense"); if (! empty($f['from'])) { $b->where('o.date >=', $f['from']); } if (! empty($f['to'])) { $b->where('o.date <=', $f['to']); } if (! empty($f['category_id'])) { $b->where('o.category_id', (int) $f['category_id']); } if (! empty($f['type'])) { $b->where('o.type', $f['type']); } $row = $b->get()->getRowArray() ?: []; $income = (float) ($row['income'] ?? 0); $expense = (float) ($row['expense'] ?? 0); return [ 'income' => $income, 'expense' => $expense, 'balance' => $income - $expense, ]; } /** * Bilans miesieczny: [ ['ym'=>'2026-07','income'=>..,'expense'=>..], ... ]. */ public function monthlyBalance(string $from, string $to): array { return $this->db->table('operations') ->select("DATE_FORMAT(date, '%Y-%m') AS ym") ->select("SUM(CASE WHEN type='income' THEN amount ELSE 0 END) AS income") ->select("SUM(CASE WHEN type='expense' THEN amount ELSE 0 END) AS expense") ->where('date >=', $from)->where('date <=', $to) ->groupBy('ym')->orderBy('ym', 'ASC') ->get()->getResultArray(); } /** * Suma wg kategorii dla danego typu: [ ['name'=>..,'total'=>..], ... ]. */ public function byCategory(string $from, string $to, string $type): array { $rows = $this->db->table('operations o') ->select('o.category_id, c.name AS cat_name, p.name AS parent_name, SUM(o.amount) AS total') ->join('categories c', 'c.id = o.category_id', 'left') ->join('categories p', 'p.id = c.parent_id', 'left') ->where('o.type', $type) ->where('o.date >=', $from)->where('o.date <=', $to) ->groupBy('o.category_id')->groupBy('c.name')->groupBy('p.name') ->orderBy('total', 'DESC') ->get()->getResultArray(); // Etykieta = pelna sciezka kategoria -> podkategoria foreach ($rows as &$r) { if (empty($r['cat_name'])) { $r['name'] = '(bez kategorii)'; } elseif (! empty($r['parent_name'])) { $r['name'] = $r['parent_name'] . ' → ' . $r['cat_name']; } else { $r['name'] = $r['cat_name']; } } return $rows; } /** * Saldo skumulowane w czasie: [ ['date'=>..,'balance'=>..], ... ]. */ public function runningBalance(string $from, string $to): array { $rows = $this->db->table('operations') ->select('date') ->select("SUM(CASE WHEN type='income' THEN amount ELSE -amount END) AS net") ->where('date >=', $from)->where('date <=', $to) ->groupBy('date')->orderBy('date', 'ASC') ->get()->getResultArray(); $balance = 0.0; $out = []; foreach ($rows as $r) { $balance += (float) $r['net']; $out[] = ['date' => $r['date'], 'balance' => round($balance, 2)]; } return $out; } }