This commit is contained in:
2025-07-28 17:38:40 +02:00
parent 7d55ac027a
commit 5762df8df9
12 changed files with 89 additions and 713 deletions

View File

@ -27,6 +27,7 @@ services:
- ./templates:/app/templates:delegated
- ./config:/app/config:delegated
- ./public/uploads:/app/public/uploads:delegated
- ./uploads:/app/uploads:delegated
- ./misc:/app/misc:delegated
- ./public/lib:/app/public/lib:delegated
- ./.env.local:/app/.env.local

View File

@ -0,0 +1,26 @@
<?php
namespace App\Controller;
use App\Service\FileService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class FileController extends AbstractController
{
#[Route('/user/file/{id}', name: 'app_files', methods: ['GET'])]
public function browse(int $id, Request $request, FileService $fileService): JsonResponse
{
$relativePath = $request->query->get('path', '');
try {
$files = $fileService->list($id, '');
return $this->json(['files' => $files]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 400);
}
}
}

View File

@ -12,6 +12,7 @@ use Doctrine\ORM\Mapping as ORM;
class Project
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
@ -40,13 +41,6 @@ class Project
return $this->id;
}
public function setId(int $id): static
{
$this->id = $id;
return $this;
}
public function getTitle(): ?string
{
return $this->title;

View File

@ -6,6 +6,7 @@ use App\Entity\Project;
use App\Entity\User;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
@ -25,6 +26,10 @@ class ProjectType extends AbstractType
'label' => 'Titre',
])
->add('status', ChoiceType::class, [
'choices' => ['Brouillon' => 0],
])
->add('users', EntityType::class, [
'label' => 'Propriétaires',
'class' => User::class,

View File

@ -0,0 +1,48 @@
<?php
namespace App\Service;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class FileService
{
private string $basePath;
public function __construct()
{
// Répertoire en dur dans le projet, pas dans /public
$this->basePath = realpath(__DIR__.'/../../uploads');
if (!$this->basePath) {
throw new \RuntimeException('Répertoire /uploads introuvable.');
}
}
/**
* Liste les fichiers/dossiers pour un projet donné
*/
public function list(string $projectId, string $relativePath = ''): array
{
$targetPath = $this->basePath.'/'.$projectId.'/'.ltrim($relativePath, '/');
$realPath = realpath($targetPath);
// Sécurité : protection contre les accès hors du dossier projet
if (!$realPath || !str_starts_with($realPath, $this->basePath.'/'.$projectId)) {
throw new NotFoundHttpException('Répertoire non autorisé ou inexistant.');
}
$finder = new Finder();
$finder->depth('== 0')->in($realPath);
$results = [];
foreach ($finder as $file) {
$results[] = [
'name' => $file->getFilename(),
'isDirectory' => $file->isDir(),
'path' => ltrim(str_replace($this->basePath.'/'.$projectId, '', $file->getRealPath()), '/'),
];
}
return $results;
}
}

View File

@ -1,46 +0,0 @@
{% extends 'base.html.twig' %}
{% block localstyle %}
<style>
</style>
{% endblock %}
{% block title %}
= Modification Issue = {{title}}
{% endblock %}
{% block body %}
<h1>
Modification Issue<br>
</h1>
<small class="text-muted" style="margin-top:-20px">{{title}}</small>
<br>
<br>
{{ form_start(form) }}
{{ form_widget(form.submit) }}
<a href="{{ path(routecancel,{id:issue.project.id}) }}" class="btn btn-secondary ms-1">Annuler</a>
{% include('include/error.html.twig') %}
<div class="row">
<div class="col-md-6 mx-auto">
<div class="card mt-3">
<div class="card-header">Information</div>
<div class="card-body">
{{ form_row(form.color) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }}
{% endblock %}
{% block localscript %}
<script>
$(document).ready(function() {
$("#project_title").focus();
});
</script>
{% endblock %}

View File

@ -1,465 +0,0 @@
{% extends 'base.html.twig' %}
{% block localstyle %}
<style>
</style>
{% endblock %}
{% block title %}
= Liste des Issues
{% endblock %}
{% block body %}
<h1>
Liste des Issues
</h1>
<div class="d-flex">
<div style="width:320px; padding: 0px 10px 0px 0px">
<div style="margin-bottom: 1em;">
<button id="toggleView" class="btn btn-primary w-100"> Afficher Vue Graphique</button>
</div>
<div style="margin-bottom: 1em;">
<label for="projectFilter"><strong>Filtrer par projet :</strong></label><br>
<select id="projectFilter" multiple style="width: 100%; height: 150px;" class="select2">
{# Options ajoutées dynamiquement #}
</select>
</div>
<div style="margin-bottom: 1em;">
<label for="statusFilter"><strong>Filtrer par statut :</strong></label><br>
<select id="statusFilter" multiple style="width: 100%; height: 150px;" class="select2">
{# Options JS dynamiques #}
</select>
</div>
<div style="margin-bottom: 1em;">
<label for="sprintFilter"><strong>Filtrer par sprint :</strong></label><br>
<select id="sprintFilter" multiple style="width: 100%; height: 150px;" class="select2">
{# Options JS dynamiques #}
</select>
</div>
<div style="margin-bottom: 1em;">
<label for="versionFilter"><strong>Filtrer par version :</strong></label><br>
<select id="versionFilter" multiple style="width: 100%; height: 150px;" class="select2">
{# Options JS dynamiques #}
</select>
</div>
<div style="margin-bottom: 1em;">
<label for="trackerFilter"><strong>Filtrer par tracker :</strong></label><br>
<select id="trackerFilter" multiple style="width: 100%; height: 150px;" class="select2">
{# Options JS dynamiques #}
</select>
</div>
<div style="margin-bottom: 1em;">
<label for="categoryFilter"><strong>Filtrer par catégorie :</strong></label><br>
<select id="categoryFilter" multiple style="width: 100%; height: 150px;" class="select2">
{# Options JS dynamiques #}
</select>
</div>
<div style="margin-bottom: 1em;">
<label for="assignedFilter"><strong>Filtrer par intervenant :</strong></label><br>
<select id="assignedFilter" multiple style="width: 100%; height: 150px;" class="select2">
{# Options JS dynamiques #}
</select>
</div>
</div>
<div style="flex-grow:1">
<div class="dataTable_wrapper">
<table class="table table-striped table-bordered table-hover" id="dataTables" style="width:100%; zoom:80%;">
<thead>
<tr>
<th width="70px">Id</th>
<th width="130px">Projet</th>
<th>Nom</th>
<th>Statut</th>
<th>Sprint</th>
<th>Version</th>
<th>Tracker</th>
<th>Catégorie</th>
<th>Affecté à</th>
<th>Point</th>
</tr>
</thead>
<tbody>
{% for issue in issues %}
<tr>
<td>#{{issue.id}}</td>
<td>{{issue.project.title}}</td>
<td>{{issue.redmine.subject}}</td>
<td>{{'%02d'|format(issue.rowStatus)}}-{{issue.redmine.status.name}}</td>
<td>{{issue.sprintName}}</td>
<td>{{issue.versionName}}</td>
<td>{{issue.redmine.tracker.name}}</td>
<td>{{issue.categoryName}}</td>
<td>{{issue.assignedName}}</td>
<td>{{issue.redmine.sprint.story_points}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="chartContainer" style="display:none; margin-top:2em;">
<canvas id="statusChart" width="400" height="200"></canvas>
</div>
</div>
</div>
{% endblock %}
{% block localscript %}
<script>
$(document).ready(function() {
function restoreSelectValue(selectId) {
const stored = localStorage.getItem(selectId);
if (stored) {
try {
const values = JSON.parse(stored);
$(`#${selectId}`).val(values).trigger('change');
} catch (e) {
console.error(`Erreur en restaurant le ${selectId}`, e);
}
}
}
const idProject=1;
const idStatus=3;
const idSprint=4;
const idVersion=5;
const idTracker=6;
const idCategory=7;
const idAssigned=8;
const table = $('#dataTables').DataTable({
columnDefs: [
{ "targets": "no-sort", "orderable": false },
{ "targets": "no-string", "type" : "num" }
],
responsive: true,
iDisplayLength: 100,
order: [[ 0, "desc" ]]
});
// Étape 1 : Récupérer les filtres possible
const projectSet = new Set();
table.column(idProject).data().each(function(value) {
projectSet.add(value);
});
const statusSet = new Set();
table.column(idStatus).data().each(function(value) {
statusSet.add(value);
});
const sprintSet = new Set();
table.column(idSprint).data().each(function(value) {
sprintSet.add(value);
});
const versionSet = new Set();
table.column(idVersion).data().each(function(value) {
versionSet.add(value);
});
const trackerSet = new Set();
table.column(idTracker).data().each(function(value) {
trackerSet.add(value);
});
const categorySet = new Set();
table.column(idCategory).data().each(function(value) {
categorySet.add(value);
});
const assignedSet = new Set();
table.column(idAssigned).data().each(function(value) {
assignedSet.add(value);
});
// Étape 2 : Remplir dynamiquement les filtres
const $selectProject = $('#projectFilter');
Array.from(projectSet).sort().forEach(function(project) {
const option = $('<option>', {
value: project,
text: project,
selected: false
});
$selectProject.append(option);
});
const $statusSelect = $('#statusFilter');
Array.from(statusSet).sort().forEach(function(status) {
const option = $('<option>', {
value: status,
text: status,
selected: false
});
$statusSelect.append(option);
});
const $sprintSelect = $('#sprintFilter');
Array.from(sprintSet).sort().forEach(function(sprint) {
const option = $('<option>', {
value: sprint,
text: (sprint?sprint:"Aucun"),
selected: false
});
$sprintSelect.append(option);
});
const $versionSelect = $('#versionFilter');
Array.from(versionSet).sort().forEach(function(version) {
const option = $('<option>', {
value: version,
text: (version?version:"Aucune"),
selected: false
});
$versionSelect.append(option);
});
const $trackerSelect = $('#trackerFilter');
Array.from(trackerSet).sort().forEach(function(tracker) {
const option = $('<option>', {
value: tracker,
text: tracker,
selected: false
});
$trackerSelect.append(option);
});
const $categorySelect = $('#categoryFilter');
Array.from(categorySet).sort().forEach(function(category) {
const option = $('<option>', {
value: category,
text: (category?category:"Aucune"),
selected: false
});
$categorySelect.append(option);
});
const $assignedSelect = $('#assignedFilter');
Array.from(assignedSet).sort().forEach(function(assigned) {
const option = $('<option>', {
value: assigned,
text: (assigned?assigned:"Aucun"),
selected: false
});
$assignedSelect.append(option);
});
// Etape 3 : restaurer les valeurs filtrés stocké en localstorage
restoreSelectValue('projectFilter');
restoreSelectValue('statusFilter');
restoreSelectValue('sprintFilter');
restoreSelectValue('versionFilter');
restoreSelectValue('trackerFilter');
restoreSelectValue('categoryFilter');
restoreSelectValue('assignedFilter');
// Étape 4 : Ajouter le filtre personnalisé à DataTables
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
const selectedProjects = $('#projectFilter').val();
const selectedStatuses = $('#statusFilter').val();
const selectedSprints = $('#sprintFilter').val();
const selectedVersions = $('#versionFilter').val();
const selectedTrackers = $('#trackerFilter').val();
const selectedCategories = $('#categoryFilter').val();
const selectedAssigneds = $('#assignedFilter').val();
const project = data[idProject];
const status = data[idStatus];
const sprint = data[idSprint];
const version = data[idVersion];
const tracker = data[idTracker];
const category = data[idCategory];
const assigned = data[idAssigned];
const projectMatch = !selectedProjects || selectedProjects.length === 0 || selectedProjects.includes(project);
const statusMatch = !selectedStatuses || selectedStatuses.length === 0 || selectedStatuses.includes(status);
const sprintMatch = !selectedSprints || selectedSprints.length === 0 || selectedSprints.includes(sprint);
const versionMatch = !selectedVersions || selectedVersions.length === 0 || selectedVersions.includes(version);
const trackerMatch = !selectedTrackers || selectedTrackers.length === 0 || selectedTrackers.includes(tracker);
const categoryMatch = !selectedCategories || selectedCategories.length === 0 || selectedCategories.includes(category);
const assignedMatch = !selectedAssigneds || selectedAssigneds.length === 0 || selectedAssigneds.includes(assigned);
return projectMatch && statusMatch && sprintMatch && versionMatch && trackerMatch && categoryMatch && assignedMatch;
});
// Étape 5 : Rafraîchir le tableau quand le select change
const filterIds = ['projectFilter', 'statusFilter', 'sprintFilter','versionFilter', 'trackerFilter', 'categoryFilter', 'assignedFilter'];
filterIds.forEach(function(filterId) {
$(`#${filterId}`).on('change', function() {
const val = $(this).val();
localStorage.setItem(filterId, JSON.stringify(val || []));
table.draw();
// Si on est en vue graphique, mettre à jour le graphique
if ($('#chartContainer').is(':visible')) {
updateChart();
}
});
});
// Etape 6 = chart
let chartInstance = null;
function generateGradientColors(startColor, endColor, steps) {
function hexToRgb(hex) {
hex = hex.replace(/^#/, '');
return {
r: parseInt(hex.substring(0, 2), 16),
g: parseInt(hex.substring(2, 4), 16),
b: parseInt(hex.substring(4, 6), 16)
};
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
const start = hexToRgb(startColor);
const end = hexToRgb(endColor);
const gradient = [];
for (let i = 0; i < steps; i++) {
const r = Math.round(start.r + ((end.r - start.r) * i) / (steps - 1));
const g = Math.round(start.g + ((end.g - start.g) * i) / (steps - 1));
const b = Math.round(start.b + ((end.b - start.b) * i) / (steps - 1));
gradient.push(rgbToHex(r, g, b));
}
return gradient;
}
$('#toggleView').on('click', function () {
const isTableVisible = $('#dataTables').is(':visible');
if (isTableVisible) {
// Passer en vue graphique
$('#dataTables_wrapper').hide();
$('#chartContainer').show();
$(this).text('Afficher Vue Tableau');
localStorage.setItem('currentView', 'chart');
if (!chartInstance) {
drawChart();
} else {
updateChart();
}
} else {
// Passer en vue tableau
$('#chartContainer').hide();
$('#dataTables_wrapper').show();
$(this).text('Afficher Vue Graphique');
localStorage.setItem('currentView', 'table');
}
});
function drawChart() {
const dataByStatus = {};
// Parcours des lignes filtrées de DataTables
table.rows({ filter: 'applied' }).every(function () {
const data = this.data();
const status = data[idStatus];
const points = parseFloat(data[9]) || 0;
if (!dataByStatus[status]) {
dataByStatus[status] = 0;
}
dataByStatus[status] += points;
});
// Tri alphabétique des statuts
const sortedLabels = Object.keys(dataByStatus).sort((a, b) =>
a.localeCompare(b, 'fr', { sensitivity: 'base' })
);
// Données triées
const sortedDataPoints = sortedLabels.map(label => dataByStatus[label]);
// Dégradé du bleu vers le vert
const gradientColors = generateGradientColors('#007bff', '#28a745', sortedLabels.length);
// Couleurs finales : rouge si "échec", sinon couleur du dégradé
const colors = sortedLabels.map((label, index) => {
const lowerLabel = label.toLowerCase();
if (lowerLabel.includes('échec') || lowerLabel.includes('echec')) {
return '#dc3545'; // rouge Bootstrap
} else {
return gradientColors[index];
}
});
// Création du graphique Chart.js
const ctx = document.getElementById('statusChart').getContext('2d');
chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: sortedLabels,
datasets: [{
label: 'Points par Statut',
data: sortedDataPoints,
backgroundColor: colors,
borderColor: colors,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function (context) {
return `${context.dataset.label}: ${context.parsed.y} points`;
}
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Total des Points'
}
},
x: {
title: {
display: true,
text: 'Statuts'
}
}
}
}
});
}
function updateChart() {
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
drawChart();
}
const storedView = localStorage.getItem('currentView');
if (storedView === 'chart') {
$('#dataTables_wrapper').hide();
$('#chartContainer').show();
$('#toggleView').text('Afficher Vue Tableau');
drawChart();
} else {
$('#chartContainer').hide();
$('#dataTables_wrapper').show();
$('#toggleView').text('Afficher Vue Graphique');
}
table.draw();
updateChart();
});
</script>
{% endblock %}

View File

@ -1,109 +0,0 @@
<div class="card mb-3">
<div class="card-header">
<div class="mb-0 d-flex" style="align-items:baseline">
<h5 style="flex-grow:1">#{{ issue.redmine.id }} = {{ issue.redmine.subject }}</h5>
<a href="{{redmineUrl}}/issues/{{issue.id}}" target="_blank" class="btn btn-primary me-1"><i class="fas fa-eye"></i></a>
<a href="{{redmineUrl}}/issues/{{issue.id}}/edit" target="_blank" class="btn btn-primary me-1"><i class="fas fa-pencil"></i></a>
<div class="btn btn-warning" onClick="hideIssue()"><i class="fas fa-window-close"></i></div>
</div>
<div class="d-flex">
<small class="text-muted" style="flex-grow:1">Projet : {{ issue.redmine.project.name }} • Tracker : {{ issue.redmine.tracker.name }}</small>
{% if issue.redmine.custom_fields is defined %}
{% for field in issue.redmine.custom_fields %}
{% if field.id==11 and field.value!="" %}
<small class="text-muted"><strong>{{ field.name }} =</strong> <a href="{{field.value}}" target="_blank">{{field.value|split('/')|last}}</a></small>
{% endif %}
{% endfor %}
{% endif %}
</div>
<div class="d-flex">
<a href="{{path("app_issue_update",{id:issue.id})}}">Modifier</a>
</div>
</div>
<div class="issueDescription card-body" style="height:500px;overflow-y:auto">
<div class="d-flex">
<div class="mb-3" style="flex-grow:0.5">
<strong>Tracker =</strong> {{issue.redmine.tracker.name}}<br>
<strong>Catégorie =</strong> {{issue.categoryName}}<br>
<strong>Statut =</strong> {{ issue.redmine.status.name }}<br>
<strong>Priorité =</strong> {{ issue.redmine.priority.name }}<br><br>
<strong>Sprint =</strong> {{issue.sprintName}}<br>
<strong>Version Cible =</strong> {{issue.versionName}}<br>
<strong>Story Point =</strong> {{issue.redmine.sprint.story_points}}<br>
</div>
<div>
<strong>Auteur =</strong> {{ issue.redmine.author.name }}<br>
<strong>Progression =</strong> {{ issue.redmine.done_ratio }}%<br>
<strong>Créé le =</strong>{{ issue.redmine.created_on|date('d/m/Y H:i') }}<br>
<strong>Mis à jour le =</strong>{{ issue.redmine.updated_on|date('d/m/Y H:i') }}<br>
<strong>Date de début =</strong> {{ issue.redmine.start_date|date('d/m/Y') }}<br>
<strong>Date de fin =</strong> {{ issue.redmine.due_date|date('d/m/Y') }}<br><br>
<strong>Affecté à =</strong> {{issue.assignedName}}<br>
</div>
</div>
<div>
{% if issue.redmine.custom_fields is defined %}
{% for field in issue.redmine.custom_fields %}
{% if field.id==32 %}
<hr>
<strong>{{ field.name }} </strong><br>
{{ field.value|textile_to_html|raw }}<br>
{% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if issue.parent %}
<hr>
<strong>Issues Parentes</strong>
<div onClick='viewIssue({{ issue.parent.id }})' style="cursor:pointer">
<small>#{{ issue.parent.id }} = {{ issue.parent.redmine.subject }}</small>
</div>
{% endif %}
{% for child in issue.childs %}
{% if loop.first %}
<hr>
<strong>Issues Liées</strong>
{%endif%}
<div onClick='viewIssue({{ child.id }})' style="cursor:pointer">
<small>#{{ child.id }} = {{ child.redmine.subject }}</small>
</div>
{% endfor %}
</div>
{% if issue.redmine.description %}
<div class="mb-3">
<hr>
<strong>Description :</strong>
<p>{{ issue.redmine.description|textile_to_html|raw }}</p>
<hr>
</div>
{% endif %}
{% if issue.redmine.journals is defined %}
{% for journal in issue.redmine.journals %}
{% if journal.notes != "" %}
<div class="card">
<div class="card-header">
<strong>Auteur =</strong> {{journal.user.name}}</strong><br>
<small class="text-muted">Créé le =</strong>{{ journal.created_on|date('d/m/Y H:i') }}</small>
</div>
<div class="card-body">
{{ journal.notes |textile_to_html|raw }}
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
<hr>
<br><br>
</div>
</div>

View File

@ -1,37 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %} = {{title}}{% endblock %}
{% block body %}
<h1>{{title}}</h1>
{{ form_start(form) }}
{{ form_widget(form.submit) }}
<a href="{{ path(routecancel) }}" class="btn btn-secondary ms-1">Annuler</a>
{%if mode=="update" %}<a href="{{ path(routedelete,{id:form.vars.value.id}) }}" class="btn btn-danger float-end" onclick="return confirm('Confirmez-vous la suppression de cet enregistrement ?')">Supprimer</a>{%endif%}
{% include('include/error.html.twig') %}
<div class="row">
<div class="col-md-6 mx-auto">
<div class="card mt-3">
<div class="card-header">Information</div>
<div class="card-body">
{{ form_row(form.labelRedmine) }}
{{ form_row(form.labelNinemine) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }}
{% endblock %}
{% block localscript %}
<script>
$(document).ready(function() {
$("#user_username").focus();
});
</script>
{% endblock %}

View File

@ -1,42 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %} = {{title}}{% endblock %}
{% block body %}
<h1>{{title}}</h1>
<a href="{{ path(routesubmit) }}" class="btn btn-success">Ajouter</a>
<div class="dataTable_wrapper">
<table class="table table-striped table-bordered table-hover" id="dataTables" style="width:100%">
<thead>
<tr>
<th width="70px" class="no-sort">Action</th>
<th class="no-sort">Label Redmine</th>
<th>Label Ninemine</th>
</tr>
</thead>
<tbody>
{% for label in labels %}
<tr>
<td><a href="{{ path(routeupdate,{id:label.id}) }}"><i class="fas fa-file fa-2x"></i></a></td>
<td>{{label.labelRedmine}}</td>
<td>{{label.labelNinemine}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block localscript %}
<script>
$(document).ready(function() {
$('#dataTables').DataTable({
columnDefs: [ { "targets": "no-sort", "orderable": false }, { "targets": "no-string", "type" : "num" } ],
responsive: true,
iDisplayLength: 100,
order: [[ 2, "asc" ]]
});
});
</script>
{% endblock %}

View File

@ -19,6 +19,7 @@
<div class="card-header">Information</div>
<div class="card-body">
{{ form_row(form.title) }}
{{ form_row(form.status) }}
</div>
</div>
</div>
@ -31,8 +32,11 @@
</div>
</div>
</div>
</div>
{% if mode=="update" %}
{{ render(path("app_files",{id:project.id}))}}
{% endif %}
{{ form_end(form) }}
{% endblock %}

View File

@ -11,9 +11,8 @@
<thead>
<tr>
<th width="100px" class="no-sort">Action</th>
<th width="70px">Logo</th>
<th width="70px">Modifier</th>
<th>Nom</th>
<th>Title</th>
<th width="200px">Modifié le</th>
</tr>
</thead>
<tbody>
@ -21,11 +20,9 @@
<tr>
<td>
<a href="{{ path(routeupdate,{id:project.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
<a href="{{ path('app_admin_project_reclone',{id:project.id}) }}" onclick="return confirm('Es-tu sûr de vouloir reinitialiser et reindexer le projet ?');"><i class="fas fa-rotate-right fa-2x"></i></a>
</td>
<td><img class="avatar" src="{{ asset(project.logo)}}"></td>
<td>{{project.title}}</td>
<td>{{project.redmine.updated_on}}</td>
<td>{{project.updateAt}}</td>
</tr>
{% endfor %}
</tbody>