Ajout des dates calculés
This commit is contained in:
@@ -13,6 +13,7 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
|
||||
@@ -12,6 +12,7 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
|
||||
@@ -171,6 +171,7 @@ export class IssueComments {
|
||||
assignee: '',
|
||||
epic: '',
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
|
||||
@@ -240,3 +240,22 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Date mode label row */
|
||||
.field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.date-mode-select {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -155,15 +155,41 @@
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="field-label">Date de début</label>
|
||||
<input
|
||||
aria-label="Date de début"
|
||||
class="form-control form-control-sm"
|
||||
[class.is-invalid]="!!dateValidationError"
|
||||
type="date"
|
||||
[(ngModel)]="issue.startDate"
|
||||
(blur)="onStartDateBlur()"
|
||||
/>
|
||||
<div class="field-label-row">
|
||||
<span class="field-label mb-0">Date de début</span>
|
||||
@if (hasDependencies) {
|
||||
<select
|
||||
class="date-mode-select"
|
||||
aria-label="Mode date de début"
|
||||
[ngModel]="issue.startDateMode"
|
||||
(ngModelChange)="startDateModeValue = $event"
|
||||
>
|
||||
<option value="calculated">Calculée</option>
|
||||
<option value="forced">Forcée</option>
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
@if (issue.startDateMode === 'calculated') {
|
||||
<input
|
||||
aria-label="Date de début calculée"
|
||||
class="form-control form-control-sm bg-body-secondary"
|
||||
type="date"
|
||||
[ngModel]="calculatedStartDate"
|
||||
readonly
|
||||
/>
|
||||
@if (startDateModeWarning) {
|
||||
<div class="text-warning small mt-1">{{ startDateModeWarning }}</div>
|
||||
}
|
||||
} @else {
|
||||
<input
|
||||
aria-label="Date de début"
|
||||
class="form-control form-control-sm"
|
||||
[class.is-invalid]="!!dateValidationError"
|
||||
type="date"
|
||||
[(ngModel)]="issue.startDate"
|
||||
(blur)="onStartDateBlur()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="field-label">Date de fin</label>
|
||||
|
||||
@@ -15,6 +15,7 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
@@ -299,6 +300,115 @@ describe('IssueDetail — existing issue', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDependencies', () => {
|
||||
it('returns false when dependsOnIds is empty', () => {
|
||||
(component as any).issue.dependsOnIds = [];
|
||||
expect((component as any).hasDependencies).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when dependsOnIds has entries', () => {
|
||||
(component as any).issue.dependsOnIds = [2];
|
||||
expect((component as any).hasDependencies).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculatedStartDate', () => {
|
||||
it('returns empty string when there are no dependencies', () => {
|
||||
(component as any).issue.dependsOnIds = [];
|
||||
expect((component as any).calculatedStartDate).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when dependencies have no endDate', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '' }));
|
||||
(component as any).issue.dependsOnIds = [10];
|
||||
expect((component as any).calculatedStartDate).toBe('');
|
||||
});
|
||||
|
||||
it('returns the endDate of the single dependency', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' }));
|
||||
(component as any).issue.dependsOnIds = [10];
|
||||
expect((component as any).calculatedStartDate).toBe('2026-03-01');
|
||||
});
|
||||
|
||||
it('returns the latest endDate when multiple dependencies exist', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' }));
|
||||
store.upsert(makeIssue({ id: 11, endDate: '2026-04-15' }));
|
||||
store.upsert(makeIssue({ id: 12, endDate: '2026-02-20' }));
|
||||
(component as any).issue.dependsOnIds = [10, 11, 12];
|
||||
expect((component as any).calculatedStartDate).toBe('2026-04-15');
|
||||
});
|
||||
|
||||
it('ignores dependencies without endDate in the max calculation', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' }));
|
||||
store.upsert(makeIssue({ id: 11, endDate: '' }));
|
||||
(component as any).issue.dependsOnIds = [10, 11];
|
||||
expect((component as any).calculatedStartDate).toBe('2026-03-01');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startDateModeWarning', () => {
|
||||
it('returns null in forced mode', () => {
|
||||
(component as any).issue.startDateMode = 'forced';
|
||||
expect((component as any).startDateModeWarning).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null in calculated mode when a dependency has an endDate', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' }));
|
||||
(component as any).issue.dependsOnIds = [10];
|
||||
(component as any).issue.startDateMode = 'calculated';
|
||||
expect((component as any).startDateModeWarning).toBeNull();
|
||||
});
|
||||
|
||||
it('returns a warning message in calculated mode when no dependency has an endDate', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '' }));
|
||||
(component as any).issue.dependsOnIds = [10];
|
||||
(component as any).issue.startDateMode = 'calculated';
|
||||
expect((component as any).startDateModeWarning).toContain('date de fin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startDateModeValue setter', () => {
|
||||
it('switching to calculated sets startDate from dependency endDate', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '2026-05-01' }));
|
||||
(component as any).issue.dependsOnIds = [10];
|
||||
(component as any).issue.startDateMode = 'forced';
|
||||
(component as any).startDateModeValue = 'calculated';
|
||||
expect((component as any).issue.startDate).toBe('2026-05-01');
|
||||
});
|
||||
|
||||
it('switching to calculated recalculates endDate', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '2026-05-01' }));
|
||||
(component as any).issue.dependsOnIds = [10];
|
||||
(component as any).issue.estimatedTime = 8;
|
||||
(component as any).startDateModeValue = 'calculated';
|
||||
expect((component as any).issue.endDate).toBe('2026-05-01');
|
||||
});
|
||||
|
||||
it('switching to forced preserves the current startDate', () => {
|
||||
(component as any).issue.startDate = '2026-04-10';
|
||||
(component as any).issue.startDateMode = 'calculated';
|
||||
(component as any).startDateModeValue = 'forced';
|
||||
expect((component as any).issue.startDate).toBe('2026-04-10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateValidationError — calculated mode', () => {
|
||||
it('does not flag dependency constraint in calculated mode', () => {
|
||||
store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' }));
|
||||
(component as any).issue.dependsOnIds = [10];
|
||||
(component as any).issue.startDate = '2026-01-01';
|
||||
(component as any).issue.startDateMode = 'calculated';
|
||||
expect((component as any).dateValidationError).toBeNull();
|
||||
});
|
||||
|
||||
it('still flags startDate > endDate in calculated mode', () => {
|
||||
(component as any).issue.startDate = '2026-02-01';
|
||||
(component as any).issue.endDate = '2026-01-01';
|
||||
(component as any).issue.startDateMode = 'calculated';
|
||||
expect((component as any).dateValidationError).toContain('supérieure à la date de fin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('recalculateEndDate (via estimatedTimeValue setter)', () => {
|
||||
it('sets endDate to startDate when estimatedTime is 8h or less', () => {
|
||||
(component as any).issue.startDate = '2026-06-01';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, effect, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
@@ -56,6 +56,18 @@ export class IssueDetail {
|
||||
this.showCreateInEpic = false;
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
void this.issues();
|
||||
if (this.issue.startDateMode === 'calculated') {
|
||||
const newStart = this.calculatedStartDate;
|
||||
if (this.issue.startDate !== newStart) {
|
||||
this.issue.startDate = newStart;
|
||||
this.recalculateEndDate();
|
||||
this.saveIssue();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected showAddDependency = false;
|
||||
@@ -78,6 +90,10 @@ export class IssueDetail {
|
||||
'Technical Story',
|
||||
];
|
||||
|
||||
protected get hasDependencies(): boolean {
|
||||
return this.issue.dependsOnIds.length > 0;
|
||||
}
|
||||
|
||||
protected get dependencyIds(): number[] {
|
||||
return this.issue.dependsOnIds;
|
||||
}
|
||||
@@ -116,6 +132,33 @@ export class IssueDetail {
|
||||
await this.saveIssue();
|
||||
}
|
||||
|
||||
protected get calculatedStartDate(): string {
|
||||
const dates = this.issue.dependsOnIds
|
||||
.map((id) => this.issuesStore.getById(id)?.endDate)
|
||||
.filter((d): d is string => !!d);
|
||||
if (dates.length === 0) return '';
|
||||
return dates.reduce((max, d) => (d > max ? d : max));
|
||||
}
|
||||
|
||||
protected get startDateModeWarning(): string | null {
|
||||
if (this.issue.startDateMode !== 'calculated') return null;
|
||||
if (!this.calculatedStartDate) return "Aucune dépendance n'a de date de fin définie.";
|
||||
return null;
|
||||
}
|
||||
|
||||
protected get startDateModeValue(): 'forced' | 'calculated' {
|
||||
return this.issue.startDateMode;
|
||||
}
|
||||
|
||||
protected set startDateModeValue(mode: 'forced' | 'calculated') {
|
||||
this.issue.startDateMode = mode;
|
||||
if (mode === 'calculated') {
|
||||
this.issue.startDate = this.calculatedStartDate;
|
||||
}
|
||||
this.recalculateEndDate();
|
||||
this.saveIssue();
|
||||
}
|
||||
|
||||
protected get estimatedTimeValue(): number | null {
|
||||
return this.issue.estimatedTime;
|
||||
}
|
||||
@@ -126,12 +169,14 @@ export class IssueDetail {
|
||||
}
|
||||
|
||||
private recalculateEndDate(): void {
|
||||
const { startDate, estimatedTime } = this.issue;
|
||||
if (!startDate || estimatedTime === null || estimatedTime <= 0) {
|
||||
const effectiveStart =
|
||||
this.issue.startDateMode === 'calculated' ? this.calculatedStartDate : this.issue.startDate;
|
||||
const { estimatedTime } = this.issue;
|
||||
if (!effectiveStart || estimatedTime === null || estimatedTime <= 0) {
|
||||
this.issue.endDate = '';
|
||||
return;
|
||||
}
|
||||
const start = new Date(startDate);
|
||||
const start = new Date(effectiveStart);
|
||||
const extraDays = Math.max(0, Math.ceil(estimatedTime / 8) - 1);
|
||||
start.setDate(start.getDate() + extraDays);
|
||||
this.issue.endDate = start.toISOString().split('T')[0];
|
||||
@@ -197,6 +242,7 @@ export class IssueDetail {
|
||||
epic: this.issue.name,
|
||||
name,
|
||||
startDate: '',
|
||||
startDateMode: 'calculated',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
@@ -301,7 +347,7 @@ export class IssueDetail {
|
||||
if (startDate && endDate && startDate > endDate) {
|
||||
return 'La date de début ne peut pas être supérieure à la date de fin.';
|
||||
}
|
||||
if (startDate && this.issue.dependsOnIds.length > 0) {
|
||||
if (this.issue.startDateMode !== 'calculated' && startDate && this.issue.dependsOnIds.length > 0) {
|
||||
for (const depId of this.issue.dependsOnIds) {
|
||||
const dep = this.issuesStore.getById(depId);
|
||||
if (dep?.endDate && startDate < dep.endDate) {
|
||||
@@ -492,6 +538,7 @@ export class IssueDetail {
|
||||
epic: '',
|
||||
name: '',
|
||||
startDate: '',
|
||||
startDateMode: 'calculated',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
@@ -515,6 +562,7 @@ export class IssueDetail {
|
||||
epic: '',
|
||||
name: '',
|
||||
startDate: '',
|
||||
startDateMode: 'calculated',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
|
||||
@@ -14,6 +14,7 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
|
||||
@@ -11,6 +11,7 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
@@ -183,6 +184,123 @@ describe('IssuesStore', () => {
|
||||
expect(store.getById(991)?.startDate).toBe('2026-01-01');
|
||||
expect(store.getById(991)?.endDate).toBe('2026-01-31');
|
||||
});
|
||||
|
||||
it('restores startDateMode when the API response omits it', async () => {
|
||||
const issue = makeIssue({ id: 1, startDateMode: 'calculated' });
|
||||
const p = store.upsert(issue);
|
||||
const apiResponse = { ...makeIssue({ id: 1 }), startDateMode: undefined };
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse);
|
||||
await p;
|
||||
expect(store.getById(1)?.startDateMode).toBe('calculated');
|
||||
});
|
||||
|
||||
it('restores linkedIssueIds in comments when API response omits them', async () => {
|
||||
const issueWithComment = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [2, 3] }] });
|
||||
const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: undefined as any }] });
|
||||
const p = store.upsert(issueWithComment);
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse);
|
||||
await p;
|
||||
expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
it('keeps existing linkedIssueIds in comments when already present in API response', async () => {
|
||||
const issue = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] });
|
||||
const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] });
|
||||
const p = store.upsert(issue);
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse);
|
||||
await p;
|
||||
expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cascade recalculation of calculated-mode issues', () => {
|
||||
it('updates startDate of a calculated-mode dependent when its dependency endDate changes', async () => {
|
||||
await loadWith([
|
||||
makeIssue({ id: 1, endDate: '2026-06-01' }),
|
||||
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', endDate: '' }),
|
||||
]);
|
||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
await p;
|
||||
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
||||
});
|
||||
|
||||
it('recalculates endDate of a calculated-mode dependent based on estimatedTime', async () => {
|
||||
await loadWith([
|
||||
makeIssue({ id: 1, endDate: '2026-06-01' }),
|
||||
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }),
|
||||
]);
|
||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
await p;
|
||||
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
||||
expect(store.getById(2)?.endDate).toBe('2026-06-11');
|
||||
});
|
||||
|
||||
it('clears endDate when dependency loses its endDate', async () => {
|
||||
await loadWith([
|
||||
makeIssue({ id: 1, endDate: '2026-06-10' }),
|
||||
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }),
|
||||
]);
|
||||
const p = store.upsert(makeIssue({ id: 1, endDate: '' }));
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '' }));
|
||||
await p;
|
||||
expect(store.getById(2)?.startDate).toBe('');
|
||||
expect(store.getById(2)?.endDate).toBe('');
|
||||
});
|
||||
|
||||
it('cascades through a chain A → B → C', async () => {
|
||||
await loadWith([
|
||||
makeIssue({ id: 1, endDate: '2026-06-01' }),
|
||||
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', estimatedTime: 8, endDate: '2026-06-01' }),
|
||||
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [2], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }),
|
||||
]);
|
||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
await p;
|
||||
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
||||
expect(store.getById(2)?.endDate).toBe('2026-06-10');
|
||||
expect(store.getById(3)?.startDate).toBe('2026-06-10');
|
||||
expect(store.getById(3)?.endDate).toBe('2026-06-11');
|
||||
});
|
||||
|
||||
it('does not affect forced-mode issues', async () => {
|
||||
await loadWith([
|
||||
makeIssue({ id: 1, endDate: '2026-06-01' }),
|
||||
makeIssue({ id: 2, startDateMode: 'forced', dependsOnIds: [1], startDate: '2026-05-01', endDate: '2026-05-15' }),
|
||||
]);
|
||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
await p;
|
||||
expect(store.getById(2)?.startDate).toBe('2026-05-01');
|
||||
expect(store.getById(2)?.endDate).toBe('2026-05-15');
|
||||
});
|
||||
|
||||
it('uses the latest endDate among multiple dependencies', async () => {
|
||||
await loadWith([
|
||||
makeIssue({ id: 1, endDate: '2026-06-01' }),
|
||||
makeIssue({ id: 2, endDate: '2026-06-05' }),
|
||||
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '', estimatedTime: 8, endDate: '' }),
|
||||
]);
|
||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||
await p;
|
||||
expect(store.getById(3)?.startDate).toBe('2026-06-10');
|
||||
expect(store.getById(3)?.endDate).toBe('2026-06-10');
|
||||
});
|
||||
|
||||
it('recalculates after deleteById removes a dependency', async () => {
|
||||
await loadWith([
|
||||
makeIssue({ id: 1, endDate: '2026-06-10' }),
|
||||
makeIssue({ id: 2, endDate: '2026-06-05' }),
|
||||
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }),
|
||||
]);
|
||||
const p = store.deleteById(1);
|
||||
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null);
|
||||
await p;
|
||||
expect(store.getById(3)?.startDate).toBe('2026-06-05');
|
||||
expect(store.getById(3)?.endDate).toBe('2026-06-05');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteById', () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ export type IssueEntity = {
|
||||
epic: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
startDateMode: 'forced' | 'calculated';
|
||||
endDate: string;
|
||||
dueDate: string;
|
||||
description: string;
|
||||
@@ -68,6 +69,7 @@ export class IssuesStore {
|
||||
const { id: _id, ...body } = normalized;
|
||||
const created = this.normalizeIssue(await firstValueFrom(this.api.create(body)));
|
||||
this.data.update((issues) => [...issues, created]);
|
||||
this.recalculateCalculatedIssues();
|
||||
return created;
|
||||
} else {
|
||||
const apiResult = await firstValueFrom(this.api.update(normalized.id, normalized));
|
||||
@@ -80,6 +82,10 @@ export class IssuesStore {
|
||||
return { ...c, linkedIssueIds: sent?.linkedIssueIds ?? [] };
|
||||
});
|
||||
}
|
||||
// L'API ne retourne pas startDateMode : on le restaure depuis les données envoyées.
|
||||
if (apiResult.startDateMode == null) {
|
||||
apiResult.startDateMode = normalized.startDateMode;
|
||||
}
|
||||
const updated = this.normalizeIssue(apiResult);
|
||||
this.data.update((issues) => {
|
||||
const idx = issues.findIndex((i) => i.id === normalized.id);
|
||||
@@ -88,6 +94,7 @@ export class IssuesStore {
|
||||
copy[idx] = updated;
|
||||
return copy;
|
||||
});
|
||||
this.recalculateCalculatedIssues();
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
@@ -99,6 +106,41 @@ export class IssuesStore {
|
||||
.filter((i) => i.id !== id)
|
||||
.map((i) => ({ ...i, dependsOnIds: i.dependsOnIds.filter((d) => d !== id) })),
|
||||
);
|
||||
this.recalculateCalculatedIssues();
|
||||
}
|
||||
|
||||
private recalculateCalculatedIssues(): void {
|
||||
let anyChanged: boolean;
|
||||
do {
|
||||
anyChanged = false;
|
||||
this.data.update((issues) => {
|
||||
const result = issues.map((issue) => {
|
||||
if (issue.startDateMode !== 'calculated') return issue;
|
||||
const newStart = this.computeStartDate(issue, issues);
|
||||
const newEnd = this.computeEndDate(newStart, issue.estimatedTime);
|
||||
if (issue.startDate === newStart && issue.endDate === newEnd) return issue;
|
||||
anyChanged = true;
|
||||
return { ...issue, startDate: newStart, endDate: newEnd };
|
||||
});
|
||||
return anyChanged ? result : issues;
|
||||
});
|
||||
} while (anyChanged);
|
||||
}
|
||||
|
||||
private computeStartDate(issue: IssueEntity, allIssues: IssueEntity[]): string {
|
||||
const dates = issue.dependsOnIds
|
||||
.map((id) => allIssues.find((i) => i.id === id)?.endDate)
|
||||
.filter((d): d is string => !!d);
|
||||
if (dates.length === 0) return '';
|
||||
return dates.reduce((max, d) => (d > max ? d : max));
|
||||
}
|
||||
|
||||
private computeEndDate(startDate: string, estimatedTime: number | null): string {
|
||||
if (!startDate || estimatedTime === null || estimatedTime <= 0) return '';
|
||||
const start = new Date(startDate);
|
||||
const extraDays = Math.max(0, Math.ceil(estimatedTime / 8) - 1);
|
||||
start.setDate(start.getDate() + extraDays);
|
||||
return start.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
private normalizeIssue(
|
||||
@@ -113,6 +155,7 @@ export class IssuesStore {
|
||||
...issue,
|
||||
type: issue.type ?? 'Story',
|
||||
startDate: issue.startDate ?? '',
|
||||
startDateMode: issue.startDateMode === 'calculated' ? 'calculated' : 'forced',
|
||||
endDate: issue.endDate ?? '',
|
||||
estimatedTime: issue.estimatedTime ?? null,
|
||||
dependsOnIds: normalizedDependencies,
|
||||
|
||||
@@ -15,6 +15,7 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
|
||||
@@ -165,6 +165,7 @@ export class MilestoneDetail {
|
||||
epic: '',
|
||||
name,
|
||||
startDate: '',
|
||||
startDateMode: 'forced',
|
||||
endDate: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
|
||||
Reference in New Issue
Block a user