update
This commit is contained in:
@@ -16,6 +16,10 @@
|
||||
<label class="form-label">Hasło</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="remember" value="1" class="form-check-input" id="remember">
|
||||
<label class="form-check-label" for="remember">Zapamiętaj mnie przez 30 dni</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Zaloguj</button>
|
||||
<?= form_close() ?>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<ul class="dropdown-menu" aria-labelledby="navFinance">
|
||||
<li><a class="dropdown-item" href="<?= site_url('finances') ?>">Pulpit finansów</a></li>
|
||||
<li><a class="dropdown-item" href="<?= site_url('operations') ?>">Operacje</a></li>
|
||||
<li><a class="dropdown-item" href="<?= site_url('receipts') ?>">Import paragonów</a></li>
|
||||
<li><a class="dropdown-item" href="<?= site_url('categories') ?>">Kategorie</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -55,6 +56,9 @@
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('warning')): ?>
|
||||
<div class="alert alert-warning"><?= esc(session()->getFlashdata('warning')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?= $this->extend('layout/main') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł';
|
||||
$badges = [
|
||||
'parsing' => 'secondary',
|
||||
'parsed' => 'info',
|
||||
'failed' => 'danger',
|
||||
'imported' => 'success',
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="h4 mb-0">Import paragonów</h1>
|
||||
<a href="<?= site_url('receipts/new') ?>" class="btn btn-primary">+ Nowy import</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Sprzedawca</th>
|
||||
<th class="text-end">Suma</th>
|
||||
<th class="text-center">Pozycje</th>
|
||||
<th class="text-center">Operacje</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($receipts)): ?>
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Brak zaimportowanych paragonów.</td></tr>
|
||||
<?php else: foreach ($receipts as $r): ?>
|
||||
<tr>
|
||||
<td><?= esc($r['receipt_date'] ?: mb_substr((string) $r['created_at'], 0, 10)) ?></td>
|
||||
<td><?= esc($r['merchant'] ?: '—') ?></td>
|
||||
<td class="text-end"><?= $r['total'] !== null ? $fmt($r['total']) : '—' ?></td>
|
||||
<td class="text-center"><?= (int) $r['items_count'] ?></td>
|
||||
<td class="text-center"><?= (int) $r['operations_count'] ?></td>
|
||||
<td><span class="badge bg-<?= $badges[$r['status']] ?? 'secondary' ?>"><?= esc($r['status']) ?></span></td>
|
||||
<td class="text-end">
|
||||
<?php if ($r['status'] === 'parsed'): ?>
|
||||
<a href="<?= site_url('receipts/' . $r['id'] . '/review') ?>" class="btn btn-sm btn-outline-primary">Przegląd</a>
|
||||
<?php else: ?>
|
||||
<a href="<?= site_url('receipts/' . $r['id']) ?>" class="btn btn-sm btn-outline-secondary">Podgląd</a>
|
||||
<?php endif; ?>
|
||||
<a href="<?= site_url('receipts/' . $r['id'] . '/delete') ?>" class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Usunąć import?')">Usuń</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?= $this->extend('layout/main') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="h4 mb-0">Nowy import paragonu</h1>
|
||||
<a href="<?= site_url('receipts') ?>" class="btn btn-link">← Historia importów</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<?= form_open_multipart('receipts', ['class' => 'row g-3']) ?>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Plik paragonu</label>
|
||||
<input type="file" name="receipt" class="form-control" required
|
||||
accept=".jpg,.jpeg,.png,.pdf,.json,image/*,application/pdf,application/json">
|
||||
<div class="form-text">
|
||||
Obsługiwane formaty: skan JPG/PNG, PDF lub JSON. Maks. 10 MB.
|
||||
Po wgraniu AI odczyta pozycje i zaproponuje kategorie do zatwierdzenia.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">Wgraj i przetwórz</button>
|
||||
</div>
|
||||
<?= form_close() ?>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,116 @@
|
||||
<?= $this->extend('layout/main') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="h4 mb-0">Przegląd paragonu</h1>
|
||||
<a href="<?= site_url('receipts') ?>" class="btn btn-link">← Historia importów</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body row g-2">
|
||||
<div class="col-md-4"><span class="text-muted small d-block">Sprzedawca</span><?= esc($receipt['merchant'] ?: '—') ?></div>
|
||||
<div class="col-md-4"><span class="text-muted small d-block">Data</span><?= esc($receipt['receipt_date'] ?: '—') ?></div>
|
||||
<div class="col-md-4"><span class="text-muted small d-block">Suma z paragonu (AI)</span><?= $receipt['total'] !== null ? $fmt($receipt['total']) : '—' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= form_open('receipts/' . $receipt['id'] . '/confirm') ?>
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<strong>Pozycje</strong> — sprawdź i popraw nazwy/kwoty (AI potrafi się pomylić), przypisz kategorię
|
||||
(podpowiedź zaznaczona; „— pomiń —” = nie księguj)
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40%">Pozycja</th>
|
||||
<th class="text-end" style="width:16%">Kwota (zł)</th>
|
||||
<th>Kategoria</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($items)): ?>
|
||||
<tr><td colspan="3" class="text-center text-muted py-4">AI nie odczytało żadnych pozycji.</td></tr>
|
||||
<?php else: foreach ($items as $it): ?>
|
||||
<?php $sel = $it['chosen_category_id'] ?? $it['suggested_category_id']; ?>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" name="display[<?= $it['id'] ?>]" class="form-control form-control-sm"
|
||||
value="<?= esc($it['display_name'] ?: $it['name']) ?>" maxlength="255">
|
||||
<span class="text-muted small">z paragonu: <?= esc($it['name']) ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" step="0.01" min="0" name="amount[<?= $it['id'] ?>]"
|
||||
class="form-control form-control-sm text-end js-amount"
|
||||
value="<?= esc(number_format((float) $it['amount'], 2, '.', '')) ?>">
|
||||
</td>
|
||||
<td>
|
||||
<select name="chosen_category_id[<?= $it['id'] ?>]" class="form-select form-select-sm js-cat">
|
||||
<option value="">— pomiń —</option>
|
||||
<?php foreach ($categories as $c): ?>
|
||||
<option value="<?= $c['id'] ?>" <?= (string) $sel === (string) $c['id'] ? 'selected' : '' ?>>
|
||||
<?= esc($c['path_label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-white"><strong>Podgląd księgowania</strong> — jedna operacja na kategorię (suma)</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group" id="groupPreview"><li class="list-group-item text-muted">—</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button type="submit" class="btn btn-primary">Zaksięguj</button>
|
||||
<a href="<?= site_url('receipts/' . $receipt['id'] . '/delete') ?>" class="btn btn-outline-danger"
|
||||
onclick="return confirm('Usunąć ten import?')">Usuń import</a>
|
||||
</div>
|
||||
<?= form_close() ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
// Podglad grupowania po kategorii liczony na zywo z pol wiersza (vanilla, bez zaleznosci).
|
||||
const fmt = (v) => v.toLocaleString('pl-PL', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' zł';
|
||||
const rows = Array.from(document.querySelectorAll('tbody tr')).filter((r) => r.querySelector('.js-cat'));
|
||||
const preview = document.getElementById('groupPreview');
|
||||
|
||||
function labelFor(sel) {
|
||||
return sel.options[sel.selectedIndex].text.trim();
|
||||
}
|
||||
function render() {
|
||||
const groups = {};
|
||||
rows.forEach((row) => {
|
||||
const sel = row.querySelector('.js-cat');
|
||||
if (!sel || !sel.value) return;
|
||||
const amt = parseFloat(row.querySelector('.js-amount').value) || 0;
|
||||
if (!groups[sel.value]) groups[sel.value] = { label: labelFor(sel), sum: 0 };
|
||||
groups[sel.value].sum += amt;
|
||||
});
|
||||
const keys = Object.keys(groups);
|
||||
if (!keys.length) {
|
||||
preview.innerHTML = '<li class="list-group-item text-muted">Brak przypisanych pozycji — nic nie zostanie zaksięgowane.</li>';
|
||||
return;
|
||||
}
|
||||
preview.innerHTML = keys.map((k) =>
|
||||
`<li class="list-group-item d-flex justify-content-between"><span>${groups[k].label}</span><strong>${fmt(groups[k].sum)}</strong></li>`
|
||||
).join('');
|
||||
}
|
||||
rows.forEach((row) => {
|
||||
row.querySelector('.js-cat').addEventListener('change', render);
|
||||
row.querySelector('.js-amount').addEventListener('input', render);
|
||||
});
|
||||
render();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?= $this->extend('layout/main') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="h4 mb-0">Paragon <?= esc($receipt['merchant'] ?: '#' . $receipt['id']) ?></h1>
|
||||
<a href="<?= site_url('receipts') ?>" class="btn btn-link">← Historia importów</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body row g-2">
|
||||
<div class="col-6"><span class="text-muted small d-block">Data</span><?= esc($receipt['receipt_date'] ?: '—') ?></div>
|
||||
<div class="col-6"><span class="text-muted small d-block">Suma (AI)</span><?= $receipt['total'] !== null ? $fmt($receipt['total']) : '—' ?></div>
|
||||
<div class="col-6"><span class="text-muted small d-block">Status</span><?= esc($receipt['status']) ?></div>
|
||||
<div class="col-6"><span class="text-muted small d-block">Plik</span>
|
||||
<a href="<?= site_url('receipts/' . $receipt['id'] . '/file') ?>" target="_blank"><?= esc($receipt['original_name']) ?></a>
|
||||
</div>
|
||||
<?php if ($receipt['status'] === 'failed' && $receipt['error_msg']): ?>
|
||||
<div class="col-12"><div class="alert alert-danger mb-0 mt-2"><?= esc($receipt['error_msg']) ?></div></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-white"><strong>Pozycje</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 align-middle">
|
||||
<thead class="table-light"><tr><th>Pozycja</th><th class="text-end">Kwota</th></tr></thead>
|
||||
<tbody>
|
||||
<?php if (empty($items)): ?>
|
||||
<tr><td colspan="2" class="text-center text-muted py-3">Brak pozycji.</td></tr>
|
||||
<?php else: foreach ($items as $it): ?>
|
||||
<tr><td><?= esc($it['display_name'] ?: $it['name']) ?></td><td class="text-end"><?= $fmt($it['amount']) ?></td></tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<?php if ($receipt['status'] === 'parsed'): ?>
|
||||
<a href="<?= site_url('receipts/' . $receipt['id'] . '/review') ?>" class="btn btn-primary">Przejdź do przeglądu</a>
|
||||
<?php endif; ?>
|
||||
<a href="<?= site_url('receipts/' . $receipt['id'] . '/delete') ?>" class="btn btn-outline-danger"
|
||||
onclick="return confirm('Usunąć ten import?')">Usuń import</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<?php if ($receipt['status'] === 'imported'): ?>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white"><strong>Zaksięgowane operacje</strong></div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach ($operations as $o): ?>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<span><?= esc($o['category_name'] ?: '(bez kategorii)') ?></span>
|
||||
<strong class="amount-expense"><?= $fmt($o['amount']) ?></strong>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (str_starts_with((string) $receipt['mime'], 'image/')): ?>
|
||||
<div class="card shadow-sm mt-3">
|
||||
<div class="card-header bg-white"><strong>Skan</strong></div>
|
||||
<div class="card-body text-center">
|
||||
<img src="<?= site_url('receipts/' . $receipt['id'] . '/file') ?>" class="img-fluid" alt="Skan paragonu">
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
Reference in New Issue
Block a user