progression 50% pour evolution des joueurs
CI - Security, Lint & Tests / validate (push) Failing after 1m13s

This commit is contained in:
cedrick2711
2026-09-05 11:30:13 -04:00
parent cf551867c9
commit f468217745
22 changed files with 2235 additions and 8 deletions
+6
View File
@@ -68,6 +68,12 @@
</a>
</li>
{% endif %}
<li>
<a href="{{ url_for('objectives.list_objectives') }}" class="{% if request.endpoint and 'objectives' in request.endpoint %}active{% endif %}">
<i class="fas fa-bullseye"></i>
<span>{{ _('Objectives') }}</span>
</a>
</li>
{% if current_user.role == 'player' %}
<li>
<a href="{{ url_for('teams.my_teams') }}" class="{% if request.endpoint == 'teams.my_teams' %}active{% endif %}">
+348
View File
@@ -0,0 +1,348 @@
{% extends "layouts/base.html" %}
{% block title %}{% if edit_mode %}{{ _('Edit Objective') }}{% else %}{{ _('New Objective') }}{% endif %} - UdeS team manager{% endblock %}
{% block page_title %}{% if edit_mode %}{{ _('Edit Objective') }}{% else %}{{ _('New Objective') }}{% endif %}{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">{{ _('Home') }} / <a href="{{ url_for('objectives.list_objectives') }}">{{ _('Objectives') }}</a> / {% if edit_mode %}{{ _('Edit') }}{% else %}{{ _('New') }}{% endif %}</span>{% endblock %}
{% block content %}
<div class="objective-editor">
<div class="objective-editor-form">
<div class="card">
<div class="card-header"><h3><i class="fas fa-edit"></i> {% if edit_mode %}{{ _('Edit Objective') }}{% else %}{{ _('Create Objective') }}{% endif %}</h3></div>
<div class="card-body">
<form method="POST" action="{% if edit_mode %}{{ url_for('objectives.update_objective', objective_id=objective.id) }}{% else %}{{ url_for('objectives.create_objective') }}{% endif %}" class="form" id="objForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-section">
<h4><i class="fas fa-info-circle"></i> {{ _('1. General Information') }}</h4>
<div class="form-row">
<div class="form-group col-6">
<label for="obj_type">{{ _('Type') }}</label>
<select name="type" id="obj_type" class="form-select" onchange="updatePreview();toggleAssignment()">
{% for val, label in objective_types %}<option value="{{ val }}"{% if objective and objective.type == val %} selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div class="form-group col-6">
<label for="obj_status">{{ _('Status') }}</label>
<select name="status" id="obj_status" class="form-select" onchange="updatePreview()">
{% for val, label in objective_statuses %}<option value="{{ val }}"{% if objective and objective.status == val %} selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label for="obj_title">{{ _('Title') }} <span class="required">*</span></label>
<input type="text" name="title" id="obj_title" class="form-input" required value="{{ objective.title if objective else '' }}" placeholder="{{ _('e.g. Improve headshot accuracy') }}" oninput="updatePreview()">
</div>
<div class="form-group">
<label for="obj_description">{{ _('Description') }}</label>
<textarea name="description" id="obj_description" rows="3" class="form-textarea" placeholder="{{ _('Describe the objective...') }}" oninput="updatePreview()">{{ objective.description if objective else '' }}</textarea>
</div>
<div class="form-row">
<div class="form-group col-6" id="playerField">
<label for="obj_player_id">{{ _('Assigned to (Player)') }}</label>
<select name="player_id" id="obj_player_id" class="form-select" onchange="updatePreview()">
<option value="">{{ _('-- Select a player --') }}</option>
{% for p in players %}<option value="{{ p.id }}"{% if objective and objective.player_id == p.id %} selected{% endif %}>{{ p.username }}</option>{% endfor %}
</select>
</div>
<div class="form-group col-6" id="teamField">
<label for="obj_team_id">{{ _('Team') }}</label>
<select name="team_id" id="obj_team_id" class="form-select" onchange="updatePreview()">
<option value="">{{ _('-- Select a team --') }}</option>
{% for t in teams %}<option value="{{ t.id }}"{% if objective and objective.team_id == t.id %} selected{% endif %}>{{ t.name }}</option>{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" name="is_template" id="obj_is_template" value="1"{% if objective and objective.is_template %} checked{% endif %} onchange="updatePreview();toggleAssignment()"> {{ _('This is a template (reusable pattern)') }}</label>
</div>
</div>
<div class="form-section">
<h4><i class="fas fa-chart-line"></i> {{ _('2. KPI & Target') }}</h4>
<div class="form-row">
<div class="form-group col-4">
<label for="obj_kpi_metric">{{ _('KPI metric') }}</label>
<input type="text" name="kpi_metric" id="obj_kpi_metric" class="form-input" value="{{ objective.kpi_metric if objective else '' }}" placeholder="{{ _('e.g. HS rate, KDA, CS/min') }}" oninput="updatePreview()">
</div>
<div class="form-group col-4">
<label for="obj_kpi_target">{{ _('Target value') }}</label>
<input type="number" step="0.01" name="kpi_target" id="obj_kpi_target" class="form-input" value="{{ objective.kpi_target if objective else '' }}" placeholder="{{ _('e.g. 40') }}" oninput="updatePreview()">
</div>
<div class="form-group col-4">
<label for="obj_kpi_unit">{{ _('Unit') }}</label>
<input type="text" name="kpi_unit" id="obj_kpi_unit" class="form-input" value="{{ objective.kpi_unit if objective else '' }}" placeholder="{{ _('e.g. points, percentage') }}" oninput="updatePreview()">
</div>
</div>
</div>
<div class="form-section">
<h4><i class="fas fa-calendar-alt"></i> {{ _('3. Deadline & Priority') }}</h4>
<div class="form-row">
<div class="form-group col-4">
<label for="obj_start_date">{{ _('Start date') }}</label>
<input type="date" name="start_date" id="obj_start_date" class="form-input" value="{{ objective.start_date.strftime('%Y-%m-%d') if objective and objective.start_date else '' }}" onchange="updatePreview()">
</div>
<div class="form-group col-4">
<label for="obj_due_date">{{ _('Deadline') }} <span class="required">*</span></label>
<input type="date" name="due_date" id="obj_due_date" class="form-input" required value="{{ objective.due_date.strftime('%Y-%m-%d') if objective else '' }}" onchange="updatePreview()">
</div>
<div class="form-group col-4">
<label for="obj_priority">{{ _('Priority') }}</label>
<select name="priority" id="obj_priority" class="form-select" onchange="updatePreview()">
{% for val, label in objective_priorities %}<option value="{{ val }}"{% if objective and objective.priority == val %} selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
</div>
</div>
<div class="form-section">
<h4><i class="fas fa-bell"></i> {{ _('4. Tracking & Alerts') }}</h4>
<div class="form-row">
<div class="form-group col-6">
<label for="obj_check_frequency">{{ _('Check frequency') }}</label>
<select name="check_frequency" id="obj_check_frequency" class="form-select">
<option value="">{{ _('-- None --') }}</option>
{% for val, label in objective_frequencies %}<option value="{{ val }}"{% if objective and objective.check_frequency == val %} selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div class="form-group col-6">
<label for="obj_progress">{{ _('Progress') }} (<span id="progressVal">{{ objective.progress if objective else 0 }}</span>%)</label>
<input type="range" id="obj_progress" name="progress" min="0" max="100" value="{{ objective.progress if objective else 0 }}" class="form-range" oninput="document.getElementById('progressVal').textContent=this.value;updatePreview()">
</div>
</div>
</div>
<div class="form-section">
<h4><i class="fas fa-cog"></i> {{ _('5. Advanced Settings') }}</h4>
<div class="form-group">
<label for="obj_category">{{ _('Category') }}</label>
<input type="text" name="category" id="obj_category" class="form-input" value="{{ objective.category if objective else '' }}" placeholder="{{ _('e.g. Mechanics, Teamplay, Communication') }}">
</div>
<div class="form-group">
<label for="obj_tags">{{ _('Tags') }}</label>
<input type="text" name="tags" id="obj_tags" class="form-input" value="{{ objective.tags if objective else '' }}" placeholder="{{ _('Separate with commas') }}">
</div>
</div>
<div class="form-actions">
<a href="{{ url_for('objectives.list_objectives') }}" class="btn btn-secondary">{{ _('Cancel') }}</a>
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> {% if edit_mode %}{{ _('Update') }}{% else %}{{ _('Create') }}{% endif %}</button>
</div>
</form>
</div>
</div>
</div>
<div class="objective-editor-preview">
<div class="card mb-4" id="previewCard">
<div class="card-header"><h3><i class="fas fa-eye"></i> {{ _('Player Preview') }}</h3></div>
<div class="card-body">
<div id="previewContent">
<h4 id="previewTitle">{{ objective.title if objective else _('Objective title') }}</h4>
<p id="previewDesc" class="text-muted small">{{ objective.description if objective else '' }}</p>
<div class="objective-tags" id="previewTags">{% if objective and objective.tags %}{% for tag in objective.tags.split(',') %}<span class="badge badge-light">{{ tag.strip() }}</span>{% endfor %}{% endif %}</div>
<div class="preview-stats">
<div class="preview-stat"><span class="preview-label">{{ _('Assigned to') }}</span><span id="previewAssignee" class="preview-value">{% if objective and objective.player %}{{ objective.player.username }}{% else %}-{% endif %}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('KPI') }}</span><span id="previewKpi" class="preview-value">{% if objective and objective.kpi_metric %}{{ objective.kpi_metric }}: {{ objective.kpi_target }}{{ objective.kpi_unit }}{% else %}-{% endif %}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('Deadline') }}</span><span id="previewDeadline" class="preview-value">{% if objective and objective.due_date %}{{ objective.due_date.strftime('%Y-%m-%d') }}{% endif %}</span></div>
</div>
<div class="progress-section">
<span class="preview-label">{{ _('Progress') }}</span>
<div class="progress-bar-container" style="width:100%"><div class="progress-bar" id="previewBar" style="width:{{ objective.progress if objective else 0 }}%"></div></div>
<strong><span id="previewProgress">{{ objective.progress if objective else 0 }}</span>%</strong>
</div>
</div>
<div class="mt-3">
<span id="previewStatusBadge" class="badge">{% if objective %}{% for val, label in objective_statuses %}{% if val == objective.status %}{{ label }}{% endif %}{% endfor %}{% else %}{{ _('Active') }}{% endif %}</span>
<span id="previewPriorityBadge" class="badge">{% if objective %}{{ {'high':_('High'),'medium':_('Medium'),'low':_('Low')}.get(objective.priority,objective.priority) }}{% else %}{{ _('Medium') }}{% endif %}</span>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-chart-line"></i> {{ _('KPI Progression') }}</h3></div>
<div class="card-body">
<svg id="progressChart" viewBox="0 0 400 200" style="width:100%;height:auto;" xmlns="http://www.w3.org/2000/svg"></svg>
{% if edit_mode and objective.kpi_target %}
<div class="mt-2 text-center" id="predictionInfo"></div>
{% endif %}
</div>
</div>
{% if edit_mode %}
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-database"></i> {{ _('KPI Measurements') }}</h3></div>
<div class="card-body">
<form method="POST" action="{{ url_for('objectives.add_entry', objective_id=objective.id) }}" class="form-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group" style="flex:1;">
<input type="number" step="0.01" name="value" class="form-input" placeholder="{{ _('Value') }}" required>
</div>
<div class="form-group" style="flex:2;">
<input type="text" name="note" class="form-input" placeholder="{{ _('Note (optional)') }}">
</div>
<button type="submit" class="btn btn-sm btn-primary"><i class="fas fa-plus"></i> {{ _('Record') }}</button>
</form>
{% if entries %}
<table class="table mt-2">
<thead><tr><th>{{ _('Date') }}</th><th>{{ _('Value') }}</th><th>{{ _('Note') }}</th></tr></thead>
<tbody>
{% for e in entries %}
<tr><td>{{ e.recorded_at.strftime('%Y-%m-%d %H:%M') }}</td><td><strong>{{ e.value }}</strong></td><td class="text-muted small">{{ e.note or '' }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-comments"></i> {{ _('Comments') }}</h3></div>
<div class="card-body">
<form method="POST" action="{{ url_for('objectives.add_comment', objective_id=objective.id) }}" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<textarea name="content" rows="2" class="form-textarea" placeholder="{{ _('Add a comment...') }}" required></textarea>
</div>
<button type="submit" class="btn btn-sm btn-primary"><i class="fas fa-paper-plane"></i> {{ _('Comment') }}</button>
</form>
{% if comments %}
<div class="comments-list mt-2">
{% for c in comments %}
<div class="comment-item">
<strong>{{ c.coach.username }}</strong> <span class="text-muted small">{{ c.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
<p>{{ c.content }}</p>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% if personal_notes or team_notes %}
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-sticky-note"></i> {{ _('Related Notes') }}</h3></div>
<div class="card-body">
{% if personal_notes %}
<h5>{{ _('Player Notes') }}</h5>
{% for n in personal_notes %}
<div class="comment-item">
<strong>{{ n.coach.username }}</strong> <span class="text-muted small">{{ n.created_at.strftime('%Y-%m-%d') }}</span>
<p>{{ n.content[:200] }}</p>
</div>
{% endfor %}
{% endif %}
{% if team_notes %}
<h5 class="mt-2">{{ _('Team Notes') }}</h5>
{% for n in team_notes %}
<div class="comment-item">
<strong>{{ n.coach.username }}</strong> <span class="text-muted small">{{ n.created_at.strftime('%Y-%m-%d') }}</span>
<p>{{ n.content[:200] }}</p>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
{% if templates %}
<div class="card">
<div class="card-header"><h3><i class="fas fa-copy"></i> {{ _('Templates') }}</h3></div>
<div class="card-body" id="templatesList">
{% for t in templates %}
<div class="template-item">
<h5>{{ t.title }}</h5>
<p class="text-muted small">{{ t.description[:120] if t.description else '' }}</p>
<button type="button" class="btn btn-sm btn-outline-primary use-template-btn" onclick="applyTemplate({{ t.id }})"><i class="fas fa-arrow-right"></i> {{ _('Use this template') }}</button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
var templateDataUrl = '{{ url_for("objectives.objective_data", objective_id=0) }}';
var templates = {};
{% for t in templates %}
(function(id) {
fetch(templateDataUrl.replace('/0/', '/' + id + '/')).then(function(r) { return r.json(); }).then(function(d) { templates[id] = d; });
})({{ t.id }});
{% endfor %}
function toggleAssignment() {
var t = document.getElementById('obj_type').value;
var isTmpl = document.getElementById('obj_is_template').checked;
var pf = document.getElementById('playerField');
var tf = document.getElementById('teamField');
if (isTmpl) { pf.style.display = 'none'; tf.style.display = 'none'; return; }
pf.style.display = t === 'team' ? 'none' : '';
tf.style.display = t === 'individual' ? 'none' : '';
}
function applyTemplate(id) {
var t = templates[id]; if (!t) return;
document.getElementById('obj_title').value = t.title || '';
document.getElementById('obj_description').value = t.description || '';
document.getElementById('obj_type').value = t.type || 'individual';
document.getElementById('obj_status').value = t.status || 'active';
document.getElementById('obj_priority').value = t.priority || 'medium';
document.getElementById('obj_kpi_metric').value = t.kpi_metric || '';
document.getElementById('obj_kpi_target').value = t.kpi_target || '';
document.getElementById('obj_kpi_unit').value = t.kpi_unit || '';
document.getElementById('obj_start_date').value = t.start_date || '';
document.getElementById('obj_due_date').value = '';
document.getElementById('obj_check_frequency').value = t.check_frequency || '';
document.getElementById('obj_category').value = t.category || '';
document.getElementById('obj_tags').value = t.tags || '';
document.getElementById('obj_progress').value = 0;
document.getElementById('progressVal').textContent = '0';
toggleAssignment(); updatePreview();
}
function updatePreview() {
var title = document.getElementById('obj_title').value || '';
var desc = document.getElementById('obj_description').value || '';
var status = document.getElementById('obj_status').value;
var priority = document.getElementById('obj_priority').value;
var kpiMetric = document.getElementById('obj_kpi_metric').value;
var kpiTarget = document.getElementById('obj_kpi_target').value;
var kpiUnit = document.getElementById('obj_kpi_unit').value;
var deadline = document.getElementById('obj_due_date').value;
var progress = document.getElementById('obj_progress').value;
var sel = document.getElementById('obj_player_id');
var playerName = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : '-';
document.getElementById('previewTitle').textContent = title || '{{ _('Objective title') | e }}';
document.getElementById('previewDesc').textContent = desc;
document.getElementById('previewAssignee').textContent = playerName;
document.getElementById('previewKpi').textContent = kpiMetric ? kpiMetric + ': ' + kpiTarget + (kpiUnit || '') : '-';
document.getElementById('previewDeadline').textContent = deadline;
document.getElementById('previewBar').style.width = progress + '%';
document.getElementById('previewProgress').textContent = progress;
var sl = {}, sc = {'active':'badge-success','pending':'badge-warning','completed':'badge-info','overdue':'badge-danger'};
var pl = {}, pc = {'high':'badge-danger','medium':'badge-warning','low':'badge-secondary'};
{% for val, label in objective_statuses %}sl['{{ val }}'] = '{{ label | e }}';{% endfor %}
{% for val, label in objective_priorities %}pl['{{ val }}'] = '{{ label | e }}';{% endfor %}
document.getElementById('previewStatusBadge').textContent = sl[status] || status;
document.getElementById('previewStatusBadge').className = 'badge ' + (sc[status] || 'badge-secondary');
document.getElementById('previewPriorityBadge').textContent = pl[priority] || priority;
document.getElementById('previewPriorityBadge').className = 'badge ' + (pc[priority] || 'badge-secondary');
drawChart(progress, kpiTarget);
}
function drawChart(progress, target) {
var svg = document.getElementById('progressChart');
var w = 400, h = 200, pt = 20, pr = 20, pb = 30, pl = 40;
var cw = w - pl - pr, ch = h - pt - pb;
var prog = Math.min(parseInt(progress) || 0, 100);
var py = pt + ch * (1 - prog / 100);
var tgt = parseFloat(target) || 100;
var tgtScale = Math.max(tgt, prog);
var tgtNorm = tgt / (tgtScale);
var ty = pt + ch * (1 - tgtNorm);
var html = '<rect x="0" y="0" width="' + w + '" height="' + h + '" fill="transparent"/>';
html += '<line x1="' + pl + '" y1="' + (pt + ch) + '" x2="' + (pl + cw) + '" y2="' + (pt + ch) + '" stroke="#ddd" stroke-width="1"/>';
html += '<line x1="' + pl + '" y1="' + pt + '" x2="' + pl + '" y2="' + (pt + ch) + '" stroke="#ddd" stroke-width="1"/>';
html += '<text x="' + (pl - 30) + '" y="' + (pt + 5) + '" fill="#999" font-size="10">' + tgtScale + '</text>';
html += '<text x="' + (pl - 30) + '" y="' + (pt + ch / 2 + 4) + '" fill="#999" font-size="10">' + (tgtScale / 2).toFixed(1) + '</text>';
html += '<text x="' + (pl - 30) + '" y="' + (pt + ch + 4) + '" fill="#999" font-size="10">0</text>';
html += '<text x="' + pl + '" y="' + (pt + ch + 18) + '" fill="#999" font-size="10">{{ _('Now') | e }}</text>';
html += '<text x="' + (pl + cw - 50) + '" y="' + (pt + ch + 18) + '" fill="#999" font-size="10">{{ _('Deadline') | e }}</text>';
html += '<line x1="' + pl + '" y1="' + ty + '" x2="' + (pl + cw) + '" y2="' + ty + '" stroke="#4CAF50" stroke-width="1.5" stroke-dasharray="4,3"/>';
html += '<text x="' + (pl + cw + 4) + '" y="' + (ty + 4) + '" fill="#4CAF50" font-size="9">{{ _('Target') | e }}</text>';
html += '<polygon points="' + pl + ',' + (pt + ch) + ' ' + pl + ',' + py + ' ' + (pl + (prog / 100) * cw) + ',' + py + ' ' + (pl + (prog / 100) * cw) + ',' + (pt + ch) + '" fill="rgba(59,130,246,0.15)"/>';
html += '<line x1="' + pl + '" y1="' + py + '" x2="' + (pl + (prog / 100) * cw) + '" y2="' + py + '" stroke="#3B82F6" stroke-width="2"/>';
html += '<circle cx="' + (pl + (prog / 100) * cw) + '" cy="' + py + '" r="4" fill="#3B82F6"/>';
html += '<text x="' + (pl + (prog / 100) * cw - 12) + '" y="' + (py - 8) + '" fill="#3B82F6" font-size="10" font-weight="bold">' + prog + '%</text>';
svg.innerHTML = html;
}
document.addEventListener('DOMContentLoaded', function() { toggleAssignment(); updatePreview(); });
</script>
{% endblock %}
+125
View File
@@ -0,0 +1,125 @@
{% extends "layouts/base.html" %}
{% block title %}{{ objective.title }} - UdeS team manager{% endblock %}
{% block page_title %}{{ objective.title }}{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">{{ _('Home') }} / <a href="{{ url_for('objectives.list_objectives') }}">{{ _('Objectives') }}</a> / {{ objective.title }}</span>{% endblock %}
{% block content %}
<div class="row">
<div class="col-8">
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-bullseye"></i> {{ objective.title }}</h3></div>
<div class="card-body">
<p>{{ objective.description or '' }}</p>
<div class="preview-stats">
<div class="preview-stat"><span class="preview-label">{{ _('Type') }}</span><span class="preview-value">{% for val, label in objective_types %}{% if val == objective.type %}{{ label }}{% endif %}{% endfor %}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('Assigned to') }}</span><span class="preview-value">{% if objective.player %}{{ objective.player.username }}{% elif objective.team %}{{ objective.team.name }}{% else %}-{% endif %}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('Category') }}</span><span class="preview-value">{{ objective.category or '-' }}</span></div>
</div>
<div class="mt-3">
<span id="statusBadge" class="badge">{% for val, label in objective_statuses %}{% if val == objective.status %}{{ label }}{% endif %}{% endfor %}</span>
<span id="priorityBadge" class="badge">{% set pl = {'high':_('High'),'medium':_('Medium'),'low':_('Low')} %}{{ pl.get(objective.priority,objective.priority) }}</span>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-chart-line"></i> {{ _('KPI Progression') }}</h3></div>
<div class="card-body">
<svg id="progressChart" viewBox="0 0 400 200" style="width:100%;height:auto;" xmlns="http://www.w3.org/2000/svg"></svg>
{% if objective.kpi_target %}<div class="mt-2 text-center" id="predictionInfo"></div>{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-database"></i> {{ _('KPI Measurements') }}</h3></div>
<div class="card-body">
<form method="POST" action="{{ url_for('objectives.add_entry', objective_id=objective.id) }}" class="form-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group" style="flex:1;"><input type="number" step="0.01" name="value" class="form-input" placeholder="{{ _('Value (optional)') }}"></div>
<div class="form-group" style="flex:2;"><input type="text" name="note" class="form-input" placeholder="{{ _('Note or comment') }}"></div>
<button type="submit" class="btn btn-sm btn-primary"><i class="fas fa-plus"></i> {{ _('Record') }}</button>
</form>
{% if entries %}<table class="table mt-2"><thead><tr><th>{{ _('Date') }}</th><th>{{ _('Value') }}</th><th>{{ _('Note') }}</th></tr></thead><tbody>
{% for e in entries %}<tr><td>{{ e.recorded_at.strftime('%Y-%m-%d %H:%M') }}</td><td><strong>{% if e.value is not none %}{{ e.value }}{% else %}-{% endif %}</strong></td><td class="text-muted small">{{ e.note or '' }}</td></tr>{% endfor %}</tbody></table>{% else %}<p class="text-muted mt-2">{{ _('No measurements recorded yet.') }}</p>{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-comments"></i> {{ _('Comments') }}</h3></div>
<div class="card-body">{% if comments %}<div class="comments-list">{% for c in comments %}<div class="comment-item"><strong>{{ c.coach.username }}</strong> <span class="text-muted small">{{ c.created_at.strftime('%Y-%m-%d %H:%M') }}</span><p>{{ c.content }}</p></div>{% endfor %}</div>{% else %}<p class="text-muted">{{ _('No comments yet.') }}</p>{% endif %}</div>
</div>
</div>
<div class="col-4">
<div class="card mb-4">
<div class="card-header"><h3><i class="fas fa-info-circle"></i> {{ _('Details') }}</h3></div>
<div class="card-body">
<div class="preview-stats">
<div class="preview-stat"><span class="preview-label">{{ _('Priority') }}</span><span class="preview-value">{% set pl = {'high':_('High'),'medium':_('Medium'),'low':_('Low')} %}{{ pl.get(objective.priority,objective.priority) }}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('Status') }}</span><span class="preview-value">{% for val, label in objective_statuses %}{% if val == objective.status %}{{ label }}{% endif %}{% endfor %}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('Start date') }}</span><span class="preview-value">{{ objective.start_date.strftime('%Y-%m-%d') if objective.start_date else '-' }}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('Deadline') }}</span><span class="preview-value">{{ objective.due_date.strftime('%Y-%m-%d') if objective.due_date else '-' }}</span></div>
<div class="preview-stat"><span class="preview-label">{{ _('Progress') }}</span><span class="preview-value">{{ objective.progress }}%</span></div>
</div>
{% if objective.kpi_target %}<div class="progress-section mt-2"><span class="preview-label">{{ _('KPI Target') }}</span><strong>{{ objective.kpi_target }}{% if objective.kpi_unit %}{{ objective.kpi_unit }}{% endif %}</strong></div>{% endif %}
{% if can_edit %}<a href="{{ url_for('objectives.edit_objective', objective_id=objective.id) }}" class="btn btn-sm btn-primary mt-3"><i class="fas fa-edit"></i> {{ _('Edit') }}</a>{% endif %}
</div>
</div>
{% if personal_notes or team_notes %}<div class="card mb-4"><div class="card-header"><h3><i class="fas fa-sticky-note"></i> {{ _('Related Notes') }}</h3></div><div class="card-body">{% if personal_notes %}<h5>{{ _('Personal Notes') }}</h5>{% for n in personal_notes %}<div class="comment-item"><strong>{{ n.coach.username }}</strong> <span class="text-muted small">{{ n.created_at.strftime('%Y-%m-%d') }}</span><p>{{ n.content[:200] }}</p></div>{% endfor %}{% endif %}{% if team_notes %}<h5 class="mt-2">{{ _('Team Notes') }}</h5>{% for n in team_notes %}<div class="comment-item"><strong>{{ n.coach.username }}</strong> <span class="text-muted small">{{ n.created_at.strftime('%Y-%m-%d') }}</span><p>{{ n.content[:200] }}</p></div>{% endfor %}{% endif %}</div></div>{% endif %}
</div>
</div>{% endblock %}
{% block scripts %}
<script>
function drawChart(progress, target) {
var svg = document.getElementById('progressChart');
if (!svg) return;
var w = 400, h = 200, pt = 20, pr = 20, pb = 30, pl = 40;
var cw = w - pl - pr, ch = h - pt - pb;
var prog = Math.min(parseInt(progress) || 0, 100);
var py = pt + ch * (1 - prog / 100);
var tgt = parseFloat(target) || 100;
var tgtScale = Math.max(tgt, prog);
var ty = pt + ch * (1 - tgt / tgtScale);
var html = '<rect x="0" y="0" width="' + w + '" height="' + h + '" fill="transparent"/>';
html += '<line x1="' + pl + '" y1="' + (pt + ch) + '" x2="' + (pl + cw) + '" y2="' + (pt + ch) + '" stroke="#ddd" stroke-width="1"/>';
html += '<line x1="' + pl + '" y1="' + pt + '" x2="' + pl + '" y2="' + (pt + ch) + '" stroke="#ddd" stroke-width="1"/>';
html += '<text x="' + (pl - 30) + '" y="' + (pt + 5) + '" fill="#999" font-size="10">' + tgtScale + '</text>';
html += '<text x="' + (pl - 30) + '" y="' + (pt + ch / 2 + 4) + '" fill="#999" font-size="10">' + (tgtScale / 2).toFixed(1) + '</text>';
html += '<text x="' + (pl - 30) + '" y="' + (pt + ch + 4) + '" fill="#999" font-size="10">0</text>';
html += '<line x1="' + pl + '" y1="' + ty + '" x2="' + (pl + cw) + '" y2="' + ty + '" stroke="#4CAF50" stroke-width="1.5" stroke-dasharray="4,3"/>';
html += '<text x="' + (pl + cw + 4) + '" y="' + (ty + 4) + '" fill="#4CAF50" font-size="9">{{ _('Target') | e }}</text>';
html += '<polygon points="' + pl + ',' + (pt + ch) + ' ' + pl + ',' + py + ' ' + (pl + (prog / 100) * cw) + ',' + py + ' ' + (pl + (prog / 100) * cw) + ',' + (pt + ch) + '" fill="rgba(59,130,246,0.15)"/>';
html += '<line x1="' + pl + '" y1="' + py + '" x2="' + (pl + (prog / 100) * cw) + '" y2="' + py + '" stroke="#3B82F6" stroke-width="2"/>';
html += '<circle cx="' + (pl + (prog / 100) * cw) + '" cy="' + py + '" r="4" fill="#3B82F6"/>';
svg.innerHTML = html;
fetchTrendAndEntries();
}
function fetchTrendAndEntries() {
var trendUrl = '{{ url_for("objectives.trend_data", objective_id=objective.id) }}';
var entriesUrl = '{{ url_for("objectives.list_entries", objective_id=objective.id) }}';
Promise.all([fetch(trendUrl).then(function(r){return r.json()}), fetch(entriesUrl).then(function(r){return r.json()})]).then(function(results) {
var trend = results[0], entries = results[1];
var infoEl = document.getElementById('predictionInfo');
if (trend.predicted_achievement_date) {
infoEl.innerHTML = '<strong>{{ _("Predicted achievement") }}:</strong> ' + new Date(trend.predicted_achievement_date).toLocaleDateString() + ' - <span class=\"' + (trend.confidence > 0.6 ? 'confidence-high' : trend.confidence > 0.3 ? 'confidence-medium' : 'confidence-low') + '\">{{ _("Confidence") }}: ' + (trend.confidence * 100).toFixed(0) + '%</span>';
}
if (entries && entries.length > 1) {
var svg = document.getElementById('progressChart');
var html = svg.innerHTML;
var w = 400, pl = 40, pt = 20, cw = 320, ch = 150;
var entryValues = entries.map(function(e){return parseFloat(e.value)||0;});
var maxVal = Math.max.apply(null, entryValues);
var range = maxVal || 1;
var stepX = cw / (entries.length - 1);
entryValues.forEach(function(v, i) {
var ex = pl + i * stepX;
var ey = pt + ch * (1 - v / range);
html += '<circle cx="' + ex + '" cy="' + ey + '" r="3" fill="#8B5CF6"/>';
if (i > 0) {
var pex = pl + (i - 1) * stepX;
var pey = pt + ch * (1 - entryValues[i-1] / range);
html += '<line x1="' + pex + '" y1="' + pey + '" x2="' + ex + '" y2="' + ey + '" stroke="#8B5CF6" stroke-width="1.5"/>';
}
});
svg.innerHTML = html;
}
}).catch(function(){});
}
document.addEventListener('DOMContentLoaded', function() { drawChart({{ objective.progress }}, '{{ objective.kpi_target if objective.kpi_target else '' }}'); });
</script>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
{% extends "layouts/base.html" %}
{% import 'layouts/_pagination.html' as pager %}
{% block title %}{{ _('Objectives') }} - UdeS team manager{% endblock %}
{% block page_title %}{{ _('Objective Management') }}{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">{{ _('Home') }} / {{ _('Objectives') }}</span>{% endblock %}
{% block content %}
<div class="stats-grid mb-4">
<div class="stat-card"><div class="stat-icon bg-primary"><i class="fas fa-flag-checkered"></i></div><div class="stat-info"><h3>{{ count_total }}</h3><p>{{ _('Total') }}</p></div></div>
<div class="stat-card"><div class="stat-icon bg-info"><i class="fas fa-chart-line"></i></div><div class="stat-info"><h3>{{ count_kpi }}</h3><p>{{ _('Custom KPIs') }}</p></div></div>
<div class="stat-card"><div class="stat-icon bg-secondary"><i class="fas fa-copy"></i></div><div class="stat-info"><h3>{{ count_templates }}</h3><p>{{ _('Templates') }}</p></div></div>
<div class="stat-card"><div class="stat-icon bg-success"><i class="fas fa-percentage"></i></div><div class="stat-info"><h3>{{ success_rate }}%</h3><p>{{ _('Success rate') }}</p></div></div>
</div>
<div class="card mb-4"><div class="card-body"><div class="objectives-toolbar">
<nav class="objectives-tabs" role="tablist">
<a href="{{ url_for('objectives.list_objectives', tab='all') }}" class="tab-link {% if tab == 'all' %}active{% endif %}">{{ _('All') }} ({{ count_total }})</a>
<a href="{{ url_for('objectives.list_objectives', tab='kpi') }}" class="tab-link {% if tab == 'kpi' %}active{% endif %}">{{ _('Custom KPIs') }} ({{ count_kpi }})</a>
<a href="{{ url_for('objectives.list_objectives', tab='modeles') }}" class="tab-link {% if tab in ('modeles','templates') %}active{% endif %}">{{ _('Templates') }} ({{ count_templates }})</a>
</nav>
<form method="GET" action="{{ url_for('objectives.list_objectives') }}" class="objectives-search-bar">
<input type="hidden" name="tab" value="{{ tab }}">
<div class="form-inline">
<div class="form-group" style="flex:1;min-width:200px;"><input type="text" name="search" class="form-input" placeholder="{{ _('Search an objective...') }}"></div>
<div class="form-group"><select name="status" class="form-select"><option value="">{{ _('Status') }}</option>{% for val, label in objective_statuses %}<option value="{{ val }}"{% if f_status == val %} selected{% endif %}>{{ label }}</option>{% endfor %}</select></div>
<div class="form-group"><select name="priority" class="form-select"><option value="">{{ _('Priority') }}</option>{% for val, label in objective_priorities %}<option value="{{ val }}"{% if f_priority == val %} selected{% endif %}>{{ label }}</option>{% endfor %}</select></div>
<div class="form-group"><select name="type" class="form-select"><option value="">{{ _('Type') }}</option>{% for val, label in objective_types %}<option value="{{ val }}"{% if f_type == val %} selected{% endif %}>{{ label }}</option>{% endfor %}</select></div>
<div class="form-group"><select name="team" class="form-select"><option value="">{{ _('Team') }}</option>{% for t in teams %}<option value="{{ t.id }}"{% if f_team == t.id %} selected{% endif %}>{{ t.name }}</option>{% endfor %}</select></div>
<button type="submit" class="btn btn-sm btn-primary"><i class="fas fa-filter"></i> {{ _('Filters') }}</button>
</div></form>
</div></div></div>
<div class="card"><div class="card-header"><h3><i class="fas fa-bullseye"></i> {{ _('Objectives') }}</h3>{% if can_edit %}<a href="{{ url_for('objectives.new_objective') }}" class="btn btn-sm btn-primary"><i class="fas fa-plus"></i> {{ _('New Objective') }}</a>{% endif %}</div>
<div class="card-body"><div class="table-container"><table class="table objectives-table"><thead><tr>
<th>{{ _('Objective') }}</th><th>{{ _('Assigned to') }}</th><th>{{ _('Team') }}</th><th>{{ _('Priority') }}</th><th>{{ _('Status') }}</th><th>{{ _('Target KPI') }}</th><th>{{ _('Progress') }}</th><th>{{ _('Deadline') }}</th>{% if can_edit %}<th>{{ _('Actions') }}</th>{% endif %}
</tr></thead><tbody>
{% for obj in objectives.items %}<tr data-objective-id="{{ obj.id }}">
<td><strong>{{ obj.title }}</strong>{% if obj.tags %}<div class="objective-tags">{% for tag in obj.tags.split(',') %}<span class="badge badge-light">{{ tag.strip() }}</span>{% endfor %}</div>{% endif %}{% if obj.kpi_metric %}<small class="text-muted d-block">{{ obj.kpi_metric }}</small>{% endif %}</td>
<td>{% if obj.player %}<span class="user-avatar avatar-sm">{{ obj.player.username[:2]|upper }}</span> {{ obj.player.username }}{% else %}<span class="text-muted">-</span>{% endif %}</td>
<td>{% if obj.team %}{{ obj.team.name }}{% else %}<span class="text-muted">-</span>{% endif %}</td>
<td>{% set pc = {'high':'badge-danger','medium':'badge-warning','low':'badge-secondary'} %}<span class="badge {{ pc.get(obj.priority,'badge-secondary') }}">{{ {'high':_('High'),'medium':_('Medium'),'low':_('Low')}.get(obj.priority,obj.priority) }}</span></td>
<td>{% set sc = {'active':'badge-success','pending':'badge-warning','completed':'badge-info','overdue':'badge-danger'} %}<span class="badge {{ sc.get(obj.status,'badge-secondary') }}">{% for val, label in objective_statuses %}{% if val == obj.status %}{{ label }}{% endif %}{% endfor %}</span></td>
<td>{% if obj.kpi_target is not none %}{{ obj.kpi_target }}{% if obj.kpi_unit %}{{ obj.kpi_unit }}{% endif %}{% else %}<span class="text-muted">-</span>{% endif %}</td>
<td><div class="progress-bar-container"><div class="progress-bar" style="width:{{ obj.progress }}%"></div></div><small>{{ obj.progress }}%</small></td>
<td>{% if obj.is_template %}-{% elif obj.due_date %}{{ obj.due_date.strftime('%Y-%m-%d') }}{% endif %}</td>
{% if can_edit %}<td class="actions-cell"><a href="{{ url_for('objectives.edit_objective', objective_id=obj.id) }}" class="btn btn-sm btn-outline-primary" title="{{ _('Edit') }}"><i class="fas fa-edit"></i></a> <a href="{{ url_for('objectives.view_objective', objective_id=obj.id) }}" class="btn btn-sm btn-outline-info" title="{{ _('View') }}"><i class="fas fa-eye"></i></a> <button class="btn btn-sm btn-outline-danger delete-obj-btn" data-obj-id="{{ obj.id }}" data-obj-title="{{ obj.title }}" title="{{ _('Delete') }}"><i class="fas fa-trash"></i></button></td>{% elif current_user.role == 'player' %}<td><a href="{{ url_for('objectives.view_objective', objective_id=obj.id) }}" class="btn btn-sm btn-outline-info"><i class="fas fa-eye"></i> {{ _('View') }}</a></td>{% endif %}
</tr>{% else %}
<tr><td colspan="{{ 9 if can_edit else 8 }}" class="text-center"><div class="empty-state"><i class="fas fa-bullseye"></i><h3>{{ _('No objectives yet') }}</h3><p>{{ _('Create your first objective to start tracking player evolution.') }}</p></div></td></tr>{% endfor %}
</tbody></table></div>{{ pager.controls(pagination) }}</div></div>
<div id="deleteModal" class="modal hidden"><div class="modal-backdrop" onclick="hideDeleteModal();"></div><div class="modal-content"><div class="modal-header"><h3><i class="fas fa-trash-alt"></i> {{ _('Delete Objective') }}</h3><button class="modal-close" onclick="hideDeleteModal();">&times;</button></div><div class="modal-body"><p>{{ _('Are you sure you want to delete this objective?') }}</p><p><strong id="deleteObjTitle"></strong></p><form id="deleteForm" method="POST" action=""><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><div class="form-actions"><button type="button" class="btn btn-secondary" onclick="hideDeleteModal();">{{ _('Cancel') }}</button><button type="submit" class="btn btn-danger"><i class="fas fa-trash"></i> {{ _('Delete') }}</button></div></form></div></div></div>
{% endblock %}
{% block scripts %}<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.delete-obj-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var id = this.getAttribute('data-obj-id');
var title = this.getAttribute('data-obj-title');
document.getElementById('deleteObjTitle').textContent = title;
document.getElementById('deleteForm').action = '{{ url_for("objectives.delete_objective", objective_id=0) }}'.replace('/0/', '/' + id + '/');
document.getElementById('deleteModal').classList.remove('hidden');
});
});
document.getElementById('deleteModal').querySelector('.modal-backdrop').addEventListener('click', hideDeleteModal);
});
function hideDeleteModal(){document.getElementById('deleteModal').classList.add('hidden');}
document.addEventListener('keydown',function(e){if(e.key==='Escape'){hideDeleteModal();}});
</script>{% endblock %}