d359ffc66f
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
358 lines
13 KiB
HTML
358 lines
13 KiB
HTML
<!-- 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>
|
||
}
|