Browse Source

corrige deux/trois bricoles

master
vincent 2 months ago
parent
commit
3004d30cc3
18 changed files with 396 additions and 115 deletions
  1. +1
    -0
      config/packages/stof_doctrine_extensions.yaml
  2. +0
    -33
      migrations/Version20240726120017.php
  3. +10
    -9
      src/Controller/ProjectController.php
  4. +65
    -44
      src/Controller/TaskController.php
  5. +8
    -0
      src/DataFixtures/AppFixtures.php
  6. +32
    -0
      src/Entity/Project.php
  7. +84
    -0
      src/Entity/Task.php
  8. +80
    -0
      src/Entity/User.php
  9. +1
    -1
      src/Service/GeoJsonManager.php
  10. +12
    -0
      templates/partials/_project-metadata.html.twig
  11. +20
    -0
      templates/partials/_task-locking.html.twig
  12. +12
    -0
      templates/partials/_task-metadata.html.twig
  13. +2
    -0
      templates/partials/_task-title.html.twig
  14. +7
    -2
      templates/project/index.html.twig
  15. +21
    -10
      templates/project/show.html.twig
  16. +2
    -2
      templates/task/create.html.twig
  17. +37
    -12
      templates/task/show.html.twig
  18. +2
    -2
      templates/task/update.html.twig

+ 1
- 0
config/packages/stof_doctrine_extensions.yaml View File

@ -5,3 +5,4 @@ stof_doctrine_extensions:
orm:
default:
sluggable: true
timestampable: true

+ 0
- 33
migrations/Version20240726120017.php View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240726120017 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json)
, osm_id INTEGER NOT NULL)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_USERNAME ON user (username)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE user');
}
}

+ 10
- 9
src/Controller/ProjectController.php View File

