Ajout milestone

This commit is contained in:
2026-05-26 21:26:32 +02:00
parent 0dc81c7c80
commit 15049c4fe3
11 changed files with 911 additions and 2 deletions
@@ -0,0 +1,233 @@
<!-- suppress HtmlUnknownAttribute -->
<!-- Top bar -->
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="cancelCreation()">
← Milestones
</button>
<div class="d-flex align-items-center gap-2">
@if (!isNewRoute) {
<div class="more-wrapper">
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="toggleMoreMenu()">···</button>
@if (moreMenuOpen) {
<div class="more-menu dropdown-menu show">
<button type="button" class="dropdown-item text-danger" (click)="deleteMilestone()">Supprimer</button>
</div>
}
@if (moreMenuOpen) {
<div class="more-backdrop" (click)="closeMoreMenu()"></div>
}
</div>
}
</div>
</div>
<!-- Titre -->
<div class="card shadow-sm mb-3">
<div class="card-body py-2">
<input
#nameInput="ngModel"
aria-label="Nom du milestone"
class="title-input form-control border-0 shadow-none p-0 fw-semibold fs-5"
[class.is-invalid]="nameInput.invalid && nameInput.touched"
type="text"
placeholder="Nom du milestone..."
required
[(ngModel)]="milestone.name"
(blur)="saveMilestone()"
/>
@if (nameInput.invalid && nameInput.touched) {
<div class="text-danger small mt-1">Le nom est obligatoire.</div>
}
</div>
</div>
<!-- Infos -->
<div class="card shadow-sm mb-3">
<div class="card-header section-header">Informations</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="field-label">Date d'échéance</label>
<input
aria-label="Date d'échéance"
class="form-control form-control-sm"
type="date"
[(ngModel)]="milestone.dueDate"
(blur)="saveMilestone()"
/>
</div>
@if (!isNewRoute) {
<div class="col-md-8">
<label class="field-label">Progression</label>
<div class="d-flex align-items-center gap-2 mt-1">
<div class="progress flex-grow-1" style="height: 8px;">
<div
class="progress-bar"
role="progressbar"
[style.width.%]="progress"
[attr.aria-valuenow]="progress"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
<span class="text-secondary small" style="min-width: 2.5rem; text-align: right;">{{ progress }}%</span>
</div>
</div>
}
</div>
</div>
</div>
<!-- Description -->
<div class="card shadow-sm mb-3">
<div class="card-header section-header">Description</div>
<div class="card-body">
@if (editingDescription) {
<textarea
aria-label="Description"
class="form-control border-0 shadow-none p-0 description-textarea"
placeholder="Ajouter une description... (Markdown supporté, coller une image avec Ctrl+V)"
[(ngModel)]="milestone.description"
(blur)="editingDescription = false; saveMilestone()"
(paste)="onDescriptionPaste($event)"
></textarea>
} @else {
<div
class="description-preview"
(click)="editingDescription = true"
title="Cliquer pour éditer"
>
@if (milestone.description) {
<div class="markdown-body" [innerHTML]="descriptionHtml"></div>
} @else {
<span class="description-placeholder">Ajouter une description...</span>
}
</div>
}
</div>
</div>
<!-- Issues liées -->
<div class="card shadow-sm mb-3">
<div class="card-header section-header d-flex align-items-center justify-content-between">
<span>Issues liées</span>
<span class="badge bg-secondary rounded-pill">{{ milestone.issueIds.length }}</span>
</div>
@if (linkedIssues.length > 0) {
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">#</th>
<th>Type</th>
<th>Titre</th>
<th>Priorité</th>
<th>Statut</th>
<th></th>
</tr>
</thead>
<tbody>
@for (issue of linkedIssues; track issue.id) {
<tr>
<td class="ps-3 text-secondary small">#{{ issue.id }}</td>
<td>
<span class="type-icon" [style.background]="typeIcon(issue.type).bg" [title]="issue.type">
{{ typeIcon(issue.type).letter }}
</span>
</td>
<td>
<button type="button" class="issue-name-btn" (click)="navigateToIssue(issue.id)">
{{ issue.name }}
</button>
</td>
<td>
<span
[style.color]="priorityDisplay(issue.priority).color"
style="font-weight: 700; font-size: 1rem; letter-spacing: -1px;"
[title]="issue.priority"
>{{ priorityDisplay(issue.priority).symbol }}</span>
</td>
<td>
<span
class="status-badge"
[style.background]="statusBadge(issue.status).bg"
[style.color]="statusBadge(issue.status).color"
>{{ statusBadge(issue.status).label }}</span>
</td>
<td class="text-end pe-3">
<button
type="button"
class="dep-remove"
aria-label="Retirer"
(click)="removeIssue(issue.id)"
>×</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
<div class="card-body" [class.pt-0]="linkedIssues.length > 0">
@if (showAddIssue) {
<div class="issue-search-wrapper">
<div class="input-group input-group-sm">
<input
type="text"
class="form-control"
placeholder="Rechercher par nom ou #id..."
[(ngModel)]="issueSearchQuery"
(focus)="showIssueSuggestions = true"
(blur)="hideIssueSuggestions()"
autofocus
/>
<button
type="button"
class="btn btn-outline-secondary"
(click)="cancelAddIssue()"
aria-label="Annuler"
>×</button>
</div>
@if (showIssueSuggestions && issueSuggestions.length > 0) {
<ul class="issue-suggestions">
@for (issue of issueSuggestions; track issue.id) {
<li>
<button type="button" class="issue-suggestion-item" (mousedown)="addIssueFromSearch(issue.id)">
<span class="text-secondary small me-1">#{{ issue.id }}</span>
<span class="type-icon me-1" [style.background]="typeIcon(issue.type).bg">{{ typeIcon(issue.type).letter }}</span>
{{ issue.name }}
</button>
</li>
}
</ul>
}
@if (showIssueSuggestions && issueSearchQuery && issueSuggestions.length === 0) {
<div class="issue-suggestions text-secondary small p-2">Aucune issue trouvée.</div>
}
</div>
} @else {
<button
type="button"
class="btn btn-sm btn-outline-secondary"
[disabled]="availableIssues.length === 0"
(click)="openAddIssue()"
>+ Ajouter une issue</button>
}
</div>
</div>
<!-- Boutons de création -->
@if (isNewRoute) {
<div class="d-flex gap-2">
<button
type="button"
class="btn btn-primary"
[disabled]="!milestone.name.trim()"
(click)="saveMilestone(true)"
>Créer</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancelCreation()">Annuler</button>
</div>
}