funky-framadate-front/src/app/core/services/poll.service.ts

666 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Inject, Injectable } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { Answer } from '../enums/answer.enum';
import { Choice } from '../models/choice.model';
import { Poll } from '../models/poll.model';
import { ApiService } from './api.service';
import { ToastService } from './toast.service';
import { UserService } from './user.service';
import { UuidService } from './uuid.service';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { StorageService } from './storage.service';
import { Title } from '@angular/platform-browser';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DOCUMENT } from '@angular/common';
import { DateChoice, TimeSlices } from '../models/dateChoice.model';
import { DateUtilitiesService } from './date.utilities.service';
import { Owner } from '../models/owner.model';
import { Stack } from '../models/stack.model';
import { Vote } from '../models/vote.model';
@Injectable({
providedIn: 'root',
})
export class PollService implements Resolve<Poll> {
public _poll: BehaviorSubject<Poll | undefined> = new BehaviorSubject<Poll | undefined>(undefined);
public readonly poll: Observable<Poll | undefined> = this._poll.asObservable();
public form: FormGroup;
public startDateInterval: string;
public endDateInterval: string;
public intervalDays: number = 1;
public intervalDaysDefault = 7;
public dateList: DateChoice[] = []; // sets of days as strings, config to set identical time for days in a special days poll
public timeList: TimeSlices[] = []; // ranges of time expressed as strings
public previousRouteName: string = '/administration';
public nextRouteName: string = '/administration/step/2';
public step_current: number = 1;
public step_max: number = 5;
public round: Function;
public pass_hash: string;
public admin_key: string;
public urlPrefix: string = window.location.origin;
public advancedDisplayEnabled = false;
public showDateInterval = false;
public allowSeveralHours = false;
public richTextMode = false;
public calendar: any;
constructor(
private http: HttpClient,
private router: Router,
private apiService: ApiService,
private storageService: StorageService,
private userService: UserService,
private uuidService: UuidService,
private toastService: ToastService,
private titleService: Title,
public DateUtilitiesService: DateUtilitiesService,
public route: ActivatedRoute,
@Inject(DOCUMENT) private document: any,
private fb: FormBuilder
) {
this.createFormGroup();
if (environment.autofill_creation) {
this.setDemoValues();
this.toastService.display('auto fill de création fait');
} else {
this.calendar = [new Date()];
}
}
/**
* add example values to the form
*/
setDemoValues(): void {
this.addChoice('orange');
this.addChoice('raisin');
this.addChoice('abricot');
this.calendar = [
this.DateUtilitiesService.addDaysToDate(1, new Date()),
this.DateUtilitiesService.addDaysToDate(2, new Date()),
this.DateUtilitiesService.addDaysToDate(3, new Date()),
];
this.form.patchValue({
title: 'mon titre',
description: 'répondez SVP <3 ! *-* ',
custom_url: this.uuidService.getUUID(),
creatorPseudo: 'Chuck Norris',
creatorEmail: 'chucknorris@example.com',
isAboutDate: true,
whoModifiesAnswers: 'everybody',
whoCanChangeAnswers: 'everybody',
isProtectedByPassword: false,
richTextMode: false,
areResultsPublic: true,
expiracyNumberOfDays: 60,
});
this.automaticSlug();
}
/**
* set the poll slug from other data of the poll
*/
automaticSlug() {
this.form.patchValue({ custom_url: this.makeSlug(this.form) });
}
public createFormGroup() {
let form = this.fb.group({
title: ['', [Validators.required, Validators.minLength(12)]],
creatorPseudo: ['', [Validators.required]],
created_at: [new Date(), [Validators.required]],
creatorEmail: ['', [Validators.required]],
custom_url: [this.uuidService.getUUID(), [Validators.required]],
description: ['', [Validators.required]],
password: ['', [Validators.required]],
choices: new FormArray([]),
whoModifiesAnswers: ['', [Validators.required]],
whoCanChangeAnswers: ['', [Validators.required]],
isAboutDate: [true, [Validators.required]],
startDateInterval: ['', [Validators.required]],
endDateInterval: ['', [Validators.required]],
expiresDaysDelay: ['', [Validators.required]],
maxCountOfAnswers: ['', [Validators.required]],
isZeroKnoledge: [false, [Validators.required]],
isProtectedByPassword: [false, [Validators.required]],
isOwnerNotifiedByEmailOnNewVote: [true, [Validators.required]],
isOwnerNotifiedByEmailOnNewComment: [true, [Validators.required]],
areResultsPublic: [true, [Validators.required]],
richTextMode: [false, [Validators.required]],
isYesAnswerAvailable: [true, [Validators.required]],
isMaybeAnswerAvailable: [true, [Validators.required]],
isNoAnswerAvailable: [true, [Validators.required]],
allowComments: [true, [Validators.required]],
hasMaxCountOfAnswers: [300, [Validators.required]],
useVoterUniqueLink: [false, [Validators.required]],
voterEmailList: ['', [Validators.required]],
allowNewDateTime: [60, [Validators.required, Validators.min(0)]],
});
this.form = form;
return form;
}
public updateSlug(): void {
console.log('this.form.value', this.form.value);
this.form.patchValue({ custom_url: this.makeSlug(this.form) });
}
/**
* auto fetch a poll when route is looking for one in the administration pattern
* @param route
* @param state
*/
public async resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Poll> {
console.log('resolve route,state', route, state);
const segments: string[] = state.url.split('/');
const wantedcustom_url: string = segments.includes('poll') ? segments[segments.indexOf('poll') + 1] : '';
if (
!this._poll.getValue() ||
!this._poll.getValue().custom_url ||
this._poll.getValue().custom_url !== wantedcustom_url
) {
if (this.pass_hash) {
this.storageService.vote_stack.pass_hash = this.pass_hash;
await this.loadPollByCustomUrlWithPasswordHash(wantedcustom_url, this.pass_hash);
} else {
await this.loadPollByCustomUrl(wantedcustom_url);
}
}
const loadedPoll = this._poll.getValue();
if (loadedPoll) {
this.storageService.vote_stack.poll_custom_url = loadedPoll.custom_url;
return loadedPoll;
} else {
this.router.navigate(['page-not-found']);
return;
}
}
/**
* get all polls
*/
getAllAvailablePolls(): void {
const baseHref = environment.api.version.apiV1.baseHref;
console.log('getAllAvailablePolls baseHref', baseHref);
const headers = ApiService.makeHeaders();
console.log('getAllAvailablePolls headers', headers);
try {
this.http.get(`${baseHref}/poll`, headers).subscribe((res: Observable<any>) => {
console.log('getAllAvailablePolls res', res);
});
} catch (e) {
console.log('getAllAvailablePolls e', e);
}
}
public async loadPollByCustomUrl(custom_url: string): Promise<void> {
if (custom_url) {
const poll: Poll | undefined = await this.apiService.getPollByCustomUrl(custom_url);
if (poll) {
this.updateCurrentPoll(poll);
this.titleService.setTitle(`☑️ ${poll.title} - ${environment.appTitle}`);
} else {
this.toastService.display(`sondage ${custom_url} non trouvé`);
this.router.navigate(['page-not-found']);
}
} else {
this.toastService.display(`sondage sans custom url : ${custom_url}`);
}
}
public async loadPollByCustomUrlWithPasswordHash(custom_url: string, hash: string): Promise<void> {
if (custom_url) {
const poll: Poll | undefined = await this.apiService.getPollByCustomUrlWithHash(custom_url, hash);
if (poll) {
this.updateCurrentPoll(poll);
this.titleService.setTitle(`☑️ ${poll.title} - ${environment.appTitle}`);
} else {
this.toastService.display(`sondage ${custom_url} non trouvé`);
this.router.navigate(['page-not-found']);
}
} else {
this.toastService.display(`sondage sans custom url : ${custom_url}`);
}
}
/**
* update poll and parse its fields
* @param poll
*/
public updateCurrentPoll(poll: Poll): void {
console.log('this.storageService.vote_stack.id', this.storageService.vote_stack.id);
if (!this.storageService.vote_stack.id || this.storageService.vote_stack.poll_custom_url !== poll.custom_url) {
console.log('set base choices', poll.choices);
// set the choices only the first time the poll loads, or if we changed the poll
console.log(
'this.storageService.vote_stack.poll_custom_url',
this.storageService.vote_stack.poll_custom_url
);
// this.storageService.setChoicesForVoteStack(poll.choices);
}
this.toastService.display('sondage bien mis à jour', 'success');
this._poll.next(poll);
}
/**
* add all the dates between the start and end dates in the interval section
*/
addIntervalOfDates(): void {
const newIntervalArray = this.DateUtilitiesService.getDatesInRange(
this.DateUtilitiesService.parseInputDateToDateObject(new Date(this.startDateInterval)),
this.DateUtilitiesService.parseInputDateToDateObject(new Date(this.endDateInterval)),
1
);
const converted = [];
newIntervalArray.forEach((element) => {
converted.push({
literal: element.literal,
date_object: element.date_object,
timeList: [],
});
});
this.dateList = [...new Set(converted)];
// add only dates that are not already present with a Set of unique items
console.log('this.dateList', this.dateList);
this.showDateInterval = false;
this.form.patchValue({ choices: this.dateList });
this.toastService.display(`les dates ont été ajoutées aux réponses possibles.`);
}
/**
* handle keyboard shortcuts
* @param $event
* @param choice_number
*/
keyOnChoice($event: KeyboardEvent, choice_number: number): void {
$event.preventDefault();
const lastChoice = this.choices.length - 1 === choice_number;
// reset field with Ctrl + D
// add a field with Ctrl + N
// go to previous choice with arrow up
// go to next choice with arrow down
if ($event.key == 'ArrowUp' && choice_number > 0) {
this.focusOnChoice(choice_number - 1);
}
if ($event.key == 'ArrowDown') {
// add a field if we are on the last choice
if (lastChoice) {
this.addChoice();
this.toastService.display('choix ajouté par raccourci "flèche bas"');
} else {
this.focusOnChoice(choice_number + 1);
}
}
if ($event.ctrlKey && $event.key == 'Backspace') {
this.deleteChoiceField(choice_number);
this.toastService.display('choix supprimé par raccourci "Ctrl + retour"');
this.focusOnChoice(Math.min(choice_number - 1, 0));
}
if ($event.ctrlKey && $event.key == 'Enter') {
// go to other fields
const elem = this.document.querySelector('#creatorEmail');
if (elem) {
elem.focus();
}
}
}
/**
* change time spans
*/
addTime() {
this.timeList.push({
literal: '',
});
}
removeAllTimes() {
this.timeList = [];
}
resetTimes() {
this.timeList = [];
}
/**
* add a time period to a specific date choice,
* focus on the new input
* @param config
* @param id
*/
addTimeToDate(config: any, id: number) {
this.timeList.push({
literal: '',
});
const selector = '[ng-reflect-choice_label="dateTime_' + id + '_Choices_' + (this.timeList.length - 1) + '"]';
const elem = this.document.querySelector(selector);
if (elem) {
elem.focus();
}
}
public createPoll(): void {
console.log('this.form', this.form);
const newpoll = this.newPollFromForm(this.form);
this.apiService.createPoll(newpoll).then((resp) => {
console.log('poll created resp', resp);
console.log('TODO fill admin_key');
// this.admin_key = resp.data.admin_key;
});
}
/**
* default interval of dates proposed is from today to 7 days more
*/
setDefaultDatesForInterval(): void {
const dateCurrent = new Date();
const dateJson = dateCurrent.toISOString();
this.startDateInterval = dateJson.substring(0, 10);
this.endDateInterval = this.DateUtilitiesService.addDaysToDate(this.intervalDaysDefault, dateCurrent)
.toISOString()
.substring(0, 10);
this.form.patchValue({
startDateInterval: this.startDateInterval,
endDateInterval: this.endDateInterval,
});
this.countDays();
}
askInitFormDefault(): void {
this.initFormDefault(environment.autofill_creation);
this.toastService.display('formulaire réinitialisé');
}
countDays(): void {
this.intervalDays = this.DateUtilitiesService.countDays(
this.DateUtilitiesService.parseInputDateToDateObject(new Date(this.startDateInterval)),
this.DateUtilitiesService.parseInputDateToDateObject(new Date(this.endDateInterval))
);
}
focusOnChoice(index): void {
const selector = '#choice_label_' + index;
const elem = this.document.querySelector(selector);
if (elem) {
elem.focus();
}
}
deleteChoiceField(index: number): void {
if (this.choices.length !== 1) {
this.choices.removeAt(index);
}
}
initFormDefault(showDemoValues = true): void {
this.form = this.createFormGroup();
this.setDefaultDatesForInterval();
if (showDemoValues) {
this.setDemoValues();
}
}
get choices(): FormArray {
return this.form.get('choices') as FormArray;
}
reinitChoices(): void {
this.choices.setValue([]);
}
addChoice(optionalLabel = ''): void {
const newControlGroup = this.fb.group({
label: this.fb.control('', [Validators.required]),
imageUrl: ['', [Validators.required]],
});
if (optionalLabel) {
newControlGroup.patchValue({
label: optionalLabel,
imageUrl: 'mon url',
});
}
this.choices.push(newControlGroup);
this.focusOnChoice(this.choices.length - 1);
}
/**
* make a uniq slug for the current poll creation
* @param form
*/
makeSlug(form: FormGroup): string {
let str = '';
str =
form.value.created_at.getFullYear() +
'_' +
(form.value.created_at.getMonth() + 1) +
'_' +
form.value.created_at.getDate() +
'_' +
form.value.creatorPseudo +
'_' +
form.value.title;
str = str.replace(/^\s+|\s+$/g, ''); // trim
str = str.toLowerCase();
// remove accents, swap ñ for n, etc
const from = 'àáäâèéëêìíïîòóöôùúüûñç·/_,:;';
const to = 'aaaaeeeeiiiioooouuuunc------';
for (let i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
}
str = str
.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by -
.replace(/-+/g, '-'); // collapse dashes
return str + '-' + this.uuidService.getUUID();
}
public async saveCurrentPoll(): Promise<void> {
const pollUrl: Subscription = await this.apiService.createPoll(this._poll.getValue());
// TODO: Maybe handle the url to update currentPoll according to backend response
if (pollUrl) {
this.toastService.display('Le sondage a été enregistré.');
} else {
this.toastService.display('Le sondage na été correctement enregistré, veuillez ré-essayer.');
}
}
public saveParticipation(choice: Choice, user: Owner, response: Answer): void {
const currentPoll = this._poll.getValue();
currentPoll.choices.find((c) => c.name === choice.name)?.updateParticipation(user, response);
this.updateCurrentPoll(currentPoll);
this.apiService.createParticipation(currentPoll.custom_url, choice.name, user.pseudo, response);
this.toastService.display('Votre participation au sondage a été enregistrée.');
}
public async deleteAllAnswers(): Promise<void> {
await this.apiService.deletePollAnswers(this._poll.getValue().custom_url);
this.toastService.display('Les participations des votants à ce sondage ont été supprimées.');
}
public async addComment(comment: string): Promise<void> {
await this.apiService.createComment(this._poll.getValue().custom_url, comment);
this.toastService.display('Votre commentaire a été enregistré.');
}
public async deleteComments(): Promise<void> {
await this.apiService.deletePollComments(this._poll.getValue().custom_url);
this.toastService.display('Les commentaires de ce sondage ont été supprimés.');
}
public buildAnswersByChoiceLabelByPseudo(poll: Poll): Map<string, Map<string, Answer>> {
const pseudos: Set<string> = new Set();
poll.choices.forEach((choice: Choice) => {
choice.participants.forEach((users: Set<Owner>) => {
users.forEach((user: Owner) => {
pseudos.add(user.pseudo);
});
});
});
const list = new Map<string, Map<string, Answer>>();
pseudos.forEach((pseudo: string) => {
list.set(
pseudo,
new Map<string, Answer>(
poll.choices.map((choice: Choice) => {
return [choice.name, undefined];
})
)
);
});
poll.choices.forEach((choice: Choice) => {
choice.participants.forEach((users: Set<Owner>, answer: Answer) => {
users.forEach((user: Owner) => {
list.get(user.pseudo).set(choice.name, answer);
});
});
});
return list;
}
public getParticipationUrlFromForm(): string {
return `${environment.frontDomain}#/poll/${this.form.value.custom_url}/consultation`;
}
public getAdministrationUrlFromForm(): string {
// admin_key is filled after creation
return `${environment.frontDomain}#/admin/${this.admin_key}/consultation`;
}
public getParticipationUrl(): string {
// http://localhost:4200/#/poll/dessin-anime/consultation
// TODO handle secure access
// http://localhost:4200/#/poll/citron/consultation/secure/1c01ed9c94fc640a1be864f197ff808c
let url = '';
if (this._poll && this._poll.getValue) {
const polltemp = this._poll.getValue();
if (polltemp) {
url = `${environment.frontDomain}#/poll/${polltemp.custom_url}/consultation`;
}
} else {
url = `${environment.frontDomain}#/poll/${this.form.value.custom_url}/consultation`;
}
// TODO handle pass access
return url;
}
public getAdministrationUrl(): string {
// http://localhost:4200/#/admin/9S75b70ECXI5J5xDc058d3H40H9r2CHfO0Kj8T02EK2U8rY8fYTn-eS659j2Dhp794Oa6R1b9V70e3WGaE30iD9h45zwdm76C85SWB4LcUCrc7e0Ncc0
let url = '';
if (this._poll && this._poll.getValue) {
const polltemp = this._poll.getValue();
if (polltemp) {
url = `${environment.frontDomain}#/admin/${polltemp.admin_key}`;
}
} else {
url = `${environment.frontDomain}#/admin/${this.form.value.admin_key}`;
}
return url;
}
/**
* enrich vote stack with missing default votes
* @param vote_stack
*/
enrichVoteStackWithCurrentPollChoicesDefaultVotes(vote_stack: Stack) {
if (this._poll && this._poll.getValue) {
const polltemp = this._poll.getValue();
polltemp.choices.map((choice) => {
// for each vote, if it has the choice_id, do nothing, else, add a default vote
if (!this.findExistingVoteFromChoiceId(choice.id, vote_stack.votes)) {
vote_stack.votes.push(new Vote(choice.id));
}
});
}
}
/**
* find an existing vote in vote_stack from its choice_id
* @param choice_id
* @param votes
*/
findExistingVoteFromChoiceId(choice_id: number, votes: Vote[]) {
return votes.find((vote: Vote) => {
if (vote.choice_id === choice_id) {
return vote;
}
});
}
convertCalendarDatesToChoices(array_dates) {
return array_dates;
}
/**
* @description convert to API version 1 data transition object
* @param form
*/
newPollFromForm(form: any): Poll {
const newOwner = this.storageService.vote_stack.owner;
const newpoll = new Poll(newOwner, form.value.custom_url, form.value.title);
const pollKeys = Object.keys(newpoll);
const formFields = Object.keys(form.value);
newpoll.allowed_answers = [];
for (const pk of pollKeys) {
if (formFields.indexOf(pk) !== -1) {
const field = form.value[pk];
newpoll[pk] = field;
} else {
console.log('manque pollKey', pk);
}
}
if (form.value.isYesAnswerAvailable) {
newpoll.allowed_answers.push('yes');
}
if (form.value.isMaybeAnswerAvailable) {
newpoll.allowed_answers.push('maybe');
}
if (form.value.isNoAnswerAvailable) {
newpoll.allowed_answers.push('no');
}
newpoll.description = form.value.description;
newpoll.has_several_hours = form.value.hasSeveralHours;
newpoll.hasSeveralHours = form.value.hasSeveralHours;
newpoll.max_count_of_answers = form.value.allowComments;
newpoll.maxCountOfAnswers = form.value.maxCountOfAnswers;
newpoll.password = form.value.password;
newpoll.kind = form.value.kind;
newpoll.allow_comments = form.value.allowComments;
// merge choices from storage
if (form.value.kind === 'date') {
// convert calendar picker dates
}
newpoll.choices = Object.assign([], this.storageService.choices);
newpoll.dateChoices = Object.assign([], this.storageService.dateChoices);
newpoll.timeSlices = Object.assign([], this.storageService.timeSlices);
console.log('newpoll', newpoll);
return newpoll;
}
}