diff --git a/src/app/issues/issue-detail/issue-detail.css b/src/app/issues/issue-detail/issue-detail.css index 8f6cb2e..e37f0c0 100644 --- a/src/app/issues/issue-detail/issue-detail.css +++ b/src/app/issues/issue-detail/issue-detail.css @@ -9,6 +9,41 @@ gap: 1rem; } +.header-meta { + display: flex; + align-items: flex-start; + gap: 1rem; + flex-wrap: wrap; + justify-content: flex-end; +} + +.status-inline { + display: flex; + align-items: center; + gap: 0.5rem; + background: #ffffff; + border: 1px solid #dbe4f0; + border-radius: 0.75rem; + padding: 0.5rem 0.75rem; +} + +.status-label { + font-size: 0.875rem; + font-weight: 700; + color: #374151; +} + +.status-select { + min-width: 9rem; +} + +.header-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + justify-content: flex-end; +} + .page-header h1 { margin: 0; font-size: 2rem; @@ -33,6 +68,56 @@ background-color: #1d4ed8; } +.more-wrapper { + position: relative; +} + +.more-button { + border: 1px solid #d1d5db; + border-radius: 0.5rem; + background-color: #ffffff; + color: #374151; + padding: 0.65rem 1rem; + font-weight: 600; + cursor: pointer; +} + +.more-button:hover { + background-color: #f9fafb; +} + +.more-menu { + position: absolute; + right: 0; + top: calc(100% + 0.5rem); + min-width: 10rem; + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 0.75rem; + box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08); + padding: 0.25rem; + z-index: 10; +} + +.more-menu-item { + width: 100%; + border: none; + background: transparent; + padding: 0.65rem 0.85rem; + border-radius: 0.5rem; + text-align: left; + font-weight: 600; + cursor: pointer; +} + +.more-menu-item:hover { + background: #f3f4f6; +} + +.delete-action { + color: #b91c1c; +} + .save-button { border: none; border-radius: 0.5rem; @@ -61,6 +146,10 @@ background-color: #f3f4f6; } +.dependency-multiselect { + min-height: 8rem; +} + .detail-card { background-color: #ffffff; border: 1px solid #e5e7eb; @@ -119,6 +208,19 @@ tr:last-child td { flex-direction: column; } + .header-meta { + width: 100%; + justify-content: flex-start; + } + + .status-inline { + width: 100%; + } + + .status-select { + width: 100%; + } + th { width: 40%; } diff --git a/src/app/issues/issue-detail/issue-detail.html b/src/app/issues/issue-detail/issue-detail.html index 6b5ea43..9c0713a 100644 --- a/src/app/issues/issue-detail/issue-detail.html +++ b/src/app/issues/issue-detail/issue-detail.html @@ -3,9 +3,39 @@

Detail de l'issue

Informations de creation et de suivi de l'issue.

- @if (!isEditing) { - - } +
+ @if (!isEditing) { +
+ Status + +
+ } + +
+ @if (!isEditing) { + + } +
+ + + @if (moreMenuOpen) { +
+ +
+ } +
+
+
@@ -35,6 +65,22 @@ } + + Depend de + + @if (isEditing) { + + } @else { + {{ resolveDependencyLabels(dependencyIds) }} + } + + Assignee @@ -55,6 +101,16 @@ } + + Temps estimé + + @if (isEditing) { + + } @else { + {{ estimatedTimeValue !== null ? estimatedTimeValue + ' h' : '-' }} + } + + Description diff --git a/src/app/issues/issue-detail/issue-detail.ts b/src/app/issues/issue-detail/issue-detail.ts index f56eaef..8b6f37c 100644 --- a/src/app/issues/issue-detail/issue-detail.ts +++ b/src/app/issues/issue-detail/issue-detail.ts @@ -17,6 +17,33 @@ export class IssueDetail { protected issue: IssueEntity = this.buildIssue(); protected isEditing = this.route.snapshot.queryParamMap.get('mode') === 'edit'; private issueBeforeEdit: IssueEntity | null = null; + protected readonly issues = this.issuesStore.issues; + protected moreMenuOpen = false; + + protected readonly statusOptions: IssueEntity['status'][] = [ + 'draft', + 'todo', + 'in-progress', + 'done', + ]; + + protected get dependencyIds(): number[] { + return this.issue.dependsOnIds; + } + + protected set dependencyIds(value: number[]) { + this.issue.dependsOnIds = Array.isArray(value) + ? value.filter((dependencyId): dependencyId is number => typeof dependencyId === 'number') + : []; + } + + protected get estimatedTimeValue(): number | null { + return this.issue.estimatedTime; + } + + protected set estimatedTimeValue(value: number | null) { + this.issue.estimatedTime = value === null || value === undefined ? null : Number(value); + } constructor() { if (this.isEditing) { @@ -43,6 +70,40 @@ export class IssueDetail { this.router.navigate(['/issues', this.issue.id]); } + protected deleteIssue(): void { + this.issuesStore.deleteById(this.issue.id); + this.router.navigate(['/issues']); + } + + protected updateStatus(status: IssueEntity['status']): void { + this.issue.status = status; + this.issuesStore.upsert(this.issue); + } + + protected toggleMoreMenu(): void { + this.moreMenuOpen = !this.moreMenuOpen; + } + + protected closeMoreMenu(): void { + this.moreMenuOpen = false; + } + + protected resolveDependencyLabels(issueIds: number[]): string { + if (issueIds.length === 0) { + return '-'; + } + + return issueIds + .map((issueId) => this.issues().find((issue) => issue.id === issueId)) + .filter((issue): issue is IssueEntity => Boolean(issue)) + .map((issue) => `#${issue.id} - ${issue.name || 'Sans nom'}`) + .join(', '); + } + + protected get dependencyCandidates(): IssueEntity[] { + return this.issues().filter((issue) => issue.id !== this.issue.id); + } + private cloneIssue(issue: IssueEntity): IssueEntity { return { ...issue }; } @@ -63,6 +124,8 @@ export class IssueDetail { name: '', dueDate: '', description: '', + estimatedTime: null, + dependsOnIds: [], priority: 'Moyenne', status: 'draft', progress: 0, @@ -79,6 +142,8 @@ export class IssueDetail { name: '', dueDate: '', description: '', + estimatedTime: null, + dependsOnIds: [], priority: 'Moyenne', status: 'draft', progress: 0, diff --git a/src/app/issues/issues.store.ts b/src/app/issues/issues.store.ts index efbab24..adcdbf1 100644 --- a/src/app/issues/issues.store.ts +++ b/src/app/issues/issues.store.ts @@ -12,6 +12,8 @@ export type IssueEntity = { name: string; dueDate: string; description: string; + estimatedTime: number | null; + dependsOnIds: number[]; priority: IssuePriority; status: IssueStatus; progress: number; @@ -25,6 +27,8 @@ const DEFAULT_ISSUES: IssueEntity[] = [ name: 'Bug affichage menu mobile', dueDate: '2026-06-10', description: 'Corriger le comportement du menu sur petits ecrans.', + estimatedTime: 8, + dependsOnIds: [], priority: 'Haute', status: 'in-progress', progress: 35, @@ -36,6 +40,8 @@ const DEFAULT_ISSUES: IssueEntity[] = [ name: 'Erreur validation formulaire projet', dueDate: '2026-06-12', description: 'Fiabiliser les regles de validation du formulaire projet.', + estimatedTime: 16, + dependsOnIds: [], priority: 'Moyenne', status: 'todo', progress: 20, @@ -47,6 +53,8 @@ const DEFAULT_ISSUES: IssueEntity[] = [ name: 'Mise a jour message de bienvenue', dueDate: '2026-06-18', description: 'Mettre a jour le wording d accueil selon la charte produit.', + estimatedTime: 4, + dependsOnIds: [], priority: 'Basse', status: 'done', progress: 100, @@ -60,7 +68,7 @@ export class IssuesStore { constructor() { const cachedIssues = this.readFromStorage(); if (cachedIssues) { - this.data.set(cachedIssues); + this.data.set(cachedIssues.map((issue) => this.normalizeIssue(issue))); } } @@ -75,23 +83,73 @@ export class IssuesStore { return ids.length > 0 ? Math.max(...ids) + 1 : 1; } + createDraftIssue(): IssueEntity { + const draftIssue: IssueEntity = this.normalizeIssue({ + id: this.getNextId(), + assignee: '', + epic: '', + name: '', + dueDate: '', + description: '', + estimatedTime: null, + dependsOnIds: [], + priority: 'Moyenne', + status: 'draft', + progress: 0, + }); + + this.upsert(draftIssue); + return draftIssue; + } + upsert(issue: IssueEntity): void { + const normalizedIssue = this.normalizeIssue(issue); + this.data.update((issues) => { const existingIndex = issues.findIndex((current) => current.id === issue.id); if (existingIndex === -1) { - const created = [...issues, issue]; + const created = [...issues, normalizedIssue]; this.persistToStorage(created); return created; } const updated = [...issues]; - updated[existingIndex] = issue; + updated[existingIndex] = normalizedIssue; this.persistToStorage(updated); return updated; }); } + deleteById(id: number): void { + this.data.update((issues) => { + const updated = issues + .filter((issue) => issue.id !== id) + .map((issue) => ({ + ...issue, + dependsOnIds: issue.dependsOnIds.filter((dependencyId) => dependencyId !== id), + })); + + this.persistToStorage(updated); + return updated; + }); + } + + private normalizeIssue( + issue: Partial & { dependsOnId?: number | null }, + ): IssueEntity { + const legacyDependency = typeof issue.dependsOnId === 'number' ? [issue.dependsOnId] : []; + const normalizedDependencies = Array.isArray(issue.dependsOnIds) + ? issue.dependsOnIds.filter((value): value is number => typeof value === 'number') + : legacyDependency; + + return { + ...issue, + estimatedTime: issue.estimatedTime ?? null, + dependsOnIds: normalizedDependencies, + } as IssueEntity; + } + private readFromStorage(): IssueEntity[] | null { if (typeof window === 'undefined') { return null; diff --git a/src/app/menu/menu.html b/src/app/menu/menu.html index bd2687c..ebc9791 100644 --- a/src/app/menu/menu.html +++ b/src/app/menu/menu.html @@ -1,5 +1,4 @@