version 1
This commit is contained in:
258
templates/projet/show.html.twig
Normal file
258
templates/projet/show.html.twig
Normal 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 %}
|
||||
Reference in New Issue
Block a user