connect new frontend to backend

This commit is contained in:
2026-07-07 22:13:24 +02:00
parent 55f7e5814c
commit a23f3587ab
6 changed files with 312 additions and 57 deletions
@@ -0,0 +1,168 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = [
'railStep', 'step', 'accentDot', 'choiceLabel', 'dropzone',
'input', 'chips', 'submitBtn', 'addMore', 'error', 'result','loading-all',
];
static values = {
step: { type: Number, default: 1 },
hash: { type: String, default: '' },
showUrl: {type: String, default: null },
};
connect() {
this.type = null;
this.endpointUrl=null;
this.files = [];
if(this.showUrlValue !== null) {
this.loadTable();
this.stepValue = 0;
}
this.render();
}
// ---- nawigacja ----
pick(event) {
const btn = event.currentTarget;
this.type = btn.dataset.type;
this.choiceLabelTarget.textContent = btn.dataset.label;
this.accentDotTarget.style.background = this.accent;
this.endpointUrl = btn.dataset.endpoint;
this.files = [];
this.updateChips();
this.hideError();
this.stepValue = 2;
}
back() {
if (this.stepValue <= 2) {
this.type = null;
this.stepValue = 1;
} else {
this.stepValue = 2;
}
}
restart() {
this.type = null;
this.files = [];
this.updateChips();
this.resultTarget.innerHTML = '';
this.hideError();
this.stepValue = 1;
}
// ---- pliki ----
openPicker() {
this.inputTarget.click();
}
onFiles() {
this.files = Array.from(this.inputTarget.files || []);
this.updateChips();
}
dragOver(event) {
event.preventDefault();
this.dropzoneTarget.classList.add('is-drag');
}
dragLeave() {
this.dropzoneTarget.classList.remove('is-drag');
}
drop(event) {
event.preventDefault();
this.dropzoneTarget.classList.remove('is-drag');
this.files = Array.from(event.dataTransfer.files || []);
this.updateChips();
}
async submit() {
if (this.files.length === 0) {
this.openPicker();
return;
}
this.hideError();
this.setLoading(true);
try {
const fd = new FormData();
fd.append('typ', this.type);
this.files.forEach((f) => fd.append('pliki[]', f));
const res = await fetch(this.endpointUrl, { method: 'POST', body: fd });
if (!res.ok) throw new Error('Błąd serwera (' + res.status + ')');
this.resultTarget.innerHTML = await res.text();
this.stepValue = 3;
} catch (err) {
this.showError(err.message || 'Nie udało się obliczyć zestawienia.');
} finally {
this.setLoading(false);
}
}
async loadTable(){
this.setLoading(true);
try {
const res = await fetch(this.showUrlValue, { method: 'GET'});
if(!res.ok){
throw new Error('Błąd serwera (' + res.status + ')');
}
this.resultTarget.innerHTML = await res.text();
this.stepValue = 3;
}catch (err) {
this.showError(err.message || 'Nie udało się zaladowac zestawienia.');
} finally {
this.setLoading(false);
}
}
// ---- reakcja na zmianę kroku ----
stepValueChanged() {
this.render();
}
render() {
this.stepTargets.forEach((el) => {
el.hidden = Number(el.dataset.step) !== this.stepValue;
});
this.railStepTargets.forEach((el) => {
const idx = Number(el.dataset.index);
el.classList.toggle('is-active', idx === this.stepValue);
el.classList.toggle('is-done', idx < this.stepValue);
});
}
// ---- helpery ----
get accent() {
return this.type === 'krypto' ? '#C79A3A' : '#3F8F6B';
}
updateChips() {
const has = this.files.length > 0;
this.chipsTarget.hidden = !has;
this.addMoreTarget.hidden = !has;
this.chipsTarget.innerHTML = this.files
.map((f) => `<span class="chip">${f.name}</span>`)
.join('');
this.submitBtnTarget.textContent = has ? 'Oblicz zestawienie' : 'Wybierz pliki';
}
setLoading(on) {
this.submitBtnTarget.disabled = on;
this.submitBtnTarget.textContent = on
? 'Obliczanie…'
: (this.files.length ? 'Oblicz zestawienie' : 'Wybierz pliki');
}
showError(msg) {
this.errorTarget.textContent = msg;
this.errorTarget.hidden = false;
}
hideError() {
this.errorTarget.hidden = true;
}
}
+29 -22
View File
@@ -41,6 +41,27 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
&.is-done {
.rail-dot {
background: #E7EFE9;
color: #3F8F6B;
border-color: #CDE3D5;
}
.rail-label {
color: #3B3A36;
}
}
&.is-active{
.rail-dot {
background: #1C1C1A;
color: #FBFBF9;
border-color: #1C1C1A;
}
.rail-label {
color: #1C1C1A;
}
}
} }
.rail-dot { .rail-dot {
width: 26px; width: 26px;
@@ -68,27 +89,6 @@
margin: 0 14px; margin: 0 14px;
min-width: 24px; min-width: 24px;
} }
&.is-done {
.rail-dot {
background: #E7EFE9;
color: #3F8F6B;
border-color: #CDE3D5;
}
.rail-label {
color: #3B3A36;
}
}
&.is-active{
.rail-dot {
background: #1C1C1A;
color: #FBFBF9;
border-color: #1C1C1A;
}
.rail-label {
color: #1C1C1A;
}
}
} }
/* ---- wybór ---- */ /* ---- wybór ---- */
.choices { .choices {
@@ -148,6 +148,10 @@
} }
.head-label { font-size: 14px; font-weight: 600; } .head-label { font-size: 14px; font-weight: 600; }
} }
.link {
margin-left: auto; background: none; border: none; color: #6B6A64;
font-size: 13px; text-decoration: underline; padding: 0; cursor: pointer; font-family: inherit;
}
.dropzone { .dropzone {
border: 1.5px dashed #CFCEC8; border-radius: 14px; background: #FFFFFF; border: 1.5px dashed #CFCEC8; border-radius: 14px; background: #FFFFFF;
padding: 34px 24px; text-align: center; transition: all .15s; padding: 34px 24px; text-align: center; transition: all .15s;
@@ -164,5 +168,8 @@
padding: 11px 20px; font-size: 13.5px; font-weight: 600; cursor: pointer; font-family: inherit; padding: 11px 20px; font-size: 13.5px; font-weight: 600; cursor: pointer; font-family: inherit;
&:disabled { opacity: .6; cursor: default; } &:disabled { opacity: .6; cursor: default; }
} }
.add-more { display: block; margin: 12px auto 0; }
}
.loading-all {
margin: auto;
} }
+5 -3
View File
@@ -8,9 +8,11 @@ use Symfony\Component\Routing\Annotation\Route;
class IndexController extends AbstractController class IndexController extends AbstractController
{ {
#[Route(path: '/', name: 'homepage')] #[Route(path: '/{hash}', name: 'homepage', defaults: ['hash'=>null])]
public function index() : Response public function index($hash) : Response
{ {
return $this->render("index/index.html.twig"); return $this->render("index/index.html.twig",[
'currentHash'=>$hash,
]);
} }
} }
+47 -9
View File
@@ -2,6 +2,7 @@
namespace App\Controller; namespace App\Controller;
use _PHPStan_5a70c2d68\Nette\Utils\Json;
use App\Enum\MarketNames; use App\Enum\MarketNames;
use App\Helper\Currency; use App\Helper\Currency;
use App\Helper\DataNeed\NeedFileName; use App\Helper\DataNeed\NeedFileName;
@@ -13,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@@ -62,8 +64,23 @@ class TaxesController extends AbstractController
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$files = $form->get('uploads')->getData(); $files = $form->get('uploads')->getData();
$hash = $this->saveFiles($files);
return $this->redirectToRoute("app_taxes_{$type}_show",["hash" => $hash]);
}
return $this->renderForm('taxes/index.html.twig', [
'controller_name' => 'TaxesController',
'form' => $form,
'sheet' => $a
]);
}
private function saveFiles(array $files) : string
{
if (!count($files)) {
return "";
}
$hash = substr(md5(random_int(0, mt_getrandmax())), 0, 16); $hash = substr(md5(random_int(0, mt_getrandmax())), 0, 16);
if ($files) {
$i = 1; $i = 1;
/** @var UploadedFile $file */ /** @var UploadedFile $file */
foreach ($files as $file) { foreach ($files as $file) {
@@ -78,14 +95,36 @@ class TaxesController extends AbstractController
); );
$i++; $i++;
} }
return $this->redirectToRoute("app_taxes_{$type}_show",["hash" => $hash]);
} return $hash;
} }
return $this->renderForm('taxes/index.html.twig', [ #[Route(path: '/taxes/summary/{type}', name: 'app_taxes_summary', methods: ['POST'])]
'controller_name' => 'TaxesController', public function takeSummary(string $type, Request $request, DividendsService $tax, ?MarketNames $market): Response
'form' => $form, {
'sheet' => $a $files = $request->files->all()['pliki'];
$hash = $this->saveFiles($files);
if ($hash) {
$tax->setHash($hash);
if ($tax->readFiles($market)) {
$gen = $tax->generate();
// dd($gen);
return $this->render("taxes/show.html.twig", [
"taxes" => $gen->getSummaryByCountry(),
"errorDividend" => $gen->getErrorDividend(),
"errorString" => $gen->getErrorString(),
"rawCount" => $gen->takeCountRawData(),
"markets" => $gen->getMarkets(),
"markets_all" => MarketNames::cases(),
"hash_param" => $tax->takeHash(),
"current_market" => $market,
]);
}
}
return $this->json([
"type" => $type,
"request" => $files,
]); ]);
} }
@@ -105,9 +144,8 @@ class TaxesController extends AbstractController
"hash_param" => $tax->takeHash(), "hash_param" => $tax->takeHash(),
"current_market" => $market, "current_market" => $market,
]); ]);
} }
return new Response("error"); return new Response("Brak danych do wyświetlenia.");
} }
#[Route(path: '/taxes/crypto/{hash}', name: 'app_taxes_crypto_show')] #[Route(path: '/taxes/crypto/{hash}', name: 'app_taxes_crypto_show')]
public function showCrypto(CryptoService $tax) : Response public function showCrypto(CryptoService $tax) : Response
+9 -1
View File
@@ -10,7 +10,7 @@ class MasterServices
protected string $uploadPath; protected string $uploadPath;
protected array $allFiles; protected array $allFiles;
public function __construct(protected EntityManager $manager, protected string $hashParam) public function __construct(protected EntityManager $manager, protected ?string $hashParam)
{ {
} }
@@ -23,6 +23,9 @@ class MasterServices
{ {
$fileNames = []; $fileNames = [];
$dir=$this->uploadPath . "/" . $this->hashParam; $dir=$this->uploadPath . "/" . $this->hashParam;
if (!is_dir($dir)) {
return false;
}
$handle = opendir($dir); $handle = opendir($dir);
while (($inDir = readdir($handle)) !== false) { while (($inDir = readdir($handle)) !== false) {
if ($inDir !== "." && $inDir !== "..") { if ($inDir !== "." && $inDir !== "..") {
@@ -46,4 +49,9 @@ class MasterServices
{ {
return $this->hashParam; return $this->hashParam;
} }
public function setHash(string $hash): void
{
$this->hashParam = $hash;
}
} }
+43 -11
View File
@@ -3,10 +3,18 @@
{% block title %}Rozliczenie podatkowe{% endblock %} {% block title %}Rozliczenie podatkowe{% endblock %}
{% block body %} {% block body %}
<div class="main-container"> <div class="main-container">
<section class="card"> <section class="card"
data-controller="app"
data-app-step-value="1"
{% if currentHash %}
data-app-show-url-value="{{ path('app_taxes_dividend_show',{'hash': currentHash}) }}"
{% endif %}
>
<div class="rail"> <div class="rail">
{% for row in [{i:'1', label:'Wybór'}, {i:'2', label:'Pliki'}, {i:'3', label:'Wynik'}] %} {% for row in [{i:'1', label:'Wybór'}, {i:'2', label:'Pliki'}, {i:'3', label:'Wynik'}] %}
<div class="rail-step" data-index="{{ loop.index }}"> <div class="rail-step"
data-app-target="railStep"
data-index="{{ loop.index }}">
<div class="rail-dot">{{ row.i }}</div> <div class="rail-dot">{{ row.i }}</div>
<span class="rail-label">{{ row.label }}</span> <span class="rail-label">{{ row.label }}</span>
</div> </div>
@@ -15,8 +23,13 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
<div class="choices"> <div class="loading-all" data-app-target="step" data-step="0">
<button type="button" class="choice" data-label="Dywidendy"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="choices" data-app-target="step" data-step="1" hidden>
<button type="button" class="choice" data-label="Dywidendy" data-type="dividends" data-action="app#pick" data-endpoint="{{ path('app_taxes_summary',{type:"dividend"}) }}">
<div class="choice-icon div">%</div> <div class="choice-icon div">%</div>
<div class="choice-body"> <div class="choice-body">
<div class="choice-title">Dywidendy</div> <div class="choice-title">Dywidendy</div>
@@ -24,7 +37,7 @@
</div> </div>
<span class="arrow">→</span> <span class="arrow">→</span>
</button> </button>
<button type="button" class="choice" data-label="Kryptowaluty"> <button type="button" class="choice" data-label="Kryptowaluty" data-type="crypto" data-action="app#pick" data-endpoint="{{ path('app_taxes_summary',{type:"crypto"}) }}">
<div class="choice-icon crypto">₿</div> <div class="choice-icon crypto">₿</div>
<div class="choice-body"> <div class="choice-body">
<div class="choice-title">Kryptowaluty</div> <div class="choice-title">Kryptowaluty</div>
@@ -34,21 +47,40 @@
</button> </button>
</div> </div>
<div > <div data-app-target="step" data-step="2" hidden>
<div class="head-row"> <div class="head-row">
<span class="dot" ></span> <span class="dot" data-app-target="accentDot"></span>
<span class="head-label">Head label</span> <span class="head-label" data-app-target="choiceLabel"></span>
<button type="button" class="link" data-action="app#back">wstecz</button>
</div> </div>
<div class="dropzone"> <div class="dropzone"
data-action="dragover->app#dragOver dragleave->app#dragLeave drop->app#drop"
data-app-target="dropzone">
<div class="icon">↑</div> <div class="icon">↑</div>
<div class="title">Przeciągnij pliki lub wybierz z komputera</div> <div class="title">Przeciągnij pliki lub wybierz z komputera</div>
<div class="sub">CSV lub XLSX · możesz wgrać kilka naraz</div> <div class="sub">CSV lub XLSX · możesz wgrać kilka naraz</div>
<input type="file" accept=".csv,.xlsx" multiple hidden> <input type="file" accept=".csv,.xlsx" multiple hidden
<button type="button" class="btn-dark">Wybierz pliki</button> data-app-target="input"
data-action="app#onFiles"
>
<div class="chips" data-app-target="chips" hidden></div>
<button type="button" class="btn-dark"
data-app-target="submitBtn"
data-action="app#submit">Wybierz pliki</button>
<button type="button" class="link add-more" hidden
data-app-target="addMore"
data-action="app#openPicker">dodaj kolejne</button>
<div class="error" data-app-target="error" hidden></div>
</div> </div>
</div> </div>
<div data-app-target="step" data-step="3" hidden>
<div data-app-target="result"></div>
</div>
</section> </section>
</div> </div>
{% endblock %} {% endblock %}