@coventia/softphone 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/index.cjs CHANGED
@@ -13540,6 +13540,36 @@ var PACKED = "AC247 AD376 AE971 AF93 AG1 AI1 AL355 AM374 AO244 AR54 AS1 AT43 AU6
13540
13540
  var DIAL_CODES = new Map(
13541
13541
  PACKED.split(" ").map((token) => [token.slice(0, 2), Number(token.slice(2))])
13542
13542
  );
13543
+ var REGIONS_BY_CODE = (() => {
13544
+ const map = /* @__PURE__ */ new Map();
13545
+ for (const [region, code] of DIAL_CODES) map.set(code, [...map.get(code) ?? [], region]);
13546
+ return map;
13547
+ })();
13548
+ var MAIN_REGION = {
13549
+ 1: "US",
13550
+ 7: "RU",
13551
+ 39: "IT",
13552
+ 44: "GB",
13553
+ 47: "NO",
13554
+ 61: "AU",
13555
+ 212: "MA",
13556
+ 262: "RE",
13557
+ 290: "SH",
13558
+ 358: "FI",
13559
+ 590: "GP",
13560
+ 599: "CW"
13561
+ };
13562
+ function regionOfInternational(typed) {
13563
+ const text = String(typed ?? "").trim();
13564
+ if (!text.startsWith("+")) return null;
13565
+ const digits = text.slice(1).replace(/\D/g, "");
13566
+ for (let length = 1; length <= Math.min(3, digits.length); length++) {
13567
+ const code = Number(digits.slice(0, length));
13568
+ const regions = REGIONS_BY_CODE.get(code);
13569
+ if (regions) return MAIN_REGION[code] ?? regions[0] ?? null;
13570
+ }
13571
+ return null;
13572
+ }
13543
13573
  var DEFAULT_COUNTRY = "ES";
