rajah 10 месяцев назад
Родитель
Сommit
5532dd3174

+ 2 - 0
src/app/app.routes.ts

@@ -36,6 +36,7 @@ import { ShowListComponent } from './composants/show-list/show-list.component';
 import { ShowLinksComponent } from './composants/show-links/show-links.component';
 import { ShowUploadComponent } from './composants/show-upload/show-upload.component';
 import { PollListComponent } from './composants/poll-list/poll-list.component';
+import { PollBoothComponent } from './composants/poll-booth/poll-booth.component';
 import { ResultListComponent } from './composants/result-list/result-list.component';
 
 export const routes: Routes = [
@@ -69,6 +70,7 @@ export const routes: Routes = [
   {path: 'show-links/:numeroCategorie', component: ShowLinksComponent, canActivate: [AdminGuard], runGuardsAndResolvers: 'always' },
   {path: 'show-upload/:numeroProduction', component: ShowUploadComponent, canActivate: [AdminGuard], runGuardsAndResolvers: 'always' },
   {path: 'poll-list', component: PollListComponent, canActivate: [UserGuard], runGuardsAndResolvers: 'always' },
+  {path: 'poll-booth/:numeroCategorie', component: PollBoothComponent, canActivate: [UserGuard], runGuardsAndResolvers: 'always' },
   {path: 'result-list', component: ResultListComponent, canActivate: [UserGuard], runGuardsAndResolvers: 'always' },
 ];
 

+ 3 - 0
src/app/composants/poll-booth/poll-booth.component.css

@@ -0,0 +1,3 @@
+#selectionChosen, #selectionDiscarded { width: 360px; min-height: 400px; overflow-x: auto; }
+
+.boutons { width: 90px; }

+ 69 - 0
src/app/composants/poll-booth/poll-booth.component.html

@@ -0,0 +1,69 @@
+<app-menu></app-menu>
+<div id="main">
+<div class="card shadow center">
+
+  <div class="card-header">
+    <span i18n>@if (nombreChoixRestant == -1) { Vos votes validés } @else { Voter pour les productions } dans la catégorie {{ categorie.libelle }}</span>
+  </div>
+  <div class="card-header shadow-sm">
+		<button type="button" (click)="goToListVotes()" class="btn bg-gradient btn-primary btn-sm" style="margin-right: 5px;"><i class="fa-solid fa-xmark"></i>&nbsp;<span i18n>Retour</span></button>
+    @if (nombreChoixRestant != -1) { <button type="button" [disabled]="chosenProductions.length == 0" class="btn bg-gradient btn-danger btn-sm" style="margin-right: 5px;" data-bs-toggle="modal" data-bs-target="#modalValider"><i class="fa-solid fa-check"></i>&nbsp;<span i18n>Valider</span></button> }
+	</div>
+  <div class="card-body hstack">
+    <div><small><span i18n>Productions préférées</span></small></div>
+    <div class="ms-auto"><small><span i18n>@if ((nombreChoixRestant > 0) && (nombreChoixRestant <= linkedProductions.length)) { Encore {{ nombreChoixRestant }} choix @if (nombreChoixRestant > 1) { possibles } @else { possible } } @else { Productions présentées }</span></small></div>
+  </div>
+  <div class="card-body"><div class="hstack align-items-start">
+
+    <div class="col">
+      <select class="form-select-sm" size="16" id="selectionChosen" #selecteurChoisis (change)="changeEtatBoutonsChosen($event)">
+      {{ resetChiffre() }} @for (prod of chosenProductions; track prod.numeroProduction) {
+        <option [value]="prod.numeroProduction" [disabled]="nombreChoixRestant == -1">{{ nextChiffre() }} - {{ prod.titre }} - {{ prod.auteurs }} / {{ prod.groupes }}</option>
+      }
+      </select>
+    </div>
+
+    <div class="col mx-auto" style="padding:7px;">
+      <br/><br/>
+      <button (click)="choisirProduction()" #boutonChoisir class="btn bg-gradient btn-primary btn-sm boutons disabled"><i class="fa-solid fa-arrow-left"></i>&nbsp;<span i18n>Choisir</span></button>
+      <hr/>
+      <button (click)="ecarterProduction()" #boutonEcarter class="btn bg-gradient btn-primary btn-sm boutons disabled"><i class="fa-solid fa-arrow-right"></i>&nbsp;<span i18n>Ecarter</span></button>
+      <br/><br/>
+      <button (click)="avancerProduction()" #boutonAvancer class="btn bg-gradient btn-primary btn-sm boutons disabled"><i class="fa-solid fa-arrow-up"></i>&nbsp;<span i18n>Avancer</span></button>
+      <br/><br/>
+      <button (click)="reculerProduction()" #boutonReculer class="btn bg-gradient btn-primary btn-sm boutons disabled"><i class="fa-solid fa-arrow-down"></i>&nbsp;<span i18n>Reculer</span></button>
+    </div>
+
+    <div class="col">
+      <select class="form-select-sm" size="16" id="selectionDiscarded" #selecteurProposes (change)="changeEtatBoutonsDiscarded($event)">
+      {{ resetLettre() }} @for (prod of linkedProductions; track prod.numeroProduction) {
+        <option [value]="prod.numeroProduction" [disabled]="dejaChoisi(prod.numeroProduction)" [selected]="(numeroProductionPropose === prod.numeroProduction) && (!dejaChoisi(prod.numeroProduction))">{{ nextLettre() }} - {{ prod.titre }} - {{ prod.auteurs }} / {{ prod.groupes }}</option>
+      }
+      </select>
+    </div>
+  </div></div>
+</div>
+
+<div class="modal fade" id="modalValider" tabindex="-1" aria-labelledby="modalValiderTitre" aria-hidden="true">
+	<div class="modal-dialog modal-dialog-centered" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+        <h5 class="modal-title text-danger" id="modalEffacerTitre"><span i18n>Valider</span></h5>
+			</div>
+      <div class="modal-body">
+        <span i18n>
+          La validation permet de geler vos choix pour cette catégorie et empêche toute modification par la suite.
+          <span class="text-secondary">Ceci est une sécurité au cas où un tiers malveillant viendrait utiliser de votre session encore ouverte en votre absence.</span>
+          <br/>
+          Cette action n'est pas obligatoire, une validation automatique sera effectuée au moment de la clôture des urnes.
+          @if ((nombreChoixRestant > 0) && (nombreChoixRestant <= linkedProductions.length)) { <br/><br/><span class="text-success">Vous pouvez encore choisir {{ nombreChoixRestant }} production@if(nombreChoixRestant > 1){s}.</span> }
+        </span>
+      </div>
+			<div class="modal-footer">
+				<button type="button" class="btn bg-gradient btn-secondary btn-sm" data-bs-dismiss="modal"><span i18n>Annuler</span></button>
+        <button type="button" class="btn bg-gradient btn-danger btn-sm" (click)="validerVote()" data-bs-dismiss="modal"><span i18n>Confirmer</span></button>
+			</div>
+		</div>
+	</div>
+</div>
+</div>

+ 123 - 0
src/app/composants/poll-booth/poll-booth.component.ts

@@ -0,0 +1,123 @@
+import { Component, OnInit, ViewChild, ElementRef, Renderer2 } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { FormsModule, NgForm } from '@angular/forms';
+
+import { MenuComponent } from '../menu/menu.component';
+import { Categorie } from '../../interfaces/categorie';
+import { CategorieService } from '../../services/categorie.service';
+import { ProductionItem, ProductionEnum } from '../../interfaces/production';
+import { PresentationService } from '../../services/presentation.service';
+import { BulletinService } from '../../services/bulletin.service';
+
+@Component({ selector: 'app-poll-booth', imports: [FormsModule, MenuComponent], templateUrl: './poll-booth.component.html', styleUrl: './poll-booth.component.css' })
+
+export class PollBoothComponent  implements OnInit
+{
+
+  @ViewChild('boutonChoisir', {static: false}) boutonChoisir!: ElementRef;
+  @ViewChild('boutonEcarter', {static: false}) boutonEcarter!: ElementRef;
+  @ViewChild('boutonAvancer', {static: false}) boutonAvancer!: ElementRef;
+  @ViewChild('boutonReculer', {static: false}) boutonReculer!: ElementRef;
+
+  @ViewChild('selecteurChoisis', {static: false}) selecteurChoisis!: ElementRef;
+  @ViewChild('selecteurProposes', {static: false}) selecteurProposes!: ElementRef;
+
+  numeroCategorie: number = 0;
+  categorie: Categorie = new Categorie();
+
+  numeroProductionChoisie: number = 0;
+  chosenProductions: ProductionItem[] = [];
+
+  numeroProductionPropose: number = 0;
+  linkedProductions: ProductionItem[] = [];
+
+  nombreChoixRestant: number = 0;
+
+  constructor(
+    private categorieService: CategorieService,
+    private presentationService: PresentationService,
+    private bulletinService: BulletinService,
+    private route: ActivatedRoute,
+    private router: Router,
+    private menu: MenuComponent,
+    private renderer: Renderer2
+  ) { }
+
+  ngOnInit()
+  {
+    this.numeroCategorie = this.route.snapshot.params['numeroCategorie'];
+    this.categorie = new Categorie();
+    this.categorieService.getByIdCategorie(this.numeroCategorie).subscribe( data => { this.categorie = data; });
+
+    this.retreiveDatas();
+    this.resetEtatBoutonsDiscarded();
+    this.resetEtatBoutonsChosen();
+  }
+
+  private retreiveDatas() { this.bulletinService.getRemainingChoices(this.numeroCategorie).subscribe(ret => { this.nombreChoixRestant = Number('' + ret); this.retreiveDatas1(); }); }
+  private retreiveDatas1() { this.bulletinService.getChosenProductions(this.numeroCategorie).subscribe(data => { this.chosenProductions = data; this.retreiveDatas2(); }); }
+  private retreiveDatas2() { this.presentationService.getLinkedProductions(this.numeroCategorie).subscribe(data => { this.linkedProductions = data; this.resetSelections(); }); }
+  private resetSelections() { if (this.selecteurProposes) { this.selecteurProposes.nativeElement.selectedIndex = -1; } }
+
+  indexChiffre: number = 0;
+
+  resetChiffre() { this.indexChiffre = 0; }
+  nextChiffre(): string { if (this.indexChiffre >= 0) { this.indexChiffre++; return "#" + (this.indexChiffre);  } return ""; }
+
+  lettresOrdre: string[] = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
+  indexLettre: number = 0;
+
+  resetLettre() { this.indexLettre = 0; }
+  nextLettre(): string { if ((this.indexLettre >= 0) && (this.indexLettre < 26)) { this.indexLettre++; return "#" + this.lettresOrdre[this.indexLettre - 1];  } return ""; }
+
+  dejaChoisi(id: number): boolean { if (this.chosenProductions.length > 0) { for (let i = 0; i < this.chosenProductions.length; i++) { if (this.chosenProductions[i].numeroProduction == id) { return true; } } } return false; }
+
+  goToListVotes() { this.router.navigate(['/poll-list']); }
+
+  changeEtatBoutonsDiscarded(event: any)
+  {
+    if (this.nombreChoixRestant == -1) { this.resetEtatBoutonsDiscarded(); return; }
+
+    this.numeroProductionPropose = event.target.value;
+
+    var b: boolean = (this.numeroProductionPropose == 0);
+
+    if (this.boutonChoisir) { if (b) { this.renderer.addClass(this.boutonChoisir.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonChoisir.nativeElement, 'disabled'); } }
+  }
+  resetEtatBoutonsDiscarded()
+  {
+    if (this.boutonChoisir) { this.renderer.addClass(this.boutonChoisir.nativeElement, 'disabled'); }
+    this.numeroProductionPropose = 0;
+  }
+
+  changeEtatBoutonsChosen(event: any)
+  {
+    if (this.nombreChoixRestant == -1) { this.resetEtatBoutonsChosen(); return; }
+
+    this.numeroProductionChoisie = event.target.value;
+
+    var b: boolean = (this.numeroProductionChoisie == 0);
+
+    if (this.boutonEcarter) { if (b) { this.renderer.addClass(this.boutonEcarter.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonEcarter.nativeElement, 'disabled'); } }
+    if (this.boutonAvancer) { if (b) { this.renderer.addClass(this.boutonAvancer.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonAvancer.nativeElement, 'disabled'); } }
+    if (this.boutonReculer) { if (b) { this.renderer.addClass(this.boutonReculer.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonReculer.nativeElement, 'disabled'); } }
+  }
+  resetEtatBoutonsChosen()
+  {
+    if (this.boutonEcarter) { this.renderer.addClass(this.boutonEcarter.nativeElement, 'disabled'); }
+    if (this.boutonAvancer) { this.renderer.addClass(this.boutonAvancer.nativeElement, 'disabled'); }
+    if (this.boutonReculer) { this.renderer.addClass(this.boutonReculer.nativeElement, 'disabled'); }
+    this.numeroProductionChoisie = 0;
+  }
+
+  choisirProduction() { if (this.numeroProductionPropose > 0) { this.bulletinService.choisirProduction(this.numeroCategorie, this.numeroProductionPropose).subscribe(() => { this.retreiveDatas(); this.resetEtatBoutonsDiscarded(); }); } }
+
+  ecarterProduction() { if (this.numeroProductionChoisie > 0) { this.bulletinService.ecarterProduction(this.numeroCategorie, this.numeroProductionChoisie).subscribe(() => { this.retreiveDatas(); this.resetEtatBoutonsChosen(); }); } }
+
+  avancerProduction() { if (this.numeroProductionChoisie > 0) { this.bulletinService.avancerProduction(this.numeroCategorie, this.numeroProductionChoisie).subscribe(() => { this.retreiveDatas(); }); } }
+
+  reculerProduction() { if (this.numeroProductionChoisie > 0) { this.bulletinService.reculerProduction(this.numeroCategorie, this.numeroProductionChoisie).subscribe(() => { this.retreiveDatas(); }); } }
+
+  validerVote() { if (this.linkedProductions.length > 0) { this.bulletinService.validateChoices(this.numeroCategorie).subscribe(msg => { if (msg.information) { this.retreiveDatas(); } }); } }
+
+}

+ 2 - 6
src/app/composants/poll-list/poll-list.component.html

@@ -14,18 +14,14 @@
   			<thead class="thead-dark">
   				<tr>
             <th class="fs-6 label-nobr"><small><span i18n>Catégorie</span></small></th>
-  					<th class="fs-6 label-nobr"><small><span i18n>Vote ouvert</span></small></th>
-  					<th class="fs-6 label-nobr"><small><span i18n>Vote terminé</span></small></th>
   				</tr>
   			</thead>
   			<tbody>
-  				@for (categorie of categories; track categorie.numeroCategorie) {
+  				@for (categorie of categories; track categorie.numeroCategorie) { @if (categorie.pollable) {
   				<tr>
             <td class="label-nobr"><a (click)="voteCategorie(categorie.numeroCategorie)" class="link-primary pointeur-souris text-decoration-none">{{ categorie.libelle }}</a>&nbsp;&nbsp;</td>
-  					<td>@if (categorie.pollable) { <i class="fa-solid fa-circle-check text-success"></i> } @else { <i class="fa-solid fa-circle-xmark text-danger"></i> }&nbsp;&nbsp;</td>
-  					<td>@if (categorie.computed) { <i class="fa-solid fa-circle-check text-success"></i> } @else { <i class="fa-solid fa-circle-xmark text-danger"></i> }&nbsp;&nbsp;</td>
   				</tr>
-  				}
+          } }
   			</tbody>
   		</table>
   	</div>

+ 1 - 1
src/app/composants/poll-list/poll-list.component.ts

@@ -24,6 +24,6 @@ export class PollListComponent implements OnInit
 
   goToRefreshListCategorie() { this.retreiveDatas(); }
 
-  voteCategorie(id: number) { this.router.navigate(['/poll-details', id]); }
+  voteCategorie(id: number) { this.router.navigate(['/poll-booth', id]); }
 
 }

