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

537 lines
17 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 { User } from '../models/user.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 { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DateChoice, defaultTimeOfDay, otherDefaultDates } from '../../features/old-stuff/config/defaultConfigs';
import { DateUtilities } from '../../features/old-stuff/config/DateUtilities';
import { DOCUMENT } from '@angular/common';
@Injectable({
providedIn: 'root',
})
export class PollService implements Resolve<Poll> {
private _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: any;
public intervalDaysDefault = 7;
public dateList: DateChoice[] = otherDefaultDates; // sets of days as strings, config to set identical time for days in a special days poll
public timeList: any = defaultTimeOfDay; // 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 urlPrefix: string = window.location.origin + '/participation/';
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 userService: UserService,
private uuidService: UuidService,
private toastService: ToastService,
public dateUtilities: DateUtilities,
public route: ActivatedRoute,
@Inject(DOCUMENT) private document: any,
private fb: FormBuilder
) {
this.createFormGroup();
if (environment.poll.autoFillDemo) {
this.setDemoValues();
} 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.dateUtilities.addDaysToDate(1, new Date()),
this.dateUtilities.addDaysToDate(2, new Date()),
this.dateUtilities.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,
isOwnerNotifiedByEmailOnNewVote: false,
isOwnerNotifiedByEmailOnNewComment: false,
isMaybeAnswerAvailable: 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]],
choices: new FormArray([]),
whoModifiesAnswers: ['', [Validators.required]],
whoCanChangeAnswers: ['', [Validators.required]],
isAboutDate: [true, [Validators.required]],
startDateInterval: ['', [Validators.required]],
endDateInterval: ['', [Validators.required]],
isProtectedByPassword: [false, [Validators.required]],
isOwnerNotifiedByEmailOnNewVote: [false, [Validators.required]],
isOwnerNotifiedByEmailOnNewComment: [false, [Validators.required]],
isMaybeAnswerAvailable: [false, [Validators.required]],
areResultsPublic: [true, [Validators.required]],
richTextMode: [false, [Validators.required]],
expiracyNumberOfDays: [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> {
const segments: string[] = state.url.split('/');
const wantedSlug: string = segments.includes('poll') ? segments[segments.indexOf('poll') + 1] : '';
if (!wantedSlug && state.url.includes('administration')) {
// creation of new poll
const poll = new Poll(this.userService.getCurrentUser(), this.uuidService.getUUID(), '');
this._poll.next(poll);
this.router.navigate(['poll/' + poll.slug + '/administration']);
}
if (!this._poll.getValue() || !this._poll.getValue().slug || this._poll.getValue().slug !== wantedSlug) {
await this.loadPollBySlug(wantedSlug);
}
if (this._poll.getValue()) {
return this._poll.getValue();
} else {
this.router.navigate(['page-not-found']);
return;
}
}
getAllAvailablePolls() {
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 loadPollBySlug(slug: string): Promise<void> {
if (slug) {
const poll: Poll | undefined = await this.apiService.getPollBySlug(slug);
console.log({ loadPollBySlugResponse: poll });
this.updateCurrentPoll(poll);
}
}
public updateCurrentPoll(poll: Poll): void {
this._poll.next(poll);
}
/**
* add all the dates between the start and end dates in the interval section
*/
addIntervalOfDates(): void {
const newIntervalArray = this.dateUtilities.getDatesInRange(
this.dateUtilities.parseInputDateToDateObject(this.startDateInterval),
this.dateUtilities.parseInputDateToDateObject(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: '',
timeList: [],
date_object: new Date(),
});
}
removeAllTimes() {
this.timeList = [];
}
resetTimes() {
this.timeList = otherDefaultDates;
}
/**
* 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: '',
timeList: [],
date_object: new Date(),
});
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);
console.log('newpoll', newpoll);
this.apiService.createPoll(newpoll);
}
/**
* 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.dateUtilities
.addDaysToDate(this.intervalDaysDefault, dateCurrent)
.toISOString()
.substring(0, 10);
this.form.patchValue({
startDateInterval: this.startDateInterval,
endDateInterval: this.endDateInterval,
});
this.countDays();
}
askInitFormDefault(): void {
this.initFormDefault(false);
this.toastService.display('formulaire réinitialisé');
}
countDays(): void {
this.intervalDays = this.dateUtilities.countDays(
this.dateUtilities.parseInputDateToDateObject(this.startDateInterval),
this.dateUtilities.parseInputDateToDateObject(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: User, response: Answer): void {
const currentPoll = this._poll.getValue();
currentPoll.choices.find((c) => c.label === choice.label)?.updateParticipation(user, response);
this.updateCurrentPoll(currentPoll);
this.apiService.createParticipation(currentPoll.slug, choice.label, 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().slug);
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().slug, comment);
this.toastService.display('Votre commentaire a été enregistré.');
}
public async deleteComments(): Promise<void> {
await this.apiService.deletePollComments(this._poll.getValue().slug);
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<User>) => {
users.forEach((user: User) => {
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.label, undefined];
})
)
);
});
poll.choices.forEach((choice: Choice) => {
choice.participants.forEach((users: Set<User>, answer: Answer) => {
users.forEach((user: User) => {
list.get(user.pseudo).set(choice.label, answer);
});
});
});
return list;
}
newPollFromForm(form: any): any {
const newpoll = new Poll(
this.userService.getCurrentUser(),
this.uuidService.getUUID(),
form.controls.title.value
);
/**
* convert to API version 1 config poll
*/
const apiV1Poll = {
menuVisible: true,
expiracyDateDefaultInDays: newpoll.configuration.expiresDaysDelay,
deletionDateAfterLastModification: newpoll.configuration.expiracyAfterLastModificationInDays,
pollType: newpoll.configuration.isAboutDate ? 'dates' : 'classic', // classic or dates
title: newpoll.title,
description: newpoll.description,
myName: newpoll.owner.pseudo,
myComment: '',
isAdmin: true, // when we create a poll, we are admin on it
myVoteStack: {},
myTempVoteStack: 0,
myEmail: newpoll.owner.email,
myPolls: [], // list of retrieved polls from the backend api
/*
date specific poll, we have the choice to setup different hours (timeList) for all possible dates (dateList), or use the same hours for all dates
*/
allowSeveralHours: 'true',
// access
visibility: newpoll.configuration.areResultsPublic, // visible to one with the link:
voteChoices: newpoll.configuration.isMaybeAnswerAvailable ? 'yes, maybe, no' : 'yes', // possible answers to a vote choice: only "yes", "yes, maybe, no"
created_at: new Date(),
expirationDate: '', // expiracy date
voteStackId: null, // id of the vote stack to update
pollId: null, // id of the current poll when created. data given by the backend api
pollSlug: null, // id of the current poll when created. data given by the backend api
currentPoll: null, // current poll selected with createPoll or getPoll of ConfigService
passwordAccess: newpoll.configuration.isProtectedByPassword,
password: newpoll.configuration.password,
customUrl: newpoll.slug, // custom slug in the url, must be unique
customUrlIsUnique: null, // given by the backend
urlSlugPublic: null,
urlPublic: null,
urlAdmin: null,
adminKey: '', // key to change config of the poll
owner_modifier_token: '', // key to change a vote stack
canModifyAnswers: newpoll.configuration.isAllowingtoChangeOwnAnswers, // bool for the frontend selector
whoModifiesAnswers: newpoll.configuration.whoCanChangeAnswers, // everybody, self, nobody (: just admin)
whoCanChangeAnswers: newpoll.configuration.whoCanChangeAnswers, // everybody, self, nobody (: just admin)
dateList: newpoll.dateChoices, // sets of days as strings, config to set identical time for days in a special days poll
timeList: newpoll.timeChoices, // ranges of time expressed as strings
answers: newpoll.choices,
// modals
displayConfirmVoteModalAdmin: false,
};
console.log('apiV1Poll', apiV1Poll);
return apiV1Poll;
}
}