diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 00000000..98ffa1da --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1 @@ + diff --git a/assets/javascripts/application.js b/assets/javascripts/application.js index af1495e2..a3f22db5 100644 --- a/assets/javascripts/application.js +++ b/assets/javascripts/application.js @@ -11,6 +11,7 @@ import ModalController from '@/controllers/modal_controller.js'; import ModalOpenerController from '@/controllers/modal_opener_controller.js'; import PopupController from '@/controllers/popup_controller.js'; import NewTicketController from '@/controllers/new_ticket_controller.js'; +import TicketEditorController from '@/controllers/ticket_editor_controller.js'; import TinymceController from '@/controllers/tinymce_controller.js'; const application = Application.start(); @@ -19,6 +20,7 @@ application.register('form-priority', FormPriorityController); application.register('modal', ModalController); application.register('modal-opener', ModalOpenerController); application.register('new-ticket', NewTicketController); +application.register('ticket-editor', TicketEditorController); application.register('popup', PopupController); application.register('tinymce', TinymceController); diff --git a/assets/javascripts/controllers/ticket_editor_controller.js b/assets/javascripts/controllers/ticket_editor_controller.js new file mode 100644 index 00000000..1f8302d3 --- /dev/null +++ b/assets/javascripts/controllers/ticket_editor_controller.js @@ -0,0 +1,30 @@ +// This file is part of Bileto. +// Copyright 2022 Probesys +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static get targets () { + return ['solutionCheckbox', 'statusSelect']; + } + + connect () { + this.updateStatus(); + } + + updateStatus () { + const isSolution = this.solutionCheckboxTarget.checked; + + if (isSolution) { + this.statusSelectTarget.value = 'resolved'; + for (const option of this.statusSelectTarget.options) { + option.disabled = option.value !== 'resolved'; + } + } else { + for (const option of this.statusSelectTarget.options) { + option.disabled = false; + } + } + } +} diff --git a/assets/stylesheets/components/messages.css b/assets/stylesheets/components/messages.css index be5db6ee..38956de8 100644 --- a/assets/stylesheets/components/messages.css +++ b/assets/stylesheets/components/messages.css @@ -62,6 +62,11 @@ border-radius: 0.5rem; } +.message--solution .message__box { + border-width: 2px; + border-color: var(--color-success6); +} + .message__box::before { content: " "; @@ -79,6 +84,10 @@ clip-path: polygon(0 50%, 100% 0, 100% 100%); } +.message--solution .message__box::before { + background-color: var(--color-success6); +} + .message__top { display: flex; padding: 1rem 2rem; @@ -90,8 +99,16 @@ border-radius: 0.5rem 0.5rem 0 0; } +.message--solution .message__top { + color: var(--color-success12); + + background-color: var(--color-success3); + border-bottom-width: 2px; + border-bottom-color: var(--color-success6); +} + .message__top * + * { - margin-left: 0.25rem; + margin-left: 0.5rem; } .message__top-separator { @@ -107,6 +124,10 @@ font-size: 0.9em; } +.message--solution .message__date { + color: var(--color-success11); +} + .message__content { padding: 2rem; diff --git a/migrations/Version20221123133721MoveSolutionToTicket.php b/migrations/Version20221123133721MoveSolutionToTicket.php new file mode 100644 index 00000000..390d8618 --- /dev/null +++ b/migrations/Version20221123133721MoveSolutionToTicket.php @@ -0,0 +1,62 @@ +connection->getDatabasePlatform()->getName(); + if ($dbPlatform === 'postgresql') { + $this->addSql('ALTER TABLE message DROP is_solution'); + $this->addSql('ALTER TABLE ticket ADD solution_id INT DEFAULT NULL'); + $this->addSql(<<addSql('CREATE UNIQUE INDEX UNIQ_97A0ADA31C0BE183 ON ticket (solution_id)'); + } elseif ($dbPlatform === 'mysql') { + $this->addSql('ALTER TABLE message DROP is_solution'); + $this->addSql('ALTER TABLE ticket ADD solution_id INT DEFAULT NULL'); + $this->addSql(<<addSql('CREATE UNIQUE INDEX UNIQ_97A0ADA31C0BE183 ON ticket (solution_id)'); + } + } + + public function down(Schema $schema): void + { + $dbPlatform = $this->connection->getDatabasePlatform()->getName(); + if ($dbPlatform === 'postgresql') { + $this->addSql('ALTER TABLE message ADD is_solution BOOLEAN NOT NULL'); + $this->addSql('ALTER TABLE ticket DROP CONSTRAINT FK_97A0ADA31C0BE183'); + $this->addSql('DROP INDEX UNIQ_97A0ADA31C0BE183'); + $this->addSql('ALTER TABLE ticket DROP solution_id'); + } elseif ($dbPlatform === 'mysql') { + $this->addSql('ALTER TABLE ticket DROP FOREIGN KEY FK_97A0ADA31C0BE183'); + $this->addSql('DROP INDEX UNIQ_97A0ADA31C0BE183 ON ticket'); + $this->addSql('ALTER TABLE ticket DROP solution_id'); + $this->addSql('ALTER TABLE message ADD is_solution TINYINT(1) NOT NULL'); + } + } +} diff --git a/public/icons.svg b/public/icons.svg index 1a96ba57..cb2a5017 100644 --- a/public/icons.svg +++ b/public/icons.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/Controller/Organizations/TicketsController.php b/src/Controller/Organizations/TicketsController.php index 614ba00d..2e38f4d0 100644 --- a/src/Controller/Organizations/TicketsController.php +++ b/src/Controller/Organizations/TicketsController.php @@ -201,7 +201,6 @@ public function create( $message->setCreatedBy($user); $message->setTicket($ticket); $message->setIsPrivate(false); - $message->setIsSolution(false); $message->setVia('webapp'); $errors = $validator->validate($message); diff --git a/src/Controller/Tickets/MessagesController.php b/src/Controller/Tickets/MessagesController.php index 7db9ad23..151e215c 100644 --- a/src/Controller/Tickets/MessagesController.php +++ b/src/Controller/Tickets/MessagesController.php @@ -36,6 +36,9 @@ public function create( $messageContent = $request->request->get('message', ''); $messageContent = $appMessageSanitizer->sanitize($messageContent); + /** @var boolean $isSolution */ + $isSolution = $request->request->get('isSolution', false); + /** @var string $status */ $status = $request->request->get('status', ''); @@ -50,6 +53,7 @@ public function create( 'message' => $messageContent, 'status' => $status, 'statuses' => Ticket::getStatusesWithLabels(), + 'isSolution' => $isSolution, 'error' => $this->csrfError(), ]); } @@ -60,7 +64,6 @@ public function create( $message->setCreatedBy($user); $message->setTicket($ticket); $message->setIsPrivate(false); - $message->setIsSolution(false); $message->setVia('webapp'); $errors = $validator->validate($message); @@ -72,10 +75,16 @@ public function create( 'message' => $messageContent, 'status' => $status, 'statuses' => Ticket::getStatusesWithLabels(), + 'isSolution' => $isSolution, 'errors' => $this->formatErrors($errors), ]); } + if ($isSolution) { + $ticket->setSolution($message); + $status = 'resolved'; + } + $ticket->setStatus($status); $errors = $validator->validate($ticket); @@ -86,6 +95,7 @@ public function create( 'message' => $messageContent, 'status' => $status, 'statuses' => Ticket::getStatusesWithLabels(), + 'isSolution' => $isSolution, 'errors' => $this->formatErrors($errors), ]); } diff --git a/src/Controller/TicketsController.php b/src/Controller/TicketsController.php index db3f2aa7..dd179c2c 100644 --- a/src/Controller/TicketsController.php +++ b/src/Controller/TicketsController.php @@ -24,6 +24,7 @@ public function show(Ticket $ticket): Response 'message' => '', 'status' => 'pending', 'statuses' => Ticket::getStatusesWithLabels(), + 'isSolution' => false, ]); } } diff --git a/src/Entity/Message.php b/src/Entity/Message.php index b39d0301..dcf8d75c 100644 --- a/src/Entity/Message.php +++ b/src/Entity/Message.php @@ -33,9 +33,6 @@ class Message #[ORM\Column] private bool $isPrivate = false; - #[ORM\Column] - private bool $isSolution = false; - #[ORM\Column(length: 32, options: ['default' => self::DEFAULT_VIA])] #[Assert\Choice( choices: self::VIAS, @@ -94,18 +91,6 @@ public function setIsPrivate(bool $isPrivate): self return $this; } - public function isSolution(): ?bool - { - return $this->isSolution; - } - - public function setIsSolution(bool $isSolution): self - { - $this->isSolution = $isSolution; - - return $this; - } - public function getVia(): ?string { return $this->via; diff --git a/src/Entity/Ticket.php b/src/Entity/Ticket.php index 6e134c97..5a5cc5ae 100644 --- a/src/Entity/Ticket.php +++ b/src/Entity/Ticket.php @@ -107,6 +107,9 @@ class Ticket #[ORM\OneToMany(mappedBy: 'ticket', targetEntity: Message::class, orphanRemoval: true)] private Collection $messages; + #[ORM\OneToOne(cascade: ['persist'])] + private ?Message $solution = null; + public function __construct() { $this->messages = new ArrayCollection(); @@ -189,9 +192,11 @@ public function getStatusBadgeColor(): ?string $this->status === 'assigned' || $this->status === 'in_progress' ) { - return 'green'; + return 'orange'; } elseif ($this->status === 'pending') { return 'blue'; + } elseif ($this->status === 'resolved') { + return 'green'; } else { return 'grey'; } @@ -364,4 +369,16 @@ public function getMessages(): Collection { return $this->messages; } + + public function getSolution(): ?Message + { + return $this->solution; + } + + public function setSolution(?Message $solution): self + { + $this->solution = $solution; + + return $this; + } } diff --git a/templates/tickets/show.html.twig b/templates/tickets/show.html.twig index d58dd389..754f0004 100644 --- a/templates/tickets/show.html.twig +++ b/templates/tickets/show.html.twig @@ -49,7 +49,7 @@
{% for message in messages %} -
+
{{ icon('circle-user') }} {% if message.createdBy == ticket.requester %} @@ -71,6 +71,12 @@
+ {% if ticket.solution == message %} +
+ {{ icon('check') }} +
+ {% endif %} +
{{ message.createdBy.email }}
@@ -93,7 +99,8 @@
@@ -128,6 +135,21 @@ >{{ message }}
+
+ + + +
+