# Dialer Vue Components

`@kmhgmbh/dialer-vue-components` — a Vue 3 component library for telephony and dialer functions against the Vocalcom Hermes call-center system. Ships as an npm package (ES/CJS/UMD builds, `vue` externalized), UI built with Vuetify.

## Documentation

Additional documentation can be found at [Confluence](https://kmhgmbh.atlassian.net/l/cp/V0fWa8B1).

## Requirements

- Node.js 18 (see `.nvmrc`)
- Vue 3 (peer dependency)

## Installation

```bash
npm install @kmhgmbh/dialer-vue-components
```

Register the plugin with the adapter type (currently only `hermes`) and import the styles:

```ts
import { createApp } from 'vue';
import plugin from '@kmhgmbh/dialer-vue-components';
import '@kmhgmbh/dialer-vue-components/style.css';

const app = createApp(App);
app.use(plugin, { type: 'hermes' });
```

Installing the plugin does three things:

1. Globally registers the public components `TopBar`, `CallController` and `CampaignsController`.
2. Creates the telephony singleton and exposes it as `globalProperties.$telephonyAdapter` and via `provide('telephonyAdapter')`.
3. Injects the Hermes vendor scripts as `<script>` tags (in order): `/hermes/agentlink_enums.js`, `/hermes/AgentLink.js`, `/hermes/swfobject.js`, `/hermes/wsjavascript.js`.

> **Important:** The consuming app must serve the files from `public/hermes/` under the `/hermes/` path, otherwise the vendor scripts cannot be loaded and the adapter will not work.

## Usage

There are two ways to use the library: via the ready-made components (recommended) or directly via the telephony adapter.

### Components

#### `<call-controller>`

The main UI with all telephony functions (connect, call, hangup, hold, transfer, pause, recording).

```vue
<call-controller
  type="hermes"
  :config="config"
  :credentials="{ username: 'agentId', password: 'secret', extension: '11010' }"
  :campaign="{ id: 'campaignId', description: 'campaignDescription' }"
  :webrtc="{ wsUrl: 'wss://…/ws', uri: 'sip:11010@…', extension: '11010' }"
  :is-webrtc="true"
  :is-snackbar-active="true"
  :is-logging-enabled="true"
  @status="onStatus"
  @error="onError"
/>
```

Props:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `String` | — (required) | Adapter type, currently `'hermes'` |
| `config` | `Object` | — (required) | Hermes connection config (see [Configuration](#configuration)) |
| `credentials` | `{ username, password, extension }` | — (required) | Login credentials |
| `campaign` | `{ id, description }` | — (required) | Manual campaign set on login |
| `isSnackbarActive` | `Boolean` | `true` | Toast notifications for errors on/off |
| `isLoggingEnabled` | `Boolean` | `true` | Adapter logging on/off |
| `isWebrtc` | `Boolean` | `false` | Audio via WebRTC (JsSIP) instead of a physical phone |
| `isOutboundUser` | `Boolean` | `false` | Outbound mode (campaign dialing, preview calls) |
| `webrtc` | `WebRtcConfig` | `undefined` | WebRTC settings: `{ wsUrl, uri, extension, logging? }` |
| `phonenumber` | `String` | `''` | Pre-filled phone number |
| `displayPhoneNumber` | `String` | `''` | Number presented to the callee (CLI) |
| `showAudioWaveform` | `Boolean` | `true` | Audio waveform visualization during the call |

Events:

| Event | Payload | Description |
| --- | --- | --- |
| `status` | `string` | Status changes (`CONNECTED`, `LOGGED_IN`, `SESSION_START`, …) |
| `error` | `string` | Error messages (also shown as snackbar if enabled) |
| `openCustomer` | `SessionInfo` | On session start in outbound mode — contains session ID, contact number, campaign |
| `loadAgentsAndServices` | — | Request from the transfer dialog to load agents/service numbers |

#### `<top-bar>`

A wrapper around `CallController` for embedding in an app header. Accepts the same props (`type`, `config`, `credentials`, `campaign`, `webrtc`, `isSnackbarActive`, `isWebrtc`, `isOutboundUser`, `isLoggingEnabled`) and forwards the `status` and `error` events.

#### `<campaigns-controller>`

UI for inbound/outbound campaigns and queues (start/stop, agent counts per queue, pause state).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showCallback` | `Boolean` | `false` | Show callback campaigns |
| `inboundAvailableAgentList` | `Array` | `[]` | Available agents for inbound queue auto-start |
| `updatedCampaignNames` | `Array` | `[]` | Overrides for campaign display names |
| `autoStartOutboundCampaign` | `String` | `''` | ID of an outbound campaign to start automatically |

### Direct use via the telephony adapter

The adapter is a singleton and can be accessed in three ways:

```ts
// 1. Composable (recommended)
import { useTelephony } from '@kmhgmbh/dialer-vue-components';
const telephonyAdapter = useTelephony('hermes');

// 2. provide/inject
const telephonyAdapter = inject('telephonyAdapter');

// 3. globalProperties
const telephonyAdapter = getCurrentInstance()?.appContext.config.globalProperties.$telephonyAdapter;
```

Typical flow:

```ts
telephonyAdapter.setConfig(config);
telephonyAdapter.init(true, false); // (isLoggingEnabled, isOutboundUser)
await telephonyAdapter.login('username', 'password', 'extension');
telephonyAdapter.call('+4912345678', '+4998765432');
telephonyAdapter.hangup();
telephonyAdapter.logout();
```

#### API overview

**Setup & login**

| Function | Description |
| --- | --- |
| `setConfig(config)` | Sets the connection configuration and creates the adapter (only once) |
| `init(isLoggingEnabled, isOutboundUser)` | Initializes the connection to the phone system |
| `login(username, password, extension)` | Logs the agent in (async; all parameters required) |
| `logout()` | Logs the agent out and unregisters WebRTC if needed |

**Calls**

| Function | Description |
| --- | --- |
| `call(phonenumber, displayPhonenumber)` | Starts a call; `displayPhonenumber` is the presented number |
| `hangup()` | Ends the call |
| `hold()` / `retrieve()` | Puts the call on hold / retrieves it |
| `redial(phonenumber)` | Redial |
| `previewCancel()` | Cancels a preview call (outbound) |
| `getSessionId()` / `getSession()` | Session ID / session info of the current call |

**Transfer**

| Function | Description |
| --- | --- |
| `doInternalBlindTransfer(agentId)` | Blind transfer to another agent |
| `doBlindTransfer(number, isExternal?)` | Blind transfer to a number |
| `doWarmHandover(number, isExternal?)` | Warm handover (announced transfer) |
| `handoverCall()` | Completes the handover |

**Campaigns & queues**

| Function | Description |
| --- | --- |
| `getManualCampaigns()` / `setManualCamapaign(campaign)` | Get / set manual campaigns (login required) |
| `getCampaigns()` / `getQueues()` | Available campaigns / queues |
| `startQueue(context, queue, campaignId)` / `stopQueue(…)` | Start / stop queues |

**Pause & agent state**

| Function | Description |
| --- | --- |
| `getPauseOptions()` | Available pause reasons |
| `requestPause(pauseCode)` / `stopPause()` | Request / end pause |
| `getState()` / `getAgentState()` / `getTelephonyState()` | Current adapter / agent / telephony state |

**Recording & call status**

| Function | Description |
| --- | --- |
| `startRecording(fileName)` / `stopRecording()` | Start / stop call recording (login required) |
| `canSetCallStatus()` / `setCallStatus(callStatus)` | Set call qualification (status, follow-up, comment) |

**WebRTC & audio**

| Function | Description |
| --- | --- |
| `registerWebRTC(config)` / `unregisterWebRTC()` | SIP registration via JsSIP (`{ wsUrl, uri, extension, logging? }`) |
| `toogleWebRTC(status)` | Enables/disables WebRTC for outgoing calls |
| `getAvailableMediaDevices()` | Available microphones/speakers |
| `setSelectedMicrophoneId(id)` / `setSelectedSpeakerId(id)` | Select audio devices |
| `getSelectedMicrophoneId()` / `getSelectedSpeakerId()` | Currently selected devices |
| `negotiateNewMediaStream()` | Renegotiates the media stream after a device change |
| `isMicropheAllowed()` | Checks the microphone permission |

**Reactive state** (Vue refs on the adapter): `state`, `isPause`, `pauseName`, `pauseTime`, `agentId`, `agentExtension`, `remoteAudioStream`, `localAudioStream`, `lastContactAgents`, `serviceNumbers`, `availableAgents`.

## Configuration

The `config` object for `setConfig()` / the `config` prop:

```ts
{
  AgentProxy: 'hermes-proxy.example.com',
  Port: 9992,
  CustomerId: 1,
  Locale: 'en-US',
  LocalWebServiceProxy: 'WSProxy.ashx',
  AdminUrl: 'http://…/hermes_net_v5/admin/',
  AdminServerUrl: 'http://…/hermes_net_v5/admin/',
  OnMediaWebService: 'http://…/MailService.asmx?OMS=…',
  CrmUrl: 'http://…/hermes_net_v5/CRM/',
  CrmServerUrl: 'http://…/hermes_net_v5/CRM/',
  apiUrl: 'http://…',
}
```

### Hermes 6.3.x authentication

As of Hermes 6.3.x, a separate authentication flow is required before the AgentLink login (verification token → RSA public key → hybrid-encrypted credentials → SignIn cookies → private token). The library handles this automatically via the internal `HermesAuthService` when the config additionally contains one of the following fields:

```ts
{
  // …config as above…
  hermesBaseUrl: 'https://hermes.example.com', // '' = relative paths (e.g. through a dev proxy)
  useHermesAuth: true,                          // alternatively: enable explicitly
}
```

Without these fields the adapter falls back to the legacy direct login. The actual `AgentLink.Login` is deferred via polling until the AgentLink connection is ready.

## Events

The adapter emits events you can subscribe to with `on()` (and unsubscribe with `off()`):

```ts
import { TelephonyEvents } from '@kmhgmbh/dialer-vue-components';

telephonyAdapter.on(TelephonyEvents.ERROR, (message: string) => {
  setNotification(message);
});
```

| Event | Description |
| --- | --- |
| `CONNECTED` | Plugin is connected to the phone system |
| `DISCONNECTED` | Connection to the phone system lost |
| `LOGGED_IN` | Agent logged in |
| `LOGGED_OUT` | Agent logged out |
| `SESSION_START` | Call established, session created (message: `SessionInfo`) |
| `SESSION_END` | Call ended |
| `SESSION_STATE` | Session state changed (message: `{ contextType, sessionId, label }`, third argument: `SessionTelephonyStates` code) |
| `AGENT_STATE` | Global agent state changed (message: localized state label, third argument: `AgentGlobalStates` code) |
| `TELEPHONY_STATE` | Telephony context state changed (message: localized state label) |
| `RECORD_START` / `RECORD_STOP` | Call recording started / stopped |
| `CALL_TRANSFERRED` | Call was transferred (blind transfer or handover) |
| `ERROR` | Error detected |
| `CONNECTION_ERROR` | Connection or login problem detected |
| `DEBUG` | Diagnostic information (e.g. WebRTC/ICE connection issues) |

All event names are exported as the `TelephonyEvents` constant; the numeric agent state codes as `AgentGlobalStates` (`Off`, `Waiting`, `Working`, `Pause`).

## Notifications

By default the `CallController` component shows a toast notification for errors. This can be toggled via the `isSnackbarActive` prop.

## Development

### Setup

```bash
npm install
cp example.env .env   # then fill in credentials/endpoints (VITE_* variables)
```

### Dev harness

`npm run dev` starts a local test app (`src/App.vue` + `TestBar.vue`) on port 8081. It is not part of the published library (the lib entry is `src/index.ts`) and reads credentials and Hermes/WebRTC endpoints from `.env`.

The Vite dev server proxies `/hermes360` to `http://localhost:8082` and rewrites the Set-Cookie headers (removes Domain/Secure, sets SameSite) so the Hermes auth cookie flow works on localhost — see [vite.config.ts](vite.config.ts).

### Commands

```bash
npm run dev          # dev harness on port 8081
npm run build        # vue-tsc type check + vite lib build
npm run lint         # eslint
npm run lint:fix     # eslint with auto-fix
npm run type-check   # vue-tsc for node/vitest/app tsconfigs
npm test             # vitest with coverage, single run
npm run test:unit    # vitest in watch mode
npx vitest run tests/Vue.spec.ts   # single test file
```

### Tests

Tests live in `tests/`, run on `happy-dom` and use the mount helpers from `tests/test-suite.ts` (`findByTestAttr()` / `findAllByTestAttr()` select elements by `data-test` attribute — use these attributes in components for testability).

### Note on the vendor scripts

The files in `public/hermes/` (`AgentLink.js`, `agentlink_enums.js`, …) are untyped vendor globals from Vocalcom. They are excluded from ESLint and test coverage and should not be refactored.

## Project structure

- `./docs` — documentation in markdown format (mkdocs)
- `./public` — public assets, including the Hermes vendor scripts (`public/hermes/`)
- `./src` — source code
  - `src/index.ts` — library entry (Vue plugin)
  - `src/components/` — Vue components (button components in `buttons/`)
  - `src/composables/` — `telephony` (facade/singleton), `webrtc` (JsSIP), `eventBus`, `timer`, `audioWaveform`
  - `src/adapters/` — `AgentLink` adapter around the Hermes vendor API
  - `src/services/` — `HermesAuthService` (Hermes 6.3.x auth flow)
  - `src/constants/` — event and state constants
- `./tests` — unit tests (Vitest)

## CI & publishing

Bitbucket Pipelines:

- **Pull requests:** sandworm audit, `npm run lint`, `npm test`
- **Tag `v*`:** build and automatic publish to npmjs.com
- **`main` branch:** publish step available as a manual trigger

Before releasing, bump the `version` in [package.json](package.json).
