Compare commits
5 Commits
af2ce4732b
...
62cad948b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 62cad948b7 | |||
| 8b3f3e4608 | |||
| a23f3587ab | |||
| 55f7e5814c | |||
| eec15db7dd |
@@ -0,0 +1,51 @@
|
||||
name: Build and Deploy Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.rhost.ovh
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./service/Dockerfile
|
||||
push: true
|
||||
tags: git.rhost.ovh/${{ github.repository }}:latest
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
port: ${{ secrets.SERVER_PORT }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
cd /home/ryjek/docker/taxes
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker compose exec --workdir /var/www/html www APP_RUNTIME_ENV=prod php bin/console secrets:decrypt-to-local --force
|
||||
docker compose exec --workdir /var/www/html www php ./bin/console doctrine:migration:migrate --dry-run
|
||||
docker compose exec --workdir /var/www/html www php ./bin/console cache:clear
|
||||
@@ -13,6 +13,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
www:
|
||||
image: git.rhost.ovh/ryjek/taxes:latest
|
||||
restart: on-failure
|
||||
build:
|
||||
context: .
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
APP_ENV=prod
|
||||
APP_SECRET=%env(APP_SECRET)%
|
||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
DATABASE_URL="postgresql://%env(DATABASE_USER)%:%env(DATABASE_PASSWORD)%@database:5432/taxes?serverVersion=13&charset=utf8"
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
// any CSS you import will output into a single css file (app.css in this case)
|
||||
import './styles/global.less';
|
||||
import './styles/main.less';
|
||||
|
||||
// start the Stimulus application
|
||||
import './bootstrap';
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
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;
|
||||
// window.history.pushState(null, null, "/");
|
||||
}
|
||||
|
||||
// ---- 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 + ')');
|
||||
// const hashSummary = res.url.replace(/^https?:\/\/[^/]+\//, "/").replace(this.endpointUrl, "");
|
||||
// window.history.pushState(null, null, hashSummary);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
@import "table";
|
||||
.main-container {
|
||||
font-family: 'Public Sans', system-ui, sans-serif;
|
||||
color: #1C1C1A;
|
||||
background: #EFEEE9;
|
||||
min-height: 100vh;
|
||||
padding: 56px 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 750px;
|
||||
background: #FBFBF9;
|
||||
border: 1px solid #E6E5E0;
|
||||
border-radius: 18px;
|
||||
padding: 36px 34px 42px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -13px;
|
||||
left: 28px;
|
||||
background: #1C1C1A;
|
||||
color: #FBFBF9;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.rail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 28px;
|
||||
.rail-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: #FFFFFF;
|
||||
color: #B7B6B0;
|
||||
border: 1px solid #E0DFD9;
|
||||
}
|
||||
.rail-label {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #A6A5A0;
|
||||
}
|
||||
.rail-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #E0DFD9;
|
||||
margin: 0 14px;
|
||||
min-width: 24px;
|
||||
}
|
||||
}
|
||||
/* ---- wybór ---- */
|
||||
.choices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.choice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: left;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E6E5E0;
|
||||
border-radius: 13px;
|
||||
padding: 18px 20px;
|
||||
transition: all .15s;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
&[data-accent="div"]:hover {
|
||||
border-color: #3F8F6B;
|
||||
background: #FCFEFD;
|
||||
}
|
||||
&[data-accent="crypto"]:hover {
|
||||
border-color: #C79A3A;
|
||||
background: #FEFDFA;
|
||||
}
|
||||
.choice-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex: none;
|
||||
border-radius: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 21px;
|
||||
font-weight: 700;
|
||||
&.div { background: #EDF6F1; color: #3F8F6B; }
|
||||
&.crypto { background: #F7F1E4; color: #C79A3A; }
|
||||
}
|
||||
.choice-body { flex: 1; }
|
||||
.choice-title { font-size: 15.5px; font-weight: 600; }
|
||||
.choice-sub { font-size: 12.5px; color: #6B6A64; }
|
||||
.arrow { color: #B7B6B0; font-size: 18px; }
|
||||
}
|
||||
|
||||
.head-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
.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 {
|
||||
border: 1.5px dashed #CFCEC8; border-radius: 14px; background: #FFFFFF;
|
||||
padding: 34px 24px; text-align: center; transition: all .15s;
|
||||
&.is-drag { border-color: #3F8F6B; background: #FCFEFD; }
|
||||
.icon {
|
||||
width: 46px; height: 46px; border-radius: 12px; background: #F2F1EC;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin: 0 auto 14px; font-size: 20px; color: #6B6A64;
|
||||
}
|
||||
.title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
||||
.sub { font-size: 12.5px; color: #6B6A64; margin-bottom: 18px; }
|
||||
.btn-dark {
|
||||
background: #1C1C1A; color: #FBFBF9; border: none; border-radius: 9px;
|
||||
padding: 11px 20px; font-size: 13.5px; font-weight: 600; cursor: pointer; font-family: inherit;
|
||||
&:disabled { opacity: .6; cursor: default; }
|
||||
}
|
||||
.add-more { display: block; margin: 12px auto 0; }
|
||||
}
|
||||
.loading-all {
|
||||
margin: auto;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
.summaryTable {
|
||||
.table-head { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
|
||||
.table-title { font-size: 13px; font-weight: 600; }
|
||||
|
||||
|
||||
.table-wrap {
|
||||
border: 1px solid #E6E5E0; border-radius: 10px; overflow-x: auto; background: #FFFFFF;
|
||||
|
||||
table { width: 100%; border-collapse: collapse; font-family: 'IBM Plex Mono', ui-monospace, monospace; font-size: 11.5px; }
|
||||
th {
|
||||
padding: 9px; font-weight: 600; color: #6B6A64; font-size: 10px;
|
||||
text-transform: uppercase; letter-spacing: .03em;
|
||||
border-bottom: 1px solid #E6E5E0; white-space: nowrap; background: #FAFAF7;
|
||||
}
|
||||
td { padding: 9px; border-bottom: 1px solid #F0EFEB; white-space: nowrap; }
|
||||
tfoot tr { background: #FAFAF7; }
|
||||
|
||||
|
||||
.foot-label {
|
||||
font-family: 'Public Sans', sans-serif; font-size: 12px; font-weight: 600;
|
||||
color: #6B6A64; text-transform: uppercase; letter-spacing: .04em;
|
||||
}
|
||||
.foot-value { text-align: right; font-weight: 700; color: #1C1C1A; font-size: 14px; }
|
||||
|
||||
}
|
||||
|
||||
.note { margin-top: 10px; font-size: 12px; color: #6B6A64; }
|
||||
.actions { display: flex; gap: 10px; margin-top: 22px; }
|
||||
.btn-accent {
|
||||
color: #FFFFFF; border: none; border-radius: 9px; padding: 11px 18px;
|
||||
font-size: 13.5px; font-weight: 600; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.btn-ghost {
|
||||
background: none; border: 1px solid #E6E5E0; border-radius: 9px; padding: 11px 18px;
|
||||
font-size: 13.5px; font-weight: 600; color: #3B3A36; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php // prod.APP_SECRET.1cf811 on Thu, 09 Jul 2026 09:49:02 +0000
|
||||
|
||||
return "\xBA\xC4\xD6\xFF\x1Bq\xEF\x21o\x8D\xC4\xD8\xA1\x99\x9D0XT\xA5\x90\x97j\xD1\x90\xA3\x3E\xCB\xAA\x1F\xEB\xE5l\xEFY\xB9\x16\x3D\xE99\xF3\xCA\x0E\xE2\xBErs0\xEC\xBB\x22\x5E\x0C\x93\xA3\xDD\x93\x60\x88\xD9\x0A76\x1A\xCE\x14\x3CD\x17\xC3\xEAT\xAE\x2F\xB95\x16j\xD0\x27T\xE9\x868HB\xFCwx\x04\x3C\x28\xEBi\x92\xDF\xF5\x98\x3E\xD9\x21K\xBE\xACm\x29\xA5\x8A\xFD\x80\xCF\xA6\xDE\xDB\x05t\x26\x96\xDC\x15B\xABh\x81\x83\xDB\xF9\xD9\xECg\xD7d\xC2Y\x9D\x2B\xAF\x15\x40\x26\xDF\xF4\xCDC\x85\xC1\xCFsDE\xF3\x13\x13\xCB\x0C\x9B\x1F\xCBpj\xD5\xBB\x09\x99W\xF55\x21\x06u\x03\xA1E\x87\xB6JU";
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php // prod.DATABASE_PASSWORD.b77cc3 on Thu, 09 Jul 2026 09:32:43 +0000
|
||||
|
||||
return "\x28\xF4k\x14O\x98d\xD3\xA5\xD3\xE5\x02Hoa\xE5jok\x1C8\xCA\x1D\xF0\xF6\xF9\xBC\xAE\x08\xCB\xC1\x03\xAD\x8E\xBB\x16\x60r\x09\xBCe\xB6\x09\xB5\x8E\x09\x22\x0F\xDB\x8A\xC42\xCC\xF5k\x1D\xCA\xE9\xD92USS\xE1";
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php // prod.DATABASE_USER.4d7edf on Thu, 09 Jul 2026 09:41:54 +0000
|
||||
|
||||
return "\xD0\x09\xD1\x96\x96\x16\x00\x94.\x28\x8F\xDE\xE9\xE2\x98-\xD3\xA4\xE4\xBB\x98\x5C4\x0B\xEF\xB2\x15\xE1\xE3\x01\xF0e4v8\x92\x20\x17\x05\x5C\x96g\xF6\x92\xC4\x08\x92\xF6\xCF\xE8\xD9\x7B\xFC\x9A\x05\x0D\x5D";
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php // prod.encrypt.public on Thu, 09 Jul 2026 09:29:52 +0000
|
||||
|
||||
return "\xA59\xA5\x8A\xF3\x02\x9A\xE8r\xDFQ\xCB4\x96\x91\x0E\x40qhg\x0E\x3F\x12\x7D\x06\x0E\x86\x93\x91\xA2H\x10";
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'APP_SECRET' => null,
|
||||
'DATABASE_PASSWORD' => null,
|
||||
'DATABASE_USER' => null,
|
||||
];
|
||||
@@ -8,9 +8,11 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/', name: 'homepage')]
|
||||
public function index() : Response
|
||||
#[Route(path: '/{hash}', name: 'homepage', defaults: ['hash'=>null])]
|
||||
public function index($hash) : Response
|
||||
{
|
||||
return $this->render("index/index.html.twig");
|
||||
return $this->render("index/index.html.twig",[
|
||||
'currentHash'=>$hash,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use _PHPStan_5a70c2d68\Nette\Utils\Json;
|
||||
use App\Enum\MarketNames;
|
||||
use App\Helper\Currency;
|
||||
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\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -21,71 +23,93 @@ use Symfony\Component\Validator\Constraints\File;
|
||||
|
||||
class TaxesController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/taxes/{type}', name: 'app_taxes')]
|
||||
public function index(Request $request, string $type): Response
|
||||
// #[Route(path: '/taxes/{type}', name: 'app_taxes')]
|
||||
// public function index(Request $request, string $type): Response
|
||||
// {
|
||||
// if (!in_array($type, ["crypto", "dividend"])) {
|
||||
// return $this->redirectToRoute("homepage");
|
||||
// }
|
||||
//
|
||||
// $a=[];
|
||||
// $form = $this->createFormBuilder([])
|
||||
// ->add("uploads",FileType::class, [
|
||||
// 'data_class' => null,
|
||||
// 'multiple' => true,
|
||||
// 'attr'=>[
|
||||
// 'multiple' => 'multiple',
|
||||
// ],
|
||||
// 'constraints' => [
|
||||
// new All([
|
||||
// 'constraints' => [
|
||||
// new File([
|
||||
// 'maxSize' => '2048k',
|
||||
// 'mimeTypes' => [
|
||||
// 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
// 'application/atom+xml',
|
||||
// 'application/xml',
|
||||
// 'text/xml',
|
||||
// 'text/xml-external-parsed-entity',
|
||||
// 'text/csv',
|
||||
// 'text/plain',
|
||||
//// 'application/x-pdf',
|
||||
// ],
|
||||
// 'mimeTypesMessage' => 'Please upload a valid XLSX or XML document',
|
||||
// ])
|
||||
// ]
|
||||
// ])
|
||||
// ],
|
||||
// ])
|
||||
// ->add("upload",SubmitType::class)
|
||||
// ->getForm();
|
||||
// $form->handleRequest($request);
|
||||
// if ($form->isSubmitted() && $form->isValid()) {
|
||||
// $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 (!in_array($type, ["crypto", "dividend"])) {
|
||||
return $this->redirectToRoute("homepage");
|
||||
if (!count($files)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$a=[];
|
||||
$form = $this->createFormBuilder([])
|
||||
->add("uploads",FileType::class, [
|
||||
'data_class' => null,
|
||||
'multiple' => true,
|
||||
'attr'=>[
|
||||
'multiple' => 'multiple',
|
||||
],
|
||||
'constraints' => [
|
||||
new All([
|
||||
'constraints' => [
|
||||
new File([
|
||||
'maxSize' => '2048k',
|
||||
'mimeTypes' => [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/atom+xml',
|
||||
'application/xml',
|
||||
'text/xml',
|
||||
'text/xml-external-parsed-entity',
|
||||
'text/csv',
|
||||
'text/plain',
|
||||
// 'application/x-pdf',
|
||||
],
|
||||
'mimeTypesMessage' => 'Please upload a valid XLSX or XML document',
|
||||
])
|
||||
]
|
||||
])
|
||||
],
|
||||
])
|
||||
->add("upload",SubmitType::class)
|
||||
->getForm();
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$files = $form->get('uploads')->getData();
|
||||
$hash = substr(md5(random_int(0, mt_getrandmax())),0,16);
|
||||
if ($files) {
|
||||
$i=1;
|
||||
/** @var UploadedFile $file */
|
||||
foreach ($files as $file) {
|
||||
$newName = $i;
|
||||
$checkName = NeedFileName::check($file->getClientOriginalName());
|
||||
if ($checkName !== null) {
|
||||
$newName .= "-".$checkName;
|
||||
}
|
||||
$file->move(
|
||||
$this->getParameter("uploads_path")."/".$hash,
|
||||
$newName.".".$file->getClientOriginalExtension()
|
||||
);
|
||||
$i++;
|
||||
}
|
||||
return $this->redirectToRoute("app_taxes_{$type}_show",["hash" => $hash]);
|
||||
$hash = substr(md5(random_int(0, mt_getrandmax())), 0, 16);
|
||||
$i = 1;
|
||||
/** @var UploadedFile $file */
|
||||
foreach ($files as $file) {
|
||||
$newName = $i;
|
||||
$checkName = NeedFileName::check($file->getClientOriginalName());
|
||||
if ($checkName !== null) {
|
||||
$newName .= "-" . $checkName;
|
||||
}
|
||||
$file->move(
|
||||
$this->getParameter("uploads_path") . "/" . $hash,
|
||||
$newName . "." . $file->getClientOriginalExtension()
|
||||
);
|
||||
$i++;
|
||||
}
|
||||
|
||||
return $this->renderForm('taxes/index.html.twig', [
|
||||
'controller_name' => 'TaxesController',
|
||||
'form' => $form,
|
||||
'sheet' => $a
|
||||
return $hash;
|
||||
}
|
||||
|
||||
#[Route(path: '/taxes/{type}', name: 'app_taxes_summary', methods: ['POST'])]
|
||||
public function takeSummary(string $type, Request $request, DividendsService $tax, ?MarketNames $market): Response
|
||||
{
|
||||
$files = $request->files->all()['pliki'];
|
||||
$hash = $this->saveFiles($files);
|
||||
if ($hash) {
|
||||
return $this->redirectToRoute("app_taxes_{$type}_show", ["hash" => $hash]);
|
||||
}
|
||||
return $this->json([
|
||||
"type" => $type,
|
||||
"request" => $files,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -94,8 +118,10 @@ class TaxesController extends AbstractController
|
||||
{
|
||||
if ($tax->readFiles($market)) {
|
||||
$gen = $tax->generate();
|
||||
// dd($gen);
|
||||
return $this->render("taxes/show.html.twig", [
|
||||
$taxes = $gen->getSummaryByCountry();
|
||||
$sum = $taxes['sum'];
|
||||
unset($taxes['sum']);
|
||||
return $this->render("taxes/table.html.twig", [
|
||||
"taxes" => $gen->getSummaryByCountry(),
|
||||
"errorDividend" => $gen->getErrorDividend(),
|
||||
"errorString" => $gen->getErrorString(),
|
||||
@@ -104,10 +130,10 @@ class TaxesController extends AbstractController
|
||||
"markets_all" => MarketNames::cases(),
|
||||
"hash_param" => $tax->takeHash(),
|
||||
"current_market" => $market,
|
||||
"sum" => $sum,
|
||||
]);
|
||||
|
||||
}
|
||||
return new Response("error");
|
||||
return new Response("Brak danych do wyświetlenia.");
|
||||
}
|
||||
#[Route(path: '/taxes/crypto/{hash}', name: 'app_taxes_crypto_show')]
|
||||
public function showCrypto(CryptoService $tax) : Response
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helper;
|
||||
final class ClearTypes
|
||||
{
|
||||
public static function clearInt($input) : int
|
||||
public static function clearInt($input): int
|
||||
{
|
||||
if (is_int($input)) {
|
||||
return $input;
|
||||
}
|
||||
return (int) $input;
|
||||
return (int)$input;
|
||||
}
|
||||
|
||||
public static function clearFloat($input): float
|
||||
|
||||
@@ -42,12 +42,19 @@ class DividendsSummary
|
||||
$this->countFromSheets();
|
||||
|
||||
if (count($this->allData) > 0) {
|
||||
$sum = 0;
|
||||
$profitSum = 0;
|
||||
$summaryRow = [
|
||||
"tax" => 0,
|
||||
"profit" => 0,
|
||||
"origin" => 0,
|
||||
"destiny" => 0,
|
||||
];
|
||||
foreach ($this->allData as $key => $item) {
|
||||
$val = round($item['tax'], 2);
|
||||
$sum += $val;
|
||||
$profitSum += $item['incomeAll'];
|
||||
$summaryRow["tax"] += $val;
|
||||
$summaryRow["profit"] += $item['incomeAll'];
|
||||
$summaryRow["origin"] += $item['destiny_tax'];
|
||||
$summaryRow["destiny"] += $item['tax_cost'];
|
||||
|
||||
$return[$key] = [
|
||||
"country" => \Locale::getDisplayRegion("-".$key),
|
||||
"income" => $item['incomeAll'],
|
||||
@@ -58,11 +65,11 @@ class DividendsSummary
|
||||
"incomesArray" => $item['incomes']
|
||||
];
|
||||
}
|
||||
$summaryRow = array_map(function ($item) {
|
||||
return intval(ceil($item));
|
||||
}, $summaryRow);
|
||||
// >
|
||||
$return['sum'] = [
|
||||
"tax" => intval(ceil($sum)),
|
||||
"profit" => $profitSum
|
||||
];
|
||||
$return['sum'] = $summaryRow;
|
||||
}
|
||||
|
||||
return $return;
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Logic\Dividends;
|
||||
|
||||
use App\Helper\Currency;
|
||||
use App\Helper\DataNeed\DividendValues;
|
||||
use ClearTypes;
|
||||
use App\Helper\ClearTypes;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
|
||||
class SheetData extends SheetDataAll
|
||||
|
||||
@@ -10,7 +10,7 @@ class MasterServices
|
||||
protected string $uploadPath;
|
||||
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 = [];
|
||||
$dir=$this->uploadPath . "/" . $this->hashParam;
|
||||
if (!is_dir($dir)) {
|
||||
return false;
|
||||
}
|
||||
$handle = opendir($dir);
|
||||
while (($inDir = readdir($handle)) !== false) {
|
||||
if ($inDir !== "." && $inDir !== "..") {
|
||||
@@ -46,4 +49,9 @@ class MasterServices
|
||||
{
|
||||
return $this->hashParam;
|
||||
}
|
||||
|
||||
public function setHash(string $hash): void
|
||||
{
|
||||
$this->hashParam = $hash;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
|
||||
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,14 +1,86 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Index{% endblock %}
|
||||
|
||||
{% block title %}Rozliczenie podatkowe{% endblock %}
|
||||
{% block body %}
|
||||
<div class="row justify-content-md-center" style="margin: 50px 0;">
|
||||
<div class="col-md-1">
|
||||
<a class="btn btn-primary" href="{{ path('app_taxes',{type:"dividend"}) }}">Dividends</a>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<a class="btn btn-primary" href="{{ path('app_taxes',{type:"crypto"}) }}">Cryptocurrency</a>
|
||||
</div>
|
||||
<div class="main-container">
|
||||
<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">
|
||||
{% for row in [{i:'1', label:'Wybór'}, {i:'2', label:'Pliki'}, {i:'3', label:'Wynik'}] %}
|
||||
<div class="rail-step"
|
||||
data-app-target="railStep"
|
||||
data-index="{{ loop.index }}">
|
||||
<div class="rail-dot">{{ row.i }}</div>
|
||||
<span class="rail-label">{{ row.label }}</span>
|
||||
</div>
|
||||
{% if not loop.last %}
|
||||
<div class="rail-line"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="loading-all" data-app-target="step" data-step="0">
|
||||
<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-body">
|
||||
<div class="choice-title">Dywidendy</div>
|
||||
<div class="choice-sub">Akcje krajowe i zagraniczne · podatek 19%</div>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
</button>
|
||||
<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-body">
|
||||
<div class="choice-title">Kryptowaluty</div>
|
||||
<div class="choice-sub">Sprzedaż aktywów · dochód i podatek 19%</div>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div data-app-target="step" data-step="2" hidden>
|
||||
<div class="head-row">
|
||||
<span class="dot" data-app-target="accentDot"></span>
|
||||
<span class="head-label" data-app-target="choiceLabel"></span>
|
||||
<button type="button" class="link" data-action="app#back">wstecz</button>
|
||||
</div>
|
||||
|
||||
<div class="dropzone"
|
||||
data-action="dragover->app#dragOver dragleave->app#dragLeave drop->app#drop"
|
||||
data-app-target="dropzone">
|
||||
|
||||
<div class="icon">↑</div>
|
||||
<div class="title">Przeciągnij pliki lub wybierz z komputera</div>
|
||||
<div class="sub">CSV lub XLSX · możesz wgrać kilka naraz</div>
|
||||
|
||||
<input type="file" accept=".csv,.xlsx" multiple hidden
|
||||
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 data-app-target="step" data-step="3" hidden>
|
||||
<div data-app-target="result"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Index{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row justify-content-md-center" style="margin: 50px 0;">
|
||||
<div class="col-md-1">
|
||||
<a class="btn btn-primary" href="{{ path('app_taxes',{type:"dividend"}) }}">Dividends</a>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<a class="btn btn-primary" href="{{ path('app_taxes',{type:"crypto"}) }}">Cryptocurrency</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,97 @@
|
||||
<div class="summaryTable">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>L.p.</th>
|
||||
<th>Country</th>
|
||||
<th>Income</th>
|
||||
{% if taxes|first.cost is defined %}
|
||||
<th>Cost</th>
|
||||
{% endif %}
|
||||
{% if taxes|first.destiny_tax is defined %}
|
||||
<th>Origin tax</th>
|
||||
{% endif %}
|
||||
{% if taxes|first.tax_cost is defined %}
|
||||
<th>Deductable tax (15%)</th>
|
||||
{% endif %}
|
||||
{% if taxes|first.profit is defined %}
|
||||
<th>Profit/Loss</th>
|
||||
{% endif %}
|
||||
<th>Tax</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key,tax in taxes %}
|
||||
{% if tax.country is defined %}
|
||||
{% set colspan = tax|length %}
|
||||
{% if tax.income > 0 %}
|
||||
<tr class="{% if tax.profit is defined and tax.profit < 0 %}table-danger{% else %}table-success{% endif %}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ tax.country }}</td>
|
||||
<td>{{ tax.income|price }}</td>
|
||||
{% if tax.cost is defined %}
|
||||
<td>{{ tax.cost|price }}</td>
|
||||
{% endif %}
|
||||
{% if tax.destiny_tax is defined %}
|
||||
<td>{{ tax.destiny_tax|price }}</td>
|
||||
{% endif %}
|
||||
{% if tax.tax_cost is defined %}
|
||||
<td>{{ tax.tax_cost|price }}</td>
|
||||
{% endif %}
|
||||
{% if tax.profit is defined %}
|
||||
<td>{{ tax.profit|price }}</td>
|
||||
{% endif %}
|
||||
<td>{{ tax.tax|price }}</td>
|
||||
</tr>
|
||||
{% if tax.incomesArray is defined and tax.incomesArray|length > 0 %}
|
||||
{% set colspan = colspan-1 %}
|
||||
{% for incomePerc,value in tax.incomesArray %}
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
<td>{{ incomePerc }}% - {{ value|price }}</td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elseif key == "sum" %}
|
||||
{# <tr class="">#}
|
||||
{# <td colspan="{{ colspan-2 }}">Summary:</td>#}
|
||||
{# <td>{{ tax.profit|price("PLN",0) }}</td>#}
|
||||
{# <td>{{ tax.tax|price("PLN",0) }}</td>#}
|
||||
{# </tr>#}
|
||||
{% else %}
|
||||
<tr class="">
|
||||
<td colspan="{{ colspan-1 }}">Summary:</td>
|
||||
<td>{{ tax|price("PLN",0) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2" class="foot-label">Podsumowanie</td>
|
||||
<td class="foot-label">{{ sum.profit|price("PLN",0) }}</td>
|
||||
<td class="foot-label">{{ sum.origin|price("PLN",0) }}</td>
|
||||
<td class="foot-label">{{ sum.destiny|price("PLN",0) }}</td>
|
||||
<td colspan="1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="foot-label">Do zaplaty</td>
|
||||
<td class="foot-value" colspan="4">{{ sum.tax|price("PLN",0) }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#{% if wynik.footNote %}#}
|
||||
<div class="note">Kwota do dopłaty w Polsce po uwzględnieniu podatku zapłaconego za granicą.</div>
|
||||
{#{% endif %}#}
|
||||
|
||||
<div class="actions">
|
||||
{# <button type="button" class="btn-accent" style="background: {{ wynik.accent }}">Eksportuj do PIT</button>#}
|
||||
<button type="button" class="btn-ghost" data-action="app#restart">Od nowa</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
+6
-14
@@ -17,10 +17,6 @@ RUN apt-get update && apt-get -y install curl
|
||||
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
RUN mkdir -p /var/www/.composer
|
||||
RUN chown www-data /var/www/.composer
|
||||
|
||||
|
||||
RUN apt-get install -y \
|
||||
libzip-dev \
|
||||
zip \
|
||||
@@ -52,17 +48,13 @@ COPY services/config/entrypoint.sh /etc/entrypoint.sh
|
||||
|
||||
RUN chmod +x /etc/entrypoint.sh
|
||||
COPY --chown=www-data:www-data ./project /var/www/html
|
||||
#COPY --chown=www-data:www-data ./index.php /var/www/html
|
||||
|
||||
|
||||
#WORKDIR /var/www/html/src/composer
|
||||
#RUN composer install --ignore-platform-reqs --no-scripts
|
||||
|
||||
#COPY --from=minimalise --chown=www-data /var/www/html/src/scripts_generated /var/www/html/src/scripts_generated
|
||||
WORKDIR /var/www/html
|
||||
#RUN composer install
|
||||
#RUN npm install
|
||||
#RUN npm run build
|
||||
#RUN apt-get purge --auto-remove -y npm
|
||||
|
||||
RUN mv .env.prod .env
|
||||
RUN composer install --ignore-platform-reqs --no-scripts
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN apt-get purge --auto-remove -y npm
|
||||
|
||||
ENTRYPOINT ["/etc/entrypoint.sh"]
|
||||
Reference in New Issue
Block a user