Ajoute date debut et date de fin

Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
2026-05-29 07:58:51 +02:00
parent 75ce668850
commit ba6a3d0827
11 changed files with 175 additions and 0 deletions
+2
View File
@@ -12,6 +12,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
assignee: '', assignee: '',
epic: '', epic: '',
name: 'Test Issue', name: 'Test Issue',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -11,6 +11,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
assignee: '', assignee: '',
epic: '', epic: '',
name: 'Test Issue', name: 'Test Issue',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -170,6 +170,8 @@ export class IssueComments {
name, name,
assignee: '', assignee: '',
epic: '', epic: '',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -153,6 +153,33 @@
<label class="field-label">Assignee</label> <label class="field-label">Assignee</label>
<input aria-label="Assignee" class="form-control form-control-sm" type="text" [(ngModel)]="issue.assignee" (blur)="saveIssue()" /> <input aria-label="Assignee" class="form-control form-control-sm" type="text" [(ngModel)]="issue.assignee" (blur)="saveIssue()" />
</div> </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)="saveIssue()"
/>
</div>
<div class="col-6">
<label class="field-label">Date de fin</label>
<input
aria-label="Date de fin"
class="form-control form-control-sm"
[class.is-invalid]="!!dateValidationError"
type="date"
[(ngModel)]="issue.endDate"
(blur)="saveIssue()"
/>
</div>
@if (dateValidationError) {
<div class="col-12 text-danger small">{{ dateValidationError }}</div>
}
</div>
<div> <div>
<label class="field-label">Date d'échéance</label> <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)]="issue.dueDate" (blur)="saveIssue()" /> <input aria-label="Date d'échéance" class="form-control form-control-sm" type="date" [(ngModel)]="issue.dueDate" (blur)="saveIssue()" />
@@ -14,6 +14,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
assignee: '', assignee: '',
epic: '', epic: '',
name: 'Test Issue', name: 'Test Issue',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -213,6 +215,86 @@ describe('IssueDetail — existing issue', () => {
(component as any).saveIssue(); (component as any).saveIssue();
expect(store.issues().length).toBe(countBefore); expect(store.issues().length).toBe(countBefore);
}); });
it('does not persist when dateValidationError is set', async () => {
(component as any).issue.name = 'Has Dates';
(component as any).issue.startDate = '2026-02-01';
(component as any).issue.endDate = '2026-01-01';
await (component as any).saveIssue();
expect(store.getById(1)?.name).not.toBe('Has Dates');
});
});
describe('dateValidationError', () => {
it('returns null when both dates are empty', () => {
(component as any).issue.startDate = '';
(component as any).issue.endDate = '';
expect((component as any).dateValidationError).toBeNull();
});
it('returns null when only startDate is set', () => {
(component as any).issue.startDate = '2026-01-01';
(component as any).issue.endDate = '';
expect((component as any).dateValidationError).toBeNull();
});
it('returns null when only endDate is set', () => {
(component as any).issue.startDate = '';
(component as any).issue.endDate = '2026-01-31';
expect((component as any).dateValidationError).toBeNull();
});
it('returns null when startDate equals endDate', () => {
(component as any).issue.startDate = '2026-01-15';
(component as any).issue.endDate = '2026-01-15';
expect((component as any).dateValidationError).toBeNull();
});
it('returns null when startDate is before endDate', () => {
(component as any).issue.startDate = '2026-01-01';
(component as any).issue.endDate = '2026-01-31';
expect((component as any).dateValidationError).toBeNull();
});
it('returns an error when startDate is after endDate', () => {
(component as any).issue.startDate = '2026-02-01';
(component as any).issue.endDate = '2026-01-01';
expect((component as any).dateValidationError).toContain('supérieure à la date de fin');
});
it('returns null when dependency has no endDate and startDate is set', async () => {
store.upsert(makeIssue({ id: 10, startDate: '2026-01-01', endDate: '' }));
(component as any).issue.dependsOnIds = [10];
(component as any).issue.startDate = '2025-12-01';
expect((component as any).dateValidationError).toBeNull();
});
it('returns an error when startDate is before dependency endDate (Finish-to-Start)', async () => {
store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' }));
(component as any).issue.dependsOnIds = [10];
(component as any).issue.startDate = '2026-01-15';
expect((component as any).dateValidationError).toContain('#10');
});
it('returns null when startDate equals dependency endDate', async () => {
store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' }));
(component as any).issue.dependsOnIds = [10];
(component as any).issue.startDate = '2026-02-01';
expect((component as any).dateValidationError).toBeNull();
});
it('returns null when startDate is after dependency endDate', async () => {
store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' }));
(component as any).issue.dependsOnIds = [10];
(component as any).issue.startDate = '2026-02-15';
expect((component as any).dateValidationError).toBeNull();
});
it('returns null when there are no dependencies', () => {
(component as any).issue.dependsOnIds = [];
(component as any).issue.startDate = '2026-01-01';
expect((component as any).dateValidationError).toBeNull();
});
}); });
describe('deleteIssue', () => { describe('deleteIssue', () => {
@@ -170,6 +170,8 @@ export class IssueDetail {
assignee: '', assignee: '',
epic: this.issue.name, epic: this.issue.name,
name, name,
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -235,6 +237,22 @@ export class IssueDetail {
return !!this.issue.epic; return !!this.issue.epic;
} }
protected get dateValidationError(): string | null {
const { startDate, endDate } = this.issue;
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) {
for (const depId of this.issue.dependsOnIds) {
const dep = this.issuesStore.getById(depId);
if (dep?.endDate && startDate < dep.endDate) {
return `La date de début ne peut pas être antérieure à la date de fin de la dépendance #${depId}.`;
}
}
}
return null;
}
protected startEditDescription(): void { protected startEditDescription(): void {
this._descriptionBeforeEdit = this.issue.description; this._descriptionBeforeEdit = this.issue.description;
this.editingDescription = true; this.editingDescription = true;
@@ -358,6 +376,7 @@ export class IssueDetail {
protected async saveIssue(explicit = false): Promise<void> { protected async saveIssue(explicit = false): Promise<void> {
if (this.isNewIssueRoute && !explicit) return; if (this.isNewIssueRoute && !explicit) return;
if (!this.issue.name.trim()) return; if (!this.issue.name.trim()) return;
if (this.dateValidationError) return;
const saved = await this.issuesStore.upsert(this.issue); const saved = await this.issuesStore.upsert(this.issue);
this.issue = { ...saved }; this.issue = { ...saved };
if (this.isNewIssueRoute) { if (this.isNewIssueRoute) {
@@ -413,6 +432,8 @@ export class IssueDetail {
assignee: '', assignee: '',
epic: '', epic: '',
name: '', name: '',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -434,6 +455,8 @@ export class IssueDetail {
assignee: '', assignee: '',
epic: '', epic: '',
name: '', name: '',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
+2
View File
@@ -13,6 +13,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
assignee: '', assignee: '',
epic: '', epic: '',
name: 'Test Issue', name: 'Test Issue',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
+27
View File
@@ -10,6 +10,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
assignee: '', assignee: '',
epic: '', epic: '',
name: 'Test Issue', name: 'Test Issue',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -156,6 +158,31 @@ describe('IssuesStore', () => {
await p; await p;
expect(store.getById(994)?.estimatedTime).toBeNull(); expect(store.getById(994)?.estimatedTime).toBeNull();
}); });
it('defaults startDate to empty string when missing in API response', async () => {
const issue = { ...makeIssue({ id: 0 }), startDate: undefined } as any;
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 993 }), startDate: undefined });
await p;
expect(store.getById(993)?.startDate).toBe('');
});
it('defaults endDate to empty string when missing in API response', async () => {
const issue = { ...makeIssue({ id: 0 }), endDate: undefined } as any;
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 992 }), endDate: undefined });
await p;
expect(store.getById(992)?.endDate).toBe('');
});
it('preserves startDate and endDate when provided', async () => {
const issue = makeIssue({ id: 0, startDate: '2026-01-01', endDate: '2026-01-31' });
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 991, startDate: '2026-01-01', endDate: '2026-01-31' }));
await p;
expect(store.getById(991)?.startDate).toBe('2026-01-01');
expect(store.getById(991)?.endDate).toBe('2026-01-31');
});
}); });
describe('deleteById', () => { describe('deleteById', () => {
+4
View File
@@ -20,6 +20,8 @@ export type IssueEntity = {
assignee: string; assignee: string;
epic: string; epic: string;
name: string; name: string;
startDate: string;
endDate: string;
dueDate: string; dueDate: string;
description: string; description: string;
estimatedTime: number | null; estimatedTime: number | null;
@@ -110,6 +112,8 @@ export class IssuesStore {
return { return {
...issue, ...issue,
type: issue.type ?? 'Story', type: issue.type ?? 'Story',
startDate: issue.startDate ?? '',
endDate: issue.endDate ?? '',
estimatedTime: issue.estimatedTime ?? null, estimatedTime: issue.estimatedTime ?? null,
dependsOnIds: normalizedDependencies, dependsOnIds: normalizedDependencies,
comments: Array.isArray(issue.comments) comments: Array.isArray(issue.comments)
@@ -14,6 +14,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
assignee: '', assignee: '',
epic: '', epic: '',
name: 'Test Issue', name: 'Test Issue',
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,
@@ -141,6 +141,8 @@ export class MilestoneDetail {
assignee: '', assignee: '',
epic: '', epic: '',
name, name,
startDate: '',
endDate: '',
dueDate: '', dueDate: '',
description: '', description: '',
estimatedTime: null, estimatedTime: null,