13544
13574
  var REGION_NAMES = (() => {
13545
13575
  try {
@@ -13591,12 +13621,16 @@ function CountryMark({ code }) {
13591
13621
  }
13592
13622
  function CountryPicker({
13593
13623
  value,
13594
- onChange
13624
+ onChange,
13625
+ fromNumber = false
13595
13626
  }) {
13596
13627
  const [open, setOpen] = React.useState(false);
13597
13628
  const [query, setQuery] = React.useState("");
13598
13629
  const rootRef = React.useRef(null);
13599
13630
  const inputRef = React.useRef(null);
13631
+ React.useEffect(() => {
13632
+ if (fromNumber) setOpen(false);
13633
+ }, [fromNumber]);
13600
13634
  const results = React.useMemo(() => {
13601
13635
  const q = query.trim().toLowerCase().replace(/^\+/, "");
13602
13636
  if (!q) return COUNTRIES;
@@ -13625,6 +13659,12 @@ function CountryPicker({
13625
13659
  else setQuery("");
13626
13660
  }, [open]);
13627
13661
  const dial = DIAL_CODES.get(value);
13662
+ if (fromNumber) {
13663
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-cp", ref: rootRef, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-cp-chip", "data-from-number": true, title: "El prefijo viene del n\xFAmero marcado", part: "country-chip", children: [
13664
+ /* @__PURE__ */ jsxRuntime.jsx(CountryMark, { code: value || "+" }),
13665
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-sr-only", children: dial ? `Prefijo del n\xFAmero marcado: ${countryName(value)} (+${dial})` : "Prefijo del n\xFAmero marcado" })
13666
+ ] }) });
13667
+ }
13628
13668
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-cp", ref: rootRef, children: [
13629
13669
  /* @__PURE__ */ jsxRuntime.jsxs(
13630
13670
  "button",
@@ -14191,18 +14231,384 @@ function playRingback() {
14191
14231
  }
14192
14232
  };
14193
14233
  }
14234
+
14235
+ // src/core/outbound-numbers.ts
14236
+ function isOutboundNumberRefused(value) {
14237
+ const refusal = value?.refusal;
14238
+ return value instanceof Error && !!refusal && (refusal.reason === "platform" || refusal.reason === "user") && typeof refusal.retryAfterSeconds === "number";
14239
+ }
14240
+ var stores = /* @__PURE__ */ new WeakMap();
14241
+ function outboundNumberStore(softphone) {
14242
+ const source = softphone.sources?.outboundNumbers;
14243
+ if (!source) return null;
14244
+ let store = stores.get(softphone);
14245
+ if (!store) {
14246
+ store = createOutboundNumberStore(source);
14247
+ stores.set(softphone, store);
14248
+ }
14249
+ return store;
14250
+ }
14251
+ function createOutboundNumberStore(source) {
14252
+ let settings = null;
14253
+ let inFlight = null;
14254
+ const listeners = /* @__PURE__ */ new Set();
14255
+ const apply = (next) => {
14256
+ settings = next;
14257
+ for (const l of listeners) l();
14258
+ };
14259
+ return {
14260
+ getSettings: () => settings,
14261
+ refresh() {
14262
+ inFlight ??= (async () => {
14263
+ if (!settings && source.cached) {
14264
+ const early = await source.cached().catch(() => null);
14265
+ if (early && !settings) apply(early);
14266
+ }
14267
+ try {
14268
+ apply(await source.get());
14269
+ } catch (error) {
14270
+ console.warn("[softphone] outbound numbers could not be loaded; keeping the previous ones", error);
14271
+ } finally {
14272
+ inFlight = null;
14273
+ }
14274
+ })();
14275
+ return inFlight;
14276
+ },
14277
+ async setMine(id) {
14278
+ apply(await source.setMine(id));
14279
+ },
14280
+ subscribe(listener) {
14281
+ listeners.add(listener);
14282
+ return () => listeners.delete(listener);
14283
+ }
14284
+ };
14285
+ }
14286
+ var noSubscribe = () => () => {
14287
+ };
14288
+ var noSettings = () => null;
14289
+ function useOutboundNumbers(store) {
14290
+ const settings = React.useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
14291
+ const [failed, setFailed] = React.useState(false);
14292
+ const load = React.useCallback(() => {
14293
+ if (!store) return () => {
14294
+ };
14295
+ let live = true;
14296
+ setFailed(false);
14297
+ void store.refresh().then(() => {
14298
+ if (live) setFailed(store.getSettings() === null);
14299
+ });
14300
+ return () => {
14301
+ live = false;
14302
+ };
14303
+ }, [store]);
14304
+ React.useEffect(load, [load]);
14305
+ return { settings, failed, retry: load };
14306
+ }
14307
+ function useKnownOutboundNumbers(store) {
14308
+ return React.useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
14309
+ }
14310
+ function useCountdown(until) {
14311
+ const [, setTick] = React.useState(0);
14312
+ React.useEffect(() => {
14313
+ if (!until) return void 0;
14314
+ const timer = setInterval(() => {
14315
+ setTick((n) => n + 1);
14316
+ if (secondsLeft(until) === 0) clearInterval(timer);
14317
+ }, 1e3);
14318
+ return () => clearInterval(timer);
14319
+ }, [until]);
14320
+ return secondsLeft(until);
14321
+ }
14322
+ function secondsLeft(until) {
14323
+ return until ? Math.max(0, Math.ceil((until - Date.now()) / 1e3)) : 0;
14324
+ }
14325
+ function numberName(n, format = formatIntl) {
14326
+ if (!n) return "N\xFAmero de la organizaci\xF3n";
14327
+ return n.label || format(`+${n.number}`);
14328
+ }
14329
+ function effectiveNumber(settings) {
14330
+ return settings?.numbers.find((n) => n.id === settings.effective);
14331
+ }
14332
+
14333
+ // src/ui/outbound/outbound-change.ts
14334
+ var KIND_TAG = {
14335
+ mobile: { tag: "M\xF3vil", hint: "No usar para atenci\xF3n al cliente ni para llamadas comerciales no solicitadas." },
14336
+ commercial: { tag: "Comercial (400)", hint: "Solo para llamadas comerciales. No recibe llamadas." }
14337
+ };
14338
+ function noticeText(situation, seconds, current) {
14339
+ switch (situation) {
14340
+ case "platform":
14341
+ return `El cambio de n\xFAmero de salida no est\xE1 disponible temporalmente. Vuelve a intentarlo en ${seconds} s. Hasta entonces, tus llamadas seguir\xE1n saliendo desde ${current}.`;
14342
+ case "user":
14343
+ return `Acabas de cambiar el n\xFAmero de salida. Podr\xE1s cambiarlo de nuevo en ${seconds} s. Hasta entonces, tus llamadas saldr\xE1n desde ${current}.`;
14344
+ case "wait":
14345
+ return `Podr\xE1s cambiar el n\xFAmero de salida en ${seconds} s.`;
14346
+ case "pending":
14347
+ return seconds > 0 ? `Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Podr\xE1s volver a elegirlo en ${seconds} s.` : "Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Vuelve a elegirlo para aplicarlo.";
14348
+ }
14349
+ }
14350
+ function useOutboundNumberChange(store, settings, current) {
14351
+ const [busy, setBusy] = React.useState(false);
14352
+ const [notice, setNotice] = React.useState(null);
14353
+ const [waitUntil, setWaitUntil] = React.useState(null);
14354
+ React.useEffect(() => {
14355
+ const s = settings?.changeAvailableInSeconds ?? 0;
14356
+ setWaitUntil(s > 0 ? Date.now() + s * 1e3 : null);
14357
+ }, [settings]);
14358
+ const refusedUntil = notice?.kind === "refused" ? notice.until : null;
14359
+ const blockedFor = useCountdown(refusedUntil ?? waitUntil);
14360
+ React.useEffect(() => {
14361
+ if (notice?.kind === "refused" && blockedFor === 0) setNotice(null);
14362
+ }, [notice, blockedFor]);
14363
+ const locked = busy || blockedFor > 0;
14364
+ const choose = async (id) => {
14365
+ if (!store || !settings || locked) return false;
14366
+ if (id === settings.mine) return true;
14367
+ setBusy(true);
14368
+ setNotice(null);
14369
+ try {
14370
+ await store.setMine(id);
14371
+ return true;
14372
+ } catch (error) {
14373
+ setNotice(
14374
+ isOutboundNumberRefused(error) ? { kind: "refused", reason: error.refusal.reason, until: Date.now() + error.refusal.retryAfterSeconds * 1e3 } : { kind: "error", text: errorText(error) }
14375
+ );
14376
+ return false;
14377
+ } finally {
14378
+ setBusy(false);
14379
+ }
14380
+ };
14381
+ return {
14382
+ busy,
14383
+ locked,
14384
+ choose,
14385
+ /** What to say about the choice right now, if anything. */
14386
+ message: settings ? currentMessage(notice, settings.state, blockedFor, current) : null,
14387
+ /** The message is an error (the change failed), not information (wait, pending). */
14388
+ isError: notice?.kind === "error"
14389
+ };
14390
+ }
14391
+ function currentMessage(notice, state, blockedFor, current) {
14392
+ if (notice?.kind === "refused") return blockedFor > 0 ? noticeText(notice.reason, blockedFor, current) : null;
14393
+ if (notice?.kind === "error") return notice.text;
14394
+ if (state === "pending" || state === "failed") return noticeText("pending", blockedFor, current);
14395
+ return blockedFor > 0 ? noticeText("wait", blockedFor, current) : null;
14396
+ }
14397
+ function errorText(error) {
14398
+ const message = error instanceof Error ? error.message : "";
14399
+ return message && !/HTTP \d+/.test(message) ? message : "No se ha podido cambiar el n\xFAmero de salida. Int\xE9ntalo de nuevo.";
14400
+ }
14401
+ var CALLOUT_MS = 6e3;
14402
+ function OutboundNumberPicker({ onBusyChange }) {
14403
+ const softphone = useSoftphone();
14404
+ const store = outboundNumberStore(softphone);
14405
+ const { settings } = useOutboundNumbers(store);
14406
+ const formatIntl2 = useIntlFormatter();
14407
+ const current = settings ? numberName(effectiveNumber(settings), formatIntl2) : "";
14408
+ const { busy, locked, choose: apply, message, isError } = useOutboundNumberChange(store, settings, current);
14409
+ const [open, setOpen] = React.useState(false);
14410
+ const [applying, setApplying] = React.useState(void 0);
14411
+ const [callout, setCallout] = React.useState(false);
14412
+ const rootRef = React.useRef(null);
14413
+ const chipRef = React.useRef(null);
14414
+ const keys = useListKeyboard(open);
14415
+ React.useEffect(() => onBusyChange?.(busy), [busy, onBusyChange]);
14416
+ React.useEffect(() => {
14417
+ if (!message) setCallout(false);
14418
+ }, [message]);
14419
+ React.useEffect(() => {
14420
+ if (!callout) return void 0;
14421
+ const timer = setTimeout(() => setCallout(false), CALLOUT_MS);
14422
+ return () => clearTimeout(timer);
14423
+ }, [callout]);
14424
+ const floating = open || callout;
14425
+ React.useEffect(() => {
14426
+ if (!floating) return;
14427
+ const close = () => {
14428
+ setOpen(false);
14429
+ setCallout(false);
14430
+ };
14431
+ const root = rootRef.current?.getRootNode() ?? document;
14432
+ const onDown = (e) => {
14433
+ if (!rootRef.current?.contains(e.target)) close();
14434
+ };
14435
+ const onKey = (e) => {
14436
+ if (e.key !== "Escape") return;
14437
+ if (rootRef.current?.contains(e.target)) chipRef.current?.focus();
14438
+ close();
14439
+ };
14440
+ root.addEventListener("pointerdown", onDown);
14441
+ document.addEventListener("keydown", onKey);
14442
+ return () => {
14443
+ root.removeEventListener("pointerdown", onDown);
14444
+ document.removeEventListener("keydown", onKey);
14445
+ };
14446
+ }, [floating]);
14447
+ if (!store || !settings || settings.numbers.length < 2) return null;
14448
+ const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
14449
+ const applyingNumber = applying === null ? orgDefault : settings.numbers.find((n) => n.id === applying);
14450
+ const shown = applying === void 0 ? current : numberName(applyingNumber, formatIntl2);
14451
+ const choose = async (id) => {
14452
+ if (locked) return;
14453
+ setOpen(false);
14454
+ chipRef.current?.focus();
14455
+ if (id === settings.mine) return;
14456
+ setCallout(false);
14457
+ setApplying(id);
14458
+ const applied = await apply(id);
14459
+ setApplying(void 0);
14460
+ if (!applied) setCallout(true);
14461
+ };
14462
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-on", ref: rootRef, children: [
14463
+ /* @__PURE__ */ jsxRuntime.jsxs(
14464
+ "button",
14465
+ {
14466
+ ref: chipRef,
14467
+ type: "button",
14468
+ className: "cv-on-chip",
14469
+ onClick: () => {
14470
+ setCallout(false);
14471
+ setOpen((o) => !o);
14472
+ },
14473
+ "aria-haspopup": "listbox",
14474
+ "aria-expanded": open,
14475
+ "aria-label": `Llamar desde: ${shown}${busy ? " (aplicando)" : ""}. Cambiar n\xFAmero de salida`,
14476
+ "aria-busy": busy,
14477
+ part: "outbound-number-chip",
14478
+ children: [
14479
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-label", children: "Llamar desde:" }),
14480
+ busy ? /* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { size: 13, className: "cv-on-icon cv-on-spin", "aria-hidden": true }) : null,
14481
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-current", children: shown }),
14482
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, { size: 13, className: "cv-cp-caret", "data-open": open, "aria-hidden": true })
14483
+ ]
14484
+ }
14485
+ ),
14486
+ open ? /* @__PURE__ */ jsxRuntime.jsxs(
14487
+ "div",
14488
+ {
14489
+ className: "cv-cp-pop cv-on-pop",
14490
+ role: "dialog",
14491
+ "aria-label": "Llamar desde",
14492
+ part: "outbound-number-list",
14493
+ onBlur: (e) => {
14494
+ if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false);
14495
+ },
14496
+ children: [
14497
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-on-title", children: "Llamar desde" }),
14498
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-cp-list", role: "listbox", "aria-disabled": locked, ref: keys.listRef, onKeyDown: keys.onKeyDown, children: [
14499
+ /* @__PURE__ */ jsxRuntime.jsx(
14500
+ OptionRow,
14501
+ {
14502
+ selected: settings.mine === null,
14503
+ disabled: locked,
14504
+ title: "Por defecto de la organizaci\xF3n",
14505
+ sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
14506
+ onSelect: () => void choose(null)
14507
+ }
14508
+ ),
14509
+ settings.numbers.map((n) => /* @__PURE__ */ jsxRuntime.jsx(
14510
+ OptionRow,
14511
+ {
14512
+ selected: settings.mine === n.id,
14513
+ disabled: locked,
14514
+ title: numberName(n, formatIntl2),
14515
+ sub: n.label ? formatIntl2(`+${n.number}`) : null,
14516
+ tag: KIND_TAG[n.kind],
14517
+ onSelect: () => void choose(n.id)
14518
+ },
14519
+ n.id
14520
+ ))
14521
+ ] }),
14522
+ message ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-on-notice", role: "status", "data-kind": isError ? "error" : "info", children: message }) : null
14523
+ ]
14524
+ }
14525
+ ) : null,
14526
+ callout && !open && message ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-on-callout", role: "status", "data-kind": isError ? "error" : "info", part: "outbound-number-notice", children: message }) : null
14527
+ ] });
14528
+ }
14529
+ function OptionRow({
14530
+ selected,
14531
+ disabled,
14532
+ title,
14533
+ sub,
14534
+ tag,
14535
+ onSelect
14536
+ }) {
14537
+ return /* @__PURE__ */ jsxRuntime.jsxs(
14538
+ "button",
14539
+ {
14540
+ type: "button",
14541
+ role: "option",
14542
+ "aria-selected": selected,
14543
+ "aria-disabled": disabled,
14544
+ className: "cv-cp-item cv-on-item",
14545
+ tabIndex: -1,
14546
+ onClick: onSelect,
14547
+ children: [
14548
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-on-text", children: [
14549
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-on-line", children: [
14550
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-cp-name", children: title }),
14551
+ tag ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-tag", children: tag.tag }) : null
14552
+ ] }),
14553
+ sub ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-sub", children: sub }) : null,
14554
+ tag ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-hint", children: tag.hint }) : null
14555
+ ] }),
14556
+ /* @__PURE__ */ jsxRuntime.jsx(Check, { size: 14, className: "cv-cp-check", "data-on": selected, "aria-hidden": true })
14557
+ ]
14558
+ }
14559
+ );
14560
+ }
14561
+ var optionsOf = (list) => list ? [...list.querySelectorAll('[role="option"]')] : [];
14562
+ var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
14563
+ var TYPEAHEAD_MS = 700;
14564
+ function useListKeyboard(open) {
14565
+ const listRef = React.useRef(null);
14566
+ const typed = React.useRef({ text: "", at: 0 });
14567
+ React.useEffect(() => {
14568
+ if (!open) return;
14569
+ const options = optionsOf(listRef.current);
14570
+ (options.find((o) => o.getAttribute("aria-selected") === "true") ?? options[0])?.focus();
14571
+ }, [open]);
14572
+ const onKeyDown = (e) => {
14573
+ const options = optionsOf(listRef.current);
14574
+ const at = options.indexOf(e.target);
14575
+ const move = {
14576
+ ArrowDown: Math.min(at + 1, options.length - 1),
14577
+ ArrowUp: Math.max(at - 1, 0),
14578
+ Home: 0,
14579
+ End: options.length - 1
14580
+ };
14581
+ let next = move[e.key];
14582
+ if (next === void 0 && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
14583
+ const now = Date.now();
14584
+ const text = now - typed.current.at < TYPEAHEAD_MS ? typed.current.text + e.key : e.key;
14585
+ typed.current = { text, at: now };
14586
+ const wanted = fold(text);
14587
+ const match = options.findIndex((o) => fold(o.querySelector(".cv-cp-name")?.textContent ?? "").startsWith(wanted));
14588
+ next = match >= 0 ? match : void 0;
14589
+ }
14590
+ if (next === void 0) return;
14591
+ e.preventDefault();
14592
+ options[next]?.focus();
14593
+ };
14594
+ return { listRef, onKeyDown };
14595
+ }
14194
14596
  var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