@ -45,6 +45,7 @@ class ProjectController extends AbstractController
$project = $createForm->getData();
try {
$project->setCreatedBy($this->getUser());
$entityManager->persist($project);
$entityManager->flush();
@ -67,10 +68,10 @@ class ProjectController extends AbstractController
]);
}
#[Route('/show/{projectSlug}', name: 'app_project_show')]
public function show(EntityManagerInterface $entityManager, $projectSlug): Response
#[Route('/{slug}', name: 'app_project_show')]
public function show(EntityManagerInterface $entityManager, $slug): Response
{
$project = $entityManager->getRepository(Project::class)->findOneBySlug($projectSlug);
$project = $entityManager->getRepository(Project::class)->findOneBySlug($slug);
$tasks = $entityManager->getRepository(Task::class)->findByProjectPaginated($project);
if (!$project) {
@ -94,11 +95,11 @@ class ProjectController extends AbstractController
]);
}
#[Route('/update/{projectSlug}', name: 'app_project_update')]
public function update(Request $request, EntityManagerInterface $entityManager, $projectSlug): Response
#[Route('/{slug}/update', name: 'app_project_update')]
public function update(Request $request, EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Project::class);
$project = $repository->findOneBySlug($projectSlug);
$project = $repository->findOneBySlug($slug);
if (!$project) {
$this->addFlash(
@ -142,11 +143,11 @@ class ProjectController extends AbstractController
]);
}
#[Route('/remove/{projectSlug}', name: 'app_project_remove')]
public function remove(EntityManagerInterface $entityManager, $projectSlug): Response
#[Route('/{slug}/remove', name: 'app_project_remove')]
public function remove(EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Project::class);
$project = $repository->findOneBySlug($projectSlug);
$project = $repository->findOneBySlug($slug);
if (!$project) {
$this->addFlash(


+ 65
- 44
src/Controller/TaskController.php View File

@ -20,11 +20,11 @@ use Symfony\Component\Workflow\WorkflowInterface;
#[Route('/task')]
class TaskController extends AbstractController
{
#[Route('/{projectSlug}/create', name: 'app_task_create')]
public function create(Request $request, EntityManagerInterface $entityManager, $projectSlug): Response
#[Route('/create', name: 'app_task_create')]
public function create(Request $request, EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Project::class);
$project = $repository->findOneBySlug($projectSlug);
$project = $repository->findOneBySlug($slug);
if (!$project) {
$this->addFlash(
@ -47,6 +47,7 @@ class TaskController extends AbstractController
$task = $createForm->getData();
try {
$task->setCreatedBy($this->getUser());
$entityManager->persist($task);
$entityManager->flush();
@ -55,7 +56,7 @@ class TaskController extends AbstractController
'Tâche créée !'
);
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirectToRoute('app_project_show', ['slug' => $slug]);
} catch (\Exception $exception) {
$this->addFlash(
'danger',
@ -71,11 +72,11 @@ class TaskController extends AbstractController
]);
}
#[Route('/{projectSlug}/show/{taskSlug}', name: 'app_task_show')]
public function show(EntityManagerInterface $entityManager, GeoJsonManager $geoJsonManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}', name: 'app_task_show')]
public function show(Request $request, EntityManagerInterface $entityManager, GeoJsonManager $geoJsonManager, $slug): Response
{
$repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($taskSlug);
$task = $repository->findOneBySlug($slug);
if (!$task) {
$this->addFlash(
@ -83,12 +84,12 @@ class TaskController extends AbstractController
'Tâche non trouvée !'
);
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirect($request->headers->get('Referer'));
}
$comment = new Comment();
$commentForm = $this->createForm(CommentType::class, $comment, [
'action' => $this->generateUrl('app_task_comment', ['projectSlug' => $projectSlug, 'taskSlug' => $taskSlug]),
'action' => $this->generateUrl('app_task_comment', ['slug' => $slug]),
]);
$commentForm->add('submit', SubmitType::class, [
'label' => 'Commenter',
@ -101,11 +102,11 @@ class TaskController extends AbstractController
]);
}
#[Route('/{projectSlug}/comment/{taskSlug}', name: 'app_task_comment')]
public function comment(Request $request, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}/comment', name: 'app_task_comment')]
public function comment(Request $request, EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($taskSlug);
$task = $repository->findOneBySlug($slug);
if (!$task) {
$this->addFlash(
@ -113,7 +114,7 @@ class TaskController extends AbstractController
'Tâche non trouvée !'
);
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirect($request->headers->get('Referer'));
}
$comment = new Comment();
@ -143,14 +144,14 @@ class TaskController extends AbstractController
}
}
return $this->redirectToRoute('app_task_show', ['projectSlug' => $projectSlug, 'taskSlug' => $taskSlug]);
return $this->redirectToRoute('app_task_show', ['slug' => $slug]);
}
#[Route('/{projectSlug}/update/{taskSlug}', name: 'app_task_update')]
public function update(Request $request, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}/update', name: 'app_task_update')]
public function update(Request $request, EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($taskSlug);
$task = $repository->findOneBySlug($slug);
if (!$task) {
$this->addFlash(
@ -158,7 +159,7 @@ class TaskController extends AbstractController
'Tâche non trouvée !'
);
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirect($request->headers->get('Referer'));
}
$updateForm = $this->createForm(TaskType::class, $task);
@ -179,7 +180,7 @@ class TaskController extends AbstractController
'Tâche modifiée !'
);
return $this->redirectToRoute('app_task_show', ['projectSlug' => $projectSlug, 'taskSlug' => $taskSlug]);
return $this->redirectToRoute('app_task_show', ['slug' => $slug]);
} catch (\Exception $exception) {
$this->addFlash(
'danger',
@ -195,21 +196,24 @@ class TaskController extends AbstractController
]);
}
#[Route('/{projectSlug}/remove/{taskSlug}', name: 'app_task_remove')]
public function remove(EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}/remove', name: 'app_task_remove')]
public function remove(Request $request, EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($taskSlug);
$task = $repository->findOneBySlug($slug);
if (!$task) {
$this->addFlash(
'warning',
'Tâche non trouvée !'
);
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirect($request->headers->get('Referer'));
}
$project = $task->getProject();
try {
$entityManager->remove($task);
$entityManager->flush();
@ -218,7 +222,7 @@ class TaskController extends AbstractController
'Tâche supprimée !'
);
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirectToRoute('app_project_show', ['slug' => $project->getSlug()]);
} catch (\Exception $exception) {
$this->addFlash(
'danger',
@ -226,24 +230,41 @@ class TaskController extends AbstractController
);
}
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirectToRoute('app_project_show', ['slug' => $slug]);
}
private function transition(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug, $status): Response
private function transition(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug, $transitionName): Response
{
$repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($taskSlug);
$task = $repository->findOneBySlug($slug);
if (!$task) {
$this->addFlash(
'warning',
'Tâche non trouvée !'
);
return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]);
return $this->redirectToRoute('app_project');
}
try {
$taskLifecycleStateMachine->apply($task, $status);
// TODO on doit pouvoir faire mieux pour le verrouillage, notamment au niveau des règles de tansition du workflow…
$transitions = array_filter(
$taskLifecycleStateMachine->getDefinition()->getTransitions(),
function ($transition) use ($transitionName) {
return $transitionName === $transition->getName();
}
);
$transition = reset($transitions);
$shouldTransitionLock = $taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['lock'];
$shouldTransitionUnlock = $taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['unlock'];
$taskLifecycleStateMachine->apply($task, $transitionName);
if ($shouldTransitionLock) {
$task->lock($this->getUser());
} elseif ($shouldTransitionUnlock) {
$task->unlock();
}
$entityManager->persist($task);
$entityManager->flush();
@ -259,31 +280,31 @@ class TaskController extends AbstractController
);
}
return $this->redirectToRoute('app_task_show', ['projectSlug' => $projectSlug, 'taskSlug' => $taskSlug]);
return $this->redirectToRoute('app_task_show', ['slug' => $slug]);
}
#[Route('/{projectSlug}/start/{taskSlug}', name: 'app_task_start')]
public function start(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}/start', name: 'app_task_start')]
public function start(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{
return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_START);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_START);
}
#[Route('/{projectSlug}/finish/{taskSlug}', name: 'app_task_finish')]
public function finish(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}/finish', name: 'app_task_finish')]
public function finish(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{
return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_FINISH);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_FINISH);
}
#[Route('/{projectSlug}/cancel/{taskSlug}', name: 'app_task_cancel')]
public function cancel(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}/cancel', name: 'app_task_cancel')]
public function cancel(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{
return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_CANCEL);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_CANCEL);
}
#[Route('/{projectSlug}/reset/{taskSlug}', name: 'app_task_reset')]
public function reset(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response
#[Route('/{slug}/reset', name: 'app_task_reset')]
public function reset(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{
return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_RESET);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_RESET);
}
#[Route('/{slug}.geojson', name: 'app_task_geojson')]
@ -297,7 +318,7 @@ class TaskController extends AbstractController
'warning',
'Tâche non trouvée !'
);
return $this->redirect($request->headers->get('referer'));
return $this->redirect($request->headers->get('Referer'));
}
return JsonResponse::fromJsonString($task->getGeojson());


