@marsaude/devtools-shell 0.1.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.
@@ -0,0 +1,3045 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, signal, Injectable, computed, TemplateRef, HostListener, ChangeDetectionStrategy, Component, ChangeDetectorRef, makeEnvironmentProviders, APP_BOOTSTRAP_LISTENER, EnvironmentInjector, createEnvironmentInjector, runInInjectionContext, ApplicationRef, createComponent } from '@angular/core';
3
+ import { HttpParams, HttpClient, HttpHeaders, HttpContext, HttpContextToken, provideHttpClient, withInterceptors } from '@angular/common/http';
4
+ import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
5
+ import { of, forkJoin, from, tap, catchError, switchMap as switchMap$1, throwError, EMPTY, Subject } from 'rxjs';
6
+ import { switchMap, map, debounceTime } from 'rxjs/operators';
7
+ import * as i1 from '@angular/forms';
8
+ import { FormsModule } from '@angular/forms';
9
+
10
+ /**
11
+ * Multi-provider token. Each `{ provide: DEVTOOLS_ACTION, useValue, multi: true }`
12
+ * adds one pluggable panel to the shell. This is the extension point: the shell
13
+ * collects every registered action and renders it — it has no built-in actions.
14
+ */
15
+ const DEVTOOLS_ACTION = new InjectionToken('DEVTOOLS_ACTION');
16
+ /** Single-value config token. Falls back to an empty config. */
17
+ const DEVTOOLS_SHELL_CONFIG = new InjectionToken('DEVTOOLS_SHELL_CONFIG', { providedIn: 'root', factory: () => ({}) });
18
+
19
+ /** FAB edge length in px — matches `.dts-fab` and the prototype (S = 58). */
20
+ const FAB_SIZE = 58;
21
+ /**
22
+ * Owns the FAB position + side and persists them. The math is ported verbatim
23
+ * from the `DevTools FAB` prototype (default corner, clamp bounds, edge snap).
24
+ * Only position/side are stored — no domain/session data.
25
+ */
26
+ class DevtoolsPositionService {
27
+ constructor() {
28
+ this.config = inject(DEVTOOLS_SHELL_CONFIG);
29
+ this.key = this.config.storageKey ?? 'marsaude.devtools-shell.pos';
30
+ /** FAB top-left in viewport coords. Null until first measured/loaded. */
31
+ this.position = signal(this.load(), ...(ngDevMode ? [{ debugName: "position" }] : []));
32
+ this.side = signal(this.loadSide(), ...(ngDevMode ? [{ debugName: "side" }] : []));
33
+ }
34
+ /** Live update during drag (not persisted). */
35
+ move(p) {
36
+ this.position.set(p);
37
+ }
38
+ /** Commit + persist the final position and side. */
39
+ commit(p, side) {
40
+ this.position.set(p);
41
+ this.side.set(side);
42
+ this.persist();
43
+ }
44
+ /** Re-clamp against the current viewport. Call on resize. */
45
+ reclamp() {
46
+ this.position.set(this.clamp(this.position()));
47
+ }
48
+ /** Prototype clamp: x ∈ [6, W-S-6], y ∈ [70, H-S-6]. */
49
+ clamp(p) {
50
+ if (typeof window === 'undefined')
51
+ return p;
52
+ const S = FAB_SIZE;
53
+ const W = window.innerWidth;
54
+ const H = window.innerHeight;
55
+ return {
56
+ x: Math.max(6, Math.min(W - S - 6, p.x)),
57
+ y: Math.max(70, Math.min(H - S - 6, p.y)),
58
+ };
59
+ }
60
+ persist() {
61
+ try {
62
+ localStorage?.setItem(this.key, JSON.stringify({ ...this.position(), side: this.side() }));
63
+ }
64
+ catch {
65
+ /* storage unavailable — non-fatal */
66
+ }
67
+ }
68
+ /** Prototype default corner: { x: W-74, y: H-150 }. */
69
+ load() {
70
+ const fallback = this.defaultPos();
71
+ if (typeof localStorage === 'undefined')
72
+ return fallback;
73
+ try {
74
+ const raw = localStorage.getItem(this.key);
75
+ if (!raw)
76
+ return fallback;
77
+ const p = JSON.parse(raw);
78
+ if (typeof p?.x === 'number' && typeof p?.y === 'number') {
79
+ return this.clamp({ x: p.x, y: p.y });
80
+ }
81
+ }
82
+ catch {
83
+ /* corrupt — fall through */
84
+ }
85
+ return fallback;
86
+ }
87
+ loadSide() {
88
+ if (typeof localStorage === 'undefined')
89
+ return 'right';
90
+ try {
91
+ const raw = localStorage.getItem(this.key);
92
+ const side = raw ? JSON.parse(raw).side : null;
93
+ return side === 'left' ? 'left' : 'right';
94
+ }
95
+ catch {
96
+ return 'right';
97
+ }
98
+ }
99
+ defaultPos() {
100
+ if (typeof window === 'undefined')
101
+ return { x: 10, y: 120 };
102
+ return { x: window.innerWidth - 74, y: window.innerHeight - 150 };
103
+ }
104
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsPositionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
105
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsPositionService, providedIn: 'root' }); }
106
+ }
107
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsPositionService, decorators: [{
108
+ type: Injectable,
109
+ args: [{ providedIn: 'root' }]
110
+ }] });
111
+
112
+ /**
113
+ * Tiny toast surface owned by the shell. Panels (or any consumer) can inject it
114
+ * to flash a short message inside the shell's own layer — no Material/snackbar
115
+ * dependency required.
116
+ */
117
+ class DevtoolsToastService {
118
+ constructor() {
119
+ /** Current message, or null when nothing is showing. */
120
+ this.message = signal(null, ...(ngDevMode ? [{ debugName: "message" }] : []));
121
+ this.timer = null;
122
+ }
123
+ show(message, durationMs = 2400) {
124
+ this.message.set(message);
125
+ if (this.timer)
126
+ clearTimeout(this.timer);
127
+ this.timer = setTimeout(() => this.message.set(null), durationMs);
128
+ }
129
+ clear() {
130
+ if (this.timer)
131
+ clearTimeout(this.timer);
132
+ this.message.set(null);
133
+ }
134
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
135
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsToastService, providedIn: 'root' }); }
136
+ }
137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsToastService, decorators: [{
138
+ type: Injectable,
139
+ args: [{ providedIn: 'root' }]
140
+ }] });
141
+
142
+ const PROJECTED = '__projected__';
143
+ const COLLAPSE_DELAY_MS = 1800;
144
+ /**
145
+ * The invólucro: a draggable FAB that opens an agnostic container.
146
+ *
147
+ * Drag, clamp/snap, the radial speed-dial geometry, the collapse-to-edge grip,
148
+ * the drawer/sheet panel and the toast are ported verbatim from the `DevTools
149
+ * FAB` prototype (Pointer Events, the exact math and the same CSS/keyframes) —
150
+ * kept entirely domain-free. Content arrives via {@link DEVTOOLS_ACTION}
151
+ * (primary) or `<ng-content>` (secondary). Auto-mounted by `provideDevtools()`.
152
+ */
153
+ class DevtoolsShellComponent {
154
+ constructor() {
155
+ this.pos = inject(DevtoolsPositionService);
156
+ this.config = inject(DEVTOOLS_SHELL_CONFIG);
157
+ this.toast = inject(DevtoolsToastService);
158
+ this.registered = inject(DEVTOOLS_ACTION, { optional: true }) ?? [];
159
+ this.actions = signal([...this.registered].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), ...(ngDevMode ? [{ debugName: "actions" }] : []));
160
+ this.open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : [])); // speed-dial open
161
+ this.panelId = signal(null, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
162
+ this.collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
163
+ this.dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : []));
164
+ this.winW = signal(typeof window === 'undefined' ? 1024 : window.innerWidth, ...(ngDevMode ? [{ debugName: "winW" }] : []));
165
+ this.winH = signal(typeof window === 'undefined' ? 768 : window.innerHeight, ...(ngDevMode ? [{ debugName: "winH" }] : []));
166
+ this.hasDial = computed(() => this.actions().length > 1, ...(ngDevMode ? [{ debugName: "hasDial" }] : []));
167
+ this.activeAction = computed(() => this.actions().find((a) => a.id === this.panelId()) ?? null, ...(ngDevMode ? [{ debugName: "activeAction" }] : []));
168
+ this.isCollapsed = computed(() => this.collapsed() && !this.open() && !this.panelId() && !this.dragging(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
169
+ /** FAB style — collapsed grip vs expanded button (prototype values). */
170
+ this.fabStyle = computed(() => {
171
+ const S = FAB_SIZE;
172
+ const W = this.winW();
173
+ const f = this.pos.position();
174
+ const side = this.pos.side();
175
+ const isOpen = this.open() || !!this.panelId();
176
+ if (this.isCollapsed()) {
177
+ const cx = side === 'left' ? 0 : W - 15;
178
+ return {
179
+ position: 'fixed',
180
+ left: cx + 'px',
181
+ top: f.y + 10 + 'px',
182
+ width: '15px',
183
+ height: '60px',
184
+ zIndex: '40',
185
+ border: 'none',
186
+ background: '#14151b',
187
+ color: '#fff',
188
+ borderRadius: side === 'left' ? '0 12px 12px 0' : '12px 0 0 12px',
189
+ display: 'flex',
190
+ alignItems: 'center',
191
+ justifyContent: 'center',
192
+ padding: '0',
193
+ boxShadow: '0 4px 16px rgba(0,0,0,.35)',
194
+ transition: 'width .2s ease, border-radius .2s ease',
195
+ touchAction: 'none',
196
+ cursor: 'pointer',
197
+ };
198
+ }
199
+ return {
200
+ position: 'fixed',
201
+ left: f.x + 'px',
202
+ top: f.y + 'px',
203
+ width: S + 'px',
204
+ height: S + 'px',
205
+ zIndex: '40',
206
+ border: 'none',
207
+ borderRadius: '18px',
208
+ display: 'flex',
209
+ alignItems: 'center',
210
+ justifyContent: 'center',
211
+ gap: '0',
212
+ background: isOpen ? '#23242c' : '#14151b',
213
+ color: '#ffc454',
214
+ boxShadow: '0 8px 24px rgba(0,0,0,.4)',
215
+ transform: isOpen ? 'rotate(45deg)' : 'rotate(0deg)',
216
+ transition: this.dragging()
217
+ ? 'none'
218
+ : 'transform .25s ease, background .2s ease, left .25s cubic-bezier(.2,.8,.2,1)',
219
+ touchAction: 'none',
220
+ cursor: this.dragging() ? 'grabbing' : 'grab',
221
+ };
222
+ }, ...(ngDevMode ? [{ debugName: "fabStyle" }] : []));
223
+ /** Radial speed-dial geometry — ported verbatim (R=268, 5°→87°). */
224
+ this.dialItems = computed(() => {
225
+ const S = FAB_SIZE;
226
+ const W = this.winW();
227
+ const H = this.winH();
228
+ const f = this.pos.position();
229
+ const isOpen = this.open();
230
+ const cx = f.x + S / 2;
231
+ const cy = f.y + S / 2;
232
+ const hx = cx < W / 2 ? 1 : -1;
233
+ const vy = cy < H * 0.42 ? 1 : -1;
234
+ const acts = this.actions();
235
+ const N = acts.length;
236
+ const R = 268;
237
+ const a0 = (5 * Math.PI) / 180;
238
+ const a1 = (87 * Math.PI) / 180;
239
+ return acts.map((action, i) => {
240
+ const t = N === 1 ? 0.5 : i / (N - 1);
241
+ const ang = a0 + (a1 - a0) * t;
242
+ const dx = hx * Math.cos(ang) * R;
243
+ const dy = vy * Math.sin(ang) * R;
244
+ return {
245
+ action,
246
+ style: {
247
+ position: 'fixed',
248
+ zIndex: '41',
249
+ width: '58px',
250
+ height: '50px',
251
+ left: cx - 29 + 'px',
252
+ top: cy - 25 + 'px',
253
+ display: 'flex',
254
+ flexDirection: 'column',
255
+ alignItems: 'center',
256
+ justifyContent: 'center',
257
+ gap: '3px',
258
+ borderRadius: '15px',
259
+ border: '1px solid #2a2c38',
260
+ background: '#1b1d25',
261
+ color: '#eceef3',
262
+ boxShadow: '0 8px 20px rgba(0,0,0,.32)',
263
+ padding: '4px',
264
+ cursor: 'pointer',
265
+ transform: isOpen
266
+ ? `translate(${dx}px,${dy}px) scale(1)`
267
+ : 'translate(0,0) scale(.3)',
268
+ opacity: isOpen ? 1 : 0,
269
+ pointerEvents: isOpen ? 'auto' : 'none',
270
+ transition: `transform .3s cubic-bezier(.2,1.1,.3,1) ${i * 28}ms, opacity .22s ease ${i * 28}ms`,
271
+ },
272
+ };
273
+ });
274
+ }, ...(ngDevMode ? [{ debugName: "dialItems" }] : []));
275
+ /** Drawer (desktop) / bottom-sheet (mobile) — prototype values. */
276
+ this.panelStyle = computed(() => {
277
+ const isDesktop = this.winW() >= 720;
278
+ return isDesktop
279
+ ? {
280
+ position: 'fixed',
281
+ top: '0',
282
+ right: '0',
283
+ bottom: '0',
284
+ width: '440px',
285
+ zIndex: '49',
286
+ background: '#101117',
287
+ borderLeft: '1px solid #23252f',
288
+ display: 'flex',
289
+ flexDirection: 'column',
290
+ animation: 'drawerIn .26s cubic-bezier(.2,.8,.2,1)',
291
+ }
292
+ : {
293
+ position: 'fixed',
294
+ left: '0',
295
+ right: '0',
296
+ bottom: '0',
297
+ maxHeight: '86vh',
298
+ zIndex: '49',
299
+ background: '#101117',
300
+ borderTop: '1px solid #23252f',
301
+ borderRadius: '20px 20px 0 0',
302
+ display: 'flex',
303
+ flexDirection: 'column',
304
+ animation: 'sheetUp .28s cubic-bezier(.2,.8,.2,1)',
305
+ };
306
+ }, ...(ngDevMode ? [{ debugName: "panelStyle" }] : []));
307
+ this.drag = null;
308
+ this.dragEndAt = 0;
309
+ this.collapseTimer = null;
310
+ this.moveHandler = (e) => this.onMove(e);
311
+ this.upHandler = (e) => this.onUp(e);
312
+ this.scheduleCollapse();
313
+ }
314
+ ngOnDestroy() {
315
+ this.detachDragListeners();
316
+ this.clearCollapse();
317
+ }
318
+ onResize() {
319
+ this.winW.set(window.innerWidth);
320
+ this.winH.set(window.innerHeight);
321
+ this.pos.reclamp();
322
+ }
323
+ // ---- drag (ported from the prototype) ---------------------------------
324
+ onFabDown(e) {
325
+ const el = e.currentTarget;
326
+ const r = el.getBoundingClientRect();
327
+ this.drag = {
328
+ ox: e.clientX - r.left,
329
+ oy: e.clientY - r.top,
330
+ sx: e.clientX,
331
+ sy: e.clientY,
332
+ moved: false,
333
+ };
334
+ this.clearCollapse();
335
+ this.dragging.set(true);
336
+ this.collapsed.set(false);
337
+ try {
338
+ el.setPointerCapture(e.pointerId);
339
+ }
340
+ catch {
341
+ /* ignore */
342
+ }
343
+ window.addEventListener('pointermove', this.moveHandler);
344
+ window.addEventListener('pointerup', this.upHandler);
345
+ }
346
+ onMove(e) {
347
+ if (!this.drag)
348
+ return;
349
+ const x = e.clientX - this.drag.ox;
350
+ const y = e.clientY - this.drag.oy;
351
+ if (Math.abs(e.clientX - this.drag.sx) + Math.abs(e.clientY - this.drag.sy) > 5) {
352
+ this.drag.moved = true;
353
+ }
354
+ this.pos.move(this.pos.clamp({ x, y }));
355
+ }
356
+ onUp(_e) {
357
+ this.detachDragListeners();
358
+ const S = FAB_SIZE;
359
+ const W = this.winW();
360
+ const f = this.pos.position();
361
+ const moved = !!this.drag?.moved;
362
+ this.dragging.set(false);
363
+ if (moved) {
364
+ this.dragEndAt = Date.now();
365
+ const side = f.x + S / 2 < W / 2 ? 'left' : 'right';
366
+ const x = side === 'left' ? 10 : W - S - 10;
367
+ this.pos.commit({ x, y: f.y }, side);
368
+ this.scheduleCollapse();
369
+ }
370
+ this.drag = null;
371
+ }
372
+ detachDragListeners() {
373
+ window.removeEventListener('pointermove', this.moveHandler);
374
+ window.removeEventListener('pointerup', this.upHandler);
375
+ }
376
+ // ---- open / panel -----------------------------------------------------
377
+ onFabClick() {
378
+ if (Date.now() - this.dragEndAt < 300)
379
+ return; // was a drag
380
+ const acts = this.actions();
381
+ if (acts.length > 1) {
382
+ this.toggleDial();
383
+ }
384
+ else {
385
+ this.togglePanel(acts[0]?.id ?? PROJECTED);
386
+ }
387
+ }
388
+ toggleDial() {
389
+ const next = !this.open();
390
+ this.clearCollapse();
391
+ this.open.set(next);
392
+ this.collapsed.set(false);
393
+ this.panelId.set(null);
394
+ if (!next)
395
+ this.scheduleCollapse();
396
+ }
397
+ openPanel(id) {
398
+ this.panelId.set(id);
399
+ this.open.set(false);
400
+ this.clearCollapse();
401
+ }
402
+ togglePanel(id) {
403
+ if (this.panelId() === id)
404
+ this.closePanel();
405
+ else
406
+ this.openPanel(id);
407
+ }
408
+ closePanel() {
409
+ this.panelId.set(null);
410
+ this.scheduleCollapse();
411
+ }
412
+ closeDial() {
413
+ this.open.set(false);
414
+ this.scheduleCollapse();
415
+ }
416
+ asTemplate(c) {
417
+ return c instanceof TemplateRef ? c : null;
418
+ }
419
+ asComponent(c) {
420
+ return typeof c === 'function' ? c : null;
421
+ }
422
+ // ---- collapse (ported from the prototype) -----------------------------
423
+ scheduleCollapse() {
424
+ this.clearCollapse();
425
+ this.collapseTimer = setTimeout(() => {
426
+ if (!this.open() && !this.panelId() && !this.dragging()) {
427
+ this.collapsed.set(true);
428
+ }
429
+ }, COLLAPSE_DELAY_MS);
430
+ }
431
+ clearCollapse() {
432
+ if (this.collapseTimer)
433
+ clearTimeout(this.collapseTimer);
434
+ this.collapseTimer = null;
435
+ if (this.collapsed())
436
+ this.collapsed.set(false);
437
+ }
438
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
439
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: DevtoolsShellComponent, isStandalone: true, selector: "devtools-shell", host: { listeners: { "window:resize": "onResize()" } }, ngImport: i0, template: `
440
+ <!-- FAB (same button doubles as the collapsed edge grip, like the prototype) -->
441
+ <button
442
+ type="button"
443
+ [style]="fabStyle()"
444
+ (pointerdown)="onFabDown($event)"
445
+ (click)="onFabClick()"
446
+ [attr.aria-expanded]="open() || !!panelId()"
447
+ aria-label="DevTools"
448
+ >
449
+ @if (isCollapsed()) {
450
+ <span
451
+ style="width:4px;height:26px;border-radius:3px;background:rgba(255,255,255,.55)"
452
+ ></span>
453
+ } @else {
454
+ <span class="dts-sym" style="font-size:26px">{{
455
+ open() || panelId()
456
+ ? 'close'
457
+ : (config.fabIcon ?? 'developer_mode')
458
+ }}</span>
459
+ }
460
+ </button>
461
+
462
+ <!-- speed-dial scrim -->
463
+ @if (open()) {
464
+ <div
465
+ style="position:fixed;inset:0;z-index:38;background:rgba(10,11,15,.32);backdrop-filter:blur(1px);animation:dts-fade .18s ease"
466
+ (click)="closeDial()"
467
+ ></div>
468
+ }
469
+
470
+ <!-- speed-dial items (always rendered when >1 action; animate via transform/opacity) -->
471
+ @if (hasDial()) {
472
+ @for (item of dialItems(); track item.action.id) {
473
+ <button type="button" [style]="item.style" (click)="openPanel(item.action.id)">
474
+ @if (item.action.icon) {
475
+ <span class="dts-sym" style="font-size:23px;line-height:1">{{ item.action.icon }}</span>
476
+ }
477
+ <span
478
+ style="font-size:8.5px;font-weight:600;line-height:1.05;text-align:center;letter-spacing:.01em;white-space:nowrap"
479
+ >{{ item.action.label }}</span
480
+ >
481
+ </button>
482
+ }
483
+ }
484
+
485
+ <!-- panel scrim + panel -->
486
+ @if (panelId()) {
487
+ <div
488
+ style="position:fixed;inset:0;z-index:48;background:rgba(10,11,15,.5);animation:dts-fade .2s ease"
489
+ (click)="closePanel()"
490
+ ></div>
491
+ <div [style]="panelStyle()">
492
+ <div
493
+ style="display:flex;align-items:center;gap:12px;padding:18px 20px;border-bottom:1px solid #23252f;flex:none"
494
+ >
495
+ <span class="dts-sym" style="font-size:22px;color:#ffc454">{{
496
+ activeAction()?.icon ?? 'tune'
497
+ }}</span>
498
+ <div style="font-size:16px;font-weight:600;color:#f0f1f4">
499
+ {{ activeAction()?.label ?? config.title ?? 'DevTools' }}
500
+ </div>
501
+ <button
502
+ type="button"
503
+ (click)="closePanel()"
504
+ aria-label="Fechar"
505
+ style="margin-left:auto;width:32px;height:32px;border-radius:8px;border:1px solid #2a2c38;background:#1b1d25;color:#9aa0ad;display:flex;align-items:center;justify-content:center;cursor:pointer"
506
+ >
507
+ <span class="dts-sym" style="font-size:19px">close</span>
508
+ </button>
509
+ </div>
510
+ <div style="overflow-y:auto;padding:20px;flex:1;color:#d7d9e0">
511
+ @if (activeAction(); as action) {
512
+ @if (asTemplate(action.content); as tpl) {
513
+ <ng-container [ngTemplateOutlet]="tpl"></ng-container>
514
+ } @else if (asComponent(action.content); as cmp) {
515
+ <ng-container [ngComponentOutlet]="cmp"></ng-container>
516
+ } @else {
517
+ <ng-content></ng-content>
518
+ }
519
+ } @else {
520
+ <ng-content></ng-content>
521
+ }
522
+ </div>
523
+ </div>
524
+ }
525
+
526
+ <!-- toast -->
527
+ @if (toast.message(); as msg) {
528
+ <div
529
+ style="position:fixed;bottom:26px;left:50%;z-index:70;transform:translateX(-50%);background:#14151b;border:1px solid #2a2c38;color:#eceef3;font-size:13px;font-weight:500;padding:11px 18px;border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.4);display:flex;align-items:center;gap:9px;animation:dts-pop .22s ease;white-space:nowrap"
530
+ >
531
+ {{ msg }}
532
+ </div>
533
+ }
534
+ `, isInline: true, styles: [":host{--dts-accent: #ffc454}.dts-sym{font-family:Material Symbols Outlined,Material Icons;line-height:1;-webkit-user-select:none;user-select:none}@keyframes dts-fade{0%{opacity:0}to{opacity:1}}@keyframes drawerIn{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes sheetUp{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes dts-pop{0%{transform:translate(-50%,8px) scale(.96);opacity:0}to{transform:translate(-50%) scale(1);opacity:1}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
535
+ }
536
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsShellComponent, decorators: [{
537
+ type: Component,
538
+ args: [{ selector: 'devtools-shell', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgComponentOutlet, NgTemplateOutlet], template: `
539
+ <!-- FAB (same button doubles as the collapsed edge grip, like the prototype) -->
540
+ <button
541
+ type="button"
542
+ [style]="fabStyle()"
543
+ (pointerdown)="onFabDown($event)"
544
+ (click)="onFabClick()"
545
+ [attr.aria-expanded]="open() || !!panelId()"
546
+ aria-label="DevTools"
547
+ >
548
+ @if (isCollapsed()) {
549
+ <span
550
+ style="width:4px;height:26px;border-radius:3px;background:rgba(255,255,255,.55)"
551
+ ></span>
552
+ } @else {
553
+ <span class="dts-sym" style="font-size:26px">{{
554
+ open() || panelId()
555
+ ? 'close'
556
+ : (config.fabIcon ?? 'developer_mode')
557
+ }}</span>
558
+ }
559
+ </button>
560
+
561
+ <!-- speed-dial scrim -->
562
+ @if (open()) {
563
+ <div
564
+ style="position:fixed;inset:0;z-index:38;background:rgba(10,11,15,.32);backdrop-filter:blur(1px);animation:dts-fade .18s ease"
565
+ (click)="closeDial()"
566
+ ></div>
567
+ }
568
+
569
+ <!-- speed-dial items (always rendered when >1 action; animate via transform/opacity) -->
570
+ @if (hasDial()) {
571
+ @for (item of dialItems(); track item.action.id) {
572
+ <button type="button" [style]="item.style" (click)="openPanel(item.action.id)">
573
+ @if (item.action.icon) {
574
+ <span class="dts-sym" style="font-size:23px;line-height:1">{{ item.action.icon }}</span>
575
+ }
576
+ <span
577
+ style="font-size:8.5px;font-weight:600;line-height:1.05;text-align:center;letter-spacing:.01em;white-space:nowrap"
578
+ >{{ item.action.label }}</span
579
+ >
580
+ </button>
581
+ }
582
+ }
583
+
584
+ <!-- panel scrim + panel -->
585
+ @if (panelId()) {
586
+ <div
587
+ style="position:fixed;inset:0;z-index:48;background:rgba(10,11,15,.5);animation:dts-fade .2s ease"
588
+ (click)="closePanel()"
589
+ ></div>
590
+ <div [style]="panelStyle()">
591
+ <div
592
+ style="display:flex;align-items:center;gap:12px;padding:18px 20px;border-bottom:1px solid #23252f;flex:none"
593
+ >
594
+ <span class="dts-sym" style="font-size:22px;color:#ffc454">{{
595
+ activeAction()?.icon ?? 'tune'
596
+ }}</span>
597
+ <div style="font-size:16px;font-weight:600;color:#f0f1f4">
598
+ {{ activeAction()?.label ?? config.title ?? 'DevTools' }}
599
+ </div>
600
+ <button
601
+ type="button"
602
+ (click)="closePanel()"
603
+ aria-label="Fechar"
604
+ style="margin-left:auto;width:32px;height:32px;border-radius:8px;border:1px solid #2a2c38;background:#1b1d25;color:#9aa0ad;display:flex;align-items:center;justify-content:center;cursor:pointer"
605
+ >
606
+ <span class="dts-sym" style="font-size:19px">close</span>
607
+ </button>
608
+ </div>
609
+ <div style="overflow-y:auto;padding:20px;flex:1;color:#d7d9e0">
610
+ @if (activeAction(); as action) {
611
+ @if (asTemplate(action.content); as tpl) {
612
+ <ng-container [ngTemplateOutlet]="tpl"></ng-container>
613
+ } @else if (asComponent(action.content); as cmp) {
614
+ <ng-container [ngComponentOutlet]="cmp"></ng-container>
615
+ } @else {
616
+ <ng-content></ng-content>
617
+ }
618
+ } @else {
619
+ <ng-content></ng-content>
620
+ }
621
+ </div>
622
+ </div>
623
+ }
624
+
625
+ <!-- toast -->
626
+ @if (toast.message(); as msg) {
627
+ <div
628
+ style="position:fixed;bottom:26px;left:50%;z-index:70;transform:translateX(-50%);background:#14151b;border:1px solid #2a2c38;color:#eceef3;font-size:13px;font-weight:500;padding:11px 18px;border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.4);display:flex;align-items:center;gap:9px;animation:dts-pop .22s ease;white-space:nowrap"
629
+ >
630
+ {{ msg }}
631
+ </div>
632
+ }
633
+ `, styles: [":host{--dts-accent: #ffc454}.dts-sym{font-family:Material Symbols Outlined,Material Icons;line-height:1;-webkit-user-select:none;user-select:none}@keyframes dts-fade{0%{opacity:0}to{opacity:1}}@keyframes drawerIn{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes sheetUp{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes dts-pop{0%{transform:translate(-50%,8px) scale(.96);opacity:0}to{transform:translate(-50%) scale(1);opacity:1}}\n"] }]
634
+ }], ctorParameters: () => [], propDecorators: { onResize: [{
635
+ type: HostListener,
636
+ args: ['window:resize']
637
+ }] } });
638
+
639
+ const DEVTOOLS_CONFIG = new InjectionToken('DEVTOOLS_CONFIG');
640
+
641
+ /**
642
+ * Plain localStorage wrapper owned by the package. DevTools-internal keys are
643
+ * prefixed `DEVTOOLS_`; the *active app session* keys (`JWT_TOKEN`,
644
+ * `JWT_REFRESH_TOKEN`, `APP_USER`) are written deliberately so the host patient
645
+ * app consumes them — that is the only intentional touch-point with the host.
646
+ */
647
+ class DevtoolsStorage {
648
+ get(key) {
649
+ try {
650
+ const raw = localStorage.getItem(key);
651
+ return raw ? JSON.parse(raw) : null;
652
+ }
653
+ catch {
654
+ return null;
655
+ }
656
+ }
657
+ set(key, value) {
658
+ try {
659
+ localStorage.setItem(key, JSON.stringify(value));
660
+ }
661
+ catch {
662
+ /* storage unavailable — non-fatal */
663
+ }
664
+ }
665
+ remove(key) {
666
+ try {
667
+ localStorage.removeItem(key);
668
+ }
669
+ catch {
670
+ /* ignore */
671
+ }
672
+ }
673
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
674
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsStorage }); }
675
+ }
676
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsStorage, decorators: [{
677
+ type: Injectable
678
+ }] });
679
+
680
+ /** Triage form cases offered by the automatic scenario. */
681
+ const OPTIONS_RESPONSES_CASE = [
682
+ { name: 'Homem, 18 anos', value: 'case1', age: 18, biologicalSex: 'MALE' },
683
+ ];
684
+ const USERS_KEY = 'DEVTOOLS_USERS';
685
+ /** Data generation + last-user storage (ported from the app's UserUtilsService). */
686
+ class DevtoolsUserUtils {
687
+ constructor() {
688
+ this.storage = inject(DevtoolsStorage);
689
+ this.firstNames = ['Ana', 'Lucas', 'Miguel', 'Gabriela', 'Julia', 'Sophia', 'Bruno', 'Gustavo', 'Fernanda', 'Patrícia', 'Joel', 'Heitor'];
690
+ this.lastNames = ['Silva', 'Santos', 'Oliveira', 'Souza', 'Rodrigues', 'Ferreira', 'Almeida', 'Pereira', 'Lima', 'Costa'];
691
+ }
692
+ generateCPF() {
693
+ let cpf = Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join('');
694
+ cpf += this.checkDigit(cpf);
695
+ cpf += this.checkDigit(cpf);
696
+ return cpf;
697
+ }
698
+ checkDigit(cpf) {
699
+ const numbers = cpf.split('').map((n) => parseInt(n, 10));
700
+ const length = numbers.length + 1;
701
+ const sum = numbers.reduce((acc, n, i) => acc + n * (length - i), 0);
702
+ const remainder = (sum * 10) % 11;
703
+ return remainder === 10 ? '0' : remainder.toString();
704
+ }
705
+ generateRandomName() {
706
+ const f = this.firstNames[Math.floor(Math.random() * this.firstNames.length)];
707
+ const l = this.lastNames[Math.floor(Math.random() * this.lastNames.length)];
708
+ return `${f} ${l}`;
709
+ }
710
+ generateRandomGender() {
711
+ return Math.random() > 0.5 ? 'MALE' : 'FEMALE';
712
+ }
713
+ generateRandomBirthDate() {
714
+ const currentYear = new Date().getFullYear();
715
+ const minBirthYear = currentYear - 70;
716
+ const maxBirthYear = currentYear - 18;
717
+ const year = Math.floor(Math.random() * (maxBirthYear - minBirthYear + 1)) + minBirthYear;
718
+ const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
719
+ const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0');
720
+ return `${year}-${month}-${day}`;
721
+ }
722
+ calculateAge(birthDateString) {
723
+ if (!birthDateString)
724
+ return '';
725
+ const birth = new Date(birthDateString);
726
+ const today = new Date();
727
+ let age = today.getFullYear() - birth.getFullYear();
728
+ const md = today.getMonth() - birth.getMonth();
729
+ if (md < 0 || (md === 0 && today.getDate() < birth.getDate()))
730
+ age--;
731
+ return age;
732
+ }
733
+ calculateBirthDate(age) {
734
+ const now = new Date();
735
+ const d = new Date(now.getFullYear() - age, now.getMonth(), now.getDate());
736
+ const month = String(d.getMonth() + 1).padStart(2, '0');
737
+ const day = String(d.getDate()).padStart(2, '0');
738
+ return `${d.getFullYear()}-${month}-${day}`;
739
+ }
740
+ setToStorage(user) {
741
+ this.storage.set(USERS_KEY, [user]);
742
+ }
743
+ getUserFromStorage() {
744
+ const list = this.storage.get(USERS_KEY) ?? [];
745
+ if (!Array.isArray(list) || !list.length)
746
+ return [];
747
+ return [list[list.length - 1]];
748
+ }
749
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsUserUtils, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
750
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsUserUtils }); }
751
+ }
752
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsUserUtils, decorators: [{
753
+ type: Injectable
754
+ }] });
755
+ /** Misc helpers (ported from the app's UtilsService) — toast/masks/params. */
756
+ class DevtoolsUtils {
757
+ constructor() {
758
+ this.toast = inject(DevtoolsToastService);
759
+ }
760
+ objectParams(queryParams) {
761
+ let params = new HttpParams();
762
+ Object.keys(queryParams).forEach((k) => {
763
+ if (queryParams[k] !== undefined && queryParams[k] !== null && queryParams[k] !== '') {
764
+ params = params.append(k, queryParams[k]);
765
+ }
766
+ });
767
+ return params;
768
+ }
769
+ setErrorToast(err = {}) {
770
+ const message = err?.error?.message;
771
+ this.toast.show(message || 'Erro desconhecido');
772
+ }
773
+ copyClipboard(value, label = 'Texto') {
774
+ try {
775
+ navigator.clipboard?.writeText(String(value));
776
+ }
777
+ catch {
778
+ /* ignore */
779
+ }
780
+ this.toast.show(`${label} copiado`);
781
+ }
782
+ removePhoneMask(phone) {
783
+ return (phone ?? '').toString().replace(/\D/g, '');
784
+ }
785
+ maskCpfCnpj(v) {
786
+ return (v ?? '').toString().replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
787
+ }
788
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsUtils, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
789
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsUtils }); }
790
+ }
791
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsUtils, decorators: [{
792
+ type: Injectable
793
+ }] });
794
+
795
+ /* Extracted verbatim from the app for self-contained packaging. */
796
+ /* eslint-disable */
797
+ const FEMALE_42 = {};
798
+ const FEMALE_18 = {};
799
+ const FEMALE_30 = {};
800
+ const MALE_18 = {
801
+ 'formSubmission': {
802
+ 'landedAt': '2025-06-04T13:24:49.132Z',
803
+ 'person': { 'id': 35008 },
804
+ 'formAnswers': [{
805
+ 'formQuestion': { 'slug': 'Race' },
806
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'BROWN' }, 'value': 'Parda' }]
807
+ }, { 'value': 120, 'formQuestion': { 'slug': 'Weight' } }, {
808
+ 'value': 180,
809
+ 'formQuestion': { 'slug': 'Height' }
810
+ }, {
811
+ 'formQuestion': { 'slug': 'BellyShapeV2' },
812
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Three' }, 'value': '3' }]
813
+ }, {
814
+ 'formQuestion': { 'slug': 'FrequencyOfRedMeat' },
815
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
816
+ }, {
817
+ 'formQuestion': { 'slug': 'FrequencyOfCheeseAndButter' },
818
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
819
+ }, {
820
+ 'formQuestion': { 'slug': 'ExtraSalt' },
821
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
822
+ }, {
823
+ 'formQuestion': { 'slug': 'EverDayFruits' },
824
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
825
+ }, {
826
+ 'formQuestion': { 'slug': 'FrequencyOfPhysicalActivityV2' },
827
+ 'formAnswerChoices': [{
828
+ 'formQuestionChoice': { 'slug': 'DontPracticeRegularPhysicalActivity' },
829
+ 'value': 'Não pratico atividade física regular'
830
+ }]
831
+ }, {
832
+ 'formQuestion': { 'slug': 'TriageSmoking' },
833
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'No' }, 'value': 'Não' }]
834
+ }, {
835
+ 'formQuestion': { 'slug': 'TriageAlcohol' },
836
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Never' }, 'value': 'Nunca' }]
837
+ }, {
838
+ 'formQuestion': { 'slug': 'TriageMedicationAbuse' },
839
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Never' }, 'value': 'Nenhuma vez' }]
840
+ }, {
841
+ 'formQuestion': { 'slug': 'TriageIllicitDrugs' },
842
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Never' }, 'value': 'Nenhuma vez' }]
843
+ }, {
844
+ 'formQuestion': { 'slug': 'FeelingNervousOrAnxietyOrTense' },
845
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Never' }, 'value': 'Nenhuma vez' }]
846
+ }, {
847
+ 'formQuestion': { 'slug': 'IncapableToControlConcerns' },
848
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Never' }, 'value': 'Nenhuma vez' }]
849
+ }, {
850
+ 'formQuestion': { 'slug': 'LastTwoWeeksDepressed' },
851
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Never' }, 'value': 'Nenhuma vez' }]
852
+ }, {
853
+ 'formQuestion': { 'slug': 'LastTwoWeeksLostOfInterestDepressed' },
854
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Never' }, 'value': 'Nenhuma vez' }]
855
+ }, {
856
+ 'formQuestion': { 'slug': 'UnprotectedSexInfectiousDiseases' },
857
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
858
+ }, {
859
+ 'formQuestion': { 'slug': 'InjectionDrugsInfectiousDiseases' },
860
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
861
+ }, {
862
+ 'formQuestion': { 'slug': 'SyphilisTreatment' },
863
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
864
+ }, {
865
+ 'formQuestion': { 'slug': 'TuberculosisInfectiousDiseases' },
866
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
867
+ }, {
868
+ 'formQuestion': { 'slug': 'PreviousDiabetes' },
869
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
870
+ }, {
871
+ 'formQuestion': { 'slug': 'MedicalMonitoringDiabetes' },
872
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
873
+ }, {
874
+ 'formQuestion': { 'slug': 'PreviousHypertension' },
875
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
876
+ }, {
877
+ 'formQuestion': { 'slug': 'MedicalMonitoringHAS' },
878
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
879
+ }, {
880
+ 'formQuestion': { 'slug': 'PreviousCholesterol' },
881
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
882
+ }, {
883
+ 'formQuestion': { 'slug': 'MedicalMonitoringDyslipidemia' },
884
+ 'formAnswerChoices': [{ 'formQuestionChoice': { 'slug': 'Yes' }, 'value': 'Sim' }]
885
+ }]
886
+ }
887
+ };
888
+ const MALE_50 = {};
889
+ const MALE_70 = {};
890
+
891
+ /** Builds the form-submission body for the automatic-signup triage flow. */
892
+ class DevtoolsTypeform {
893
+ constructor() {
894
+ this.userUtils = inject(DevtoolsUserUtils);
895
+ this.cases = {
896
+ MALE_18,
897
+ MALE_50,
898
+ MALE_70,
899
+ FEMALE_18,
900
+ FEMALE_30,
901
+ FEMALE_42,
902
+ };
903
+ }
904
+ getCase(dataToReplace) {
905
+ const { biologicalSex, birthdate, personId } = dataToReplace;
906
+ const key = `${biologicalSex}_${this.userUtils.calculateAge(birthdate)}`;
907
+ const formJson = this.cases[key];
908
+ if (!formJson || !formJson.formSubmission) {
909
+ return {
910
+ formSubmission: {
911
+ person: { id: personId },
912
+ landedAt: new Date().toISOString(),
913
+ formAnswers: [],
914
+ },
915
+ };
916
+ }
917
+ return {
918
+ formSubmission: {
919
+ ...formJson.formSubmission,
920
+ person: { id: personId },
921
+ landedAt: new Date().toISOString(),
922
+ },
923
+ };
924
+ }
925
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTypeform, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
926
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTypeform }); }
927
+ }
928
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTypeform, decorators: [{
929
+ type: Injectable
930
+ }] });
931
+
932
+ /* Extracted verbatim from the app for self-contained packaging. */
933
+ const EXAMS_18 = [
934
+ {
935
+ 'person': {
936
+ 'id': 5518
937
+ },
938
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
939
+ 'performedAt': '2024-08-12',
940
+ 'exam': {
941
+ 'id': 4
942
+ },
943
+ 'examParse': {},
944
+ 'examResults': [
945
+ {
946
+ 'value': '20',
947
+ 'expectedExamResult': {
948
+ 'id': 5
949
+ }
950
+ }
951
+ ],
952
+ 'laboratory': null,
953
+ 'id': null,
954
+ 'source': 'MANUAL'
955
+ },
956
+ {
957
+ 'person': {
958
+ 'id': 5518
959
+ },
960
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
961
+ 'performedAt': '2024-08-12',
962
+ 'exam': {
963
+ 'id': 2
964
+ },
965
+ 'examParse': {},
966
+ 'examResults': [
967
+ {
968
+ 'value': '12',
969
+ 'expectedExamResult': {
970
+ 'id': 2
971
+ }
972
+ },
973
+ {
974
+ 'value': '8',
975
+ 'expectedExamResult': {
976
+ 'id': 3
977
+ }
978
+ }
979
+ ],
980
+ 'laboratory': null,
981
+ 'id': null,
982
+ 'source': 'MANUAL'
983
+ },
984
+ {
985
+ 'person': {
986
+ 'id': 5518
987
+ },
988
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
989
+ 'performedAt': '2024-08-12',
990
+ 'exam': {
991
+ 'id': 5
992
+ },
993
+ 'examParse': {},
994
+ 'examResults': [
995
+ {
996
+ 'value': '24',
997
+ 'expectedExamResult': {
998
+ 'id': 6
999
+ }
1000
+ },
1001
+ {
1002
+ 'value': '24',
1003
+ 'expectedExamResult': {
1004
+ 'id': 7
1005
+ }
1006
+ },
1007
+ {
1008
+ 'value': '24',
1009
+ 'expectedExamResult': {
1010
+ 'id': 8
1011
+ }
1012
+ }
1013
+ ],
1014
+ 'laboratory': null,
1015
+ 'id': null,
1016
+ 'source': 'MANUAL'
1017
+ },
1018
+ {
1019
+ 'person': {
1020
+ 'id': 5518
1021
+ },
1022
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
1023
+ 'performedAt': '2024-08-12',
1024
+ 'exam': {
1025
+ 'id': 6
1026
+ },
1027
+ 'examParse': {},
1028
+ 'examResults': [
1029
+ {
1030
+ 'value': '12',
1031
+ 'expectedExamResult': {
1032
+ 'id': 9
1033
+ }
1034
+ }
1035
+ ],
1036
+ 'laboratory': null,
1037
+ 'id': null,
1038
+ 'source': 'MANUAL'
1039
+ },
1040
+ {
1041
+ 'person': {
1042
+ 'id': 5518
1043
+ },
1044
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
1045
+ 'performedAt': '2024-08-12',
1046
+ 'exam': {
1047
+ 'id': 7895
1048
+ },
1049
+ 'examParse': {},
1050
+ 'examResults': [
1051
+ {
1052
+ 'value': 'Positivo',
1053
+ 'expectedExamResult': {
1054
+ 'id': 39
1055
+ }
1056
+ }
1057
+ ],
1058
+ 'laboratory': null,
1059
+ 'id': null,
1060
+ 'source': 'MANUAL'
1061
+ },
1062
+ {
1063
+ 'person': {
1064
+ 'id': 5518
1065
+ },
1066
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
1067
+ 'performedAt': '2024-08-12',
1068
+ 'exam': {
1069
+ 'id': 19
1070
+ },
1071
+ 'examParse': {},
1072
+ 'examResults': [
1073
+ {
1074
+ 'value': 'Reagente',
1075
+ 'expectedExamResult': {
1076
+ 'id': 23
1077
+ }
1078
+ }
1079
+ ],
1080
+ 'laboratory': null,
1081
+ 'id': null,
1082
+ 'source': 'MANUAL'
1083
+ },
1084
+ {
1085
+ 'person': {
1086
+ 'id': 5518
1087
+ },
1088
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
1089
+ 'performedAt': '2024-08-12',
1090
+ 'exam': {
1091
+ 'id': 20
1092
+ },
1093
+ 'examParse': {},
1094
+ 'examResults': [
1095
+ {
1096
+ 'value': 'Reagente',
1097
+ 'expectedExamResult': {
1098
+ 'id': 24
1099
+ }
1100
+ }
1101
+ ],
1102
+ 'laboratory': null,
1103
+ 'id': null,
1104
+ 'source': 'MANUAL'
1105
+ },
1106
+ {
1107
+ 'person': {
1108
+ 'id': 5518
1109
+ },
1110
+ 'documentUrl': 'https://stag-mar-static.s3.us-east-2.amazonaws.com/performed-exam/83379b83-ce53-4738-9bb1-fd48fea247ae.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJIMEYCIQCXmt5XvaK1l5abS6W5Wb20oJ51DrMszJPXJUOG5n%2BDxQIhAKOacn2Oj9PJqZQ02yuao2%2BparPJXCigLaaG0bEx7wtvKo4ECJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMODU4NTIwMjAxODY0IgyIKcHluQuoiNMjPu4q4gMfnluXq8jJxspyzNCwZ948wmhjj0ebDm8TmzFmUic68sdlE4WsZAcUPQmTcZDImBX0KaukGRcI3jdFX62XaMMJ4Byuxj0EloP%2BGZyah2eDrPZr2%2FhDIKZuyrUtebZUCpZe%2FLI%2FWCBnblI12u14TtJGbcVTbtiw58onl2kvOCgwb9FUskI3dJGmha8CqDMCdOAb3sMQozvU1PgXRIj1o5bDj0U5jFT7VVtuAGv3LqsEfd9dxAj%2FcCwgwCJQ%2BbrjH3ABdxwdiVacDOfqTDWuvemFvjIDjIKXG%2Fq%2BcDXEifcMBNdWk%2BVffcLb0RZLfvF2JW6h4n1vUrfqBiL6FW7s%2FWYF6qrv7h1ZfGt1JtPXiP%2B98vU9MEZhUqB6RmMDTtuvI3o11V7C9GuSdvP15imWhkbmylz0oA98MzjO6RsN5D7CvT1STICeXR1WTGKAvH8p2E5HNL%2BGo11oidE6wMFC0NLDbMD5tbVyPeTGA%2BYzX8XE21gmWoOfntRryK5kyZNbu6K7Y1L0owZ6nYGMBFV9G6jxIsZ3SaPbzh1M3sndmwFYit%2FxaeYNA1rIDyAUkZjdEFPbNMhTK3kKgdb5e8ZWbsXBBuzPdoBpIShIUIVvM%2FnbGkvxBo%2FCOl%2FwrU40by0MWhObpTDX%2Fui1BjqkAXoYq3ayR5DIQFaLyIqrRtqhKUugq5Mf7ecpa08dP729n7YMxd69OLKwLV%2BUAPdUsvsRGJq01BLFJqFyvX4big29FCgavwNb5BtaCPEbtGaGUkOhI44nPc0nIUTMEUc5K26kNbELQfk6sXElU7AfoQtP%2Bj6JmFO9FfJ%2FFTne1VZRcYdjhtPHeOmglGChz7sb3DSODSIVXBXK4fhY%2FD3LKfK6JNwP&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240812T192159Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIA4PY6KVKEEJCK2C4E%2F20240812%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Expires=3600&X-Amz-Signature=0cecd10df31c9c0ab2f890f30643441d595ce016865a02b7bd94065b605c4852',
1111
+ 'performedAt': '2024-08-12',
1112
+ 'exam': {
1113
+ 'id': 21
1114
+ },
1115
+ 'examParse': {},
1116
+ 'examResults': [
1117
+ {
1118
+ 'value': '1:8',
1119
+ 'expectedExamResult': {
1120
+ 'id': 25
1121
+ }
1122
+ }
1123
+ ],
1124
+ 'laboratory': null,
1125
+ 'id': null,
1126
+ 'source': 'MANUAL'
1127
+ }
1128
+ ];
1129
+
1130
+ /**
1131
+ * All API access for the DevTools, on the package's OWN HttpClient (isolated
1132
+ * interceptor). Mirrors the host UserService surface plus the OTP/Google auth
1133
+ * endpoints, so panels port across with no behavioural change.
1134
+ */
1135
+ class DevtoolsApiService {
1136
+ constructor() {
1137
+ this.http = inject(HttpClient);
1138
+ this.config = inject(DEVTOOLS_CONFIG);
1139
+ this.utils = inject(DevtoolsUtils);
1140
+ this.typeform = inject(DevtoolsTypeform);
1141
+ }
1142
+ get api() {
1143
+ return this.config.apiBaseUrl;
1144
+ }
1145
+ get media() {
1146
+ return this.config.media ?? 'WHATSAPP';
1147
+ }
1148
+ // ---- auth endpoints ---------------------------------------------------
1149
+ checkRegistration(cpf) {
1150
+ return this.http.get(`${this.api}/person-registrations`, {
1151
+ params: this.utils.objectParams({ document: onlyDigits(cpf) }),
1152
+ });
1153
+ }
1154
+ requestCode(input) {
1155
+ return this.recaptcha().pipe(switchMap((token) => {
1156
+ const headers = new HttpHeaders({ 'X-Google-Recaptcha-Token': token });
1157
+ const recipientValidationCode = {
1158
+ document: onlyDigits(input.document),
1159
+ phone: input.phone,
1160
+ media: this.media,
1161
+ };
1162
+ if (input.personProperties)
1163
+ recipientValidationCode['personProperties'] = input.personProperties;
1164
+ return this.http.post(`${this.api}/recipient-validation-codes`, { recipientValidationCode }, { headers });
1165
+ }));
1166
+ }
1167
+ exchangeOtp(document, code) {
1168
+ return this.http.post(`${this.api}/token`, {
1169
+ grantType: 'RECIPIENT_VALIDATION_CODE',
1170
+ code: onlyDigits(code),
1171
+ document: onlyDigits(document),
1172
+ });
1173
+ }
1174
+ /** Exchange the OTP code and load the profile in one shot. */
1175
+ otpLogin(document, code) {
1176
+ return this.exchangeOtp(document, code).pipe(switchMap((tokens) => this.self(tokens.accessToken).pipe(map((profile) => ({ accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, profile })))));
1177
+ }
1178
+ exchangeGoogle(googleAccessToken) {
1179
+ return this.http.post(`${this.api}/token`, {
1180
+ grantType: 'GOOGLE_ACCESS_TOKEN',
1181
+ googleAccessToken,
1182
+ });
1183
+ }
1184
+ refreshToken(refreshToken) {
1185
+ return this.http.post(`${this.api}/token`, {
1186
+ grantType: 'REFRESH_TOKEN',
1187
+ refreshToken,
1188
+ });
1189
+ }
1190
+ /** GET /persons/self. Pass a token to use it instead of the admin token. */
1191
+ self(accessToken) {
1192
+ const context = accessToken ? new HttpContext().set(DC_AUTH_TOKEN, accessToken) : undefined;
1193
+ return this.http
1194
+ .get(`${this.api}/persons/self`, context ? { context } : {})
1195
+ .pipe(map((r) => r?.person ?? null));
1196
+ }
1197
+ // ---- users ------------------------------------------------------------
1198
+ findUser(queryParams) {
1199
+ return this.http.get(`${this.api}/persons`, { params: this.utils.objectParams(queryParams) });
1200
+ }
1201
+ findRemoveUserPhone(phone) {
1202
+ return this.findUser({ phones: `+55${phone}` }).pipe(map((res) => res?.persons?.[0]), switchMap((user) => {
1203
+ if (user?.id) {
1204
+ return this.http.patch(`${this.api}/persons/${user.id}`, { person: { phone: null } });
1205
+ }
1206
+ return of('');
1207
+ }));
1208
+ }
1209
+ signupForm(body, phone) {
1210
+ const request = this.http.post(`${this.api}/person-imports`, this.personImportBody(body));
1211
+ if (phone)
1212
+ return this.findRemoveUserPhone(phone).pipe(switchMap(() => request));
1213
+ return request;
1214
+ }
1215
+ autoSignup(body, phone) {
1216
+ return this.signupForm(body, phone).pipe(switchMap(() => this.findUser({ q: body.document }).pipe(map((r) => r?.persons?.[0]))), switchMap((person) => {
1217
+ const { biologicalSex, birthdate, fullName } = body;
1218
+ const formBody = this.typeform.getCase({ personId: person?.id, biologicalSex, birthdate, name: fullName });
1219
+ return this.http.post(`${this.api}/form-submissions`, formBody);
1220
+ }));
1221
+ }
1222
+ personImportBody(body) {
1223
+ const { document, fullName: name, birthdate, biologicalSex, orgDocument, orgDocumentType, customerOrganizationId } = body;
1224
+ return {
1225
+ personImport: {
1226
+ customerOrganization: { id: customerOrganizationId },
1227
+ persons: [
1228
+ {
1229
+ document,
1230
+ personProperties: { name, biologicalSex, birthdate },
1231
+ subscription: {
1232
+ ownership: 'HOLDER',
1233
+ organization: { document: orgDocument, documentType: orgDocumentType },
1234
+ },
1235
+ },
1236
+ ],
1237
+ },
1238
+ };
1239
+ }
1240
+ generatePerformedExams(user, age, documentUrl) {
1241
+ return this.findUser({ documents: user.document }).pipe(switchMap((res) => {
1242
+ const person = res?.persons?.[0];
1243
+ const today = new Date();
1244
+ const performedAt = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
1245
+ if (age === 18) {
1246
+ const performedExams = EXAMS_18.map((exam) => ({
1247
+ ...exam,
1248
+ person: { id: person?.id },
1249
+ performedAt,
1250
+ examSource: { name: 'ADMIN' },
1251
+ documentUrl,
1252
+ }));
1253
+ return this.http
1254
+ .post(`${this.api}/performed-exams/create-many`, { performedExams })
1255
+ .pipe(map((r) => r?.performedExams ?? []));
1256
+ }
1257
+ return of([]);
1258
+ }));
1259
+ }
1260
+ // ---- organizations + permissions -------------------------------------
1261
+ getWithQuery(params) {
1262
+ return this.http
1263
+ .get(`${this.api}/organizations`, { params: this.utils.objectParams(params) })
1264
+ .pipe(map((r) => r?.organizations ?? []));
1265
+ }
1266
+ getOrganizationsByIds(ids) {
1267
+ if (!ids.length)
1268
+ return of([]);
1269
+ return this.getWithQuery({ minActiveSubscriptions: 1, ids: ids.join(','), offset: 0, limit: 15 });
1270
+ }
1271
+ getAuthStatements(personId) {
1272
+ return this.http
1273
+ .get(`${this.api}/auth-statements`, { params: this.utils.objectParams({ principalPersonIds: personId, roleNames: 'MANAGER,DOCTOR' }) })
1274
+ .pipe(map((r) => r?.authStatements ?? []));
1275
+ }
1276
+ getAuthStatementsByPerson(personId) {
1277
+ return this.http
1278
+ .get(`${this.api}/auth-statements`, { params: this.utils.objectParams({ principalPersonIds: personId }) })
1279
+ .pipe(map((r) => r?.authStatements ?? []));
1280
+ }
1281
+ createAuthStatement(personId, roleName, organizationId) {
1282
+ return this.http.post(`${this.api}/auth-statements`, {
1283
+ authStatement: {
1284
+ role: { name: roleName },
1285
+ principal: { person: { id: personId } },
1286
+ resource: { organization: { id: organizationId } },
1287
+ },
1288
+ });
1289
+ }
1290
+ deleteAuthStatement(authStatementId) {
1291
+ return this.http.delete(`${this.api}/auth-statements/${authStatementId}`);
1292
+ }
1293
+ deleteAuthStatementsByIds(ids) {
1294
+ const valid = ids.filter((id) => !!id);
1295
+ if (!valid.length)
1296
+ return of([]);
1297
+ return forkJoin(valid.map((id) => this.deleteAuthStatement(id)));
1298
+ }
1299
+ // ---- reCAPTCHA --------------------------------------------------------
1300
+ recaptcha() {
1301
+ const key = this.config.recaptchaSiteKey;
1302
+ const grecaptcha = window.grecaptcha;
1303
+ if (!key || !grecaptcha?.execute)
1304
+ return of('');
1305
+ return from(new Promise((resolve) => {
1306
+ grecaptcha.ready(() => grecaptcha.execute(key, { action: 'login' }).then(resolve).catch(() => resolve('')));
1307
+ }));
1308
+ }
1309
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1310
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsApiService }); }
1311
+ }
1312
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsApiService, decorators: [{
1313
+ type: Injectable
1314
+ }] });
1315
+ function onlyDigits(v) {
1316
+ return (v ?? '').replace(/\D/g, '');
1317
+ }
1318
+
1319
+ const ADMIN_TOKEN = 'DEVTOOLS_ADMIN_TOKEN';
1320
+ const ADMIN_REFRESH = 'DEVTOOLS_ADMIN_REFRESH';
1321
+ const ADMIN_USER = 'DEVTOOLS_ADMIN_USER';
1322
+ const OAUTH_FLAG = 'DEVTOOLS_OAUTH_PENDING';
1323
+ /**
1324
+ * The DevTools' OWN identity (admin). Completely separate from the host app's
1325
+ * session: its tokens live under `DEVTOOLS_ADMIN_*` and are what the package's
1326
+ * interceptor attaches. The package is "logged in" iff an admin token exists.
1327
+ */
1328
+ class DevtoolsAuthService {
1329
+ constructor() {
1330
+ this.storage = inject(DevtoolsStorage);
1331
+ this.config = inject(DEVTOOLS_CONFIG);
1332
+ this.api = inject(DevtoolsApiService);
1333
+ this._token = signal(this.storage.get(ADMIN_TOKEN), ...(ngDevMode ? [{ debugName: "_token" }] : []));
1334
+ this.logged = computed(() => !!this._token(), ...(ngDevMode ? [{ debugName: "logged" }] : []));
1335
+ }
1336
+ adminToken() {
1337
+ return this._token();
1338
+ }
1339
+ adminRefreshToken() {
1340
+ return this.storage.get(ADMIN_REFRESH);
1341
+ }
1342
+ adminProfile() {
1343
+ return this.storage.get(ADMIN_USER);
1344
+ }
1345
+ isLogged() {
1346
+ return !!this._token();
1347
+ }
1348
+ /** Begin Google OAuth for the admin (full-page redirect). */
1349
+ loginGoogleRedirect() {
1350
+ const clientId = this.config.googleClientId;
1351
+ const redirect = this.config.googleRedirectUri || window.location.origin;
1352
+ if (!clientId) {
1353
+ console.error('[devtools] googleClientId not configured');
1354
+ return;
1355
+ }
1356
+ try {
1357
+ sessionStorage.setItem(OAUTH_FLAG, '1');
1358
+ }
1359
+ catch {
1360
+ /* ignore */
1361
+ }
1362
+ window.location.href =
1363
+ `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}` +
1364
+ `&redirect_uri=${encodeURIComponent(redirect)}` +
1365
+ `&response_type=token&scope=openid+email&include_granted_scopes=true`;
1366
+ }
1367
+ /**
1368
+ * Consume a Google OAuth return that WE initiated (guarded by a flag so we
1369
+ * never steal the host app's own OAuth redirect). Exchanges the access token
1370
+ * for admin JWTs and loads the admin profile.
1371
+ */
1372
+ handleGoogleReturn() {
1373
+ let pending = false;
1374
+ try {
1375
+ pending = sessionStorage.getItem(OAUTH_FLAG) === '1';
1376
+ }
1377
+ catch {
1378
+ /* ignore */
1379
+ }
1380
+ if (!pending || !window.location.hash)
1381
+ return;
1382
+ const params = new URLSearchParams(window.location.hash.replace(/^#/, ''));
1383
+ const googleToken = params.get('access_token');
1384
+ if (!googleToken)
1385
+ return;
1386
+ try {
1387
+ sessionStorage.removeItem(OAUTH_FLAG);
1388
+ }
1389
+ catch {
1390
+ /* ignore */
1391
+ }
1392
+ this.exchangeGoogle(googleToken).subscribe({
1393
+ next: () => {
1394
+ try {
1395
+ history.replaceState(null, '', window.location.pathname + window.location.search);
1396
+ }
1397
+ catch {
1398
+ /* ignore */
1399
+ }
1400
+ },
1401
+ error: (e) => console.error('[devtools] admin google login failed', e),
1402
+ });
1403
+ }
1404
+ exchangeGoogle(googleToken) {
1405
+ return this.api.exchangeGoogle(googleToken).pipe(tap((tokens) => this.setTokens(tokens)), tap(() => this.api.self(this._token() ?? undefined).subscribe((p) => this.storage.set(ADMIN_USER, p))));
1406
+ }
1407
+ /** Used by the interceptor on 401. */
1408
+ refresh() {
1409
+ return this.api
1410
+ .refreshToken(this.adminRefreshToken() ?? '')
1411
+ .pipe(tap((tokens) => this.setTokens(tokens)));
1412
+ }
1413
+ setTokens(tokens) {
1414
+ this.storage.set(ADMIN_TOKEN, tokens.accessToken);
1415
+ this.storage.set(ADMIN_REFRESH, tokens.refreshToken);
1416
+ this._token.set(tokens.accessToken);
1417
+ }
1418
+ logout() {
1419
+ this.storage.remove(ADMIN_TOKEN);
1420
+ this.storage.remove(ADMIN_REFRESH);
1421
+ this.storage.remove(ADMIN_USER);
1422
+ this._token.set(null);
1423
+ }
1424
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1425
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsAuthService }); }
1426
+ }
1427
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsAuthService, decorators: [{
1428
+ type: Injectable
1429
+ }] });
1430
+
1431
+ /**
1432
+ * Per-request override token: when set, this access token is used instead of
1433
+ * the admin token (e.g. the just-issued test-user token for `persons/self`).
1434
+ */
1435
+ const DC_AUTH_TOKEN = new HttpContextToken(() => null);
1436
+ /**
1437
+ * The package's OWN HTTP interceptor — registered only on the package's
1438
+ * isolated HttpClient, never on the host's. By default it attaches the DevTools
1439
+ * admin token + `X-Mar-Locale`, and refreshes the admin token on 401. `/token`
1440
+ * is sent bare (no auth, no locale); S3 is passed through untouched.
1441
+ */
1442
+ const devtoolsHttpInterceptor = (req, next) => {
1443
+ const config = inject(DEVTOOLS_CONFIG);
1444
+ const auth = inject(DevtoolsAuthService);
1445
+ if (req.url.includes('.s3.'))
1446
+ return next(req);
1447
+ const isTokenEndpoint = req.url.includes('/token');
1448
+ const locale = config.locale ?? 'pt_BR';
1449
+ const override = req.context.get(DC_AUTH_TOKEN);
1450
+ const token = override ?? (isTokenEndpoint ? null : auth.adminToken());
1451
+ const setHeaders = isTokenEndpoint
1452
+ ? {}
1453
+ : { 'X-Mar-Locale': locale };
1454
+ if (token && !isTokenEndpoint)
1455
+ setHeaders['Authorization'] = `Bearer ${token}`;
1456
+ const authed = req.clone({ setHeaders });
1457
+ return next(authed).pipe(catchError((err) => {
1458
+ const canRefresh = err?.status === 401 &&
1459
+ !isTokenEndpoint &&
1460
+ !override &&
1461
+ !!auth.adminRefreshToken();
1462
+ if (canRefresh) {
1463
+ return auth.refresh().pipe(switchMap$1(() => next(req.clone({
1464
+ setHeaders: {
1465
+ 'X-Mar-Locale': locale,
1466
+ Authorization: `Bearer ${auth.adminToken()}`,
1467
+ },
1468
+ }))), catchError((e) => {
1469
+ auth.logout();
1470
+ return throwError(() => e);
1471
+ }));
1472
+ }
1473
+ return throwError(() => err);
1474
+ }));
1475
+ };
1476
+
1477
+ const SESSIONS_KEY = 'DEVTOOLS_SESSIONS';
1478
+ const ACTIVE_KEY = 'DEVTOOLS_ACTIVE';
1479
+ // The active app session — read by the HOST patient app. This is the ONE
1480
+ // intentional touch-point with the host.
1481
+ const JWT_TOKEN = 'JWT_TOKEN';
1482
+ const JWT_REFRESH_TOKEN = 'JWT_REFRESH_TOKEN';
1483
+ const APP_USER = 'APP_USER';
1484
+ /**
1485
+ * Manages the *active app session* that the host patient app consumes, letting
1486
+ * the QA switch it between the fixed admin (the DevTools identity) and any saved
1487
+ * generated user — without re-OTP. The DevTools' own API calls do NOT use this;
1488
+ * they always use the admin token via the package interceptor.
1489
+ */
1490
+ class DevtoolsSessionService {
1491
+ constructor() {
1492
+ this.storage = inject(DevtoolsStorage);
1493
+ this.auth = inject(DevtoolsAuthService);
1494
+ this._sessions = signal(this.storage.get(SESSIONS_KEY) ?? [], ...(ngDevMode ? [{ debugName: "_sessions" }] : []));
1495
+ this.sessions = this._sessions.asReadonly();
1496
+ this.activeId = signal(this.storage.get(ACTIVE_KEY), ...(ngDevMode ? [{ debugName: "activeId" }] : []));
1497
+ /** Admin session derived from the DevTools identity (auth). */
1498
+ this.admin = computed(() => {
1499
+ const token = this.auth.adminToken();
1500
+ if (!token)
1501
+ return null;
1502
+ return {
1503
+ id: 'admin',
1504
+ kind: 'admin',
1505
+ name: this.auth.adminProfile()?.name || this.auth.adminProfile()?.email || 'Admin (DevTools)',
1506
+ accessToken: token,
1507
+ refreshToken: this.auth.adminRefreshToken() ?? '',
1508
+ profile: this.auth.adminProfile(),
1509
+ };
1510
+ }, ...(ngDevMode ? [{ debugName: "admin" }] : []));
1511
+ }
1512
+ saveTestSession(session) {
1513
+ const full = { ...session, id: session.document, kind: 'test' };
1514
+ const next = [full, ...this._sessions().filter((s) => s.id !== full.id)].slice(0, 20);
1515
+ this._sessions.set(next);
1516
+ this.storage.set(SESSIONS_KEY, next);
1517
+ return full;
1518
+ }
1519
+ /** Write a stored session into the host's active-session keys. */
1520
+ switchTo(id) {
1521
+ const session = id === 'admin' ? this.admin() : this._sessions().find((s) => s.id === id) ?? null;
1522
+ if (!session)
1523
+ return null;
1524
+ this.storage.set(JWT_TOKEN, session.accessToken);
1525
+ this.storage.set(JWT_REFRESH_TOKEN, session.refreshToken);
1526
+ this.storage.set(APP_USER, session.profile ?? null);
1527
+ this.activeId.set(id);
1528
+ this.storage.set(ACTIVE_KEY, id);
1529
+ return session;
1530
+ }
1531
+ removeSession(id) {
1532
+ const next = this._sessions().filter((s) => s.id !== id);
1533
+ this._sessions.set(next);
1534
+ this.storage.set(SESSIONS_KEY, next);
1535
+ if (this.activeId() === id && this.admin())
1536
+ this.switchTo('admin');
1537
+ }
1538
+ active() {
1539
+ const id = this.activeId();
1540
+ if (!id)
1541
+ return null;
1542
+ return id === 'admin' ? this.admin() : this._sessions().find((s) => s.id === id) ?? null;
1543
+ }
1544
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsSessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1545
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsSessionService }); }
1546
+ }
1547
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsSessionService, decorators: [{
1548
+ type: Injectable
1549
+ }] });
1550
+
1551
+ /**
1552
+ * Shared inline-style objects for the DevTools domain panels. Bound via
1553
+ * `[style]="UI.x"` so there are no view-encapsulation surprises when the panels
1554
+ * are rendered inside the shell via NgComponentOutlet. Colours follow the
1555
+ * shell's dark theme (the `--dts-*` custom properties).
1556
+ */
1557
+ const UI = {
1558
+ intro: {
1559
+ fontSize: '13px',
1560
+ color: '#9a9eab',
1561
+ lineHeight: '1.5',
1562
+ margin: '0 0 16px',
1563
+ },
1564
+ field: {
1565
+ border: '1px solid #2a2c38',
1566
+ borderRadius: '11px',
1567
+ background: '#16181f',
1568
+ padding: '8px 13px',
1569
+ marginBottom: '12px',
1570
+ },
1571
+ label: {
1572
+ fontSize: '10.5px',
1573
+ fontFamily: "'JetBrains Mono',monospace",
1574
+ letterSpacing: '.06em',
1575
+ color: '#7a7e8b',
1576
+ marginBottom: '2px',
1577
+ textTransform: 'uppercase',
1578
+ },
1579
+ input: {
1580
+ width: '100%',
1581
+ border: 'none',
1582
+ background: 'transparent',
1583
+ color: '#eceef3',
1584
+ fontSize: '14.5px',
1585
+ outline: 'none',
1586
+ padding: '2px 0',
1587
+ },
1588
+ primary: {
1589
+ width: '100%',
1590
+ marginTop: '8px',
1591
+ padding: '14px',
1592
+ borderRadius: '12px',
1593
+ border: 'none',
1594
+ background: '#ffc454',
1595
+ color: '#1a1b22',
1596
+ fontSize: '14.5px',
1597
+ fontWeight: '700',
1598
+ cursor: 'pointer',
1599
+ display: 'flex',
1600
+ alignItems: 'center',
1601
+ justifyContent: 'center',
1602
+ gap: '9px',
1603
+ },
1604
+ ghost: {
1605
+ width: '100%',
1606
+ marginTop: '10px',
1607
+ padding: '12px',
1608
+ borderRadius: '12px',
1609
+ border: '1px solid #2a2c38',
1610
+ background: 'transparent',
1611
+ color: '#8b8f9c',
1612
+ fontSize: '13px',
1613
+ cursor: 'pointer',
1614
+ },
1615
+ card: {
1616
+ background: '#16181f',
1617
+ border: '1px solid #24262f',
1618
+ borderRadius: '12px',
1619
+ padding: '13px 15px',
1620
+ marginBottom: '9px',
1621
+ },
1622
+ row: {
1623
+ display: 'flex',
1624
+ alignItems: 'center',
1625
+ gap: '12px',
1626
+ width: '100%',
1627
+ textAlign: 'left',
1628
+ background: '#16181f',
1629
+ border: '1px solid #24262f',
1630
+ borderRadius: '12px',
1631
+ padding: '13px 15px',
1632
+ marginBottom: '10px',
1633
+ color: '#eceef3',
1634
+ cursor: 'pointer',
1635
+ },
1636
+ mono: {
1637
+ fontFamily: "'JetBrains Mono',monospace",
1638
+ fontSize: '11.5px',
1639
+ color: '#8b8f9c',
1640
+ },
1641
+ pre: {
1642
+ fontFamily: "'JetBrains Mono',monospace",
1643
+ fontSize: '12px',
1644
+ lineHeight: '1.7',
1645
+ color: '#c4c7d0',
1646
+ whiteSpace: 'pre-wrap',
1647
+ wordBreak: 'break-all',
1648
+ margin: '0',
1649
+ background: '#16181f',
1650
+ border: '1px solid #24262f',
1651
+ borderRadius: '10px',
1652
+ padding: '12px',
1653
+ },
1654
+ };
1655
+
1656
+ /**
1657
+ * DevTools admin login (Google OAuth, handled inside the package). The admin is
1658
+ * the package's own fixed identity — its tokens are stored separately and used
1659
+ * by the package interceptor for all DevTools calls.
1660
+ */
1661
+ class AdminLoginPanelComponent {
1662
+ constructor() {
1663
+ this.UI = UI;
1664
+ this.auth = inject(DevtoolsAuthService);
1665
+ this.session = inject(DevtoolsSessionService);
1666
+ this.toast = inject(DevtoolsToastService);
1667
+ }
1668
+ short(token) {
1669
+ return token && token.length > 42 ? token.slice(0, 42) + '…' : token ?? '';
1670
+ }
1671
+ activateAdmin() {
1672
+ this.session.switchTo('admin');
1673
+ this.toast.show('Sessão admin ativada no app');
1674
+ }
1675
+ logout() {
1676
+ this.auth.logout();
1677
+ this.toast.show('DevTools deslogado');
1678
+ }
1679
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminLoginPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1680
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: AdminLoginPanelComponent, isStandalone: true, selector: "devtools-admin-login-panel", ngImport: i0, template: `
1681
+ @if (auth.logged()) {
1682
+ <div [style]="UI.card">
1683
+ <div style="display:flex;align-items:center;gap:8px;color:#1fbe7e;font-size:13px;font-weight:600">
1684
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px">shield_person</span>
1685
+ DevTools logado (admin)
1686
+ </div>
1687
+ <div [style]="UI.mono" style="margin-top:8px;word-break:break-all">{{ short(auth.adminToken()) }}</div>
1688
+ </div>
1689
+ @if (session.activeId() === 'admin') {
1690
+ <p [style]="UI.intro" style="color:#1fbe7e">Sessão do app ativa: admin.</p>
1691
+ } @else {
1692
+ <button type="button" [style]="UI.primary" (click)="activateAdmin()">
1693
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:19px">switch_account</span>
1694
+ Ativar sessão admin no app
1695
+ </button>
1696
+ }
1697
+ <button type="button" [style]="UI.ghost" (click)="auth.loginGoogleRedirect()">Refazer login Google</button>
1698
+ <button type="button" [style]="UI.ghost" (click)="logout()">Sair (logout DevTools)</button>
1699
+ } @else {
1700
+ <p [style]="UI.intro">
1701
+ O DevTools tem login próprio (admin). Entre com o Google para liberar as
1702
+ ferramentas — o acesso fica guardado separado do app.
1703
+ </p>
1704
+ <button type="button" [style]="UI.primary" (click)="auth.loginGoogleRedirect()">
1705
+ <span style="width:20px;height:20px;border-radius:50%;background:#23242b;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;font-family:'JetBrains Mono',monospace">G</span>
1706
+ Entrar com Google (admin)
1707
+ </button>
1708
+ }
1709
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1710
+ }
1711
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AdminLoginPanelComponent, decorators: [{
1712
+ type: Component,
1713
+ args: [{
1714
+ selector: 'devtools-admin-login-panel',
1715
+ standalone: true,
1716
+ changeDetection: ChangeDetectionStrategy.OnPush,
1717
+ template: `
1718
+ @if (auth.logged()) {
1719
+ <div [style]="UI.card">
1720
+ <div style="display:flex;align-items:center;gap:8px;color:#1fbe7e;font-size:13px;font-weight:600">
1721
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px">shield_person</span>
1722
+ DevTools logado (admin)
1723
+ </div>
1724
+ <div [style]="UI.mono" style="margin-top:8px;word-break:break-all">{{ short(auth.adminToken()) }}</div>
1725
+ </div>
1726
+ @if (session.activeId() === 'admin') {
1727
+ <p [style]="UI.intro" style="color:#1fbe7e">Sessão do app ativa: admin.</p>
1728
+ } @else {
1729
+ <button type="button" [style]="UI.primary" (click)="activateAdmin()">
1730
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:19px">switch_account</span>
1731
+ Ativar sessão admin no app
1732
+ </button>
1733
+ }
1734
+ <button type="button" [style]="UI.ghost" (click)="auth.loginGoogleRedirect()">Refazer login Google</button>
1735
+ <button type="button" [style]="UI.ghost" (click)="logout()">Sair (logout DevTools)</button>
1736
+ } @else {
1737
+ <p [style]="UI.intro">
1738
+ O DevTools tem login próprio (admin). Entre com o Google para liberar as
1739
+ ferramentas — o acesso fica guardado separado do app.
1740
+ </p>
1741
+ <button type="button" [style]="UI.primary" (click)="auth.loginGoogleRedirect()">
1742
+ <span style="width:20px;height:20px;border-radius:50%;background:#23242b;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;font-family:'JetBrains Mono',monospace">G</span>
1743
+ Entrar com Google (admin)
1744
+ </button>
1745
+ }
1746
+ `,
1747
+ }]
1748
+ }] });
1749
+
1750
+ /** App login for a generated user via CPF + OTP. Saves + switches the session. */
1751
+ class LoginPanelComponent {
1752
+ constructor() {
1753
+ this.UI = UI;
1754
+ this.api = inject(DevtoolsApiService);
1755
+ this.session = inject(DevtoolsSessionService);
1756
+ this.userUtils = inject(DevtoolsUserUtils);
1757
+ this.utils = inject(DevtoolsUtils);
1758
+ this.toast = inject(DevtoolsToastService);
1759
+ this.cdr = inject(ChangeDetectorRef);
1760
+ this.step = 'cpf';
1761
+ this.busy = false;
1762
+ this.message = '';
1763
+ this.cpf = '';
1764
+ this.name = '';
1765
+ this.birthdate = '';
1766
+ this.biologicalSex = 'MALE';
1767
+ this.gender = 'MALE';
1768
+ this.phone = '';
1769
+ this.code = '';
1770
+ this.registration = null;
1771
+ this.doneName = '';
1772
+ const [last] = this.userUtils.getUserFromStorage();
1773
+ if (last?.document)
1774
+ this.cpf = this.utils.maskCpfCnpj(last.document);
1775
+ }
1776
+ get media() {
1777
+ return this.api.media;
1778
+ }
1779
+ checkCpf() {
1780
+ const document = this.cpf.replace(/\D/g, '');
1781
+ if (document.length !== 11)
1782
+ return this.fail('CPF inválido — informe 11 dígitos.');
1783
+ this.start('Verificando CPF…');
1784
+ this.api.checkRegistration(document).subscribe({
1785
+ next: (reg) => {
1786
+ this.registration = reg;
1787
+ this.busy = false;
1788
+ this.message = '';
1789
+ this.step = reg.hasDocument ? 'phone' : 'signup';
1790
+ this.cdr.markForCheck();
1791
+ },
1792
+ error: (err) => this.failFromHttp(err, 'Falha ao verificar o CPF.'),
1793
+ });
1794
+ }
1795
+ sendCode(isSignup) {
1796
+ const phoneE164 = this.toE164(this.phone);
1797
+ if (!/^\+55\d{10,11}$/.test(phoneE164))
1798
+ return this.fail('Celular inválido — use DDD + número.');
1799
+ let personProperties;
1800
+ if (isSignup) {
1801
+ if (!this.name.trim())
1802
+ return this.fail('Informe o nome.');
1803
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(this.birthdate))
1804
+ return this.fail('Data de nascimento inválida (YYYY-MM-DD).');
1805
+ personProperties = { name: this.name.trim(), birthdate: this.birthdate, biologicalSex: this.biologicalSex, gender: this.gender };
1806
+ }
1807
+ this.start('Enviando código…');
1808
+ this.api.requestCode({ document: this.cpf, phone: phoneE164, personProperties }).subscribe({
1809
+ next: () => {
1810
+ this.busy = false;
1811
+ this.message = '';
1812
+ this.step = 'code';
1813
+ this.toast.show('Código enviado via ' + this.media);
1814
+ this.cdr.markForCheck();
1815
+ },
1816
+ error: (err) => this.failFromHttp(err, 'Falha ao enviar o código.'),
1817
+ });
1818
+ }
1819
+ resend() {
1820
+ this.sendCode(!!this.registration && !this.registration.hasDocument);
1821
+ }
1822
+ confirm() {
1823
+ if (!/^\d{6}$/.test(this.code))
1824
+ return this.fail('Código inválido — 6 dígitos.');
1825
+ const document = this.cpf.replace(/\D/g, '');
1826
+ this.start('Entrando…');
1827
+ this.api.otpLogin(document, this.code).subscribe({
1828
+ next: (res) => {
1829
+ const profile = res.profile;
1830
+ const name = profile?.personProperties?.name || profile?.name || this.name.trim() || this.fmtCpf(document);
1831
+ const saved = this.session.saveTestSession({
1832
+ name,
1833
+ document,
1834
+ accessToken: res.accessToken,
1835
+ refreshToken: res.refreshToken,
1836
+ profile,
1837
+ });
1838
+ this.session.switchTo(saved.id);
1839
+ this.doneName = name;
1840
+ this.busy = false;
1841
+ this.message = '';
1842
+ this.step = 'done';
1843
+ this.toast.show('Login efetuado · sessão do app ativada');
1844
+ this.cdr.markForCheck();
1845
+ },
1846
+ error: (err) => this.failFromHttp(err, 'Código incorreto ou expirado.'),
1847
+ });
1848
+ }
1849
+ reset() {
1850
+ this.step = 'cpf';
1851
+ this.code = '';
1852
+ this.message = '';
1853
+ this.cdr.markForCheck();
1854
+ }
1855
+ toE164(phone) {
1856
+ const digits = (phone || '').replace(/\D/g, '');
1857
+ if (!digits)
1858
+ return '';
1859
+ return digits.startsWith('55') ? `+${digits}` : `+55${digits}`;
1860
+ }
1861
+ fmtCpf(d) {
1862
+ return (d || '').replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
1863
+ }
1864
+ start(msg) {
1865
+ this.busy = true;
1866
+ this.message = msg;
1867
+ this.cdr.markForCheck();
1868
+ }
1869
+ fail(msg) {
1870
+ this.busy = false;
1871
+ this.message = msg;
1872
+ this.cdr.markForCheck();
1873
+ }
1874
+ failFromHttp(err, fallback) {
1875
+ this.busy = false;
1876
+ this.message = err?.error?.message || fallback;
1877
+ this.utils.setErrorToast(err);
1878
+ this.cdr.markForCheck();
1879
+ }
1880
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LoginPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1881
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: LoginPanelComponent, isStandalone: true, selector: "devtools-login-panel", ngImport: i0, template: `
1882
+ @if (step === 'cpf') {
1883
+ <p [style]="UI.intro">Login no app do paciente com um CPF gerado. O código de 6 dígitos chega via {{ media }}.</p>
1884
+ <div [style]="UI.field"><div [style]="UI.label">CPF gerado</div>
1885
+ <input [style]="UI.input" [(ngModel)]="cpf" placeholder="000.000.000-00" inputmode="numeric" /></div>
1886
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="checkCpf()">{{ busy ? 'Verificando…' : 'Continuar' }}</button>
1887
+ }
1888
+
1889
+ @if (step === 'signup') {
1890
+ <p [style]="UI.intro">CPF novo — preencha os dados para criar a conta no app.</p>
1891
+ <div [style]="UI.field"><div [style]="UI.label">Nome completo</div><input [style]="UI.input" [(ngModel)]="name" placeholder="João Silva" /></div>
1892
+ <div [style]="UI.field"><div [style]="UI.label">Data de nascimento</div><input [style]="UI.input" type="date" [(ngModel)]="birthdate" /></div>
1893
+ <div [style]="UI.field"><div [style]="UI.label">Sexo biológico</div>
1894
+ <select [style]="UI.input" [(ngModel)]="biologicalSex"><option value="MALE">MALE</option><option value="FEMALE">FEMALE</option></select></div>
1895
+ <div [style]="UI.field"><div [style]="UI.label">Gênero</div>
1896
+ <select [style]="UI.input" [(ngModel)]="gender"><option value="MALE">MALE</option><option value="FEMALE">FEMALE</option><option value="NON_BINARY">NON_BINARY</option><option value="OTHER">OTHER</option></select></div>
1897
+ <div [style]="UI.field"><div [style]="UI.label">Celular</div><input [style]="UI.input" [(ngModel)]="phone" placeholder="(11) 98765-4321" inputmode="tel" /></div>
1898
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="sendCode(true)">{{ busy ? 'Enviando…' : 'Enviar código' }}</button>
1899
+ }
1900
+
1901
+ @if (step === 'phone') {
1902
+ <p [style]="UI.intro">
1903
+ CPF encontrado. Código para o celular
1904
+ @if (registration?.phoneAreaCode || registration?.phoneEnding) {
1905
+ <strong style="color:#eceef3">({{ registration?.phoneAreaCode }}) •••••-{{ registration?.phoneEnding }}</strong>.
1906
+ }
1907
+ Confirme o número completo.
1908
+ </p>
1909
+ <div [style]="UI.field"><div [style]="UI.label">Celular</div><input [style]="UI.input" [(ngModel)]="phone" placeholder="(11) 98765-4321" inputmode="tel" /></div>
1910
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="sendCode(false)">{{ busy ? 'Enviando…' : 'Enviar código' }}</button>
1911
+ }
1912
+
1913
+ @if (step === 'code') {
1914
+ <p [style]="UI.intro">Digite o código de 6 dígitos enviado via {{ media }}.</p>
1915
+ <div [style]="UI.field"><div [style]="UI.label">Código</div>
1916
+ <input [style]="UI.input" [(ngModel)]="code" placeholder="000000" inputmode="numeric" maxlength="6"
1917
+ style="font-family:'JetBrains Mono',monospace;letter-spacing:.4em;font-size:20px" /></div>
1918
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="confirm()">{{ busy ? 'Entrando…' : 'Entrar' }}</button>
1919
+ <button type="button" [style]="UI.ghost" [disabled]="busy" (click)="resend()">Reenviar código</button>
1920
+ }
1921
+
1922
+ @if (step === 'done') {
1923
+ <div [style]="UI.card">
1924
+ <div style="display:flex;align-items:center;gap:8px;color:#1fbe7e;font-size:13px;font-weight:600">
1925
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px">check_circle</span>
1926
+ Logado como {{ doneName }}
1927
+ </div>
1928
+ <div [style]="UI.mono" style="margin-top:6px">Sessão do app ativada e salva.</div>
1929
+ </div>
1930
+ <button type="button" [style]="UI.ghost" (click)="reset()">Entrar com outro CPF</button>
1931
+ }
1932
+
1933
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#d6604f">{{ message }}</div> }
1934
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1935
+ }
1936
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LoginPanelComponent, decorators: [{
1937
+ type: Component,
1938
+ args: [{
1939
+ selector: 'devtools-login-panel',
1940
+ standalone: true,
1941
+ changeDetection: ChangeDetectionStrategy.OnPush,
1942
+ imports: [FormsModule],
1943
+ template: `
1944
+ @if (step === 'cpf') {
1945
+ <p [style]="UI.intro">Login no app do paciente com um CPF gerado. O código de 6 dígitos chega via {{ media }}.</p>
1946
+ <div [style]="UI.field"><div [style]="UI.label">CPF gerado</div>
1947
+ <input [style]="UI.input" [(ngModel)]="cpf" placeholder="000.000.000-00" inputmode="numeric" /></div>
1948
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="checkCpf()">{{ busy ? 'Verificando…' : 'Continuar' }}</button>
1949
+ }
1950
+
1951
+ @if (step === 'signup') {
1952
+ <p [style]="UI.intro">CPF novo — preencha os dados para criar a conta no app.</p>
1953
+ <div [style]="UI.field"><div [style]="UI.label">Nome completo</div><input [style]="UI.input" [(ngModel)]="name" placeholder="João Silva" /></div>
1954
+ <div [style]="UI.field"><div [style]="UI.label">Data de nascimento</div><input [style]="UI.input" type="date" [(ngModel)]="birthdate" /></div>
1955
+ <div [style]="UI.field"><div [style]="UI.label">Sexo biológico</div>
1956
+ <select [style]="UI.input" [(ngModel)]="biologicalSex"><option value="MALE">MALE</option><option value="FEMALE">FEMALE</option></select></div>
1957
+ <div [style]="UI.field"><div [style]="UI.label">Gênero</div>
1958
+ <select [style]="UI.input" [(ngModel)]="gender"><option value="MALE">MALE</option><option value="FEMALE">FEMALE</option><option value="NON_BINARY">NON_BINARY</option><option value="OTHER">OTHER</option></select></div>
1959
+ <div [style]="UI.field"><div [style]="UI.label">Celular</div><input [style]="UI.input" [(ngModel)]="phone" placeholder="(11) 98765-4321" inputmode="tel" /></div>
1960
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="sendCode(true)">{{ busy ? 'Enviando…' : 'Enviar código' }}</button>
1961
+ }
1962
+
1963
+ @if (step === 'phone') {
1964
+ <p [style]="UI.intro">
1965
+ CPF encontrado. Código para o celular
1966
+ @if (registration?.phoneAreaCode || registration?.phoneEnding) {
1967
+ <strong style="color:#eceef3">({{ registration?.phoneAreaCode }}) •••••-{{ registration?.phoneEnding }}</strong>.
1968
+ }
1969
+ Confirme o número completo.
1970
+ </p>
1971
+ <div [style]="UI.field"><div [style]="UI.label">Celular</div><input [style]="UI.input" [(ngModel)]="phone" placeholder="(11) 98765-4321" inputmode="tel" /></div>
1972
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="sendCode(false)">{{ busy ? 'Enviando…' : 'Enviar código' }}</button>
1973
+ }
1974
+
1975
+ @if (step === 'code') {
1976
+ <p [style]="UI.intro">Digite o código de 6 dígitos enviado via {{ media }}.</p>
1977
+ <div [style]="UI.field"><div [style]="UI.label">Código</div>
1978
+ <input [style]="UI.input" [(ngModel)]="code" placeholder="000000" inputmode="numeric" maxlength="6"
1979
+ style="font-family:'JetBrains Mono',monospace;letter-spacing:.4em;font-size:20px" /></div>
1980
+ <button type="button" [style]="UI.primary" [disabled]="busy" (click)="confirm()">{{ busy ? 'Entrando…' : 'Entrar' }}</button>
1981
+ <button type="button" [style]="UI.ghost" [disabled]="busy" (click)="resend()">Reenviar código</button>
1982
+ }
1983
+
1984
+ @if (step === 'done') {
1985
+ <div [style]="UI.card">
1986
+ <div style="display:flex;align-items:center;gap:8px;color:#1fbe7e;font-size:13px;font-weight:600">
1987
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px">check_circle</span>
1988
+ Logado como {{ doneName }}
1989
+ </div>
1990
+ <div [style]="UI.mono" style="margin-top:6px">Sessão do app ativada e salva.</div>
1991
+ </div>
1992
+ <button type="button" [style]="UI.ghost" (click)="reset()">Entrar com outro CPF</button>
1993
+ }
1994
+
1995
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#d6604f">{{ message }}</div> }
1996
+ `,
1997
+ }]
1998
+ }], ctorParameters: () => [] });
1999
+
2000
+ /** Switch the active app session between admin and saved generated users. */
2001
+ class SwitchPanelComponent {
2002
+ constructor() {
2003
+ this.UI = UI;
2004
+ this.session = inject(DevtoolsSessionService);
2005
+ this.toast = inject(DevtoolsToastService);
2006
+ }
2007
+ rows() {
2008
+ const admin = this.session.admin();
2009
+ return [...(admin ? [admin] : []), ...this.session.sessions()];
2010
+ }
2011
+ active() {
2012
+ return this.session.activeId();
2013
+ }
2014
+ switch(s) {
2015
+ this.session.switchTo(s.id);
2016
+ this.toast.show('Sessão ativa: ' + s.name);
2017
+ }
2018
+ remove(event, s) {
2019
+ event.stopPropagation();
2020
+ this.session.removeSession(s.id);
2021
+ this.toast.show('Sessão removida');
2022
+ }
2023
+ fmtCpf(d) {
2024
+ return (d || '').replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
2025
+ }
2026
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SwitchPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2027
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: SwitchPanelComponent, isStandalone: true, selector: "devtools-switch-panel", ngImport: i0, template: `
2028
+ <p [style]="UI.intro">
2029
+ Troque a sessão ativa do app. O admin fica sempre guardado; os usuários
2030
+ gerados são salvos para evitar refazer o OTP.
2031
+ </p>
2032
+ @for (s of rows(); track s.id) {
2033
+ <button type="button" [style]="UI.row" (click)="switch(s)">
2034
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:20px"
2035
+ [style.color]="s.kind === 'admin' ? '#ffc454' : '#5b8cff'">
2036
+ {{ s.kind === 'admin' ? 'shield_person' : 'person' }}
2037
+ </span>
2038
+ <div style="flex:1;min-width:0">
2039
+ <div style="font-size:14px;font-weight:600;color:#eceef3">{{ s.name }}</div>
2040
+ <div [style]="UI.mono">{{ s.document ? fmtCpf(s.document) : 'DevTools' }}</div>
2041
+ </div>
2042
+ @if (active() === s.id) {
2043
+ <span style="font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;color:#1fbe7e;background:#10241b;border:1px solid #1e4634;border-radius:6px;padding:4px 8px">ATIVO</span>
2044
+ } @else if (s.kind === 'test') {
2045
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px;color:#6a6e7b" (click)="remove($event, s)">delete</span>
2046
+ }
2047
+ </button>
2048
+ } @empty {
2049
+ <p [style]="UI.intro">Nenhuma sessão salva. Faça login admin e gere usuários.</p>
2050
+ }
2051
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2052
+ }
2053
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SwitchPanelComponent, decorators: [{
2054
+ type: Component,
2055
+ args: [{
2056
+ selector: 'devtools-switch-panel',
2057
+ standalone: true,
2058
+ changeDetection: ChangeDetectionStrategy.OnPush,
2059
+ template: `
2060
+ <p [style]="UI.intro">
2061
+ Troque a sessão ativa do app. O admin fica sempre guardado; os usuários
2062
+ gerados são salvos para evitar refazer o OTP.
2063
+ </p>
2064
+ @for (s of rows(); track s.id) {
2065
+ <button type="button" [style]="UI.row" (click)="switch(s)">
2066
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:20px"
2067
+ [style.color]="s.kind === 'admin' ? '#ffc454' : '#5b8cff'">
2068
+ {{ s.kind === 'admin' ? 'shield_person' : 'person' }}
2069
+ </span>
2070
+ <div style="flex:1;min-width:0">
2071
+ <div style="font-size:14px;font-weight:600;color:#eceef3">{{ s.name }}</div>
2072
+ <div [style]="UI.mono">{{ s.document ? fmtCpf(s.document) : 'DevTools' }}</div>
2073
+ </div>
2074
+ @if (active() === s.id) {
2075
+ <span style="font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;color:#1fbe7e;background:#10241b;border:1px solid #1e4634;border-radius:6px;padding:4px 8px">ATIVO</span>
2076
+ } @else if (s.kind === 'test') {
2077
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px;color:#6a6e7b" (click)="remove($event, s)">delete</span>
2078
+ }
2079
+ </button>
2080
+ } @empty {
2081
+ <p [style]="UI.intro">Nenhuma sessão salva. Faça login admin e gere usuários.</p>
2082
+ }
2083
+ `,
2084
+ }]
2085
+ }] });
2086
+
2087
+ /** Real user generator (manual + automatic) + post-run OTP login. */
2088
+ class GeneratorPanelComponent {
2089
+ constructor() {
2090
+ this.UI = UI;
2091
+ this.api = inject(DevtoolsApiService);
2092
+ this.userUtils = inject(DevtoolsUserUtils);
2093
+ this.utils = inject(DevtoolsUtils);
2094
+ this.auth = inject(DevtoolsAuthService);
2095
+ this.session = inject(DevtoolsSessionService);
2096
+ this.config = inject(DEVTOOLS_CONFIG);
2097
+ this.toast = inject(DevtoolsToastService);
2098
+ this.cdr = inject(ChangeDetectorRef);
2099
+ this.scenarios = [
2100
+ { key: 'manual', title: 'Cadastro manual', description: 'Preenche e envia formulário com dados visíveis.' },
2101
+ { key: 'auto', title: 'Cadastro automático', description: 'Cria usuário e envia formulário automático de triagem.' },
2102
+ ];
2103
+ this.responsesFormCase = OPTIONS_RESPONSES_CASE;
2104
+ this.fixedOrgId = this.config.fixedOrgId ?? 200;
2105
+ this.scenario = 'manual';
2106
+ this.removePhone = false;
2107
+ this.phone = '';
2108
+ this.responseCase = OPTIONS_RESPONSES_CASE[0];
2109
+ this.selectedOrgName = 'Mar Salt Of';
2110
+ this.customerOrganizationId = String(this.config.fixedOrgId ?? 200);
2111
+ this.gender = 'MALE';
2112
+ this.status = 'idle';
2113
+ this.executionMessage = 'Aguardando execução';
2114
+ this.dadosText = '';
2115
+ this.message = '';
2116
+ this.generatedCpf = '';
2117
+ this.generatedPhone = '';
2118
+ this.loginStep = 'idle';
2119
+ this.loginCode = '';
2120
+ this.loginBusy = false;
2121
+ }
2122
+ get media() {
2123
+ return this.api.media;
2124
+ }
2125
+ ngOnInit() {
2126
+ this.api.getWithQuery({ ids: String(this.fixedOrgId) }).subscribe({
2127
+ next: (list) => {
2128
+ const [org] = list ?? [];
2129
+ if (org)
2130
+ this.applyOrg(org);
2131
+ this.cdr.markForCheck();
2132
+ },
2133
+ error: () => { },
2134
+ });
2135
+ }
2136
+ toggleSms() {
2137
+ this.removePhone = !this.removePhone;
2138
+ if (this.removePhone && !this.phone.replace(/\D/g, ''))
2139
+ this.phone = this.adminPhone();
2140
+ }
2141
+ adminPhone() {
2142
+ const profile = this.session.admin()?.profile;
2143
+ const raw = profile?.phone || profile?.phones?.[0] || this.config.adminPhone || '';
2144
+ return String(raw).replace(/\D/g, '').replace(/^55/, '');
2145
+ }
2146
+ applyOrg(org) {
2147
+ const { id, parent, document, documentType, name } = org ?? {};
2148
+ if (id || parent) {
2149
+ this.customerOrganizationId = (parent?.id ?? id)?.toString() ?? this.customerOrganizationId;
2150
+ this.orgDocument = document;
2151
+ this.orgDocumentType = documentType;
2152
+ }
2153
+ if (name)
2154
+ this.selectedOrgName = name;
2155
+ }
2156
+ selectScenario(key) {
2157
+ this.scenario = key;
2158
+ this.status = 'idle';
2159
+ this.executionMessage = `Cenário selecionado: ${key === 'manual' ? 'Cadastro manual' : 'Cadastro automático'}`;
2160
+ if (key === 'manual' && !this.fullName) {
2161
+ this.fullName = this.userUtils.generateRandomName();
2162
+ this.document = this.userUtils.generateCPF();
2163
+ this.gender = this.userUtils.generateRandomGender();
2164
+ this.bday = this.userUtils.generateRandomBirthDate();
2165
+ }
2166
+ }
2167
+ run() {
2168
+ if (!this.auth.isLogged()) {
2169
+ this.toast.show('Faça login admin para gerar usuários');
2170
+ return;
2171
+ }
2172
+ const isManual = this.scenario === 'manual';
2173
+ const fullName = (this.fullName && this.fullName.trim()) || this.userUtils.generateRandomName();
2174
+ const document = (this.document && this.document.replace(/\D/g, '')) || this.userUtils.generateCPF();
2175
+ if (!isManual) {
2176
+ this.fullName = fullName;
2177
+ this.document = document;
2178
+ }
2179
+ const payload = {
2180
+ fullName,
2181
+ document,
2182
+ biologicalSex: isManual ? this.gender : this.responseCase.biologicalSex,
2183
+ birthdate: isManual ? (this.bday || this.userUtils.generateRandomBirthDate()) : this.userUtils.calculateBirthDate(this.responseCase.age),
2184
+ typeGenerator: isManual ? 'signup' : 'autoSignup',
2185
+ orgDocument: this.orgDocument,
2186
+ orgDocumentType: this.orgDocumentType,
2187
+ customerOrganizationId: this.customerOrganizationId,
2188
+ };
2189
+ const phoneToRelease = this.removePhone ? this.phone.replace(/\D/g, '') : undefined;
2190
+ const call = isManual ? this.api.signupForm(payload, phoneToRelease) : this.api.autoSignup(payload, phoneToRelease);
2191
+ this.status = 'running';
2192
+ this.executionMessage = 'Executando cenário…';
2193
+ this.message = '';
2194
+ this.loginStep = 'idle';
2195
+ this.cdr.markForCheck();
2196
+ call.subscribe({
2197
+ next: () => {
2198
+ this.userUtils.setToStorage(payload);
2199
+ this.status = 'success';
2200
+ this.executionMessage = 'Cenário executado com sucesso.';
2201
+ this.generatedCpf = document;
2202
+ this.generatedPhone = phoneToRelease ?? this.phone.replace(/\D/g, '');
2203
+ this.dadosText = this.buildDados(payload);
2204
+ this.toast.show('Usuário gerado · ' + fullName);
2205
+ this.cdr.markForCheck();
2206
+ },
2207
+ error: (err) => {
2208
+ this.status = 'error';
2209
+ this.executionMessage = 'Falha na execução do cenário.';
2210
+ this.utils.setErrorToast(err);
2211
+ this.cdr.markForCheck();
2212
+ },
2213
+ });
2214
+ }
2215
+ startLogin() {
2216
+ const phoneE164 = this.toE164(this.generatedPhone || this.phone);
2217
+ if (!/^\+55\d{10,11}$/.test(phoneE164)) {
2218
+ this.message = 'Defina o celular do usuário (toggle SMS) para receber o código.';
2219
+ this.cdr.markForCheck();
2220
+ return;
2221
+ }
2222
+ this.loginBusy = true;
2223
+ this.message = '';
2224
+ this.cdr.markForCheck();
2225
+ this.api.requestCode({ document: this.generatedCpf, phone: phoneE164 }).subscribe({
2226
+ next: () => {
2227
+ this.loginBusy = false;
2228
+ this.loginStep = 'code';
2229
+ this.toast.show('Código enviado via ' + this.media);
2230
+ this.cdr.markForCheck();
2231
+ },
2232
+ error: (err) => {
2233
+ this.loginBusy = false;
2234
+ this.message = err?.error?.message || 'Falha ao enviar o código.';
2235
+ this.cdr.markForCheck();
2236
+ },
2237
+ });
2238
+ }
2239
+ confirmLogin() {
2240
+ if (!/^\d{6}$/.test(this.loginCode)) {
2241
+ this.message = 'Código inválido — 6 dígitos.';
2242
+ this.cdr.markForCheck();
2243
+ return;
2244
+ }
2245
+ this.loginBusy = true;
2246
+ this.cdr.markForCheck();
2247
+ this.api.otpLogin(this.generatedCpf, this.loginCode).subscribe({
2248
+ next: (res) => {
2249
+ const profile = res.profile;
2250
+ const name = profile?.personProperties?.name || profile?.name || this.fullName || this.generatedCpf;
2251
+ const saved = this.session.saveTestSession({
2252
+ name,
2253
+ document: this.generatedCpf,
2254
+ accessToken: res.accessToken,
2255
+ refreshToken: res.refreshToken,
2256
+ profile,
2257
+ });
2258
+ this.session.switchTo(saved.id);
2259
+ this.loginBusy = false;
2260
+ this.loginStep = 'done';
2261
+ this.toast.show('Login efetuado · sessão do app ativada');
2262
+ this.cdr.markForCheck();
2263
+ },
2264
+ error: (err) => {
2265
+ this.loginBusy = false;
2266
+ this.message = err?.error?.message || 'Código incorreto ou expirado.';
2267
+ this.cdr.markForCheck();
2268
+ },
2269
+ });
2270
+ }
2271
+ toE164(phone) {
2272
+ const digits = (phone || '').replace(/\D/g, '');
2273
+ if (!digits)
2274
+ return '';
2275
+ return digits.startsWith('55') ? `+${digits}` : `+55${digits}`;
2276
+ }
2277
+ buildDados(u) {
2278
+ const age = this.userUtils.calculateAge(u.birthdate);
2279
+ return `Documento: ${u.document}\nNome: ${u.fullName}\nSexo biológico: ${u.biologicalSex}\nNascimento (idade): ${u.birthdate} (${age})\nTipo: ${u.typeGenerator}`;
2280
+ }
2281
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GeneratorPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2282
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: GeneratorPanelComponent, isStandalone: true, selector: "devtools-generator-panel", ngImport: i0, template: `
2283
+ <div [style]="UI.label">Cenários</div>
2284
+ @for (card of scenarios; track card.key) {
2285
+ <button type="button" [style]="UI.card"
2286
+ [style.borderColor]="scenario === card.key ? '#4b4ea8' : '#24262f'"
2287
+ [style.background]="scenario === card.key ? '#1a1b2e' : '#16181f'"
2288
+ style="width:100%;text-align:left;cursor:pointer" (click)="selectScenario(card.key)">
2289
+ <div style="font-size:14px;font-weight:600" [style.color]="scenario === card.key ? '#bcbdff' : '#cdd0d9'">{{ card.title }}</div>
2290
+ <div style="font-size:12px;color:#8b8f9c;margin-top:3px;line-height:1.4">{{ card.description }}</div>
2291
+ </button>
2292
+ }
2293
+
2294
+ <label style="display:flex;align-items:center;gap:12px;margin:16px 0;cursor:pointer">
2295
+ <button type="button" (click)="toggleSms()"
2296
+ [style.background]="removePhone ? '#ffc454' : '#33353f'"
2297
+ style="width:44px;height:26px;border-radius:999px;border:none;padding:3px;display:flex;flex:none"
2298
+ [style.justifyContent]="removePhone ? 'flex-end' : 'flex-start'">
2299
+ <span style="width:20px;height:20px;border-radius:50%;background:#fff;display:block"></span>
2300
+ </button>
2301
+ <span style="font-size:13.5px;color:#cdd0d9">Liberar meu celular para testes SMS</span>
2302
+ </label>
2303
+ @if (removePhone) {
2304
+ <div [style]="UI.field"><div [style]="UI.label">Celular (será liberado no cadastro)</div>
2305
+ <input [style]="UI.input" [(ngModel)]="phone" placeholder="11985024625" inputmode="tel" /></div>
2306
+ }
2307
+
2308
+ @if (scenario === 'auto') {
2309
+ <div [style]="UI.field"><div [style]="UI.label">Tipo de formulário</div>
2310
+ <select [style]="UI.input" [(ngModel)]="responseCase">
2311
+ @for (c of responsesFormCase; track c.value) { <option [ngValue]="c">{{ c.name }}</option> }
2312
+ </select></div>
2313
+ }
2314
+
2315
+ <div [style]="UI.field"><div [style]="UI.label">Empresa do tipo RH (fixa)</div>
2316
+ <div style="color:#eceef3;font-size:14px;padding:2px 0">{{ selectedOrgName }}</div></div>
2317
+
2318
+ @if (scenario === 'manual') {
2319
+ <div [style]="UI.field"><div [style]="UI.label">Nome</div><input [style]="UI.input" [(ngModel)]="fullName" placeholder="Auto se vazio" /></div>
2320
+ <div [style]="UI.field"><div [style]="UI.label">CPF</div><input [style]="UI.input" [(ngModel)]="document" placeholder="Auto se vazio" /></div>
2321
+ <div [style]="UI.field"><div [style]="UI.label">Sexo Biológico *</div>
2322
+ <select [style]="UI.input" [(ngModel)]="gender"><option value="MALE">Masculino</option><option value="FEMALE">Feminino</option></select></div>
2323
+ <div [style]="UI.field"><div [style]="UI.label">Nascimento (YYYY-MM-DD)</div><input [style]="UI.input" [(ngModel)]="bday" placeholder="1990-05-15" /></div>
2324
+ }
2325
+
2326
+ <div style="margin:12px 0;padding:12px 14px;background:#16181f;border:1px solid #24262f;border-radius:11px">
2327
+ <div [style]="UI.label">Execução</div>
2328
+ <div style="font-size:13px;color:#d7d9e0;margin-top:2px">{{ executionMessage }}</div>
2329
+ </div>
2330
+
2331
+ <button type="button" [style]="UI.primary" [disabled]="status === 'running'" (click)="run()">
2332
+ {{ status === 'running' ? 'Executando…' : 'Executar cenário' }}
2333
+ </button>
2334
+
2335
+ @if (generatedCpf) {
2336
+ <div style="margin-top:22px;border-top:1px solid #23252f;padding-top:18px">
2337
+ <div style="font-size:13px;font-weight:600;color:#f0f1f4;margin-bottom:10px">Usuário criado</div>
2338
+ @if (scenario === 'manual') { <pre [style]="UI.pre" style="white-space:pre-wrap">{{ dadosText }}</pre> }
2339
+ @else { <div [style]="UI.mono">Usuário automático criado (dados ocultos).</div> }
2340
+
2341
+ @if (loginStep === 'idle') {
2342
+ <button type="button" [style]="UI.primary" (click)="startLogin()">
2343
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:19px">login</span>
2344
+ Login com este usuário
2345
+ </button>
2346
+ } @else if (loginStep === 'code') {
2347
+ <p [style]="UI.intro" style="margin-top:14px">Código enviado via {{ media }} para o celular do usuário.</p>
2348
+ <div [style]="UI.field"><div [style]="UI.label">Código (6 dígitos)</div>
2349
+ <input [style]="UI.input" [(ngModel)]="loginCode" placeholder="000000" inputmode="numeric" maxlength="6"
2350
+ style="font-family:'JetBrains Mono',monospace;letter-spacing:.4em;font-size:20px" /></div>
2351
+ <button type="button" [style]="UI.primary" [disabled]="loginBusy" (click)="confirmLogin()">{{ loginBusy ? 'Entrando…' : 'Entrar' }}</button>
2352
+ } @else {
2353
+ <div [style]="UI.card" style="margin-top:14px;color:#1fbe7e">Logado e sessão do app ativada.</div>
2354
+ }
2355
+ </div>
2356
+ }
2357
+
2358
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#d6604f">{{ message }}</div> }
2359
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2360
+ }
2361
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GeneratorPanelComponent, decorators: [{
2362
+ type: Component,
2363
+ args: [{
2364
+ selector: 'devtools-generator-panel',
2365
+ standalone: true,
2366
+ changeDetection: ChangeDetectionStrategy.OnPush,
2367
+ imports: [FormsModule],
2368
+ template: `
2369
+ <div [style]="UI.label">Cenários</div>
2370
+ @for (card of scenarios; track card.key) {
2371
+ <button type="button" [style]="UI.card"
2372
+ [style.borderColor]="scenario === card.key ? '#4b4ea8' : '#24262f'"
2373
+ [style.background]="scenario === card.key ? '#1a1b2e' : '#16181f'"
2374
+ style="width:100%;text-align:left;cursor:pointer" (click)="selectScenario(card.key)">
2375
+ <div style="font-size:14px;font-weight:600" [style.color]="scenario === card.key ? '#bcbdff' : '#cdd0d9'">{{ card.title }}</div>
2376
+ <div style="font-size:12px;color:#8b8f9c;margin-top:3px;line-height:1.4">{{ card.description }}</div>
2377
+ </button>
2378
+ }
2379
+
2380
+ <label style="display:flex;align-items:center;gap:12px;margin:16px 0;cursor:pointer">
2381
+ <button type="button" (click)="toggleSms()"
2382
+ [style.background]="removePhone ? '#ffc454' : '#33353f'"
2383
+ style="width:44px;height:26px;border-radius:999px;border:none;padding:3px;display:flex;flex:none"
2384
+ [style.justifyContent]="removePhone ? 'flex-end' : 'flex-start'">
2385
+ <span style="width:20px;height:20px;border-radius:50%;background:#fff;display:block"></span>
2386
+ </button>
2387
+ <span style="font-size:13.5px;color:#cdd0d9">Liberar meu celular para testes SMS</span>
2388
+ </label>
2389
+ @if (removePhone) {
2390
+ <div [style]="UI.field"><div [style]="UI.label">Celular (será liberado no cadastro)</div>
2391
+ <input [style]="UI.input" [(ngModel)]="phone" placeholder="11985024625" inputmode="tel" /></div>
2392
+ }
2393
+
2394
+ @if (scenario === 'auto') {
2395
+ <div [style]="UI.field"><div [style]="UI.label">Tipo de formulário</div>
2396
+ <select [style]="UI.input" [(ngModel)]="responseCase">
2397
+ @for (c of responsesFormCase; track c.value) { <option [ngValue]="c">{{ c.name }}</option> }
2398
+ </select></div>
2399
+ }
2400
+
2401
+ <div [style]="UI.field"><div [style]="UI.label">Empresa do tipo RH (fixa)</div>
2402
+ <div style="color:#eceef3;font-size:14px;padding:2px 0">{{ selectedOrgName }}</div></div>
2403
+
2404
+ @if (scenario === 'manual') {
2405
+ <div [style]="UI.field"><div [style]="UI.label">Nome</div><input [style]="UI.input" [(ngModel)]="fullName" placeholder="Auto se vazio" /></div>
2406
+ <div [style]="UI.field"><div [style]="UI.label">CPF</div><input [style]="UI.input" [(ngModel)]="document" placeholder="Auto se vazio" /></div>
2407
+ <div [style]="UI.field"><div [style]="UI.label">Sexo Biológico *</div>
2408
+ <select [style]="UI.input" [(ngModel)]="gender"><option value="MALE">Masculino</option><option value="FEMALE">Feminino</option></select></div>
2409
+ <div [style]="UI.field"><div [style]="UI.label">Nascimento (YYYY-MM-DD)</div><input [style]="UI.input" [(ngModel)]="bday" placeholder="1990-05-15" /></div>
2410
+ }
2411
+
2412
+ <div style="margin:12px 0;padding:12px 14px;background:#16181f;border:1px solid #24262f;border-radius:11px">
2413
+ <div [style]="UI.label">Execução</div>
2414
+ <div style="font-size:13px;color:#d7d9e0;margin-top:2px">{{ executionMessage }}</div>
2415
+ </div>
2416
+
2417
+ <button type="button" [style]="UI.primary" [disabled]="status === 'running'" (click)="run()">
2418
+ {{ status === 'running' ? 'Executando…' : 'Executar cenário' }}
2419
+ </button>
2420
+
2421
+ @if (generatedCpf) {
2422
+ <div style="margin-top:22px;border-top:1px solid #23252f;padding-top:18px">
2423
+ <div style="font-size:13px;font-weight:600;color:#f0f1f4;margin-bottom:10px">Usuário criado</div>
2424
+ @if (scenario === 'manual') { <pre [style]="UI.pre" style="white-space:pre-wrap">{{ dadosText }}</pre> }
2425
+ @else { <div [style]="UI.mono">Usuário automático criado (dados ocultos).</div> }
2426
+
2427
+ @if (loginStep === 'idle') {
2428
+ <button type="button" [style]="UI.primary" (click)="startLogin()">
2429
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:19px">login</span>
2430
+ Login com este usuário
2431
+ </button>
2432
+ } @else if (loginStep === 'code') {
2433
+ <p [style]="UI.intro" style="margin-top:14px">Código enviado via {{ media }} para o celular do usuário.</p>
2434
+ <div [style]="UI.field"><div [style]="UI.label">Código (6 dígitos)</div>
2435
+ <input [style]="UI.input" [(ngModel)]="loginCode" placeholder="000000" inputmode="numeric" maxlength="6"
2436
+ style="font-family:'JetBrains Mono',monospace;letter-spacing:.4em;font-size:20px" /></div>
2437
+ <button type="button" [style]="UI.primary" [disabled]="loginBusy" (click)="confirmLogin()">{{ loginBusy ? 'Entrando…' : 'Entrar' }}</button>
2438
+ } @else {
2439
+ <div [style]="UI.card" style="margin-top:14px;color:#1fbe7e">Logado e sessão do app ativada.</div>
2440
+ }
2441
+ </div>
2442
+ }
2443
+
2444
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#d6604f">{{ message }}</div> }
2445
+ `,
2446
+ }]
2447
+ }] });
2448
+
2449
+ /** Release a phone number (with the 2013 conflict recovery). */
2450
+ class PhonePanelComponent {
2451
+ constructor() {
2452
+ this.UI = UI;
2453
+ this.api = inject(DevtoolsApiService);
2454
+ this.utils = inject(DevtoolsUtils);
2455
+ this.auth = inject(DevtoolsAuthService);
2456
+ this.toast = inject(DevtoolsToastService);
2457
+ this.cdr = inject(ChangeDetectorRef);
2458
+ this.phone = '';
2459
+ this.running = false;
2460
+ this.message = '';
2461
+ }
2462
+ release() {
2463
+ if (!this.auth.isLogged()) {
2464
+ this.toast.show('Faça login admin para liberar celulares');
2465
+ return;
2466
+ }
2467
+ const phone = this.utils.removePhoneMask(this.phone);
2468
+ if (!phone) {
2469
+ this.toast.show('Informe um número');
2470
+ return;
2471
+ }
2472
+ this.running = true;
2473
+ this.message = 'Liberando celular…';
2474
+ this.cdr.markForCheck();
2475
+ this.api.findRemoveUserPhone(phone).subscribe({
2476
+ next: () => this.done('Celular liberado para testes.'),
2477
+ error: (err) => {
2478
+ if (err?.error?.code === 2013 || err?.error?.code === '2013') {
2479
+ this.resolveConflictAndRetry(phone);
2480
+ return;
2481
+ }
2482
+ this.fail(err);
2483
+ },
2484
+ });
2485
+ }
2486
+ resolveConflictAndRetry(phone) {
2487
+ this.message = 'Removendo permissões vinculadas e tentando novamente…';
2488
+ this.cdr.markForCheck();
2489
+ this.api
2490
+ .findUser({ phones: `+55${phone}` })
2491
+ .pipe(map((response) => response?.persons?.[0]), switchMap((person) => {
2492
+ if (!person?.id) {
2493
+ this.fail({ error: { message: 'Usuário do número não identificado.' } });
2494
+ return EMPTY;
2495
+ }
2496
+ return this.api.getAuthStatementsByPerson(person.id).pipe(switchMap((statements) => {
2497
+ if (statements.some((s) => s?.role?.name === 'ADMIN')) {
2498
+ this.running = false;
2499
+ this.message = 'Usuário admin detectado — peça a remoção manual.';
2500
+ this.toast.show('Não é possível liberar número de admin');
2501
+ this.cdr.markForCheck();
2502
+ return EMPTY;
2503
+ }
2504
+ const ids = statements.map((s) => s?.id).filter((id) => !!id);
2505
+ return this.api.deleteAuthStatementsByIds(ids).pipe(switchMap(() => this.api.findRemoveUserPhone(phone)));
2506
+ }));
2507
+ }))
2508
+ .subscribe({
2509
+ next: () => this.done('Celular liberado após remover permissões vinculadas.'),
2510
+ error: (err) => this.fail(err),
2511
+ });
2512
+ }
2513
+ done(msg) {
2514
+ this.running = false;
2515
+ this.message = msg;
2516
+ this.toast.show('Celular liberado');
2517
+ this.cdr.markForCheck();
2518
+ }
2519
+ fail(err) {
2520
+ this.running = false;
2521
+ this.message = 'Falha ao liberar celular.';
2522
+ this.utils.setErrorToast(err);
2523
+ this.cdr.markForCheck();
2524
+ }
2525
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PhonePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2526
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: PhonePanelComponent, isStandalone: true, selector: "devtools-phone-panel", ngImport: i0, template: `
2527
+ <p [style]="UI.intro">Desvincula um número de celular para reutilizá-lo em testes de SMS / OTP.</p>
2528
+ <div [style]="UI.field"><div [style]="UI.label">Celular (DDD + número)</div>
2529
+ <input [style]="UI.input" [(ngModel)]="phone" placeholder="11990000000" inputmode="tel" /></div>
2530
+ <button type="button" [style]="UI.primary" [disabled]="running" (click)="release()">{{ running ? 'Liberando…' : 'Liberar celular' }}</button>
2531
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#9a9eab">{{ message }}</div> }
2532
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2533
+ }
2534
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PhonePanelComponent, decorators: [{
2535
+ type: Component,
2536
+ args: [{
2537
+ selector: 'devtools-phone-panel',
2538
+ standalone: true,
2539
+ changeDetection: ChangeDetectionStrategy.OnPush,
2540
+ imports: [FormsModule],
2541
+ template: `
2542
+ <p [style]="UI.intro">Desvincula um número de celular para reutilizá-lo em testes de SMS / OTP.</p>
2543
+ <div [style]="UI.field"><div [style]="UI.label">Celular (DDD + número)</div>
2544
+ <input [style]="UI.input" [(ngModel)]="phone" placeholder="11990000000" inputmode="tel" /></div>
2545
+ <button type="button" [style]="UI.primary" [disabled]="running" (click)="release()">{{ running ? 'Liberando…' : 'Liberar celular' }}</button>
2546
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#9a9eab">{{ message }}</div> }
2547
+ `,
2548
+ }]
2549
+ }] });
2550
+
2551
+ /** Generate performed exams for the last generated user (18yo flow). */
2552
+ class ExamsPanelComponent {
2553
+ constructor() {
2554
+ this.UI = UI;
2555
+ this.api = inject(DevtoolsApiService);
2556
+ this.userUtils = inject(DevtoolsUserUtils);
2557
+ this.utils = inject(DevtoolsUtils);
2558
+ this.auth = inject(DevtoolsAuthService);
2559
+ this.toast = inject(DevtoolsToastService);
2560
+ this.cdr = inject(ChangeDetectorRef);
2561
+ this.running = false;
2562
+ this.message = '';
2563
+ }
2564
+ lastUser() {
2565
+ return this.userUtils.getUserFromStorage()[0];
2566
+ }
2567
+ age(birthdate) {
2568
+ return this.userUtils.calculateAge(birthdate) || 0;
2569
+ }
2570
+ send(user) {
2571
+ if (!this.auth.isLogged()) {
2572
+ this.toast.show('Faça login admin para enviar exames');
2573
+ return;
2574
+ }
2575
+ const age = this.age(user.birthdate);
2576
+ this.running = true;
2577
+ this.message = 'Gerando exames…';
2578
+ this.cdr.markForCheck();
2579
+ this.api.generatePerformedExams(user, age, '').subscribe({
2580
+ next: (exams) => {
2581
+ this.running = false;
2582
+ this.message = exams?.length ? `Exames gerados: ${exams.length}` : 'Nenhum exame aplicável para o cenário.';
2583
+ this.toast.show(exams?.length ? 'Exames enviados' : 'Sem exames para o perfil');
2584
+ this.cdr.markForCheck();
2585
+ },
2586
+ error: (err) => {
2587
+ this.running = false;
2588
+ this.message = 'Falha ao gerar exames.';
2589
+ this.utils.setErrorToast(err);
2590
+ this.cdr.markForCheck();
2591
+ },
2592
+ });
2593
+ }
2594
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ExamsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2595
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ExamsPanelComponent, isStandalone: true, selector: "devtools-exams-panel", ngImport: i0, template: `
2596
+ @if (lastUser(); as u) {
2597
+ <p [style]="UI.intro">
2598
+ Enviar exames para o último usuário gerado
2599
+ <strong style="color:#eceef3">{{ u.fullName }}</strong>
2600
+ ({{ age(u.birthdate) }} anos).
2601
+ </p>
2602
+ <button type="button" [style]="UI.primary" [disabled]="running" (click)="send(u)">
2603
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:19px">biotech</span>
2604
+ {{ running ? 'Enviando…' : 'Enviar exames' }}
2605
+ </button>
2606
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#9a9eab">{{ message }}</div> }
2607
+ } @else {
2608
+ <p [style]="UI.intro">Nenhum usuário gerado ainda. Gere um pelo painel "Gerador".</p>
2609
+ }
2610
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2611
+ }
2612
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ExamsPanelComponent, decorators: [{
2613
+ type: Component,
2614
+ args: [{
2615
+ selector: 'devtools-exams-panel',
2616
+ standalone: true,
2617
+ changeDetection: ChangeDetectionStrategy.OnPush,
2618
+ template: `
2619
+ @if (lastUser(); as u) {
2620
+ <p [style]="UI.intro">
2621
+ Enviar exames para o último usuário gerado
2622
+ <strong style="color:#eceef3">{{ u.fullName }}</strong>
2623
+ ({{ age(u.birthdate) }} anos).
2624
+ </p>
2625
+ <button type="button" [style]="UI.primary" [disabled]="running" (click)="send(u)">
2626
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:19px">biotech</span>
2627
+ {{ running ? 'Enviando…' : 'Enviar exames' }}
2628
+ </button>
2629
+ @if (message) { <div style="margin-top:14px;font-size:13px;color:#9a9eab">{{ message }}</div> }
2630
+ } @else {
2631
+ <p [style]="UI.intro">Nenhum usuário gerado ainda. Gere um pelo painel "Gerador".</p>
2632
+ }
2633
+ `,
2634
+ }]
2635
+ }] });
2636
+
2637
+ /** View the last generated user's data + manage auth-statements (no Material). */
2638
+ class DataPanelComponent {
2639
+ constructor() {
2640
+ this.UI = UI;
2641
+ this.api = inject(DevtoolsApiService);
2642
+ this.userUtils = inject(DevtoolsUserUtils);
2643
+ this.utils = inject(DevtoolsUtils);
2644
+ this.auth = inject(DevtoolsAuthService);
2645
+ this.toast = inject(DevtoolsToastService);
2646
+ this.cdr = inject(ChangeDetectorRef);
2647
+ this.hasUser = false;
2648
+ this.loading = false;
2649
+ this.error = '';
2650
+ this.person = null;
2651
+ this.rawText = '{}';
2652
+ this.permissions = [];
2653
+ this.newRole = 'MANAGER';
2654
+ this.orgQuery = '';
2655
+ this.orgResults = [];
2656
+ this.newOrgName = '';
2657
+ this.newOrgId = null;
2658
+ this.orgSearch$ = new Subject();
2659
+ }
2660
+ ngOnInit() {
2661
+ const [lastUser] = this.userUtils.getUserFromStorage();
2662
+ this.hasUser = !!lastUser?.document;
2663
+ if (this.hasUser && this.auth.isLogged())
2664
+ this.refresh(lastUser.document);
2665
+ this.orgSearch$
2666
+ .pipe(debounceTime(300), switchMap((q) => ((q ?? '').trim() ? this.api.getWithQuery({ q: (q ?? '').trim() }) : of([]))))
2667
+ .subscribe({
2668
+ next: (list) => {
2669
+ this.orgResults = (list ?? []).slice(0, 6);
2670
+ this.cdr.markForCheck();
2671
+ },
2672
+ error: () => {
2673
+ this.orgResults = [];
2674
+ this.cdr.markForCheck();
2675
+ },
2676
+ });
2677
+ }
2678
+ refresh(documentNumber) {
2679
+ this.loading = true;
2680
+ this.error = '';
2681
+ this.cdr.markForCheck();
2682
+ this.api.findUser({ documents: documentNumber }).subscribe({
2683
+ next: (response) => {
2684
+ const [person] = response?.persons ?? [];
2685
+ this.loading = false;
2686
+ if (!person) {
2687
+ this.error = 'Usuário não encontrado.';
2688
+ this.person = null;
2689
+ this.cdr.markForCheck();
2690
+ return;
2691
+ }
2692
+ this.person = person;
2693
+ this.rawText = JSON.stringify(person, null, 2);
2694
+ this.loadPermissions(person.id);
2695
+ this.cdr.markForCheck();
2696
+ },
2697
+ error: (err) => {
2698
+ this.loading = false;
2699
+ this.error = 'Falha ao consultar dados do usuário.';
2700
+ this.utils.setErrorToast(err);
2701
+ this.cdr.markForCheck();
2702
+ },
2703
+ });
2704
+ }
2705
+ loadPermissions(personId) {
2706
+ this.api
2707
+ .getAuthStatements(personId)
2708
+ .pipe(switchMap((statements) => {
2709
+ if (!statements.length)
2710
+ return of({ statements, organizations: [] });
2711
+ const ids = Array.from(new Set(statements.map((s) => s?.resource?.organization?.id).filter((id) => !!id)));
2712
+ return this.api.getOrganizationsByIds(ids).pipe(switchMap((organizations) => of({ statements, organizations })));
2713
+ }))
2714
+ .subscribe({
2715
+ next: ({ statements, organizations }) => {
2716
+ const byId = new Map();
2717
+ organizations.forEach((o) => o?.id && byId.set(o.id, o));
2718
+ this.permissions = statements.map((s) => {
2719
+ const orgId = s?.resource?.organization?.id;
2720
+ const role = s?.role?.name ?? '';
2721
+ return {
2722
+ authStatementId: s?.id ?? 0,
2723
+ roleLabel: role === 'MANAGER' ? 'Gestor' : role === 'DOCTOR' ? 'Médico' : role || '—',
2724
+ organizationName: (orgId && byId.get(orgId)?.name) || `Org #${orgId ?? '-'}`,
2725
+ };
2726
+ });
2727
+ this.cdr.markForCheck();
2728
+ },
2729
+ error: (err) => {
2730
+ this.permissions = [];
2731
+ this.utils.setErrorToast(err);
2732
+ this.cdr.markForCheck();
2733
+ },
2734
+ });
2735
+ }
2736
+ pickOrg(org) {
2737
+ this.newOrgId = org?.id ?? null;
2738
+ this.newOrgName = org?.name ?? '';
2739
+ this.orgQuery = '';
2740
+ this.orgResults = [];
2741
+ }
2742
+ add() {
2743
+ if (!this.person?.id || !this.newOrgId)
2744
+ return;
2745
+ if (this.person?.phoneStatus !== 'VERIFIED') {
2746
+ this.toast.show('Phone não verificado — verifique no App do usuário');
2747
+ return;
2748
+ }
2749
+ this.api.createAuthStatement(this.person.id, this.newRole, this.newOrgId).subscribe({
2750
+ next: () => {
2751
+ this.toast.show('Permissão cadastrada');
2752
+ this.newOrgId = null;
2753
+ this.newOrgName = '';
2754
+ this.loadPermissions(this.person.id);
2755
+ },
2756
+ error: (err) => this.utils.setErrorToast(err),
2757
+ });
2758
+ }
2759
+ remove(item) {
2760
+ if (!item.authStatementId)
2761
+ return;
2762
+ if (!window.confirm(`Remover a permissão ${item.roleLabel} de ${item.organizationName}?`))
2763
+ return;
2764
+ this.api.deleteAuthStatement(item.authStatementId).subscribe({
2765
+ next: () => {
2766
+ this.toast.show('Permissão removida');
2767
+ if (this.person?.id)
2768
+ this.loadPermissions(this.person.id);
2769
+ },
2770
+ error: (err) => this.utils.setErrorToast(err),
2771
+ });
2772
+ }
2773
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DataPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2774
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: DataPanelComponent, isStandalone: true, selector: "devtools-data-panel", ngImport: i0, template: `
2775
+ @if (!hasUser) {
2776
+ <p [style]="UI.intro">Nenhum usuário gerado ainda. Gere um pelo painel "Gerador".</p>
2777
+ } @else {
2778
+ @if (loading) { <p [style]="UI.intro">Carregando dados do usuário…</p> }
2779
+ @if (error) { <p [style]="UI.intro" style="color:#d6604f">{{ error }}</p> }
2780
+
2781
+ @if (person) {
2782
+ <div style="font-size:13px;font-weight:600;color:#f0f1f4;margin-bottom:10px">Permissões</div>
2783
+ @for (item of permissions; track item.authStatementId) {
2784
+ <div [style]="UI.card" style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
2785
+ <div style="flex:1;min-width:0">
2786
+ <div style="font-size:13px;color:#eceef3">{{ item.roleLabel }}</div>
2787
+ <div [style]="UI.mono">{{ item.organizationName }}</div>
2788
+ </div>
2789
+ <button type="button" [style]="UI.ghost" style="width:auto;margin:0;color:#d6604f" (click)="remove(item)">Remover</button>
2790
+ </div>
2791
+ } @empty {
2792
+ <p [style]="UI.intro">Nenhuma permissão cadastrada.</p>
2793
+ }
2794
+
2795
+ <!-- add permission (inline) -->
2796
+ <div [style]="UI.card" style="margin-top:8px">
2797
+ <div [style]="UI.label">Nova permissão</div>
2798
+ <select [style]="UI.input" [(ngModel)]="newRole" style="margin:6px 0">
2799
+ <option value="MANAGER">MANAGER (Gestor)</option>
2800
+ <option value="DOCTOR">DOCTOR (Médico)</option>
2801
+ </select>
2802
+ <input [style]="UI.input" [(ngModel)]="orgQuery" (ngModelChange)="orgSearch$.next($event)"
2803
+ [placeholder]="newOrgName || 'Buscar organização…'" style="margin:6px 0" />
2804
+ @for (org of orgResults; track org.id) {
2805
+ <button type="button" (click)="pickOrg(org)"
2806
+ style="display:block;width:100%;text-align:left;padding:8px 10px;background:#101117;border:1px solid #24262f;border-radius:8px;color:#eceef3;font-size:12.5px;cursor:pointer;margin-bottom:4px">
2807
+ {{ org.name }}
2808
+ </button>
2809
+ }
2810
+ <button type="button" [style]="UI.primary" [disabled]="!newOrgId" (click)="add()">Adicionar permissão</button>
2811
+ </div>
2812
+
2813
+ <div style="font-size:13px;font-weight:600;color:#f0f1f4;margin:22px 0 10px">Dados brutos</div>
2814
+ <pre [style]="UI.pre">{{ rawText }}</pre>
2815
+ }
2816
+ }
2817
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2818
+ }
2819
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DataPanelComponent, decorators: [{
2820
+ type: Component,
2821
+ args: [{
2822
+ selector: 'devtools-data-panel',
2823
+ standalone: true,
2824
+ changeDetection: ChangeDetectionStrategy.OnPush,
2825
+ imports: [FormsModule],
2826
+ template: `
2827
+ @if (!hasUser) {
2828
+ <p [style]="UI.intro">Nenhum usuário gerado ainda. Gere um pelo painel "Gerador".</p>
2829
+ } @else {
2830
+ @if (loading) { <p [style]="UI.intro">Carregando dados do usuário…</p> }
2831
+ @if (error) { <p [style]="UI.intro" style="color:#d6604f">{{ error }}</p> }
2832
+
2833
+ @if (person) {
2834
+ <div style="font-size:13px;font-weight:600;color:#f0f1f4;margin-bottom:10px">Permissões</div>
2835
+ @for (item of permissions; track item.authStatementId) {
2836
+ <div [style]="UI.card" style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
2837
+ <div style="flex:1;min-width:0">
2838
+ <div style="font-size:13px;color:#eceef3">{{ item.roleLabel }}</div>
2839
+ <div [style]="UI.mono">{{ item.organizationName }}</div>
2840
+ </div>
2841
+ <button type="button" [style]="UI.ghost" style="width:auto;margin:0;color:#d6604f" (click)="remove(item)">Remover</button>
2842
+ </div>
2843
+ } @empty {
2844
+ <p [style]="UI.intro">Nenhuma permissão cadastrada.</p>
2845
+ }
2846
+
2847
+ <!-- add permission (inline) -->
2848
+ <div [style]="UI.card" style="margin-top:8px">
2849
+ <div [style]="UI.label">Nova permissão</div>
2850
+ <select [style]="UI.input" [(ngModel)]="newRole" style="margin:6px 0">
2851
+ <option value="MANAGER">MANAGER (Gestor)</option>
2852
+ <option value="DOCTOR">DOCTOR (Médico)</option>
2853
+ </select>
2854
+ <input [style]="UI.input" [(ngModel)]="orgQuery" (ngModelChange)="orgSearch$.next($event)"
2855
+ [placeholder]="newOrgName || 'Buscar organização…'" style="margin:6px 0" />
2856
+ @for (org of orgResults; track org.id) {
2857
+ <button type="button" (click)="pickOrg(org)"
2858
+ style="display:block;width:100%;text-align:left;padding:8px 10px;background:#101117;border:1px solid #24262f;border-radius:8px;color:#eceef3;font-size:12.5px;cursor:pointer;margin-bottom:4px">
2859
+ {{ org.name }}
2860
+ </button>
2861
+ }
2862
+ <button type="button" [style]="UI.primary" [disabled]="!newOrgId" (click)="add()">Adicionar permissão</button>
2863
+ </div>
2864
+
2865
+ <div style="font-size:13px;font-weight:600;color:#f0f1f4;margin:22px 0 10px">Dados brutos</div>
2866
+ <pre [style]="UI.pre">{{ rawText }}</pre>
2867
+ }
2868
+ }
2869
+ `,
2870
+ }]
2871
+ }] });
2872
+
2873
+ /** Copy data from the active app session (admin or generated user). */
2874
+ class CopyPanelComponent {
2875
+ constructor() {
2876
+ this.UI = UI;
2877
+ this.session = inject(DevtoolsSessionService);
2878
+ this.userUtils = inject(DevtoolsUserUtils);
2879
+ this.utils = inject(DevtoolsUtils);
2880
+ this.toast = inject(DevtoolsToastService);
2881
+ }
2882
+ rows() {
2883
+ const active = this.session.active();
2884
+ const [lastUser] = this.userUtils.getUserFromStorage();
2885
+ const cpf = lastUser?.document;
2886
+ const short = (v) => (v ? v.slice(0, 30) + '…' : '');
2887
+ return [
2888
+ { key: 'cpf', label: 'CPF / documento', icon: 'badge', value: cpf, preview: cpf },
2889
+ { key: 'token', label: 'accessToken', icon: 'vpn_key', value: active?.accessToken, preview: short(active?.accessToken) },
2890
+ { key: 'refresh', label: 'refreshToken', icon: 'autorenew', value: active?.refreshToken, preview: short(active?.refreshToken) },
2891
+ {
2892
+ key: 'payload',
2893
+ label: 'Payload completo',
2894
+ icon: 'data_object',
2895
+ value: JSON.stringify({ accessToken: active?.accessToken, refreshToken: active?.refreshToken, document: cpf }, null, 2),
2896
+ preview: 'JSON da sessão',
2897
+ },
2898
+ ];
2899
+ }
2900
+ copy(value, label) {
2901
+ if (!value) {
2902
+ this.toast.show(label + ' indisponível');
2903
+ return;
2904
+ }
2905
+ this.utils.copyClipboard(value, label);
2906
+ }
2907
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CopyPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2908
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: CopyPanelComponent, isStandalone: true, selector: "devtools-copy-panel", ngImport: i0, template: `
2909
+ <p [style]="UI.intro">Copie rapidamente dados da sessão ativa do app.</p>
2910
+ @for (item of rows(); track item.key) {
2911
+ <button type="button" [style]="UI.row" (click)="copy(item.value, item.label)">
2912
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:20px;color:#ffc454">{{ item.icon }}</span>
2913
+ <div style="flex:1;min-width:0">
2914
+ <div style="font-size:13px;font-weight:600;color:#eceef3">{{ item.label }}</div>
2915
+ <div [style]="UI.mono" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ item.preview || '—' }}</div>
2916
+ </div>
2917
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px;color:#6a6e7b">content_copy</span>
2918
+ </button>
2919
+ }
2920
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2921
+ }
2922
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CopyPanelComponent, decorators: [{
2923
+ type: Component,
2924
+ args: [{
2925
+ selector: 'devtools-copy-panel',
2926
+ standalone: true,
2927
+ changeDetection: ChangeDetectionStrategy.OnPush,
2928
+ template: `
2929
+ <p [style]="UI.intro">Copie rapidamente dados da sessão ativa do app.</p>
2930
+ @for (item of rows(); track item.key) {
2931
+ <button type="button" [style]="UI.row" (click)="copy(item.value, item.label)">
2932
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:20px;color:#ffc454">{{ item.icon }}</span>
2933
+ <div style="flex:1;min-width:0">
2934
+ <div style="font-size:13px;font-weight:600;color:#eceef3">{{ item.label }}</div>
2935
+ <div [style]="UI.mono" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ item.preview || '—' }}</div>
2936
+ </div>
2937
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:18px;color:#6a6e7b">content_copy</span>
2938
+ </button>
2939
+ }
2940
+ `,
2941
+ }]
2942
+ }] });
2943
+
2944
+ const BUILTIN_ACTIONS = [
2945
+ { id: 'admin', label: 'Admin', icon: 'shield_person', content: AdminLoginPanelComponent, order: 0 },
2946
+ { id: 'app-login', label: 'Login App', icon: 'login', content: LoginPanelComponent, order: 1 },
2947
+ { id: 'switch', label: 'Trocar', icon: 'switch_account', content: SwitchPanelComponent, order: 2 },
2948
+ { id: 'generator', label: 'Gerador', icon: 'groups', content: GeneratorPanelComponent, order: 3 },
2949
+ { id: 'phone', label: 'Celular', icon: 'sms', content: PhonePanelComponent, order: 4 },
2950
+ { id: 'exams', label: 'Exames', icon: 'biotech', content: ExamsPanelComponent, order: 5 },
2951
+ { id: 'data', label: 'Dados', icon: 'badge', content: DataPanelComponent, order: 6 },
2952
+ { id: 'copy', label: 'Copiar', icon: 'content_copy', content: CopyPanelComponent, order: 7 },
2953
+ ];
2954
+ /**
2955
+ * Mount the self-contained DevTools. Add to `bootstrapApplication` providers or
2956
+ * `@NgModule({ providers })`. Everything runs in an ISOLATED environment
2957
+ * injector with the package's OWN HttpClient + interceptor — it never touches
2958
+ * the host app's services, interceptors or session beyond the deliberate
2959
+ * active-session keys used by the Switch panel.
2960
+ */
2961
+ function provideDevtools(options) {
2962
+ const { enabled = true, actions = [], apiBaseUrl, title, fabIcon, ...rest } = options ?? {};
2963
+ // Layer 1 gate (Layer 2 = fileReplacements; see README).
2964
+ if (!enabled || !apiBaseUrl)
2965
+ return makeEnvironmentProviders([]);
2966
+ const config = { apiBaseUrl, ...rest };
2967
+ const allActions = [...BUILTIN_ACTIONS, ...actions];
2968
+ return makeEnvironmentProviders([
2969
+ {
2970
+ provide: APP_BOOTSTRAP_LISTENER,
2971
+ multi: true,
2972
+ useFactory: () => {
2973
+ const parent = inject(EnvironmentInjector);
2974
+ return () => mountIsolated(parent, config, allActions, { title, fabIcon });
2975
+ },
2976
+ },
2977
+ ]);
2978
+ }
2979
+ /** Load the Material Symbols Outlined font the package's icons rely on, so it
2980
+ * never depends on the host having it. Injected once. */
2981
+ function ensureIconFont() {
2982
+ const id = 'dc-material-symbols-font';
2983
+ if (document.getElementById(id))
2984
+ return;
2985
+ const link = document.createElement('link');
2986
+ link.id = id;
2987
+ link.rel = 'stylesheet';
2988
+ link.href =
2989
+ 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap';
2990
+ document.head.appendChild(link);
2991
+ }
2992
+ function mountIsolated(parent, config, actions, shell) {
2993
+ if (typeof document === 'undefined')
2994
+ return;
2995
+ ensureIconFont();
2996
+ const injector = createEnvironmentInjector([
2997
+ // OWN HttpClient + interceptor — fully isolated from the host.
2998
+ provideHttpClient(withInterceptors([devtoolsHttpInterceptor])),
2999
+ { provide: DEVTOOLS_CONFIG, useValue: { media: 'WHATSAPP', locale: 'pt_BR', fixedOrgId: 200, ...config } },
3000
+ {
3001
+ provide: DEVTOOLS_SHELL_CONFIG,
3002
+ useValue: {
3003
+ title: shell.title ?? 'DevTools',
3004
+ fabIcon: shell.fabIcon ?? 'developer_mode',
3005
+ storageKey: 'marsaude.devtools.fab',
3006
+ },
3007
+ },
3008
+ ...actions.map((action) => ({ provide: DEVTOOLS_ACTION, useValue: action, multi: true })),
3009
+ DevtoolsStorage,
3010
+ DevtoolsUtils,
3011
+ DevtoolsUserUtils,
3012
+ DevtoolsTypeform,
3013
+ DevtoolsApiService,
3014
+ DevtoolsAuthService,
3015
+ DevtoolsSessionService,
3016
+ DevtoolsPositionService,
3017
+ DevtoolsToastService,
3018
+ ], parent);
3019
+ // Consume our own Google OAuth return (guarded) to finish admin login.
3020
+ runInInjectionContext(injector, () => inject(DevtoolsAuthService).handleGoogleReturn());
3021
+ const appRef = parent.get(ApplicationRef);
3022
+ const host = document.createElement('div');
3023
+ host.setAttribute('data-devtools-shell-host', '');
3024
+ document.body.appendChild(host);
3025
+ const ref = createComponent(DevtoolsShellComponent, { environmentInjector: injector, hostElement: host });
3026
+ appRef.attachView(ref.hostView);
3027
+ appRef.onDestroy(() => {
3028
+ ref.destroy();
3029
+ host.remove();
3030
+ injector.destroy();
3031
+ });
3032
+ }
3033
+
3034
+ /*
3035
+ * Public API of @marsaude/devtools-shell — a self-contained, draggable DevTools
3036
+ * package. The host only calls provideDevtools(config); everything else
3037
+ * (HttpClient, interceptor, auth, panels) lives inside the package.
3038
+ */
3039
+
3040
+ /**
3041
+ * Generated bundle index. Do not edit.
3042
+ */
3043
+
3044
+ export { DEVTOOLS_ACTION, DEVTOOLS_SHELL_CONFIG, DevtoolsAuthService, DevtoolsSessionService, DevtoolsShellComponent, DevtoolsToastService, provideDevtools };
3045
+ //# sourceMappingURL=marsaude-devtools-shell.mjs.map