14597
+ var DISPLAY_SIZES = ["1.375rem", "1.25rem", "1.125rem", "1rem"];
14195
14598
  function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
14196
14599
  const softphone = useSoftphone();
14197
14600
  const state = useSoftphoneState();
14198
14601
  const [value, setValue] = React.useState("");
14199
14602
  const [country2, setCountry] = React.useState(DEFAULT_COUNTRY);
14603
+ const [changingNumber, setChangingNumber] = React.useState(false);
14200
14604
  const format = useNumberFormatter();
14201
14605
  const inputRef = React.useRef(null);
14202
14606
  const caretRef = React.useRef(null);
14203
14607
  const display = format(country2, value);
14204
14608
  const target = toE164(country2, value);
14205
- const canCall = state === "online" && target.length > 1;
14609
+ const canCall = state === "online" && target.length > 1 && !changingNumber;
14610
+ const typedInternational = value.trim().startsWith("+");
14611
+ const numberRegion = regionOfInternational(value);
14206
14612
  React.useLayoutEffect(() => {
14207
14613
  const caret = caretRef.current;
14208
14614
  if (caret === null || !inputRef.current) return;
@@ -14210,6 +14616,14 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
14210
14616
  const pos = caretAfterDigits(display, caret);
14211
14617
  inputRef.current.setSelectionRange(pos, pos);
14212
14618
  }, [display]);
