From 173bc01da3b73ae71acb7358d135945541c26caf Mon Sep 17 00:00:00 2001 From: ma7payne Date: Fri, 13 Feb 2026 10:56:09 -0300 Subject: [PATCH] feat(REC): implementa buscador generico/comercial --- src/app/interfaces/supplies.ts | 1 + .../professional-form.component.html | 79 ++++----- .../professional-form.component.ts | 154 ++---------------- .../supply-search.component.html | 22 +++ .../supply-search.component.sass | 16 ++ .../supply-search/supply-search.component.ts | 126 ++++++++++++++ src/app/professionals/professionals.module.ts | 2 + .../unified-printer.component.ts | 5 + 8 files changed, 211 insertions(+), 194 deletions(-) create mode 100644 src/app/professionals/components/supply-search/supply-search.component.html create mode 100644 src/app/professionals/components/supply-search/supply-search.component.sass create mode 100644 src/app/professionals/components/supply-search/supply-search.component.ts diff --git a/src/app/interfaces/supplies.ts b/src/app/interfaces/supplies.ts index 087270a6..26cf0beb 100644 --- a/src/app/interfaces/supplies.ts +++ b/src/app/interfaces/supplies.ts @@ -5,4 +5,5 @@ export default interface Supplies { quantity: string; _id: string; name: string; + brand?: string; } diff --git a/src/app/professionals/components/professional-form/professional-form.component.html b/src/app/professionals/components/professional-form/professional-form.component.html index 269b9c4c..d5235a5e 100644 --- a/src/app/professionals/components/professional-form/professional-form.component.html +++ b/src/app/professionals/components/professional-form/professional-form.component.html @@ -6,14 +6,14 @@
+ [@step]="currentTab === 'form' ? 'left' : currentTab === 'certificates' ? 'center-left' : currentTab === 'practices' ? 'center-right' : 'right'">
@@ -27,19 +27,19 @@

+ (ngSubmit)="onSubmitProfessionalForm(professionalNgForm)">
+ fxLayoutGap.xs="0">
+ fxLayoutAlign.xs="center center"> Fecha receta + [min]="minDate" [max]="maxDate" autocomplete="off" required + tabIndex="-1" readonly="true"> @@ -54,21 +54,22 @@

+ [layout]="'row'" formControlName="patient"> - +
+ *ngIf="suppliesForm.errors?.minLengthFilled && suppliesForm.invalid && suppliesForm.touched"> Debe seleccionar al menos un medicamento
+ fxLayout="row" fxLayoutGap="10px">
- - Medicamento - - - - - - {{sup.term}} - - - - - Debe seleccionar un medicamento - - - Debe seleccionar un medicamento del listado - - - {{ control.get('supply').getError('invalid') }} - - +
@@ -110,13 +87,13 @@

Cantidad + formControlName="quantityPresentation" required> + *ngIf="control.get('quantityPresentation').hasError('required')"> Debe ingresar una cantidad + *ngIf="control.get('quantityPresentation').hasError('min')"> Debe ingresar un mínimo de {{control.get('quantityPresentation').errors?.min.min}} @@ -128,7 +105,7 @@

Cantidad de envases + formControlName="quantity" required> Debe ingresar una cantidad @@ -154,17 +131,17 @@

Serie + maxlength="1" placeholder="Ingrese una letra" required> Número + placeholder="Ingrese un número" required>

+ style="position: relative; font-style: italic; bottom: 15px;"> Recuerde que sigue siendo obligatoria la @@ -173,7 +150,7 @@

Diagnóstico + formControlName="diagnostic" required> Debe ingresar un diagnóstico @@ -184,22 +161,22 @@

Indicaciones + formControlName="indication">

+ fxFlex="100%"> @@ -224,11 +201,11 @@

+ (anulateCertificateEvent)="showCertificados()">
+ (certificateCreatedEvent)="onCertificateCreated()">

