@7365admin1/layer-common 3.0.20 → 3.0.21

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,1890 @@
1
+ <template>
2
+ <section class="hid-intercom-management">
3
+ <div class="top-bar">
4
+ <v-btn
5
+ v-if="activeTab === 'contacts'"
6
+ rounded="xl"
7
+ class="text-none add-button"
8
+ variant="flat"
9
+ prepend-icon="mdi-plus"
10
+ @click="openContactDialog()"
11
+ >
12
+ Add Contacts
13
+ </v-btn>
14
+
15
+ <div v-else class="top-placeholder" />
16
+
17
+ <div class="toolbar-spacer" />
18
+
19
+ <v-text-field
20
+ v-model="search"
21
+ placeholder="Search"
22
+ variant="outlined"
23
+ density="compact"
24
+ hide-details
25
+ class="search-input"
26
+ />
27
+
28
+ <v-select
29
+ v-model="statusFilter"
30
+ :items="statusOptions"
31
+ variant="outlined"
32
+ density="compact"
33
+ hide-details
34
+ class="status-input"
35
+ />
36
+ </div>
37
+
38
+ <v-alert
39
+ v-if="message.text"
40
+ :type="message.type"
41
+ variant="tonal"
42
+ density="compact"
43
+ class="mb-3"
44
+ >
45
+ {{ message.text }}
46
+ </v-alert>
47
+
48
+ <v-card flat border class="table-card">
49
+ <div class="table-refresh">
50
+ <v-btn icon="mdi-refresh" variant="text" density="comfortable" :loading="loading" @click="reload" />
51
+ <div class="toolbar-spacer" />
52
+ <span>{{ pageRange }}</span>
53
+ <v-btn icon="mdi-chevron-left" variant="text" density="compact" :disabled="page <= 1" @click="page--" />
54
+ <v-btn icon="mdi-chevron-right" variant="text" density="compact" :disabled="page >= pages" @click="page++" />
55
+ </div>
56
+
57
+ <v-tabs v-model="activeTab" class="intercom-tabs" density="compact">
58
+ <v-tab value="intercom" class="text-none">Intercom</v-tab>
59
+ <v-tab value="contacts" class="text-none">Intercom Contacts</v-tab>
60
+ </v-tabs>
61
+
62
+ <div class="table-body">
63
+ <v-table>
64
+ <thead>
65
+ <tr v-if="activeTab === 'intercom'">
66
+ <th>Name</th>
67
+ <th>Location</th>
68
+ <th>SIP Server Address</th>
69
+ <th>Operating Mode</th>
70
+ <th>Dial Code</th>
71
+ <th>SIP Status</th>
72
+ <th class="text-right"></th>
73
+ </tr>
74
+ <tr v-else>
75
+ <th>Name</th>
76
+ <th>Location</th>
77
+ <th>SIP Server Address</th>
78
+ <th>Dial Code</th>
79
+ <th class="text-right"></th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ <template v-if="activeTab === 'intercom'">
84
+ <tr v-for="row in paginatedIntercomRows" :key="row.reader._id">
85
+ <td>{{ row.name }}</td>
86
+ <td>{{ row.location }}</td>
87
+ <td>
88
+ <span :class="{ 'link-text': row.serverAddress !== 'N/A' }">{{ row.serverAddress }}</span>
89
+ </td>
90
+ <td>{{ row.operatingMode }}</td>
91
+ <td>{{ row.dialCode }}</td>
92
+ <td>
93
+ <v-chip size="small" variant="flat" :class="getStatusClass(row.status)">
94
+ {{ row.status }}
95
+ </v-chip>
96
+ </td>
97
+ <td class="text-right">
98
+ <v-menu location="bottom end">
99
+ <template #activator="{ props: menuProps }">
100
+ <v-btn v-bind="menuProps" icon="mdi-dots-vertical" variant="text" density="comfortable" />
101
+ </template>
102
+ <v-list density="compact" class="row-menu">
103
+ <v-list-item title="View" @click="openViewDialog(row)" />
104
+ <v-list-item title="SIP Settings" @click="openSipDialog(row)" />
105
+ <v-list-item title="Call Settings" @click="openCallDialog(row)" />
106
+ <v-list-item title="Dial Code" @click="openDialCodeDialog(row)" />
107
+ </v-list>
108
+ </v-menu>
109
+ </td>
110
+ </tr>
111
+ </template>
112
+
113
+ <template v-else>
114
+ <tr v-for="contact in paginatedContacts" :key="contact._rowKey">
115
+ <td>{{ contact.name }}</td>
116
+ <td>{{ contact.location }}</td>
117
+ <td>
118
+ <span :class="{ 'link-text': contact.serverAddress !== 'N/A' }">{{ contact.serverAddress }}</span>
119
+ </td>
120
+ <td>{{ contact.dialCode }}</td>
121
+ <td class="text-right">
122
+ <v-menu location="bottom end">
123
+ <template #activator="{ props: menuProps }">
124
+ <v-btn v-bind="menuProps" icon="mdi-dots-vertical" variant="text" density="comfortable" />
125
+ </template>
126
+ <v-list density="compact" class="row-menu">
127
+ <v-list-item title="View" @click="openContactView(contact)" />
128
+ <v-list-item title="Edit" @click="openContactDialog(contact)" />
129
+ <v-list-item title="Delete" @click="openDeleteContact(contact)" />
130
+ </v-list>
131
+ </v-menu>
132
+ </td>
133
+ </tr>
134
+ </template>
135
+ </tbody>
136
+ </v-table>
137
+
138
+ <div v-if="!visibleRows.length" class="empty-state">
139
+ No intercom records found.
140
+ </div>
141
+ </div>
142
+
143
+ <div class="table-footer">
144
+ <span>{{ pageRange }}</span>
145
+ <v-btn icon="mdi-chevron-left" variant="text" density="comfortable" :disabled="page <= 1" @click="page--" />
146
+ <v-btn icon="mdi-chevron-right" variant="text" density="comfortable" :disabled="page >= pages" @click="page++" />
147
+ </div>
148
+ </v-card>
149
+
150
+ <v-dialog v-model="viewDialog" max-width="360">
151
+ <v-card class="detail-dialog" rounded="lg">
152
+ <v-card-title class="dialog-title">{{ selectedRow?.name || 'HID Reader' }}</v-card-title>
153
+ <v-card-text class="detail-body">
154
+ <template v-for="section in intercomDetailSections" :key="section.title">
155
+ <div class="detail-section-title">{{ section.title }}</div>
156
+ <div v-for="item in section.items" :key="`${section.title}-${item.label}`" class="detail-row">
157
+ <span>{{ item.label }}</span>
158
+ <strong :class="{ 'link-text': item.link, 'success-text': item.success }">
159
+ {{ item.value || 'N/A' }}
160
+ </strong>
161
+ </div>
162
+ </template>
163
+ </v-card-text>
164
+ <v-card-actions class="dialog-actions pa-0">
165
+ <v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="viewDialog = false">
166
+ Close
167
+ </v-btn>
168
+ </v-card-actions>
169
+ </v-card>
170
+ </v-dialog>
171
+
172
+ <v-dialog v-model="sipDialog" max-width="430" persistent>
173
+ <v-card class="form-dialog sip-settings-dialog" rounded="lg">
174
+ <v-card-title class="dialog-title">SIP Settings</v-card-title>
175
+ <v-card-text class="dialog-body">
176
+ <div class="sip-switch-row">
177
+ <span>Enable / Disable SIP</span>
178
+ <v-switch v-model="sipForm.enabled" color="success" density="compact" hide-details />
179
+ </div>
180
+ <div class="sip-label">Server Address <span>*</span></div>
181
+ <v-text-field v-model="sipForm.serverIp" :disabled="!sipForm.enabled" placeholder="SIP server address" variant="outlined" density="compact" hide-details="auto" class="sip-input" />
182
+ <div class="sip-label">Port <span>*</span></div>
183
+ <v-text-field v-model="sipForm.serverPort" :disabled="!sipForm.enabled" placeholder="0000" variant="outlined" density="compact" hide-details="auto" class="sip-input" />
184
+ <div class="form-two-col">
185
+ <div>
186
+ <div class="sip-label">Initial RTP Port <span>*</span></div>
187
+ <v-text-field v-model="sipForm.initialRtpPort" :disabled="!sipForm.enabled" placeholder="0000" variant="outlined" density="compact" hide-details="auto" class="sip-input" />
188
+ </div>
189
+ <div>
190
+ <div class="sip-label">Final RTP Port <span>*</span></div>
191
+ <v-text-field v-model="sipForm.finalRtpPort" :disabled="!sipForm.enabled" placeholder="0000" variant="outlined" density="compact" hide-details="auto" class="sip-input" />
192
+ </div>
193
+ </div>
194
+ <div class="sip-switch-row sip-section-switch">
195
+ <span>Numeric Extension</span>
196
+ <v-switch v-model="sipForm.numericExtension" :disabled="!sipForm.enabled" color="success" density="compact" hide-details />
197
+ </div>
198
+ <div class="sip-label">Extension <span>*</span></div>
199
+ <v-text-field v-model="sipForm.extension" :disabled="areSipCredentialFieldsDisabled" placeholder="0000" variant="outlined" density="compact" hide-details="auto" class="sip-input" />
200
+ <div class="sip-label">Login <span>*</span></div>
201
+ <v-text-field v-model="sipForm.login" :disabled="areSipCredentialFieldsDisabled" placeholder="Enter username" variant="outlined" density="compact" hide-details="auto" class="sip-input" />
202
+ <div class="sip-label">Password <span>*</span></div>
203
+ <v-text-field
204
+ v-model="sipForm.password"
205
+ placeholder="Enter password"
206
+ :type="showSipPassword ? 'text' : 'password'"
207
+ :disabled="areSipCredentialFieldsDisabled"
208
+ variant="outlined"
209
+ density="compact"
210
+ hide-details="auto"
211
+ class="sip-input"
212
+ :append-inner-icon="showSipPassword ? 'mdi-eye-off' : 'mdi-eye'"
213
+ @click:append-inner="showSipPassword = !showSipPassword"
214
+ />
215
+ <div class="sip-label">Operating Mode <span>*</span></div>
216
+ <v-btn-toggle v-model="sipForm.operatingMode" :disabled="!sipForm.enabled" mandatory divided class="mode-toggle">
217
+ <v-btn value="sip-client" class="text-none">SIP Client</v-btn>
218
+ <v-btn value="peer-to-peer" class="text-none">Peer-to-Peer</v-btn>
219
+ </v-btn-toggle>
220
+ <div class="sip-label">Protocol <span>*</span></div>
221
+ <v-radio-group v-model="sipForm.protocol" :disabled="!sipForm.enabled" inline hide-details class="sip-protocol">
222
+ <v-radio label="UDP" value="UDP" />
223
+ <v-radio label="TCP" value="TCP" />
224
+ </v-radio-group>
225
+ </v-card-text>
226
+ <v-card-actions class="dialog-actions pa-0">
227
+ <v-btn class="text-none action-cancel" variant="flat" height="44" @click="sipDialog = false">Cancel</v-btn>
228
+ <v-btn class="text-none action-submit" variant="flat" height="44" :loading="saving" @click="saveSipSettings">Save</v-btn>
229
+ </v-card-actions>
230
+ </v-card>
231
+ </v-dialog>
232
+
233
+ <v-dialog v-model="rebootConfirmDialog" max-width="700" persistent>
234
+ <v-card class="reboot-dialog" rounded="0">
235
+ <button class="reboot-close" type="button" aria-label="Close" @click="rebootConfirmDialog = false">
236
+ <v-icon icon="mdi-close" />
237
+ </button>
238
+ <v-card-title class="reboot-title">Reboot is needed</v-card-title>
239
+ <v-card-text class="reboot-message">
240
+ It is necessary to restart the device after applying the configurations. Do you want to proceed?
241
+ </v-card-text>
242
+ <v-card-actions class="reboot-actions">
243
+ <v-btn class="text-none reboot-cancel" variant="text" @click="rebootConfirmDialog = false">
244
+ Cancel
245
+ </v-btn>
246
+ <v-btn class="text-none reboot-ok" variant="flat" @click="startRebootWait">
247
+ OK
248
+ </v-btn>
249
+ </v-card-actions>
250
+ </v-card>
251
+ </v-dialog>
252
+
253
+ <v-dialog v-model="rebootingDialog" max-width="900" persistent>
254
+ <v-card class="rebooting-card" rounded="0">
255
+ <div class="wait-word">Wait!</div>
256
+ <div class="rebooting-copy">
257
+ <h3>Rebooting Equipment</h3>
258
+ <p>Wait {{ rebootCountdown }} seconds for the equipment to reboot.</p>
259
+ </div>
260
+ </v-card>
261
+ </v-dialog>
262
+
263
+ <v-dialog v-model="callDialog" max-width="360" persistent>
264
+ <v-card class="form-dialog call-dialog" rounded="lg">
265
+ <v-card-title class="dialog-title">Call Settings</v-card-title>
266
+ <v-card-text class="dialog-body">
267
+ <div class="call-label">Dial mode <span>*</span></div>
268
+ <v-radio-group v-model="callForm.dialMode" hide-details class="dial-mode-list call-radio-list">
269
+ <v-radio label="Automatic" value="0" class="call-radio-option" />
270
+ <v-radio label="From the contact list" value="1" class="call-radio-option" />
271
+ <v-radio label="Numeric Keypad + Contacts" value="2" class="call-radio-option" />
272
+ </v-radio-group>
273
+ <div class="call-toggle-row">
274
+ <span>Automatic Answering</span>
275
+ <v-switch v-model="callForm.autoAnswer" color="success" density="compact" hide-details />
276
+ </div>
277
+ <div class="call-label">Automatic answering delay (seconds)</div>
278
+ <v-text-field v-model="callForm.autoAnswerDelay" :disabled="!callForm.autoAnswer" placeholder="0" variant="outlined" density="compact" hide-details="auto" class="call-input" />
279
+ <div class="form-two-col call-two-col">
280
+ <div>
281
+ <div class="call-label">Registration time (seconds)</div>
282
+ <v-text-field v-model="callForm.registrationTime" :disabled="!callForm.autoAnswer" placeholder="0" variant="outlined" density="compact" hide-details="auto" class="call-input" />
283
+ </div>
284
+ <div>
285
+ <div class="call-label">Keep-alive (seconds)</div>
286
+ <v-text-field v-model="callForm.keepAlive" :disabled="!callForm.autoAnswer" placeholder="0" variant="outlined" density="compact" hide-details="auto" class="call-input" />
287
+ </div>
288
+ </div>
289
+ <div class="call-label">Maximum call time (seconds)</div>
290
+ <v-text-field v-model="callForm.maxCallTime" :disabled="!callForm.autoAnswer" placeholder="0" variant="outlined" density="compact" hide-details="auto" class="call-input" />
291
+ <div class="call-toggle-row">
292
+ <span>Dial Button</span>
293
+ <v-switch v-model="callForm.dialButton" color="success" density="compact" hide-details />
294
+ </div>
295
+ <div class="call-toggle-row">
296
+ <span>External Dial Button</span>
297
+ <v-switch v-model="callForm.externalDialButton" color="success" density="compact" hide-details />
298
+ </div>
299
+ <div class="call-toggle-row">
300
+ <span>Facial identification during the call</span>
301
+ <v-switch v-model="callForm.faceDuringCall" color="success" density="compact" hide-details />
302
+ </div>
303
+ <div class="call-toggle-row">
304
+ <span>Custom Icon</span>
305
+ <v-switch v-model="callForm.customIcon" color="success" density="compact" hide-details />
306
+ </div>
307
+ <div class="upload-row call-upload-row">
308
+ <div class="upload-drop" :class="{ 'is-disabled': !callForm.customIcon }"><v-icon size="16">mdi-cloud-upload-outline</v-icon> Drag and drop files here</div>
309
+ <v-btn icon="mdi-upload" variant="flat" class="upload-button" :disabled="!callForm.customIcon" />
310
+ </div>
311
+ <div v-if="callForm.customIcon" class="call-file-row">
312
+ <div class="call-file-name"><v-icon size="20">mdi-phone</v-icon> call.svg</div>
313
+ <v-btn icon="mdi-delete-outline" variant="text" density="compact" />
314
+ </div>
315
+ <div class="call-toggle-row">
316
+ <span>Automatic Call with unknown contact</span>
317
+ <v-switch v-model="callForm.unknownContact" color="success" density="compact" hide-details />
318
+ </div>
319
+ <div class="call-label">Call after (seconds)</div>
320
+ <v-text-field v-model="callForm.callAfter" :disabled="!callForm.unknownContact" placeholder="0" variant="outlined" density="compact" hide-details="auto" class="call-input" />
321
+ </v-card-text>
322
+ <v-card-actions class="dialog-actions pa-0">
323
+ <v-btn class="text-none action-cancel" variant="flat" height="44" @click="callDialog = false">Cancel</v-btn>
324
+ <v-btn class="text-none action-submit" variant="flat" height="44" :loading="saving" @click="saveCallSettings">Save</v-btn>
325
+ </v-card-actions>
326
+ </v-card>
327
+ </v-dialog>
328
+
329
+ <v-dialog v-model="dialCodeDialog" max-width="360" persistent>
330
+ <v-card class="form-dialog dial-code-dialog" rounded="lg">
331
+ <v-card-title class="dialog-title">Dial Code</v-card-title>
332
+ <v-card-text class="dialog-body">
333
+ <div class="call-toggle-row dial-toggle-row">
334
+ <span>Dial Code</span>
335
+ <v-switch v-model="dialForm.enabled" color="success" density="compact" hide-details />
336
+ </div>
337
+ <div class="call-label">Dial code to unlock</div>
338
+ <v-text-field v-model="dialForm.code" :disabled="!dialForm.enabled" placeholder="0" variant="outlined" density="compact" hide-details="auto" class="call-input dial-input" />
339
+ </v-card-text>
340
+ <v-card-actions class="dialog-actions pa-0">
341
+ <v-btn class="text-none action-cancel" variant="flat" height="44" @click="dialCodeDialog = false">Cancel</v-btn>
342
+ <v-btn class="text-none action-submit" variant="flat" height="44" :loading="saving" @click="saveDialCode">Save</v-btn>
343
+ </v-card-actions>
344
+ </v-card>
345
+ </v-dialog>
346
+
347
+ <v-dialog v-model="contactDialog" max-width="360" persistent>
348
+ <v-card class="form-dialog" rounded="lg">
349
+ <v-card-title class="dialog-title">{{ contactForm.id ? 'Edit Contact' : 'Add Contact' }}</v-card-title>
350
+ <v-card-text class="dialog-body">
351
+ <FieldLabel text="Name" required />
352
+ <v-text-field v-model="contactForm.name" placeholder="Enter name" variant="outlined" density="compact" hide-details="auto" />
353
+ <FieldLabel text="Location" />
354
+ <v-text-field v-model="contactForm.location" placeholder="Location" variant="outlined" density="compact" hide-details="auto" />
355
+ <FieldLabel text="Server Address" required />
356
+ <v-text-field v-model="contactForm.serverAddress" placeholder="192.168.1.1" variant="outlined" density="compact" hide-details="auto" />
357
+ </v-card-text>
358
+ <v-card-actions class="dialog-actions pa-0">
359
+ <v-btn class="text-none action-cancel" variant="flat" height="44" @click="contactDialog = false">Cancel</v-btn>
360
+ <v-btn class="text-none action-submit" variant="flat" height="44" :loading="contactsSaving" @click="saveContact">Save</v-btn>
361
+ </v-card-actions>
362
+ </v-card>
363
+ </v-dialog>
364
+
365
+ <v-dialog v-model="contactViewDialog" max-width="360">
366
+ <v-card class="detail-dialog" rounded="lg">
367
+ <v-card-title class="dialog-title">{{ selectedContact?.name || 'Contact' }}</v-card-title>
368
+ <v-card-text class="detail-body">
369
+ <div v-for="item in contactDetailRows" :key="item.label" class="detail-row">
370
+ <span>{{ item.label }}</span>
371
+ <strong :class="{ 'link-text': item.link, 'success-text': item.success }">
372
+ {{ item.value || 'N/A' }}
373
+ </strong>
374
+ </div>
375
+ </v-card-text>
376
+ <v-card-actions class="dialog-actions pa-0">
377
+ <v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="contactViewDialog = false">
378
+ Close
379
+ </v-btn>
380
+ </v-card-actions>
381
+ </v-card>
382
+ </v-dialog>
383
+
384
+ <v-dialog v-model="deleteContactDialog" max-width="340" persistent>
385
+ <v-card class="form-dialog" rounded="lg">
386
+ <v-card-title class="dialog-title">Delete Contact</v-card-title>
387
+ <v-card-text class="dialog-body">
388
+ Are you sure you want to delete this intercom contact?
389
+ </v-card-text>
390
+ <v-card-actions class="dialog-actions pa-0">
391
+ <v-btn class="text-none action-cancel" variant="flat" height="44" @click="deleteContactDialog = false">Cancel</v-btn>
392
+ <v-btn class="text-none action-submit" variant="flat" height="44" :loading="contactsSaving" @click="deleteContact">Delete</v-btn>
393
+ </v-card-actions>
394
+ </v-card>
395
+ </v-dialog>
396
+ </section>
397
+ </template>
398
+
399
+ <script setup lang="ts">
400
+ const props = defineProps({
401
+ site: {
402
+ type: String,
403
+ required: true,
404
+ },
405
+ });
406
+
407
+ type MessageType = "success" | "info" | "warning" | "error";
408
+ type IntercomTab = "intercom" | "contacts";
409
+ type ContactRow = {
410
+ _rowKey: string;
411
+ id?: number;
412
+ readerId: string;
413
+ name: string;
414
+ location: string;
415
+ serverAddress: string;
416
+ dialCode: string;
417
+ };
418
+
419
+ type DetailItem = {
420
+ label: string;
421
+ value?: string | number | boolean;
422
+ link?: boolean;
423
+ success?: boolean;
424
+ };
425
+
426
+ type IntercomRow = {
427
+ reader: Record<string, any>;
428
+ name: string;
429
+ location: string;
430
+ serverAddress: string;
431
+ serverPort: string;
432
+ initialRtpPort: string;
433
+ finalRtpPort: string;
434
+ monitorPath: string;
435
+ status: string;
436
+ sipEnabled: boolean;
437
+ numericExtension: boolean;
438
+ extension: string;
439
+ login: string;
440
+ password: string;
441
+ operatingMode: string;
442
+ protocol: string;
443
+ dialMode: string;
444
+ dialModeLabel: string;
445
+ dialCode: string;
446
+ autoAnswer: boolean;
447
+ autoAnswerDelay: string;
448
+ registrationTime: string;
449
+ keepAlive: string;
450
+ maxCallTime: string;
451
+ dialButton: boolean;
452
+ externalDialButton: boolean;
453
+ unknownContact: boolean;
454
+ callAfter: string;
455
+ pjsip: Record<string, any>;
456
+ };
457
+
458
+ const {
459
+ getReaders,
460
+ getReaderConfiguration,
461
+ setReaderConfiguration,
462
+ runObjectOperation,
463
+ getIntercomStatus,
464
+ } = useHidAmico();
465
+
466
+ const activeTab = ref<IntercomTab>("intercom");
467
+ const readers = ref<Record<string, any>[]>([]);
468
+ const intercomRows = ref<IntercomRow[]>([]);
469
+ const contacts = ref<ContactRow[]>([]);
470
+ const loading = ref(false);
471
+ const saving = ref(false);
472
+ const contactsSaving = ref(false);
473
+ const search = ref("");
474
+ const statusFilter = ref("Status");
475
+ const page = ref(1);
476
+ const limit = 20;
477
+ const selectedRow = ref<IntercomRow | null>(null);
478
+ const selectedContact = ref<ContactRow | null>(null);
479
+ const showSipPassword = ref(false);
480
+
481
+ const viewDialog = ref(false);
482
+ const sipDialog = ref(false);
483
+ const rebootConfirmDialog = ref(false);
484
+ const rebootingDialog = ref(false);
485
+ const callDialog = ref(false);
486
+ const dialCodeDialog = ref(false);
487
+ const contactDialog = ref(false);
488
+ const contactViewDialog = ref(false);
489
+ const deleteContactDialog = ref(false);
490
+
491
+ const message = reactive<{ type: MessageType; text: string }>({ type: "info", text: "" });
492
+ const rebootCountdown = ref(59);
493
+ let rebootTimer: ReturnType<typeof setInterval> | undefined;
494
+
495
+ const statusOptions = ["Status", "Connected", "Not connected", "Connecting", "Disabled", "Failed"];
496
+
497
+ const pjsipKeys = [
498
+ "enabled",
499
+ "server_ip",
500
+ "server_port",
501
+ "server_outbound_port",
502
+ "server_outbound_port_range",
503
+ "numeric_branch_enabled",
504
+ "branch",
505
+ "login",
506
+ "password",
507
+ "peer_to_peer_enabled",
508
+ "reg_status_query_period",
509
+ "server_retry_interval",
510
+ "max_call_time",
511
+ "auto_answer_enabled",
512
+ "auto_answer_delay",
513
+ "auto_call_button_enabled",
514
+ "rex_enabled",
515
+ "dialing_display_mode",
516
+ "auto_call_target",
517
+ "custom_identifier_auto_call",
518
+ "video_enabled",
519
+ "open_door_enabled",
520
+ "open_door_command",
521
+ "mic_volume",
522
+ "speaker_volume",
523
+ ];
524
+
525
+ const sipForm = reactive({
526
+ enabled: false,
527
+ serverIp: "",
528
+ serverPort: "5060",
529
+ initialRtpPort: "10000",
530
+ finalRtpPort: "1000",
531
+ numericExtension: false,
532
+ extension: "",
533
+ login: "",
534
+ password: "",
535
+ operatingMode: "sip-client",
536
+ protocol: "UDP",
537
+ });
538
+
539
+ const callForm = reactive({
540
+ dialMode: "0",
541
+ autoAnswer: false,
542
+ autoAnswerDelay: "0",
543
+ registrationTime: "0",
544
+ keepAlive: "0",
545
+ maxCallTime: "0",
546
+ dialButton: false,
547
+ externalDialButton: false,
548
+ faceDuringCall: false,
549
+ customIcon: false,
550
+ unknownContact: false,
551
+ callAfter: "0",
552
+ });
553
+
554
+ const dialForm = reactive({
555
+ enabled: false,
556
+ code: "",
557
+ });
558
+
559
+ const contactForm = reactive({
560
+ id: undefined as number | undefined,
561
+ readerId: "",
562
+ name: "",
563
+ location: "",
564
+ serverAddress: "",
565
+ });
566
+
567
+ const visibleRows = computed(() => activeTab.value === "intercom" ? filteredIntercomRows.value : filteredContacts.value);
568
+ const pages = computed(() => Math.max(1, Math.ceil(visibleRows.value.length / limit)));
569
+ const pageRange = computed(() => {
570
+ if (!visibleRows.value.length) return "0-0 of 0";
571
+ const currentPage = Math.min(page.value, pages.value);
572
+ const start = (currentPage - 1) * limit + 1;
573
+ const end = Math.min(currentPage * limit, visibleRows.value.length);
574
+ return `${start}-${end} of ${visibleRows.value.length}`;
575
+ });
576
+
577
+ const filteredIntercomRows = computed(() => {
578
+ const query = search.value.trim().toLowerCase();
579
+ return intercomRows.value.filter((row) => {
580
+ const matchesSearch = !query || [
581
+ row.name,
582
+ row.location,
583
+ row.serverAddress,
584
+ row.operatingMode,
585
+ row.dialCode,
586
+ row.status,
587
+ ].some((value) => String(value || "").toLowerCase().includes(query));
588
+ const matchesStatus = statusFilter.value === "Status" || row.status === statusFilter.value;
589
+ return matchesSearch && matchesStatus;
590
+ });
591
+ });
592
+
593
+ const filteredContacts = computed(() => {
594
+ const query = search.value.trim().toLowerCase();
595
+ return contacts.value.filter((contact) => !query || [
596
+ contact.name,
597
+ contact.location,
598
+ contact.serverAddress,
599
+ contact.dialCode,
600
+ ].some((value) => String(value || "").toLowerCase().includes(query)));
601
+ });
602
+
603
+ const paginatedIntercomRows = computed(() => paginate(filteredIntercomRows.value));
604
+ const paginatedContacts = computed(() => paginate(filteredContacts.value));
605
+ const isPeerToPeerMode = computed(() => sipForm.operatingMode === "peer-to-peer");
606
+ const areSipCredentialFieldsDisabled = computed(() => !sipForm.enabled || isPeerToPeerMode.value);
607
+ const intercomDetailSections = computed(() => {
608
+ const row = selectedRow.value;
609
+ return [
610
+ {
611
+ title: "SIP (Session Initiation Protocol)",
612
+ items: [
613
+ { label: "SIP (Session Initiation Protocol)", value: row?.sipEnabled ? "Enabled" : "Disabled" },
614
+ { label: "Server Address", value: row?.serverAddress, link: true },
615
+ { label: "Port", value: row?.serverPort },
616
+ { label: "Initial RTP Port", value: row?.initialRtpPort },
617
+ { label: "Final RTP Port", value: row?.finalRtpPort },
618
+ { label: "Monitor Path", value: row?.monitorPath },
619
+ { label: "Status", value: row?.status, success: true },
620
+ ],
621
+ },
622
+ {
623
+ title: "Numeric Extension",
624
+ items: [
625
+ { label: "Numeric Extension", value: row?.numericExtension ? "Enabled" : "Disabled" },
626
+ { label: "Extension", value: row?.extension },
627
+ { label: "Login", value: row?.login },
628
+ { label: "Password", value: maskValue(row?.password) },
629
+ ],
630
+ },
631
+ {
632
+ title: "Operating Mode",
633
+ items: [
634
+ { label: "Operating Mode", value: row?.operatingMode },
635
+ { label: "Protocol", value: row?.protocol },
636
+ ],
637
+ },
638
+ {
639
+ title: "Call",
640
+ items: [
641
+ { label: "Dial mode", value: row?.dialModeLabel },
642
+ { label: "Automatic Answering", value: row?.autoAnswer ? "Enabled" : "Disabled" },
643
+ { label: "Automatic answering delay (seconds)", value: row?.autoAnswerDelay },
644
+ { label: "Registration time (seconds)", value: row?.registrationTime },
645
+ { label: "Keep-alive (seconds)", value: row?.keepAlive },
646
+ { label: "Dial Button", value: row?.dialButton ? "Enabled" : "Disabled" },
647
+ { label: "External Dial Button", value: row?.externalDialButton ? "Enabled" : "Disabled" },
648
+ { label: "Automatic Call with unknown contact", value: row?.unknownContact ? "Enabled" : "Disabled" },
649
+ { label: "Call after (seconds)", value: row?.callAfter },
650
+ ],
651
+ },
652
+ ] as { title: string; items: DetailItem[] }[];
653
+ });
654
+ const contactDetailRows = computed<DetailItem[]>(() => [
655
+ { label: "Name", value: selectedContact.value?.name },
656
+ { label: "Server Address", value: selectedContact.value?.serverAddress, link: true },
657
+ { label: "Location", value: selectedContact.value?.location },
658
+ ]);
659
+
660
+ watch(
661
+ () => props.site,
662
+ () => reload(),
663
+ { immediate: true },
664
+ );
665
+
666
+ watch([activeTab, search, statusFilter], () => {
667
+ page.value = 1;
668
+ });
669
+
670
+ watch(pages, () => {
671
+ if (page.value > pages.value) page.value = pages.value;
672
+ });
673
+
674
+ onUnmounted(() => {
675
+ clearRebootTimer();
676
+ });
677
+
678
+ async function reload() {
679
+ if (!props.site) return;
680
+ loading.value = true;
681
+ try {
682
+ const response = await getReaders({ site: props.site, page: 1, limit: 100 });
683
+ readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
684
+ await loadIntercomRows();
685
+ await loadContacts();
686
+ } catch (error: any) {
687
+ showMessage(getErrorMessage(error), "error");
688
+ } finally {
689
+ loading.value = false;
690
+ }
691
+ }
692
+
693
+ async function loadIntercomRows() {
694
+ const rows: IntercomRow[] = [];
695
+
696
+ for (const reader of readers.value) {
697
+ const [configurationResult, statusResult] = await Promise.allSettled([
698
+ getReaderConfiguration(reader._id, { pjsip: pjsipKeys }),
699
+ getIntercomStatus(reader._id),
700
+ ]);
701
+ const configurationResponse = configurationResult.status === "fulfilled" ? configurationResult.value : {};
702
+ const statusResponse = statusResult.status === "fulfilled"
703
+ ? statusResult.value
704
+ : { data: { status: undefined, nativeError: getErrorMessage(statusResult.reason) } };
705
+ const statusPayload = statusResponse?.data ?? statusResponse;
706
+ const pjsip = normalizePjsip(configurationResponse, statusPayload);
707
+ const status = normalizeStatus(statusPayload, pjsip);
708
+ rows.push(toIntercomRow(reader, pjsip, status));
709
+ }
710
+
711
+ intercomRows.value = rows;
712
+ }
713
+
714
+ async function loadContacts() {
715
+ const rows: ContactRow[] = [];
716
+
717
+ for (const reader of readers.value) {
718
+ try {
719
+ const response = await runObjectOperation(reader._id, {
720
+ operation: "load",
721
+ object: "contacts",
722
+ fields: ["id", "name", "number"],
723
+ limit: 100,
724
+ });
725
+ rows.push(...normalizeContacts(reader, response?.data ?? response));
726
+ } catch {
727
+ // Some firmware builds may not expose contacts until intercom is configured.
728
+ }
729
+ }
730
+
731
+ contacts.value = rows;
732
+ }
733
+
734
+ function paginate<T>(items: T[]) {
735
+ const currentPage = Math.min(page.value, pages.value);
736
+ const start = (currentPage - 1) * limit;
737
+ return items.slice(start, start + limit);
738
+ }
739
+
740
+ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>, status: string): IntercomRow {
741
+ const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
742
+ const dialMode = toText(getPjsipValue(pjsip, ["dialing_display_mode", "dialingDisplayMode"]), "0");
743
+
744
+ return {
745
+ reader,
746
+ name: reader.name || reader.deviceId || "HID Reader",
747
+ location: reader.location || "N/A",
748
+ serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
749
+ serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
750
+ initialRtpPort: toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A"),
751
+ finalRtpPort: toText(getPjsipValue(pjsip, ["server_outbound_port_range", "finalRtpPort", "final_rtp_port"]), "N/A"),
752
+ monitorPath: reader.monitorPath || "N/A",
753
+ status,
754
+ sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
755
+ numericExtension: toFlag(getPjsipValue(pjsip, ["numeric_branch_enabled", "numericExtension", "numericExtensionEnabled"])) === "1",
756
+ extension: toText(getPjsipValue(pjsip, ["branch", "extension"]), "N/A"),
757
+ login: toText(getPjsipValue(pjsip, ["login", "username"]), "N/A"),
758
+ password: toText(getPjsipValue(pjsip, ["password"]), ""),
759
+ operatingMode: peerToPeer ? "Peer-to-Peer" : "SIP Client",
760
+ protocol: toText(getPjsipValue(pjsip, ["protocol"]), "UDP"),
761
+ dialMode,
762
+ dialModeLabel: getDialModeLabel(dialMode),
763
+ dialCode: toText(getPjsipValue(pjsip, ["open_door_command", "dialCode"]), "N/A"),
764
+ autoAnswer: toFlag(getPjsipValue(pjsip, ["auto_answer_enabled", "autoAnswerEnabled"])) === "1",
765
+ autoAnswerDelay: toText(getPjsipValue(pjsip, ["auto_answer_delay", "autoAnswerDelay"]), "N/A"),
766
+ registrationTime: toText(getPjsipValue(pjsip, ["reg_status_query_period", "registrationTime"]), "N/A"),
767
+ keepAlive: toText(getPjsipValue(pjsip, ["server_retry_interval", "keepAlive"]), "N/A"),
768
+ maxCallTime: toText(getPjsipValue(pjsip, ["max_call_time", "maxCallTime"]), "N/A"),
769
+ dialButton: toFlag(getPjsipValue(pjsip, ["auto_call_button_enabled", "dialButtonEnabled"])) === "1",
770
+ externalDialButton: toFlag(getPjsipValue(pjsip, ["rex_enabled", "externalDialButtonEnabled"])) === "1",
771
+ unknownContact: Boolean(toText(getPjsipValue(pjsip, ["auto_call_target", "autoCallTarget"]))),
772
+ callAfter: toText(getPjsipValue(pjsip, ["auto_call_delay", "callAfter", "auto_answer_delay"]), "N/A"),
773
+ pjsip,
774
+ };
775
+ }
776
+
777
+ function normalizePjsip(...sources: Record<string, any>[]) {
778
+ for (const source of sources) {
779
+ const candidate = findPjsipCandidate(source);
780
+ if (candidate) return candidate;
781
+ }
782
+ return {};
783
+ }
784
+
785
+ function findPjsipCandidate(source: Record<string, any> | undefined): Record<string, any> | undefined {
786
+ if (!source || typeof source !== "object") return undefined;
787
+ const candidates = [
788
+ source.pjsip,
789
+ source.configuration?.pjsip,
790
+ source.result?.pjsip,
791
+ source.result?.configuration?.pjsip,
792
+ source.data?.pjsip,
793
+ source.data?.configuration?.pjsip,
794
+ source.data?.result?.pjsip,
795
+ source.data?.result?.configuration?.pjsip,
796
+ source.data?.data?.pjsip,
797
+ source.data?.data?.configuration?.pjsip,
798
+ source.configuration,
799
+ source.data?.configuration,
800
+ source.data?.data?.configuration,
801
+ source,
802
+ ];
803
+
804
+ return candidates.find((candidate) => isPjsipCandidate(candidate));
805
+ }
806
+
807
+ function isPjsipCandidate(value: unknown): value is Record<string, any> {
808
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
809
+ return pjsipKeys.some((key) => Object.prototype.hasOwnProperty.call(value, key)) ||
810
+ ["serverAddress", "serverIp", "serverPort", "extension", "peerToPeerEnabled"].some((key) => Object.prototype.hasOwnProperty.call(value, key));
811
+ }
812
+
813
+ function getPjsipValue(source: Record<string, any>, keys: string[]) {
814
+ for (const key of keys) {
815
+ if (source?.[key] !== undefined && source?.[key] !== null && source?.[key] !== "") return source[key];
816
+ }
817
+ return undefined;
818
+ }
819
+
820
+ function normalizeStatus(response: Record<string, any>, pjsip: Record<string, any>) {
821
+ const status = Number(response?.status);
822
+ if (status === 200) return "Connected";
823
+ if (status === -1 || toFlag(pjsip.enabled) === "0") return "Disabled";
824
+ if (status === 0 || status === 100) return "Connecting";
825
+ if ([401, 403, 408, 503].includes(status)) return "Failed";
826
+ return "Not connected";
827
+ }
828
+
829
+ function normalizeContacts(reader: Record<string, any>, response: Record<string, any>): ContactRow[] {
830
+ const items = response?.contacts ?? response?.values ?? response?.data?.contacts ?? response?.data ?? [];
831
+ return (Array.isArray(items) ? items : []).map((contact, index) => ({
832
+ _rowKey: `${reader._id}-${contact.id ?? index}`,
833
+ id: toNumber(contact.id, undefined),
834
+ readerId: reader._id,
835
+ name: toText(contact.name, "N/A"),
836
+ location: reader.location || "N/A",
837
+ serverAddress: toText(contact.number || contact.serverAddress, "N/A"),
838
+ dialCode: toText(contact.number || contact.serverAddress, "N/A"),
839
+ }));
840
+ }
841
+
842
+ function openViewDialog(row: IntercomRow) {
843
+ selectedRow.value = row;
844
+ viewDialog.value = true;
845
+ }
846
+
847
+ function openSipDialog(row: IntercomRow) {
848
+ selectedRow.value = row;
849
+ sipForm.enabled = row.sipEnabled;
850
+ sipForm.serverIp = row.serverAddress === "N/A" ? "" : row.serverAddress;
851
+ sipForm.serverPort = row.serverPort === "N/A" ? "5060" : row.serverPort;
852
+ sipForm.initialRtpPort = row.initialRtpPort === "N/A" ? "10000" : row.initialRtpPort;
853
+ sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "1000" : row.finalRtpPort;
854
+ sipForm.numericExtension = row.numericExtension;
855
+ sipForm.extension = row.extension === "N/A" ? "" : row.extension;
856
+ sipForm.login = row.login === "N/A" ? "" : row.login;
857
+ sipForm.password = row.password;
858
+ sipForm.operatingMode = row.operatingMode === "Peer-to-Peer" ? "peer-to-peer" : "sip-client";
859
+ sipForm.protocol = row.protocol || "UDP";
860
+ sipDialog.value = true;
861
+ }
862
+
863
+ function openCallDialog(row: IntercomRow) {
864
+ selectedRow.value = row;
865
+ callForm.dialMode = row.dialMode || "0";
866
+ callForm.autoAnswer = row.autoAnswer;
867
+ callForm.autoAnswerDelay = row.autoAnswerDelay === "N/A" ? "0" : row.autoAnswerDelay;
868
+ callForm.registrationTime = row.registrationTime === "N/A" ? "0" : row.registrationTime;
869
+ callForm.keepAlive = row.keepAlive === "N/A" ? "0" : row.keepAlive;
870
+ callForm.maxCallTime = row.maxCallTime === "N/A" ? "0" : row.maxCallTime;
871
+ callForm.dialButton = row.dialButton;
872
+ callForm.externalDialButton = row.externalDialButton;
873
+ callForm.unknownContact = row.unknownContact;
874
+ callForm.callAfter = row.callAfter === "N/A" ? "0" : row.callAfter;
875
+ callDialog.value = true;
876
+ }
877
+
878
+ function openDialCodeDialog(row: IntercomRow) {
879
+ selectedRow.value = row;
880
+ dialForm.enabled = row.dialCode !== "N/A" && row.dialCode !== "";
881
+ dialForm.code = row.dialCode === "N/A" ? "" : row.dialCode;
882
+ dialCodeDialog.value = true;
883
+ }
884
+
885
+ function openContactDialog(contact?: ContactRow) {
886
+ selectedContact.value = contact || null;
887
+ contactForm.id = contact?.id;
888
+ contactForm.readerId = contact?.readerId || readers.value[0]?._id || "";
889
+ contactForm.name = contact?.name === "N/A" ? "" : contact?.name || "";
890
+ contactForm.location = contact?.location === "N/A" ? "" : contact?.location || readers.value[0]?.location || "";
891
+ contactForm.serverAddress = contact?.serverAddress === "N/A" ? "" : contact?.serverAddress || "";
892
+ contactDialog.value = true;
893
+ }
894
+
895
+ function openContactView(contact: ContactRow) {
896
+ selectedContact.value = contact;
897
+ contactViewDialog.value = true;
898
+ }
899
+
900
+ function openDeleteContact(contact: ContactRow) {
901
+ selectedContact.value = contact;
902
+ deleteContactDialog.value = true;
903
+ }
904
+
905
+ async function saveSipSettings() {
906
+ if (!selectedRow.value) return;
907
+ const isPeerToPeer = sipForm.operatingMode === "peer-to-peer";
908
+ const saved = await savePjsip(selectedRow.value.reader._id, {
909
+ enabled: sipForm.enabled ? "1" : "0",
910
+ server_ip: sipForm.serverIp,
911
+ server_port: sipForm.serverPort,
912
+ server_outbound_port: sipForm.initialRtpPort,
913
+ server_outbound_port_range: sipForm.finalRtpPort,
914
+ numeric_branch_enabled: sipForm.numericExtension ? "1" : "0",
915
+ branch: isPeerToPeer ? "" : sipForm.extension,
916
+ login: isPeerToPeer ? "" : sipForm.login,
917
+ password: isPeerToPeer ? "" : sipForm.password,
918
+ peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
919
+ }, { showSuccessMessage: false });
920
+ if (saved) {
921
+ sipDialog.value = false;
922
+ rebootConfirmDialog.value = true;
923
+ }
924
+ }
925
+
926
+ async function saveCallSettings() {
927
+ if (!selectedRow.value) return;
928
+ await savePjsip(selectedRow.value.reader._id, {
929
+ dialing_display_mode: callForm.dialMode,
930
+ auto_answer_enabled: callForm.autoAnswer ? "1" : "0",
931
+ auto_answer_delay: callForm.autoAnswerDelay,
932
+ reg_status_query_period: callForm.registrationTime,
933
+ server_retry_interval: callForm.keepAlive,
934
+ max_call_time: callForm.maxCallTime,
935
+ auto_call_button_enabled: callForm.dialButton ? "1" : "0",
936
+ rex_enabled: callForm.externalDialButton ? "1" : "0",
937
+ });
938
+ callDialog.value = false;
939
+ }
940
+
941
+ async function saveDialCode() {
942
+ if (!selectedRow.value) return;
943
+ await savePjsip(selectedRow.value.reader._id, {
944
+ open_door_enabled: dialForm.enabled ? "1" : "0",
945
+ open_door_command: dialForm.enabled ? dialForm.code : "",
946
+ });
947
+ dialCodeDialog.value = false;
948
+ }
949
+
950
+ async function savePjsip(readerId: string, payload: Record<string, string>, options: { showSuccessMessage?: boolean } = {}) {
951
+ saving.value = true;
952
+ try {
953
+ await setReaderConfiguration(readerId, { pjsip: payload });
954
+ await reload();
955
+ if (options.showSuccessMessage !== false) {
956
+ showMessage("Intercom settings saved.", "success");
957
+ }
958
+ return true;
959
+ } catch (error: any) {
960
+ showMessage(getErrorMessage(error), "error");
961
+ return false;
962
+ } finally {
963
+ saving.value = false;
964
+ }
965
+ }
966
+
967
+ function startRebootWait() {
968
+ rebootConfirmDialog.value = false;
969
+ rebootCountdown.value = 59;
970
+ rebootingDialog.value = true;
971
+ clearRebootTimer();
972
+
973
+ rebootTimer = setInterval(() => {
974
+ rebootCountdown.value -= 1;
975
+ if (rebootCountdown.value <= 0) {
976
+ clearRebootTimer();
977
+ rebootingDialog.value = false;
978
+ showMessage("Equipment reboot wait completed.", "success");
979
+ reload();
980
+ }
981
+ }, 1000);
982
+ }
983
+
984
+ function clearRebootTimer() {
985
+ if (rebootTimer) {
986
+ clearInterval(rebootTimer);
987
+ rebootTimer = undefined;
988
+ }
989
+ }
990
+
991
+ async function saveContact() {
992
+ if (!contactForm.readerId || !contactForm.name.trim() || !contactForm.serverAddress.trim()) {
993
+ showMessage("Please complete the required contact fields.", "warning");
994
+ return;
995
+ }
996
+
997
+ contactsSaving.value = true;
998
+ try {
999
+ if (contactForm.id) {
1000
+ await runObjectOperation(contactForm.readerId, {
1001
+ operation: "modify",
1002
+ object: "contacts",
1003
+ values: {
1004
+ name: contactForm.name.trim(),
1005
+ number: contactForm.serverAddress.trim(),
1006
+ },
1007
+ where: { contacts: { id: contactForm.id } },
1008
+ });
1009
+ } else {
1010
+ await runObjectOperation(contactForm.readerId, {
1011
+ operation: "create",
1012
+ object: "contacts",
1013
+ values: [{
1014
+ name: contactForm.name.trim(),
1015
+ number: contactForm.serverAddress.trim(),
1016
+ }],
1017
+ });
1018
+ }
1019
+ contactDialog.value = false;
1020
+ await loadContacts();
1021
+ showMessage("Intercom contact saved.", "success");
1022
+ } catch (error: any) {
1023
+ showMessage(getErrorMessage(error), "error");
1024
+ } finally {
1025
+ contactsSaving.value = false;
1026
+ }
1027
+ }
1028
+
1029
+ async function deleteContact() {
1030
+ if (!selectedContact.value?.id) return;
1031
+ contactsSaving.value = true;
1032
+ try {
1033
+ await runObjectOperation(selectedContact.value.readerId, {
1034
+ operation: "destroy",
1035
+ object: "contacts",
1036
+ where: { contacts: { id: selectedContact.value.id } },
1037
+ });
1038
+ deleteContactDialog.value = false;
1039
+ await loadContacts();
1040
+ showMessage("Intercom contact deleted.", "success");
1041
+ } catch (error: any) {
1042
+ showMessage(getErrorMessage(error), "error");
1043
+ } finally {
1044
+ contactsSaving.value = false;
1045
+ }
1046
+ }
1047
+
1048
+ function getStatusClass(status: string) {
1049
+ if (status === "Connected") return "status-success";
1050
+ if (status === "Disabled" || status === "Not connected") return "status-muted";
1051
+ if (status === "Failed") return "status-error";
1052
+ return "status-warning";
1053
+ }
1054
+
1055
+ function getDialModeLabel(value: string) {
1056
+ if (value === "1") return "From the contact list";
1057
+ if (value === "2") return "Numeric Keypad + Contacts";
1058
+ return "Automatic";
1059
+ }
1060
+
1061
+ function maskValue(value?: string) {
1062
+ if (!value || value === "N/A") return "N/A";
1063
+ return "*****";
1064
+ }
1065
+
1066
+ function showMessage(text: string, type: MessageType = "info") {
1067
+ message.text = text;
1068
+ message.type = type;
1069
+ }
1070
+
1071
+ function getErrorMessage(error: any) {
1072
+ return error?.data?.message || error?.response?._data?.message || error?.message || "HID intercom request failed.";
1073
+ }
1074
+
1075
+ function toText(value: unknown, fallback = "") {
1076
+ if (value === undefined || value === null || value === "") return fallback;
1077
+ return String(value);
1078
+ }
1079
+
1080
+ function toFlag(value: unknown, fallback = "0") {
1081
+ const text = toText(value, fallback);
1082
+ return text === "1" || text === "true" ? "1" : "0";
1083
+ }
1084
+
1085
+ function toNumber(value: unknown, fallback?: number) {
1086
+ const number = Number(value);
1087
+ return Number.isFinite(number) ? number : fallback as number;
1088
+ }
1089
+ </script>
1090
+
1091
+ <script lang="ts">
1092
+ export default {
1093
+ components: {
1094
+ FieldLabel: {
1095
+ props: {
1096
+ text: String,
1097
+ required: Boolean,
1098
+ },
1099
+ template: `
1100
+ <div class="field-label">{{ text }} <span v-if="required">*</span></div>
1101
+ `,
1102
+ },
1103
+ ToggleField: {
1104
+ props: {
1105
+ modelValue: Boolean,
1106
+ label: String,
1107
+ },
1108
+ emits: ["update:modelValue"],
1109
+ template: `
1110
+ <div class="toggle-field">
1111
+ <span>{{ label }}</span>
1112
+ <v-switch
1113
+ :model-value="modelValue"
1114
+ color="success"
1115
+ density="compact"
1116
+ hide-details
1117
+ @update:model-value="$emit('update:modelValue', $event)"
1118
+ />
1119
+ </div>
1120
+ `,
1121
+ },
1122
+ },
1123
+ };
1124
+ </script>
1125
+
1126
+ <style scoped>
1127
+ .hid-intercom-management {
1128
+ display: flex;
1129
+ flex-direction: column;
1130
+ padding: 12px;
1131
+ }
1132
+
1133
+ .top-bar {
1134
+ display: flex;
1135
+ align-items: center;
1136
+ gap: 12px;
1137
+ margin-bottom: 12px;
1138
+ }
1139
+
1140
+ .toolbar-spacer {
1141
+ flex: 1 1 auto;
1142
+ }
1143
+
1144
+ .top-placeholder {
1145
+ width: 160px;
1146
+ }
1147
+
1148
+ .add-button {
1149
+ min-width: 160px;
1150
+ background: #dedede;
1151
+ color: #0b2c40;
1152
+ font-size: 12px;
1153
+ }
1154
+
1155
+ .search-input {
1156
+ max-width: 220px;
1157
+ }
1158
+
1159
+ .status-input {
1160
+ max-width: 160px;
1161
+ }
1162
+
1163
+ .table-card {
1164
+ display: flex;
1165
+ flex-direction: column;
1166
+ min-height: 520px;
1167
+ overflow: hidden;
1168
+ background: #fff;
1169
+ border-color: #e2e5e9;
1170
+ border-radius: 6px;
1171
+ }
1172
+
1173
+ .table-refresh {
1174
+ display: flex;
1175
+ align-items: center;
1176
+ min-height: 42px;
1177
+ padding: 0 12px;
1178
+ background: #f6f6f6;
1179
+ color: #0b2c40;
1180
+ font-size: 12px;
1181
+ }
1182
+
1183
+ .intercom-tabs {
1184
+ border-bottom: 1px solid #e5e7eb;
1185
+ }
1186
+
1187
+ .intercom-tabs :deep(.v-tab) {
1188
+ flex: 1 1 0;
1189
+ min-width: 0;
1190
+ font-size: 12px;
1191
+ text-transform: none;
1192
+ }
1193
+
1194
+ .intercom-tabs :deep(.v-tab--selected) {
1195
+ color: #0b4ea2;
1196
+ }
1197
+
1198
+ .table-body {
1199
+ position: relative;
1200
+ flex: 1 1 auto;
1201
+ min-height: 0;
1202
+ overflow: auto;
1203
+ }
1204
+
1205
+ .table-body :deep(table) {
1206
+ table-layout: fixed;
1207
+ min-width: 840px;
1208
+ }
1209
+
1210
+ .table-body :deep(th) {
1211
+ height: 48px;
1212
+ color: #0b2c40;
1213
+ font-size: 12px;
1214
+ font-weight: 500;
1215
+ }
1216
+
1217
+ .table-body :deep(td) {
1218
+ height: 54px;
1219
+ color: #1f2937;
1220
+ font-size: 12px;
1221
+ }
1222
+
1223
+ .table-footer {
1224
+ display: flex;
1225
+ flex: 0 0 auto;
1226
+ align-items: center;
1227
+ justify-content: flex-end;
1228
+ gap: 4px;
1229
+ min-height: 42px;
1230
+ padding: 0 12px;
1231
+ background: #f6f6f6;
1232
+ color: #0b2c40;
1233
+ font-size: 12px;
1234
+ }
1235
+
1236
+ .empty-state {
1237
+ display: grid;
1238
+ min-height: 180px;
1239
+ place-items: center;
1240
+ color: #667085;
1241
+ font-size: 13px;
1242
+ }
1243
+
1244
+ .row-menu {
1245
+ min-width: 160px;
1246
+ border-radius: 6px;
1247
+ }
1248
+
1249
+ .link-text {
1250
+ color: #1565c0;
1251
+ }
1252
+
1253
+ .success-text {
1254
+ color: #2faf4a;
1255
+ }
1256
+
1257
+ .status-success {
1258
+ background: #46b85a;
1259
+ color: #fff;
1260
+ }
1261
+
1262
+ .status-muted {
1263
+ background: #a7a7a7;
1264
+ color: #fff;
1265
+ }
1266
+
1267
+ .status-warning {
1268
+ background: #fff2cc;
1269
+ color: #8a5b00;
1270
+ }
1271
+
1272
+ .status-error {
1273
+ background: #f9d0d0;
1274
+ color: #991b1b;
1275
+ }
1276
+
1277
+ .form-dialog,
1278
+ .detail-dialog {
1279
+ overflow: hidden;
1280
+ background: #fff;
1281
+ }
1282
+
1283
+ .sip-settings-dialog {
1284
+ width: 430px;
1285
+ max-width: 92vw;
1286
+ }
1287
+
1288
+ .dialog-title {
1289
+ min-height: 48px;
1290
+ padding: 14px 16px;
1291
+ background: #f4f4f4;
1292
+ color: #0d1720;
1293
+ font-size: 14px;
1294
+ font-weight: 700;
1295
+ }
1296
+
1297
+ .dialog-body,
1298
+ .detail-body {
1299
+ padding: 16px;
1300
+ }
1301
+
1302
+ .sip-settings-dialog .dialog-body {
1303
+ padding: 26px 28px 24px;
1304
+ }
1305
+
1306
+ .call-dialog .dialog-body {
1307
+ max-height: 72vh;
1308
+ overflow: auto;
1309
+ }
1310
+
1311
+ .field-label {
1312
+ margin: 12px 0 7px;
1313
+ color: #344054;
1314
+ font-size: 10px;
1315
+ font-weight: 500;
1316
+ }
1317
+
1318
+ .field-label span {
1319
+ color: #d92d20;
1320
+ }
1321
+
1322
+ .sip-settings-dialog .field-label {
1323
+ margin-top: 14px;
1324
+ }
1325
+
1326
+ .sip-label {
1327
+ margin: 12px 0 6px;
1328
+ color: #263542;
1329
+ font-size: 10px;
1330
+ font-weight: 500;
1331
+ line-height: 1.2;
1332
+ }
1333
+
1334
+ .sip-label span {
1335
+ color: #e5484d;
1336
+ }
1337
+
1338
+ .sip-switch-row {
1339
+ display: flex;
1340
+ align-items: center;
1341
+ justify-content: space-between;
1342
+ min-height: 40px;
1343
+ margin-bottom: 12px;
1344
+ padding: 0 12px;
1345
+ border: 1px solid #dfe3e8;
1346
+ border-radius: 3px;
1347
+ background: #fff;
1348
+ color: #25313b;
1349
+ font-size: 12px;
1350
+ }
1351
+
1352
+ .sip-section-switch {
1353
+ margin-top: 18px;
1354
+ margin-bottom: 12px;
1355
+ }
1356
+
1357
+ .sip-switch-row :deep(.v-switch) {
1358
+ flex: 0 0 auto;
1359
+ }
1360
+
1361
+ .sip-switch-row :deep(.v-selection-control) {
1362
+ min-height: 32px;
1363
+ }
1364
+
1365
+ .sip-switch-row :deep(.v-selection-control__wrapper) {
1366
+ height: 28px;
1367
+ }
1368
+
1369
+ .sip-switch-row :deep(.v-switch__track) {
1370
+ height: 24px;
1371
+ min-width: 42px;
1372
+ opacity: 1;
1373
+ }
1374
+
1375
+ .sip-switch-row :deep(.v-switch__thumb) {
1376
+ background-color: #fff !important;
1377
+ color: #fff !important;
1378
+ height: 20px;
1379
+ width: 20px;
1380
+ }
1381
+
1382
+ .sip-switch-row :deep(.v-selection-control--dirty .v-switch__thumb) {
1383
+ background-color: #fff !important;
1384
+ color: #fff !important;
1385
+ }
1386
+
1387
+ .sip-switch-row :deep(.v-selection-control--dirty .v-switch__thumb::before) {
1388
+ background-color: #fff !important;
1389
+ }
1390
+
1391
+ .toggle-field {
1392
+ display: flex;
1393
+ align-items: center;
1394
+ justify-content: space-between;
1395
+ min-height: 38px;
1396
+ margin-bottom: 8px;
1397
+ padding: 0 10px;
1398
+ border: 1px solid #e5e7eb;
1399
+ border-radius: 3px;
1400
+ color: #344054;
1401
+ font-size: 12px;
1402
+ }
1403
+
1404
+ .sip-input :deep(.v-field) {
1405
+ min-height: 40px;
1406
+ border-radius: 3px;
1407
+ background: #fff;
1408
+ font-size: 12px;
1409
+ box-shadow: none;
1410
+ }
1411
+
1412
+ .sip-input :deep(.v-field__outline) {
1413
+ --v-field-border-opacity: 1;
1414
+ color: #dfe3e8;
1415
+ }
1416
+
1417
+ .sip-input :deep(.v-field--focused .v-field__outline) {
1418
+ color: #9db7d1;
1419
+ }
1420
+
1421
+ .sip-input :deep(.v-field__outline__start),
1422
+ .sip-input :deep(.v-field__outline__end) {
1423
+ border-radius: 3px;
1424
+ }
1425
+
1426
+ .sip-input :deep(.v-field__input) {
1427
+ min-height: 40px;
1428
+ padding-top: 0;
1429
+ padding-bottom: 0;
1430
+ padding-inline: 12px;
1431
+ color: #4a5562;
1432
+ font-size: 12px;
1433
+ }
1434
+
1435
+ .sip-input :deep(.v-field__append-inner) {
1436
+ min-height: 40px;
1437
+ color: #697586;
1438
+ }
1439
+
1440
+ .sip-input :deep(.v-field--disabled) {
1441
+ background: #f7f8fa;
1442
+ opacity: 1;
1443
+ }
1444
+
1445
+ .sip-input :deep(.v-field--disabled .v-field__input),
1446
+ .sip-input :deep(.v-field--disabled input::placeholder) {
1447
+ color: #98a2b3;
1448
+ opacity: 1;
1449
+ }
1450
+
1451
+ .sip-input :deep(input::placeholder) {
1452
+ color: #697586;
1453
+ opacity: 1;
1454
+ }
1455
+
1456
+ .form-two-col {
1457
+ display: grid;
1458
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
1459
+ gap: 12px;
1460
+ }
1461
+
1462
+ .mode-toggle {
1463
+ display: grid;
1464
+ width: 100%;
1465
+ grid-template-columns: 1fr 1fr;
1466
+ margin-bottom: 6px;
1467
+ border: 1px solid #dfe3e8;
1468
+ border-radius: 3px;
1469
+ overflow: hidden;
1470
+ }
1471
+
1472
+ .mode-toggle :deep(.v-btn) {
1473
+ min-width: 0;
1474
+ min-height: 40px;
1475
+ border-radius: 0;
1476
+ color: #667085;
1477
+ font-size: 12px;
1478
+ font-weight: 400;
1479
+ letter-spacing: 0;
1480
+ }
1481
+
1482
+ .mode-toggle :deep(.v-btn--active) {
1483
+ background: #082b40;
1484
+ color: #fff;
1485
+ }
1486
+
1487
+ .mode-toggle :deep(.v-btn--disabled) {
1488
+ background: #f7f8fa;
1489
+ color: #98a2b3;
1490
+ opacity: 1;
1491
+ }
1492
+
1493
+ .sip-settings-dialog :deep(.v-radio-group) {
1494
+ margin-top: 0;
1495
+ }
1496
+
1497
+ .sip-settings-dialog :deep(.v-selection-control-group--inline) {
1498
+ gap: 34px;
1499
+ min-height: 34px;
1500
+ }
1501
+
1502
+ .sip-settings-dialog :deep(.v-radio .v-label) {
1503
+ color: #344054;
1504
+ font-size: 12px;
1505
+ }
1506
+
1507
+ .sip-settings-dialog :deep(.v-radio .v-selection-control__wrapper) {
1508
+ width: 28px;
1509
+ }
1510
+
1511
+ .sip-protocol {
1512
+ margin-bottom: 8px;
1513
+ }
1514
+
1515
+ .call-dialog {
1516
+ overflow: hidden;
1517
+ }
1518
+
1519
+ .reboot-dialog {
1520
+ position: relative;
1521
+ min-height: 220px;
1522
+ overflow: hidden;
1523
+ background: #fff;
1524
+ }
1525
+
1526
+ .reboot-close {
1527
+ position: absolute;
1528
+ top: 14px;
1529
+ right: 14px;
1530
+ display: grid;
1531
+ width: 40px;
1532
+ height: 40px;
1533
+ padding: 0;
1534
+ border: 0;
1535
+ place-items: center;
1536
+ background: transparent;
1537
+ color: #c7c7c7;
1538
+ cursor: pointer;
1539
+ }
1540
+
1541
+ .reboot-title {
1542
+ padding: 24px 20px 12px;
1543
+ color: #111;
1544
+ font-size: 28px;
1545
+ font-weight: 400;
1546
+ letter-spacing: 0;
1547
+ line-height: 1.2;
1548
+ }
1549
+
1550
+ .reboot-message {
1551
+ padding: 24px 20px 28px;
1552
+ color: #222;
1553
+ font-size: 17px;
1554
+ line-height: 1.45;
1555
+ }
1556
+
1557
+ .reboot-actions {
1558
+ display: flex;
1559
+ justify-content: flex-end;
1560
+ gap: 20px;
1561
+ padding: 0 20px 20px;
1562
+ }
1563
+
1564
+ .reboot-cancel {
1565
+ color: #075fb7;
1566
+ font-size: 16px;
1567
+ letter-spacing: 0;
1568
+ }
1569
+
1570
+ .reboot-ok {
1571
+ min-width: 64px;
1572
+ border-radius: 5px;
1573
+ background: #062f4d;
1574
+ color: #fff;
1575
+ font-size: 16px;
1576
+ letter-spacing: 0;
1577
+ }
1578
+
1579
+ .rebooting-card {
1580
+ display: grid;
1581
+ min-height: 240px;
1582
+ grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
1583
+ align-items: center;
1584
+ gap: 24px;
1585
+ padding: 20px 54px;
1586
+ background: #fff;
1587
+ }
1588
+
1589
+ .wait-word {
1590
+ color: #347bd6;
1591
+ font-size: 120px;
1592
+ font-weight: 400;
1593
+ letter-spacing: 0;
1594
+ line-height: 0.95;
1595
+ }
1596
+
1597
+ .rebooting-copy h3 {
1598
+ margin: 0 0 18px;
1599
+ color: #000;
1600
+ font-size: 30px;
1601
+ font-weight: 400;
1602
+ letter-spacing: 0;
1603
+ }
1604
+
1605
+ .rebooting-copy p {
1606
+ margin: 0;
1607
+ color: #000;
1608
+ font-size: 16px;
1609
+ }
1610
+
1611
+ .call-dialog .dialog-body {
1612
+ padding: 18px 24px 24px;
1613
+ }
1614
+
1615
+ .call-label {
1616
+ margin: 8px 0 6px;
1617
+ color: #1f2937;
1618
+ font-size: 10px;
1619
+ font-weight: 500;
1620
+ letter-spacing: 0;
1621
+ }
1622
+
1623
+ .call-label span {
1624
+ color: #d92d20;
1625
+ }
1626
+
1627
+ .call-radio-list {
1628
+ margin-bottom: 12px;
1629
+ }
1630
+
1631
+ .call-radio-list :deep(.v-input__control) {
1632
+ width: 100%;
1633
+ }
1634
+
1635
+ .call-radio-list :deep(.v-selection-control-group) {
1636
+ gap: 8px;
1637
+ }
1638
+
1639
+ .call-radio-list :deep(.v-selection-control) {
1640
+ min-height: 36px;
1641
+ margin-bottom: 0;
1642
+ padding: 0 10px;
1643
+ border: 1px solid #dfe3e8;
1644
+ border-radius: 3px;
1645
+ background: #fff;
1646
+ }
1647
+
1648
+ .call-radio-list :deep(.v-selection-control__wrapper) {
1649
+ width: 24px;
1650
+ }
1651
+
1652
+ .call-radio-list :deep(.v-selection-control__input) {
1653
+ transform: scale(0.9);
1654
+ }
1655
+
1656
+ .call-radio-list :deep(.v-label) {
1657
+ color: #475467;
1658
+ font-size: 11px;
1659
+ letter-spacing: 0;
1660
+ opacity: 1;
1661
+ }
1662
+
1663
+ .call-toggle-row {
1664
+ display: flex;
1665
+ align-items: center;
1666
+ justify-content: space-between;
1667
+ min-height: 36px;
1668
+ margin-bottom: 10px;
1669
+ padding: 0 10px;
1670
+ border: 1px solid #e5e7eb;
1671
+ border-radius: 3px;
1672
+ background: #fff;
1673
+ color: #1f2937;
1674
+ font-size: 11px;
1675
+ letter-spacing: 0;
1676
+ }
1677
+
1678
+ .call-toggle-row :deep(.v-selection-control) {
1679
+ min-height: 28px;
1680
+ }
1681
+
1682
+ .call-toggle-row :deep(.v-selection-control__wrapper) {
1683
+ height: 28px;
1684
+ }
1685
+
1686
+ .call-toggle-row :deep(.v-switch__track) {
1687
+ height: 22px;
1688
+ min-width: 40px;
1689
+ opacity: 1;
1690
+ }
1691
+
1692
+ .call-toggle-row :deep(.v-switch__thumb) {
1693
+ width: 18px;
1694
+ height: 18px;
1695
+ background-color: #fff !important;
1696
+ color: #fff !important;
1697
+ }
1698
+
1699
+ .call-input {
1700
+ margin-bottom: 8px;
1701
+ }
1702
+
1703
+ .call-input :deep(.v-field) {
1704
+ min-height: 38px;
1705
+ border-radius: 3px;
1706
+ background: #fff;
1707
+ box-shadow: none;
1708
+ }
1709
+
1710
+ .call-input :deep(.v-field__outline) {
1711
+ --v-field-border-opacity: 1;
1712
+ color: #cfd5dc;
1713
+ }
1714
+
1715
+ .call-input :deep(.v-field__input) {
1716
+ min-height: 38px;
1717
+ padding-top: 0;
1718
+ padding-bottom: 0;
1719
+ padding-inline: 12px;
1720
+ color: #4a4f55;
1721
+ font-size: 12px;
1722
+ letter-spacing: 0;
1723
+ }
1724
+
1725
+ .call-input :deep(.v-field--disabled) {
1726
+ background: #f7f8fa;
1727
+ opacity: 1;
1728
+ }
1729
+
1730
+ .call-input :deep(.v-field--disabled .v-field__input),
1731
+ .call-input :deep(.v-field--disabled input::placeholder) {
1732
+ color: #98a2b3;
1733
+ opacity: 1;
1734
+ }
1735
+
1736
+ .dial-code-dialog .dialog-body {
1737
+ padding: 22px 24px 26px;
1738
+ }
1739
+
1740
+ .dial-toggle-row {
1741
+ margin-bottom: 10px;
1742
+ }
1743
+
1744
+ .dial-input {
1745
+ margin-bottom: 0;
1746
+ }
1747
+
1748
+ .call-two-col {
1749
+ gap: 12px;
1750
+ }
1751
+
1752
+ .call-upload-row,
1753
+ .upload-row {
1754
+ display: grid;
1755
+ grid-template-columns: minmax(0, 1fr) 42px;
1756
+ gap: 8px;
1757
+ margin: 8px 0 10px;
1758
+ }
1759
+
1760
+ .call-upload-row {
1761
+ grid-template-columns: minmax(0, 1fr) 40px;
1762
+ }
1763
+
1764
+ .upload-drop {
1765
+ display: flex;
1766
+ align-items: center;
1767
+ justify-content: center;
1768
+ gap: 6px;
1769
+ min-height: 38px;
1770
+ border: 1px dashed #c9d2dc;
1771
+ color: #475467;
1772
+ font-size: 11px;
1773
+ letter-spacing: 0;
1774
+ }
1775
+
1776
+ .upload-drop.is-disabled {
1777
+ background: #fafafa;
1778
+ color: #98a2b3;
1779
+ }
1780
+
1781
+ .upload-button {
1782
+ width: 40px;
1783
+ height: 38px;
1784
+ border-radius: 3px;
1785
+ background: #1976d2;
1786
+ color: #fff;
1787
+ }
1788
+
1789
+ .upload-button.v-btn--disabled {
1790
+ background: #eceff3;
1791
+ color: #c0c6cf;
1792
+ opacity: 1;
1793
+ }
1794
+
1795
+ .call-file-row {
1796
+ display: flex;
1797
+ align-items: center;
1798
+ justify-content: space-between;
1799
+ min-height: 32px;
1800
+ margin: 0 0 10px;
1801
+ color: #475467;
1802
+ font-size: 11px;
1803
+ }
1804
+
1805
+ .call-file-name {
1806
+ display: inline-flex;
1807
+ align-items: center;
1808
+ gap: 8px;
1809
+ }
1810
+
1811
+ .call-file-name .v-icon {
1812
+ color: #2fb344;
1813
+ }
1814
+
1815
+ .detail-section-title {
1816
+ margin: 14px 0 8px;
1817
+ color: #0d1720;
1818
+ font-size: 13px;
1819
+ font-weight: 700;
1820
+ }
1821
+
1822
+ .detail-section-title:first-child {
1823
+ margin-top: 0;
1824
+ }
1825
+
1826
+ .detail-row {
1827
+ display: flex;
1828
+ align-items: center;
1829
+ justify-content: space-between;
1830
+ gap: 16px;
1831
+ min-height: 26px;
1832
+ color: #667085;
1833
+ font-size: 11px;
1834
+ }
1835
+
1836
+ .detail-row strong {
1837
+ color: #101828;
1838
+ font-weight: 500;
1839
+ text-align: right;
1840
+ }
1841
+
1842
+ .dialog-actions {
1843
+ display: grid;
1844
+ grid-template-columns: 1fr 1fr;
1845
+ border-top: 1px solid #e5e7eb;
1846
+ }
1847
+
1848
+ .sip-settings-dialog .dialog-actions {
1849
+ min-height: 46px;
1850
+ }
1851
+
1852
+ .action-cancel {
1853
+ background: #fff;
1854
+ color: #0d1720;
1855
+ border-radius: 0;
1856
+ }
1857
+
1858
+ .action-submit {
1859
+ background: #082b40;
1860
+ color: #fff;
1861
+ border-radius: 0;
1862
+ }
1863
+
1864
+ .full-action {
1865
+ grid-column: 1 / -1;
1866
+ }
1867
+
1868
+ @media (max-width: 900px) {
1869
+ .top-bar {
1870
+ align-items: stretch;
1871
+ flex-direction: column;
1872
+ }
1873
+
1874
+ .top-placeholder,
1875
+ .search-input,
1876
+ .status-input {
1877
+ width: 100%;
1878
+ max-width: none;
1879
+ }
1880
+
1881
+ .rebooting-card {
1882
+ grid-template-columns: 1fr;
1883
+ padding: 28px;
1884
+ }
1885
+
1886
+ .wait-word {
1887
+ font-size: 80px;
1888
+ }
1889
+ }
1890
+ </style>