14619
+ React.useLayoutEffect(() => {
14620
+ const input = inputRef.current;
14621
+ if (!input) return;
14622
+ for (const size of DISPLAY_SIZES) {
14623
+ input.style.fontSize = size;
14624
+ if (input.scrollWidth <= input.clientWidth) break;
14625
+ }
14626
+ }, [display]);
14213
14627
  const append = (key) => {
14214
14628
  onIntent?.();
14215
14629
  setValue((v) => v + key);
@@ -14225,9 +14639,16 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
14225
14639
  void softphone.call(target);
14226
14640
  };
14227
14641
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
14228
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-dial-prefix", children: /* @__PURE__ */ jsxRuntime.jsx(CountryPicker, { value: country2, onChange: setCountry }) }),
14642
+ /* @__PURE__ */ jsxRuntime.jsx(OutboundNumberPicker, { onBusyChange: setChangingNumber }),
14229
14643
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-dial-head", children: [
14230
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-dial-side", "aria-hidden": true }),
14644
+ /* @__PURE__ */ jsxRuntime.jsx(
14645
+ CountryPicker,
14646
+ {
14647
+ value: numberRegion ?? country2,
14648
+ onChange: setCountry,
14649
+ fromNumber: typedInternational
14650
+ }
14651
+ ),
14231
14652
  /* @__PURE__ */ jsxRuntime.jsx(
14232
14653
  "input",
14233
14654
  {
@@ -14251,6 +14672,8 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
14251
14672
  type: "button",
14252
14673
  className: "cv-dial-side",
14253
14674
  "data-hidden": value.length === 0,
14675
+ tabIndex: value.length === 0 ? -1 : void 0,
14676
+ "aria-hidden": value.length === 0 || void 0,
14254
14677
  onClick: backspace,
14255
14678
  "aria-label": "Borrar",
14256
14679
  children: /* @__PURE__ */ jsxRuntime.jsx(Delete, { size: 19 })
@@ -14675,6 +15098,9 @@ function CallScreen({
14675
15098
  }) {
14676
15099
  const view = useCall(call);
14677
15100
  const softphone = useSoftphone();
15101
+ const outbound = useOutboundNumbers(call.direction === "outbound" ? outboundNumberStore(softphone) : null).settings;
15102
+ const formatIntl2 = useIntlFormatter();
15103
+ const fromName = outbound && outbound.numbers.length > 1 ? numberName(effectiveNumber(outbound), formatIntl2) : null;
14678
15104
  const [panel, setPanel] = React.useState(null);
14679
15105
  const showTransfer = panel === "transfer";
14680
15106
  const showKeypad = panel === "keypad";
@@ -14885,6 +15311,13 @@ function CallScreen({
14885
15311
  ] }),
14886
15312
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-name", children: name }),
14887
15313
  showNumber ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-number", children: number }) : null,
15314
+ fromName ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-from", children: [
15315
+ /* @__PURE__ */ jsxRuntime.jsx(PhoneOutgoing, { size: 11, "aria-hidden": true }),
15316
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
15317
+ "Desde ",
15318
+ fromName
15319
+ ] })
15320
+ ] }) : null,
14888
15321
  s === "active" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
