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

+
+
+ {include formErrors form => $form} +
+
+ + + {include inputErrors input => $form[amount]} +
+
+ + + {include inputErrors input => $form[issueDate]} +
+
+ + + {include inputErrors input => $form[dueDate]} +
+
+ +
+
+{else} {* We have invoice, add payment if wanted. *} +

Přidat platbu k faktuře {$invoice->getId()}

+
+
+ {include formErrors form => $form} +
+
+ + + {include inputErrors input => $form[amount]} +
+
+ +
+
+{/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} + + + + + + + + + + + + + + + + + + + + +
ČástkaZaplacenoDatum vystaveníDatum splatnostiAkce
{$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

-
-
- {include formErrors form => $form} -
-
- - - {include inputErrors input => $form[amount]} -
-
- - - {include inputErrors input => $form[issueDate]} -
-
- -
-
- -
- - - - - - - - - - - - - - -
ČástkaDatum 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'); + } + } +}