Files
Bonsai-webapp/src/app/issues/issues.store.ts
T
2026-05-28 18:50:36 +02:00

124 lines
4.0 KiB
TypeScript

import { Injectable, inject, signal } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { IssuesApiService } from './issues-api.service';
export type IssueStatus = 'draft' | 'todo' | 'done' | 'in-progress';
export type IssuePriority = 'TRES_FAIBLE' | 'BASSE' | 'MOYENNE' | 'HAUTE' | 'TRES_HAUTE';
export type IssueType = 'Epic' | 'Bug' | 'Study' | 'Story' | 'Task' | 'Technical Story';
export type IssueComment = {
id: number;
text: string;
createdAt: string;
updatedAt: string | null;
linkedIssueIds: number[];
};
export type IssueEntity = {
id: number;
type: IssueType;
assignee: string;
epic: string;
name: string;
dueDate: string;
description: string;
estimatedTime: number | null;
dependsOnIds: number[];
comments: IssueComment[];
priority: IssuePriority;
status: IssueStatus;
progress: number;
};
@Injectable({ providedIn: 'root' })
export class IssuesStore {
private readonly api = inject(IssuesApiService);
private readonly data = signal<IssueEntity[]>([]);
readonly loading = signal(false);
readonly loaded = signal(false);
readonly issues = this.data.asReadonly();
getById(id: number): IssueEntity | undefined {
return this.data().find((i) => i.id === id);
}
getNextId(): number {
const ids = this.data().map((i) => i.id);
return ids.length === 0 ? 1 : Math.max(...ids) + 1;
}
async load(): Promise<void> {
if (this.loaded()) return;
this.loading.set(true);
try {
const issues = await firstValueFrom(this.api.getAll());
this.data.set(issues.map((i) => this.normalizeIssue(i)));
this.loaded.set(true);
} finally {
this.loading.set(false);
}
}
async upsert(issue: IssueEntity): Promise<IssueEntity> {
const normalized = this.normalizeIssue(issue);
if (!normalized.id) {
const { id: _id, ...body } = normalized;
const created = this.normalizeIssue(await firstValueFrom(this.api.create(body)));
this.data.update((issues) => [...issues, created]);
return created;
} else {
const apiResult = await firstValueFrom(this.api.update(normalized.id, normalized));
// L'API ne retourne pas linkedIssueIds dans les commentaires : on le restaure
// depuis les données envoyées pour ne pas perdre les liens.
if (Array.isArray(apiResult.comments) && Array.isArray(normalized.comments)) {
apiResult.comments = apiResult.comments.map((c: IssueComment) => {
if (Array.isArray(c.linkedIssueIds)) return c;
const sent = normalized.comments.find((nc) => nc.id === c.id);
return { ...c, linkedIssueIds: sent?.linkedIssueIds ?? [] };
});
}
const updated = this.normalizeIssue(apiResult);
this.data.update((issues) => {
const idx = issues.findIndex((i) => i.id === normalized.id);
if (idx === -1) return issues;
const copy = [...issues];
copy[idx] = updated;
return copy;
});
return updated;
}
}
async deleteById(id: number): Promise<void> {
await firstValueFrom(this.api.remove(id));
this.data.update((issues) =>
issues
.filter((i) => i.id !== id)
.map((i) => ({ ...i, dependsOnIds: i.dependsOnIds.filter((d) => d !== id) })),
);
}
private normalizeIssue(
issue: Partial<IssueEntity> & { 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,
type: issue.type ?? 'Story',
estimatedTime: issue.estimatedTime ?? null,
dependsOnIds: normalizedDependencies,
comments: Array.isArray(issue.comments)
? issue.comments.map((c) => ({
...c,
linkedIssueIds: Array.isArray(c.linkedIssueIds) ? c.linkedIssueIds : [],
}))
: [],
} as IssueEntity;
}
}