14889
15322
  /* @__PURE__ */ jsxRuntime.jsx("b", { children: mm }),
14890
15323
  /* @__PURE__ */ jsxRuntime.jsx("i", { children: ":" }),
@@ -15300,12 +15733,23 @@ function HandlerChain({ handlers }) {
15300
15733
  ] }, `${h.extension}-${i}`);
15301
15734
  }) });
15302
15735
  }
15736
+ function outboundFrom(entry, known, formatIntl2) {
15737
+ if (entry.direction !== "outbound" || !entry.from) return null;
15738
+ const number = formatIntl2(`+${entry.from}`);
15739
+ const label = known?.numbers.find((n) => n.number === entry.from)?.label;
15740
+ if (!label) return number;
15741
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
15742
+ label,
15743
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-detail-row-sub", children: number })
15744
+ ] });
15745
+ }
15303
15746
  function HistoryDetail({
15304
15747
  entry,
15305
15748
  onBack
15306
15749
  }) {
15307
15750
  const softphone = useSoftphone();
15308
15751
  const formatIntl2 = useIntlFormatter();
15752
+ const knownNumbers = useKnownOutboundNumbers(outboundNumberStore(softphone));
15309
15753
  const [detail, setDetail] = React.useState(null);
15310
15754
  const [loadingDetail, setLoadingDetail] = React.useState(false);
15311
15755
  React.useEffect(() => {
@@ -15321,6 +15765,7 @@ function HistoryDetail({
15321
15765
  }, [entry]);
15322
15766
  const formatted = formatIntl2(entry.number);
15323
15767
  const name = entry.name ?? formatted;
15768
+ const from = outboundFrom(entry, knownNumbers, formatIntl2);
15324
15769
  const handledBy = detail?.handledBy ?? entry.handledBy;
15325
15770
  const recordingUrls = detail?.recordingUrls ?? [];
15326
15771
  const loadingRecording = entry.recorded && loadingDetail && recordingUrls.length === 0;
@@ -15357,7 +15802,8 @@ function HistoryDetail({
15357
15802
  entry.durationSec > 0 ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Timer, eyebrow: "Duraci\xF3n", text: formatDuration(entry.durationSec) }) : null,
15358
15803
  /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Phone, eyebrow: "N\xFAmero", text: formatted }),
15359
15804
  handledBy.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: User, eyebrow: handlerLabel, text: /* @__PURE__ */ jsxRuntime.jsx(HandlerChain, { handlers: handledBy }) }) : null,
