svg
This commit is contained in:
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
@ -12,7 +14,11 @@ class HomeController extends AbstractController
|
|||||||
#[Route('/', name: 'app_home')]
|
#[Route('/', name: 'app_home')]
|
||||||
public function home(Request $request): Response
|
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', [
|
return $this->render('home/home.html.twig', [
|
||||||
'usemenu' => true,
|
'usemenu' => true,
|
||||||
|
@ -42,12 +42,6 @@ class IssueController extends AbstractController
|
|||||||
{
|
{
|
||||||
$data = $request->request;
|
$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'));
|
$target = explode('|', $data->get('target'));
|
||||||
$targetStatus = $target[0];
|
$targetStatus = $target[0];
|
||||||
$targetSprint = $target[1];
|
$targetSprint = $target[1];
|
||||||
@ -55,31 +49,60 @@ class IssueController extends AbstractController
|
|||||||
|
|
||||||
$targetIssues = $data->all('targetIssues');
|
$targetIssues = $data->all('targetIssues');
|
||||||
|
|
||||||
if (!$sourceIssues || !$source || !$targetIssues || !$target) {
|
if (!$targetIssues || !$target) {
|
||||||
return new JsonResponse(['error' => 'Données incomplètes.'], 400);
|
return new JsonResponse(['message' => 'target vide on fait rien']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$allowed) {
|
||||||
|
return new JsonResponse(['message' => 'Erreur Redmine : Déplacement Interdit'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'fixed_version_id' => $target,
|
'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' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
// $redmineService->updateIssue($id, $payload);
|
foreach ($targetIssues as $key => $issue) {
|
||||||
|
$payload['positions'][$issue] = ['position' => $key];
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ Tu peux stocker l’ordre si besoin dans Redmine via custom field (optionnel)
|
$this->redmineService->updatePosition($payload, $this->getParameter('redmineApikey'));
|
||||||
// ou en interne selon ta logique métier
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'message' => 'Ordre mis à jour',
|
'message' => 'Ordre mis à jour',
|
||||||
'moved' => $id,
|
'moved' => $id,
|
||||||
'sourceIssues' => $sourceIssues,
|
|
||||||
'sourceStatus' => $targetStatus,
|
|
||||||
'sourceSprint' => $sourceSprint,
|
|
||||||
'sourceVersion' => $sourceVersion,
|
|
||||||
|
|
||||||
'targetIssues' => $targetIssues,
|
'targetIssues' => $targetIssues,
|
||||||
'targetStatus' => $targetStatus,
|
'targetStatus' => $targetStatus,
|
||||||
'targetSprint' => $targetSprint,
|
'targetSprint' => $targetSprint,
|
||||||
'targetVersion' => $targetVersion,
|
'targetVersion' => $targetVersion,
|
||||||
|
'allowed' => $allowed,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ use App\Entity\Project;
|
|||||||
use App\Repository\IssueRepository;
|
use App\Repository\IssueRepository;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
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\Exception\TransportExceptionInterface;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
@ -273,6 +275,31 @@ class RedmineService
|
|||||||
return $allIssues;
|
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
|
public function getIssueAgile(int $issueId, string $apiKey): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@ -296,4 +323,64 @@ class RedmineService
|
|||||||
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
|
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, c’est 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,17 @@
|
|||||||
</content>
|
</content>
|
||||||
</main>
|
</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 id="mymodal" class="modal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -417,26 +417,15 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
update: function (event, ui) {
|
update: function (event, ui) {
|
||||||
// ❗ Se déclenche même pour tri dans la même colonne
|
console.log("UPDATE");
|
||||||
if (!event.originalEvent) return; // ← ignorer les appels indirects
|
if (!event.originalEvent) return;
|
||||||
const $targetContainer = $(this);
|
const $targetContainer = $(this);
|
||||||
|
|
||||||
const sourceId = $sourceContainer.data('id');
|
const sourceId = $sourceContainer.data('id');
|
||||||
const targetId = $targetContainer.data('id');
|
const targetId = $targetContainer.data('id');
|
||||||
|
|
||||||
// Ne pas dupliquer l'appel si c’est 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);
|
sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex) {
|
function sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex) {
|
||||||
@ -455,17 +444,14 @@
|
|||||||
url: url,
|
url: url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
source: sourceId,
|
|
||||||
target: targetId,
|
target: targetId,
|
||||||
sourceIssues: sourceIssues,
|
|
||||||
targetIssues: targetIssues
|
targetIssues: targetIssues
|
||||||
},
|
},
|
||||||
success: function (response) {
|
success: function (response) {
|
||||||
console.log('Déplacement réussi', response);
|
console.log('Déplacement réussi', response);
|
||||||
},
|
},
|
||||||
error: function (xhr) {
|
error: function (xhr) {
|
||||||
console.error('Erreur AJAX', xhr);
|
console.log(xhr);
|
||||||
|
|
||||||
// Annuler le déplacement
|
// Annuler le déplacement
|
||||||
if ($movedItem && $sourceContainer) {
|
if ($movedItem && $sourceContainer) {
|
||||||
const items = $sourceContainer.children();
|
const items = $sourceContainer.children();
|
||||||
@ -475,6 +461,14 @@
|
|||||||
$movedItem.insertBefore(items.eq(originalIndex));
|
$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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user