diff --git a/src/app/form/form.component.html b/src/app/form/form.component.html index 494f7d0199025917e57d0a4743c8a9021c873e89..2c73700d2a60d847c31febcdffe725e61da76431 100644 --- a/src/app/form/form.component.html +++ b/src/app/form/form.component.html @@ -144,7 +144,8 @@ </div> <div formGroupName="hours"> <p>Heures</p> - <div *ngFor="let day of weekDay | keyvalue"> + <app-hour-picker></app-hour-picker> + <!-- <div *ngFor="let day of weekDay | keyvalue"> <div [formGroupName]="day.key"> <p>Ouvert le {{ day.value }} ? :</p> <input type="radio" formControlName="open" (click)="addTime(day.key)" [value]="true" />Oui <br /> @@ -167,7 +168,7 @@ </div> </div> </div> - </div> + </div> --> </div> <p>Fermetures exceptionnelles</p> <input type="text" formControlName="exceptionalClosures" /> diff --git a/src/app/form/form.component.ts b/src/app/form/form.component.ts index 93ab67f337a7341b780192c3fc491270853aeb2e..96750559da0285a18692ccb2828db30613b1cee6 100644 --- a/src/app/form/form.component.ts +++ b/src/app/form/form.component.ts @@ -13,6 +13,7 @@ import { typeStructureEnum } from '../shared/enum/typeStructure.enum'; import { FonctionContactEnum } from '../shared/enum/fonctionContact.enum'; import { ProfileService } from '../profile/services/profile.service'; import { User } from '../models/user.model'; +import { Week } from '../models/week.model'; @Component({ selector: 'app-structureForm', @@ -25,6 +26,7 @@ export class FormComponent implements OnInit { @Input() public profile?: User; @Output() closeEvent = new EventEmitter<Structure>(); public structureForm: FormGroup; + public tmp = new Week(); public userAlreadyExist = false; diff --git a/src/app/shared/components/hour-picker/hour-picker.component.html b/src/app/shared/components/hour-picker/hour-picker.component.html new file mode 100644 index 0000000000000000000000000000000000000000..206db62500856b30351f2e785002c6e70572123d --- /dev/null +++ b/src/app/shared/components/hour-picker/hour-picker.component.html @@ -0,0 +1,176 @@ +<p> + Horaires d'ouverture :<br /> + + <span class="sub-text" #test> + Entrez les horaires généraux d'ouverture. Les horaires propres à chaque services proposés seront à remplir plus + loin. + </span> +</p> + +<div class="days"> + <p>la ?</p> + <div *ngFor="let day of structure.hours" (click)="activateDay(day)" class="day" [ngClass]="{ active: day.active }"> + <div + class="header-container sub-text" + [ngClass]="modifiedFields && modifiedFields.hours && modifiedFields.hours[day.name] ? 'modified' : ''" + > + <div class="header"> + <div> + {{ day.name | titlecase }} + </div> + + <div> + <img + *ngIf="copiedDayName === day.name" + (click)="cancelCopy()" + src="../../../../../assets/img/black/cross.svg" + matTooltip="Annuler la copie" + matTooltipClass="tooltip" + /> + <img + *ngIf="copiedDayName !== day.name && copiedDayName.length > 0" + (click)="paste(day)" + src="../../../../../assets/img/black/paste.svg" + matTooltip="Coller les heures copiées" + matTooltipClass="tooltip" + /> + <img + *ngIf="!copiedDayName" + (click)="copy(day)" + src="../../../../../assets/img/black/copy.svg" + matTooltip="Copier les heures" + matTooltipClass="tooltip" + /> + </div> + + <div> + <input + type="checkbox" + id="{{ day.name }}" + class="toggle-checkbox hidden" + (click)="toggleOpenDay(day, $event.target.checked)" + [checked]="day.open" + /> + <label for="{{ day.name }}" class="toggle-label"></label> + </div> + </div> + </div> + + <div *ngIf="!day.open"></div> + + <div *ngIf="day.open"> + <div class="active" *ngIf="day.active"> + <div class="hour" *ngFor="let hour of day.hours; let i = index"> + <div>De</div> + + <div class="input-container"> + <input type="time" [(ngModel)]="hour.start" (change)="checkHoursValid()" /> + </div> + + <div>à</div> + + <div class="input-container"> + <input type="time" [(ngModel)]="hour.end" (change)="checkHoursValid()" /> + </div> + + <div class="input-container"> + <select [(ngModel)]="hour.type"> + <option value="withoutAppointment">Présentiel (Sans rendez-vous)</option> + <option value="appointment">Présentiel (Avec rendez-vous)</option> + <option value="phone">Téléphonique</option> + </select> + </div> + + <img + src="../../../../../assets/img/black/add-full.svg" + *ngIf="day.hours.length === 1" + (click)="addHours(day)" + class="add" + /> + <img + src="../../../../../assets/img/black/cross.svg" + *ngIf="day.hours.length > 1" + (click)="removeHours(day, i)" + /> + + <div> + <div *ngIf="hour.error === 'incomplete'" class="warning-message">!</div> + <div *ngIf="hour.error === 'wrong'" class="error-message">?</div> + </div> + </div> + + <img + src="../../../../../assets/img/black/add-full.svg" + *ngIf="day.hours.length > 1 && day.hours.length < 5" + (click)="addHours(day)" + class="add" + /> + </div> + + <div class="inactive hour" *ngIf="!day.active"> + <div>De</div> + + <div> + <input type="time" [(ngModel)]="day.hours[0].start" (change)="checkHoursValid()" /> + </div> + + <div>à</div> + + <div> + <input type="time" [(ngModel)]="day.hours[0].end" (change)="checkHoursValid()" /> + </div> + + <div class="input-container"> + <select [(ngModel)]="day.hours[0].type"> + <option value="withoutAppointment">Présentiel (Sans rendez-vous)</option> + <option value="appointment">Présentiel (Avec rendez-vous)</option> + <option value="phone">Téléphonique</option> + </select> + </div> + + <img + src="../../../../../assets/img/black/add-full.svg" + *ngIf="day.hours.length === 1" + (click)="addHours(day)" + class="add" + /> + <div *ngIf="day.hours.length > 1">...</div> + + <div> + <div + *ngIf=" + day.hours[0].error === 'incomplete' || + (day.hours[1] && day.hours[1].error === 'incomplete') || + (day.hours[2] && day.hours[2].error === 'incomplete') || + (day.hours[3] && day.hours[3].error === 'incomplete') || + (day.hours[4] && day.hours[4].error === 'incomplete'); + else wrong + " + class="warning-message" + > + ! + </div> + <ng-template #wrong> + <div + *ngIf=" + day.hours[0].error === 'wrong' || + (day.hours[1] && day.hours[1].error === 'wrong') || + (day.hours[2] && day.hours[2].error === 'wrong') || + (day.hours[23] && day.hours[3].error === 'wrong') || + (day.hours[4] && day.hours[4].error === 'wrong') + " + class="error-message" + > + ? + </div> + </ng-template> + </div> + </div> + </div> + </div> +</div> + +<p class="legend"> + ! : Horaire incomplet<br /> + ? : Horaire incohérent +</p> diff --git a/src/app/shared/components/hour-picker/hour-picker.component.scss b/src/app/shared/components/hour-picker/hour-picker.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..1179c9cd8afda6e098a906e7702dd258c4049d5f --- /dev/null +++ b/src/app/shared/components/hour-picker/hour-picker.component.scss @@ -0,0 +1,69 @@ +.days { + display: grid; + row-gap: 15px; + + .day { + display: grid; + grid-template-columns: 175px 1fr; + column-gap: 20px; + + .header-container { + .header { + display: grid; + grid-template-columns: 1fr auto auto; + column-gap: 20px; + align-items: center; + height: 40px; + } + } + + .active { + display: grid; + row-gap: 10px; + } + + .hour { + height: 40px; + display: grid; + grid-template-columns: auto 100px auto 100px 1fr 30px 20px; + column-gap: 10px; + align-items: center; + justify-items: center; + } + } +} + +p { + margin-top: 0px; +} + +img { + cursor: pointer; + height: 15px; + width: 15px; + &.add { + height: 20px; + width: 20px; + } +} + +.modified { + border-left: 3px solid red; + padding-left: 8px; + margin-left: -11px; + border-radius: 3px; +} + +.warning-message, +.error-message { + font-weight: bold; + font-size: 1em; + display: grid; + align-items: center; +} + +.legend { + font-weight: normal; + font-style: italic; + text-align: right; +} diff --git a/src/app/shared/components/hour-picker/hour-picker.component.spec.ts b/src/app/shared/components/hour-picker/hour-picker.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e05b8606443aecf6462165b4464c22dc7b4f2ba9 --- /dev/null +++ b/src/app/shared/components/hour-picker/hour-picker.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HourPickerComponent } from './hour-picker.component'; + +describe('HourPickerComponent', () => { + let component: HourPickerComponent; + let fixture: ComponentFixture<HourPickerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HourPickerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HourPickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/hour-picker/hour-picker.component.ts b/src/app/shared/components/hour-picker/hour-picker.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb50fd442bd0e1761b6d010fcb4a89bc8cc47fe5 --- /dev/null +++ b/src/app/shared/components/hour-picker/hour-picker.component.ts @@ -0,0 +1,259 @@ +import { Component, Input, Output, EventEmitter, OnDestroy, ViewChild, OnChanges, OnInit } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; +import * as _ from 'lodash'; +import { Structure } from '../../../models/structure.model'; +import { Time } from '../../../models/time.model'; +import { Week } from '../../../models/week.model'; + +@Component({ + selector: 'app-hour-picker', + templateUrl: './hour-picker.component.html', + styleUrls: ['./hour-picker.component.scss'], +}) +export class HourPickerComponent implements OnChanges, OnDestroy { + @ViewChild('test', { static: true }) test; + @Input() modifiedFields: any; + // @Input() structure: any; + + @Output() updateHoursError = new EventEmitter<{ badHoursFormat: boolean }>(); + + private copiedDay: any; + public copiedDayName = ''; + public structure = { + hours: [ + { + name: 'Lundi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Mardi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Mercredi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Jeudi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Vendredi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Samedi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Dimanche', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + ], + }; + public structureHoursDefault: any[] = [ + { + name: 'Lundi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Mardi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Mercredi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Jeudi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Vendredi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Samedi', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + { + name: 'Dimanche', + hours: [{ start: '', end: '', type: 'withoutAppointment', error: 'incomplete' }], + open: false, + active: false, + }, + ]; + + ngOnChanges(): void { + this.formatHoursForEdition(); + } + + ngOnDestroy(): void { + this.formatHoursForSave(); + } + + /** + * Intégrer les horaires dans les horaires par défaut du composant + */ + formatHoursForEdition() { + console.log('formatHoursForEdition'); + if (this.structure.hours) { + for (const dayDefault of this.structureHoursDefault) { + const foundDay = this.structure.hours.filter((day) => day.name === dayDefault.name); + + // if (foundDay.length && !foundDay[0].error) { + if (foundDay.length) { + foundDay[0].open = true; + } else if (!foundDay.length) { + this.structure.hours.push(dayDefault); + } + } + } else { + this.structure.hours = this.structureHoursDefault; + } + } + + /** + * Formater les horaires pour l'enregistrement en base : + * supprimer les données inutiles + */ + formatHoursForSave(): any { + console.log('formatHoursForSave'); + if (!this.structure.hours) { + return; + } + + this.structure.hours = this.structure.hours.filter((day) => day.open === true); + + for (const day of this.structure.hours) { + delete day.open; + delete day.active; + for (const hour of day.hours) { + delete hour.error; + } + } + } + + activateDay(day: any): void { + console.log('activateDay'); + this.structure.hours.forEach((dayHours) => { + dayHours.active = false; + }); + day.active = true; + } + + toggleOpenDay(day: any, value: any): void { + day.open = value; + + this.checkHoursValid(); + } + + /** + * Ajouter une ligne d'horaires à un jour + */ + addHours(day: any): void { + if (day.hours.length >= 5) { + return; + } + + day.hours.push({ + start: '', + end: '', + type: 'withoutAppointment', + error: 'incomplete', + }); + + this.checkHoursValid(); + } + + /** + * Supprimer la dernière ligne d'horaires d'un jour + */ + removeHours(day: any, index: number): void { + if (index > -1) { + day.hours.splice(index, 1); + } + } + + /** + * Copier les horaires d'un jour pour les coller par dessus les horaires d'un autre jour + */ + copy(day): void { + this.copiedDayName = day.name; + this.copiedDay = day; + } + + /** + * Remplacer les horaires d'un jour par les horaires copiés précédemment + */ + paste(day): void { + day.hours = JSON.parse(JSON.stringify(this.copiedDay.hours)); + day.open = this.copiedDay.open; + } + + /** + * Annuler la copie des horaires + */ + cancelCopy(): void { + this.copiedDayName = ''; + this.copiedDay = null; + } + + /** + * Vérifier que le format des horaires est correct + */ + checkHoursValid() { + let error = false; + + console.log('checkHoursValid'); + for (const day of this.structure.hours) { + if (day.open) { + for (const hour of day.hours) { + if (hour.start === '' || hour.end === '') { + hour.error = 'incomplete'; + error = true; + } else if (hour.end <= hour.start) { + hour.error = 'wrong'; + error = true; + } else { + hour.error = null; + } + } + } + } + + // Émettre l'erreur à ajouter au formulaire pour autoriser + // ou empêcher de passer à l'étape suivante + if (error) { + this.updateHoursError.emit({ badHoursFormat: true }); + } else { + this.updateHoursError.emit(null); + } + } +} diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index e4d55982fd57f00574f4456459841eed11994a92..2b6cc7e465a49e9c6ee054ea763ade5378967c0a 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -7,6 +7,7 @@ import { SvgIconComponent } from './svg-icon/svg-icon.component'; import { ValidatorFormComponent } from './validator-form/validator-form.component'; import { CreateAccountFormComponent } from './create-account-form/create-account-form.component'; import { AddressAutocompleteComponent } from './address-autocomplete/address-autocomplete.component'; +import { HourPickerComponent } from './hour-picker/hour-picker.component'; // tslint:disable-next-line: max-line-length export { @@ -19,6 +20,7 @@ export { SignInModalComponent, CreateAccountFormComponent, AddressAutocompleteComponent, + HourPickerComponent, }; // tslint:disable-next-line:variable-name @@ -32,4 +34,5 @@ export const SharedComponents = [ SignInModalComponent, CreateAccountFormComponent, AddressAutocompleteComponent, + HourPickerComponent, ]; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 2709454bdff75e2077f2c0fc772a1b073fa58c03..c0829cf6b1ac5f9abd45de845b404f473f5ed840 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -8,9 +8,17 @@ import { SharedPipes } from './pipes'; import { SharedDirectives } from './directives'; import { SvgIconComponent } from './components/svg-icon/svg-icon.component'; import { AddressAutocompleteComponent } from './components/address-autocomplete/address-autocomplete.component'; +import { HourPickerComponent } from './components/hour-picker/hour-picker.component'; @NgModule({ - imports: [CommonModule, RouterModule, FlexLayoutModule, ReactiveFormsModule], - declarations: [...SharedPipes, ...SharedComponents, ...SharedDirectives, SvgIconComponent, AddressAutocompleteComponent], + imports: [CommonModule, FormsModule, RouterModule, FlexLayoutModule, ReactiveFormsModule], + declarations: [ + ...SharedPipes, + ...SharedComponents, + ...SharedDirectives, + SvgIconComponent, + AddressAutocompleteComponent, + HourPickerComponent, + ], exports: [ ...SharedPipes, ...SharedComponents,