+ 8
- 0
src/DataFixtures/AppFixtures.php View File

@ -5,6 +5,7 @@ namespace App\DataFixtures;
use App\Entity\Project;
use App\Entity\Tag;
use App\Entity\Task;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
@ -12,11 +13,17 @@ class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
$user = new User();
$user->setUsername('caboulot');
$user->setOsmId(80638);
$manager->persist($user);
$tag = new Tag();
$tag->setName('test');
$manager->persist($tag);
$task = new Task();
$task->setCreatedBy($user);
$task->setName('PR52 D’un canal à l’autre');
$task->setDescription('boucle au départ du port de Plaisance');
$task->setStatus(Task::STATUS_TODO);
@ -1428,6 +1435,7 @@ GEOJSON);
$manager->persist($task);
$project = new Project();
$project->setCreatedBy($user);
$project->setName('Sentiers PR Gard');
$project->setDescription('Cf <https://lite.framacalc.org/xi8ua2l6q5-a8we>');
$project->addTag($tag);


+ 32
- 0
src/Entity/Project.php View File

@ -39,6 +39,14 @@ class Project
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'project', orphanRemoval: true)]
private Collection $tasks;
#[ORM\ManyToOne(inversedBy: 'projects')]
#[ORM\JoinColumn(nullable: false)]
private ?User $createdBy = null;
#[ORM\Column]
#[Gedmo\Timestampable(on: 'create')]
private ?\DateTimeImmutable $createdAt = null;
public function __construct()
{
$this->tags = new ArrayCollection();
@ -133,4 +141,28 @@ class Project
return $this;
}
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
public function setCreatedBy(?User $createdBy): static
{
$this->createdBy = $createdBy;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
}

+ 84
- 0
src/Entity/Task.php View File

