diff --git a/buildConfig/data/staging/form/form-user_manageduser_create_v2.json b/buildConfig/data/staging/form/form-user_manageduser_create_v2.json new file mode 100644 index 0000000000..0be722216e --- /dev/null +++ b/buildConfig/data/staging/form/form-user_manageduser_create_v2.json @@ -0,0 +1,51 @@ +{ + "id": "api.form.read", + "params": { + "resmsgid": "e6af6255-4c10-440d-ba35-9fc870a04163", + "msgid": "573d138b-846d-4b26-9c21-a173433073c0", + "status": "successful" + }, + "responseCode": "OK", + "result": { + "form": { + "type": "user", + "subType": "manageduser", + "action": "create_v2", + "component": "app", + "framework": "*", + "rootOrgId": "*", + "data": { + "templateName": "manageduser", + "action": "create", + "fields": [ + { + "code": "name", + "type": "input", + "templateOptions": { + "label": "FULL_NAME", + "placeholder": "ENTER_USER_NAME" + }, + "validations": [ + { + "type": "required", + "value": true, + "message": "NAME_IS_REQUIRED" + } + ] + }, + { + "code": "updatePreference", + "type": "label", + "templateOptions": { + "label": "PREFERENCES_CAN_BE_UPDATED" + } + } + ] + }, + "created_on": "2019-06-28T05:55:40.560Z", + "last_modified_on": null + } + }, + "ts": "2019-06-28T07:21:30.126Z", + "ver": "1.0" +} \ No newline at end of file diff --git a/main.js b/main.js index 326e3b7640..b1ce45f669 100644 --- a/main.js +++ b/main.js @@ -54,6 +54,11 @@ var formRequestArray = [{ 'subType': 'manageduser', 'action': 'create' }, + { + 'type': 'user', + 'subType': 'manageduser', + 'action': 'create_v2' + }, { 'type': 'group', 'subType': 'activities', diff --git a/src/app/components/common-forms/common-forms.component.html b/src/app/components/common-forms/common-forms.component.html index 7c7a9db08f..50a8afd33f 100644 --- a/src/app/components/common-forms/common-forms.component.html +++ b/src/app/components/common-forms/common-forms.component.html @@ -1,52 +1,115 @@ -
- - - - - - {{ formElement.templateOptions?.label | translate }} - - -
- {{ validation.message | translate }} +
+ + +
+
+ + + {{ field.templateOptions?.label | translate }} + +  * + + + + + + + {{option?.label}} + + + + + + {{option?.label}} + + + + + + +
+
+ +
+ + + + {{ field.templateOptions?.label | translate }} + +  * + + + +
+ + {{field.templateOptions?.prefix}} + + + + + + verification success + verification failure + empty field + + +
+ + +
+ {{ validation.message | translate }} +
+
+
+ + +
+ + {{field.asyncValidation.trigger}} +
- -
- - - {{ formElement.templateOptions?.label | translate }} - - - {{selectElement?.label}} - - - - -
+ + +
+ +
- +
- - {{ formElement.templateOptions?.label | translate}} + + {{ field.templateOptions?.label | translate}} - -
+ +
-
- {{formElement.templateOptions?.label | translate}} +
+ {{field.templateOptions?.label | translate}}
- - + + + + +
diff --git a/src/app/components/common-forms/common-forms.component.scss b/src/app/components/common-forms/common-forms.component.scss index b0571c0d2c..321eaf3dae 100644 --- a/src/app/components/common-forms/common-forms.component.scss +++ b/src/app/components/common-forms/common-forms.component.scss @@ -78,7 +78,6 @@ .item-label-stacked ion-input { border-radius: 5px; - margin-top: 16px; padding-left: 16px !important; padding-right: 16px !important; font-size: 14px; @@ -86,11 +85,11 @@ .cf-input-primary ion-input{ font-weight: bold; - border: 1px solid map-get($colors, primary); + border: none; --placeholder-opacity: 0.3 !important; } - .cf-input-error ion-input{ + .cf-input-error{ border: 1px solid red; } @@ -186,4 +185,47 @@ ion-checkbox { margin-right: 8px; --background-checked: #{$blue} !important; --border-color-checked: #{$blue} !important; +} + +.merged-input-container { + border: 1px solid map-get($colors, primary); + display: flex; + flex: 1 1 auto; + border-radius: 6px; + .decorator { + display: inline-block; + max-width: 50px; + } + .custom { + display: inline-block; + } + ion-input{ + border: none; + } + span{ + font-size: 14px; + opacity: 0.7; + margin: auto; + } + +} + +.verification-btn{ + text-align: center; + ion-button { + --background: #008840 !important; + } +} + +.otp-validator{ + padding-left: 8px; + padding-right: 8px; +} + +.prefix{ + padding-left: 8px;; +} + +.required-star{ + color: red; } \ No newline at end of file diff --git a/src/app/components/common-forms/common-forms.component.ts b/src/app/components/common-forms/common-forms.component.ts index 54e693d6cf..b6aee0f41f 100644 --- a/src/app/components/common-forms/common-forms.component.ts +++ b/src/app/components/common-forms/common-forms.component.ts @@ -1,192 +1,241 @@ -import { Component, Input, OnInit, Output, Inject, EventEmitter } from '@angular/core'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - +import {Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, QueryList, ViewChildren, AfterViewInit} from '@angular/core'; +import { FieldConfig, FieldConfigInputType, FieldConfigValidationType, FieldConfigOption, FieldConfigOptionsBuilder } from './field-config'; +import {FormBuilder, FormGroup, Validators, FormControl} from '@angular/forms'; +import {Subject, Subscription, Observable} from 'rxjs'; +import {distinctUntilChanged, map, scan, tap} from 'rxjs/operators'; import { CommonUtilService } from '@app/services/common-util.service'; -import { SharedPreferences } from 'sunbird-sdk'; - -enum InputType { - INPUT = 'input', - CHECKBOX = 'checkbox', - SELECT = 'select', - LABEL = 'label' -} - -enum ValidationType { - REQUIRED = 'required', - PATTERN = 'pattern', - MINLENGTH = 'minLength', - MAXLENGTH = 'maxLength' -} +import { ValueComparator } from './value-comparator'; @Component({ selector: 'app-common-forms', templateUrl: './common-forms.component.html', styleUrls: ['./common-forms.component.scss'], }) -export class CommonFormsComponent implements OnInit { - -// template -// value -// value changes -// submit -// reset - - @Input() formList: any = []; - @Output() onFormDataChange = new EventEmitter(); - @Output() onCommonFormInitialized = new EventEmitter() - - commonFormGroup: FormGroup; - formInputTypes = InputType; - formValidationTypes = ValidationType; - appName = ''; - +export class CommonFormsComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit { + @Output() initialize = new EventEmitter(); + @Output() finalize = new EventEmitter(); + @Output() valueChanges = new EventEmitter(); + @Output() statusChanges = new EventEmitter(); + @Output() dataLoadStatus = new EventEmitter<'LOADING' | 'LOADED'>(); + @Input() config; + @Input() dataLoadStatusDelegate = new Subject<'LOADING' | 'LOADED'>(); + @ViewChildren('validationTrigger') validationTriggers: QueryList; + + formGroup: FormGroup; + FieldConfigInputType = FieldConfigInputType; + ValueComparator = ValueComparator; + optionsMap$: {[code: string]: Observable[]>} = {}; + requiredFieldsMap: {[code: string]: boolean} = {}; + + private statusChangesSubscription: Subscription; + private valueChangesSubscription: Subscription; + private dataLoadStatusSinkSubscription: Subscription; constructor( - @Inject('SHARED_PREFERENCES') private sharedPreferences: SharedPreferences, private formBuilder: FormBuilder, - private commonUtilService: CommonUtilService, - ) { } + private commonUtilService: CommonUtilService + ) { + if (!window['forms']) { + window['forms'] = []; + } + window['forms'].push(this); + } + ngOnDestroy(): void { + this.finalize.emit(); + if (this.statusChangesSubscription) { + this.statusChangesSubscription.unsubscribe(); + } + if (this.valueChangesSubscription) { + this.valueChangesSubscription.unsubscribe(); + } + if (this.dataLoadStatusSinkSubscription) { + this.dataLoadStatusSinkSubscription.unsubscribe(); + } + } + ngOnInit() { + } + ngOnChanges(changes: SimpleChanges): void { + if (changes['config']) { + if ((changes['config'].currentValue && changes['config'].firstChange) || changes['config'].previousValue !== changes['config'].currentValue) { + this.initializeForm(); + + changes['config'].currentValue.forEach((config: FieldConfig) => { + if (config.validations && config.validations.length) { + this.requiredFieldsMap[config.code] = !!config.validations.find(val => val.type === FieldConfigValidationType.REQUIRED); + } + if (!config.templateOptions) { + return; + } + if (!config.templateOptions.options) { + config.templateOptions.options = []; + } + if (this.isOptionsClosure(config.templateOptions.options)) { + this.optionsMap$[config.code] = (config.templateOptions.options as FieldConfigOptionsBuilder)( + this.formGroup.get(config.code) as FormControl, + this.formGroup.get(config.context) as FormControl, + () => this.dataLoadStatusDelegate.next('LOADING'), + () => this.dataLoadStatusDelegate.next('LOADED') + ) as any; + } + }); + } + } + if (this.dataLoadStatusSinkSubscription) { + this.dataLoadStatusSinkSubscription.unsubscribe(); + } + if (this.statusChangesSubscription) { + this.statusChangesSubscription.unsubscribe(); + } + if (this.valueChangesSubscription) { + this.valueChangesSubscription.unsubscribe(); + } + this.dataLoadStatusSinkSubscription = this.dataLoadStatusDelegate.pipe( + scan<'LOADING' | 'LOADED', { loadingCount: 0, loadedCount: 0 }>((acc, event) => { + if (event === 'LOADED') { + acc.loadedCount++; + return acc; + } + acc.loadingCount++; + return acc; + }, {loadingCount: 0, loadedCount: 0}), + map<{ loadingCount: 0, loadedCount: 0 }, 'LOADING' | 'LOADED'>((aggregates) => { + if (aggregates.loadingCount !== aggregates.loadedCount) { + return 'LOADING'; + } + return 'LOADED'; + }), + distinctUntilChanged(), + tap((result) => { + if (result === 'LOADING') { + this.dataLoadStatus.emit('LOADING'); + } else { + this.dataLoadStatus.emit('LOADED'); + } + }) + ).subscribe(); + this.statusChangesSubscription = this.formGroup.valueChanges.pipe( + tap((v) => { + this.statusChanges.emit({ + isPristine: this.formGroup.pristine, + isDirty: this.formGroup.dirty, + isInvalid: this.formGroup.invalid, + isValid: this.formGroup.valid + }); + }) + ).subscribe(); + this.valueChangesSubscription = this.formGroup.valueChanges.pipe( + tap((v) => { + this.valueChanges.emit(v); + }) + ).subscribe(); + } - ngOnInit(): void { - this.initilizeForm(); - this.sharedPreferences.getString('app_name').toPromise().then(value => { - this.appName = value; + ngAfterViewInit() { + this.config.forEach(element => { + if ( element.asyncValidation && element.asyncValidation.asyncValidatorFactory && this.formGroup.get(element.code)) { + this.formGroup.get(element.code).setAsyncValidators(element.asyncValidation.asyncValidatorFactory( + element.asyncValidation.marker, + this.validationTriggers + )); + } }); } - initilizeForm() { - if (!this.formList.length) { + onNestedFormFinalize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig) { + if (!this.formGroup.get('children') || !this.formGroup.get(`children.${fieldConfig.code}`)) { + return; + } + (this.formGroup.get('children') as FormGroup).removeControl(fieldConfig.code); + if (!Object.keys((this.formGroup.get('children') as FormGroup).controls).length) { + this.formGroup.removeControl('children'); + } + } + onNestedFormInitialize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig) { + if (!this.formGroup.get('children')) { + this.formGroup.addControl('children', new FormGroup({})); + } + (this.formGroup.get('children') as FormGroup).addControl(fieldConfig.code, nestedFormGroup); + } + private initializeForm() { + if (!this.config.length) { console.error('FORM LIST IS EMPTY'); return; } const formGroupData = {}; - this.formList.forEach((element: any, index) => { - if (element.type !== this.formInputTypes.LABEL) { + this.config.forEach((element: any, index) => { + if (element.type !== FieldConfigInputType.LABEL) { const formValueList = this.prepareFormValidationData(element, index); formGroupData[element.code] = formValueList; } }); - - this.commonFormGroup = this.formBuilder.group(formGroupData); - setTimeout(() => { - this.onCommonFormInitialized.emit(true); - }, 100); + this.formGroup = this.formBuilder.group(formGroupData); + this.initialize.emit(this.formGroup); } - - /** - * @return [''/0/[]/false, Validator.required] - */ - private prepareFormValidationData(element, index) { + private prepareFormValidationData(element: FieldConfig, index) { const formValueList = []; const validationList = []; - let defaultVal: any = ''; switch (element.type) { - case this.formInputTypes.INPUT: - defaultVal = element.templateOptions.type === 'number' ? 0 : ''; + case FieldConfigInputType.INPUT: + defaultVal = element.templateOptions.type === 'number' ? + (element.default && Number.isInteger(element.default) ? element.default : 0) : + (element.default && (typeof element.default) === 'string' ? element.default : ''); break; - case this.formInputTypes.SELECT: - defaultVal = element.templateOptions.multiple ? [] : ''; + case FieldConfigInputType.SELECT: + case FieldConfigInputType.NESTED_SELECT: + defaultVal = element.templateOptions.multiple ? + (element.default && Array.isArray(element.default) ? element.default : []) : (element.default || null); break; - case this.formInputTypes.CHECKBOX: - defaultVal = false; + case FieldConfigInputType.CHECKBOX: + defaultVal = false || !!element.default; break; } formValueList.push(defaultVal); - if (element.validations && element.validations.length) { element.validations.forEach((data, i) => { switch (data.type) { - case this.formValidationTypes.REQUIRED: - validationList.push(element.type === this.formInputTypes.CHECKBOX ? Validators.requiredTrue : Validators.required); - if (this.formList[index].templateOptions && this.formList[index].templateOptions.label) { - this.formList[index].templateOptions.label = - this.commonUtilService.translateMessage(this.formList[index].templateOptions.label) + ' *'; + case FieldConfigValidationType.REQUIRED: + if (element.type === FieldConfigInputType.CHECKBOX) { + validationList.push(Validators.requiredTrue); + } else if (element.type === FieldConfigInputType.SELECT || element.type === FieldConfigInputType.NESTED_SELECT) { + validationList.push((c) => { + if (element.templateOptions.multiple) { + return c.value && c.value.length ? null : 'error'; + } + return !!c.value ? null : 'error'; + }); + } else { + validationList.push(Validators.required); } break; - case this.formValidationTypes.PATTERN: - validationList.push(Validators.pattern(element.validations[i].value)); + case FieldConfigValidationType.PATTERN: + validationList.push(Validators.pattern(element.validations[i].value as string)); break; - case this.formValidationTypes.MINLENGTH: - validationList.push(Validators.minLength(element.validations[i].value)); + case FieldConfigValidationType.MINLENGTH: + validationList.push(Validators.minLength(element.validations[i].value as number)); break; - case this.formValidationTypes.MAXLENGTH: - validationList.push(Validators.maxLength(element.validations[i].value)); + case FieldConfigValidationType.MAXLENGTH: + validationList.push(Validators.maxLength(element.validations[i].value as number)); break; } }); } - formValueList.push(Validators.compose(validationList)); - return formValueList; } - fetchInterfaceOption(fieldName) { - return { - header: this.commonUtilService.translateMessage(fieldName).toLocaleUpperCase(), - cssClass: 'select-box', - animated: false - }; - } - - onInputChange(event) { - setTimeout(() => { - this.onFormDataChange.emit(this.commonFormGroup); - }, 0); + isOptionsArray(options: any) { + return Array.isArray(options); } - initilizeInputData(data) { - this.commonFormGroup.patchValue({[data.code]: data.value}); + isOptionsClosure(options: any) { + return typeof options === 'function'; } - initilizeFormData(data) { - for (let index = 0; index < this.formList.length; index++) { - const formDetails = this.formList[index]; - if (formDetails.code === data.code && formDetails.templateOptions && formDetails.templateOptions.link && - formDetails.templateOptions.link.label) { - this.setFormData(index, data.path, data.value); - } - - if (formDetails.code === data.code && formDetails.templateOptions && formDetails.templateOptions.options) { - this.setFormData(index, data.path, data.value); - } - } - } - - setFormData(index, path, value) { - path.reduce((a, b, level) => { - if (typeof a[b] === 'undefined' && level !== path.length - 1) { - a[b] = {}; - return a[b]; - } - if (level === path.length - 1) { - a[b] = value; - return value; - } - return a[b]; - }, this.formList[index]); - console.log(this.formList[index]); -} - - showInAppBrowser(url) { - this.commonUtilService.openLink(url); - } - - handleClick(event: MouseEvent) { + handleLinkClick(event: MouseEvent) { if (event.target && event.target['hasAttribute'] && (event.target as HTMLAnchorElement).hasAttribute('href')) { this.commonUtilService.openLink((event.target as HTMLAnchorElement).getAttribute('href')); } } - checkDisableCondition(formElement) { - if (formElement.templateOptions && formElement.templateOptions.prefill && formElement.templateOptions.prefill.length) { - for (let index = 0; index < formElement.templateOptions.prefill.length; index++) { - if (!(this.commonFormGroup.value[formElement.templateOptions.prefill[index].code]).length) { - return true; - } - } - } - return false; - } - } + diff --git a/src/app/components/common-forms/field-config.ts b/src/app/components/common-forms/field-config.ts new file mode 100644 index 0000000000..d1835e2f0a --- /dev/null +++ b/src/app/components/common-forms/field-config.ts @@ -0,0 +1,66 @@ +import { Observable } from 'rxjs'; +import { FormControl, AsyncValidatorFn } from '@angular/forms'; +import { QueryList } from '@angular/core'; + +export enum FieldConfigInputType { + INPUT = 'input', + CHECKBOX = 'checkbox', + SELECT = 'select', + LABEL = 'label', + NESTED_SELECT = 'nested_select', + NESTED_GROUP = 'nested_group' +} + +export enum FieldConfigValidationType { + REQUIRED = 'required', + MAXLENGTH = 'maxLength', + MINLENGTH = 'minLength', + PATTERN = 'pattern' +} + +export type FieldConfigOptionsBuilder = + (control: FormControl, context?: FormControl, notifyLoading?: () => void, notifyLoaded?: () => void) => + Observable[]> | Promise[]>; + +export type AsyncValidatorFactory = (marker: string, triggers: QueryList) => AsyncValidatorFn; + +export interface FieldConfigOption { + label: string; + value: T; + extras?: T; +} + +export interface FieldConfigOptionAssociations { + [key: string]: FieldConfigOption[]; +} + +export interface FieldConfig { + code: string; + type: FieldConfigInputType | string; + default?: any; + context?: string; + children?: FieldConfig[]; + templateOptions: { + type?: string, + label?: string, + placeHolder?: string, + prefix?: string, + multiple?: boolean, + hidden?: boolean, + options?: FieldConfigOption[] | FieldConfigOptionsBuilder | FieldConfigOptionAssociations, + labelHtml?: { + contents: string, + values: {[key: string]: string} + } + }; + validations?: { + type: FieldConfigValidationType | string, + value?: string | boolean | number | RegExp, + message?: string + }[]; + asyncValidation?: { + marker: string, + trigger?: string, + asyncValidatorFactory?: AsyncValidatorFactory + }; +} diff --git a/src/app/components/common-forms/value-comparator.ts b/src/app/components/common-forms/value-comparator.ts new file mode 100644 index 0000000000..9ea0200462 --- /dev/null +++ b/src/app/components/common-forms/value-comparator.ts @@ -0,0 +1,67 @@ +export class ValueComparator { + static valueComparator(v1, v2): boolean { + if (typeof v1 === 'object' && typeof v2 === 'object') { + return ObjectUtil.equals(v1, v2); + } else if (v1 === v2) { + return true; + } else if (!v1 && !v2) { + return true; + } + return false; + } +} +class ObjectUtil { + public static equals(a: any, b: any): boolean { + const countProps = (obj) => { + let count = 0; + for (const k in obj) { + if (obj.hasOwnProperty(k)) { + count++; + } + } + return count; + }; + const objectEquals = (v1: any, v2: any) => { + if (typeof (v1) !== typeof (v2)) { + return false; + } + if (typeof (v1) === 'function') { + return v1.toString() === v2.toString(); + } + if (v1 instanceof Object && v2 instanceof Object) { + if (countProps(v1) !== countProps(v2)) { + return false; + } + let r = true; + for (const k in v1) { + r = objectEquals(v1[k], v2[k]); + if (!r) { + return false; + } + } + return true; + } else { + return v1 === v2; + } + }; + return objectEquals(a, b); + } + public static getPropDiff(newObj: {}, oldObj: {}): string[] { + return Object.keys(newObj).reduce((acc: string[], key) => { + if (ObjectUtil.equals(newObj[key], oldObj[key])) { + return acc; + } + acc.push(key); + return acc; + }, []); + } + public static getTruthyProps(obj: {}): string[] { + return Object.keys(obj).filter((key) => !!obj[key]); + } + public static toOrderedString(obj: {}): string { + return JSON.stringify(Object.keys(obj).sort().reduce<{}>((acc, k) => { + acc[k] = obj[k]; + return acc; + }, {})); + } +} diff --git a/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.scss b/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.scss index a63b61133a..a69c0326ee 100644 --- a/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.scss +++ b/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.scss @@ -31,7 +31,7 @@ } .ar-header { - background-color: map-get($colors, primary); + background-color: #{$blue}; color: map-get($colors, white); padding: 16px 0; } @@ -43,4 +43,17 @@ .custom-shadow { box-shadow: 0 -5px 5px -5px rgba(0, 0, 0, 0.2); +} + +ion-button{ + --background: #{$blue} !important; + color: map-get($colors, white); + --background-hover: #{$blue} !important; + --background-activated: #{$blue} !important; +} + +ion-button[fill="outline"]{ + --background: map-get($colors, white) !important; + color: #{$blue}; + --border-color: #{$blue}; } \ No newline at end of file diff --git a/src/app/components/popups/edit-contact-details-popup/edit-contact-details-popup.component.scss b/src/app/components/popups/edit-contact-details-popup/edit-contact-details-popup.component.scss index f8dead0916..8680110bc7 100644 --- a/src/app/components/popups/edit-contact-details-popup/edit-contact-details-popup.component.scss +++ b/src/app/components/popups/edit-contact-details-popup/edit-contact-details-popup.component.scss @@ -76,8 +76,22 @@ } .ecd-header { - background-color: map-get($colors, primary); + background-color: #{$blue}; color: map-get($colors, white); padding: 16px 0; } + + ion-button{ + --background: #{$blue} !important; + color: map-get($colors, white); + --background-hover: #{$blue} !important; + --background-activated: #{$blue} !important; + } + + ion-button[fill="outline"]{ + --background: map-get($colors, white) !important; + color: #{$blue}; + --border-color: #{$blue}; + } + } diff --git a/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.html b/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.html index e2ef704936..357ff8eb4e 100644 --- a/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.html +++ b/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.html @@ -1,8 +1,10 @@ +
+
{{title | titlecase}}
+
-

{{title}}

{{description}}

diff --git a/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.scss b/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.scss index 176d8097bd..c7f2a6cb61 100644 --- a/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.scss +++ b/src/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component.scss @@ -11,5 +11,29 @@ max-width: 45px; } } + + .ecv-header { + background-color: #{$blue}; + color: map-get($colors, white); + padding: 16px 0; + } + + ion-button{ + --background: #{$blue} !important; + color: map-get($colors, white); + --background-hover: #{$blue} !important; + --background-activated: #{$blue} !important; + } + + ion-button[fill="outline"]{ + --background: map-get($colors, white) !important; + color: #{$blue}; + --border-color: #{$blue}; + } + + ion-button[fill="clear"]{ + --background: map-get($colors, white) !important; + color: #{$blue}; + } } diff --git a/src/app/profile/profile.page.html b/src/app/profile/profile.page.html index 5a281e88c4..6d2f5ec2e2 100644 --- a/src/app/profile/profile.page.html +++ b/src/app/profile/profile.page.html @@ -178,6 +178,16 @@
+
+
{{'STATE' | translate }}:
+
{{selfDeclaredTeacherDetails?.state}}
+
+ +
+
{{'DISTRICT' | translate }}:
+
{{selfDeclaredTeacherDetails?.district}}
+
+
{{'SCHOOL_OR_ORG_NAME' | translate }}
{{selfDeclaredTeacherDetails?.schoolName}}
diff --git a/src/app/profile/profile.page.ts b/src/app/profile/profile.page.ts index a4d08d7ef6..7a71c95c89 100644 --- a/src/app/profile/profile.page.ts +++ b/src/app/profile/profile.page.ts @@ -6,7 +6,15 @@ import { IonRefresher, } from '@ionic/angular'; import { generateInteractTelemetry } from '@app/app/telemetryutil'; -import { ContentCard, ContentType, MimeType, ProfileConstants, RouterLinks, ContentFilterConfig } from '@app/app/app.constant'; +import { + ContentCard, + ContentType, + MimeType, + ProfileConstants, + RouterLinks, + ContentFilterConfig, + Location as loc +} from '@app/app/app.constant'; import { FormAndFrameworkUtilService } from '@app/services/formandframeworkutil.service'; import { AppGlobalService } from '@app/services/app-global-service.service'; import { CommonUtilService } from '@app/services/common-util.service'; @@ -31,7 +39,8 @@ import { CourseCertificate, SharedPreferences, CertificateAlreadyDownloaded, - NetworkError + NetworkError, + LocationSearchCriteria } from 'sunbird-sdk'; import { Environment, InteractSubtype, InteractType, PageId, ID } from '@app/services/telemetry-constants'; import { ActivatedRoute, Router, NavigationExtras } from '@angular/router'; @@ -102,6 +111,8 @@ export class ProfilePage implements OnInit { mappedTrainingCertificates: CourseCertificate[] = []; isDefaultChannelProfile$: Observable; selfDeclaredTeacherDetails: any; + private stateList: any; + constructor( @Inject('PROFILE_SERVICE') private profileService: ProfileService, @Inject('AUTH_SERVICE') private authService: AuthService, @@ -163,6 +174,7 @@ export class ProfilePage implements OnInit { } }); this.appName = await this.appVersion.getAppName(); + this.stateList = await this.commonUtilService.getStateList(); } ionViewWillEnter() { @@ -980,16 +992,29 @@ export class ProfilePage implements OnInit { }); } - getSelfDeclaredTeacherDetails() { + async getSelfDeclaredTeacherDetails() { this.selfDeclaredTeacherDetails = { + state: '', + district: '', schoolName: '', udiseId: '', teacherId: '' }; if (this.isCustodianOrgId && this.profile && this.profile.externalIds) { + let stateCode = ''; + let districtCode = ''; + let stateDetails; + let districtDetails; + this.profile.externalIds.forEach(ele => { switch (ele.idType) { + case 'declared-state': + stateCode = ele.id; + break; + case 'declared-district': + districtCode = ele.id; + break; case 'declared-school-name': this.selfDeclaredTeacherDetails.schoolName = ele.id; break; @@ -1001,7 +1026,18 @@ export class ProfilePage implements OnInit { break; } }); + if (stateCode && this.stateList && this.stateList.length) { + stateDetails = this.stateList.find(state => state.code === stateCode); + this.selfDeclaredTeacherDetails.state = (stateDetails && stateDetails.name) || ''; + } + if (stateDetails && stateDetails.id) { + const districtList = await this.commonUtilService.getDistrictList(stateDetails.id); + districtDetails = districtList.find(district => district.code === districtCode); + this.selfDeclaredTeacherDetails.district = (districtDetails && districtDetails.name) || ''; + } } } + + } diff --git a/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.html b/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.html index d1a064dc67..2c71271aa6 100644 --- a/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.html +++ b/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.html @@ -2,9 +2,12 @@

{{'PLEASE_PROVIDE_FOLLOWING_DETAILS' | translate}}

{{'UPDATE_DETAILS' | translate}}

-

{{'ADD_EDIT_SELF_DECLARED_TEACHER_INFO' | translate}}

- + + diff --git a/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.ts b/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.ts index 9e04d497eb..6ec2b6e9bc 100644 --- a/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.ts +++ b/src/app/profile/self-declared-teacher-edit/self-declared-teacher-edit.page.ts @@ -1,17 +1,45 @@ -import {Component, Inject, NgZone, ViewChild} from '@angular/core'; +import {Component, Inject, ViewChild} from '@angular/core'; import { - LocationSearchCriteria, ProfileService, - SharedPreferences, Profile, LocationSearchResult, CachedItemRequestSourceFrom, FormRequest, FormService, FrameworkService + LocationSearchCriteria, + ProfileService, + SharedPreferences, + LocationSearchResult, + CachedItemRequestSourceFrom, + FormRequest, + FormService, + FrameworkService, + AuditState, + CorrelationData, + TelemetryObject, + ServerProfile } from 'sunbird-sdk'; import { Location as loc, PreferenceKey } from '../../../app/app.constant'; -import { AppHeaderService, CommonUtilService, AppGlobalService, ID, TelemetryGeneratorService, InteractType, Environment, InteractSubtype, PageId, ImpressionType } from '@app/services'; +import { + AppHeaderService, + CommonUtilService, + AppGlobalService, + ID, + TelemetryGeneratorService, + InteractType, + Environment, + InteractSubtype, + PageId, + ImpressionType, + AuditType, + CorReleationDataType +} from '@app/services'; import { Router, ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; import { Events, PopoverController } from '@ionic/angular'; -import { Subscription } from 'rxjs'; +import { Subscription, of, defer } from 'rxjs'; import { Platform } from '@ionic/angular'; import { CommonFormsComponent } from '@app/app/components/common-forms/common-forms.component'; import { SbPopoverComponent } from '@app/app/components/popups/sb-popover/sb-popover.component'; +import { FieldConfig, FieldConfigOptionsBuilder, FieldConfigOption } from '@app/app/components/common-forms/field-config'; +import { FormValidationAsyncFactory } from '@app/services/form-validation-async-factory/form-validation-async-factory'; +import { AppVersion } from '@ionic-native/app-version/ngx'; +import { FormControl } from '@angular/forms'; +import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; @Component({ selector: 'app-self-declared-teacher-edit', @@ -21,18 +49,22 @@ import { SbPopoverComponent } from '@app/app/components/popups/sb-popover/sb-pop export class SelfDeclaredTeacherEditPage { private formValue: any; - private profile: any; + private profile: ServerProfile; private initialExternalIds: any; private backButtonFunc: Subscription; private availableLocationDistrict: string; private availableLocationState: string; + private loader: any + private latestFormValue: any; + private latestFormStatus: any; + private selectedStateCode: any; editType = 'add'; isFormValid = false; stateList: LocationSearchResult[] = []; districtList: LocationSearchResult[] = []; - teacherDetailsForm = []; - formInitilized = false; + teacherDetailsForm: FieldConfig[] = []; + appName = ''; @ViewChild('commonForms') commonForms: CommonFormsComponent; @@ -45,13 +77,13 @@ export class SelfDeclaredTeacherEditPage { private commonUtilService: CommonUtilService, private router: Router, private location: Location, - private appGlobalService: AppGlobalService, private events: Events, private platform: Platform, - private ngZone: NgZone, private activatedRoute: ActivatedRoute, private popoverCtrl: PopoverController, private telemetryGeneratorService: TelemetryGeneratorService, + private formValidationAsyncFactory: FormValidationAsyncFactory, + private appVersion: AppVersion ) { const navigation = this.router.getCurrentNavigation(); if (navigation && navigation.extras && navigation.extras.state) { @@ -75,7 +107,8 @@ export class SelfDeclaredTeacherEditPage { ); } - ionViewDidEnter() { + async ionViewDidEnter() { + this.appName = await this.appVersion.getAppName(); this.getTeacherDetailsFormApi(); } @@ -85,7 +118,7 @@ export class SelfDeclaredTeacherEditPage { from: CachedItemRequestSourceFrom.SERVER, type: 'user', subType: 'teacherDetails', - action: 'submit', + action: 'submit_v2', rootOrgId: rootOrgId || '*', component: 'app' }; @@ -97,13 +130,9 @@ export class SelfDeclaredTeacherEditPage { } if (formData && formData.form && formData.form.data) { - const data = formData.form.data.fields; - if (data.length) { - this.formInitilized = false; - setTimeout(() => { - this.teacherDetailsForm = data; - this.formInitilized = true; - }, 100); + const formConfig = formData.form.data.fields; + if (formConfig.length) { + this.initializeFormData(formConfig, !!rootOrgId); } } @@ -117,49 +146,96 @@ export class SelfDeclaredTeacherEditPage { }); } - async onCommonFormInitialized(event) { - this.initializeFormData(); - if (!this.stateList || !this.stateList.length) { - await this.getStates(); - } - } + async initializeFormData(formConfig, formLoaded) { + + this.teacherDetailsForm = formConfig.map((config: FieldConfig) => { + if (config.code === 'externalIds' && config.children) { + config.children = config.children.map((childConfig: FieldConfig) => { + + if (childConfig.templateOptions['dataSrc'] && childConfig.templateOptions['dataSrc'].marker === 'LOCATION_LIST') { + if (childConfig.templateOptions['dataSrc'].params.id === 'state') { + let stateCode; + if (this.selectedStateCode) { + stateCode = this.selectedStateCode; + } else { + let stateDetails; + if (this.profile.externalIds && this.profile.externalIds.length) { + stateDetails = this.profile.externalIds.find(eId => eId.idType === childConfig.code); + } + stateCode = stateDetails && stateDetails.id; + } + childConfig.templateOptions.options = this.buildStateListClosure(stateCode); + } else if (childConfig.templateOptions['dataSrc'].params.id === 'district') { + let districtDetails; + if (this.profile.externalIds && this.profile.externalIds.length) { + districtDetails = this.profile.externalIds.find(eId => eId.idType === childConfig.code); + } + childConfig.templateOptions.options = this.buildDistrictListClosure(districtDetails && districtDetails.id, formLoaded); + } + return childConfig; + } - initializeFormData() { - if (this.profile && this.profile.externalIds && this.profile.externalIds.length) { - this.initialExternalIds = {}; - this.profile.externalIds.forEach((externalData) => { - this.teacherDetailsForm.forEach((formData) => { - this.initialExternalIds[formData.code] = { - name: this.commonUtilService.translateMessage(formData.templateOptions.label) || '', - value: (this.initialExternalIds[formData.code] && this.initialExternalIds[formData.code].value) ? - this.initialExternalIds[formData.code].value : '', - }; - if (formData.code === externalData.idType) { - this.commonForms.commonFormGroup.patchValue({ - [formData.code]: externalData.id - }); - this.initialExternalIds[formData.code] = { - name: formData.templateOptions.label || '', - value: externalData.id - }; + if (childConfig.asyncValidation) { + if (childConfig.asyncValidation.marker === 'MOBILE_OTP_VALIDATION') { + childConfig.asyncValidation.asyncValidatorFactory = + this.formValidationAsyncFactory.mobileVerificationAsyncFactory(childConfig, this.profile); + } else if (childConfig.asyncValidation.marker === 'EMAIL_OTP_VALIDATION') { + childConfig.asyncValidation.asyncValidatorFactory = + this.formValidationAsyncFactory.emailVerificationAsyncFactory(childConfig, this.profile); + } + childConfig = this.assignDefaultValue(childConfig, formLoaded); + return childConfig; } + + this.assignDefaultValue(childConfig, formLoaded); + + return childConfig; }); - }); + return config; + } + + if (config.code === 'tnc') { + if (this.editType === 'edit') { + return undefined; + } + if (config.templateOptions && config.templateOptions.labelHtml && + config.templateOptions.labelHtml.contents) { + config.templateOptions.labelHtml.values['$url'] = this.profile.tncLatestVersionUrl; + config.templateOptions.labelHtml.values['$appName'] = ' ' + this.appName + ' '; + return config; + } + return config; + } + return config; + }).filter((formData) => formData); + + } + + private assignDefaultValue(childConfig: FieldConfig, formLoaded) { + if (formLoaded) { + return; } - if (this.stateList && this.stateList.length && this.formValue.state) { - const formStateList = []; - this.stateList.forEach(stateData => { - formStateList.push({ label: stateData.name, value: stateData.id }); - }); - this.commonForms.initilizeFormData({ code: 'state', path: ['templateOptions', 'options'], value: formStateList }); - this.commonForms.commonFormGroup.patchValue({ - state: this.formValue.state + if (this.profile.externalIds && this.profile.externalIds.length) { + this.profile.externalIds.forEach(eId => { + if (childConfig.code === eId.idType) { + childConfig.default = eId.id; + } }); - this.getDistrict(this.formValue.state); } + + if (this.editType === 'add') { + if (childConfig.code === 'declared-phone') { + childConfig.default = this.profile['maskedPhone']; + } + + if (childConfig.code === 'declared-email') { + childConfig.default = this.profile['maskedEmail']; + } + } + return childConfig; } - async checkLocationAvailability() { + private async checkLocationAvailability() { let stateId; let availableLocationData; if (this.profile && this.profile['userLocations'] && this.profile['userLocations'].length) { @@ -195,88 +271,6 @@ export class SelfDeclaredTeacherEditPage { } } - async getStates() { - const loader = await this.commonUtilService.getLoader(); - await loader.present(); - const req: LocationSearchCriteria = { - from: CachedItemRequestSourceFrom.SERVER, - filters: { - type: loc.TYPE_STATE - } - }; - this.profileService.searchLocation(req).subscribe(async (success) => { - const locations = success; - this.ngZone.run(async () => { - if (locations && Object.keys(locations).length) { - this.stateList = locations; - - const formStateList = []; - this.stateList.forEach(stateData => { - formStateList.push({ label: stateData.name, value: stateData.id }); - }); - - this.commonForms.initilizeFormData({ code: 'state', path: ['templateOptions', 'options'], value: formStateList }); - - if ((this.formValue && this.formValue.state) || this.availableLocationState) { - const state = this.stateList.find(s => (s.id === (this.formValue && this.formValue.state) || s.name === this.availableLocationState)); - this.commonForms.initilizeInputData({ code: 'state', value: state.id }); - if (state) { - await this.getDistrict(state.id); - } - } - } else { - this.districtList = []; - this.commonUtilService.showToast('NO_DATA_FOUND'); - } - await loader.dismiss(); - }); - }, async (error) => { - await loader.dismiss(); - }); - } - - async getDistrict(pid: string) { - const loader = await this.commonUtilService.getLoader(); - await loader.present(); - const req: LocationSearchCriteria = { - from: CachedItemRequestSourceFrom.SERVER, - filters: { - type: loc.TYPE_DISTRICT, - parentId: pid - } - }; - this.profileService.searchLocation(req).subscribe(async (success) => { - this.ngZone.run(async () => { - if (success && Object.keys(success).length) { - this.districtList = success; - - const formDistrictList = []; - this.districtList.forEach(districtData => { - formDistrictList.push({ label: districtData.name, value: districtData.id }); - }); - this.commonForms.initilizeFormData({ code: 'district', path: ['templateOptions', 'options'], value: formDistrictList }); - - if (this.availableLocationDistrict) { - const district = this.districtList.find(d => d.name === this.availableLocationDistrict); - if (district) { - this.commonForms.initilizeInputData({ code: 'district', value: district.id }); - } else { - this.commonForms.initilizeInputData({ code: 'district', value: '' }); - } - } - await loader.dismiss(); - } else { - this.availableLocationDistrict = ''; - await loader.dismiss(); - this.districtList = []; - this.commonUtilService.showToast('NO_DATA_FOUND'); - } - }); - }, async (error) => { - await loader.dismiss(); - }); - } - async submit() { if (!this.commonUtilService.networkInfo.isNetworkAvailable) { this.commonUtilService.showToast('NEED_INTERNET_TO_CHANGE'); @@ -289,68 +283,75 @@ export class SelfDeclaredTeacherEditPage { let telemetryValue; try { - if (!this.commonForms && !this.commonForms.commonFormGroup && !this.commonForms.commonFormGroup.value) { + if (!this.latestFormValue) { this.commonUtilService.showToast('SOMETHING_WENT_WRONG'); return; } - const formValue = this.commonForms.commonFormGroup.value; - const orgDetails: any = await this.frameworkService.searchOrganization({ filters: { locationIds: [formValue.state] } }).toPromise(); + const formValue = this.latestFormValue.children.externalIds; + const selectdState = this.getStateIdFromCode(formValue['declared-state']); + const orgDetails: any = await this.frameworkService.searchOrganization({ + filters: { + locationIds: [selectdState && selectdState.id], + isRootOrg: true + } + }).toPromise(); if (!orgDetails || !orgDetails.content || !orgDetails.content.length || !orgDetails.content[0].channel) { this.commonUtilService.showToast('SOMETHING_WENT_WRONG'); return; } + const rootOrgId = orgDetails.content[0].channel; await loader.present(); - const stateCode = this.stateList.find(state => state.id === formValue.state).code; - const districtCode = this.districtList.find(district => district.id === formValue.district).code; const externalIds = this.removeExternalIdsOnStateChange(rootOrgId); - this.teacherDetailsForm.forEach(formData => { - if (formData.code !== 'state' && formData.code !== 'district') { - // no externalIds declared - if (!this.profile.externalIds || !this.profile.externalIds.length || !this.profile.externalIds.find(eid => { - return eid.idType === formData.code; - })) { - if (formValue[formData.code]) { + this.teacherDetailsForm.forEach(config => { + if (config.code === 'externalIds' && config.children) { + config.children.forEach(formData => { + + // no externalIds declared + if (!this.profile.externalIds || !this.profile.externalIds.length || !this.profile.externalIds.find(eid => { + return eid.idType === formData.code; + })) { + if (formValue[formData.code]) { + externalIds.push({ + id: formValue[formData.code], + operation: 'add', + idType: formData.code, + provider: rootOrgId + }); + return; + } + } + + // externalIds declared but removed + if (!formValue[formData.code] && this.profile.externalIds && this.profile.externalIds.find(eid => { + return eid.idType === formData.code; + })) { externalIds.push({ - id: formValue[formData.code], - operation: 'add', + id: 'remove', + operation: 'remove', idType: formData.code, provider: rootOrgId }); return; } - } - // externalIds declared but removed - if (!formValue[formData.code] && this.profile.externalIds && this.profile.externalIds.find(eid => { - return eid.idType === formData.code; - })) { - externalIds.push({ - id: 'remove', - operation: 'remove', - idType: formData.code, - provider: rootOrgId - }); - return; - } - - // external id declared and modified - if (formValue[formData.code]) { - externalIds.push({ - id: formValue[formData.code], - operation: 'edit', - idType: formData.code, - provider: rootOrgId - }); - } + // external id declared and modified + if (formValue[formData.code]) { + externalIds.push({ + id: formValue[formData.code], + operation: 'edit', + idType: formData.code, + provider: rootOrgId + }); + } + }); } }); const req = { userId: this.profile.userId, - locationCodes: [stateCode, districtCode], externalIds }; @@ -364,6 +365,7 @@ export class SelfDeclaredTeacherEditPage { this.generateTelemetryInteract(InteractType.SUBMISSION_SUCCESS, ID.TEACHER_DECLARATION, telemetryValue); this.location.back(); if (this.editType === 'add') { + this.generateTncAudit(); this.showAddedSuccessfullPopup(); } else { this.commonUtilService.showToast(this.commonUtilService.translateMessage('UPDATED_SUCCESSFULLY')); @@ -377,19 +379,6 @@ export class SelfDeclaredTeacherEditPage { } } - onFormDataChange(event) { - if (event) { - if (event.value && this.formValue && event.value.state !== this.formValue.state) { - this.getTeacherDetailsFormApi(event.value.state); - } - - if (event.value) { - this.formValue = event.value; - } - this.isFormValid = event.valid; - } - } - async showAddedSuccessfullPopup() { const confirm = await this.popoverCtrl.create({ component: SbPopoverComponent, @@ -409,9 +398,6 @@ export class SelfDeclaredTeacherEditPage { await confirm.present(); const { data } = await confirm.onDidDismiss(); - if (data && data.canDelete) { - console.log(data); - } } generateTelemetryInteract(type, id, value?) { @@ -431,7 +417,7 @@ export class SelfDeclaredTeacherEditPage { getUpdatedValues(formVal) { const telemetryValue = []; - this.profile.userLocations.forEach(ele => { + this.profile['userLocations'].forEach(ele => { if (ele.type === 'state' && ele.id !== formVal.state) { telemetryValue.push('State'); } @@ -440,7 +426,6 @@ export class SelfDeclaredTeacherEditPage { } }); - console.log(this.initialExternalIds); for (const data in this.initialExternalIds) { if (data !== 'state' && data !== 'district' && this.initialExternalIds[data].value !== formVal[data]) { telemetryValue.push(this.initialExternalIds[data].name); @@ -466,4 +451,168 @@ export class SelfDeclaredTeacherEditPage { return externalIds; } + private generateTncAudit() { + const corRelationList: Array = [{ id: PageId.TEACHER_SELF_DECLARATION, type: CorReleationDataType.FROM_PAGE }]; + const telemetryObject = new TelemetryObject(ID.DATA_SHARING, 'TnC', this.profile.tncLatestVersion); + this.telemetryGeneratorService.generateAuditTelemetry( + Environment.USER, + AuditState.AUDIT_UPDATED, + [], + AuditType.TNC_DATA_SHARING, + telemetryObject.id, + telemetryObject.type, + telemetryObject.version, + corRelationList + ); + } + + private buildStateListClosure(stateCode?): FieldConfigOptionsBuilder { + return (formControl: FormControl, _: FormControl, notifyLoading, notifyLoaded) => { + return defer(async () => { + + const formStateList: FieldConfigOption[] = []; + let selectedState; + + const loader = await this.commonUtilService.getLoader(); + await loader.present(); + const req: LocationSearchCriteria = { + from: CachedItemRequestSourceFrom.SERVER, + filters: { + type: loc.TYPE_STATE + } + }; + try { + const locations = await this.profileService.searchLocation(req).toPromise(); + + if (locations && Object.keys(locations).length) { + this.stateList = locations; + + this.stateList.forEach(stateData => { + formStateList.push({ label: stateData.name, value: stateData.code }); + }); + + if (this.editType === 'add' && this.availableLocationState) { + selectedState = this.stateList.find(s => + (s.name === this.availableLocationState) + ); + } + + setTimeout(() => { + formControl.patchValue(stateCode || (selectedState && selectedState.code) || null); + }, 0); + + } else { + this.commonUtilService.showToast('NO_DATA_FOUND'); + } + } catch (e) { + console.log(e); + } finally { + loader.dismiss(); + } + + + return formStateList; + }); + }; + } + + private buildDistrictListClosure(districtCode?, formLoaded?): FieldConfigOptionsBuilder { + return (formControl: FormControl, contextFormControl: FormControl, notifyLoading, notifyLoaded) => { + if (!contextFormControl) { + return of([]); + } + + return contextFormControl.valueChanges.pipe( + distinctUntilChanged(), + tap(() => { + formControl.patchValue(null); + }), + switchMap((value) => { + return defer(async () => { + const formDistrictList: FieldConfigOption[] = []; + let selectedDistrict; + + const loader = await this.commonUtilService.getLoader(); + await loader.present(); + + const selectdState = this.getStateIdFromCode(contextFormControl.value); + + const req: LocationSearchCriteria = { + from: CachedItemRequestSourceFrom.SERVER, + filters: { + type: loc.TYPE_DISTRICT, + parentId: selectdState && selectdState.id + } + }; + try { + const districtList = await this.profileService.searchLocation(req).toPromise(); + + if (districtList && Object.keys(districtList).length) { + this.districtList = districtList; + + this.districtList.forEach(districtData => { + formDistrictList.push({ label: districtData.name, value: districtData.code }); + }); + + if (!formLoaded) { + + if (this.editType === 'add' && this.availableLocationDistrict) { + selectedDistrict = this.districtList.find(s => + (s.name === this.availableLocationDistrict) + ); + } + + setTimeout(() => { + formControl.patchValue(districtCode || (selectedDistrict && selectedDistrict.code) || null); + }, 0); + } else { + setTimeout(() => { + formControl.patchValue(null); + }, 0); + } + + } else { + this.availableLocationDistrict = ''; + this.districtList = []; + this.commonUtilService.showToast('NO_DATA_FOUND'); + } + } catch (e) { + console.log(e); + } finally { + loader.dismiss(); + } + return formDistrictList; + }); + }) + ); + }; + } + + formValueChanges(event) { + this.latestFormValue = event; + if (event && event.children && event.children.externalIds) { + if (!this.selectedStateCode && event.children.externalIds['declared-state']) { + this.selectedStateCode = event.children.externalIds['declared-state']; + } + if (event.children.externalIds['declared-state'] && this.selectedStateCode && this.selectedStateCode !== event.children.externalIds['declared-state']) { + this.selectedStateCode = event.children.externalIds['declared-state']; + const selectedState = this.getStateIdFromCode(this.selectedStateCode); + this.getTeacherDetailsFormApi(selectedState && selectedState.id); + } + } + } + + formStatusChanges(event) { + this.latestFormStatus = event; + this.isFormValid = event.isValid; + } + + private getStateIdFromCode(code) { + if (this.stateList && this.stateList.length) { + const selectedState = this.stateList.find(state => state.code === code); + return selectedState; + } + return null; + } + } diff --git a/src/app/profile/sub-profile-edit/sub-profile-edit.page.html b/src/app/profile/sub-profile-edit/sub-profile-edit.page.html index 87c06ac7f7..700451dbf9 100644 --- a/src/app/profile/sub-profile-edit/sub-profile-edit.page.html +++ b/src/app/profile/sub-profile-edit/sub-profile-edit.page.html @@ -7,7 +7,11 @@
- + + diff --git a/src/app/profile/sub-profile-edit/sub-profile-edit.page.ts b/src/app/profile/sub-profile-edit/sub-profile-edit.page.ts index fb81e44e63..17423c233b 100644 --- a/src/app/profile/sub-profile-edit/sub-profile-edit.page.ts +++ b/src/app/profile/sub-profile-edit/sub-profile-edit.page.ts @@ -91,8 +91,7 @@ export class SubProfileEditPage { const req: FormRequest = { type: 'user', subType: 'manageduser', - action: 'create', - component: 'app' + action: 'create_v2' }; this.formService.getForm(req).toPromise() .then((res: any) => { @@ -193,13 +192,6 @@ export class SubProfileEditPage { this.commonUtilService.openLink(this.profile.serverProfile.tncLatestVersionUrl); } - onFormDataChange(event) { - if (event) { - this.isFormValid = event.valid; - this.formValue = event.value || undefined; - } - } - handleHeaderEvents($event) { switch ($event.name) { case 'back': @@ -226,4 +218,12 @@ export class SubProfileEditPage { ); } + formValueChanges(event) { + this.formValue = event; + } + + formStatusChanges(event) { + this.isFormValid = event.isValid; + } + } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b1ee6e7f0a..ae5abe56a8 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -20,7 +20,6 @@ "ADD_ACTIVITY": "Add activity", "ADD_ANOTHER_USER": "Add another user", "ADD_DISTRICT": "Add District", - "ADD_EDIT_SELF_DECLARED_TEACHER_INFO": "These details may be made available to your declared State to validate and follow progress", "ADD_EMAIL": "Add Email Address", "ADD_MEMBER": "Add member", "ADD_PHONE": "Add Mobile Number", @@ -266,6 +265,7 @@ "ERROR_OTP": "Incorrect OTP. Number of attempts remaining : {{%s}}", "ERROR_PHONE_EXISTS": "This mobile number is already registered. Try with a different mobile number", "ERROR_PHONE_INVALID": "Enter a valid mobile number", + "ERROR_PHONE_REQUIRED": "Mobile number is required", "ERROR_SAME_EMAIL_UPDATED": "This email address is already linked to your profile. Enter another email address", "ERROR_SAME_PHONE_UPDATED": "This mobile number is already linked to your profile. Enter another mobile number", "ERROR_SERVER_CONNECTION": "Unable to connect to server", @@ -792,6 +792,9 @@ "ADD_MEMBER_POPUP_TITLE": "Where can I see the {{%s}} ID?", "ADD_MEMBER_POPUP_DESC": "Use the following instructions to guide members to identify their unique {{%s}} ID.", "ADD_MEMBER_POPUP_STEP_1": "Step 1 : Click Profile tab", - "ADD_MEMBER_POPUP_STEP_2": "Step 2 : See the {{%s}} ID here" + "ADD_MEMBER_POPUP_STEP_2": "Step 2 : See the {{%s}} ID here", + "PRIVACY_POLICY": "Privacy Policy", + "SELF_DECLARE_TEACHER_TNC": "I agree to share these details with the Administrators of", + "AS_PER_THE": "as per the" } diff --git a/src/assets/imgs/empty_circle.svg b/src/assets/imgs/empty_circle.svg new file mode 100644 index 0000000000..9cf5719848 --- /dev/null +++ b/src/assets/imgs/empty_circle.svg @@ -0,0 +1,11 @@ + + + 0A5AE039-B21E-4EB7-98C2-533D33CCFEB7 + + + + + + + + \ No newline at end of file diff --git a/src/assets/imgs/green_tick.svg b/src/assets/imgs/green_tick.svg new file mode 100644 index 0000000000..9a4a96edc4 --- /dev/null +++ b/src/assets/imgs/green_tick.svg @@ -0,0 +1,15 @@ + + + 8B4CB432-FD76-4469-94B8-BCC3D605009F + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/imgs/red_exclamation.svg b/src/assets/imgs/red_exclamation.svg new file mode 100644 index 0000000000..4d11353e74 --- /dev/null +++ b/src/assets/imgs/red_exclamation.svg @@ -0,0 +1,14 @@ + + + F7461929-9D59-4C06-97CD-845F9C506BE5 + + + + + + + + + + + \ No newline at end of file diff --git a/src/services/common-util.service.spec.ts b/src/services/common-util.service.spec.ts index d827710cce..ce60b96bd3 100644 --- a/src/services/common-util.service.spec.ts +++ b/src/services/common-util.service.spec.ts @@ -17,7 +17,7 @@ import { Network } from '@ionic-native/network/ngx'; import { NgZone } from '@angular/core'; import { WebView } from '@ionic-native/ionic-webview/ngx'; import { AppVersion } from '@ionic-native/app-version/ngx'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { Router } from '@angular/router'; import { AndroidPermissionsService } from '.'; @@ -563,4 +563,66 @@ describe('CommonUtilService', () => { }); }); + describe('getStateList', () => { + it('should return the state list', (done) => { + // arrange + mockProfileService.searchLocation = jest.fn(() => of([])); + // act + commonUtilService.getStateList().then((res) => { + // assert + expect(res).toEqual([]); + done(); + }); + }); + + it('should return empty state list', (done) => { + // arrange + mockProfileService.searchLocation = jest.fn(() => throwError(new Error())); + // act + commonUtilService.getStateList().then((res) => { + // assert + expect(res).toEqual([]); + done(); + }); + }); + }); + + describe('getDistrictList', () => { + it('should return the district list with state id', (done) => { + // arrange + const id = 'state_id'; + mockProfileService.searchLocation = jest.fn(() => of([])); + // act + commonUtilService.getDistrictList(id).then((res) => { + // assert + expect(res).toEqual([]); + done(); + }); + }); + + it('should return the district list with state code', (done) => { + // arrange + const code = 'state_code'; + mockProfileService.searchLocation = jest.fn(() => of([])); + // act + commonUtilService.getDistrictList(code).then((res) => { + // assert + expect(res).toEqual([]); + done(); + }); + }); + + it('should return empty district list', (done) => { + // arrange + const id = 'state_id'; + mockProfileService.searchLocation = jest.fn(() => throwError(new Error())); + // act + commonUtilService.getDistrictList(id).then((res) => { + // assert + expect(res).toEqual([]); + done(); + }); + }); + }); + }); diff --git a/src/services/common-util.service.ts b/src/services/common-util.service.ts index eed982aa8a..acab6dccaa 100644 --- a/src/services/common-util.service.ts +++ b/src/services/common-util.service.ts @@ -9,10 +9,9 @@ import { import { TranslateService } from '@ngx-translate/core'; import { Network } from '@ionic-native/network/ngx'; import { WebView } from '@ionic-native/ionic-webview/ngx'; -import { SharedPreferences, ProfileService, Profile, ProfileType, CorrelationData } from 'sunbird-sdk'; +import { SharedPreferences, ProfileService, Profile, ProfileType, CorrelationData, CachedItemRequestSourceFrom, LocationSearchCriteria } from 'sunbird-sdk'; -import { PreferenceKey, ProfileConstants, RouterLinks } from '@app/app/app.constant'; -import { appLanguages } from '@app/app/app.constant'; +import { PreferenceKey, ProfileConstants, RouterLinks, appLanguages, Location as loc } from '@app/app/app.constant'; import { TelemetryGeneratorService } from '@app/services/telemetry-generator.service'; import { InteractType, InteractSubtype, PageId, Environment, CorReleationDataType, ImpressionType, ObjectType } from '@app/services/telemetry-constants'; @@ -599,4 +598,36 @@ export class CommonUtilService { } return initial; } + + async getStateList() { + const req: LocationSearchCriteria = { + from: CachedItemRequestSourceFrom.SERVER, + filters: { + type: loc.TYPE_STATE + } + }; + try { + const stateList = await this.profileService.searchLocation(req).toPromise(); + return stateList || []; + } catch { + return []; + } + } + + async getDistrictList(id?: string, code?: string) { + const req: LocationSearchCriteria = { + from: CachedItemRequestSourceFrom.SERVER, + filters: { + type: loc.TYPE_DISTRICT, + parentId: id || undefined, + code: code || undefined + } + }; + try { + const districtList = await this.profileService.searchLocation(req).toPromise(); + return districtList || []; + } catch { + return []; + } + } } diff --git a/src/services/form-validation-async-factory/form-validation-async-factory.ts b/src/services/form-validation-async-factory/form-validation-async-factory.ts new file mode 100644 index 0000000000..97fba7fb7e --- /dev/null +++ b/src/services/form-validation-async-factory/form-validation-async-factory.ts @@ -0,0 +1,146 @@ +import { Injectable, QueryList, Inject } from '@angular/core'; +import { FormControl, ValidationErrors } from '@angular/forms'; +import { IonButton, PopoverController } from '@ionic/angular'; +import { GenerateOtpRequest, ProfileService, ServerProfile } from '@project-sunbird/sunbird-sdk'; +import { ProfileConstants } from '@app/app/app.constant'; +import { CommonUtilService } from '../common-util.service'; +import { EditContactVerifyPopupComponent } from '@app/app/components/popups/edit-contact-verify-popup/edit-contact-verify-popup.component'; +import { FieldConfig } from '@app/app/components/common-forms/field-config'; +import { resolve } from 'dns'; + + +@Injectable({ providedIn: 'root' }) +export class FormValidationAsyncFactory { + + constructor( + @Inject('PROFILE_SERVICE') private profileService: ProfileService, + private commonUtilService: CommonUtilService, + private popoverCtrl: PopoverController + ) { } + + mobileVerificationAsyncFactory(formElement: FieldConfig, profile: ServerProfile) { + return (marker: string, triggers: QueryList) => { + if (marker === 'MOBILE_OTP_VALIDATION') { + return async (control: FormControl) => { + return new Promise(resolve => { + const trigger: IonButton = triggers.find(e => e['el'].getAttribute('data-marker') === 'MOBILE_OTP_VALIDATION'); + if (trigger) { + const that = this; + trigger['el'].onclick = (async () => { + const isOtpVerified: boolean = await that.generateAndVerifyOTP(profile, control, ProfileConstants.CONTACT_TYPE_PHONE); + if (isOtpVerified) { + resolve(null); + } else { + resolve({ asyncValidation: 'error' }); + } + }).bind(this); + return; + } + resolve(null); + }); + }; + } + return async () => null; + }; + } + + emailVerificationAsyncFactory(formElement: FieldConfig, profile: ServerProfile) { + return (marker: string, triggers: QueryList) => { + if (marker === 'EMAIL_OTP_VALIDATION') { + return async (control: FormControl) => { + return new Promise(resolve => { + const trigger: IonButton = triggers.find(e => e['el'].getAttribute('data-marker') === 'EMAIL_OTP_VALIDATION'); + if (trigger) { + const that = this; + trigger['el'].onclick = (async () => { + const isOtpVerified: boolean = await that.generateAndVerifyOTP(profile, control, ProfileConstants.CONTACT_TYPE_EMAIL); + if (isOtpVerified) { + resolve(null); + } else { + resolve({ asyncValidation: 'error' }); + } + }).bind(this); + return; + } + resolve(null); + }); + }; + } + return async () => null; + }; + } + + private async generateAndVerifyOTP(profile, control, type): Promise { + const loader = await this.commonUtilService.getLoader(); + try { + const req: GenerateOtpRequest = { + key: control.value, + type + }; + + await loader.present(); + await this.profileService.generateOTP(req).toPromise(); + + await loader.dismiss(); + + const isOtpVerified = await this.checkOtpVerification(profile, type, control.value); + if (isOtpVerified) { + return true; + } else { + return false; + } + } catch (e) { + if (e.hasOwnProperty(e) === 'ERROR_RATE_LIMIT_EXCEEDED') { + this.commonUtilService.showToast('You have exceeded the maximum limit for OTP, Please try after some time.'); + } + return false; + } finally { + if (loader) { + await loader.dismiss(); + } + } + } + + private async checkOtpVerification(profile, type: string, key: any): Promise { + if (type === ProfileConstants.CONTACT_TYPE_PHONE) { + const componentProps = { + key, + phone: profile.phone, + title: this.commonUtilService.translateMessage('VERIFY_PHONE_OTP_TITLE'), + description: this.commonUtilService.translateMessage('VERIFY_PHONE_OTP_DESCRIPTION'), + type: ProfileConstants.CONTACT_TYPE_PHONE, + userId: profile.userId + }; + + const data = await this.openContactVerifyPopup(EditContactVerifyPopupComponent, componentProps, 'popover-alert input-focus'); + if (data && data.OTPSuccess) { + return true; + } + return false; + } else { + const componentProps = { + key, + phone: profile.email, + title: this.commonUtilService.translateMessage('VERIFY_EMAIL_OTP_TITLE'), + description: this.commonUtilService.translateMessage('VERIFY_EMAIL_OTP_DESCRIPTION'), + type: ProfileConstants.CONTACT_TYPE_EMAIL, + userId: profile.userId + }; + + const data = await this.openContactVerifyPopup(EditContactVerifyPopupComponent, componentProps, 'popover-alert input-focus'); + if (data && data.OTPSuccess) { + return true; + } + return false; + } + } + + private async openContactVerifyPopup(component, componentProps, cssClass) { + const popover = await this.popoverCtrl.create({ component, componentProps, cssClass }); + await popover.present(); + const { data } = await popover.onDidDismiss(); + + return data; + } + +} diff --git a/src/services/telemetry-constants.ts b/src/services/telemetry-constants.ts index 97413928d8..fe6fd758fb 100644 --- a/src/services/telemetry-constants.ts +++ b/src/services/telemetry-constants.ts @@ -505,7 +505,8 @@ export enum ID { BTN_UPDATE = 'btn-update', BTN_I_AM_A_TEACHER = 'btn-i-am-a-teacher', TEACHER_DECLARATION = 'teacher-declaration', - MUA_USER_CREATION = 'mua-user-creation' + MUA_USER_CREATION = 'mua-user-creation', + DATA_SHARING = 'data-sharing' } export enum ActionButtonType { @@ -567,6 +568,7 @@ export enum AuditType { SET_PROFILE = 'set-profile', UNIT_PROGRESS = 'unit-progress', COURSE_PROGRESS = 'course-progress', - TOAST_SEEN = 'toast-seen' + TOAST_SEEN = 'toast-seen', + TNC_DATA_SHARING = 'tnc-data-sharing' }