Files
Bonsai-webapp/src/app/milestones/milestone-detail/milestone-detail.html
T
2026-05-30 13:43:38 +02:00

358 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 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 de début</label>
<input
aria-label="Date de début"
class="form-control form-control-sm"
type="date"
[(ngModel)]="milestone.startDate"
(blur)="saveMilestone()"
/>
</div>
<div class="col-md-4">
<label class="field-label">Date de fin</label>
<input
aria-label="Date de fin"
class="form-control form-control-sm"
type="date"
[(ngModel)]="milestone.endDate"
(blur)="saveMilestone()"
/>
</div>
<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 class="col-md-4">
<label class="field-label">Temps estimé total (h)</label>
<div class="form-control form-control-sm bg-body-secondary text-secondary">{{ totalEstimatedTime !== null ? totalEstimatedTime : '—' }}</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>
<!-- Dépendances -->
@if (!isNewRoute) {
<div class="card shadow-sm mb-3">
<div class="card-header section-header">Dépendances</div>
<div class="card-body">
@if (hasDependencies) {
<div class="d-flex flex-wrap gap-2 mb-3">
@for (depId of dependencyIds; track depId) {
<span class="dep-badge">
<span class="dep-id">#{{ depId }}</span>
{{ resolveDependency(depId)?.name || 'Sans nom' }}
<button type="button" class="dep-remove" (click)="removeDependency(depId)" title="Supprimer">×</button>
</span>
}
</div>
}
@if (showAddDependency) {
<div class="d-flex gap-2 flex-wrap">
<select aria-label="Choisir un milestone" class="form-select form-select-sm dep-select" [(ngModel)]="selectedCandidateMilestoneId">
<option [ngValue]="null">Choisir un milestone...</option>
@for (candidate of availableCandidates; track candidate.id) {
<option [ngValue]="candidate.id">#{{ candidate.id }} {{ candidate.name || 'Sans nom' }}</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary text-nowrap" (click)="confirmAddDependency()" [disabled]="selectedCandidateMilestoneId === null">Ajouter</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-nowrap" (click)="cancelAddDependency()">Annuler</button>
</div>
} @else {
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openAddDependency()">+ Ajouter une dépendance</button>
}
</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 displayedIssues; 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-2]="linkedIssues.length > 0">
@if (showCreateIssue) {
<div class="d-flex gap-2 align-items-center">
<div class="type-dropdown-wrapper">
<button type="button" class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-1" (click)="toggleTypeDropdown()">
<span class="type-icon" [style.background]="typeIcon(newIssueType).bg" [title]="newIssueType">{{ typeIcon(newIssueType).letter }}</span>
{{ newIssueType }}
<span class="dropdown-toggle ms-1"></span>
</button>
@if (showTypeDropdown) {
<div class="type-backdrop" (click)="closeTypeDropdown()"></div>
<ul class="type-dropdown dropdown-menu show">
@for (type of typeOptions; track type) {
<li>
<button type="button" class="dropdown-item d-flex align-items-center gap-2" [class.active]="newIssueType === type" (click)="selectNewIssueType(type)">
<span class="type-icon" [style.background]="typeIcon(type).bg">{{ typeIcon(type).letter }}</span>
{{ type }}
</button>
</li>
}
</ul>
}
</div>
<input
#newIssueInput
aria-label="Titre de la nouvelle issue"
class="form-control form-control-sm dep-select"
type="text"
placeholder="Titre de l'issue..."
[(ngModel)]="newIssueName"
(keydown.enter)="confirmCreateIssue()"
(keydown.escape)="cancelCreateIssue()"
/>
<button type="button" class="btn btn-sm btn-primary text-nowrap" (click)="confirmCreateIssue()" [disabled]="!newIssueName.trim()">Créer</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-nowrap" (click)="cancelCreateIssue()">Annuler</button>
</div>
} @else 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 {
<div class="d-flex gap-2">
<div class="btn-group create-issue-wrapper">
<button type="button" class="btn btn-sm btn-primary" (click)="openCreateIssue()">+ Créer une issue</button>
<button type="button" class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split" (click)="toggleCreateDropdown()">
<span class="visually-hidden">Choisir le type</span>
</button>
@if (showCreateDropdown) {
<div class="create-backdrop" (click)="closeCreateDropdown()"></div>
<ul class="create-type-dropdown dropdown-menu show">
@for (type of typeOptions; track type) {
<li>
<button type="button" class="dropdown-item d-flex align-items-center gap-2" (click)="openCreateIssue(type)">
<span class="type-icon" [style.background]="typeIcon(type).bg">{{ typeIcon(type).letter }}</span>
{{ type }}
</button>
</li>
}
</ul>
}
</div>
<button
type="button"
class="btn btn-sm btn-outline-secondary"
[disabled]="availableIssues.length === 0"
(click)="openAddIssue()"
>Ajouter une existante</button>
</div>
}
</div>
</div>
<!-- Diagramme Gantt -->
@if (!isNewRoute) {
<div class="card shadow-sm mb-3">
<div class="card-header section-header">Diagramme Gantt</div>
<div class="card-body">
<app-gantt-diagram [tasks]="milestoneGanttTasks" />
</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>
}