This commit is contained in:
2025-07-07 21:48:40 +02:00
parent f7de5f8f9c
commit 4a97dad74f
5 changed files with 159 additions and 38 deletions

View File

@ -2,7 +2,9 @@
namespace App\Controller;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@ -12,7 +14,11 @@ class HomeController extends AbstractController
#[Route('/', name: 'app_home')]
public function home(Request $request): Response
{
$projects = $this->getUser()->getProjects();
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedException('Vous n\'avez pas accès à cette ressource.');
}
$projects = $user->getProjects();
return $this->render('home/home.html.twig', [
'usemenu' => true,

View File

@ -42,12 +42,6 @@ class IssueController extends AbstractController
{
$data = $request->request;
$sourceIssues = $data->all('sourceIssues');
$source = explode('|', $data->get('source'));
$sourceStatus = $source[0];
$sourceSprint = $source[1];
$sourceVersion = $source[2];
$target = explode('|', $data->get('target'));
$targetStatus = $target[0];
$targetSprint = $target[1];
@ -55,31 +49,60 @@ class IssueController extends AbstractController
$targetIssues = $data->all('targetIssues');
if (!$sourceIssues || !$source || !$targetIssues || !$target) {
return new JsonResponse(['error' => 'Données incomplètes.'], 400);
if (!$targetIssues || !$target) {
return new JsonResponse(['message' => 'target vide on fait rien']);
}
$payload = [
'fixed_version_id' => $target,
];
// Modification de l'issue si issue présent dans targetIssues
$allowed = false;
if (in_array($id, $targetIssues)) {
// Verifier que l'on a la permission de changer de statut
$rissue = $this->redmineService->getIssue($id, $this->getParameter('redmineApikey'));
foreach ($rissue['allowed_statuses'] as $status) {
if ($status['id'] == $targetStatus) {
$allowed = true;
break;
}
}
// $redmineService->updateIssue($id, $payload);
if (!$allowed) {
return new JsonResponse(['message' => 'Erreur Redmine : Déplacement Interdit'], 400);
}
// ✅ Tu peux stocker lordre si besoin dans Redmine via custom field (optionnel)
// ou en interne selon ta logique métier
$payload = [
'fixed_version_id' => $targetVersion,
'agile_data_attributes' => ['agile_sprint_id' => $targetSprint],
'status_id' => $targetStatus,
];
$this->redmineService->updateIssue($id, $payload, $this->getParameter('redmineApikey'));
/*
$payload =
[
'id' => $id,
'issue' => [
'status_id' => $targetStatus,
'fixed_version_id' => $targetVersion,
],
'positions' => [],
];
foreach ($targetIssues as $key => $issue) {
$payload['positions'][$issue] = ['position' => $key];
}
$this->redmineService->updatePosition($payload, $this->getParameter('redmineApikey'));
*/
}
return new JsonResponse([
'message' => 'Ordre mis à jour',
'moved' => $id,
'sourceIssues' => $sourceIssues,
'sourceStatus' => $targetStatus,
'sourceSprint' => $sourceSprint,
'sourceVersion' => $sourceVersion,
'targetIssues' => $targetIssues,
'targetStatus' => $targetStatus,
'targetSprint' => $targetSprint,
'targetVersion' => $targetVersion,
'allowed' => $allowed,
]);
}
}

View File

@ -7,6 +7,8 @@ use App\Entity\Project;
use App\Repository\IssueRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -273,6 +275,31 @@ class RedmineService
return $allIssues;
}
public function getIssue(int $issueId, string $apiKey): array
{
try {
$url = "{$this->baseUrl}/issues/{$issueId}.json?include=allowed_statuses";
$response = $this->client->request('GET', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
$data['issue']['sprint'] = $this->getIssueAgile($issueId, $apiKey);
return $data['issue'] ?? [];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function getIssueAgile(int $issueId, string $apiKey): array
{
try {
@ -296,4 +323,64 @@ class RedmineService
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function updateIssue(int $id, array $data, string $apiKey): array
{
$url = $this->baseUrl.'/issues/'.$id.'.json';
try {
$response = $this->client->request('PUT', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => ['issue' => $data],
]);
$statusCode = $response->getStatusCode();
$content = trim($response->getContent(false));
// Si vide et code 200, cest peut-être une réussite silencieuse
if ('' === $content) {
return ['success' => true, 'message' => 'OK, mais pas de contenu'];
}
$decoded = json_decode($content, true);
if (isset($decoded['errors']) && is_array($decoded['errors']) && count($decoded['errors']) > 0) {
throw new \RuntimeException('Erreur Redmine : '.implode(', ', $decoded['errors']));
}
return $decoded;
} catch (ClientExceptionInterface|ServerExceptionInterface $e) {
// Erreur HTTP (4xx ou 5xx)
$errorBody = $e->getResponse()->getContent(false);
throw new \RuntimeException('Erreur Redmine: '.$errorBody, $e->getCode(), $e);
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function updatePosition(array $data, string $apiKey): array
{
$url = $this->baseUrl.'/agile/board';
try {
$response = $this->client->request('PUT', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => ['issue' => $data],
]);
return ['ok'];
} catch (ClientExceptionInterface|ServerExceptionInterface $e) {
// Erreur HTTP (4xx ou 5xx)
$errorBody = $e->getResponse()->getContent(false);
throw new \RuntimeException('Erreur Redmine: '.$errorBody, $e->getCode(), $e);
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
}

View File

@ -98,6 +98,17 @@
</content>
</main>
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
<div id="ajaxErrorToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="ajaxErrorMessage">
Une erreur est survenue.
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Fermer"></button>
</div>
</div>
</div>
<div id="mymodal" class="modal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">

View File

@ -417,26 +417,15 @@
},
update: function (event, ui) {
// ❗ Se déclenche même pour tri dans la même colonne
if (!event.originalEvent) return; // ← ignorer les appels indirects
console.log("UPDATE");
if (!event.originalEvent) return;
const $targetContainer = $(this);
const sourceId = $sourceContainer.data('id');
const targetId = $targetContainer.data('id');
// Ne pas dupliquer l'appel si cest un vrai "receive"
if (ui.sender) return;
sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex);
},
receive: function (event, ui) {
const $targetContainer = $(this);
const sourceId = $sourceContainer.data('id');
const targetId = $targetContainer.data('id');
sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex);
}
},
});
function sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex) {
@ -455,17 +444,14 @@
url: url,
method: 'POST',
data: {
source: sourceId,
target: targetId,
sourceIssues: sourceIssues,
targetIssues: targetIssues
},
success: function (response) {
console.log('Déplacement réussi', response);
},
error: function (xhr) {
console.error('Erreur AJAX', xhr);
console.log(xhr);
// Annuler le déplacement
if ($movedItem && $sourceContainer) {
const items = $sourceContainer.children();
@ -475,6 +461,14 @@
$movedItem.insertBefore(items.eq(originalIndex));
}
}
const message = xhr.responseJSON?.message || 'Une erreur est survenue lors de la requête.';
$('#ajaxErrorMessage').text(message);
const toast = new bootstrap.Toast(document.getElementById('ajaxErrorToast'), {
delay: 5000
});
toast.show();
}
});
}