'required|max_length[100]', 'type' => 'required|in_list[income,expense]', 'parent_id' => 'permit_empty|is_natural_no_zero', ]; /** * Plaska lista kategorii, opcjonalnie filtrowana typem. */ public function flatList(?string $type = null): array { $builder = $this->orderBy('type', 'ASC')->orderBy('name', 'ASC'); if ($type !== null && $type !== '') { $builder = $builder->where('type', $type); } return $builder->findAll(); } /** * Czy kategoria jest uzywana przez jakies operacje. */ public function usedByOperations(int $id): bool { return (bool) model(OperationModel::class)->where('category_id', $id)->countAllResults(); } /** * Kategorie w porzadku drzewa, posortowane po nazwie na kazdym poziomie. * Kazdy element dostaje 'depth' oraz 'indent_label' (nazwa z wcieciem) do list i selectow. */ public function tree(?string $type = null): array { $builder = $this->orderBy('name', 'ASC'); if ($type !== null && $type !== '') { $builder->where('type', $type); } $all = $builder->findAll(); // Grupowanie dzieci wg rodzica (0 = korzen) $childrenByParent = []; foreach ($all as $c) { $childrenByParent[(int) ($c['parent_id'] ?: 0)][] = $c; } $indent = "\u{00A0}\u{00A0}\u{00A0}\u{00A0}"; // 4x twarda spacja na poziom $out = []; $walk = static function (int $parentId, int $depth, string $parentPath) use (&$walk, &$out, $childrenByParent, $indent): void { foreach ($childrenByParent[$parentId] ?? [] as $c) { $c['depth'] = $depth; $c['indent_label'] = str_repeat($indent, $depth) . $c['name']; // Pelna sciezka "Nadrzedna → Podrzedna" — jednoznaczna nawet gdy JS uciasnie wciecia. $c['path_label'] = $parentPath === '' ? $c['name'] : $parentPath . ' → ' . $c['name']; $out[] = $c; $walk((int) $c['id'], $depth + 1, $c['path_label']); } }; $walk(0, 0, ''); // Sieroty (np. rodzic odfiltrowany typem) — dopnij na koncu bez wciecia $seen = array_column($out, null, 'id'); foreach ($all as $c) { if (! isset($seen[$c['id']])) { $c['depth'] = 0; $c['indent_label'] = $c['name']; $c['path_label'] = $c['name']; $out[] = $c; } } return $out; } }