diff --git a/.gitignore b/.gitignore
index 39783ad..d2d88a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,5 +23,6 @@ config/local.neon
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+.idea/
-bin/objednavky
\ No newline at end of file
+bin/objednavky
diff --git a/app/Form/InvoiceForm/InvoiceForm.latte b/app/Form/InvoiceForm/InvoiceForm.latte
new file mode 100644
index 0000000..81205bb
--- /dev/null
+++ b/app/Form/InvoiceForm/InvoiceForm.latte
@@ -0,0 +1,45 @@
+{import ../../Presenters/templates/@commonBlocks.latte}
+
+{varType ?Netvor\Invoice\Model\Entities\Invoice $invoice}
+
+{if $invoice === null} {* We have no invoice yet, then create one. *}
+
Nová faktura
+
+{else} {* We have invoice, add payment if wanted. *}
+ Přidat platbu k faktuře {$invoice->getId()}
+
+{/if}
diff --git a/app/Form/InvoiceForm/InvoiceForm.php b/app/Form/InvoiceForm/InvoiceForm.php
new file mode 100644
index 0000000..8b3dffb
--- /dev/null
+++ b/app/Form/InvoiceForm/InvoiceForm.php
@@ -0,0 +1,183 @@
+template->setFile(__DIR__ . '/InvoiceForm.latte');
+ $this->template->invoice = $this->invoice;
+ $this->template->render();
+ }
+
+
+ public function createComponentPaymentForm(): Form
+ {
+ $form = new Form;
+ $form->addProtection('Vaše relace vypršela. Vraťte se na domovskou stránku a zkuste to znovu.');
+
+ $form->addText('amount')
+ ->setRequired('Zadejte prosím částku.');
+
+ $form->addHidden('invoiceId', $this->invoice?->getId() ?? null)
+ ->setRequired();
+ $form->addSubmit('submit');
+
+ $form->onValidate[] = [$this, 'paymentFormValidate'];
+ $form->onSuccess[] = [$this, 'paymentFormSuccess'];
+ $form->onError[] = function (): void {
+ if ($this->getPresenter()->isAjax() === true) {
+ $this->redrawControl('paymentForm');
+ return;
+ }
+
+ $this->redirect('this');
+ };
+
+ return $form;
+ }
+
+
+ public function paymentFormValidate(Form $form, stdClass $data): void
+ {
+ if (Validators::isNumeric($data->amount) === false) {
+ /** @var TextInput $amountInput */
+ $amountInput = $form['amount'];
+ $amountInput->addError('Částka musí být validní číslo.');
+ }
+ }
+
+
+ public function paymentFormSuccess(Form $form, stdClass $data): void
+ {
+ $this->invoice = $this->invoiceService->get((int) $data->invoiceId);
+
+ if ($this->invoice === null) {
+ $form->addError('Faktura ke které se snažíte přidat platbu neexistuje.');
+ return;
+ }
+
+ $amount = Money::CZK((int) ((float) $data->amount * 100)); // TODO: currency
+ $unpaidAmount = $this->invoice->getUnpaidAmount();
+
+ if ((int) $amount->getAmount() > $unpaidAmount) {
+ /** @var TextInput $amountInput */
+ $amountInput = $form['amount'];
+ $amountInput->addError(sprintf('Částka nemůže převyšovat %dKč.', $unpaidAmount / 100));
+ return;
+ }
+
+ $this->invoiceService->addPayment(new Payment($this->invoice, $amount));
+ $this->invoice = null;
+ }
+
+
+ protected function createComponentInvoiceForm(): Form
+ {
+ // TODO: translate
+ $form = new Form;
+ $form->addProtection('Vaše relace vypršela. Vraťte se na domovskou stránku a zkuste to znovu.');
+
+ $form->addText('amount')
+ ->setRequired('Zadejte prosím částku.');
+
+ $form->addText('issueDate')
+ ->setHtmlType('date')
+ ->setRequired('Zvolte prosím datum vytavení')
+ ->setDefaultValue(date('Y-m-d'));
+
+ $form->addText('dueDate')
+ ->setHtmlType('date')
+ ->setRequired('Zvolte prosím datum splatnosti')
+ ->setDefaultValue(date('Y-m-d'));
+
+ $form->addSubmit('submit');
+
+ $form->onSuccess[] = [$this, 'invoiceFormSuccess'];
+ $form->onValidate[] = [$this, 'invoiceFormValidate'];
+ $form->onError[] = function (): void {
+ if ($this->getPresenter()->isAjax() === true) {
+ $this->redrawControl('invoiceForm');
+ return;
+ }
+
+ $this->redirect('this');
+ };
+
+ return $form;
+ }
+
+
+ public function invoiceFormValidate(Form $form, stdClass $data): void
+ {
+ // TODO: translate
+ if (Validators::isNumeric($data->amount) === false) {
+ /** @var TextInput $amountInput */
+ $amountInput = $form['amount'];
+ $amountInput->addError('Částka musí být validní číslo.');
+ }
+ }
+
+
+ /**
+ * @throws AbortException
+ */
+ public function invoiceFormSuccess(Form $form, stdClass $data): void
+ {
+ $issueDate = DateTimeImmutable::createFromFormat('Y-m-d', $data->issueDate);
+
+ if ($issueDate === false) {
+ /** @var TextInput $issueDateInput */
+ $issueDateInput = $form['issueDate'];
+ $issueDateInput->addError('Zadejte prosím platné datum.');
+ return;
+ }
+
+ $dueDate = DateTimeImmutable::createFromFormat('Y-m-d', $data->issueDate);
+
+ if ($dueDate === false) {
+ /** @var TextInput $dueDateInput */
+ $dueDateInput = $form['dueDate'];
+ $dueDateInput->addError('Zadejte prosím platné datum.');
+ return;
+ }
+
+ $amount = Money::CZK((int) ((float) $data->amount * 100)); // TODO: currency
+ $this->invoiceService->create($this->client, $amount, $issueDate, $dueDate);
+
+ if ($this->getPresenter()->isAjax() === false) {
+ $this->redirect('this');
+ }
+
+ $form->reset();
+ $this->redrawControl('invoiceForm');
+ }
+}
diff --git a/app/Form/InvoiceForm/InvoiceFormFactory.php b/app/Form/InvoiceForm/InvoiceFormFactory.php
new file mode 100644
index 0000000..ff604b3
--- /dev/null
+++ b/app/Form/InvoiceForm/InvoiceFormFactory.php
@@ -0,0 +1,13 @@
+Částka: {$invoice->amount|number:2, ',', ' '} Kč
-Datum vystavení: {$invoice->issueDate|date:'j. n. Y'}
+Částka: {$invoice->getAmount()->getAmount() / 100|number:2, ',', ' '} Kč
+Datum vystavení: {$invoice->getIssueDate()|date:'j. n. Y'}
diff --git a/app/Model/ARESRegistrySubjectFinder.php b/app/Model/ARESRegistrySubjectFinder.php
new file mode 100644
index 0000000..e8d83bd
--- /dev/null
+++ b/app/Model/ARESRegistrySubjectFinder.php
@@ -0,0 +1,22 @@
+ 'CURRENT_TIMESTAMP'],
+ )]
+ private DateTimeImmutable $createdAt;
public function __construct(
@@ -73,7 +61,7 @@ public function __construct(
$this->setCity($city);
$this->setPostalCode($postalCode);
- $this->createdAt = new Nette\Utils\DateTime;
+ $this->createdAt = new DateTimeImmutable;
}
@@ -95,9 +83,6 @@ public function getIc(): string
}
- /**
- * @return $this
- */
public function setIc(string $ic): self
{
$this->ic = $ic;
@@ -124,9 +109,6 @@ public function getFirstName(): string
}
- /**
- * @return $this
- */
public function setFirstName(string $firstName): self
{
$this->firstName = $firstName;
@@ -140,9 +122,6 @@ public function getLastName(): string
}
- /**
- * @return $this
- */
public function setLastName(string $lastName): self
{
$this->lastName = $lastName;
@@ -169,9 +148,6 @@ public function getCity(): string
}
- /**
- * @return $this
- */
public function setCity(string $city): self
{
$this->city = $city;
@@ -185,9 +161,6 @@ public function getPostalCode(): string
}
- /**
- * @return $this
- */
public function setPostalCode(string $postalCode): self
{
$this->postalCode = $postalCode;
@@ -195,8 +168,8 @@ public function setPostalCode(string $postalCode): self
}
- public function getCreatedAt(): Nette\Utils\DateTime
+ public function getCreatedAt(): DateTimeImmutable
{
- return Nette\Utils\DateTime::from($this->createdAt);
+ return $this->createdAt;
}
}
diff --git a/app/Model/Entities/Invoice.php b/app/Model/Entities/Invoice.php
index 7819298..395d799 100644
--- a/app/Model/Entities/Invoice.php
+++ b/app/Model/Entities/Invoice.php
@@ -4,46 +4,58 @@
namespace Netvor\Invoice\Model\Entities;
+use DateTimeImmutable;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
-use Nette;
+use Money\Money;
-/**
- * @ORM\Entity
- * @property-read ?int $id
- * @property-read Client $client
- * @property-read string $amount
- * @property-read Nette\Utils\DateTime $issueDate
- */
+#[ORM\Entity]
class Invoice
{
- use Nette\SmartObject;
-
- /**
- * @ORM\Column(type="integer")
- * @ORM\Id
- * @ORM\GeneratedValue
- */
+ #[ORM\Column(type: Types::INTEGER)]
+ #[ORM\Id]
+ #[ORM\GeneratedValue]
private ?int $id = null;
- /**
- * @ORM\ManyToOne(targetEntity="Client")
- * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
- */
+ #[ORM\ManyToOne(targetEntity: Client::class)]
+ #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Client $client;
- /** @ORM\Column(type="decimal", precision=12, scale=2) */
- private string $amount;
+ #[ORM\Column(type: Types::STRING)]
+ private int $amount;
- /** @ORM\Column(type="datetime", options={"default": "CURRENT_TIMESTAMP"}) */
- private \DateTime $issueDate;
+ #[ORM\Column(
+ type: Types::DATETIME_IMMUTABLE,
+ options: ['default' => 'CURRENT_TIMESTAMP'],
+ )]
+ private DateTimeImmutable $issueDate;
+ // must be nullable, because we can already have some data in db if already in production
+ // and that would end-up with error, since current rows have no default value
+ // some different strategy can be used, just to be sure for this case, where we already have data
+ #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
+ private ?DateTimeImmutable $dueDate;
+
+ /** @var Collection */
+ #[ORM\OneToMany(mappedBy: 'invoice', targetEntity: Payment::class, fetch: 'EAGER')]
+ private Collection $payments;
+
+
+ public function __construct(
+ Client $client,
+ Money $amount,
+ DateTimeImmutable $issueDate,
+ ?DateTimeImmutable $dueDate = null,
+ ) {
+ $this->payments = new ArrayCollection;
- public function __construct(Client $client, string $amount, \DateTime $issueDate)
- {
- $this->client = $client;
$this->setAmount($amount);
- $this->setIssueDate($issueDate);
+ $this->client = $client;
+ $this->issueDate = $issueDate;
+ $this->dueDate = $dueDate;
}
@@ -65,38 +77,91 @@ public function getClient(): Client
}
- public function getAmount(): string
+ public function getAmount(): Money
{
- return $this->amount;
+ // TODO: currency
+ return Money::CZK($this->amount);
}
- /**
- * @return $this
- */
- public function setAmount(string $amount): self
+ public function setAmount(Money $amount): self
{
- if (!Nette\Utils\Validators::isNumeric($amount)) {
- throw new \InvalidArgumentException;
- }
+ $this->amount = (int) $amount->getAmount();
+ return $this;
+ }
+
+
+ public function getDueDate(): ?DateTimeImmutable
+ {
+ return $this->dueDate;
+ }
+
+
+ public function setDueDate(?DateTimeImmutable $dueDate): self
+ {
+ $this->dueDate = $dueDate;
+ return $this;
+ }
+
+
+ public function getIssueDate(): DateTimeImmutable
+ {
+ return $this->issueDate;
+ }
- $this->amount = $amount;
+
+ public function setIssueDate(DateTimeImmutable $issueDate): self
+ {
+ $this->issueDate = $issueDate;
return $this;
}
- public function getIssueDate(): Nette\Utils\DateTime
+ public function getPaidAmount(): int
+ {
+ $paidAmount = 0;
+
+ foreach ($this->payments as $payment) {
+ $paidAmount += (int) $payment->getAmount()->getAmount();
+ }
+
+ return $paidAmount;
+ }
+
+
+ public function getUnpaidAmount(): int
{
- return Nette\Utils\DateTime::from($this->issueDate);
+ $unpaidAmount = $this->amount;
+
+ foreach ($this->payments as $payment) {
+ $unpaidAmount -= (int) $payment->getAmount()->getAmount();
+ }
+
+ return $unpaidAmount;
}
- /**
- * @return $this
- */
- public function setIssueDate(\DateTime $issueDate): self
+ /** @return Payment[] */
+ public function getPayments(): array
{
- $this->issueDate = Nette\Utils\DateTime::from($issueDate);
+ return $this->payments->toArray();
+ }
+
+
+ /** @param Payment[] $payments */
+ public function setPayments(array $payments): self
+ {
+ $this->payments = new ArrayCollection($payments);
+
+ return $this;
+ }
+
+
+ public function addPayment(Payment $payment): self
+ {
+ $payment->setInvoice($this);
+ $this->payments[] = $payment;
+
return $this;
}
}
diff --git a/app/Model/Entities/Payment.php b/app/Model/Entities/Payment.php
new file mode 100644
index 0000000..1d6d26d
--- /dev/null
+++ b/app/Model/Entities/Payment.php
@@ -0,0 +1,94 @@
+ 'CURRENT_TIMESTAMP'],
+ )]
+ private DateTimeImmutable $createdAt;
+
+
+ public function __construct(Invoice $invoice, Money $amount)
+ {
+ $this->invoice = $invoice;
+ $this->createdAt = new DateTimeImmutable;
+
+ $this->setAmount($amount);
+ }
+
+
+ public function __clone()
+ {
+ $this->id = null;
+ }
+
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+
+ public function getInvoice(): Invoice
+ {
+ return $this->invoice;
+ }
+
+
+ public function setInvoice(Invoice $invoice): self
+ {
+ $this->invoice = $invoice;
+ return $this;
+ }
+
+
+ public function getAmount(): Money
+ {
+ // TODO: currency
+ return Money::CZK($this->amount);
+ }
+
+
+ public function setAmount(Money $amount): self
+ {
+ $this->amount = (int) $amount->getAmount();
+ return $this;
+ }
+
+
+ public function getCreatedAt(): DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+
+ public function setCreatedAt(DateTimeImmutable $createdAt): self
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+}
diff --git a/app/Model/IRegistrySubjectFinder.php b/app/Model/IRegistrySubjectFinder.php
new file mode 100644
index 0000000..3752399
--- /dev/null
+++ b/app/Model/IRegistrySubjectFinder.php
@@ -0,0 +1,12 @@
+ */
- private EntityRepository $repository;
+ private ObjectRepository $repository;
private MailService $mailService;
@@ -37,6 +45,40 @@ public function get(int $id): ?Entities\Invoice
}
+ /**
+ * @return Invoice[]
+ */
+ public function findUnpaidInvoicesByClient(Client $client): array
+ {
+ $sumPaymentQuery = $this->entityManager->createQueryBuilder();
+ $sumPaymentQuery->select('SUM(p1.amount)')
+ ->from(Payment::class, 'p1')
+ ->where($sumPaymentQuery->expr()->eq('p1.invoice', 'i'));
+
+ $countPaymentQuery = $this->entityManager->createQueryBuilder();
+ $countPaymentQuery->select('COUNT(p2.amount)')
+ ->from(Payment::class, 'p2')
+ ->where($countPaymentQuery->expr()->eq('p2.invoice', 'i'));
+
+ $qb = $this->entityManager->createQueryBuilder();
+ $qb->select('i')
+ ->from(Invoice::class, 'i')
+ ->where('' . $qb->expr()->lte('i.dueDate', ':today'))
+ ->andWhere($qb->expr()->eq('i.client', ':client') . '')
+ ->andWhere(
+ $qb->expr()->orX(
+ $qb->expr()->lt('(' . $sumPaymentQuery->getDQL() . ')', 'i.amount'),
+ $qb->expr()->eq('(' . $countPaymentQuery->getDQL() . ')', 0),
+ ),
+ );
+
+ $qb->setParameter('client', $client);
+ $qb->setParameter('today', new DateTime);
+
+ return $qb->getQuery()->getResult();
+ }
+
+
/**
* @return Entities\Invoice[]
*/
@@ -48,9 +90,22 @@ public function getAllByClient(Entities\Client $client): array
}
- public function create(Entities\Client $client, string $amount, \DateTime $issueDate): Entities\Invoice
+ public function addPayment(Payment $payment): void
{
- $invoice = new Entities\Invoice($client, $amount, $issueDate);
+ $this->entityManager->persist($payment);
+ $this->entityManager->flush();
+ }
+
+
+ public function create(
+ Entities\Client $client,
+ Money $amount,
+ DateTimeImmutable $issueDate,
+ DateTimeImmutable $dueDate,
+ ): Entities\Invoice {
+ // this method should be split into its own class, since this class has multiple purposes cuz of this method
+ // leave it out like this for now
+ $invoice = new Entities\Invoice($client, $amount, $issueDate, $dueDate);
$this->entityManager->persist($invoice);
$this->entityManager->flush();
diff --git a/app/Presenters/ClientPresenter.php b/app/Presenters/ClientPresenter.php
new file mode 100644
index 0000000..530fd9d
--- /dev/null
+++ b/app/Presenters/ClientPresenter.php
@@ -0,0 +1,120 @@
+model->get($id);
+
+ if ($client === null) {
+ $this->flashMessage('Klient nebyl nalezen.', 'danger');
+ $this->redirect('Homepage:');
+ }
+
+ $this->client = $client;
+ }
+
+
+ public function renderDetail(): void
+ {
+ $showUnpaid = (bool) $this->getParameter('showUnpaid');
+
+ $this->template->showUnpaid = $showUnpaid;
+ $this->template->client = $this->client;
+
+ if ($showUnpaid === true) {
+ $this->template->invoices = $this->invoiceModel->findUnpaidInvoicesByClient($this->client);
+ return;
+ }
+
+ $this->template->invoices = $this->invoiceModel->getAllByClient($this->client);
+ }
+
+
+ public function handleShowUnpaid(bool $showUnpaid): void
+ {
+ $this->template->invoices = $showUnpaid === true
+ ? $this->invoiceModel->findUnpaidInvoicesByClient($this->client)
+ : $this->invoiceModel->getAllByClient($this->client);
+
+ if ($this->isAjax() === true) {
+ $this->redrawControl('invoicesTable');
+ $this->redrawControl('showUnpaid');
+ }
+ }
+
+
+ /**
+ * @throws AbortException
+ */
+ public function handleAddPayment(int $invoiceId): void
+ {
+ $invoice = $this->invoiceModel->get($invoiceId);
+
+ if ($invoice === null) {
+ $this->flashMessage('Faktura s tímto ID neexistuje.', 'success');
+ $this->redirect('this');
+ }
+
+ $this->invoice = $invoice;
+ $this->redrawControl('invoiceFormSnippet');
+ }
+
+
+ public function createComponentInvoiceForm(): InvoiceForm
+ {
+ $invoiceFormComponent = $this->invoiceFormFactory->create($this->client, $this->invoice);
+
+ /** @var UI\Form $invoiceForm */
+ $invoiceForm = $invoiceFormComponent->getComponent('invoiceForm');
+ $invoiceForm->onValidate[] = fn () => $this->redrawControl('invoicesTable');
+ $invoiceForm->onSuccess[] = function (): void {
+ $this->payload->postGet = true;
+ $this->payload->url = $this->link('this');
+ $this->redrawControl('invoicesTable');
+ };
+
+ /** @var UI\Form $paymentForm */
+ $paymentForm = $invoiceFormComponent->getComponent('paymentForm');
+ $paymentForm->onSuccess[] = function (): void {
+ $this->payload->postGet = true;
+ $this->payload->url = $this->link('this');
+ $this->redrawControl('invoicesTable');
+ $this->redrawControl('invoiceFormSnippet');
+ };
+
+ return $invoiceFormComponent;
+ }
+}
diff --git a/app/presenters/Error4xxPresenter.php b/app/Presenters/Error4xxPresenter.php
similarity index 100%
rename from app/presenters/Error4xxPresenter.php
rename to app/Presenters/Error4xxPresenter.php
diff --git a/app/presenters/ErrorPresenter.php b/app/Presenters/ErrorPresenter.php
similarity index 100%
rename from app/presenters/ErrorPresenter.php
rename to app/Presenters/ErrorPresenter.php
diff --git a/app/presenters/HomepagePresenter.php b/app/Presenters/HomepagePresenter.php
similarity index 91%
rename from app/presenters/HomepagePresenter.php
rename to app/Presenters/HomepagePresenter.php
index 8ca29c6..5218dca 100644
--- a/app/presenters/HomepagePresenter.php
+++ b/app/Presenters/HomepagePresenter.php
@@ -12,8 +12,13 @@
final class HomepagePresenter extends UI\Presenter
{
- /** @inject */
- public ClientService $model;
+ // pass by interface, not by implementation
+ public function __construct(
+ private ClientService $model,
+// private IRegistrySubjectFinder $subjectFinder,
+ ) {
+ parent::__construct();
+ }
public function renderDefault(): void
diff --git a/app/presenters/templates/@commonBlocks.latte b/app/Presenters/templates/@commonBlocks.latte
similarity index 100%
rename from app/presenters/templates/@commonBlocks.latte
rename to app/Presenters/templates/@commonBlocks.latte
diff --git a/app/presenters/templates/@layout.latte b/app/Presenters/templates/@layout.latte
similarity index 100%
rename from app/presenters/templates/@layout.latte
rename to app/Presenters/templates/@layout.latte
diff --git a/app/Presenters/templates/Client/detail.latte b/app/Presenters/templates/Client/detail.latte
new file mode 100644
index 0000000..2fc0213
--- /dev/null
+++ b/app/Presenters/templates/Client/detail.latte
@@ -0,0 +1,50 @@
+{import ../@commonBlocks.latte}
+
+{varType Netvor\Invoice\Model\Entities\Client $client}
+{varType Netvor\Invoice\Model\Entities\Invoice[] $invoices}
+
+{block content}
+
+Faktury klienta {$client->getFirstName()} {$client->getLastName()} ({$client->getIc()})
+
+
+{snippet invoiceFormSnippet}
+ {control invoiceForm}
+{/snippet}
+
+
+
+{snippet showUnpaid}
+ {if $showUnpaid === true}Zobrazit všechny{else}Zobrazit nezaplacené{/if}
+{/snippet}
+
+
+
+
+ | Částka |
+ Zaplaceno |
+ Datum vystavení |
+ Datum splatnosti |
+ Akce |
+
+
+
+
+ | {$invoice->getAmount()->getAmount() / 100|number:2, ',', ' '} Kč |
+ {$invoice->getPaidAmount() / 100|number:2, ',', ' '} Kč |
+ {$invoice->getIssueDate()|date:'j. n. Y'} |
+
+ {if $invoice->getDueDate() !== null}
+ {$invoice->getDueDate()|date:'j. n. Y'}
+ {else}
+ -
+ {/if}
+ |
+
+ {if (int)$invoice->getAmount()->getAmount() !== $invoice->getPaidAmount()}
+ Přidat platbu
+ {/if}
+ |
+
+
+
diff --git a/app/presenters/templates/Error/500.phtml b/app/Presenters/templates/Error/500.phtml
similarity index 100%
rename from app/presenters/templates/Error/500.phtml
rename to app/Presenters/templates/Error/500.phtml
diff --git a/app/presenters/templates/Error/503.phtml b/app/Presenters/templates/Error/503.phtml
similarity index 100%
rename from app/presenters/templates/Error/503.phtml
rename to app/Presenters/templates/Error/503.phtml
diff --git a/app/presenters/templates/Error4xx/403.latte b/app/Presenters/templates/Error4xx/403.latte
similarity index 100%
rename from app/presenters/templates/Error4xx/403.latte
rename to app/Presenters/templates/Error4xx/403.latte
diff --git a/app/presenters/templates/Error4xx/404.latte b/app/Presenters/templates/Error4xx/404.latte
similarity index 100%
rename from app/presenters/templates/Error4xx/404.latte
rename to app/Presenters/templates/Error4xx/404.latte
diff --git a/app/presenters/templates/Error4xx/405.latte b/app/Presenters/templates/Error4xx/405.latte
similarity index 100%
rename from app/presenters/templates/Error4xx/405.latte
rename to app/Presenters/templates/Error4xx/405.latte
diff --git a/app/presenters/templates/Error4xx/410.latte b/app/Presenters/templates/Error4xx/410.latte
similarity index 100%
rename from app/presenters/templates/Error4xx/410.latte
rename to app/Presenters/templates/Error4xx/410.latte
diff --git a/app/presenters/templates/Error4xx/4xx.latte b/app/Presenters/templates/Error4xx/4xx.latte
similarity index 100%
rename from app/presenters/templates/Error4xx/4xx.latte
rename to app/Presenters/templates/Error4xx/4xx.latte
diff --git a/app/presenters/templates/Homepage/default.latte b/app/Presenters/templates/Homepage/default.latte
similarity index 86%
rename from app/presenters/templates/Homepage/default.latte
rename to app/Presenters/templates/Homepage/default.latte
index 0d4cbd6..9d21309 100644
--- a/app/presenters/templates/Homepage/default.latte
+++ b/app/Presenters/templates/Homepage/default.latte
@@ -69,12 +69,12 @@
- | {$client->ic} |
- {$client->email} |
- {$client->firstName} {$client->lastName} |
- {[$client->street, $client->city, $client->postalCode]|join:', '} |
+ {$client->getIc()} |
+ {$client->getEmail()} |
+ {$client->getFirstName()} {$client->getLastName()} |
+ {[$client->getStreet(), $client->getCity(), $client->getPostalCode()]|join:', '} |
- Detail
+ Detail
|
diff --git a/app/presenters/ClientDetailPresenter.php b/app/presenters/ClientDetailPresenter.php
deleted file mode 100644
index 7c2307a..0000000
--- a/app/presenters/ClientDetailPresenter.php
+++ /dev/null
@@ -1,103 +0,0 @@
-client = $this->checkClient($id);
- }
-
-
- public function renderDefault(): void
- {
- $this->template->client = $this->client;
- $this->template->invoices = $this->invoiceModel->getAllByClient($this->client);
-
- $this['invoiceForm']->setDefaults([
- 'issueDate' => date('Y-m-d'),
- ]);
- }
-
-
- protected function createComponentInvoiceForm(): UI\Form
- {
- $form = new UI\Form;
- $form->addProtection('Vaše relace vypršela. Vraťte se na domovskou stránku a zkuste to znovu.');
-
- $form->addText('amount')
- ->setRequired('Zadejte prosím částku.')
- ->addRule(UI\Form::NUMERIC, 'Zadejte prosím číslo.');
-
- $form->addText('issueDate')
- ->setHtmlType('date')
- ->setRequired('Zvolte prosím datum.');
-
- $form->addSubmit('submit');
-
- $form->onSuccess[] = [$this, 'invoiceFormSucceeded'];
- $form->onError[] = function (): void {
- if ($this->isAjax()) {
- $this->redrawControl('invoiceForm');
- }
- };
-
- return $form;
- }
-
-
- public function invoiceFormSucceeded(UI\Form $form, \stdClass $data): void
- {
- try {
- $issueDate = new Nette\Utils\DateTime($data->issueDate);
- } catch (\Exception $e) {
- /** @var Nette\Forms\Controls\TextInput $issueDateInput */
- $issueDateInput = $form['issueDate'];
- $issueDateInput->addError('Zadejte prosím platné datum.');
- return;
- }
-
- $this->invoiceModel->create($this->client, $data->amount, $issueDate);
-
- if (!$this->isAjax()) {
- $this->redirect('this');
- }
-
- $form->reset();
- $this->payload->postGet = true;
- $this->payload->url = $this->link('this');
- $this->redrawControl('invoiceForm');
- $this->redrawControl('invoicesTable');
- }
-
-
- private function checkClient(int $id): Client
- {
- $client = $this->model->get($id);
- if ($client === null) {
- $this->flashMessage('Klient nebyl nalezen.', 'danger');
- $this->redirect('Homepage:');
- }
-
- return $client;
- }
-}
diff --git a/app/presenters/templates/ClientDetail/default.latte b/app/presenters/templates/ClientDetail/default.latte
deleted file mode 100644
index 835c0e7..0000000
--- a/app/presenters/templates/ClientDetail/default.latte
+++ /dev/null
@@ -1,46 +0,0 @@
-{import ../@commonBlocks.latte}
-
-{varType Netvor\Invoice\Model\Entities\Client $client}
-{varType Netvor\Invoice\Model\Entities\Invoice[] $invoices}
-
-{block content}
-
-Faktury klienta {$client->firstName} {$client->lastName} ({$client->ic})
-
-
-Nová faktura
-
-
-
-
-
-
-
- | Částka |
- Datum vystavení |
-
-
-
-
- | {$invoice->amount|number:2, ',', ' '} Kč |
- {$invoice->issueDate|date:'j. n. Y'} |
-
-
-
diff --git a/composer.json b/composer.json
index 1bff0f4..bbd9961 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,8 @@
"latte/latte": "^2.10",
"tracy/tracy": "^2.9",
"contributte/webpack": "^2.1",
- "nettrine/orm": "^0.8.3"
+ "nettrine/orm": "^0.8.3",
+ "moneyphp/money": "^3.3"
},
"require-dev": {
"ext-dom": "*",
diff --git a/composer.lock b/composer.lock
index 716d1f2..a6a506b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "17f777a9f3a73d4b6070d2ffd6cebf0d",
+ "content-hash": "150b0631fc6cef4cbd52c7ee5cec00bd",
"packages": [
{
"name": "contributte/di",
@@ -1237,6 +1237,92 @@
},
"time": "2022-04-07T13:21:53+00:00"
},
+ {
+ "name": "moneyphp/money",
+ "version": "v3.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/moneyphp/money.git",
+ "reference": "0dc40e3791c67e8793e3aa13fead8cf4661ec9cd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/moneyphp/money/zipball/0dc40e3791c67e8793e3aa13fead8cf4661ec9cd",
+ "reference": "0dc40e3791c67e8793e3aa13fead8cf4661ec9cd",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "cache/taggable-cache": "^0.4.0",
+ "doctrine/instantiator": "^1.0.5",
+ "ext-bcmath": "*",
+ "ext-gmp": "*",
+ "ext-intl": "*",
+ "florianv/exchanger": "^1.0",
+ "florianv/swap": "^3.0",
+ "friends-of-phpspec/phpspec-code-coverage": "^3.1.1 || ^4.3",
+ "moneyphp/iso-currencies": "^3.2.1",
+ "php-http/message": "^1.4",
+ "php-http/mock-client": "^1.0.0",
+ "phpspec/phpspec": "^3.4.3",
+ "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.18 || ^8.5",
+ "psr/cache": "^1.0",
+ "symfony/phpunit-bridge": "^4"
+ },
+ "suggest": {
+ "ext-bcmath": "Calculate without integer limits",
+ "ext-gmp": "Calculate without integer limits",
+ "ext-intl": "Format Money objects with intl",
+ "florianv/exchanger": "Exchange rates library for PHP",
+ "florianv/swap": "Exchange rates library for PHP",
+ "psr/cache-implementation": "Used for Currency caching"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Money\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mathias Verraes",
+ "email": "mathias@verraes.net",
+ "homepage": "http://verraes.net"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ },
+ {
+ "name": "Frederik Bosch",
+ "email": "f.bosch@genkgo.nl"
+ }
+ ],
+ "description": "PHP implementation of Fowler's Money pattern",
+ "homepage": "http://moneyphp.org",
+ "keywords": [
+ "Value Object",
+ "money",
+ "vo"
+ ],
+ "support": {
+ "issues": "https://github.com/moneyphp/money/issues",
+ "source": "https://github.com/moneyphp/money/tree/v3.3.3"
+ },
+ "time": "2022-09-21T07:43:36+00:00"
+ },
{
"name": "nette/application",
"version": "v3.1.5",
@@ -2873,7 +2959,7 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.0.1",
+ "version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@@ -2920,7 +3006,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.1"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2"
},
"funding": [
{
@@ -3103,16 +3189,16 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.25.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
+ "reference": "219aa369ceff116e673852dce47c3a41794c14bd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd",
+ "reference": "219aa369ceff116e673852dce47c3a41794c14bd",
"shasum": ""
},
"require": {
@@ -3124,7 +3210,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -3167,7 +3253,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0"
},
"funding": [
{
@@ -3183,20 +3269,20 @@
"type": "tidelift"
}
],
- "time": "2021-02-19T12:13:01+00:00"
+ "time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.25.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
- "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
@@ -3211,7 +3297,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -3250,7 +3336,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@@ -3266,20 +3352,20 @@
"type": "tidelift"
}
],
- "time": "2021-11-30T18:21:41+00:00"
+ "time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php72",
- "version": "v1.25.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
- "reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
+ "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
- "reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2",
+ "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2",
"shasum": ""
},
"require": {
@@ -3288,7 +3374,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -3326,7 +3412,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php72/tree/v1.25.0"
+ "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0"
},
"funding": [
{
@@ -3342,7 +3428,7 @@
"type": "tidelift"
}
],
- "time": "2021-05-27T09:17:38+00:00"
+ "time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php73",
diff --git a/config/common.neon b/config/common.neon
index 795de4d..15b0c93 100644
--- a/config/common.neon
+++ b/config/common.neon
@@ -9,7 +9,7 @@ extensions:
nettrine.cache: Nettrine\Cache\DI\CacheExtension
nettrine.dbal: Nettrine\DBAL\DI\DbalExtension
nettrine.orm: Nettrine\ORM\DI\OrmExtension
- nettrine.orm.annotations: Nettrine\ORM\DI\OrmAnnotationsExtension
+ nettrine.orm.attributes: Nettrine\ORM\DI\OrmAttributesExtension
nettrine.orm.cache: Nettrine\ORM\DI\OrmCacheExtension
webpack: Contributte\Webpack\DI\WebpackExtension(%debugMode%, %consoleMode%)
@@ -30,7 +30,7 @@ di:
nettrine.dbal:
connection:
driver: pdo_mysql
- host: 127.0.0.1
+ host: mysql # could not connect through command line, may be some config include priority problem
dbname: invoice-test-app
charset: utf8
user:
@@ -41,11 +41,9 @@ nettrine.dbal:
debug:
panel: %debugMode%
-
-nettrine.orm.annotations:
- mapping:
- Netvor\Invoice: %appDir%
-
+nettrine.orm.attributes:
+ mapping:
+ Netvor\Invoice: %appDir%
session:
autoStart: always
@@ -68,6 +66,10 @@ services:
- Netvor\Invoice\Mails\MailService(templateDir: %email.templateDir%, defaultFromEmail: %email.from%)
- Netvor\Invoice\Model\ClientService
- Netvor\Invoice\Model\InvoiceService
+ - Netvor\Invoice\Model\ARESRegistrySubjectFinder
+
+ # FORMS
+ - Netvor\Invoice\Form\InvoiceForm\InvoiceFormFactory
- Netvor\Invoice\Router\RouterFactory::createRouter
diff --git a/docker-compose.yml b/docker-compose.yml
index d0a3d19..4be38ff 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -57,7 +57,7 @@ services:
- webpack
volumes:
- .:/app
- - vendor:/app/vendor
+ - ./vendor:/app/vendor
- composer:/home/docker-container-user/.composer
environment:
- NETTE_DEBUG
diff --git a/migrations/Version20221019160643.php b/migrations/Version20221019160643.php
new file mode 100644
index 0000000..21bb267
--- /dev/null
+++ b/migrations/Version20221019160643.php
@@ -0,0 +1,40 @@
+hasTable('invoice') === true) {
+ $table = $schema->getTable('invoice');
+ if ($table->hasColumn('due_date') === false) {
+ $table->addColumn('due_date', Types::DATETIME_IMMUTABLE)->setNotnull(false);
+ }
+ }
+ }
+
+
+ public function down(Schema $schema): void
+ {
+ if ($schema->hasTable('invoice') === true) {
+ $table = $schema->getTable('invoice');
+ if ($table->hasColumn('due_date') === true) {
+ // TODO: dump to some SQL file to prevent data loss on rollback?
+ $table->dropColumn('due_date');
+ }
+ }
+ }
+}
diff --git a/migrations/Version20221019180709.php b/migrations/Version20221019180709.php
new file mode 100644
index 0000000..1e554d5
--- /dev/null
+++ b/migrations/Version20221019180709.php
@@ -0,0 +1,73 @@
+hasTable('invoice') === true) {
+ $table = $schema->getTable('invoice');
+
+ if ($table->hasColumn('amount') === true) {
+ // TODO: backup before executing
+ $this->connection->executeQuery('UPDATE invoice SET amount = amount * 100'); // migrate to cents
+ $table->getColumn('amount')->setType(new IntegerType);
+ }
+ }
+
+ if ($schema->hasTable('payment') === false) {
+ $table = $schema->createTable('payment');
+ $table->addColumn('id', Types::INTEGER)
+ ->setAutoincrement(true);
+ $table->addColumn('invoice_id', Types::INTEGER)->setNotnull(true);
+ $table->addColumn('amount', Types::INTEGER)->setNotnull(true);
+ $table->addColumn('created_at', Types::DATETIME_IMMUTABLE)->setNotnull(true);
+ $table->addForeignKeyConstraint('invoice', ['invoice_id'], ['id']);
+ $table->setPrimaryKey(['id']);
+ }
+ }
+
+
+ public function down(Schema $schema): void
+ {
+ if ($schema->hasTable('invoice') === true) {
+ $table = $schema->getTable('invoice');
+
+ if ($table->hasColumn('amount') === true) {
+ // TODO: backup before executing
+ $table->getColumn('amount')->setType(new DecimalType)->setOptions(['scale' => 2, 'precision' => 12]);
+ $this->connection->executeQuery('UPDATE invoice SET amount = amount / 100'); // migrate back to deciaml
+ }
+ }
+
+ if ($schema->hasTable('payment') === true) {
+ // TODO: backup table before executing
+ $schema->dropTable('payment');
+ }
+ }
+}