Skip to content

Commit

Permalink
new: Allow to assign contracts to tickets
Browse files Browse the repository at this point in the history
  • Loading branch information
marien-probesys committed Sep 26, 2023
2 parents 256b79a + bceeeae commit fc3e93e
Show file tree
Hide file tree
Showing 20 changed files with 742 additions and 3 deletions.
2 changes: 1 addition & 1 deletion assets/stylesheets/components/progress.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ progress {
height: 2rem;

background-color: var(--color-primary2);
border: 0.25rem solid var(--color-primary11);
border: 0.25rem solid var(--color-primary9);
border-radius: 0.5rem;

appearance: none;
Expand Down
92 changes: 92 additions & 0 deletions migrations/Version20230925082614CreateTicketContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

// This file is part of Bileto.
// Copyright 2022-2023 Probesys
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20230925082614CreateTicketContract extends AbstractMigration
{
public function getDescription(): string
{
return 'Create the ticket_contract table.';
}

public function up(Schema $schema): void
{
$dbPlatform = $this->connection->getDatabasePlatform();
if ($dbPlatform instanceof PostgreSQLPlatform) {
$this->addSql(<<<SQL
CREATE TABLE ticket_contract (
ticket_id INT NOT NULL,
contract_id INT NOT NULL,
PRIMARY KEY(ticket_id, contract_id)
)
SQL);
$this->addSql('CREATE INDEX IDX_6CE6D4D8700047D2 ON ticket_contract (ticket_id)');
$this->addSql('CREATE INDEX IDX_6CE6D4D82576E0FD ON ticket_contract (contract_id)');
$this->addSql(<<<SQL
ALTER TABLE ticket_contract
ADD CONSTRAINT FK_6CE6D4D8700047D2
FOREIGN KEY (ticket_id)
REFERENCES ticket (id)
ON DELETE CASCADE
NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<SQL
ALTER TABLE ticket_contract
ADD CONSTRAINT FK_6CE6D4D82576E0FD
FOREIGN KEY (contract_id)
REFERENCES contract (id)
ON DELETE CASCADE
NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
} elseif ($dbPlatform instanceof MariaDBPlatform) {
$this->addSql(<<<SQL
CREATE TABLE ticket_contract (
ticket_id INT NOT NULL,
contract_id INT NOT NULL,
INDEX IDX_6CE6D4D8700047D2 (ticket_id),
INDEX IDX_6CE6D4D82576E0FD (contract_id),
PRIMARY KEY(ticket_id, contract_id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
SQL);
$this->addSql(<<<SQL
ALTER TABLE ticket_contract
ADD CONSTRAINT FK_6CE6D4D8700047D2
FOREIGN KEY (ticket_id)
REFERENCES ticket (id)
ON DELETE CASCADE
SQL);
$this->addSql(<<<SQL
ALTER TABLE ticket_contract
ADD CONSTRAINT FK_6CE6D4D82576E0FD
FOREIGN KEY (contract_id)
REFERENCES contract (id)
ON DELETE CASCADE
SQL);
}
}

public function down(Schema $schema): void
{
$dbPlatform = $this->connection->getDatabasePlatform();
if ($dbPlatform instanceof PostgreSQLPlatform) {
$this->addSql('ALTER TABLE ticket_contract DROP CONSTRAINT FK_6CE6D4D8700047D2');
$this->addSql('ALTER TABLE ticket_contract DROP CONSTRAINT FK_6CE6D4D82576E0FD');
$this->addSql('DROP TABLE ticket_contract');
} elseif ($dbPlatform instanceof MariaDBPlatform) {
$this->addSql('ALTER TABLE ticket_contract DROP FOREIGN KEY FK_6CE6D4D8700047D2');
$this->addSql('ALTER TABLE ticket_contract DROP FOREIGN KEY FK_6CE6D4D82576E0FD');
$this->addSql('DROP TABLE ticket_contract');
}
}
}
4 changes: 4 additions & 0 deletions src/Command/SeedsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'orga:create:tickets:messages:confidential',
'orga:see',
'orga:see:tickets:all',
'orga:see:tickets:contracts',
'orga:see:tickets:messages:confidential',
'orga:update:tickets:actors',
'orga:update:tickets:priority',
Expand All @@ -85,7 +86,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'orga:see:contracts',
'orga:see:contracts:notes',
'orga:see:tickets:all',
'orga:see:tickets:contracts',
'orga:see:tickets:messages:confidential',
'orga:update:tickets:contracts',
],
]);

Expand All @@ -98,6 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'orga:create:tickets',
'orga:create:tickets:messages',
'orga:see',
'orga:see:tickets:contracts',
'orga:update:tickets:title',
],
]);
Expand Down
5 changes: 4 additions & 1 deletion src/Controller/Organizations/ContractsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Entity\Organization;
use App\Repository\ContractRepository;
use App\Repository\OrganizationRepository;
use App\Service\Sorter\ContractSorter;
use App\Utils;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
Expand All @@ -28,6 +29,7 @@ public function index(
Request $request,
ContractRepository $contractRepository,
OrganizationRepository $organizationRepository,
ContractSorter $contractSorter,
Security $security,
): Response {
$this->denyAccessUnlessGranted('orga:see:contracts', $organization);
Expand All @@ -51,7 +53,8 @@ public function index(

$contracts = $contractRepository->findBy([
'organization' => $allowedOrganizations,
], ['endAt' => 'DESC']);
]);
$contractSorter->sort($contracts);