\ No newline at end of file diff --git a/src/app/professionals/components/professional-form/professional-form.component.ts b/src/app/professionals/components/professional-form/professional-form.component.ts index 601e0f74..ce70e054 100644 --- a/src/app/professionals/components/professional-form/professional-form.component.ts +++ b/src/app/professionals/components/professional-form/professional-form.component.ts @@ -12,72 +12,11 @@ import { InteractionService } from '@professionals/interaction.service'; import { PatientsService } from '@root/app/services/patients.service'; import { CertificatesService } from '@services/certificates.service'; import { PrescriptionsService } from '@services/prescriptions.service'; -import { SnomedSuppliesService } from '@services/snomedSupplies.service'; + import { PatientFormComponent } from '@shared/components/patient-form/patient-form.component'; import { OrganizacionFormSessionService } from '@professionals/services/organizacion-form-session.service'; -import { of, Subject, Subscription, Observable } from 'rxjs'; -import { map, startWith, catchError, debounceTime, distinctUntilChanged, filter, switchMap, takeUntil } from 'rxjs/operators'; - -// Validador personalizado para fechas -function validDateValidator(): ValidatorFn { - return (control: AbstractControl): { [key: string]: any } | null => { - if (!control.value) { - return null; // Si no hay valor, no validamos (required se encarga) - } - - const date = new Date(control.value); - const isValidDate = date instanceof Date && !isNaN(date.getTime()); - - if (!isValidDate) { - return { 'invalidDate': { value: control.value } }; - } - - // Validar que la fecha no sea futura para fecha de nacimiento - const today = new Date(); - if (date > today) { - return { 'futureDate': { value: control.value } }; - } - - // Validar que la fecha sea razonable (no muy antigua) - const minDate = new Date('1900-01-01'); - if (date < minDate) { - return { 'tooOldDate': { value: control.value } }; - } - - return null; - }; -} - -function medicationSelectedValidator(): ValidatorFn { - return (control: AbstractControl): { [key: string]: any } | null => { - if (!control.value) { - return null; - } - - const supplyGroup = control.parent; - if (!supplyGroup) { - return null; - } - - const snomedConcept = supplyGroup.get('snomedConcept'); - if (!snomedConcept || !snomedConcept.value || !snomedConcept.value.conceptId) { - return { 'medicationNotSelected': { value: control.value } }; - } - - return null; - }; -} - -function noWhitespaceValidator(): ValidatorFn { - return (control: AbstractControl): { [key: string]: any } | null => { - if (!control.value) { - return null; - } - - const isWhitespace = (control.value || '').trim().length === 0; - return isWhitespace ? { 'whitespace': { value: control.value } } : null; - }; -} +import { Subject, Subscription, Observable, of } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; @Component({ selector: 'app-professional-form', @@ -88,12 +27,10 @@ function noWhitespaceValidator(): ValidatorFn { stepLink ] }) - -export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewInit { +export class ProfessionalFormComponent implements OnInit, OnDestroy { obraSocialControl = new FormControl(''); filteredObrasSociales: Observable; organizacionControl = new FormControl(''); - // Suscripciones private subscriptions: Subscription = new Subscription(); @@ -124,7 +61,7 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn private destroy$ = new Subject(); professionalForm: FormGroup; - filteredSupplies = []; + request; storedSupplies = []; today = new Date(); @@ -135,7 +72,7 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn minDate = new Date(); maxDate = new Date(new Date().setMonth(new Date().getMonth() + 1)); isSubmit = false; - supplySpinner: { show: boolean }[] = [{ show: false }, { show: false }]; + myPrescriptions: Prescriptions[] = []; isEditCertificate = false; isEdit = false; @@ -155,7 +92,7 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn constructor( // private suppliesService: SuppliesService, - private snomedSuppliesService: SnomedSuppliesService, + private fBuilder: FormBuilder, private apiPatients: PatientsService, private apiPrescriptions: PrescriptionsService, // privado @@ -228,12 +165,6 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn } } - ngAfterViewInit() { - // Implementation not needed for this case - } - - // Método removido - funcionalidad manejada por patient-form component - initProfessionalForm() { this.today = new Date((new Date())); this.minDate = new Date(); @@ -275,12 +206,6 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn quantity.updateValueAndValidity(); } - // Método removido - funcionalidad manejada por patient-form component - - // Método removido - funcionalidad manejada por patient-form component - - // Método removido - funcionalidad manejada por patient-form component - onSubmitProfessionalForm(professionalNgForm: FormGroupDirective): void { if (this.professionalForm.valid) { const newPrescription = { ...this.professionalForm.value }; @@ -377,33 +302,6 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn return this.professionalForm.get('supplies') as FormArray; } - // Getters removidos - funcionalidad manejada por patient-form component - - // Método removido - funcionalidad manejada por patient-form component - - displayFn(supply): string { - return supply ? supply : ''; - } - - onSupplySelected(supply, index: number) { - const control = this.suppliesForm.at(index); // Obtiene el FormGroup en la posición del array - const supplyControl = control.get('supply'); - - // Actualiza el valor del 'supply' con el 'term' en el 'name' - supplyControl.get('name').setValue(supply.term); // Actualiza solo el 'term' en 'name' - - // También actualizamos el 'snomedConcept' completo con todos los campos - supplyControl.setValue({ - name: supply.term, // Solo el 'term' va en 'name' - snomedConcept: { - term: supply.term, - fsn: supply.fsn, - conceptId: supply.conceptId, - semanticTag: supply.semanticTag - } - }); - supplyControl.get('name').updateValueAndValidity(); - } addSupply() { const supplies = this.fBuilder.group({ @@ -419,6 +317,7 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn conceptId: [''], semanticTag: [''] }), + brand: [null] }), quantity: ['', [ Validators.required, @@ -443,40 +342,10 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn triplicateDataGroup.get('numero')?.disable(); this.suppliesForm.push(supplies); - this.supplySpinner.push({ show: false }); - this.subscribeToSupplyChanges(supplies, this.suppliesForm.length - 1); - this.subscribeToTriplicateChanges(supplies, this.suppliesForm.length - 1); - this.subscribeToDuplicateChanges(supplies, this.suppliesForm.length - 1); - } - subscribeToSupplyChanges(control: FormGroup, index: number) { - control.get('supply.name').valueChanges.pipe( - debounceTime(300), - distinctUntilChanged() - ).subscribe((supply: string) => { - if (typeof supply === 'string') { - const snomedConcept = control.get('supply.snomedConcept'); - const currentConceptId = snomedConcept?.get('conceptId')?.value; - - if (currentConceptId && supply !== snomedConcept?.get('term')?.value) { - snomedConcept.reset(); - control.get('supply.name').updateValueAndValidity(); - } - if (supply.length > 3) { - this.supplySpinner[index] = { show: true }; - this.snomedSuppliesService.get(supply).pipe( - catchError(() => { - this.supplySpinner[index] = { show: false }; - return of([]); - }) - ).subscribe((res) => { - this.supplySpinner[index] = { show: false }; - this.filteredSupplies = [...res]; - }); - } - } - }); + this.subscribeToTriplicateChanges(supplies, this.suppliesForm.length - 1); + this.subscribeToDuplicateChanges(supplies, this.suppliesForm.length - 1); } medicationSelectedValidator(): ValidatorFn { @@ -509,7 +378,6 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn return isWhitespace ? { 'whitespace': { value: control.value } } : null; }; } - subscribeToTriplicateChanges(control: FormGroup, index: number) { const triplicateControl = control.get('triplicate'); const triplicateDataGroup = control.get('triplicateData') as FormGroup; @@ -558,7 +426,7 @@ export class ProfessionalFormComponent implements OnInit, OnDestroy, AfterViewIn deleteSupply(index: number) { this.suppliesForm.removeAt(index); - this.supplySpinner.splice(index, 1); + } // set form with prescriptions values and disabled npt editable fields diff --git a/src/app/professionals/components/supply-search/supply-search.component.html b/src/app/professionals/components/supply-search/supply-search.component.html new file mode 100644 index 00000000..3223d22b --- /dev/null +++ b/src/app/professionals/components/supply-search/supply-search.component.html @@ -0,0 +1,22 @@ + + + Medicamento + + + + +
{{sup.display.name | titlecase}}
+
+ Marca sugerida: {{sup.display.brand | titlecase}} +
+
+
+ + + Debe seleccionar un insumo + + + {{ parentForm.getError('invalid') }} + +
+
\ No newline at end of file diff --git a/src/app/professionals/components/supply-search/supply-search.component.sass b/src/app/professionals/components/supply-search/supply-search.component.sass new file mode 100644 index 00000000..76370179 --- /dev/null +++ b/src/app/professionals/components/supply-search/supply-search.component.sass @@ -0,0 +1,16 @@ +:host + display: block + width: 100% + +.w-100 + width: 100% + +.item-title + font-size: 15px + font-weight: 400 + line-height: normal + +.item-subtitle + font-size: 14.5px + color: #4056b5 + line-height: normal diff --git a/src/app/professionals/components/supply-search/supply-search.component.ts b/src/app/professionals/components/supply-search/supply-search.component.ts new file mode 100644 index 00000000..1d0ae46d --- /dev/null +++ b/src/app/professionals/components/supply-search/supply-search.component.ts @@ -0,0 +1,126 @@ +import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; +import { SnomedSuppliesService } from '@services/snomedSupplies.service'; +import { Subscription, of } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; +import { ThemePalette } from '@angular/material/core'; + +@Component({ + selector: 'app-supply-search', + templateUrl: './supply-search.component.html', + styleUrls: ['./supply-search.component.sass'] +}) +export class SupplySearchComponent implements OnInit, OnDestroy { + @Input() parentForm: FormGroup; + + filteredSupplies: any[] = []; + loading = false; + readonly spinnerColor: ThemePalette = 'primary'; + private sub: Subscription; + + constructor(private snomedSuppliesService: SnomedSuppliesService) { } + + ngOnInit() { + if (this.parentForm && this.parentForm.get('name')) { + this.sub = this.parentForm.get('name').valueChanges.pipe( + debounceTime(300), + distinctUntilChanged(), + filter((term: string) => typeof term === 'string' && term.length > 3), + switchMap((term: string) => { + this.loading = true; + return this.snomedSuppliesService.get(term).pipe( + catchError(() => { + this.loading = false; + return of([]); + }) + ); + }) + ).subscribe((res) => { + this.loading = false; + this.filteredSupplies = res.map(supply => { + const display = this.parseSupplyTerm(supply); + return { ...supply, display }; + }); + }); + } + } + + + private parseSupplyTerm(supply: any): { name: string, brand?: string } { + if (supply.semanticTag === 'fármaco de uso clínico comercial') { + const match = supply.term.match(/^(.*?) \[(.*?)\] (.*)$/); + + if (match) { + const brand = match[1]; + const generic = match[2]; + const form = match[3]; + return { + name: `${generic} (${form})`, + brand: brand + }; + } + } + return { name: supply.term }; + } + + + ngOnDestroy() { + if (this.sub) { + this.sub.unsubscribe(); + } + } + + onSupplySelected(event: any) { + const supply = event.option.value; + const term = this.toTitleCase(supply.term); + // Update name with just the term + this.parentForm.get('name').setValue(term, { emitEvent: false }); + + let snomedConcept = { + term: supply.term, + fsn: supply.fsn, + conceptId: supply.conceptId, + semanticTag: supply.semanticTag + }; + + if (supply.relationships && supply.relationships.length > 0) { + const rel = supply.relationships[0]; + snomedConcept = { + term: rel.term, + fsn: rel.fsn, + conceptId: rel.conceptId, + semanticTag: rel.semanticTag + }; + if (!snomedConcept.semanticTag) { + const match = rel.fsn.match(/\(([^)]+)\)$/); + if (match) { + snomedConcept.semanticTag = match[1]; + } + } + } + + const formValue: any = { + name: term, + snomedConcept: snomedConcept, + brand: null + }; + + if (supply.semanticTag === 'fármaco de uso clínico comercial') { + const parsed = this.parseSupplyTerm(supply); + if (parsed.brand) { + formValue.brand = parsed.brand; + formValue.name = this.toTitleCase(parsed.name); + } + } + + // Update the whole form group with known structure + this.parentForm.setValue(formValue, { emitEvent: false }); + } + + private toTitleCase(str: string): string { + if (!str) { return ''; } + return str.toLowerCase().split(' ').map(word => { + return (word.charAt(0).toUpperCase() + word.slice(1)); + }).join(' '); + } +} diff --git a/src/app/professionals/professionals.module.ts b/src/app/professionals/professionals.module.ts index 30cc03cc..c02251f9 100644 --- a/src/app/professionals/professionals.module.ts +++ b/src/app/professionals/professionals.module.ts @@ -37,6 +37,7 @@ import { PracticesFormComponent } from './components/practices-form/practices-fo import { CertificatePracticePrinterComponent } from './components/certificate-practice-printer/certificate-practice-printer.component'; import { SelectorAmbitoComponent } from './components/selector-ambito/selector-ambito.component'; import { EditUserInfoComponent } from './components/edit-user-info/edit-user-info.component'; +import { SupplySearchComponent } from './components/supply-search/supply-search.component'; import { PatientFormComponent } from '@shared/components/patient-form/patient-form.component'; import { SharedModule } from '@shared/shared.module'; import { PatientNamePipe } from '@shared/pipes/patient-name.pipe'; @@ -56,6 +57,7 @@ import { OrganizacionDialogComponent } from './components/organizacion-dialog/or OrganizacionesSelectorComponent, OrganizacionDialogComponent, PatientFormComponent, + SupplySearchComponent ], imports: [ CommonModule, diff --git a/src/app/shared/components/unified-printer/unified-printer.component.ts b/src/app/shared/components/unified-printer/unified-printer.component.ts index 633d462e..7a59e429 100644 --- a/src/app/shared/components/unified-printer/unified-printer.component.ts +++ b/src/app/shared/components/unified-printer/unified-printer.component.ts @@ -122,6 +122,11 @@ export class UnifiedPrinterComponent { new Columns([new Txt(`${cant} `).bold().end]).end]).end); pdf.add(new Txt('\n').end); + if (supply.supply.brand) { + pdf.add(new Txt('Marca sugerida: ' + supply.supply.brand).end); + pdf.add(new Txt('\n').end); + } + if (supply.diagnostic) { pdf.add(new Txt('\n').end); pdf.add(new Txt('Diagnóstico').bold().end);