mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
Compare commits
4 Commits
9890559ec1
...
a837c95d8c
| Author | SHA1 | Date | |
|---|---|---|---|
| a837c95d8c | |||
| ac76ef5487 | |||
| 99954c1ff2 | |||
| 4b84792619 |
@@ -10,7 +10,7 @@ export default defineConfig({
|
|||||||
fileName: () => 'widget.js',
|
fileName: () => 'widget.js',
|
||||||
formats: ['iife'],
|
formats: ['iife'],
|
||||||
},
|
},
|
||||||
outDir: '../../helix-engage-server/public',
|
outDir: './dist',
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
minify: 'esbuild',
|
minify: 'esbuild',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export class SetupStateController {
|
|||||||
uiFlags() {
|
uiFlags() {
|
||||||
return {
|
return {
|
||||||
setupManaged: process.env.HELIX_SETUP_MANAGED === 'true',
|
setupManaged: process.env.HELIX_SETUP_MANAGED === 'true',
|
||||||
|
telephonyEnabled: process.env.TELEPHONY_ENABLED !== 'false', // default true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import { PlatformModule } from '../platform/platform.module';
|
|||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { ConfigThemeModule } from '../config/config-theme.module';
|
import { ConfigThemeModule } from '../config/config-theme.module';
|
||||||
import { CallerResolutionModule } from '../caller/caller-resolution.module';
|
import { CallerResolutionModule } from '../caller/caller-resolution.module';
|
||||||
|
import { LeadsModule } from '../leads/leads.module';
|
||||||
|
import { SupervisorModule } from '../supervisor/supervisor.module';
|
||||||
|
|
||||||
// WidgetKeysService lives in ConfigThemeModule now — injected here via the
|
|
||||||
// module's exports. This module only owns the widget-facing API endpoints
|
|
||||||
// (init / chat / book / lead) plus the NestJS guards that consume the keys.
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PlatformModule, AuthModule, ConfigThemeModule, forwardRef(() => CallerResolutionModule)],
|
imports: [PlatformModule, AuthModule, ConfigThemeModule, forwardRef(() => CallerResolutionModule), LeadsModule, SupervisorModule],
|
||||||
controllers: [WidgetController, WebhooksController],
|
controllers: [WidgetController, WebhooksController],
|
||||||
providers: [WidgetService, WidgetChatService],
|
providers: [WidgetService, WidgetChatService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { WidgetInitResponse, WidgetBookRequest, WidgetLeadRequest } from '.
|
|||||||
import { ThemeService } from '../config/theme.service';
|
import { ThemeService } from '../config/theme.service';
|
||||||
import { DOCTOR_VISIT_SLOTS_FRAGMENT, normalizeDoctors, type NormalizedDoctor } from '../shared/doctor-utils';
|
import { DOCTOR_VISIT_SLOTS_FRAGMENT, normalizeDoctors, type NormalizedDoctor } from '../shared/doctor-utils';
|
||||||
import { CallerResolutionService } from '../caller/caller-resolution.service';
|
import { CallerResolutionService } from '../caller/caller-resolution.service';
|
||||||
|
import { LeadAutoAssignService } from '../leads/lead-auto-assign.service';
|
||||||
|
import { SupervisorService } from '../supervisor/supervisor.service';
|
||||||
|
|
||||||
// Dedup window: any lead created for this phone within the last 24h is
|
// Dedup window: any lead created for this phone within the last 24h is
|
||||||
// considered the same visitor's lead — chat + book + contact by the same
|
// considered the same visitor's lead — chat + book + contact by the same
|
||||||
@@ -15,6 +17,7 @@ export type FindOrCreateLeadOpts = {
|
|||||||
source?: string;
|
source?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
interestedService?: string;
|
interestedService?: string;
|
||||||
|
createPatient?: boolean; // default false — only booking creates patients
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -27,6 +30,8 @@ export class WidgetService {
|
|||||||
private theme: ThemeService,
|
private theme: ThemeService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private caller: CallerResolutionService,
|
private caller: CallerResolutionService,
|
||||||
|
private autoAssign: LeadAutoAssignService,
|
||||||
|
private supervisor: SupervisorService,
|
||||||
) {
|
) {
|
||||||
this.apiKey = config.get<string>('platform.apiKey') ?? '';
|
this.apiKey = config.get<string>('platform.apiKey') ?? '';
|
||||||
}
|
}
|
||||||
@@ -56,26 +61,27 @@ export class WidgetService {
|
|||||||
const lastName = name.split(' ').slice(1).join(' ') || '';
|
const lastName = name.split(' ').slice(1).join(' ') || '';
|
||||||
|
|
||||||
if (resolved.isNew) {
|
if (resolved.isNew) {
|
||||||
// Net-new visitor — create Patient + Lead with the widget-
|
// Net-new visitor — create Lead. Patient is only created
|
||||||
// collected name. Both records get the real name from the
|
// when explicitly requested (e.g., booking an appointment).
|
||||||
// first moment they exist.
|
|
||||||
let patientId: string | undefined;
|
let patientId: string | undefined;
|
||||||
try {
|
if (opts.createPatient) {
|
||||||
const p = await this.platform.queryWithAuth<any>(
|
try {
|
||||||
`mutation($data: PatientCreateInput!) { createPatient(data: $data) { id } }`,
|
const p = await this.platform.queryWithAuth<any>(
|
||||||
{
|
`mutation($data: PatientCreateInput!) { createPatient(data: $data) { id } }`,
|
||||||
data: {
|
{
|
||||||
name: `${firstName} ${lastName}`.trim() || 'Unknown',
|
data: {
|
||||||
fullName: { firstName, lastName },
|
name: `${firstName} ${lastName}`.trim() || 'Unknown',
|
||||||
phones: { primaryPhoneNumber: `+91${phone}` },
|
fullName: { firstName, lastName },
|
||||||
patientType: 'NEW',
|
phones: { primaryPhoneNumber: `+91${phone}` },
|
||||||
|
patientType: 'NEW',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
this.auth,
|
||||||
this.auth,
|
);
|
||||||
);
|
patientId = p?.createPatient?.id;
|
||||||
patientId = p?.createPatient?.id;
|
} catch (err) {
|
||||||
} catch (err) {
|
this.logger.warn(`Widget patient create failed (${phone}): ${err}`);
|
||||||
this.logger.warn(`Widget patient create failed (${phone}): ${err}`);
|
}
|
||||||
}
|
}
|
||||||
const created = await this.platform.queryWithAuth<any>(
|
const created = await this.platform.queryWithAuth<any>(
|
||||||
`mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`,
|
`mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`,
|
||||||
@@ -259,6 +265,7 @@ export class WidgetService {
|
|||||||
source: 'WEBSITE',
|
source: 'WEBSITE',
|
||||||
status: 'APPOINTMENT_SET',
|
status: 'APPOINTMENT_SET',
|
||||||
interestedService: req.chiefComplaint ?? 'Appointment Booking',
|
interestedService: req.chiefComplaint ?? 'Appointment Booking',
|
||||||
|
createPatient: true,
|
||||||
});
|
});
|
||||||
// Idempotent upgrade: if the lead was reused from an earlier chat/
|
// Idempotent upgrade: if the lead was reused from an earlier chat/
|
||||||
// contact, promote its status and reflect the new interest.
|
// contact, promote its status and reflect the new interest.
|
||||||
@@ -274,6 +281,13 @@ export class WidgetService {
|
|||||||
const reference = appt.createAppointment.id.substring(0, 8).toUpperCase();
|
const reference = appt.createAppointment.id.substring(0, 8).toUpperCase();
|
||||||
this.logger.log(`Widget booking: ${req.patientName} → ${req.doctorId} at ${req.scheduledAt} (ref: ${reference})`);
|
this.logger.log(`Widget booking: ${req.patientName} → ${req.doctorId} at ${req.scheduledAt} (ref: ${reference})`);
|
||||||
|
|
||||||
|
// Emit SSE so agents see the new appointment immediately
|
||||||
|
this.supervisor.emitWorklistUpdate({
|
||||||
|
type: 'widget-appointment',
|
||||||
|
callerPhone: this.normalizePhone(req.patientPhone),
|
||||||
|
callerName: req.patientName,
|
||||||
|
});
|
||||||
|
|
||||||
return { appointmentId: appt.createAppointment.id, reference };
|
return { appointmentId: appt.createAppointment.id, reference };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +298,18 @@ export class WidgetService {
|
|||||||
interestedService: req.interest ?? 'Website Enquiry',
|
interestedService: req.interest ?? 'Website Enquiry',
|
||||||
});
|
});
|
||||||
this.logger.log(`Widget contact: ${req.name} (${this.normalizePhone(req.phone)}) — ${req.interest ?? 'general'}`);
|
this.logger.log(`Widget contact: ${req.name} (${this.normalizePhone(req.phone)}) — ${req.interest ?? 'general'}`);
|
||||||
|
|
||||||
|
// Trigger immediate auto-assign + SSE so agent sees the lead instantly
|
||||||
|
this.autoAssign.runOnce().then((result) => {
|
||||||
|
if (result.assigned > 0) {
|
||||||
|
this.supervisor.emitWorklistUpdate({
|
||||||
|
type: 'widget-lead',
|
||||||
|
callerPhone: this.normalizePhone(req.phone),
|
||||||
|
callerName: req.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
return { leadId };
|
return { leadId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "widget-src", "public", "data", "**/*spec.ts"]
|
"exclude": ["node_modules", "test", "dist", "widget-src", "packages", "public", "data", "**/*spec.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,5 +22,5 @@
|
|||||||
"strictBindCallApply": true,
|
"strictBindCallApply": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"exclude": ["widget-src", "public", "data"]
|
"exclude": ["widget-src", "packages", "public", "data"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user