+ Organisez des évènements ou récoltez l’opinion de vos proches, simplement, librement.
+
+
+ Grâce à Framadate planifiez, organisez et prenez des décisions rapidement, simplement et sans
+ inscription.
+
+
+
+
+
+
+
@@ -22,7 +37,9 @@
*ngIf="environment.showDemoWarning"
class="demo demo-warning well has-background-warning-light padded marged"
>
- Ce que l'on peut faire sur cette démo:
+
+ Ce que l'on peut faire sur cette démo:
+
☑️ Créer un nouveau sondage
@@ -39,7 +56,9 @@
>
- Ce qu'on ne peut pas encore faire:
+
+ Ce qu'on ne peut pas encore faire:
+
🚴️ mettre à jour son vote à un sondage
@@ -58,6 +77,8 @@
{{ 'home.search_title' | translate }}
+
+
@@ -75,7 +96,7 @@
-
-
+
+
+
+
diff --git a/src/app/core/components/home/home.component.scss b/src/app/core/components/home/home.component.scss
index 05590871..b76a9e58 100644
--- a/src/app/core/components/home/home.component.scss
+++ b/src/app/core/components/home/home.component.scss
@@ -1,5 +1,9 @@
+@import '../../../../styles/variables';
+
:host {
text-align: center;
+ margin-left: -2em;
+ margin-right: -2em;
a .fa {
margin-right: 1ch;
}
@@ -9,4 +13,7 @@
.poll-list {
margin: 2em 0;
}
+ .presentation {
+ background: $secondary_color;
+ }
}
diff --git a/src/app/core/components/home/home.component.ts b/src/app/core/components/home/home.component.ts
index d3e1acdc..bda8fab6 100644
--- a/src/app/core/components/home/home.component.ts
+++ b/src/app/core/components/home/home.component.ts
@@ -4,6 +4,7 @@ import { StorageService } from '../../services/storage.service';
import { ApiService } from '../../services/api.service';
import { ToastService } from '../../services/toast.service';
import { DOCUMENT } from '@angular/common';
+import { Title } from '@angular/platform-browser';
@Component({
selector: 'app-home',
@@ -18,8 +19,11 @@ export class HomeComponent {
@Inject(DOCUMENT) private document: any,
public storageService: StorageService,
public toastService: ToastService,
+ public titleService: Title,
private api: ApiService
- ) {}
+ ) {
+ this.titleService.setTitle(environment.appTitle + ' - Accueil ');
+ }
searchMyPolls() {
const email = this.storageService.vote_stack.owner.email;
diff --git a/src/app/core/models/owner.model.ts b/src/app/core/models/owner.model.ts
index 693d3398..580afd49 100644
--- a/src/app/core/models/owner.model.ts
+++ b/src/app/core/models/owner.model.ts
@@ -6,8 +6,8 @@ export class Owner {
public pseudo: string = 'pseudo',
public email: string = '_nonexistent_contact@cipherbliss.com',
public polls: Poll[] = [],
- public role?: UserRole,
- public modifier_token?: string,
- public created_at?: string
+ public role: UserRole = UserRole.ADMIN,
+ public modifier_token: string = '',
+ public created_at: string = new Date().toISOString()
) {}
}
diff --git a/src/app/core/models/poll.DTO.model.ts b/src/app/core/models/poll.DTO.model.ts
new file mode 100644
index 00000000..a2cffb5e
--- /dev/null
+++ b/src/app/core/models/poll.DTO.model.ts
@@ -0,0 +1,46 @@
+import { Choice, ChoiceGroup } from './choice.model';
+import { DateChoice, TimeSlices } from './dateChoice.model';
+
+export class PollDTO {
+ menuVisible = true;
+ expiracyDateDefaultInDays;
+ deletionDateAfterLastModification;
+ pollType: string = 'date'; // classic or dates
+ title;
+ description;
+ myName;
+ myComment = '';
+ isAdmin; // when we create a poll; we are admin on it
+ myVoteStack;
+ myTempVoteStack;
+ myEmail;
+ myPolls; // list of retrieved polls from the backend api
+ allowSeveralHours;
+ visibility; // visible to one with the link:
+ voteChoices = 'yes; maybe; no'; // possible answers to a vote choice: only "yes"; "yes; maybe; no"
+ created_at;
+ expirationDate; // expiracy date
+ voteStackId; // id of the vote stack to update
+ pollId; // id of the current poll when created. data given by the backend api
+ pollSlug; // id of the current poll when created. data given by the backend api
+ currentPoll; // current poll selected with createPoll or getPoll of ConfigService
+ passwordAccess;
+ password;
+ customUrl; // custom slug in the url; must be unique
+ customUrlIsUnique; // given by the backend
+ urlSlugPublic;
+ urlPublic;
+ urlAdmin;
+ adminKey; // key to change config of the poll
+ owner_modifier_token; // key to change a vote stack
+ canModifyAnswers; // bool for the frontend selector
+ whoModifiesAnswers; // everybody; self; nobody (: just admin)
+ whoCanChangeAnswers; // everybody; self; nobody (: just admin)
+ dateList; // sets of days as strings; config to set identical time for days in a special days poll
+ timeList; // ranges of time expressed as strings
+
+ answers;
+ displayConfirmVoteModalAdmin;
+
+ constructor() {}
+}
diff --git a/src/app/core/services/api.service.ts b/src/app/core/services/api.service.ts
index 3f69f88b..4b72865d 100644
--- a/src/app/core/services/api.service.ts
+++ b/src/app/core/services/api.service.ts
@@ -16,6 +16,8 @@ const apiBaseHref = environment.api.version[apiVersion].baseHref;
const apiEndpoints = environment.api.endpoints;
+class PollDTO {}
+
@Injectable({
providedIn: 'root',
})
@@ -36,16 +38,17 @@ export class ApiService {
this.axiosInstance = axios.create({ baseURL: apiBaseHref });
this.axiosInstance.defaults.timeout = 2500;
- this.axiosInstance.defaults.headers.post['Content-Type'] = 'application/json';
- this.axiosInstance.defaults.headers.post['Accept'] = 'application/json';
- this.axiosInstance.defaults.headers.post['Charset'] = 'UTF-8';
+ // this.axiosInstance.defaults.headers.post['Content-Type'] = 'application/json';
+ // this.axiosInstance.defaults.headers.post['Accept'] = 'application/json';
+ // this.axiosInstance.defaults.headers.post['Charset'] = 'UTF-8';
// this.axiosInstance.defaults.headers.post['Accept-Charset'] = 'UTF-8';
- this.axiosInstance.defaults.headers.post['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
- this.axiosInstance.defaults.headers.post['Referrer-Policy'] = 'origin-when-cross-origin';
- this.axiosInstance.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
- this.axiosInstance.defaults.headers.post['Allow-Origin'] = '*';
- this.axiosInstance.defaults.headers.post['Access-Control-Allow-Headers'] =
- 'Origin, X-Requested-With, Content-Type, Accept';
+ // this.axiosInstance.defaults.headers.post['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
+ // this.axiosInstance.defaults.headers.post['Referrer-Policy'] = 'origin-when-cross-origin';
+ // this.axiosInstance.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
+ // this.axiosInstance.defaults.headers.post['Control-Allow-Origin'] = '*';
+ // this.axiosInstance.defaults.headers.post['Allow-Origin'] = '*';
+ // this.axiosInstance.defaults.headers.post['Access-Control-Allow-Headers'] =
+ // 'Origin, X-Requested-With, Content-Type, Accept';
console.log('this.axiosInstance.defaults.headers', this.axiosInstance.defaults.headers);
}
@@ -66,25 +69,29 @@ export class ApiService {
// Accept: 'application/json',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
- mode: 'no-cors',
+ // mode: 'no-cors',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Accept,Accept-Language,Content-Language,Content-Type',
- // 'Access-Control-Allow-Origin': '*',
};
-
- return {
+ const headersAxios = {
headers: headerDict,
body: bodyContent,
};
+ console.log('headersAxios', headersAxios);
+ return headersAxios;
}
- public async createPoll(poll: Poll): Promise {
+ /**
+ *
+ * @param poll
+ */
+ public async createPoll(poll: PollDTO): Promise {
// this.loaderService.setStatus(true);
- console.log('createPoll config', poll);
+ console.log('apiservice createPoll config', poll);
return this.axiosInstance.post(
`${this.baseHref}${currentApiRoutes['api_new_poll']}`,
- poll,
- ApiService.makeHeaders()
+ poll
+ // ApiService.makeHeaders()
);
}
@@ -134,10 +141,14 @@ export class ApiService {
}
}
- //////////
+ /**
+ * get all polls published by the API
+ */
public async getAllAvailablePolls(): Promise {
// TODO: used for facilities in DEV, should be removed in production
try {
+ this.axiosInstance.options(this.pollsEndpoint);
+
const response: AxiosResponse = await this.axiosInstance.get(`${this.pollsEndpoint}`);
return response?.data;
} catch (error) {
@@ -145,10 +156,32 @@ export class ApiService {
}
}
- public async getPollByCustomUrl(slug: string): Promise {
+ /**
+ * get one poll by its admin key
+ * @param admin_key
+ */
+ public async getPollByAdminKey(admin_key: string): Promise {
try {
- console.log('fetch API : asking for poll with custom_url=' + slug);
- const response: AxiosResponse = await this.axiosInstance.get(`${this.pollsEndpoint}/${slug}`);
+ console.log('fetch API : asking for poll with admin_key=' + admin_key);
+ const response: AxiosResponse = await this.axiosInstance.get(
+ `${this.pollsEndpoint}/admin/${admin_key}`
+ );
+ return response && response.data && !Array.isArray(response.data) ? response.data : undefined;
+ } catch (error) {
+ if (error.response?.status === 404) {
+ return undefined;
+ } else {
+ ApiService.handleError(error);
+ }
+ }
+ }
+
+ public async getPollByCustomUrl(custom_url: string): Promise {
+ try {
+ console.log('fetch API : asking for poll with custom_url=' + custom_url);
+ const response: AxiosResponse = await this.axiosInstance.get(
+ `${this.pollsEndpoint}/${custom_url}`
+ );
return response && response.data && !Array.isArray(response.data) ? response.data : undefined;
} catch (error) {
@@ -160,12 +193,12 @@ export class ApiService {
}
}
- public async getPollByCustomUrlWithHash(slug: string, hash: string): Promise {
+ public async getPollByCustomUrlWithHash(custom_url: string, hash: string): Promise {
try {
const response: AxiosResponse = await this.axiosInstance.get(
- `${this.pollsEndpoint}/${slug}/pass/${hash}`
+ `${this.pollsEndpoint}/${custom_url}/pass/${hash}`
);
- console.log('fetch API : asking for poll with custom_url=' + slug, { response });
+ console.log('fetch API : asking for poll with custom_url=' + custom_url, { response });
return response && response.data && !Array.isArray(response.data) ? response.data : undefined;
} catch (error) {
@@ -179,11 +212,11 @@ export class ApiService {
}
}
- public async getSlug(slug: string): Promise {
+ public async getSlug(custom_url: string): Promise {
try {
// TODO: scenario should be : if we can get this custom_url, it exists. if not, it doesn't. It's just a GET.
const response: AxiosResponse = await this.axiosInstance.get(
- `${this.pollsEndpoint}${this.slugsEndpoint}/${slug}`
+ `${this.pollsEndpoint}${this.slugsEndpoint}/${custom_url}`
);
if (response?.status !== 404) {
return false;
@@ -199,7 +232,7 @@ export class ApiService {
////////////
// UPDATE //
-
+ ////////////
public async sendUpdateVoteStack(vote_stack: Stack) {
try {
return await this.axiosInstance.patch(
diff --git a/src/app/core/services/date.utilities.service.ts b/src/app/core/services/date.utilities.service.ts
index 0f5a52a2..e6519eb2 100644
--- a/src/app/core/services/date.utilities.service.ts
+++ b/src/app/core/services/date.utilities.service.ts
@@ -100,8 +100,6 @@ export class DateUtilitiesService {
const ladate2 = this.addDaysToDate(1, today);
const ladate3 = this.addDaysToDate(2, today);
const ladate4 = this.addDaysToDate(3, today);
- const ladate5 = this.addDaysToDate(4, today);
- const ladate6 = this.addDaysToDate(5, today);
return [
{
@@ -124,16 +122,6 @@ export class DateUtilitiesService {
timeSlices: Object.create(defaultTimeOfDay),
date_object: ladate4,
},
- {
- literal: this.formateDateToInputStringNg(ladate5),
- timeSlices: Object.create(defaultTimeOfDay),
- date_object: ladate5,
- },
- {
- literal: this.formateDateToInputStringNg(ladate6),
- timeSlices: Object.create(defaultTimeOfDay),
- date_object: ladate6,
- },
];
}
}
diff --git a/src/app/core/services/language.service.ts b/src/app/core/services/language.service.ts
index ba481e0c..d13f559e 100644
--- a/src/app/core/services/language.service.ts
+++ b/src/app/core/services/language.service.ts
@@ -37,6 +37,10 @@ export class LanguageService {
this.setLanguageOnInit();
}
+ public getPrimeNgStrings() {
+ return this.translate.get('calendar_widget');
+ }
+
private setLanguageOnInit(): void {
// set language from storage
if (!this.translate.currentLang) {
diff --git a/src/app/core/services/poll.service.ts b/src/app/core/services/poll.service.ts
index 5114b7c8..a1e7d8b0 100644
--- a/src/app/core/services/poll.service.ts
+++ b/src/app/core/services/poll.service.ts
@@ -1,11 +1,10 @@
-import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
-import { BehaviorSubject, Observable } from 'rxjs';
+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 { Owner } from '../models/owner.model';
import { ApiService } from './api.service';
import { ToastService } from './toast.service';
import { UserService } from './user.service';
@@ -14,7 +13,12 @@ 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';
@@ -22,9 +26,29 @@ import { Vote } from '../models/vote.model';
providedIn: 'root',
})
export class PollService implements Resolve {
- _poll: BehaviorSubject = new BehaviorSubject(undefined);
+ public _poll: BehaviorSubject = new BehaviorSubject(undefined);
public readonly poll: Observable = 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: Date[] = [new Date()];
+ public disabled_dates: Date[] = [];
constructor(
private http: HttpClient,
@@ -33,15 +57,132 @@ export class PollService implements Resolve {
private storageService: StorageService,
private userService: UserService,
private uuidService: UuidService,
- private dateUtils: DateUtilitiesService,
+ private toastService: ToastService,
private titleService: Title,
- private toastService: ToastService
+ public DateUtilitiesService: DateUtilitiesService,
+ public route: ActivatedRoute,
+ @Inject(DOCUMENT) private document: any,
+ private fb: FormBuilder
) {
- this._poll.next(new Poll(null, 'titre', 'custom-title'));
+ this.createFormGroup();
+
+ // fill in the next 3 days of the calendar date picker
+ this.calendar = [
+ this.DateUtilitiesService.addDaysToDate(1, new Date()),
+ this.DateUtilitiesService.addDaysToDate(2, new Date()),
+ this.DateUtilitiesService.addDaysToDate(3, new Date()),
+ ];
+ // disable days before today
+ for (let i = 1; i < 31; i++) {
+ this.disabled_dates.push(this.DateUtilitiesService.addDaysToDate(-i, new Date()));
+ }
+ if (environment.autofill_creation) {
+ this.setDemoValues();
+ this.toastService.display('auto fill de création fait');
+ }
+ if (environment.autoSendNewPoll) {
+ this.createPoll();
+ }
+ }
+
+ /**
+ * add example values to the form for demo env
+ */
+ setDemoValues(): void {
+ this.form.patchValue({
+ title: 'Mon titre de sondage du ' + this.DateUtilitiesService.formateDateToInputStringNg(new Date()),
+ 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,
+ expiresDaysDelay: environment.expiresDaysDelay,
+ });
+ 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 minlengthValidation = environment.production ? 12 : 0;
+ let form = this.fb.group({
+ title: ['mon titre de sondage', [Validators.required, Validators.minLength(minlengthValidation)]],
+ creatorPseudo: ['', [Validators.required]],
+ created_at: [new Date(), [Validators.required]],
+ creatorEmail: ['', [Validators.required]],
+ custom_url: [this.uuidService.getUUID(), [Validators.required]],
+ description: ['', [Validators.required]],
+ password: ['', []],
+ choices: new FormArray([]),
+ whoModifiesAnswers: ['', [Validators.required]],
+ whoCanChangeAnswers: ['', [Validators.required]],
+ isAboutDate: [true, [Validators.required]],
+ expiresDaysDelay: [environment.expiresDaysDelay, []],
+ maxCountOfAnswers: [300, []],
+ 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: ['', []],
+ allowNewDateTime: [true, [Validators.required]],
+ });
+ this.form = form;
+ return form;
+ }
+
+ /**
+ * set default configs to the form
+ */
+ public patchFormDefaultValues() {
+ this.form.patchValue({
+ title: 'mon titre de sondage',
+ description: '',
+ custom_url: this.uuidService.getUUID(),
+ creatorPseudo: '',
+ creatorEmail: '',
+ isAboutDate: true,
+ whoModifiesAnswers: 'everybody',
+ whoCanChangeAnswers: 'everybody',
+ isProtectedByPassword: false,
+ richTextMode: false,
+ areResultsPublic: true,
+ expiresDaysDelay: environment.expiresDaysDelay,
+ maxCountOfAnswers: 300,
+ voterEmailList: '',
+ password: '',
+ });
+ this.setDefaultDatesForInterval();
+ }
+
+ /**
+ * get a new slug from form title and creation date
+ */
+ 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
+ * DO NOT USE - needs refacto
* @param route
* @param state
*/
@@ -57,9 +198,9 @@ export class PollService implements Resolve {
) {
if (this.pass_hash) {
this.storageService.vote_stack.pass_hash = this.pass_hash;
- await this.loadPollBycustom_urlWithPasswordHash(wantedcustom_url, this.pass_hash);
+ await this.loadPollByCustomUrlWithPasswordHash(wantedcustom_url, this.pass_hash);
} else {
- await this.loadPollBycustom_url(wantedcustom_url);
+ await this.loadPollByCustomUrl(wantedcustom_url);
}
}
const loadedPoll = this._poll.getValue();
@@ -77,17 +218,19 @@ export class PollService implements Resolve {
*/
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) => {
console.log('getAllAvailablePolls res', res);
});
} catch (e) {
- console.error('getAllAvailablePolls e', e);
+ console.log('getAllAvailablePolls e', e);
}
}
- public async loadPollBycustom_url(custom_url: string): Promise {
+ public async loadPollByCustomUrl(custom_url: string): Promise {
if (custom_url) {
const poll: Poll | undefined = await this.apiService.getPollByCustomUrl(custom_url);
@@ -103,7 +246,7 @@ export class PollService implements Resolve {
}
}
- public async loadPollBycustom_urlWithPasswordHash(custom_url: string, hash: string): Promise {
+ public async loadPollByCustomUrlWithPasswordHash(custom_url: string, hash: string): Promise {
if (custom_url) {
const poll: Poll | undefined = await this.apiService.getPollByCustomUrlWithHash(custom_url, hash);
@@ -123,50 +266,243 @@ export class PollService implements Resolve {
* update poll and parse its fields
* @param poll
*/
- public updateCurrentPoll(poll: Poll): void {
+ public updateCurrentPoll(poll: Poll): Poll {
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);
- }
+ // 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);
+ console.log('next poll', poll);
+
+ this.storageService.setChoicesForVoteStack(poll.choices);
+
+ this.toastService.display(`sondage ${poll.title} bien mis à jour`, 'success');
+ return poll;
}
/**
- * make a uniq custom_url for the current poll creation
- * @param poll
+ * add all the dates between the start and end dates in the interval section
*/
- makecustom_url(poll: Poll): string {
+ 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();
+ }
+ }
+
+ /**
+ * convert form data to DTO to create a new poll, and store the admin key
+ */
+ public createPoll(): Promise {
+ this.toastService.display('sending...');
+ const newpoll = this.newPollFromForm();
+ return this.apiService.createPoll(newpoll).then(
+ (resp: any) => {
+ console.log('poll created resp', resp);
+ this.admin_key = resp.data.poll.admin_key;
+ this.storageService.userPolls.push(resp.data.poll);
+ },
+ (error) => {
+ this.toastService.display('BOOM, the createPoll went wrong');
+ this.apiService.ousideHandleError(error);
+ }
+ );
+ }
+
+ /**
+ * 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.patchFormDefaultValues();
+ 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 = '';
- const creation_date = new Date(poll.creation_date);
str =
- creation_date.getFullYear() +
+ form.value.created_at.getFullYear() +
'_' +
- (creation_date.getMonth() + 1) +
+ (form.value.created_at.getMonth() + 1) +
'_' +
- creation_date.getDate() +
+ form.value.created_at.getDate() +
'_' +
- poll.owner.pseudo +
+ form.value.creatorPseudo +
'_' +
- poll.title;
-
- return this.convertTextToSlug(str) + '-' + this.uuidService.getUUID();
- }
-
- /**
- * convert a text to a slug
- * @param str
- */
- public convertTextToSlug(str: string): string {
- str = str.trim();
+ form.value.title;
str = str.replace(/^\s+|\s+$/g, ''); // trim
str = str.toLowerCase();
@@ -181,10 +517,25 @@ export class PollService implements Resolve {
.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by -
.replace(/-+/g, '-'); // collapse dashes
- return str;
+
+ return str + '-' + this.uuidService.getUUID();
+ }
+
+ public async saveCurrentPoll(): Promise {
+ 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 n’a é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.');
}
@@ -203,72 +554,85 @@ export class PollService implements Resolve {
this.toastService.display('Les commentaires de ce sondage ont été supprimés.');
}
- /**
- * @description convert to API version 1 data transition object
- * @param form
- */
- newPollFromForm(form: any): Poll {
- const newOwner = this.storageService.vote_stack.owner;
+ public buildAnswersByChoiceLabelByPseudo(poll: Poll): Map> {
+ const pseudos: Set = new Set();
+ poll.choices.forEach((choice: Choice) => {
+ choice.participants.forEach((users: Set) => {
+ users.forEach((user: Owner) => {
+ pseudos.add(user.pseudo);
+ });
+ });
+ });
- const newpoll = new Poll(newOwner, form.value.custom_url, form.value.title);
+ const list = new Map>();
+ pseudos.forEach((pseudo: string) => {
+ list.set(
+ pseudo,
+ new Map(
+ poll.choices.map((choice: Choice) => {
+ return [choice.name, undefined];
+ })
+ )
+ );
+ });
- const pollKeys = Object.keys(newpoll);
- const formFields = Object.keys(form.value);
- newpoll.allowed_answers = ['yes'];
+ poll.choices.forEach((choice: Choice) => {
+ choice.participants.forEach((users: Set, answer: Answer) => {
+ users.forEach((user: Owner) => {
+ list.get(user.pseudo).set(choice.name, answer);
+ });
+ });
+ });
- 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.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
- newpoll.choices = Object.assign([], this.storageService.choices);
- newpoll.dateChoices = Object.assign([], this.storageService.dateChoices);
- newpoll.timeSlices = Object.assign([], this.storageService.timeSlices);
- return newpoll;
+ return list;
}
- public getAdministrationUrl(): string {
- let url = '';
- if (this._poll && this._poll.getValue) {
- const polltemp = this._poll.getValue();
- if (polltemp) {
- url = `${environment.frontDomain}#/poll/admin/${polltemp.admin_key}`;
- }
- }
- return url;
+ public getParticipationUrlFromForm(): string {
+ return `${environment.frontDomain}#/poll/${this.form.value.custom_url}/consultation`;
+ }
+
+ public getAdministrationUrlFromForm(): string {
+ // admin_key is filled after creation
+ // example http://localhost:4200/#/administration/key/8Ubcg2YI99f69xz946cn4O64bQAeb
+
+ return `${environment.frontDomain}#/administration/key/${this.admin_key}`;
}
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
@@ -297,4 +661,78 @@ export class PollService implements Resolve {
}
});
}
+
+ convertCalendarDatesToChoices(array_dates) {
+ return array_dates;
+ }
+
+ patchFormWithPoll(poll: Poll) {
+ this.form.patchValue({
+ ...poll,
+ isAboutDate: poll.kind == 'date',
+ });
+ }
+
+ /**
+ * @description convert to API version 1 data transition object
+ */
+ newPollFromForm(): Poll {
+ let form = this.form;
+ console.log('this.form.value', this.form.value);
+ 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 = [];
+
+ // comparer les champs de formulaire avec le DTO de création de sondage
+ 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.max_count_of_answers = form.value.maxCountOfAnswers;
+ newpoll.maxCountOfAnswers = form.value.maxCountOfAnswers;
+ newpoll.password = form.value.password;
+ newpoll.kind = form.value.isAboutDate ? 'date' : 'classic';
+ newpoll.allow_comments = form.value.allowComments;
+ // merge choices from storage
+ if (form.value.isAboutDate) {
+ // convert calendar picker dates
+ console.log('this.calendar', this.calendar);
+
+ for (let elem of this.calendar) {
+ console.log('elem', elem);
+ let converted_day = {
+ literal: this.DateUtilitiesService.formateDateToInputStringNg(elem),
+ timeSlices: [],
+ date_object: elem,
+ };
+ newpoll.dateChoices.push(converted_day);
+ }
+ console.log('newpoll.dateChoices', newpoll.dateChoices);
+ }
+ 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;
+ }
}
diff --git a/src/app/core/services/poll.utilities.service.ts b/src/app/core/services/poll.utilities.service.ts
index 0b6471fe..4b669c40 100644
--- a/src/app/core/services/poll.utilities.service.ts
+++ b/src/app/core/services/poll.utilities.service.ts
@@ -65,16 +65,16 @@ export class PollUtilitiesService {
* @param bodyContent
*/
makeHeaders(bodyContent?: any) {
- const headerDict = {
- Charset: 'UTF-8',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
- 'Access-Control-Allow-Origin': '*',
- };
+ // const headerDict = {
+ // Charset: 'UTF-8',
+ // 'Content-Type': 'application/json',
+ // Accept: 'application/json',
+ // 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ // 'Access-Control-Allow-Origin': '*',
+ // };
return {
- headers: headerDict,
+ headers: [],
body: bodyContent,
};
}
diff --git a/src/app/core/services/storage.service.ts b/src/app/core/services/storage.service.ts
index c173c8f0..ac2c8148 100644
--- a/src/app/core/services/storage.service.ts
+++ b/src/app/core/services/storage.service.ts
@@ -46,18 +46,13 @@ export class StorageService {
public choices: Choice[] = [];
constructor(public dateUtilities: DateUtilitiesService, private toastService: ToastService) {
- if (environment.autofill) {
- this.toastService.display('autofill des sondages utilisateur');
+ if (environment.autofill_participation) {
this.userPolls.push(new Poll(new Owner(), 'Démo: Anniversaire de tonton Patrick', 'aujourdhui-ou-demain'));
this.userPolls.push(new Poll(new Owner(), 'Démo: Atelier cuisine du quartier', 'aujourdhui-ou-demain'));
this.userPolls.push(
new Poll(new Owner(), 'Démo: Réunion du département des chatons', 'aujourdhui-ou-demain')
);
}
-
- if (!this.dateChoices.length) {
- this.dateChoices = this.dateUtilities.makeDefaultDateChoices();
- }
}
/**
@@ -70,15 +65,16 @@ export class StorageService {
if (!this.vote_stack.id) {
this.vote_stack = new Stack();
+ console.log('choices_list', choices_list);
for (const choice of choices_list) {
- if (environment.autofill) {
- console.log('autofill au hasard des votes à ce sondage');
- this.toastService.display('autofill au hasard des votes à ce sondage');
- const defaultvalue = Math.random() > 0.75 ? 'yes' : '';
- this.vote_stack.votes.push(new Vote(choice.id, defaultvalue));
- } else {
- this.vote_stack.votes.push(new Vote(choice.id));
- }
+ // if (environment.autofill_participation) {
+ // console.log('autofill au hasard des votes à ce sondage');
+ // this.toastService.display('autofill au hasard des votes à ce sondage');
+ // const defaultvalue = Math.random() > 0.75 ? 'yes' : '';
+ // this.vote_stack.votes.push(new Vote(choice.id, defaultvalue));
+ // } else {
+ this.vote_stack.votes.push(new Vote(choice.id));
+ // }
}
}
}
diff --git a/src/app/features/administration/administration-routing.module.ts b/src/app/features/administration/administration-routing.module.ts
index 9fad9e8b..4676f048 100644
--- a/src/app/features/administration/administration-routing.module.ts
+++ b/src/app/features/administration/administration-routing.module.ts
@@ -2,11 +2,34 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdministrationComponent } from './administration.component';
-import { NamingComponent } from './naming/naming.component';
+import { StepTwoComponent } from './form/steps/step-two/step-two.component';
+import { StepThreeComponent } from './form/steps/step-three/step-three.component';
+import { StepFourComponent } from './form/steps/step-four/step-four.component';
+import { StepFiveComponent } from './form/steps/step-five/step-five.component';
+import { StepOneComponent } from './form/steps/step-one/step-one.component';
+import { SuccessComponent } from './success/success.component';
+import { AdminConsultationComponent } from './consultation/consultation.component';
const routes: Routes = [
- { path: '', component: AdministrationComponent, data: { animation: 'AdminPage' } },
- { path: 'naming', component: NamingComponent },
+ {
+ path: '',
+ component: AdministrationComponent,
+ },
+ { path: 'key/:admin_key', component: AdminConsultationComponent },
+ {
+ path: 'step',
+ children: [
+ { path: '1', component: StepOneComponent },
+ { path: '2', component: StepTwoComponent },
+ { path: '3', component: StepThreeComponent },
+ { path: '4', component: StepFourComponent },
+ { path: '5', component: StepFiveComponent },
+ ],
+ },
+ {
+ path: 'success',
+ component: SuccessComponent,
+ },
];
@NgModule({
diff --git a/src/app/features/administration/administration.component.ts b/src/app/features/administration/administration.component.ts
index 54bdcd71..8d8d2286 100644
--- a/src/app/features/administration/administration.component.ts
+++ b/src/app/features/administration/administration.component.ts
@@ -20,7 +20,7 @@ export class AdministrationComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.routeSubscription = this.route.data.subscribe((data: { poll: Poll }) => {
- console.log('data', data);
+ console.log('routeSubscription data', data);
if (data.poll) {
this.poll = data.poll;
}
diff --git a/src/app/features/administration/administration.module.ts b/src/app/features/administration/administration.module.ts
index 8d906d69..70e204ed 100644
--- a/src/app/features/administration/administration.module.ts
+++ b/src/app/features/administration/administration.module.ts
@@ -9,6 +9,12 @@ import { AdministrationComponent } from './administration.component';
import { StepperComponent } from './stepper/stepper.component';
import { NamingComponent } from './naming/naming.component';
import { FormComponent } from './form/form.component';
+import { StepOneComponent } from './form/steps/step-one/step-one.component';
+import { StepTwoComponent } from './form/steps/step-two/step-two.component';
+import { StepThreeComponent } from './form/steps/step-three/step-three.component';
+import { StepFourComponent } from './form/steps/step-four/step-four.component';
+import { StepFiveComponent } from './form/steps/step-five/step-five.component';
+import { CalendarModule } from 'primeng/calendar';
import { SuccessComponent } from './success/success.component';
import { DateSelectComponent } from './form/date-select/date-select.component';
import { TextSelectComponent } from './form/text-select/text-select.component';
@@ -21,6 +27,7 @@ import { IntervalComponent } from './form/date/interval/interval.component';
import { DayListComponent } from './form/date/list/day/day-list.component';
import { PickerComponent } from './form/date/picker/picker.component';
import { TimeListComponent } from './form/date/list/time/time-list.component';
+import { AdminConsultationComponent } from './consultation/consultation.component';
@NgModule({
declarations: [
@@ -34,14 +41,22 @@ import { TimeListComponent } from './form/date/list/time/time-list.component';
KindSelectComponent,
BaseConfigComponent,
AdvancedConfigComponent,
+ StepOneComponent,
+ StepTwoComponent,
+ StepThreeComponent,
+ StepFourComponent,
+ StepFiveComponent,
+ SuccessComponent,
IntervalComponent,
DayListComponent,
PickerComponent,
TimeListComponent,
+ AdminConsultationComponent,
],
imports: [
AdministrationRoutingModule,
CommonModule,
+ CalendarModule,
ReactiveFormsModule,
SharedModule,
FormsModule,
diff --git a/src/app/features/administration/consultation/consultation.component.html b/src/app/features/administration/consultation/consultation.component.html
new file mode 100644
index 00000000..e21f1a2f
--- /dev/null
+++ b/src/app/features/administration/consultation/consultation.component.html
@@ -0,0 +1,16 @@
+
+
Consulter le sondage
+
+
+
{{ form.value.title }}
+
+
+
{{ poll.title }}
+
+ Créé le {{ poll.created_at | date }} par {{ poll.owner.pseudo }}, {{ poll.owner.email }}
+
@@ -128,7 +114,7 @@
- Nombre de réponses limitées à ce nombre
+ Nombre de réponses limitées à ce nombre. Utile pour réserver des places à un évènement.
-