Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Suite à l'arrivée des workflows, correction des régressions UI et améliorations UX mail/modales (reviews Lucile Schnödt, Tristan Schnödtin). **Specs & décisions :** `docs/superpowers/specs/2026-05-20-workflow-ui-fixes-design.md` **Plan d'implémentation :** `docs/superpowers/plans/2026-05-21-workflow-ui-fixes.md` Cette PR contient désormais **les specs ET l'implémentation complète**. ## Chantiers livrés | # | Chantier | Détail | |---|----------|--------| | 2 | Sélecteur de statut filtré par workflow | `statusOptions` dérivé de `project.workflow.statuses`, statut courant conservé s'il est hors workflow | | 1 | Drag & drop « Mes tâches » | handlers `@dragover/@drop` ; résolution par workflow/catégorie (0→refus, 1→PATCH, ≥2→popover `StatusPickerPopover`) | | 4 | Couleurs | (a) migration Doctrine remettant les hex classiques sur le workflow Standard ; (b) entêtes kanban teintées via `STATUS_CATEGORY_COLOR` + contraste auto ; (c) couleur par défaut par catégorie dans `WorkflowDrawer` | | 5 | Suppression du bouton « Lier un mail » | + retrait de `MailPickerModal` et i18n associée | | 6 | Création de tâche depuis un mail | back : `assigneeId` + `statusId` (défaut = 1er statut du workflow), priorité retirée (TDD) ; front : `MailCreateTaskModal` sur `AppModal` + sélecteurs user/statut | | 7 | Modale réutilisable | nouveau `components/ui/AppModal.vue` (footer sticky) ; footer de `TaskModal` sorti du form scrollable | | 3 | Cartes responsive | badges en `flex-wrap` pleine taille (plus aucun débordement) | | 8 | (dette) Sélecteur de catégorie en `MalioSelect` | la lib supporte les valeurs `string` ; note CLAUDE.md corrigée | ## Vérifications - Build frontend OK ; PHPUnit **34 tests verts** (nouveau test fonctionnel TDD sur `create-task`). - Vérif navigateur (Chrome MCP) sur **données prod importées en local** : #2, #3, #4, #5, #6, #7 confirmés. - Revue de code finale : **APPROVED_WITH_NITS**. ## À noter - ⚠️ **#1 (D&D)** : le drag & drop HTML5 natif n'est pas auto-testable → **test manuel requis**. - 🗄️ **#4 (migration)** : `migrations/Version20260521094948.php` s'appliquera en **prod au prochain `make migration-migrate`**. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Matthieu <mtholot19@gmail.com> Reviewed-on: #6
119 lines
4.2 KiB
PHP
119 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controller\Mail;
|
|
|
|
use App\Entity\Project;
|
|
use App\Entity\Task;
|
|
use App\Entity\TaskGroup;
|
|
use App\Entity\TaskMailLink;
|
|
use App\Entity\TaskStatus;
|
|
use App\Entity\User;
|
|
use App\Repository\MailMessageRepository;
|
|
use App\Repository\TaskRepository;
|
|
use App\Security\MailAccessChecker;
|
|
use DateTimeImmutable;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
#[Route('/api/mail/messages/{id}/create-task', name: 'mail_create_task', methods: ['POST'], priority: 1, requirements: ['id' => '\d+'])]
|
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
|
class MailCreateTaskController extends AbstractController
|
|
{
|
|
public function __construct(
|
|
private readonly MailMessageRepository $messageRepository,
|
|
private readonly EntityManagerInterface $em,
|
|
private readonly MailAccessChecker $accessChecker,
|
|
private readonly TaskRepository $taskRepository,
|
|
) {}
|
|
|
|
public function __invoke(Request $request, int $id): JsonResponse
|
|
{
|
|
$this->accessChecker->ensureCanAccessMail($this->getUser());
|
|
|
|
$message = $this->messageRepository->find($id);
|
|
if (null === $message) {
|
|
throw new NotFoundHttpException('Message not found');
|
|
}
|
|
|
|
$body = json_decode($request->getContent(), true);
|
|
$projectId = $body['projectId'] ?? null;
|
|
|
|
if (null === $projectId) {
|
|
throw new UnprocessableEntityHttpException('projectId is required');
|
|
}
|
|
|
|
$project = $this->em->getRepository(Project::class)->find($projectId);
|
|
if (null === $project) {
|
|
throw new NotFoundHttpException('Project not found');
|
|
}
|
|
|
|
$title = $message->getSubject() ?? 'Mail sans sujet';
|
|
if (mb_strlen($title) > 255) {
|
|
$title = mb_substr($title, 0, 252).'...';
|
|
}
|
|
|
|
$result = $this->em->wrapInTransaction(function () use ($project, $title, $body, $message) {
|
|
$maxNumber = $this->taskRepository->findMaxNumberByProjectForUpdate($project);
|
|
|
|
$task = new Task();
|
|
$task->setProject($project);
|
|
$task->setTitle($title);
|
|
$task->setNumber($maxNumber + 1);
|
|
|
|
if (isset($body['taskGroupId']) && null !== $body['taskGroupId']) {
|
|
$taskGroup = $this->em->getRepository(TaskGroup::class)->find($body['taskGroupId']);
|
|
if (null !== $taskGroup) {
|
|
$task->setGroup($taskGroup);
|
|
}
|
|
}
|
|
|
|
if (isset($body['assigneeId']) && null !== $body['assigneeId']) {
|
|
$assignee = $this->em->getRepository(User::class)->find($body['assigneeId']);
|
|
if (null !== $assignee) {
|
|
$task->setAssignee($assignee);
|
|
}
|
|
}
|
|
|
|
// Statut : celui fourni, sinon le premier statut du workflow du projet (par position)
|
|
$status = null;
|
|
if (isset($body['statusId']) && null !== $body['statusId']) {
|
|
$status = $this->em->getRepository(TaskStatus::class)->find($body['statusId']);
|
|
}
|
|
if (null === $status) {
|
|
$status = $project->getWorkflow()?->getStatuses()->first() ?: null;
|
|
}
|
|
if (null !== $status) {
|
|
$task->setStatus($status);
|
|
}
|
|
|
|
$this->em->persist($task);
|
|
|
|
$link = new TaskMailLink();
|
|
$link->setTask($task);
|
|
$link->setMailMessage($message);
|
|
$link->setLinkedAt(new DateTimeImmutable());
|
|
$link->setLinkedBy($this->getUser());
|
|
$this->em->persist($link);
|
|
|
|
$this->em->flush();
|
|
|
|
return $task;
|
|
});
|
|
|
|
return $this->json([
|
|
'taskId' => $result->getId(),
|
|
'taskNumber' => $result->getNumber(),
|
|
'taskTitle' => $result->getTitle(),
|
|
'messageId' => $message->getId(),
|
|
], 201);
|
|
}
|
|
}
|