+ 5 - 5
src/app/composants/show-links/show-links.component.html

@@ -12,17 +12,17 @@
     <div><small><span i18n>Dans {{ categorie.libelle }}</span></small></div>
     <div class="ms-auto"><small><span i18n>Sans catégorie</span></small></div>
   </div>
-	<div class="card-body"><div class="row align-items-start">
+  <div class="card-body"><div class="hstack align-items-start">
 
     <div class="col">
-      <select class="form-select-sm" size="16" id="selectionLinked" (change)="changeEtatBoutonsLinked($event)">
-      {{ resetLettre() }} @for (prod of linkedProductions; track prod.numeroProduction) { 
+      <select class="form-select-sm" size="16" id="selectionLinked" #selecteurLinked (change)="changeEtatBoutonsLinked($event)">
+      {{ resetLettre() }} @for (prod of linkedProductions; track prod.numeroProduction) {
         <option [value]="prod.numeroProduction">{{ nextLettre() }} - {{ prod.titre }} - {{ prod.auteurs }} / {{ prod.groupes }}</option>
       }
       </select>
     </div>
 
-    <div class="col mx-auto">
+    <div class="col mx-auto" style="padding:7px;">
       <br/><br/>
       <button (click)="lierProduction()" #boutonLier class="btn bg-gradient btn-primary btn-sm boutons disabled"><i class="fa-solid fa-arrow-left"></i>&nbsp;<span i18n>Lier</span></button>
       <hr/>
@@ -34,7 +34,7 @@
     </div>
 
     <div class="col">
-      <select class="form-select-sm" size="16" id="selectionUnlinked" (change)="changeEtatBoutonsUnlinked($event)">
+      <select class="form-select-sm" size="16" id="selectionUnlinked" #selecteurUnlinked (change)="changeEtatBoutonsUnlinked($event)">
       @for (prod of unlinkedProductions; track prod.numeroProduction) {
         <option [value]="prod.numeroProduction"> {{ prod.titre }} - {{ prod.auteurs }} / {{ prod.groupes }}</option>
       }

+ 9 - 6
src/app/composants/show-links/show-links.component.ts

@@ -18,6 +18,9 @@ export class ShowLinksComponent implements OnInit
   @ViewChild('boutonAvancer', {static: false}) boutonAvancer!: ElementRef;
   @ViewChild('boutonReculer', {static: false}) boutonReculer!: ElementRef;
 
+  @ViewChild('selecteurLinked', {static: false}) selecteurLinked!: ElementRef;
+  @ViewChild('selecteurUnlinked', {static: false}) selecteurUnlinked!: ElementRef;
+
   numeroCategorie: number = 0;
   categorie: Categorie = new Categorie();
 
@@ -63,9 +66,9 @@ export class ShowLinksComponent implements OnInit
   {
     this.numeroProduction = event.target.value;
 
-    var b: boolean = (this.numeroProduction != 0);
+    var b: boolean = (this.numeroProduction == 0);
 
-    if (this.boutonLier) { if (b) { this.renderer.removeClass(this.boutonLier.nativeElement, 'disabled'); } else { this.renderer.addClass(this.boutonLier.nativeElement, 'disabled'); } }
+    if (this.boutonLier) { if (b) { this.renderer.addClass(this.boutonLier.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonLier.nativeElement, 'disabled'); } }
   }
   resetEtatBoutonsUnlinked()
   {
@@ -76,11 +79,11 @@ export class ShowLinksComponent implements OnInit
   {
     this.numeroProduction = -event.target.value;
 
-    var b: boolean = (this.numeroProduction != 0);
+    var b: boolean = (this.numeroProduction == 0);
 
-    if (this.boutonRetirer) { if (b) { this.renderer.removeClass(this.boutonRetirer.nativeElement, 'disabled'); } else { this.renderer.addClass(this.boutonRetirer.nativeElement, 'disabled'); } }
-    if (this.boutonAvancer) { if (b) { this.renderer.removeClass(this.boutonAvancer.nativeElement, 'disabled'); } else { this.renderer.addClass(this.boutonAvancer.nativeElement, 'disabled'); } }
-    if (this.boutonReculer) { if (b) { this.renderer.removeClass(this.boutonReculer.nativeElement, 'disabled'); } else { this.renderer.addClass(this.boutonReculer.nativeElement, 'disabled'); } }
+    if (this.boutonRetirer) { if (b) { this.renderer.addClass(this.boutonRetirer.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonRetirer.nativeElement, 'disabled'); } }
+    if (this.boutonAvancer) { if (b) { this.renderer.addClass(this.boutonAvancer.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonAvancer.nativeElement, 'disabled'); } }
+    if (this.boutonReculer) { if (b) { this.renderer.addClass(this.boutonReculer.nativeElement, 'disabled'); } else { this.renderer.removeClass(this.boutonReculer.nativeElement, 'disabled'); } }
   }
   resetEtatBoutonsLinked()
   {

+ 64 - 0
src/app/services/bulletin.service.ts

@@ -0,0 +1,64 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http'
+import { Observable } from 'rxjs';
+
+import { Environnement } from '../env';
+import { ProductionItem } from '../interfaces/production';
+import { Message } from '../interfaces/divers';
+
+@Injectable({ providedIn: 'root' })
+
+export class BulletinService
+{
+
+  private baseURL = Environnement.apiUrl + "urne";
+
+  constructor(private httpClient: HttpClient) { }
+
+  getRemainingChoices(id: number): Observable<Object> { return this.httpClient.get<Object>(`${this.baseURL}/count/${id}`); }
+
+  validateChoices(id: number): Observable<Message> { return this.httpClient.get<Message>(`${this.baseURL}/validate/${id}`); }
+
+  getChosenProductions(id: number): Observable<ProductionItem[]> { return this.httpClient.get<ProductionItem[]>(`${this.baseURL}/list/${id}`); }
+
+  choisirProduction(id_cat: number, id_prod: number): Observable<Object>
+  {
+    let params = new HttpParams();
+
+    params = params.append('id_cat', id_cat);
+    params = params.append('id_prod', id_prod);
+
+    return this.httpClient.get(`${this.baseURL}/choose`, { params: params });
+  }
+
+  ecarterProduction(id_cat: number, id_prod: number): Observable<Object>
+  {
+    let params = new HttpParams();
+
+    params = params.append('id_cat', id_cat);
+    params = params.append('id_prod', id_prod);
+
+    return this.httpClient.get(`${this.baseURL}/discard`, { params: params });
+  }
+
+  avancerProduction(id_cat: number, id_prod: number): Observable<Object>
+  {
+    let params = new HttpParams();
+
+    params = params.append('id_cat', id_cat);
+    params = params.append('id_prod', id_prod);
+
+    return this.httpClient.get(`${this.baseURL}/up`, { params: params });
+  }
+
+  reculerProduction(id_cat: number, id_prod: number): Observable<Object>
+  {
+    let params = new HttpParams();
+
+    params = params.append('id_cat', id_cat);
+    params = params.append('id_prod', id_prod);
+
+    return this.httpClient.get(`${this.baseURL}/down`, { params: params });
+  }
+
+}