From 7fea3dac33d9be540fd938729101381fd5161278 Mon Sep 17 00:00:00 2001 From: Mike Risher <mrisher@catalyte.io> Date: Tue, 24 Sep 2019 17:41:27 +0000 Subject: [PATCH] lp1845240 port of Surveys UI from DOJO to Angular idlClass asv holds the surveys, asvq holds their questions, and asva holds the answers to those questions. The surveys are in their own module and are lazy loaded Signed-off-by: Mike Risher <mrisher@catalyte.io> Modernize the survey create API by migrating it to cstore. Additionally, make it possible to modify an existing survey top-level object by setting 'ischanged' to the inbound survey. Signed-off-by: Bill Erickson <berickxx@gmail.com> LP1845240 Migrate survey create API to cstore Modernize the survey create API by migrating it to cstore. New API supports full range of isnew / ischanged / isdeleted actions on the survey, questions, and answers. Signed-off-by: Bill Erickson <berickxx@gmail.com> LP1845240 Survey API returns updated fleshed survey Signed-off-by: Bill Erickson <berickxx@gmail.com> Changes to be committed: modified: Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html modified: Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts new file: Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html new file: Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts new file: Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts new file: Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html new file: Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts new file: Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts modified: Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Survey.pm modified: Open-ILS/tests/datasets/sql/surveys.sql Signed-off-by: Bill Erickson <berickxx@gmail.com> --- .../admin/local/admin-local-splash.component.html | 3 +- .../src/app/staff/admin/local/routing.module.ts | 3 + .../admin/local/survey/survey-edit.component.html | 124 +++++++++ .../admin/local/survey/survey-edit.component.ts | 307 +++++++++++++++++++++ .../admin/local/survey/survey-routing.module.ts | 22 ++ .../staff/admin/local/survey/survey.component.html | 37 +++ .../staff/admin/local/survey/survey.component.ts | 122 ++++++++ .../app/staff/admin/local/survey/survey.module.ts | 25 ++ .../lib/OpenILS/Application/Circ/Survey.pm | 152 +++++++++- 9 files changed, 788 insertions(+), 7 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html index e051d37c42..223d1817cc 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html @@ -63,11 +63,10 @@ <eg-link-table-link i18n-label label="Statistical Popularity Badges" routerLink="/staff/admin/local/rating/badge"></eg-link-table-link> <eg-link-table-link i18n-label label="Surveys" - url="/eg/staff/admin/local/action/survey"></eg-link-table-link> + routerLink="/staff/admin/local/action/survey"></eg-link-table-link> <eg-link-table-link i18n-label label="Transit List" url="/eg/staff/circ/transits/list"></eg-link-table-link> <eg-link-table-link i18n-label label="Work Log" url="/eg/staff/admin/workstation/log"></eg-link-table-link> - </eg-link-table> </div> diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts index 39c6be7179..15a9153201 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts @@ -23,6 +23,9 @@ const routes: Routes = [{ path: 'config/standing_penalty', component: StandingPenaltyComponent }, { + path: 'action/survey', + loadChildren: '@eg/staff/admin/local/survey/survey.module#SurveyModule' +}, { path: ':schema/:table', component: BasicAdminPageComponent }]; diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html new file mode 100644 index 0000000000..86f2f20d81 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html @@ -0,0 +1,124 @@ +<eg-staff-banner bannerText="Survey ID # {{surveyId}}" i18n-bannerText + class="mb-3"></eg-staff-banner> +<ngb-tabset #surveyTabs [activeId]="surveyTab" (tabChange)="onTabChange($event)" class="mb-3"> + <ngb-tab title="Edit Survey" i18n-title id="edit"> + <ng-template ngbTabContent> + <div class="col-lg-6 offset-lg-3 mt-3"> + <div style="text-align: center;"> + <button class="p-2 mb-3 btn btn-danger btn-lg" + (click)="endSurvey()" i18n> + End Survey Now + </button> + </div> + <eg-fm-record-editor displayMode="inline" + hiddenFieldsList="id" + datetimeFieldsList="start_date,end_date" + idlClass="asv" + mode="update" + [record]="surveyObj"> + </eg-fm-record-editor> + </div> + </ng-template> + </ngb-tab> + <ngb-tab title="Questions and Answers" i18n-title id="qanda"> + <ng-template ngbTabContent> + <div class="col-lg-8 offset-lg-2 mt-3"> + <eg-staff-banner bannerText="Questions & Answers" i18n-bannerText> + </eg-staff-banner> + <div *ngFor="let question of localArray; let questionIndex = index;"> + <div class="mb-3 mt-3 p-2 bg-light input-group"> + <label class="input-group-text"> + <b>Question</b> + </label> + <input type="text" [(ngModel)]="question.words" class="form-control" + name="question-{{questionIndex}}"> + <span class="input-group-append"> + <button class="ml-2 btn btn-info" + (click)="updateQuestion(question)" i18n> + Save + </button> + <button class="ml-1 btn btn-danger" + (click)="deleteQuestion(question)" i18n> + Delete Question & Answers + </button> + </span> + </div> + <div *ngFor="let answer of question.answers; let answerIndex = index;" + class="mb-2 input-group"> + <input class="form-control" type="text" + [(ngModel)]="answer.words" + name="answer-{{questionIndex}}-{{answerIndex}}"> + <span class="input-group-append"> + <button class="ml-2 btn btn-info" + (click)="updateAnswer(answer, question, questionIndex, answerIndex)" + i18n> + Save + </button> + <button class="ml-1 btn btn-danger" (click)="deleteAnswer(answer)" + i18n> + Delete + </button> + </span> + </div> + <div class="mb-2 input-group"> + <input class="form-control" type="text" + [(ngModel)]="newAnswerArray[questionIndex].inputText" + value=""> + <span class="input-group-append"> + <button class="ml-2 btn btn-info" + (click)="createAnswer(newAnswerArray[questionIndex].inputText, question)" + i18n> + Add Answer + </button> + </span> + </div> + </div> + <div class="mb-3 mt-3 p-2 bg-light input-group"> + <label class="input-group-text"> + <b>New Question</b> + </label> + <input #newQuestionInput + class="form-control" + type="text" + [(ngModel)]="newQuestionText" + name="question-new" value=""> + <span class="input-group-append"> + <button class="ml-2 btn btn-info" + (click)="createQuestion(newQuestionText)" i18n> + Save Question & Add Answer + </button> + </span> + </div> + </div> + </ng-template> + </ngb-tab> +</ngb-tabset> + +<eg-string #createAnswerString i18n-text text="New Answer Added"></eg-string> +<eg-string #createAnswerErrString i18n-text text="Failed to Create New Answer"> + </eg-string> +<eg-string #createQuestionString i18n-text text="New Question Added"></eg-string> +<eg-string #createQuestionErrString i18n-text text="Failed to Create New Question"> + </eg-string> +<eg-string #delAnswerSuccessStr i18n-text text="Survey Answer deleted"> + </eg-string> +<eg-string #delAnswerFailStr i18n-text text="Survey Answer deletion failed"> + </eg-string> +<eg-string #delQuestionSuccessStr i18n-text text="Survey Question deleted"> + </eg-string> +<eg-string #delQuestionFailStr i18n-text text="Survey Question deletion failed"> + </eg-string> +<eg-string #updateAnswerSuccessStr i18n-text text="Survey Answer updated"> + </eg-string> +<eg-string #updateAnswerFailStr i18n-text text="Survey Answer update failed"> + </eg-string> +<eg-string #updateQuestionSuccessStr i18n-text text="Survey Question updated"> + </eg-string> +<eg-string #updateQuestionFailStr i18n-text text="Survey Question update failed"> + </eg-string> +<eg-string #endSurveyFailedString i18n-text + text="Ending Survey failed or was not allowed"></eg-string> +<eg-string #endSurveySuccessString i18n-text text="Survey ended"></eg-string> +<eg-string #questionAlreadyStartedErrString i18n-text + text="The survey Start Date must be set for the future to add new questions or modify existing questions."> + </eg-string> \ No newline at end of file diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts new file mode 100644 index 0000000000..73b3a7bca6 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts @@ -0,0 +1,307 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {IdlObject, IdlService } from '@eg/core/idl.service'; +import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + templateUrl: './survey-edit.component.html' +}) + +export class SurveyEditComponent implements OnInit { + surveyId: number; + surveyObj: IdlObject; + localArray: any; + newAnswerArray: object[]; + newQuestionText: string; + surveyTab: string; + + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + + @ViewChild('createAnswerString', { static: true }) + createAnswerString: StringComponent; + @ViewChild('createAnswerErrString', { static: true }) + createAnswerErrString: StringComponent; + @ViewChild('createQuestionString', { static: true }) + createQuestionString: StringComponent; + @ViewChild('createQuestionErrString', { static: true }) + createQuestionErrString: StringComponent; + + @ViewChild('updateQuestionSuccessStr', { static: true }) + updateQuestionSuccessStr: StringComponent; + @ViewChild('updateQuestionFailStr', { static: true }) + updateQuestionFailStr: StringComponent; + @ViewChild('updateAnswerSuccessStr', { static: true }) + updateAnswerSuccessStr: StringComponent; + @ViewChild('updateAnswerFailStr', { static: true }) + updateAnswerFailStr: StringComponent; + + @ViewChild('delAnswerSuccessStr', { static: true }) + delAnswerSuccessStr: StringComponent; + @ViewChild('delAnswerFailStr', { static: true }) + delAnswerFailStr: StringComponent; + @ViewChild('delQuestionSuccessStr', { static: true }) + delQuestionSuccessStr: StringComponent; + @ViewChild('delQuestionFailStr', { static: true }) + delQuestionFailStr: StringComponent; + + @ViewChild('endSurveyFailedString', { static: true }) + endSurveyFailedString: StringComponent; + @ViewChild('endSurveySuccessString', { static: true }) + endSurveySuccessString: StringComponent; + @ViewChild('questionAlreadyStartedErrString', { static: true }) + questionAlreadyStartedErrString: StringComponent; + + constructor( + private auth: AuthService, + private net: NetService, + private route: ActivatedRoute, + private toast: ToastService, + private idl: IdlService, + ) { + } + + ngOnInit() { + this.surveyId = parseInt(this.route.snapshot.paramMap.get('id'), 10); + this.updateData(); + } + + updateData() { + this.newQuestionText = ''; + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.fleshed.retrieve', + this.surveyId + ).subscribe(res => { + this.surveyObj = res; + this.buildLocalArray(res); + return res; + }); + } + + onTabChange(event: NgbTabChangeEvent) { + this.surveyTab = event.nextId; + } + + buildLocalArray(res) { + this.localArray = []; + this.newAnswerArray = []; + const allQuestions = res.questions(); + allQuestions.forEach((question, index) => { + this.newAnswerArray.push({inputText: ''}); + question.words = question.question(); + question.answers = question.answers(); + this.localArray.push(question); + question.answers.forEach(answer => { + answer.words = answer.answer(); + }); + this.sortAnswers(index); + }); + this.sortQuestions(); + } + + sortQuestions() { + this.localArray.sort(function(a, b) { + const q1 = a.question().toUpperCase(); + const q2 = b.question().toUpperCase(); + return (q1 < q2) ? -1 : (q1 > q2) ? 1 : 0; + }); + } + + sortAnswers(questionIndex) { + this.localArray[questionIndex].answers.sort(function(a, b) { + const a1 = a.answer().toUpperCase(); + const a2 = b.answer().toUpperCase(); + return (a1 < a2) ? -1 : (a1 > a2) ? 1 : 0; + }); + } + + updateQuestion(questionToChange) { + if (this.surveyHasBegun()) { + return; + } + questionToChange.question(questionToChange.words); + questionToChange.ischanged(true); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.update', + this.auth.token(), this.surveyObj + ).subscribe(res => { + if (res.debug) { + this.updateQuestionFailStr.current().then(msg => this.toast.warning(msg)); + return res; + } else { + this.surveyObj = res; + this.buildLocalArray(this.surveyObj); + this.updateQuestionSuccessStr.current().then(msg => this.toast.success(msg)); + return res; + } + }); + } + + deleteQuestion(questionToDelete) { + if (this.surveyHasBegun()) { + return; + } + questionToDelete.isdeleted(true); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.update', + this.auth.token(), this.surveyObj + ).subscribe(res => { + if (res.debug) { + this.delQuestionFailStr.current().then(msg => this.toast.warning(msg)); + return res; + } else { + this.surveyObj = res; + this.buildLocalArray(this.surveyObj); + this.delQuestionSuccessStr.current().then(msg => this.toast.success(msg)); + return res; + } + + }); + } + + createQuestion(newQuestionText) { + if (this.surveyHasBegun()) { + return; + } + const newQuestion = this.idl.create('asvq'); + newQuestion.question(newQuestionText); + newQuestion.isnew(true); + let questionObjects = []; + questionObjects = this.surveyObj.questions(); + questionObjects.push(newQuestion); + this.surveyObj.questions(questionObjects); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.update', + this.auth.token(), this.surveyObj + ).subscribe(res => { + if (res.debug) { + this.newQuestionText = ''; + this.createQuestionErrString.current().then(msg => this.toast.warning(msg)); + return res; + } else { + this.surveyObj = res; + this.buildLocalArray(this.surveyObj); + this.newQuestionText = ''; + this.createQuestionString.current().then(msg => this.toast.success(msg)); + return res; + } + + }); + } + + deleteAnswer(answerObj) { + if (this.surveyHasBegun()) { + return; + } + answerObj.isdeleted(true); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.update', + this.auth.token(), this.surveyObj + ).subscribe(res => { + if (res.debug) { + this.delAnswerFailStr.current().then(msg => this.toast.warning(msg)); + return res; + } else { + this.surveyObj = res; + this.buildLocalArray(this.surveyObj); + this.delAnswerSuccessStr.current().then(msg => this.toast.success(msg)); + return res; + } + }); + } + + updateAnswer(answerObj) { + if (this.surveyHasBegun()) { + return; + } + answerObj.answer(answerObj.words); + answerObj.ischanged(true); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.update', + this.auth.token(), this.surveyObj + ).subscribe(res => { + if (res.debug) { + this.updateAnswerFailStr.current().then(msg => this.toast.warning(msg)); + return res; + } else { + this.surveyObj = res; + this.buildLocalArray(this.surveyObj); + this.updateAnswerSuccessStr.current().then(msg => this.toast.success(msg)); + return res; + } + }); + } + + createAnswer(newAnswerText, questionObj) { + // Create answer *is* allowed if survey has already begun + const questionId = questionObj.id(); + const newAnswer = this.idl.create('asva'); + newAnswer.answer(newAnswerText); + newAnswer.question(questionId); + newAnswer.isnew(true); + questionObj.answers.push(newAnswer); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.update', + this.auth.token(), this.surveyObj + ).subscribe(res => { + if (res.debug) { + this.createAnswerErrString.current().then(msg => this.toast.warning(msg)); + return res; + } else { + this.surveyObj = res; + this.buildLocalArray(this.surveyObj); + this.createAnswerString.current().then(msg => this.toast.success(msg)); + return res; + } + }); + } + + endSurvey() { + const today = new Date().toISOString(); + this.surveyObj.end_date(today); + this.surveyObj.ischanged(true); + // to get fm-editor to display changed date we need to set + // this.surveyObj to null temporarily + const surveyClone = this.idl.clone(this.surveyObj); + this.surveyObj = null; + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.update', + this.auth.token(), surveyClone + ).subscribe(res => { + if (res.debug) { + this.endSurveyFailedString.current().then(msg => this.toast.warning(msg)); + return res; + } else { + this.surveyObj = res; + this.surveyObj.ischanged(false); + this.buildLocalArray(this.surveyObj); + this.endSurveySuccessString.current().then(msg => this.toast.success(msg)); + return res; + } + }); + } + + surveyHasBegun() { + const surveyStartDate = new Date(this.surveyObj.start_date()); + const now = new Date(); + if (surveyStartDate <= now) { + this.questionAlreadyStartedErrString.current().then(msg => + this.toast.warning(msg)); + return true; + } + return false; + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts new file mode 100644 index 0000000000..cd36869e70 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts @@ -0,0 +1,22 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {SurveyComponent} from './survey.component'; +import {SurveyEditComponent} from './survey-edit.component'; + +const routes: Routes = [{ + path: '', + component: SurveyComponent +}, { + path: ':id', + component: SurveyEditComponent +}]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class SurveyRoutingModule {} + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html new file mode 100644 index 0000000000..394d837c16 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html @@ -0,0 +1,37 @@ +<eg-staff-banner bannerText="Survey Configuration" i18n-bannerText> +</eg-staff-banner> + +<eg-grid #grid idlClass="asv" [dataSource]="gridDataSource" +[sortable]="true"> + <eg-grid-toolbar-button label="New Survey" i18n-label [action]="createNew"> + </eg-grid-toolbar-button> + <eg-grid-toolbar-action label="Edit Selected" i18n-label [action]="editSelected"> + </eg-grid-toolbar-action> + <eg-grid-toolbar-action label="Delete Selected" i18n-label + (onClick)="deleteSelected($event)"></eg-grid-toolbar-action> + <eg-grid-toolbar-action label="End Survey Now" i18n-label + (onClick)="endSurvey($event)"></eg-grid-toolbar-action> +</eg-grid> + +<eg-fm-record-editor + datetimeFieldsList="start_date,end_date" + hiddenFieldsList="id" + #editDialog + idlClass="asv"> +</eg-fm-record-editor> + +<eg-string #createString i18n-text text="New Survey Added"></eg-string> +<eg-string #createErrString i18n-text text="Failed to Create New Survey"> + </eg-string> +<eg-string #endSurveyFailedString i18n-text + text="Ending Survey failed or was not allowed"></eg-string> +<eg-string #endSurveySuccessString i18n-text text="Survey ended"> + </eg-string> +<eg-string #deleteFailedString i18n-text + text="Delete of Survey failed or was not allowed"></eg-string> +<eg-string #deleteSuccessString i18n-text text="Delete of Survey succeeded"> + </eg-string> +<eg-string #successString i18n-text text="Update of Survey succeeded"> + </eg-string> +<eg-string #updateFailedString i18n-text text="Update of Survey succeeded"> + </eg-string> diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts new file mode 100644 index 0000000000..9865a4b3e6 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts @@ -0,0 +1,122 @@ +import {Pager} from '@eg/share/util/pager'; +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {Router} from '@angular/router'; +import {IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; + +@Component({ + templateUrl: './survey.component.html' +}) + +export class SurveyComponent implements OnInit { + + gridDataSource: GridDataSource; + + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('grid', { static: true }) grid: GridComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('createString', { static: true }) createString: StringComponent; + @ViewChild('createErrString', { static: true }) createErrString: StringComponent; + @ViewChild('updateFailedString', { static: true }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + @ViewChild('endSurveyFailedString', { static: true }) endSurveyFailedString: StringComponent; + @ViewChild('endSurveySuccessString', { static: true }) endSurveySuccessString: StringComponent; + + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + + constructor( + private auth: AuthService, + private net: NetService, + private pcrud: PcrudService, + private toast: ToastService, + private router: Router + ) { + this.gridDataSource = new GridDataSource(); + } + + ngOnInit() { + this.gridDataSource.getRows = (pager: Pager, sort: any[]) => { + return this.pcrud.retrieveAll('asv', {}); + }; + + this.grid.onRowActivate.subscribe( + (idlThing: IdlObject) => { + const idToEdit = idlThing.id(); + this.navigateToEditPage(idToEdit); + } + ); + } + + showEditDialog(idlThing: IdlObject): Promise<any> { + return; + } + + editSelected = (surveys: IdlObject[]) => { + const idToEdit = surveys[0].id(); + this.navigateToEditPage(idToEdit); + } + + endSurvey = (surveys: IdlObject[]) => { + const today = new Date().toISOString(); + for (let i = 0; i < surveys.length; i++) { + surveys[i].end_date(today); + this.pcrud.update(surveys[i]).toPromise().then( + async (ok) => { + this.toast.success(await this.endSurveySuccessString.current()); + }, + async (err) => { + this.toast.warning(await this.endSurveyFailedString.current()); + } + ); + } + } + + deleteSelected = (surveys: IdlObject[]) => { + for (let i = 0; i < surveys.length; i++) { + const idToDelete = surveys[i].id(); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.survey.delete.cascade.override', + this.auth.token(), idToDelete + ).subscribe(res => { + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + return res; + }, (err) => { + this.deleteFailedString.current() + .then(str => this.toast.success(str)); + }); + } + } + + navigateToEditPage(id: any) { + this.router.navigate(['/staff/admin/local/action/survey/' + id]); + } + + createNew = () => { + this.editDialog.mode = 'create'; + this.editDialog.datetimeFields = 'start_date,end_date'; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts new file mode 100644 index 0000000000..21ba53f64e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts @@ -0,0 +1,25 @@ +import {NgModule} from '@angular/core'; +import {AdminCommonModule} from '@eg/staff/admin/common.module'; +import {SurveyComponent} from './survey.component'; +import {FormsModule} from '@angular/forms'; +import {SurveyEditComponent} from './survey-edit.component'; +import {SurveyRoutingModule} from './survey-routing.module'; + +@NgModule({ + declarations: [ + SurveyComponent, + SurveyEditComponent + ], + imports: [ + AdminCommonModule, + SurveyRoutingModule, + FormsModule, + ], + exports: [ + ], + providers: [ + ] +}) + +export class SurveyModule { +} diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Survey.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Survey.pm index 3b83e36a2b..2f684c4b32 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Survey.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Survey.pm @@ -22,9 +22,155 @@ use Data::Dumper; use OpenILS::Event; use Time::HiRes qw(time); use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenSRF::Utils::Logger qw/$logger/; my $apputils = "OpenILS::Application::AppUtils"; +__PACKAGE__->register_method( + method => "update_survey", + api_name => "open-ils.circ.survey.update", + signature => { + desc => q/Create, update, delete surveys, survey questions, and + survey answers. Relies on isnew ; isnchanged ; isdeleted + attributes of provided objects to determine outcome. + /, + params => [ + {desc => 'Authtoken', type => 'string'}, + {desc => 'Fleshed survey (asv) object', type => 'object'} + ], + return => '1 on success, event on error' + } +); + + +sub update_survey { + my ($self, $client, $auth, $survey) = @_; + + my $e = new_editor(authtoken => $auth, xact => 1); + return $e->die_event unless $e->checkauth; + return $e->die_event unless $e->allowed('ADMIN_SURVEY', $survey->owner); + + my $questions = $survey->questions || []; + + if ($survey->isdeleted) { + + $questions = $e->search_action_survey_question({survey => $survey->id}); + $_->isdeleted(1) for @$questions; + $survey->questions($questions); + + # Remove dependent data first. + return $e->die_event if update_questions($e, $survey); + return $e->die_event unless $e->delete_action_survey($survey); + + } else { + + if ($survey->isnew) { + + $survey->clear_id; + return $e->die_event unless $e->create_action_survey($survey); + + $_->isnew(1) for @$questions; + + } elsif ($survey->ischanged) { + + return $e->die_event unless $e->update_action_survey($survey); + } + + return $e->die_event if update_questions($e, $survey); + } + + $e->commit; + + $e->xact_begin; + $survey = $e->retrieve_action_survey([ + $survey->id, + { flesh => 2, + flesh_fields => {asv => ['questions'], 'asvq' => ['answers']} + } + ]); + $e->rollback; + + return $survey; +} + +# returns undef on success, event on error +sub update_questions { + my ($e, $survey) = @_; + + for my $question (@{$survey->questions}) { + + if ($question->isdeleted) { + + # Get the full set + my $answers = + $e->search_action_survey_answer({question => $question->id}); + $_->isdeleted(1) for @$answers; + $question->answers($answers); + + # Delete linked objects first. + return 1 if update_answers($e, $question); + return $e->die_event + unless $e->delete_action_survey_question($question); + + } else { + + if ($question->ischanged) { + + return $e->die_event + unless $e->update_action_survey_question($question); + + } elsif ($question->isnew) { + + $question->survey($survey->id); + $question->clear_id; + + return $e->die_event unless $e->create_action_survey_question($question); + } + + return 1 if update_answers($e, $question); + } + } + + return undef; +} + +sub update_answers { + my ($e, $question) = @_; + + return undef unless $question->answers; + + for my $answer (@{$question->answers}) { + + if ($answer->isdeleted) { + my $responses = + $e->search_action_survey_response({answer => $answer->id}); + + for my $response (@$responses) { + return $e->die_event unless + $e->delete_action_survey_response($response); + } + + return $e->die_event unless $e->delete_action_survey_answer($answer); + + } elsif ($answer->isnew) { + + $answer->clear_id; + $answer->question($question->id); + + return $e->die_event + unless $e->create_action_survey_answer($answer); + + } elsif ($answer->ischanged) { + return $e->die_event + unless $e->update_action_survey_answer($answer); + } + } + + return undef; +} + + + # - creates a new survey # expects a survey complete with questions and answers __PACKAGE__->register_method( @@ -78,10 +224,6 @@ sub _add_survey { return $survey; } -sub _update_survey { - my($session, $survey) = @_; -} - sub _add_questions { my($session, $survey) = @_; @@ -423,4 +565,4 @@ sub delete_survey { -1; +1; \ No newline at end of file -- 2.11.0