@ -61,6 +61,20 @@ class Task
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\ManyToOne(inversedBy: 'tasks')]
#[ORM\JoinColumn(nullable: false)]
private ?User $createdBy = null;
#[ORM\Column]
#[Gedmo\Timestampable(on: 'create')]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\ManyToOne]
private ?User $lockedBy = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $lockedAt = null;
public function __construct()
{
$this->comments = new ArrayCollection();
@ -201,4 +215,74 @@ class Task
return $this;
}
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
public function setCreatedBy(?User $createdBy): static
{
$this->createdBy = $createdBy;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getLockedBy(): ?User
{
return $this->lockedBy;
}
public function setLockedBy(?User $lockedBy): static
{
$this->lockedBy = $lockedBy;
return $this;
}
public function getLockedAt(): ?\DateTimeImmutable
{
return $this->lockedAt;
}
public function setLockedAt(?\DateTimeImmutable $lockedAt): static
{
$this->lockedAt = $lockedAt;
return $this;
}
public function isLocked(): bool
{
return is_null($this->lockedBy);
}
public function lock(User $user): static
{
$this->setLockedBy($user);
$this->setLockedAt(new \DateTimeImmutable('now'));
return $this;
}
public function unlock(): static
{
$this->setLockedBy(null);
$this->setLockedAt(null);
return $this;
}
}

+ 80
- 0
src/Entity/User.php View File

@ -3,6 +3,8 @@
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
@ -27,6 +29,24 @@ class User implements UserInterface
#[ORM\Column]
private ?int $osmId = null;
/**
* @var Collection<int, Project>
*/
#[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'createdBy', orphanRemoval: true)]
private Collection $projects;
/**
* @var Collection<int, Task>
*/
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'createdBy', orphanRemoval: true)]
private Collection $tasks;
public function __construct()
{
$this->projects = new ArrayCollection();
$this->tasks = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
@ -98,4 +118,64 @@ class User implements UserInterface
return $this;
}
/**
* @return Collection<int, Project>
*/
public function getProjects(): Collection
{
return $this->projects;
}
public function addProject(Project $project): static
{
if (!$this->projects->contains($project)) {
$this->projects->add($project);
$project->setCreatedBy($this);
}
return $this;
}
public function removeProject(Project $project): static
{
if ($this->projects->removeElement($project)) {
// set the owning side to null (unless already changed)
if ($project->getCreatedBy() === $this) {
$project->setCreatedBy(null);
}
}
return $this;
}
/**
* @return Collection<int, Task>
*/
public function getTasks(): Collection
{
return $this->tasks;
}
public function addTask(Task $task): static
{
if (!$this->tasks->contains($task)) {
$this->tasks->add($task);
$task->setCreatedBy($this);
}
return $this;
}
public function removeTask(Task $task): static
{
if ($this->tasks->removeElement($task)) {
// set the owning side to null (unless already changed)
if ($task->getCreatedBy() === $this) {
$task->setCreatedBy(null);
}
}
return $this;
}
}

+ 1
- 1
src/Service/GeoJsonManager.php View File