return $this->render('organizations/contracts/index.html.twig', [
'organization' => $organization,
Expand Down
7 changes: 7 additions & 0 deletions src/Controller/Organizations/TicketsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Entity\Message;
use App\Entity\Organization;
use App\Entity\Ticket;
use App\Repository\ContractRepository;
use App\Repository\MessageRepository;
use App\Repository\MessageDocumentRepository;
use App\Repository\OrganizationRepository;
Expand Down Expand Up @@ -132,6 +133,7 @@ public function new(
public function create(
Organization $organization,
Request $request,
ContractRepository $contractRepository,
MessageRepository $messageRepository,
MessageDocumentRepository $messageDocumentRepository,
OrganizationRepository $organizationRepository,
Expand Down Expand Up @@ -284,6 +286,11 @@ public function create(
$ticket->setAssignee($assignee);
}

$contracts = $contractRepository->findOngoingByOrganization($organization);
if (count($contracts) === 1) {
$ticket->addContract($contracts[0]);
}

$errors = $validator->validate($ticket);
if (count($errors) > 0) {
return $this->renderBadRequest('organizations/tickets/new.html.twig', [
Expand Down
106 changes: 106 additions & 0 deletions src/Controller/Tickets/ContractsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

// This file is part of Bileto.
// Copyright 2022-2023 Probesys
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace App\Controller\Tickets;

use App\Controller\BaseController;
use App\Entity\Ticket;
use App\Repository\ContractRepository;
use App\Repository\TicketRepository;
use App\Service\Sorter\ContractSorter;
use App\Utils\ConstraintErrorsFormatter;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;

class ContractsController extends BaseController
{
#[Route('/tickets/{uid}/contracts/edit', name: 'edit ticket contracts', methods: ['GET', 'HEAD'])]
public function edit(
Ticket $ticket,
ContractRepository $contractRepository,
ContractSorter $contractSorter,
): Response {
$organization = $ticket->getOrganization();
$this->denyAccessUnlessGranted('orga:update:tickets:contracts', $organization);

/** @var \App\Entity\User $user */
$user = $this->getUser();

if (!$ticket->hasActor($user)) {
$this->denyAccessUnlessGranted('orga:see:tickets:all', $organization);
}

$ongoingContracts = $contractRepository->findOngoingByOrganization($organization);
$contractSorter->sort($ongoingContracts);
$initialOngoingContract = $ticket->getOngoingContract();

return $this->render('tickets/contracts/edit.html.twig', [
'ticket' => $ticket,
'ongoingContracts' => $ongoingContracts,
'ongoingContractUid' => $initialOngoingContract ? $initialOngoingContract->getUid() : null,
]);
}

#[Route('/tickets/{uid}/contracts/edit', name: 'update ticket contracts', methods: ['POST'])]
public function update(
Ticket $ticket,
Request $request,
ContractRepository $contractRepository,
TicketRepository $ticketRepository,
ContractSorter $contractSorter,
TranslatorInterface $translator,
): Response {
$organization = $ticket->getOrganization();
$this->denyAccessUnlessGranted('orga:update:tickets:contracts', $organization);

/** @var \App\Entity\User $user */
$user = $this->getUser();

if (!$ticket->hasActor($user)) {
$this->denyAccessUnlessGranted('orga:see:tickets:all', $organization);
}

$ongoingContractUid = $request->request->getString('ongoingContractUid');

$csrfToken = $request->request->getString('_csrf_token');

$ongoingContracts = $contractRepository->findOngoingByOrganization($organization);
$contractSorter->sort($ongoingContracts);
$initialOngoingContract = $ticket->getOngoingContract();

if (!$this->isCsrfTokenValid('update ticket contracts', $csrfToken)) {
return $this->renderBadRequest('tickets/contracts/edit.html.twig', [
'ticket' => $ticket,
'ongoingContracts' => $ongoingContracts,
'ongoingContractUid' => $ongoingContractUid,
'error' => $translator->trans('csrf.invalid', [], 'errors'),
]);
}

$newOngoingContract = null;
foreach ($ongoingContracts as $contract) {
if ($contract->getUid() === $ongoingContractUid) {
$newOngoingContract = $contract;
}
}

if ($initialOngoingContract) {
$ticket->removeContract($initialOngoingContract);
}

if ($newOngoingContract) {
$ticket->addContract($newOngoingContract);
}

$ticketRepository->save($ticket, true);

return $this->redirectToRoute('ticket', [
'uid' => $ticket->getUid(),
]);
}
}
19 changes: 19 additions & 0 deletions src/Entity/Contract.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use App\EntityListener\EntitySetMetaListener;
use App\Repository\ContractRepository;
use App\Utils;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Translation\TranslatableMessage;
Expand Down Expand Up @@ -87,6 +89,15 @@ class Contract implements MetaEntityInterface, ActivityRecordableInterface
#[ORM\JoinColumn(nullable: false)]
private ?Organization $organization = null;

/** @var Collection<int, Ticket> $tickets */
#[ORM\ManyToMany(targetEntity: Ticket::class, mappedBy: 'contracts')]
private Collection $tickets;

public function __construct()
{
$this->tickets = new ArrayCollection();
}

public function getId(): ?int
{
return $this->id;
Expand Down Expand Up @@ -239,4 +250,12 @@ public function getDaysProgress(): int
$interval = $this->startAt->diff($now);
return intval($interval->format('%a'));
}

/**
* @return Collection<int, Ticket>
*/
public function getTickets(): Collection
{
return $this->tickets;
}
}
2 changes: 2 additions & 0 deletions src/Entity/Role.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ class Role implements MetaEntityInterface, ActivityRecordableInterface
'orga:see:contracts',
'orga:see:contracts:notes',
'orga:see:tickets:all',
'orga:see:tickets:contracts',
'orga:see:tickets:messages:confidential',
'orga:update:tickets:actors',
'orga:update:tickets:contracts',
'orga:update:tickets:priority',
'orga:update:tickets:status',
'orga:update:tickets:title',
Expand Down
42 changes: 42 additions & 0 deletions src/Entity/Ticket.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,14 @@ class Ticket implements MetaEntityInterface, ActivityRecordableInterface
#[ORM\OneToOne(cascade: ['persist'])]
private ?Message $solution = null;

/** @var Collection<int, Contract> $contracts */
#[ORM\ManyToMany(targetEntity: Contract::class, inversedBy: 'tickets')]
private Collection $contracts;

public function __construct()
{
$this->messages = new ArrayCollection();
$this->contracts = new ArrayCollection();
}

public function getId(): ?int
Expand Down Expand Up @@ -403,4 +408,41 @@ public function setSolution(?Message $solution): self

return $this;
}

/**
* @return Collection<int, Contract>
*/
public function getContracts(): Collection
{
return $this->contracts;
}

public function getOngoingContract(): ?Contract
{
$contracts = $this->getContracts();

foreach ($contracts as $contract) {
if ($contract->getStatus() === 'ongoing') {
return $contract;
}
}

return null;
}

public function addContract(Contract $contract): static
{
if (!$this->contracts->contains($contract)) {
$this->contracts->add($contract);
}

return $this;
}

public function removeContract(Contract $contract): static
{
$this->contracts->removeElement($contract);

return $this;
}
}
Loading

0 comments on commit fc3e93e

Please sign in to comment.