version 1

This commit is contained in:
Freedyx29
2025-10-24 16:18:08 +02:00
parent 464156fdd1
commit 4ffa0efb5a
21 changed files with 3718 additions and 2246 deletions

View File

@@ -0,0 +1,25 @@
{% extends 'base.html.twig' %}
{% block title %}Détails Assistant IA{% endblock %}
{% block body %}
<div class="container py-4">
<h1 class="mb-4">Détails de l'assistant IA</h1>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">{{ assistant.nom }}</h5>
</div>
</div>
<h3 class="mt-4">Contributions IA</h3>
<ul class="list-group mb-4">
{% for contribIa in assistant.contribIas %}
<li class="list-group-item">
<strong>Projet :</strong> {{ contribIa.contribution.projet.nom }}<br>
<strong>Utilisateur :</strong> {{ contribIa.contribution.membre.nom }} {{ contribIa.contribution.membre.prenom }}<br>
<strong>Commentaire :</strong> {{ contribIa.commentaire }}
</li>
{% else %}
<li class="list-group-item text-muted">Aucune contribution IA liée.</li>
{% endfor %}
</ul>
<a href="{{ path('assistant_ia_index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Retour</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,90 @@
{% extends 'base.html.twig' %}
{% block title %}Assistants IA{% endblock %}
{% block body %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h1 class="display-5 fw-bold mb-0">Assistants IA</h1>
<button class="btn btn-primary btn-lg shadow" data-bs-toggle="modal" data-bs-target="#addAssistantModal">
<i class="bi bi-plus-lg me-1"></i> Ajouter un assistant IA
</button>
</div>
<div class="table-responsive shadow-sm rounded">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Nom</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for assistant in assistants %}
<tr>
<td><span class="fw-semibold text-primary">{{ assistant.nom }}</span></td>
<td>
<a href="{{ path('assistant_ia_detail', {id: assistant.id}) }}" class="btn btn-sm btn-info me-1">
<i class="bi bi-eye"></i> Détails
</a>
<button class="btn btn-sm btn-outline-secondary me-1" data-bs-toggle="modal" data-bs-target="#editAssistantModal{{ assistant.id }}">
<i class="bi bi-pencil"></i> Modifier
</button>
<form method="post" action="{{ path('assistant_ia_delete', {id: assistant.id}) }}" style="display:inline-block" onsubmit="return confirm('Supprimer cet assistant IA ?');">
<input type="hidden" name="_token" value="{{ csrf_token('delete_assistant_ia_' ~ assistant.id) }}">
<button class="btn btn-sm btn-danger"><i class="bi bi-trash"></i> Supprimer</button>
</form>
<div class="modal fade" id="editAssistantModal{{ assistant.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{{ path('assistant_ia_edit', {id: assistant.id}) }}">
<div class="modal-header">
<h5 class="modal-title">Modifier l'assistant IA</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label class="form-label">Nom</label>
<input type="text" name="nom" class="form-control" value="{{ assistant.nom }}" required maxlength="50">
</div>
{# Le champ 'type' a été supprimé car il n'existe pas dans l'entité #}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="text-center text-muted">Aucun assistant IA trouvé.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Modal ajout assistant IA #}
<div class="modal fade" id="addAssistantModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{{ path('assistant_ia_add') }}">
<div class="modal-header">
<h5 class="modal-title">Ajouter un assistant IA</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label class="form-label">Nom</label>
<input type="text" name="nom" class="form-control" required maxlength="50">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Ajouter</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -5,13 +5,76 @@
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %}
{# Bootstrap CSS via CDN (par défaut) - les templates enfants peuvent ajouter leurs propres styles ici #}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="" crossorigin="anonymous">
{% endblock %}
{% block javascripts %}
{# Importmap (si utilisé) placé dans le head par défaut #}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{# Navbar global #}
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
<div class="container-fluid">
<a class="navbar-brand" href="{{ path('projet_index') }}">ContribV2</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{{ path('projet_index') }}">Projets</a></li>
<li class="nav-item"><a class="nav-link" href="{{ path('dashboard') }}">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="{{ path('membre_index') }}">Utilisateurs</a></li>
<li class="nav-item"><a class="nav-link" href="{{ path('assistant_ia_index') }}">Assistants IA</a></li>
</ul>
<div class="d-flex">
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#helpModal">Aide</button>
</div>
</div>
</div>
</nav>
<div class="container my-4">
{# messages flash #}
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label == 'danger' ? 'danger' : (label == 'warning' ? 'warning' : (label == 'success' ? 'success' : 'info')) }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
{% block body %}{% endblock %}
</div>
{# Modale d'aide globale #}
<div class="modal fade" id="helpModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Aide</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Cette application permet de gérer des projets et leurs contributions. Depuis la liste des projets vous pouvez accéder aux détails d'un projet, ajouter/modifier/supprimer des contributions, et gérer les assistants IA.</p>
<ul>
<li>Sur la page projet : ajouter des contributions, indiquer si une contribution a utilisé un assistant IA.</li>
<li>Suppression de projet : si le projet contient des contributions, une confirmation supplémentaire est requise.</li>
<li>Utilisez les boutons « Détails », « Modifier » et « Supprimer » pour gérer les éléments.</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
{# Scripts JS à la fin du body pour de meilleures performances. Les templates enfants peuvent étendre ce bloc. #}
{% block bottom_javascripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="" crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,90 @@
{% extends 'base.html.twig' %}
{% block title %}Dashboard{% endblock %}
{% block body %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h1 class="display-5 fw-bold mb-0">Dashboard</h1>
<span class="badge rounded-pill bg-primary fs-5 px-3 py-2 shadow">Vue synthétique</span>
</div>
<div class="alert alert-info shadow-sm mb-4">
<h5 class="mb-2"><i class="bi bi-robot me-2"></i>Suggestions IA</h5>
<ul class="mb-0 ps-3">
<li>Vous avez <strong>{{ nbProjets }}</strong> projet{{ nbProjets > 1 ? 's' : '' }}, <strong>{{ nbMembres }}</strong> utilisateur{{ nbMembres > 1 ? 's' : '' }}, <strong>{{ nbAssistants }}</strong> assistant{{ nbAssistants > 1 ? 's IA' : ' IA' }}, et <strong>{{ nbContributions }}</strong> contribution{{ nbContributions > 1 ? 's' : '' }}.</li>
<li>Surveillez les activités récentes pour suivre lavancement et limplication des membres.</li>
<li>Relancez les membres ou clôturez les projets inactifs si besoin.</li>
<li>Utilisez les tris et filtres pour analyser la répartition des contributions.</li>
<li>Besoin daide? <span class="text-primary">Cliquez sur «Aide» dans la barre de navigation.</span></li>
</ul>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-0 shadow h-100 text-bg-primary">
<div class="card-body text-center">
<i class="bi bi-kanban-fill fs-1 mb-2"></i>
<h5 class="card-title">Projets</h5>
<p class="card-text display-6 fw-bold">{{ nbProjets }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow h-100 text-bg-success">
<div class="card-body text-center">
<i class="bi bi-people-fill fs-1 mb-2"></i>
<h5 class="card-title">Utilisateurs</h5>
<p class="card-text display-6 fw-bold">{{ nbMembres }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow h-100 text-bg-info">
<div class="card-body text-center">
<i class="bi bi-cpu-fill fs-1 mb-2"></i>
<h5 class="card-title">Assistants IA</h5>
<p class="card-text display-6 fw-bold">{{ nbAssistants }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow h-100 text-bg-warning">
<div class="card-body text-center">
<i class="bi bi-lightbulb-fill fs-1 mb-2"></i>
<h5 class="card-title">Contributions</h5>
<p class="card-text display-6 fw-bold">{{ nbContributions }}</p>
</div>
</div>
</div>
</div>
<h2 class="h4 mt-4 mb-3"><i class="bi bi-clock-history me-2"></i>Activités récentes</h2>
<div class="table-responsive shadow-sm rounded">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th style="display:none;">ID</th>
<th>Projet</th>
<th>Membre</th>
<th>Durée</th>
<th>Date</th>
<th>Commentaire</th>
</tr>
</thead>
<tbody>
{% for contrib in recentContribs %}
<tr>
<td style="display:none;"><input type="hidden" value="{{ contrib.id }}" /></td>
<td><span class="fw-semibold text-primary">{{ contrib.projet.nom }}</span></td>
<td>{{ contrib.membre ? '<span class="badge bg-secondary">' ~ contrib.membre.nom ~ ' ' ~ contrib.membre.prenom ~ '</span>' : '' }}</td>
<td><span class="badge bg-info text-dark">{{ contrib.duree }}</span></td>
<td>{{ contrib.dateContribution ? contrib.dateContribution|date('d/m/Y') : '' }}</td>
<td>{{ contrib.commentaire }}</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center text-muted">Aucune activité récente.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends 'base.html.twig' %}
{% block title %}Détails utilisateur{% endblock %}
{% block body %}
<div class="container py-4">
<h1 class="mb-4">Détails de l'utilisateur</h1>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">{{ membre.nom }} {{ membre.prenom }}</h5>
<p class="card-text"><strong>Email :</strong> {{ membre.email }}</p>
</div>
</div>
<h3 class="mt-4">Contributions</h3>
<ul class="list-group mb-4">
{% for contribution in membre.contributions %}
<li class="list-group-item">
<strong>Projet :</strong> {{ contribution.projet.nom }}<br>
{% set iaList = [] %}
{% for contribIa in contribution.contribIas %}
{% if contribIa.assistantIa is not null %}
{% set iaList = iaList|merge([contribIa.assistantIa.nom]) %}
{% endif %}
{% endfor %}
<strong>Assistants IA :</strong> {{ iaList|join(', ') }}<br>
<strong>Description :</strong> {{ contribution.commentaire }}
</li>
{% else %}
<li class="list-group-item text-muted">Aucune contribution liée.</li>
{% endfor %}
</ul>
<a href="{{ path('membre_index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Retour</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,109 @@
{% extends 'base.html.twig' %}
{% block title %}Utilisateurs{% endblock %}
{% block body %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h1 class="display-5 fw-bold mb-0">Utilisateurs</h1>
<button class="btn btn-primary btn-lg shadow" data-bs-toggle="modal" data-bs-target="#addMembreModal">
<i class="bi bi-plus-lg me-1"></i> Ajouter un utilisateur
</button>
</div>
<div class="table-responsive shadow-sm rounded">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for membre in membres %}
<tr>
<td><span class="fw-semibold text-primary">{{ membre.nom }}</span></td>
<td>{{ membre.prenom }}</td>
<td>{{ membre.email }}</td>
<td>
<a href="{{ path('membre_detail', {id: membre.id}) }}" class="btn btn-sm btn-info me-1">
<i class="bi bi-eye"></i> Détails
</a>
<button class="btn btn-sm btn-outline-secondary me-1" data-bs-toggle="modal" data-bs-target="#editMembreModal{{ membre.id }}">
<i class="bi bi-pencil"></i> Modifier
</button>
<form method="post" action="{{ path('membre_delete', {id: membre.id}) }}" style="display:inline-block" onsubmit="return confirm('Supprimer cet utilisateur ?');">
<input type="hidden" name="_token" value="{{ csrf_token('delete_membre_' ~ membre.id) }}">
<button class="btn btn-sm btn-danger"><i class="bi bi-trash"></i> Supprimer</button>
</form>
<div class="modal fade" id="editMembreModal{{ membre.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{{ path('membre_edit', {id: membre.id}) }}">
<div class="modal-header">
<h5 class="modal-title">Modifier l'utilisateur</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label class="form-label">Nom</label>
<input type="text" name="nom" class="form-control" value="{{ membre.nom }}" required maxlength="50">
</div>
<div class="mb-2">
<label class="form-label">Prénom</label>
<input type="text" name="prenom" class="form-control" value="{{ membre.prenom }}" required maxlength="50">
</div>
<div class="mb-2">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" value="{{ membre.email }}" required maxlength="100">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="text-center text-muted">Aucun utilisateur trouvé.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Modal ajout utilisateur #}
<div class="modal fade" id="addMembreModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{{ path('membre_add') }}">
<div class="modal-header">
<h5 class="modal-title">Ajouter un utilisateur</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label class="form-label">Nom</label>
<input type="text" name="nom" class="form-control" required maxlength="50">
</div>
<div class="mb-2">
<label class="form-label">Prénom</label>
<input type="text" name="prenom" class="form-control" required maxlength="50">
</div>
<div class="mb-2">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" required maxlength="100">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Ajouter</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,171 @@
{% extends 'base.html.twig' %}
{% block title %}Liste des projets{% endblock %}
{% block body %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h1 class="display-5 fw-bold mb-0">Projets</h1>
<button class="btn btn-primary btn-lg shadow" data-bs-toggle="modal" data-bs-target="#addProjetModal">
<i class="bi bi-plus-lg me-1"></i> Nouveau projet
</button>
</div>
<div class="table-responsive shadow-sm rounded">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Nom</th>
<th>Commentaire</th>
<th>Date de lancement</th>
<th>Date de clôture</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for projet in projets %}
<tr>
<td><span class="fw-semibold text-primary">{{ projet.nom }}</span></td>
<td>{{ projet.commentaire }}</td>
<td>{{ projet.dateLancement ? projet.dateLancement|date('d/m/Y') : '' }}</td>
<td>{{ projet.dateCloture ? projet.dateCloture|date('d/m/Y') : '' }}</td>
<td><span class="badge bg-info text-dark">{{ projet.statut }}</span></td>
<td>
<a class="btn btn-sm btn-outline-primary me-1" href="{{ path('projet_show', {id: projet.id}) }}">
<i class="bi bi-eye"></i> Détails
</a>
<button class="btn btn-sm btn-outline-secondary me-1" data-bs-toggle="modal" data-bs-target="#editProjetModal{{ projet.id }}">
<i class="bi bi-pencil"></i> Modifier
</button>
<form method="post" action="{{ path('projet_delete', {id: projet.id}) }}" style="display:inline-block" data-contrib-count="{{ projet.contributions|length }}">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ projet.id) }}">
<input type="hidden" name="_force" value="0">
<button class="btn btn-sm btn-danger btn-delete-projet"><i class="bi bi-trash"></i> Supprimer</button>
</form>
<div class="modal fade" id="editProjetModal{{ projet.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{{ path('projet_edit', {id: projet.id}) }}">
<input type="hidden" name="_token" value="{{ csrf_token('edit_projet_' ~ projet.id) }}">
<div class="modal-header">
<h5 class="modal-title">Modifier le projet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label class="form-label">Nom</label>
<input type="text" name="nom" class="form-control" value="{{ projet.nom }}" required maxlength="50">
</div>
<div class="mb-2">
<label class="form-label">Commentaire</label>
<textarea name="commentaire" class="form-control">{{ projet.commentaire }}</textarea>
</div>
<div class="mb-2">
<label class="form-label">Date de lancement</label>
<input type="date" name="dateLancement" class="form-control" value="{{ projet.dateLancement ? projet.dateLancement|date('Y-m-d') : '' }}">
</div>
<div class="mb-2">
<label class="form-label">Date de clôture</label>
<input type="date" name="dateCloture" class="form-control" value="{{ projet.dateCloture ? projet.dateCloture|date('Y-m-d') : '' }}">
</div>
<div class="mb-2">
<label class="form-label">Statut</label>
<select name="statut" class="form-select" required>
<option value="en_attente" {% if projet.statut == 'en_attente' %}selected{% endif %}>En attente</option>
<option value="en_cours" {% if projet.statut == 'en_cours' %}selected{% endif %}>En cours</option>
<option value="termine" {% if projet.statut == 'termine' %}selected{% endif %}>Terminé</option>
<option value="annule" {% if projet.statut == 'annule' %}selected{% endif %}>Annulé</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center text-muted">Aucun projet trouvé.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block bottom_javascripts %}
{{ parent() }}
<!-- Modal ajout projet -->
<div class="modal fade" id="addProjetModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{{ path('projet_add') }}">
<div class="modal-header">
<h5 class="modal-title">Ajouter un projet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="_token" value="{{ csrf_token('projet_add') }}">
<div class="mb-2">
<label class="form-label">Nom</label>
<input type="text" name="nom" class="form-control" required maxlength="50">
</div>
<div class="mb-2">
<label class="form-label">Commentaire</label>
<textarea name="commentaire" class="form-control"></textarea>
</div>
<div class="mb-2">
<label class="form-label">Date de lancement</label>
<input type="date" name="dateLancement" class="form-control">
</div>
<div class="mb-2">
<label class="form-label">Date de clôture</label>
<input type="date" name="dateCloture" class="form-control">
</div>
<div class="mb-2">
<label class="form-label">Statut</label>
<select name="statut" class="form-select" required>
<option value="en_attente">En attente</option>
<option value="en_cours">En cours</option>
<option value="termine">Terminé</option>
<option value="annule">Annulé</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Ajouter</button>
</div>
</form>
</div>
</div>
</div>
<script>
(function(){
document.querySelectorAll('form[data-contrib-count]').forEach(function(form){
var btn = form.querySelector('.btn-delete-projet');
btn.addEventListener('click', function(ev){
ev.preventDefault();
var count = parseInt(form.dataset.contribCount || '0', 10);
var proceed = false;
if (count > 0) {
proceed = confirm('Ce projet contient ' + count + ' contribution(s). La suppression entrainera la perte des contributions. Confirmer la suppression ?');
if (proceed) {
form.querySelector('input[name="_force"]').value = '1';
}
} else {
proceed = confirm('Confirmer la suppression du projet ?');
}
if (proceed) {
form.submit();
}
});
});
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,258 @@
{# show.html.twig — partie contributions (remplace la portion correspondante) #}
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Contributions</h5>
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<small class="text-muted">Trier par :</small>
<a href="?sort=date&direction={{ sort == 'date' and direction == 'asc' ? 'desc' : 'asc' }}" class="btn btn-sm btn-link">Date</a>
<a href="?sort=membre&direction={{ sort == 'membre' and direction == 'asc' ? 'desc' : 'asc' }}" class="btn btn-sm btn-link">Développeur</a>
<a href="?sort=duree&direction={{ sort == 'duree' and direction == 'asc' ? 'desc' : 'asc' }}" class="btn btn-sm btn-link">Durée</a>
</div>
<div>
<button class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#addContribModal">Ajouter une contribution</button>
</div>
</div>
{% if contribs is empty %}
<p>Aucune contribution pour ce projet.</p>
{% else %}
<div class="table-responsive">
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>Développeur</th>
<th>Date</th>
<th>Durée</th>
<th>Utilise IA ?</th>
<th>Commentaires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for c in contribs %}
<tr id="contrib-row-{{ c.id }}" data-token="{{ csrf_token('contrib_edit' ~ c.id) }}">
<td>{{ c.membre }}</td>
<td>{{ c.dateContribution ? c.dateContribution|date('d/m/Y') : '' }}</td>
{# Durée (affichage + input caché) #}
<td class="contrib-duree">
<span class="read-mode">{{ c.duree }} min</span>
<input class="edit-mode form-control form-control-sm" type="number" min="0" value="{{ c.duree }}" style="display:none; width:100px;">
</td>
{# Utilise IA ? (badge remains) #}
<td>
{% if c.contribIas|length > 0 %}
<span class="badge bg-success">Oui</span>
{% else %}
<span class="badge bg-secondary">Non</span>
{% endif %}
</td>
{# Commentaire (span + textarea) #}
<td class="contrib-commentaire">
<div class="read-mode text-truncate" style="max-width:320px;">{{ c.commentaire }}</div>
<textarea class="edit-mode form-control form-control-sm" style="display:none; max-width:320px;" rows="2">{{ c.commentaire }}</textarea>
</td>
{# Assistant (display name + select) #}
<td style="min-width:220px;">
<div class="d-flex gap-2 align-items-center">
<div class="flex-grow-1">
<div class="read-mode">
{% if c.contribIas|first %}
{{ c.contribIas|first.assistantIa.nom }}
{% else %}
Aucun
{% endif %}
</div>
<select class="edit-mode form-select form-select-sm" style="display:none;">
<option value="">Aucun</option>
{% for a in assistants %}
<option value="{{ a.id }}" {% if c.contribIas|first and c.contribIas|first.assistantIa.id == a.id %}selected{% endif %}>{{ a.nom }}</option>
{% endfor %}
</select>
</div>
{# Actions: cadenas + save/cancel + suppression #}
<div class="btn-group btn-group-sm" role="group" aria-label="actions">
<button type="button" class="btn btn-outline-secondary btn-lock" title="Éditer">
<i class="bi bi-lock-fill"></i>
</button>
<button type="button" class="btn btn-primary btn-save" style="display:none;">
<i class="bi bi-check-lg"></i> Enregistrer
</button>
<button type="button" class="btn btn-secondary btn-cancel" style="display:none;">
Annuler
</button>
<form method="post" action="{{ path('contrib_delete', {id: c.id}) }}" style="display:inline-block" onsubmit="return confirm('Confirmer la suppression de cette contribution ?');">
<input type="hidden" name="_token" value="{{ csrf_token('contrib_delete' ~ c.id) }}">
<button class="btn btn-sm btn-danger" type="submit"><i class="bi bi-trash"></i></button>
</form>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<p><strong>Durée totale :</strong> {{ totalDuree }} minutes ({{ (totalDuree // 60) }}h{{ '%02d'|format(totalDuree % 60) }})</p>
<p><strong>Développeurs ayant travaillé sur ce projet :</strong> {{ developpeurs|join(', ') }}</p>
</div>
{% endif %}
</div>
</div>
<a href="{{ path('projet_index') }}" class="btn btn-outline-secondary">Retour à la liste</a>
{# keep addContribModal from your original file here (unchanged) #}
{# ... (modal addContribModal) ... #}
{# JS pour édition inline et cadenas #}
{% block bottom_javascripts %}
{{ parent() }}
<style>
/* petit style utilitaire */
.btn-lock { width:38px; }
tr.editing { background: rgba(13,110,253,0.04); }
.text-truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
</style>
<script>
(function(){
// Helper: find elements in a row
function rowEls(row){
return {
row: row,
lockBtn: row.querySelector('.btn-lock'),
saveBtn: row.querySelector('.btn-save'),
cancelBtn: row.querySelector('.btn-cancel'),
dureeSpan: row.querySelector('.contrib-duree .read-mode'),
dureeInput: row.querySelector('.contrib-duree .edit-mode'),
commentaireSpan: row.querySelector('.contrib-commentaire .read-mode'),
commentaireTextarea: row.querySelector('.contrib-commentaire .edit-mode'),
assistantRead: row.querySelector('td:nth-child(6) .read-mode'),
assistantSelect: row.querySelector('td:nth-child(6) .edit-mode'),
token: row.dataset.token || ''
};
}
function setEditMode(row, enabled){
var els = rowEls(row);
if(enabled){
row.classList.add('editing');
// show inputs
Array.from(row.querySelectorAll('.read-mode')).forEach(e => e.style.display = 'none');
Array.from(row.querySelectorAll('.edit-mode')).forEach(e => e.style.display = '');
els.lockBtn.innerHTML = '<i class="bi bi-unlock-fill"></i>';
els.saveBtn.style.display = '';
els.cancelBtn.style.display = '';
} else {
row.classList.remove('editing');
Array.from(row.querySelectorAll('.read-mode')).forEach(e => e.style.display = '');
Array.from(row.querySelectorAll('.edit-mode')).forEach(e => e.style.display = 'none');
els.lockBtn.innerHTML = '<i class="bi bi-lock-fill"></i>';
els.saveBtn.style.display = 'none';
els.cancelBtn.style.display = 'none';
}
}
function showError(row, message){
// simple alert for now; you can replace with nicer toast
alert('Erreur: ' + message);
console.error(message);
}
// Attache handlers pour chaque ligne
document.querySelectorAll('tr[id^="contrib-row-"]').forEach(function(row){
var els = rowEls(row);
// Lock/unlock
els.lockBtn.addEventListener('click', function(){
var editing = row.classList.contains('editing');
if(editing){
// if currently editing, act like "lock" (cancel changes)
if(confirm('Verrouiller la ligne et annuler les modifications non sauvegardées ?')) {
// reset inputs to original values from read-mode
els.dureeInput.value = (els.dureeSpan.textContent || '').replace(' min','').trim();
els.commentaireTextarea.value = els.commentaireSpan.textContent || '';
// assistant - try to select option by text
var currentAssistText = els.assistantRead ? els.assistantRead.textContent.trim() : '';
var found = Array.from(els.assistantSelect.options).find(o => o.text === currentAssistText);
els.assistantSelect.value = found ? found.value : '';
setEditMode(row, false);
}
} else {
setEditMode(row, true);
}
});
// Cancel button
els.cancelBtn.addEventListener('click', function(){
if(confirm('Annuler les modifications ?')){
els.dureeInput.value = (els.dureeSpan.textContent || '').replace(' min','').trim();
els.commentaireTextarea.value = els.commentaireSpan.textContent || '';
var currentAssistText = els.assistantRead ? els.assistantRead.textContent.trim() : '';
var found = Array.from(els.assistantSelect.options).find(o => o.text === currentAssistText);
els.assistantSelect.value = found ? found.value : '';
setEditMode(row, false);
}
});
// Save button -> ajax POST JSON
els.saveBtn.addEventListener('click', function(){
var id = row.id.replace('contrib-row-','');
var data = {
duree: parseInt(els.dureeInput.value || '0', 10),
assistant: els.assistantSelect.value || null,
commentaire: els.commentaireTextarea.value || '',
token: els.token
};
// simple validation
if(isNaN(data.duree) || data.duree < 0){
return showError(row, 'Durée invalide.');
}
els.saveBtn.disabled = true;
els.saveBtn.textContent = 'Enregistrement...';
fetch('{{ path('contrib_edit_inline', {'id': 'REPLACE_ID'}) }}'.replace('REPLACE_ID', id), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
}).then(function(resp){
return resp.json().then(function(json){ return { ok: resp.ok, status: resp.status, json: json }; });
}).then(function(res){
if(!res.ok){
showError(row, res.json && res.json.message ? res.json.message : ('HTTP ' + res.status));
return;
}
// update read mode values with returned data
var j = res.json;
els.dureeSpan.textContent = (j.duree || 0) + ' min';
els.commentaireSpan.textContent = j.commentaire || '';
if(els.assistantRead) els.assistantRead.textContent = j.assistantName || 'Aucun';
// update total duration? (optional) — not handled here
setEditMode(row, false);
}).catch(function(err){
showError(row, err.message || err);
}).finally(function(){
els.saveBtn.disabled = false;
els.saveBtn.innerHTML = '<i class="bi bi-check-lg"></i> Enregistrer';
});
});
});
})();
</script>
{% endblock %}