@ -22,7 +22,7 @@ class GeoJsonManager
$data = json_decode($task->getGeojson(), true);
$data['features'][0]['properties'] = array_merge($data['features'][0]['properties'], [
'name' => $task->getName(),
'url' => $this->router->generate('app_task_show', ['projectSlug' => $task->getProject()->getSlug(), 'taskSlug' => $task->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL),
'url' => $this->router->generate('app_task_show', ['slug' => $task->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL),
'color' => $this->taskLifecycleStateMachine->getMetadataStore()->getPlaceMetadata($task->getStatus())['color'],
]);
return $data;


+ 12
- 0
templates/partials/_project-metadata.html.twig View File

@ -0,0 +1,12 @@
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
</svg>
{{ project.createdBy.username }}
</span>
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
</svg>
{{ project.createdAt|format_datetime('short', 'short', locale='fr') }}
</span>

+ 20
- 0
templates/partials/_task-locking.html.twig View File

@ -0,0 +1,20 @@
{% if workflow_metadata(task, 'locking', task.status) %}
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2M5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1"/>
</svg>
{{ task.lockedBy.username }}
</span>
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
</svg>
{{ task.createdAt|format_datetime('short', 'short', locale='fr') }}
</span>
{% else %}
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-unlock" viewBox="0 0 16 16">
<path d="M11 1a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V3a3 3 0 0 1 6 0v4a.5.5 0 0 1-1 0V3a2 2 0 0 0-2-2M3 8a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1z"/>
</svg>
</span>
{% endif %}

+ 12
- 0
templates/partials/_task-metadata.html.twig View File

@ -0,0 +1,12 @@
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
</svg>
{{ task.createdBy.username }}
</span>
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
</svg>
{{ task.createdAt|format_datetime('short', 'short', locale='fr') }}
</span>

+ 2
- 0
templates/partials/_task-title.html.twig View File

@ -0,0 +1,2 @@
{{ task.name }}
<span class="badge {{ 'text-bg-' ~ workflow_metadata(task, 'color', task.status) }} ms-2">{{ workflow_metadata(task, 'title', task.status) }}</span>

+ 7
- 2
templates/project/index.html.twig View File

@ -7,11 +7,15 @@
{% block page_title %}Projets{% endblock %}
{% block page_content %}
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
<div class="row">
<div class="col mb-3">
<a href="{{ path('app_project_create') }}" class="btn btn-primary">Créer un projet</a>
<div class="btn-group">
<a href="{{ path('app_project_create') }}" class="btn btn-secondary">Créer un projet</a>
</div>
</div>
</div>
{% endif %}
{% if projects is not empty %}
<div class="row">
@ -25,8 +29,9 @@
<span class="badge text-bg-info ms-2">{{ tag.name }}</span>
{% endfor %}
</h2>
<p class="card-subtitle mb-2 text-muted">{% include 'partials/_project-metadata.html.twig' %}</p>
{% if project.description %}<p class="card-text">{{ project.description|markdown_to_html }}</p>{% endif %}
<a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}" class="btn btn-primary">Voir</a>
<a href="{{ path('app_project_show', {'slug': project.slug}) }}" class="btn btn-primary">Voir le détail</a>
</div>
</div>
</div>


+ 21
- 10
templates/project/show.html.twig View File

@ -3,7 +3,7 @@
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'slug': project.slug}) }}">{{ project.name }}</a></li>
{% endblock %}
{% block page_title %}
@ -16,10 +16,22 @@
{% block page_content %}
<div class="row">
<div class="col mb-3">
<a href="{{ path('app_project') }}" class="btn btn-primary">Revenir aux projets</a>
<a href="{{ path('app_project_update', {'projectSlug': project.slug}) }}" class="btn btn-primary">Modifier le projet</a>
<a href="{{ path('app_project_remove', {'projectSlug': project.slug}) }}" class="btn btn-primary">Supprimer le projet</a>
<a href="{{ path('app_task_create', {'projectSlug': project.slug}) }}" class="btn btn-primary">Créer une tâche</a>
<div class="btn-group">
<a href="{{ path('app_project') }}" class="btn btn-secondary">Revenir aux projets</a>
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
{% if app.user is same as(project.createdBy) %}
<a href="{{ path('app_project_update', {'slug': project.slug}) }}" class="btn btn-secondary">Modifier le projet</a>
<a href="{{ path('app_project_remove', {'slug': project.slug}) }}" class="btn btn-secondary">Supprimer le projet</a>
{% endif %}
<a href="{{ path('app_task_create', {'slug': project.slug}) }}" class="btn btn-secondary">Créer une tâche</a>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col mb-3">
<p class="text-muted">{% include 'partials/_project-metadata.html.twig' %}</p>
</div>
</div>
@ -37,6 +49,8 @@
</div>
{% if project.tasks is not empty %}
{% set dummy = tasks.setParam('_fragment', 'tasks') %}
<h2 id="tasks" class="mb-3">Tâches</h2>
<div class="row">
<div class="col mb-3">
<div class="progress-stacked">
@ -50,13 +64,11 @@
</div>
</div>
{% set dummy = tasks.setParam('_fragment', 'tasks') %}
<div id="tasks" class="row">
<div class="row">
<div class="col">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ macro.paginated(tasks, 'Identifiant', 't.id') }}</th>
<th scope="col">{{ macro.paginated(tasks, 'Nom', 't.name') }}</th>
<th scope="col">{{ macro.paginated(tasks, 'État', 't.status') }}</th>
<th scope="col">{{ macro.paginated(tasks, 'Importance', 't.important') }}</th>
@ -66,8 +78,7 @@
<tbody>
{% for task in tasks %}
<tr class="{{ 'table-' ~ workflow_metadata(task, 'color', task.status) }}">
<th scope="row">{{ task.id }}</th>
<td><a href="{{ path('app_task_show', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}">{{ task.name }}</a></td>
<td><a href="{{ path('app_task_show', {'slug': task.slug}) }}">{{ task.name }}</a></td>
<td>{{ workflow_metadata(task, 'title', task.status) }}</td>
<td>{{ task.important }}</td>
<td>{{ task.urgent }}</td>


