update
This commit is contained in:
@@ -60,15 +60,17 @@ class CategoryModel extends Model
|
||||
$indent = "\u{00A0}\u{00A0}\u{00A0}\u{00A0}"; // 4x twarda spacja na poziom
|
||||
$out = [];
|
||||
|
||||
$walk = static function (int $parentId, int $depth) use (&$walk, &$out, $childrenByParent, $indent): void {
|
||||
$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);
|
||||
$walk((int) $c['id'], $depth + 1, $c['path_label']);
|
||||
}
|
||||
};
|
||||
$walk(0, 0);
|
||||
$walk(0, 0, '');
|
||||
|
||||
// Sieroty (np. rodzic odfiltrowany typem) — dopnij na koncu bez wciecia
|
||||
$seen = array_column($out, null, 'id');
|
||||
@@ -76,6 +78,7 @@ class CategoryModel extends Model
|
||||
if (! isset($seen[$c['id']])) {
|
||||
$c['depth'] = 0;
|
||||
$c['indent_label'] = $c['name'];
|
||||
$c['path_label'] = $c['name'];
|
||||
$out[] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
/**
|
||||
* Uczona pamiec dopasowan: znormalizowana nazwa pozycji -> kategoria.
|
||||
* Sluzy WYLACZNIE do podpowiedzi; nic nie jest ksiegowane automatycznie.
|
||||
*/
|
||||
class ReceiptCategoryMapModel extends Model
|
||||
{
|
||||
protected $table = 'receipt_category_map';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = ''; // tabela ma tylko updated_at
|
||||
protected $allowedFields = ['item_name', 'category_id', 'updated_at'];
|
||||
|
||||
/**
|
||||
* Normalizacja nazwy pozycji jako klucz uczenia:
|
||||
* trim + lowercase + redukcja wielokrotnych bialych znakow.
|
||||
* ponytail: bez usuwania gramatur/liczb — zaczynamy od dokladnego dopasowania nazwy.
|
||||
*/
|
||||
public function normalize(string $name): string
|
||||
{
|
||||
$n = mb_strtolower(trim($name));
|
||||
|
||||
return preg_replace('/\s+/u', ' ', $n) ?? $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Podpowiedziana kategoria dla nazwy pozycji lub null.
|
||||
*/
|
||||
public function suggest(string $name): ?int
|
||||
{
|
||||
$row = $this->where('item_name', $this->normalize($name))->first();
|
||||
|
||||
return $row ? (int) $row['category_id'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zapamietaj wybor uzytkownika (upsert po znormalizowanej nazwie).
|
||||
*/
|
||||
public function remember(string $name, int $categoryId): void
|
||||
{
|
||||
$key = $this->normalize($name);
|
||||
if ($key === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $this->where('item_name', $key)->first();
|
||||
if ($existing) {
|
||||
$this->update($existing['id'], ['category_id' => $categoryId, 'updated_at' => date('Y-m-d H:i:s')]);
|
||||
} else {
|
||||
$this->insert(['item_name' => $key, 'category_id' => $categoryId, 'updated_at' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ReceiptItemModel extends Model
|
||||
{
|
||||
protected $table = 'receipt_items';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = [
|
||||
'receipt_id', 'name', 'display_name', 'qty', 'amount',
|
||||
'suggested_category_id', 'chosen_category_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Pozycje danego paragonu w kolejnosci wprowadzenia.
|
||||
*/
|
||||
public function forReceipt(int $receiptId): array
|
||||
{
|
||||
return $this->where('receipt_id', $receiptId)
|
||||
->orderBy('id', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ReceiptModel extends Model
|
||||
{
|
||||
protected $table = 'receipts';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = [
|
||||
'file_path', 'original_name', 'mime', 'merchant',
|
||||
'receipt_date', 'total', 'status', 'raw_json', 'error_msg',
|
||||
];
|
||||
|
||||
public function hasOriginalName(string $name): bool
|
||||
{
|
||||
return $this->where('original_name', $name)->countAllResults() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista paragonow do historii, z liczba pozycji i utworzonych operacji.
|
||||
*/
|
||||
public function withCounts(): array
|
||||
{
|
||||
return $this->db->table('receipts r')
|
||||
->select('r.*')
|
||||
->select('(SELECT COUNT(*) FROM receipt_items ri WHERE ri.receipt_id = r.id) AS items_count', false)
|
||||
->select('(SELECT COUNT(*) FROM operations o WHERE o.receipt_id = r.id) AS operations_count', false)
|
||||
->orderBy('r.id', 'DESC')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class RememberTokenModel extends Model
|
||||
{
|
||||
public const LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
||||
|
||||
protected $table = 'remember_tokens';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['selector', 'validator_hash', 'expires_at'];
|
||||
|
||||
public function issueToken(): string
|
||||
{
|
||||
$selector = bin2hex(random_bytes(16));
|
||||
$validator = bin2hex(random_bytes(32));
|
||||
|
||||
$this->insert([
|
||||
'selector' => $selector,
|
||||
'validator_hash' => hash('sha256', $validator),
|
||||
'expires_at' => date('Y-m-d H:i:s', time() + self::LIFETIME_SECONDS),
|
||||
]);
|
||||
|
||||
return $selector . ':' . $validator;
|
||||
}
|
||||
|
||||
public function verifyToken(string $token): bool
|
||||
{
|
||||
[$selector, $validator] = $this->splitToken($token);
|
||||
if ($selector === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = $this->where('selector', $selector)->first();
|
||||
if (!$row || strtotime($row['expires_at']) <= time()) {
|
||||
if ($row) {
|
||||
$this->delete($row['id']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals($row['validator_hash'], hash('sha256', $validator));
|
||||
}
|
||||
|
||||
public function deleteToken(string $token): void
|
||||
{
|
||||
[$selector] = $this->splitToken($token);
|
||||
if ($selector !== null) {
|
||||
$this->where('selector', $selector)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteExpired(): void
|
||||
{
|
||||
$this->where('expires_at <=', date('Y-m-d H:i:s'))->delete();
|
||||
}
|
||||
|
||||
private function splitToken(string $token): array
|
||||
{
|
||||
$parts = explode(':', $token, 2);
|
||||
if (count($parts) !== 2 || !ctype_xdigit($parts[0]) || strlen($parts[0]) !== 32
|
||||
|| !ctype_xdigit($parts[1]) || strlen($parts[1]) !== 64) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user