From 5b42f999c47ca624567c51a2d01946f2e3cbbe48 Mon Sep 17 00:00:00 2001 From: Saniya <37302318+Saby-Bishops@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:51:28 -0500 Subject: [PATCH 1/3] Refine health update forms with typed controls --- src/app/health/health-update.component.ts | 38 ++++++++++++++++++----- src/app/shared/table-helpers.ts | 13 +++++--- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/app/health/health-update.component.ts b/src/app/health/health-update.component.ts index 7f9b62d8a5..efa13faa9a 100644 --- a/src/app/health/health-update.component.ts +++ b/src/app/health/health-update.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, HostListener } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { CustomValidators } from '../validators/custom-validators'; import { ValidatorService } from '../validators/validator.service'; import { UserService } from '../shared/user.service'; @@ -13,14 +13,36 @@ import { warningMsg } from '../shared/unsaved-changes.component'; import { interval, of, race } from 'rxjs'; import { debounce } from 'rxjs/operators'; +interface ProfileForm { + name: FormControl; + firstName: FormControl; + middleName: FormControl; + lastName: FormControl; + email: FormControl; + language: FormControl; + phoneNumber: FormControl; + birthDate: FormControl; + birthplace: FormControl; +} + +interface HealthForm { + emergencyContactName: FormControl; + emergencyContactType: FormControl; + emergencyContact: FormControl; + specialNeeds: FormControl; + immunizations: FormControl; + allergies: FormControl; + notes: FormControl; +} + @Component({ templateUrl: './health-update.component.html', styleUrls: [ './health-update.scss' ] }) export class HealthUpdateComponent implements OnInit, CanComponentDeactivate { - profileForm: UntypedFormGroup; - healthForm: UntypedFormGroup; + profileForm: FormGroup; + healthForm: FormGroup; existingData: any = {}; languages = languages; minBirthDate: Date = this.userService.minBirthDate; @@ -28,7 +50,7 @@ export class HealthUpdateComponent implements OnInit, CanComponentDeactivate { hasUnsavedChanges = false; constructor( - private fb: UntypedFormBuilder, + private fb: FormBuilder, private validatorService: ValidatorService, private userService: UserService, private healthService: HealthService, @@ -83,7 +105,7 @@ export class HealthUpdateComponent implements OnInit, CanComponentDeactivate { } initProfileForm() { - this.profileForm = this.fb.group({ + this.profileForm = this.fb.nonNullable.group({ name: '', firstName: [ '', CustomValidators.required ], middleName: '', @@ -101,7 +123,7 @@ export class HealthUpdateComponent implements OnInit, CanComponentDeactivate { } initHealthForm() { - this.healthForm = this.fb.group({ + this.healthForm = this.fb.nonNullable.group({ emergencyContactName: '', emergencyContactType: '', emergencyContact: '', @@ -118,8 +140,8 @@ export class HealthUpdateComponent implements OnInit, CanComponentDeactivate { onSubmit() { if (!(this.profileForm.valid && this.healthForm.valid)) { - showFormErrors(this.profileForm.controls); - showFormErrors(this.healthForm.controls); + showFormErrors(Object.values(this.profileForm.controls)); + showFormErrors(Object.values(this.healthForm.controls)); return; } forkJoin([ diff --git a/src/app/shared/table-helpers.ts b/src/app/shared/table-helpers.ts index c0bc41fcc2..3cc85a239e 100644 --- a/src/app/shared/table-helpers.ts +++ b/src/app/shared/table-helpers.ts @@ -204,11 +204,14 @@ export const trackByIdVal = (index, item: { id: string }) => item.id; export const trackByIndex = (index: number) => index; -export const showFormErrors = (controls: { [key: string]: AbstractControl }) => { - Object.values(controls).forEach(control => { - control.markAsTouched({ onlySelf: true }); - }); -}; +export const showFormErrors = ( + controls: { [key: string]: AbstractControl } | AbstractControl[] +) => { + const controlList = Array.isArray(controls) ? controls : Object.values(controls); + controlList.forEach(control => { + control.markAsTouched({ onlySelf: true }); + }); +}; export const filterIds = (filterObj: { ids: string[] }) => { return (data: any, filter: string) => { From 89b026e07038b74fed5d0dc4ff00d32c0cc5a6a3 Mon Sep 17 00:00:00 2001 From: Saniya <37302318+Saby-Bishops@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:16:10 -0600 Subject: [PATCH 2/3] Fix typed health update forms --- src/app/health/health-update.component.ts | 47 +++++++++++------------ src/app/shared/table-helpers.ts | 2 +- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/app/health/health-update.component.ts b/src/app/health/health-update.component.ts index efa13faa9a..e588d9beac 100644 --- a/src/app/health/health-update.component.ts +++ b/src/app/health/health-update.component.ts @@ -105,32 +105,31 @@ export class HealthUpdateComponent implements OnInit, CanComponentDeactivate { } initProfileForm() { - this.profileForm = this.fb.nonNullable.group({ - name: '', - firstName: [ '', CustomValidators.required ], - middleName: '', - lastName: [ '', CustomValidators.required ], - email: [ '', [ Validators.required, Validators.email ] ], - language: [ '', Validators.required ], - phoneNumber: [ '', CustomValidators.required ], - birthDate: [ - '', - CustomValidators.dateValidRequired, - ac => this.validatorService.notDateInFuture$(ac) - ], - birthplace: '' + this.profileForm = this.fb.nonNullable.group({ + name: this.fb.nonNullable.control(''), + firstName: this.fb.nonNullable.control('', { validators: CustomValidators.required }), + middleName: this.fb.nonNullable.control(''), + lastName: this.fb.nonNullable.control('', { validators: CustomValidators.required }), + email: this.fb.nonNullable.control('', { validators: [ Validators.required, Validators.email ] }), + language: this.fb.nonNullable.control('', { validators: Validators.required }), + phoneNumber: this.fb.nonNullable.control('', { validators: CustomValidators.required }), + birthDate: this.fb.nonNullable.control('', { + validators: CustomValidators.dateValidRequired, + asyncValidators: ac => this.validatorService.notDateInFuture$(ac) + }), + birthplace: this.fb.nonNullable.control('') }); } initHealthForm() { - this.healthForm = this.fb.nonNullable.group({ - emergencyContactName: '', - emergencyContactType: '', - emergencyContact: '', - specialNeeds: '', - immunizations: '', - allergies: '', - notes: '' + this.healthForm = this.fb.nonNullable.group({ + emergencyContactName: this.fb.nonNullable.control(''), + emergencyContactType: this.fb.nonNullable.control(''), + emergencyContact: this.fb.nonNullable.control(''), + specialNeeds: this.fb.nonNullable.control(''), + immunizations: this.fb.nonNullable.control(''), + allergies: this.fb.nonNullable.control(''), + notes: this.fb.nonNullable.control('') }); this.healthForm.controls.emergencyContactType.valueChanges.subscribe(value => { this.healthForm.controls.emergencyContact.setValidators(value === 'email' ? Validators.email : null); @@ -140,8 +139,8 @@ export class HealthUpdateComponent implements OnInit, CanComponentDeactivate { onSubmit() { if (!(this.profileForm.valid && this.healthForm.valid)) { - showFormErrors(Object.values(this.profileForm.controls)); - showFormErrors(Object.values(this.healthForm.controls)); + showFormErrors(this.profileForm.controls); + showFormErrors(this.healthForm.controls); return; } forkJoin([ diff --git a/src/app/shared/table-helpers.ts b/src/app/shared/table-helpers.ts index 3cc85a239e..41d27be567 100644 --- a/src/app/shared/table-helpers.ts +++ b/src/app/shared/table-helpers.ts @@ -1,4 +1,4 @@ -import { FormControl, AbstractControl } from '../../../node_modules/@angular/forms'; +import { FormControl, AbstractControl } from '@angular/forms'; import { FuzzySearchService } from './fuzzy-search.service'; const dropdownString = (fieldValue: any, value: string) => { From c54a2161ff58f684ec88703ac0ade597c7f5a102 Mon Sep 17 00:00:00 2001 From: Saniya <37302318+Saby-Bishops@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:49:51 -0600 Subject: [PATCH 3/3] Delete src/app/shared/table-helpers.ts --- src/app/shared/table-helpers.ts | 220 -------------------------------- 1 file changed, 220 deletions(-) delete mode 100644 src/app/shared/table-helpers.ts diff --git a/src/app/shared/table-helpers.ts b/src/app/shared/table-helpers.ts deleted file mode 100644 index 41d27be567..0000000000 --- a/src/app/shared/table-helpers.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { FormControl, AbstractControl } from '@angular/forms'; -import { FuzzySearchService } from './fuzzy-search.service'; - -const dropdownString = (fieldValue: any, value: string) => { - if (fieldValue === undefined || value === undefined) { - // If there is no value to filter, include item. If the data field is undefined, exclude item. - return value !== undefined; - } - - if (fieldValue instanceof Array) { - return fieldValue.indexOf(value) === -1; - } - - // Ensure both value and fieldValue are strings before calling toLowerCase - if (typeof value === 'string' && typeof fieldValue === 'string') { - return value.toLowerCase() !== fieldValue.toLowerCase(); - } -}; - -const dropdownArray = (fieldValue: any, values: string[]) => { - return values.findIndex(value => !dropdownString(fieldValue, value)) === -1; -}; - -const checkFilterItems = (data: any) => ((includeItem: boolean, [ field, val ]) => { - const dataField = getProperty(data, field); - // If field is an array field, check if one value matches. If not check if values match exactly. - const noMatch = val instanceof Array ? dropdownArray(dataField, val) : dropdownString(dataField, val); - if (val && noMatch) { - return false; - } - return includeItem; -}); - -// Multi level field filter by spliting each field by '.' -export const filterSpecificFields = (filterFields: string[]): any => { - return (data: any, filter: string) => { - const normalizedFilter = filter.trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); - for (let i = 0; i < filterFields.length; i++) { - const fieldValue = getProperty(data, filterFields[i]); - if (typeof fieldValue === 'string' && - fieldValue.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').indexOf(normalizedFilter) > -1) { - return true; - } - } - return false; - }; -}; - -export const filterSpecificFieldsByWord = (filterFields: string[]): any => { - return (data: any, filter: string) => { - // Normalize each word - const words = filter.split(' ').map(value => value.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')); - return words.every(word => { - return filterFields.some(field => { - const fieldValue = getProperty(data, field); - return typeof fieldValue === 'string' && - fieldValue.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').includes(word); - }); - }); - }; -}; - -// Enhanced version that combines exact and fuzzy search -export const filterSpecificFieldsHybrid = (filterFields: string[], fuzzySearchService?: FuzzySearchService): any => { - return (data: any, filter: string) => { - const normalizedFilter = filter.trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); - if (!normalizedFilter) { return true; } - - return filterFields.some(field => { - const fieldValue = getProperty(data, field); - if (typeof fieldValue !== 'string') { return false; } - - // Try exact match first, then fuzzy if available - const normalizedFieldValue = fieldValue.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); - return normalizedFieldValue.includes(normalizedFilter) || - (fuzzySearchService?.fuzzyWordMatch(filter, fieldValue, { threshold: 0.6, maxDistance: 2 }) ?? false); - }); - }; -}; - -// Takes an object and string of dot seperated property keys. Returns the nested value of the succession of -// keys or undefined. -function getProperty(data: any, fields: string) { - const propertyArray = fields.split('.'); - return propertyArray.reduce((obj, prop) => (obj && obj[prop] !== undefined) ? obj[prop] : undefined, data); -} - -export const filterDropdowns = (filterObj: any) => { - return (data: any, filter: string) => { - // Object.entries returns an array of each key/value pair as arrays in the form of [ key, value ] - return Object.entries(filterObj).reduce(checkFilterItems(data), true); - }; -}; - -// Takes array of field names and if trueIfExists is true, return true if field exists -// if false return true if it does not exist -export const filterFieldExists = (filterFields: string[], trueIfExists: boolean): any => { - return (data: any, filter: string) => { - for (let i = 0; i < filterFields.length; i++) { - return trueIfExists === (getProperty(data, filterFields[i]) !== undefined); - } - return true; - }; -}; - -const matchAllItems = (filterItems: string[], propItems: string[]) => { - return filterItems.reduce((isMatch, filter) => isMatch && propItems.indexOf(filter) > -1, true); -}; - -export const filterArrayField = (filterField: string, filterItems: string[]) => { - return (data: any, filter: string) => { - return matchAllItems(filterItems, getProperty(data, filterField) || []); - }; -}; - -export const filterTags = (filterControl: FormControl) => { - return (data: any, filter: string) => { - return filterArrayField('tags', filterControl.value)({ tags: data.tags.map((tag: any) => tag._id) }, filter); - }; -}; - -export const filterAdvancedSearch = (searchObj: any) => { - return (data: any, filter: string) => { - return Object.entries(searchObj).reduce( - (isMatch, [ field, val ]: any[]) => isMatch && (field.indexOf('_') > -1 || filterArrayField(field, val)(data.doc, filter)), - true - ); - }; -}; - -// filterOnOff must be an object so it references a variable on component & changes with component changes -export const filterShelf = (filterOnOff: { value: 'on' | 'off' }, filterField: string) => { - return (data: any, filter: string) => { - return filterOnOff.value === 'off' || data[filterField] === true; - }; -}; - -// Special filter for showing members that are admins -export const filterAdmin = (data, filter) => data.doc.isUserAdmin && data.doc.roles.length === 0; - -// Takes an array of the above filtering functions and returns true if all match -export const composeFilterFunctions = (filterFunctions: any[]) => { - return (data: any, filter: any) => { - return filterFunctions.reduce((isMatch, filterFunction) => { - return isMatch && filterFunction(data, filter); - }, true); - }; -}; - -export const sortNumberOrString = (item, property) => { - switch (typeof item[property]) { - case 'number': - return item[property]; - case 'string': - return item[property].trim().toLowerCase(); - } -}; - -// Returns a space to fill the MatTable filter field so filtering runs for dropdowns when -// search text is deleted, but does not run when there are no active filters. -export const dropdownsFill = (filterObj) => Object.entries(filterObj).reduce((emptySpace, [ field, val ]) => { - if (val) { - return ' '; - } - return emptySpace; -}, ''); - -export const filteredItemsInPage = (filteredData: any[], pageIndex: number, pageSize: number) => { - return pageIndex === undefined ? filteredData : filteredData.slice(pageIndex * pageSize, (pageIndex * pageSize) + pageSize); -}; - -export const selectedOutOfFilter = (filteredData: any[], selection: any, paginator: any = {}) => { - const itemsInPage = filteredItemsInPage(filteredData, paginator.pageIndex, paginator.pageSize); - return selection.selected.filter((selectedId) => itemsInPage.find((filtered: any) => filtered._id === selectedId ) === undefined); -}; - -export const createDeleteArray = (array) => array.map((item: any) => ({ _id: item._id, _rev: item._rev, _deleted: true })); - -export const commonSortingDataAccessor = (item: any, property: string) => { - switch (property) { - case 'rating': - return item.rating.rateSum / item.rating.totalRating || 0; - default: - return item[property] ? sortNumberOrString(item, property) : sortNumberOrString(item.doc, property); - } -}; - -export const deepSortingDataAccessor = (item: any, property: string) => { - const keys = property.split('.'); - const simpleItem = keys.reduce((newItem, key, index) => { - if (index === keys.length - 1 || newItem[key] === undefined || newItem[key] === null) { - return newItem; - } - return newItem[key]; - }, item); - return sortNumberOrString(simpleItem, keys[keys.length - 1]); -}; - -export const trackById = (index, item) => item._id; - -export const trackByCategory = (index, item: { category: string }) => item.category; - -export const trackByIdVal = (index, item: { id: string }) => item.id; - -export const trackByIndex = (index: number) => index; - -export const showFormErrors = ( - controls: { [key: string]: AbstractControl } | AbstractControl[] -) => { - const controlList = Array.isArray(controls) ? controls : Object.values(controls); - controlList.forEach(control => { - control.markAsTouched({ onlySelf: true }); - }); -}; - -export const filterIds = (filterObj: { ids: string[] }) => { - return (data: any, filter: string) => { - return filterObj.ids.length > 0 ? filterObj.ids.indexOf(data._id) > -1 : true; - }; -};