+ 2
- 2
templates/task/create.html.twig View File

@ -2,8 +2,8 @@
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_create', {'projectSlug': project.slug}) }}">Créer une nouvelle tâche</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'slug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_create', {'slug': project.slug}) }}">Créer une nouvelle tâche</a></li>
{% endblock %}
{% block page_title %}Créer une nouvelle tâche{% endblock %}


+ 37
- 12
templates/task/show.html.twig View File

@ -3,26 +3,51 @@
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_show', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}">Tâche {{ task.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'slug': project.slug}) }}">{{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_show', {'slug': task.slug}) }}">{{ task.name }}</a></li>
{% endblock %}
{% block page_title %}
{{ task.name }}
<span class="badge {{ 'text-bg-' ~ workflow_metadata(task, 'color', task.status) }} ms-2">{{ workflow_metadata(task, 'title', task.status) }}</span>
{% include 'partials/_task-title.html.twig' %}
{% endblock %}
{% block page_content %}
<div class="row">
<div class="col mb-3">
<a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}" class="btn btn-primary">Revenir au projet</a>
<a href="{{ path('app_task_update', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}" class="btn btn-primary">Modifier la tâche</a>
<a href="{{ path('app_task_remove', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}" target="_blank" class="btn btn-primary">Supprimer la tâche</a>
<a href="{{ path('app_task_geojson', {'slug': task.slug}) }}" target="_blank" class="btn btn-primary">Télécharger GeoJSON</a>
{% for transition in workflow_transitions(task) %}
<a href="{{ path(workflow_metadata(task, 'route', transition), {'projectSlug': project.slug, 'taskSlug': task.slug}) }}" class="btn btn-primary">{{ workflow_metadata(task, 'title', transition) }}</a>
{% endfor %}
<button class="btn btn-primary" type="button" data-controller="josm" data-action="click->josm#remoteControl" data-josm-importurl-value="{{ url('app_task_osm', {'slug': task.slug}) }}" data-josm-layername-value="{{ task.name }}">Télécommande JOSM</button>
<div class="btn-group">
<a href="{{ path('app_project_show', {'slug': project.slug}) }}" class="btn btn-secondary">Revenir au projet</a>
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
{% if app.user is same as(task.createdBy) %}
<a href="{{ path('app_task_update', {'slug': task.slug}) }}" class="btn btn-secondary">Modifier la tâche</a>
<a href="{{ path('app_task_remove', {'slug': task.slug}) }}" target="_blank" class="btn btn-secondary">Supprimer la tâche</a>
{% endif %}
{% for transition in workflow_transitions(task) %}
{% if not workflow_metadata(task, 'locking', task.status) or app.user is same as(task.lockedBy) %}
<a href="{{ path(workflow_metadata(task, 'route', transition), {'slug': task.slug}) }}" class="btn btn-secondary">{{ workflow_metadata(task, 'title', transition) }}</a>
{% endif %}
{% endfor %}
{% if workflow_metadata(task, 'locking', task.status) and app.user is same as(task.lockedBy) %}
<div class="btn-group">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Télécharger la tâche
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ path('app_task_geojson', {'slug': task.slug}) }}" target="_blank">GeoJSON</a></li>
</ul>
</div>
<button class="btn btn-secondary" type="button" data-controller="josm" data-action="click->josm#remoteControl" data-josm-importurl-value="{{ url('app_task_osm', {'slug': task.slug}) }}" data-josm-layername-value="{{ task.name }}">Télécommande JOSM</button>
{% endif %}
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col mb-3">
<p class="text-muted">
{% include 'partials/_task-metadata.html.twig' %}
{% include 'partials/_task-locking.html.twig' %}
</p>
</div>
</div>


+ 2
- 2
templates/task/update.html.twig View File

@ -2,8 +2,8 @@
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_show', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}">Tâche {{ task.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'slug': project.slug}) }}">{{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_show', {'slug': task.slug}) }}">{{ task.name }}</a></li>
{% endblock %}
{% block page_title %}Modifier la tâche {{ task.name }}{% endblock %}


Loading…
Cancel
Save