15360
- entry.direction === "inbound" && entry.did ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null
15805
+ entry.direction === "inbound" && entry.did ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null,
15806
+ from ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: PhoneOutgoing, eyebrow: "Llamada desde", text: from }) : null
15361
15807
  ] })
15362
15808
  ] }),
15363
15809
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-detail-foot", children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -15552,14 +15998,14 @@ function HistoryTab({ source }) {
15552
15998
  }
15553
15999
 
15554
16000
  // src/ui/audio/ringtone-store.ts
15555
- var stores = /* @__PURE__ */ new WeakMap();
16001
+ var stores2 = /* @__PURE__ */ new WeakMap();
15556
16002
  function ringtoneStore(softphone) {
15557
16003
  const source = softphone.sources?.ringtones;
15558
16004
  if (!source) return null;
15559
- let store = stores.get(softphone);
16005
+ let store = stores2.get(softphone);
15560
16006
  if (!store) {
15561
16007
  store = createRingtoneStore(source);
15562
- stores.set(softphone, store);
16008
+ stores2.set(softphone, store);
15563
16009
  }
15564
16010
  return store;
15565
16011
  }
@@ -15685,17 +16131,19 @@ function ChoiceList({
15685
16131
  choices,
15686
16132
  value,
15687
16133
  onSelect,
15688
- onReselect
16134
+ onReselect,
16135
+ disabled = false
15689
16136
  }) {
15690
16137
  const name = React.useId();
15691
16138
  const pointer = React.useRef(false);
15692
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, children: choices.map((c) => {
16139
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, "aria-disabled": disabled || void 0, children: choices.map((c) => {
15693
16140
  const checked = c.value === value;
15694
16141
  return /* @__PURE__ */ jsxRuntime.jsxs(
15695
16142
  "label",
15696
16143
  {
15697
16144
  className: "cv-choice",
15698
16145
  "data-checked": checked || void 0,
16146
+ "data-disabled": disabled || void 0,
15699
16147
  onPointerDown: () => {
15700
16148
  pointer.current = true;
15701
16149
  },
@@ -15711,19 +16159,23 @@ function ChoiceList({
15711
16159
  onChange: () => {
15712
16160
  const via = pointer.current ? "pointer" : "keyboard";
15713
16161
  pointer.current = false;
15714
- onSelect(c.value, via);
16162
+ if (!disabled) onSelect(c.value, via);
15715
16163
  },
15716
16164
  onClick: () => {
15717
16165
  if (c.value === value) {
15718
16166
  pointer.current = false;
15719
- onReselect?.(c.value);
16167
+ if (!disabled) onReselect?.(c.value);
15720
16168
  }
15721
16169
  }
15722
16170
  }
15723
16171
  ),
15724
16172
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-text", children: [
15725
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-label", children: c.label }),
15726
- c.sub ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-sub", children: c.sub }) : null
16173
+ c.tag ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-line", children: [
16174
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-label", children: c.label }),
16175
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-tag", children: c.tag })
16176
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-label", children: c.label }),
16177
+ c.sub ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-sub", children: c.sub }) : null,
16178
+ c.hint ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-hint", children: c.hint }) : null
15727
16179
  ] }),
15728
16180
  c.trailing,
15729
16181
  /* @__PURE__ */ jsxRuntime.jsx(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
@@ -15885,11 +16337,11 @@ function previewRingtone(ringtone, options = {}) {
15885
16337
  }
15886
16338
 
15887
16339
  // src/ui/views/settings/ringtone-state.ts
15888
- var noSubscribe = () => () => {
16340
+ var noSubscribe2 = () => () => {
15889
16341
  };
15890
- var noSettings = () => null;
16342
+ var noSettings2 = () => null;
15891
16343
  function useRingtoneSettings(store) {
15892
- const settings = React.useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
16344
+ const settings = React.useSyncExternalStore(store?.subscribe ?? noSubscribe2, store?.getSettings ?? noSettings2);
15893
16345
  const [failed, setFailed] = React.useState(false);
15894
16346
  const load = React.useCallback(() => {
15895
16347
  if (!store) return () => {
@@ -16090,11 +16542,109 @@ function useSerialSave(store, handlers) {
16090
16542
  });
16091
16543
  return save;
16092
16544
  }
16545
+ var ORG_DEFAULT2 = "";
16546
+ var KEYBOARD_SETTLE_MS2 = 800;
16547
+ function OutboundNumberPage({
16548
+ store,
16549
+ settings,
16550
+ onBack
16551
+ }) {
16552
+ const formatIntl2 = useIntlFormatter();
16553
+ const current = numberName(effectiveNumber(settings), formatIntl2);
16554
+ const change = useOutboundNumberChange(store, settings, current);
16555
+ const [pending, setPending] = React.useState(void 0);
16556
+ const settle = React.useRef(null);
16557
+ const choose = React.useRef(change.choose);
16558
+ choose.current = change.choose;
16559
+ React.useEffect(
16560
+ () => () => {
16561
+ if (settle.current) clearTimeout(settle.current);
16562
+ },
16563
+ []
16564
+ );
16565
+ const notice = React.useRef(null);
16566
+ const apply = async (value) => {
16567
+ setPending(value);
16568
+ const applied = await choose.current(value || null);
16569
+ setPending(void 0);
16570
+ if (!applied) requestAnimationFrame(() => notice.current?.scrollIntoView?.({ block: "nearest" }));
16571
+ };
16572
+ const onSelect = (value, via) => {
16573
+ if (settle.current) clearTimeout(settle.current);
16574
+ settle.current = null;
16575
+ if (via === "pointer") {
16576
+ void apply(value);
16577
+ return;
16578
+ }
16579
+ setPending(value);
16580
+ settle.current = setTimeout(() => {
16581
+ settle.current = null;
16582
+ void apply(value);
16583
+ }, KEYBOARD_SETTLE_MS2);
16584
+ };
16585
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16586
+ SettingsPage,
16587
+ {
16588
+ title: "N\xFAmero de salida",
16589
+ intro: "El n\xFAmero que ve la persona a la que llamas. Es el mismo en todos tus dispositivos y tambi\xE9n puedes cambiarlo desde el teclado.",
16590
+ onBack,
16591
+ children: [
16592
+ change.message ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-notice", role: "status", "data-kind": change.isError ? "error" : "info", ref: notice, children: change.message }) : null,
16593
+ /* @__PURE__ */ jsxRuntime.jsx(
16594
+ ChoiceList,
16595
+ {
16596
+ label: "N\xFAmero de salida",
16597
+ choices: choicesFor2(settings, formatIntl2, change.busy ? pending : void 0),
16598
+ value: pending ?? currentValue2(settings),
16599
+ onSelect,
16600
+ disabled: change.locked
16601
+ }
16602
+ )
16603
+ ]
16604
+ }
16605
+ );
16606
+ }
16607
+ function currentValue2(settings) {
16608
+ return settings.mine && settings.numbers.some((n) => n.id === settings.mine) ? settings.mine : ORG_DEFAULT2;
16609
+ }
16610
+ function choicesFor2(settings, formatIntl2, applying) {
16611
+ const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
16612
+ const spinner = (value) => applying === value ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-applying", children: [
16613
+ /* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { size: 15, className: "cv-choice-applying-icon", "aria-hidden": true }),
16614
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-sr-only", children: "Aplicando" })
16615
+ ] }) : void 0;
16616
+ return [
16617
+ {
16618
+ value: ORG_DEFAULT2,
16619
+ label: "Por defecto de la organizaci\xF3n",
16620
+ sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
16621
+ trailing: spinner(ORG_DEFAULT2)
16622
+ },
16623
+ ...settings.numbers.map((n) => {
16624
+ const kind = KIND_TAG[n.kind];
16625
+ return {
16626
+ value: n.id,
16627
+ label: numberName(n, formatIntl2),
16628
+ ...n.label ? { sub: formatIntl2(`+${n.number}`) } : {},
16629
+ ...kind ? { tag: kind.tag, hint: kind.hint } : {},
16630
+ trailing: spinner(n.id)
16631
+ };
16632
+ })
16633
+ ];
16634
+ }
16635
+ function outboundSummary(settings, formatIntl2) {
16636
+ const effective = effectiveNumber(settings);
16637
+ const name = numberName(effective, formatIntl2);
16638
+ return settings.mine || !effective ? name : `${name} \xB7 organizaci\xF3n`;
16639
+ }
16093
16640
  function SettingsTab() {
16094
16641
  const softphone = useSoftphone();
16095
16642
  const devices = useDevices();
16096
16643
  const store = ringtoneStore(softphone);
16097
16644
  const ringtone = useRingtoneSettings(store);
16645
+ const outStore = outboundNumberStore(softphone);
16646
+ const outbound = useOutboundNumbers(outStore).settings;
16647
+ const formatIntl2 = useIntlFormatter();
16098
16648
  const [page, setPage] = React.useState(null);
16099
16649
  const list = React.useRef(null);
16100
16650
  const rows = React.useRef({});
@@ -16129,6 +16679,16 @@ function SettingsTab() {
16129
16679
  rowRef: rowRef("tone")
16130
16680
  }
16131
16681
  ) }) : null,
16682
+ outStore && outbound && outbound.numbers.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(SettingsSection, { title: "Llamadas salientes", children: /* @__PURE__ */ jsxRuntime.jsx(
16683
+ SettingsNavRow,
16684
+ {
16685
+ icon: PhoneOutgoing,
16686
+ title: "N\xFAmero de salida",
16687
+ value: outboundSummary(outbound, formatIntl2),
16688
+ onOpen: open("outbound"),
16689
+ rowRef: rowRef("outbound")
16690
+ }
16691
+ ) }) : null,
16132
16692
  /* @__PURE__ */ jsxRuntime.jsxs(SettingsSection, { title: "Dispositivos", children: [
16133
16693
  /* @__PURE__ */ jsxRuntime.jsx(
16134
16694
  SettingsNavRow,
@@ -16182,7 +16742,7 @@ function SettingsTab() {
16182
16742
  onRetry: ringtone.retry,
16183
16743
  onBack: back
16184
16744
  }
16185
- ) : page !== null && page !== "tone" ? /* @__PURE__ */ jsxRuntime.jsx(DevicePage, { kind: page, onBack: back }) : null
16745
+ ) : page === "outbound" && outStore && outbound ? /* @__PURE__ */ jsxRuntime.jsx(OutboundNumberPage, { store: outStore, settings: outbound, onBack: back }) : page !== null && page !== "tone" && page !== "outbound" ? /* @__PURE__ */ jsxRuntime.jsx(DevicePage, { kind: page, onBack: back }) : null
16186
16746
  ] });
16187
16747
  }
16188
16748