diff --git a/src/app.module.ts b/src/app.module.ts index fd8db41..5c55f4a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { GraphqlProxyModule } from './graphql-proxy/graphql-proxy.module'; import { HealthModule } from './health/health.module'; import { WorklistModule } from './worklist/worklist.module'; import { CallAssistModule } from './call-assist/call-assist.module'; +import { SearchModule } from './search/search.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { CallAssistModule } from './call-assist/call-assist.module'; HealthModule, WorklistModule, CallAssistModule, + SearchModule, ], }) export class AppModule {} diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts index 5849852..40885e6 100644 --- a/src/ozonetel/ozonetel-agent.controller.ts +++ b/src/ozonetel/ozonetel-agent.controller.ts @@ -53,6 +53,29 @@ export class OzonetelAgentController { } } + @Post('agent-state') + async agentState( + @Body() body: { state: 'Ready' | 'Pause'; pauseReason?: string }, + ) { + if (!body.state) { + throw new HttpException('state required', 400); + } + + this.logger.log(`Agent state change: ${this.defaultAgentId} → ${body.state} (${body.pauseReason ?? ''})`); + + try { + const result = await this.ozonetelAgent.changeAgentState({ + agentId: this.defaultAgentId, + state: body.state, + pauseReason: body.pauseReason, + }); + return result; + } catch (error: any) { + const message = error.response?.data?.message ?? error.message ?? 'State change failed'; + return { status: 'error', message }; + } + } + @Post('agent-ready') async agentReady() { this.logger.log(`Force ready: logging out and back in agent ${this.defaultAgentId}`); diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts new file mode 100644 index 0000000..9944c51 --- /dev/null +++ b/src/search/search.controller.ts @@ -0,0 +1,94 @@ +import { Controller, Get, Query, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PlatformGraphqlService } from '../platform/platform-graphql.service'; + +@Controller('api/search') +export class SearchController { + private readonly logger = new Logger(SearchController.name); + private readonly platformApiKey: string; + + constructor( + private readonly platform: PlatformGraphqlService, + private readonly config: ConfigService, + ) { + this.platformApiKey = config.get('platform.apiKey') ?? ''; + } + + @Get() + async search(@Query('q') query?: string) { + if (!query || query.length < 2) { + return { leads: [], patients: [], appointments: [] }; + } + + const authHeader = this.platformApiKey ? `Bearer ${this.platformApiKey}` : ''; + if (!authHeader) { + return { leads: [], patients: [], appointments: [] }; + } + + this.logger.log(`Search: "${query}"`); + + // Fetch all three in parallel, filter client-side for flexible matching + try { + const [leadsResult, patientsResult, appointmentsResult] = await Promise.all([ + this.platform.queryWithAuth( + `{ leads(first: 50) { edges { node { + id name contactName { firstName lastName } + contactPhone { primaryPhoneNumber } + source status interestedService + } } } }`, + undefined, authHeader, + ).catch(() => ({ leads: { edges: [] } })), + + this.platform.queryWithAuth( + `{ patients(first: 50) { edges { node { + id name fullName { firstName lastName } + phones { primaryPhoneNumber } + gender dateOfBirth + } } } }`, + undefined, authHeader, + ).catch(() => ({ patients: { edges: [] } })), + + this.platform.queryWithAuth( + `{ appointments(first: 50, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { + id scheduledAt doctorName department appointmentStatus patientId + } } } }`, + undefined, authHeader, + ).catch(() => ({ appointments: { edges: [] } })), + ]); + + const q = query.toLowerCase(); + + const leads = (leadsResult.leads?.edges ?? []) + .map((e: any) => e.node) + .filter((l: any) => { + const name = `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase(); + const phone = l.contactPhone?.primaryPhoneNumber ?? ''; + return name.includes(q) || phone.includes(q) || (l.name ?? '').toLowerCase().includes(q); + }) + .slice(0, 5); + + const patients = (patientsResult.patients?.edges ?? []) + .map((e: any) => e.node) + .filter((p: any) => { + const name = `${p.fullName?.firstName ?? ''} ${p.fullName?.lastName ?? ''}`.toLowerCase(); + const phone = p.phones?.primaryPhoneNumber ?? ''; + return name.includes(q) || phone.includes(q) || (p.name ?? '').toLowerCase().includes(q); + }) + .slice(0, 5); + + const appointments = (appointmentsResult.appointments?.edges ?? []) + .map((e: any) => e.node) + .filter((a: any) => { + const doctor = (a.doctorName ?? '').toLowerCase(); + const dept = (a.department ?? '').toLowerCase(); + return doctor.includes(q) || dept.includes(q); + }) + .slice(0, 5); + + return { leads, patients, appointments }; + } catch (err: any) { + this.logger.error(`Search failed: ${err.message}`); + return { leads: [], patients: [], appointments: [] }; + } + } +} diff --git a/src/search/search.module.ts b/src/search/search.module.ts new file mode 100644 index 0000000..59cdd81 --- /dev/null +++ b/src/search/search.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { SearchController } from './search.controller'; +import { PlatformModule } from '../platform/platform.module'; + +@Module({ + imports: [PlatformModule], + controllers: [SearchController], +}) +export class SearchModule {}