feat: upgrade Angular dependencies to version 21 and implement new animation features
- Updated Angular dependencies in package.json to version 21.x.x. - Added `provideAnimationsAsync` to app configuration for improved animation handling. - Refactored user input component to utilize a dynamic list of vacation days with animations. - Introduced a new `DayInput` component for managing individual vacation days. - Enhanced styling and animations for user input and day input components. - Updated calendar service to support new vacation day structure. - Adjusted tests to accommodate changes in component structure and dependencies.
This commit is contained in:
1 parent
5ab29ef770
commit
e10f2ff4e5
15 files changed
+15528
-15284
No files matched your search
Generated
+15134
-15137
File diff suppressed because it is too large.
Load diff
+69
-68
@@ -1,70 +1,71 @@
|
|||||||
{
|
{
|
||||||
"name": "vac-optimizer",
|
"name": "vac-optimizer",
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs",
|
"serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs",
|
||||||
"lint": "eslint \"src/**/*.ts\"",
|
"lint": "eslint \"src/**/*.ts\"",
|
||||||
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
||||||
"format": "prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
|
"format": "prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
|
||||||
"format:check": "prettier --check \"src/**/*.{ts,html,scss,css,json}\""
|
"format:check": "prettier --check \"src/**/*.{ts,html,scss,css,json}\""
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": "*.html",
|
"files": "*.html",
|
||||||
"options": {
|
"options": {
|
||||||
"parser": "angular"
|
"parser": "angular"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^20.2.12",
|
"@angular/animations": "^21.0.1",
|
||||||
"@angular/common": "^20.3.10",
|
"@angular/cdk": "^21.0.1",
|
||||||
"@angular/compiler": "^20.3.10",
|
"@angular/common": "^21.0.1",
|
||||||
"@angular/core": "^20.3.10",
|
"@angular/compiler": "^21.0.1",
|
||||||
"@angular/forms": "^20.3.10",
|
"@angular/core": "^21.0.1",
|
||||||
"@angular/material": "^20.2.12",
|
"@angular/forms": "^21.0.1",
|
||||||
"@angular/platform-browser": "^20.3.10",
|
"@angular/material": "^21.0.1",
|
||||||
"@angular/platform-server": "^20.3.10",
|
"@angular/platform-browser": "^21.0.1",
|
||||||
"@angular/router": "^20.3.10",
|
"@angular/platform-server": "^21.0.1",
|
||||||
"@angular/ssr": "^20.3.10",
|
"@angular/router": "^21.0.1",
|
||||||
"date-holidays": "^3.26.2",
|
"@angular/ssr": "^21.0.1",
|
||||||
"dayjs": "^1.11.19",
|
"date-holidays": "^3.26.2",
|
||||||
"express": "^5.1.0",
|
"dayjs": "^1.11.19",
|
||||||
"rxjs": "~7.8.0",
|
"express": "^5.1.0",
|
||||||
"tslib": "^2.3.0"
|
"rxjs": "~7.8.0",
|
||||||
},
|
"tslib": "^2.3.0"
|
||||||
"devDependencies": {
|
},
|
||||||
"@angular/build": "^20.3.10",
|
"devDependencies": {
|
||||||
"@angular/cli": "^20.3.10",
|
"@angular/build": "^21.0.1",
|
||||||
"@angular/compiler-cli": "^20.3.10",
|
"@angular/cli": "^21.0.1",
|
||||||
"@eslint/js": "^9.39.1",
|
"@angular/compiler-cli": "^21.0.1",
|
||||||
"@types/express": "^5.0.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/express": "^5.0.1",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/node": "^24.10.0",
|
"@types/jest": "^30.0.0",
|
||||||
"eslint": "^9.39.1",
|
"@types/node": "^24.10.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"jasmine-core": "~5.12.1",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"jest": "^30.2.0",
|
"jasmine-core": "~5.12.1",
|
||||||
"jest-preset-angular": "^15.0.3",
|
"jest": "^30.2.0",
|
||||||
"karma": "~6.4.0",
|
"jest-preset-angular": "^15.0.3",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma": "~6.4.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"prettier": "^3.6.2",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.9.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript-eslint": "^8.46.4"
|
"typescript": "~5.9.2",
|
||||||
}
|
"typescript-eslint": "^8.46.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
withEventReplay,
|
withEventReplay,
|
||||||
withIncrementalHydration,
|
withIncrementalHydration,
|
||||||
} from '@angular/platform-browser';
|
} from '@angular/platform-browser';
|
||||||
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -18,5 +19,6 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideZonelessChangeDetection(),
|
provideZonelessChangeDetection(),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideClientHydration(withEventReplay(), withIncrementalHydration()),
|
provideClientHydration(withEventReplay(), withIncrementalHydration()),
|
||||||
|
provideAnimationsAsync(),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -3,14 +3,14 @@ import { UserInputService } from '../../services/user-input-service';
|
|||||||
import { MatCard, MatCardContent } from '@angular/material/card';
|
import { MatCard, MatCardContent } from '@angular/material/card';
|
||||||
import { CalendarMonth } from '../calendar-month/calendar-month';
|
import { CalendarMonth } from '../calendar-month/calendar-month';
|
||||||
import { CalendarService, SelectedDates } from '../../services/calendar-service';
|
import { CalendarService, SelectedDates } from '../../services/calendar-service';
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||||
import { CalendarSettingsService } from '../../services/calendar-settings-service';
|
import { CalendarSettingsService } from '../../services/calendar-settings-service';
|
||||||
import { DateCacheService } from '../../services/date-cache.service';
|
import { DateCacheService } from '../../services/date-cache.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-calendar-year',
|
selector: 'app-calendar-year',
|
||||||
imports: [MatCard, CalendarMonth, MatCardContent, CommonModule, MatProgressSpinner],
|
imports: [MatCard, CalendarMonth, MatCardContent, MatProgressSpinner],
|
||||||
templateUrl: './calendar-year.html',
|
templateUrl: './calendar-year.html',
|
||||||
styleUrl: './calendar-year.scss',
|
styleUrl: './calendar-year.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
<p>Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !</p>
|
<p>Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !</p>
|
||||||
<form [formGroup]="userInputForm">
|
<div class="listOfDays">
|
||||||
<mat-form-field appearance="fill">
|
@for (day of daysOff; track day.id) {
|
||||||
<mat-label>Combien de CP par an ?</mat-label>
|
<div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
|
||||||
<input matInput type="number" max="365" min="0" formControlName="CP" />
|
<app-day-input [day]="day" (deleteEntry)="removeFromList($index)"></app-day-input>
|
||||||
</mat-form-field>
|
</div>
|
||||||
<mat-form-field appearance="fill">
|
}
|
||||||
<mat-label>Combien de jours de RTT par an ?</mat-label>
|
<mat-card class="add-day-card mat-elevation-z2" (click)="addDay()">
|
||||||
<input matInput type="number" max="365" min="0" formControlName="RTT" />
|
<button matFab aria-label="Add entry">
|
||||||
</mat-form-field>
|
<mat-icon>add</mat-icon>
|
||||||
<mat-form-field appearance="fill">
|
</button>
|
||||||
<mat-label>Autres jours</mat-label>
|
<h3>Ajouter un jour de vacances</h3>
|
||||||
<input matInput type="number" max="365" min="0" formControlName="Others" />
|
</mat-card>
|
||||||
</mat-form-field>
|
</div>
|
||||||
</form>
|
|
||||||
@if (hasRemainingDays()) {
|
@if (hasRemainingDays()) {
|
||||||
<mat-card class="mat-elevation-z2 warning-card">
|
<mat-card class="mat-elevation-z2 warning-card">
|
||||||
<mat-card-content class="warning-content">
|
<mat-card-content class="warning-content">
|
||||||
|
|||||||
@@ -25,7 +25,61 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listOfDays {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-input-wrapper {
|
||||||
|
animation: slide-fade-in 200ms ease-out;
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-enter {
|
||||||
|
animation: slide-fade-in 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-leave {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
transition:
|
||||||
|
opacity 200ms ease-in,
|
||||||
|
transform 200ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-day-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-day-card h3 {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { provideZonelessChangeDetection } from '@angular/core';
|
import { provideZonelessChangeDetection } from '@angular/core';
|
||||||
import { UserInput } from './user-input';
|
import { UserInput } from './user-input';
|
||||||
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
|
||||||
describe('UserInput', () => {
|
describe('UserInput', () => {
|
||||||
let component: UserInput;
|
let component: UserInput;
|
||||||
@@ -9,7 +10,7 @@ describe('UserInput', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UserInput],
|
imports: [UserInput],
|
||||||
providers: [provideZonelessChangeDetection()],
|
providers: [provideZonelessChangeDetection(), provideAnimationsAsync()],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(UserInput);
|
fixture = TestBed.createComponent(UserInput);
|
||||||
|
|||||||
@@ -1,79 +1,61 @@
|
|||||||
import { Component, OnInit, OnDestroy, effect, computed } from '@angular/core';
|
import {
|
||||||
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
Component,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { UserInputService } from '../../services/user-input-service';
|
import { UserInputService } from '../../services/user-input-service';
|
||||||
import { VacationsNumber } from '../../services/calendar-service';
|
import { Subject } from 'rxjs';
|
||||||
import { debounceTime, takeUntil, Subject } from 'rxjs';
|
|
||||||
import { CalendarSettings } from '../calendar-settings/calendar-settings';
|
import { CalendarSettings } from '../calendar-settings/calendar-settings';
|
||||||
import { MatCard, MatCardContent } from '@angular/material/card';
|
import { MatCard, MatCardContent } from '@angular/material/card';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
|
import { DayInput } from '../userInput/day-input/day-input';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-input',
|
selector: 'app-user-input',
|
||||||
imports: [
|
imports: [
|
||||||
MatFormField,
|
|
||||||
MatLabel,
|
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
CalendarSettings,
|
CalendarSettings,
|
||||||
MatCard,
|
MatCard,
|
||||||
MatCardContent,
|
MatCardContent,
|
||||||
MatIcon,
|
MatIcon,
|
||||||
|
MatButtonModule,
|
||||||
|
DayInput,
|
||||||
],
|
],
|
||||||
templateUrl: './user-input.html',
|
templateUrl: './user-input.html',
|
||||||
styleUrl: './user-input.scss',
|
styleUrl: './user-input.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class UserInput implements OnInit, OnDestroy {
|
export class UserInput implements OnInit, OnDestroy {
|
||||||
constructor(private userInputService: UserInputService) {
|
private userInputService: UserInputService = inject(UserInputService);
|
||||||
// Watch for signal changes and update form
|
public daysOff = this.userInputService.vacationNumberSignal();
|
||||||
effect(() => {
|
|
||||||
const vacationData = this.userInputService.vacationNumberSignal();
|
|
||||||
this.userInputForm.patchValue(
|
|
||||||
{
|
|
||||||
CP: vacationData.cp,
|
|
||||||
RTT: vacationData.rtt,
|
|
||||||
Others: vacationData.other,
|
|
||||||
},
|
|
||||||
{ emitEvent: false },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
userInputForm: FormGroup = new FormGroup({
|
|
||||||
CP: new FormControl<number | null>(null),
|
|
||||||
RTT: new FormControl<number | null>(null),
|
|
||||||
Others: new FormControl<number | null>(null),
|
|
||||||
});
|
|
||||||
|
|
||||||
protected remainingDays = computed(() => {
|
protected remainingDays = computed(() => {
|
||||||
const remaining = this.userInputService.remainingVacationDaysSignal();
|
const remaining = this.userInputService.remainingVacationDaysSignal();
|
||||||
return remaining.cp + remaining.rtt + remaining.other;
|
return remaining.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
protected hasRemainingDays = computed(() => this.remainingDays() > 0);
|
protected hasRemainingDays = computed(() => this.remainingDays() > 0);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {}
|
||||||
this.userInputForm.valueChanges
|
|
||||||
.pipe(debounceTime(1500), takeUntil(this.destroy$))
|
|
||||||
.subscribe(() => this.submitForm());
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private submitForm() {
|
public addDay() {
|
||||||
if (this.userInputForm.valid) {
|
this.daysOff.push(this.userInputService.createVacationDay());
|
||||||
const vacationData: VacationsNumber = {
|
}
|
||||||
cp: this.userInputForm.get('CP')?.value || 0,
|
|
||||||
rtt: this.userInputForm.get('RTT')?.value || 0,
|
public removeFromList(index: number) {
|
||||||
other: this.userInputForm.get('Others')?.value || 0,
|
this.daysOff.splice(index, 1);
|
||||||
};
|
|
||||||
this.userInputService.vacationNumberSignal.set(vacationData);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!-- <mat-list-item role="listitem"> -->
|
||||||
|
<mat-card class="card">
|
||||||
|
<mat-form-field class="numberInput">
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
[value]="day().numberOfDays"
|
||||||
|
(input)="onInput($event)"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="dayTypeInput">
|
||||||
|
<mat-select (valueChange)="dayChange.emit($event.value)" [value]="day().type">
|
||||||
|
<mat-option></mat-option>
|
||||||
|
@for (dayType of selectableDayTypes; track $index) {
|
||||||
|
<mat-option [value]="dayType">{{ tooltipTypeMapping[dayType] }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="dateInput">
|
||||||
|
<mat-label>Choose a date</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="datetime"
|
||||||
|
[matDatepicker]="picker"
|
||||||
|
[value]="day().expiryDate"
|
||||||
|
(dateChange)="addEvent($event)"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #picker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
<button matFab aria-label="Delete entry" (click)="deleteEntry.emit()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-card>
|
||||||
|
<!-- </mat-list-item> -->
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateInput {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numberInput {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DayInput } from './day-input';
|
||||||
|
|
||||||
|
describe('DayInput', () => {
|
||||||
|
let component: DayInput;
|
||||||
|
let fixture: ComponentFixture<DayInput>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DayInput],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DayInput);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
import { SelectableDayType, tooltipTypeMapping, VacDay } from '../../../services/calendar-service';
|
||||||
|
import { MatCard } from '@angular/material/card';
|
||||||
|
import { MatOption, MatSelect } from '@angular/material/select';
|
||||||
|
import { MatInput, MatInputModule, MatLabel } from '@angular/material/input';
|
||||||
|
import {
|
||||||
|
MatDatepicker,
|
||||||
|
MatDatepickerInput,
|
||||||
|
MatDatepickerInputEvent,
|
||||||
|
MatDatepickerToggle,
|
||||||
|
} from '@angular/material/datepicker';
|
||||||
|
import {
|
||||||
|
DateAdapter,
|
||||||
|
MAT_DATE_FORMATS,
|
||||||
|
MAT_DATE_LOCALE,
|
||||||
|
NativeDateAdapter,
|
||||||
|
} from '@angular/material/core';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
|
class CustomDateAdapter extends NativeDateAdapter {
|
||||||
|
override format(date: Date, displayFormat: unknown): string {
|
||||||
|
if (displayFormat === 'input') {
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
return `${day}/${month}/${year}`;
|
||||||
|
}
|
||||||
|
return super.format(date, displayFormat as object);
|
||||||
|
}
|
||||||
|
|
||||||
|
override parse(value: string): Date | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const parts = value.split('/');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const day = parseInt(parts[0], 10);
|
||||||
|
const month = parseInt(parts[1], 10) - 1;
|
||||||
|
const year = parseInt(parts[2], 10);
|
||||||
|
if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
|
||||||
|
return new Date(year, month, day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MY_DATE_FORMATS = {
|
||||||
|
parse: {
|
||||||
|
dateInput: 'input',
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
dateInput: 'input',
|
||||||
|
monthYearLabel: { year: 'numeric', month: 'short' },
|
||||||
|
dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },
|
||||||
|
monthYearA11yLabel: { year: 'numeric', month: 'long' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-day-input',
|
||||||
|
imports: [
|
||||||
|
MatIconModule,
|
||||||
|
MatCard,
|
||||||
|
MatSelect,
|
||||||
|
MatOption,
|
||||||
|
MatInput,
|
||||||
|
MatDatepicker,
|
||||||
|
MatLabel,
|
||||||
|
MatDatepickerToggle,
|
||||||
|
MatDatepickerInput,
|
||||||
|
MatInputModule,
|
||||||
|
MatButtonModule,
|
||||||
|
],
|
||||||
|
templateUrl: './day-input.html',
|
||||||
|
styleUrl: './day-input.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
providers: [
|
||||||
|
{ provide: DateAdapter, useClass: CustomDateAdapter },
|
||||||
|
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
|
||||||
|
{ provide: MAT_DATE_FORMATS, useValue: MY_DATE_FORMATS },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DayInput {
|
||||||
|
public day = input.required<VacDay>();
|
||||||
|
public dayChange = output<VacDay>();
|
||||||
|
public deleteEntry = output<void>();
|
||||||
|
|
||||||
|
protected readonly selectableDayTypes = Object.values(SelectableDayType);
|
||||||
|
protected readonly tooltipTypeMapping = tooltipTypeMapping;
|
||||||
|
|
||||||
|
onInput(event: Event) {
|
||||||
|
const value = Number((event.target as HTMLInputElement).value);
|
||||||
|
this.day().numberOfDays = value;
|
||||||
|
this.dayChange.emit(this.day());
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvent(event: MatDatepickerInputEvent<Date>) {
|
||||||
|
this.day().expiryDate = event.value!;
|
||||||
|
this.dayChange.emit(this.day());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,10 +184,11 @@ export class CalendarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VacationsNumber = {
|
export type VacDay = {
|
||||||
rtt: number;
|
id: string;
|
||||||
cp: number;
|
type: SelectableDayType;
|
||||||
other: number;
|
numberOfDays: number;
|
||||||
|
expiryDate: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Week = (Date | null)[];
|
export type Week = (Date | null)[];
|
||||||
@@ -203,6 +204,12 @@ export enum DayType {
|
|||||||
TODAY = 'today',
|
TODAY = 'today',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SelectableDayType {
|
||||||
|
CP = DayType.CP,
|
||||||
|
RTT = DayType.RTT,
|
||||||
|
OTHER = DayType.OTHER,
|
||||||
|
}
|
||||||
|
|
||||||
export const tooltipTypeMapping: { [key in DayType]: string } = {
|
export const tooltipTypeMapping: { [key in DayType]: string } = {
|
||||||
[DayType.WEEKEND]: 'Weekend',
|
[DayType.WEEKEND]: 'Weekend',
|
||||||
[DayType.CLOSED_DAY]: 'Jour Férié',
|
[DayType.CLOSED_DAY]: 'Jour Férié',
|
||||||
@@ -537,11 +544,11 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async optimizeVacations(
|
async optimizeVacations(
|
||||||
vacationsNumber: VacationsNumber,
|
vacationsNumber: any,
|
||||||
calendarSettingsService: CalendarSettingsService,
|
calendarSettingsService: CalendarSettingsService,
|
||||||
calendarService: CalendarService,
|
calendarService: CalendarService,
|
||||||
progressCallback?: (progress: number) => void,
|
progressCallback?: (progress: number) => void,
|
||||||
): Promise<VacationsNumber> {
|
): Promise<any> {
|
||||||
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);
|
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);
|
||||||
|
|
||||||
const initialVacations = { ...vacationsNumber };
|
const initialVacations = { ...vacationsNumber };
|
||||||
@@ -605,7 +612,7 @@ export class SelectedDates implements SelectedDateInterface {
|
|||||||
return vacationsNumber;
|
return vacationsNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
samediMalin(vacationsNumber: VacationsNumber): void {
|
samediMalin(vacationsNumber: any): void {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setHours(0, 0, 0, 0);
|
now.setHours(0, 0, 0, 0);
|
||||||
const nowTime = now.getTime();
|
const nowTime = now.getTime();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, signal } from '@angular/core';
|
import { Injectable, signal } from '@angular/core';
|
||||||
import { VacationsNumber } from './calendar-service';
|
import { SelectableDayType, VacDay } from './calendar-service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -9,18 +9,39 @@ export class UserInputService {
|
|||||||
return Math.floor(Math.random() * 9) + 1;
|
return Math.floor(Math.random() * 9) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInitialValues(): VacationsNumber {
|
private getInitialValues(): VacDay[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: SelectableDayType.CP,
|
||||||
|
numberOfDays: this.getRandomNumber(),
|
||||||
|
// today + 365 days
|
||||||
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: SelectableDayType.RTT,
|
||||||
|
numberOfDays: this.getRandomNumber(),
|
||||||
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: SelectableDayType.OTHER,
|
||||||
|
numberOfDays: this.getRandomNumber(),
|
||||||
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public createVacationDay(): VacDay {
|
||||||
return {
|
return {
|
||||||
cp: this.getRandomNumber(),
|
id: crypto.randomUUID(),
|
||||||
rtt: this.getRandomNumber(),
|
type: SelectableDayType.CP,
|
||||||
other: this.getRandomNumber(),
|
numberOfDays: 1,
|
||||||
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly vacationNumberSignal = signal<VacationsNumber>(this.getInitialValues());
|
readonly vacationNumberSignal = signal<VacDay[]>(this.getInitialValues());
|
||||||
readonly remainingVacationDaysSignal = signal<VacationsNumber>({
|
readonly remainingVacationDaysSignal = signal<VacDay[]>([]);
|
||||||
cp: 0,
|
|
||||||
rtt: 0,
|
|
||||||
other: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
|
import { provideZoneChangeDetection } from "@angular/core";
|
||||||
import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
|
import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { App } from './app/app';
|
import { App } from './app/app';
|
||||||
import { config } from './app/app.config.server';
|
import { config } from './app/app.config.server';
|
||||||
|
|
||||||
const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context);
|
const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, {...config, providers: [provideZoneChangeDetection(), ...config.providers]}, context);
|
||||||
|
|
||||||
export default bootstrap;
|
export default bootstrap;
|
||||||
Reference in new issue
Block a user