@7365admin1/layer-common 3.0.18 → 3.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 3.0.20
4
+
5
+ ### Patch Changes
6
+
7
+ - ea81bba: add dialog confirm for client and update service provider
8
+
9
+ ## 3.0.19
10
+
11
+ ### Patch Changes
12
+
13
+ - 96c813f: Update Layer Common version
14
+
3
15
  ## 3.0.18
4
16
 
5
17
  ### Patch Changes
@@ -15,8 +15,8 @@
15
15
  <v-btn fab icon density="comfortable" @click="getBuildings()">
16
16
  <v-icon>mdi-refresh</v-icon>
17
17
  </v-btn>
18
- <v-text-field v-model="searchInput" autocomplete="off" placeholder="Search" density="compact" hide-details dense clearable
19
- class="mx-2" style="width: 200px;" />
18
+ <v-text-field v-model="searchInput" autocomplete="off" placeholder="Search" density="compact" hide-details
19
+ dense clearable class="mx-2" style="width: 200px;" />
20
20
  </template>
21
21
 
22
22
  <template #append>
@@ -45,7 +45,7 @@
45
45
  <v-dialog v-model="createDialog" width="450" persistent>
46
46
  <BuildingForm :site="site" mode="add" :blocks="items" @cancel="createDialog = false" @success="successCreate()"
47
47
  @success:create-more="getBuildings()" />
48
- </v-dialog>
48
+ </v-dialog>
49
49
 
50
50
  <!-- Edit Dialog -->
51
51
  <v-dialog v-model="editDialog" width="450" persistent>
@@ -222,7 +222,10 @@ const site = (useRoute().params.site as string) ?? "";
222
222
  const status = (useRoute().params.status as string) ?? "active";
223
223
 
224
224
  const { debounce } = useUtils();
225
- const { getAll: _getBuildings, deleteById } = useBuilding();
225
+ const {
226
+ getAll: _getBuildings,
227
+ deleteById,
228
+ } = useBuilding();
226
229
 
227
230
  const searchInput = ref("");
228
231
  const headerSearch = ref("");
@@ -6,6 +6,13 @@
6
6
  v-if="canCreate && canCreateUnit">
7
7
  Add Unit
8
8
  </v-btn>
9
+ <v-btn class="text-none ml-2" rounded="pill" variant="tonal" @click="openBulkUploadFilePicker()" size="large"
10
+ :loading="bulkUploadLoading" v-if="canCreate && canCreateUnit">
11
+ <v-icon start>mdi-upload</v-icon>
12
+ Bulk Upload
13
+ </v-btn>
14
+ <input ref="bulkUploadFileInput" type="file" accept=".xlsx,.xls,.csv" class="d-none"
15
+ @change="handleBulkUploadFileChange" />
9
16
  </v-row>
10
17
  </v-col>
11
18
  <v-col cols="12">
@@ -30,8 +37,7 @@
30
37
  </v-toolbar>
31
38
 
32
39
  <v-data-table :headers="props.headers" :items="items" item-value="_id" items-per-page="20" fixed-header
33
- hide-default-footer @click:row="tableRowClickHandler"
34
- style="max-height: calc(100vh - (200px))">
40
+ hide-default-footer @click:row="tableRowClickHandler" style="max-height: calc(100vh - (200px))">
35
41
  <template #item.block="{ value, item }">
36
42
  {{ value ? `Block ${value}` : "" }}
37
43
  </template>
@@ -44,10 +50,10 @@
44
50
  <BuildingUnitFormAdd :site="site" @cancel="createDialog = false" @success="successCreate()"
45
51
  @success:create-more="getBuildingUnit()" />
46
52
  </v-dialog>
47
-
48
- <v-dialog v-if="canViewUnitDetails" v-model="dialogPreview":width="smAndUp ? '70svw' : '100%'" persistent>
49
- <BuildingUnitFormEdit v-if="selectedBuildingUnit" @cancel="dialogPreview = false" @success="successUpdate()" @delete-unit="openDeleteDialog()"
50
- :roomFacility="selectedBuildingUnit" />
53
+
54
+ <v-dialog v-if="canViewUnitDetails" v-model="dialogPreview" :width="smAndUp ? '70svw' : '100%'" persistent>
55
+ <BuildingUnitFormEdit v-if="selectedBuildingUnit" @cancel="dialogPreview = false" @success="successUpdate()"
56
+ @delete-unit="openDeleteDialog()" :roomFacility="selectedBuildingUnit" />
51
57
  </v-dialog>
52
58
 
53
59
  <v-dialog v-model="dialogDelete" :loading="deleteLoading" width="450" persistent>
@@ -88,6 +94,67 @@
88
94
  </v-card>
89
95
  </v-dialog>
90
96
 
97
+ <v-dialog v-model="bulkUploadSkippedDialog" :width="smAndUp ? '720' : '100%'" persistent>
98
+ <v-card width="100%">
99
+ <v-toolbar density="compact" class="pl-4">
100
+ <span class="font-weight-medium text-h5">Skipped Rows</span>
101
+ </v-toolbar>
102
+
103
+ <v-card-text style="max-height: 70vh; overflow-y: auto">
104
+ <p class="text-subtitle-2 mb-4">
105
+ Some rows were not imported. Review the details below and update the sheet if needed.
106
+ </p>
107
+
108
+ <v-alert v-if="bulkUploadSkippedSummary.length" type="warning" variant="tonal" density="compact"
109
+ class="mb-4">
110
+ {{ bulkUploadSkippedSummary.length }} row(s) skipped during bulk upload.
111
+ </v-alert>
112
+
113
+ <!-- <v-list v-if="bulkUploadSkippedSummary.length" density="compact" class="pa-0 mb-4">
114
+ <v-list-item v-for="(item, index) in bulkUploadSkippedSummary" :key="`summary-${index}`"
115
+ class="px-0">
116
+ <v-list-item-title class="text-subtitle-2">
117
+ Row {{ item.rowNumber ?? item.row ?? "N/A" }}
118
+ </v-list-item-title>
119
+ <v-list-item-subtitle class="text-wrap">
120
+ {{ formatSkippedSummaryReason(item) }}
121
+ </v-list-item-subtitle>
122
+ </v-list-item>
123
+ </v-list> -->
124
+
125
+ <template v-if="bulkUploadSkippedErrors.length">
126
+ <!-- <v-divider class="mb-4" /> -->
127
+ <p class="text-subtitle-2 mb-2">Details</p>
128
+ <v-table density="compact">
129
+ <thead>
130
+ <tr>
131
+ <th class="text-left">Row</th>
132
+ <th class="text-left">Block</th>
133
+ <th class="text-left">Building</th>
134
+ <th class="text-left">Reason</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ <tr v-for="(item, index) in bulkUploadSkippedErrors" :key="`error-${index}`">
139
+ <td>{{ item.row ?? "N/A" }}</td>
140
+ <td>{{ item.block ?? "N/A" }}</td>
141
+ <td>{{ item.name ?? "N/A" }}</td>
142
+ <td>{{ item.reason ?? "Skipped" }}</td>
143
+ </tr>
144
+ </tbody>
145
+ </v-table>
146
+ </template>
147
+ </v-card-text>
148
+
149
+ <v-toolbar density="compact">
150
+ <v-btn tile block size="48" color="black" variant="flat" class="text-none"
151
+ @click="bulkUploadSkippedDialog = false">
152
+ Close
153
+ </v-btn>
154
+ </v-toolbar>
155
+ </v-card>
156
+ </v-dialog>
157
+
91
158
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
92
159
  </v-row>
93
160
  </template>
@@ -103,7 +170,7 @@ const props = defineProps({
103
170
  title: "Unit Name",
104
171
  value: "name",
105
172
  },
106
- {
173
+ {
107
174
  title: "Level",
108
175
  value: "level.name",
109
176
  },
@@ -115,7 +182,7 @@ const props = defineProps({
115
182
  title: "Block Name",
116
183
  value: "buildingName",
117
184
  },
118
-
185
+
119
186
  // {
120
187
  // title: "Type",
121
188
  // value: "type",
@@ -162,6 +229,8 @@ const { debounce, formatDateDDMMYYYYLocal, toOrdinal } = useUtils();
162
229
 
163
230
  const { getAll, deleteById } = useBuildingUnit();
164
231
 
232
+ const { uploadSpreadsheetBuilding } = useBuilding();
233
+
165
234
  const searchInput = ref("");
166
235
  const headerSearch = ref("");
167
236
  const page = ref(1);
@@ -243,6 +312,12 @@ function openEditDialog() {
243
312
 
244
313
  const dialogDelete = ref(false);
245
314
  const deleteLoading = ref(false);
315
+ const bulkUploadFile = ref<File | null>(null);
316
+ const bulkUploadFileInput = ref<HTMLInputElement | null>(null);
317
+ const bulkUploadLoading = ref(false);
318
+ const bulkUploadSkippedDialog = ref(false);
319
+ const bulkUploadSkippedErrors = ref<Array<Record<string, any>>>([]);
320
+ const bulkUploadSkippedSummary = ref<Array<Record<string, any>>>([]);
246
321
 
247
322
  function openDeleteDialog() {
248
323
  dialogDelete.value = true;
@@ -272,6 +347,109 @@ async function handleDeleteBuildingUnit() {
272
347
  }
273
348
  }
274
349
 
350
+ function openBulkUploadFilePicker() {
351
+ if (bulkUploadLoading.value) {
352
+ return;
353
+ }
354
+
355
+ bulkUploadFile.value = null;
356
+ bulkUploadSkippedErrors.value = [];
357
+ bulkUploadSkippedSummary.value = [];
358
+ bulkUploadSkippedDialog.value = false;
359
+ if (bulkUploadFileInput.value) {
360
+ bulkUploadFileInput.value.value = "";
361
+ bulkUploadFileInput.value.click();
362
+ }
363
+ }
364
+
365
+ function formatSkippedSummaryReason(item: Record<string, any>) {
366
+ if (Array.isArray(item.errors) && item.errors.length) {
367
+ return item.errors.join(", ");
368
+ }
369
+
370
+ return item.reason ?? "Skipped";
371
+ }
372
+
373
+ function getSkippedRowsCount(result: Record<string, any> | null | undefined) {
374
+ if (typeof result?.skippedRows === "number") {
375
+ return result.skippedRows;
376
+ }
377
+
378
+ if (Array.isArray(result?.skippedRows)) {
379
+ return result.skippedRows.length;
380
+ }
381
+
382
+ return 0;
383
+ }
384
+
385
+ function getBulkUploadSkippedErrors(result: Record<string, any> | null | undefined) {
386
+ if (Array.isArray(result?.skippedErrors)) {
387
+ return result.skippedErrors;
388
+ }
389
+
390
+ if (Array.isArray(result?.skippedRows)) {
391
+ return result.skippedRows;
392
+ }
393
+
394
+ return [];
395
+ }
396
+
397
+ function getBulkUploadSkippedSummary(result: Record<string, any> | null | undefined) {
398
+ if (Array.isArray(result?.skippedItemsSummary)) {
399
+ return result.skippedItemsSummary;
400
+ }
401
+
402
+ if (Array.isArray(result?.skippedRows)) {
403
+ return result.skippedRows.map((item: Record<string, any>) => ({
404
+ rowNumber: item.row,
405
+ reason: item.reason,
406
+ block: item.block,
407
+ name: item.name,
408
+ data: item,
409
+ }));
410
+ }
411
+
412
+ return [];
413
+ }
414
+
415
+ async function handleBulkUploadFileChange(event: Event) {
416
+ const input = event.target as HTMLInputElement;
417
+ const file = input.files?.[0] ?? null;
418
+
419
+ if (!file) {
420
+ return;
421
+ }
422
+
423
+ bulkUploadFile.value = file;
424
+ bulkUploadLoading.value = true;
425
+
426
+ try {
427
+ const result = await uploadSpreadsheetBuilding({
428
+ site,
429
+ file,
430
+ });
431
+ await getBuildingUnit();
432
+ bulkUploadSkippedErrors.value = getBulkUploadSkippedErrors(result);
433
+ bulkUploadSkippedSummary.value = getBulkUploadSkippedSummary(result);
434
+ bulkUploadSkippedDialog.value =
435
+ bulkUploadSkippedErrors.value.length > 0 ||
436
+ bulkUploadSkippedSummary.value.length > 0;
437
+ showMessage(
438
+ `Bulk upload completed. Inserted ${result?.insertedUnits ?? 0} unit(s). Skipped ${getSkippedRowsCount(result)} row(s).`,
439
+ "success"
440
+ );
441
+ } catch (error: any) {
442
+ showMessage(
443
+ error?.response?._data?.message || "Failed to upload spreadsheet.",
444
+ "error"
445
+ );
446
+ } finally {
447
+ bulkUploadLoading.value = false;
448
+ bulkUploadFile.value = null;
449
+ input.value = "";
450
+ }
451
+ }
452
+
275
453
  function setBuilding({
276
454
  mode = "create",
277
455
  dialog = true,
@@ -290,7 +468,7 @@ function setBuilding({
290
468
  </script>
291
469
 
292
470
  <style scoped>
293
- .v-data-table :deep(thead th) {
471
+ .v-data-table :deep(thead th) {
294
472
  font-weight: bold !important;
295
473
  }
296
474
  </style>
@@ -26,11 +26,15 @@
26
26
  v-model:utc="startDate"
27
27
  density="compact"
28
28
  hide-details
29
+ :date-only="true"
30
+ placeholder="Start Date"
29
31
  />
30
32
  <InputDateTimePicker
31
33
  v-model:utc="endDate"
32
34
  density="compact"
33
35
  hide-details
36
+ :date-only="true"
37
+ placeholder="End Date"
34
38
  />
35
39
  <v-select
36
40
  v-model="status"
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="page-wrap">
3
3
 
4
- <!-- ===== TABLE VIEW: topbar + table đi chung 1 khối ===== -->
4
+ <!-- ===== TABLE VIEW ===== -->
5
5
  <template v-if="viewMode === 'table'">
6
6
 
7
7
  <!-- ===== TOP BAR ===== -->
@@ -24,7 +24,7 @@
24
24
  </svg>
25
25
  </div>
26
26
 
27
- <!-- Filters: chỉ hiện ở active/suspended -->
27
+ <!-- Filters -->
28
28
  <div v-if="tab !== 'pending'" class="filter-wrap">
29
29
  <div class="filter-field">
30
30
  <label>Filter by status</label>
@@ -104,7 +104,7 @@
104
104
  <div v-if="loading" class="loading-bar"><div class="loading-bar-fill" /></div>
105
105
 
106
106
  <!-- TABLE: active / suspended -->
107
- <div v-if="tab !== 'pending'" class="table-scroll">
107
+ <div v-if="tab !== 'pending'" ref="tableScrollEl" class="table-scroll" @scroll="closeMenu">
108
108
  <table>
109
109
  <thead>
110
110
  <tr>
@@ -156,32 +156,9 @@
156
156
  </td>
157
157
  <td>
158
158
  <div class="action-wrap">
159
- <button class="dots-btn" @click.stop="toggleMenu(item._id)">
159
+ <button class="dots-btn" @click.stop="toggleMenu(item._id, $event)">
160
160
  <span /><span /><span />
161
161
  </button>
162
- <div v-if="openMenuId === item._id" class="dropdown">
163
- <button
164
- class="dd-item"
165
- @click="viewClient(item)"
166
- >
167
- View Organization
168
- </button>
169
- <button
170
- v-if="item.status === 'suspended'"
171
- class="dd-item"
172
- @click="activateClient(item)"
173
- >
174
- Activate
175
- </button>
176
-
177
- <button
178
- v-else
179
- class="dd-item danger"
180
- @click="suspendClient(item)"
181
- >
182
- Suspend
183
- </button>
184
- </div>
185
162
  </div>
186
163
  </td>
187
164
  </tr>
@@ -228,7 +205,6 @@
228
205
 
229
206
  </template>
230
207
 
231
- <!-- ===== DETAIL VIEW: chiếm toàn bộ page-wrap, không còn topbar/table ===== -->
232
208
  <div v-else-if="viewMode === 'detail'" class="client-detail">
233
209
  <ClientDetailForm
234
210
  :client="selectedClient"
@@ -237,17 +213,59 @@
237
213
  />
238
214
  </div>
239
215
 
240
- <!-- ===== DIALOG (luôn nằm ngoài, có thể mở ở bất kỳ view nào) ===== -->
241
216
  <div v-if="dialog" class="overlay" @click.self="dialog = false">
242
217
  <div class="dialog-box">
243
218
  <InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
244
219
  </div>
245
220
  </div>
221
+
222
+ <!-- ===== SUSPEND CONFIRMATION DIALOG ===== -->
223
+ <div v-if="confirmSuspendItem" class="overlay" @click.self="cancelSuspend">
224
+ <div class="confirm-box">
225
+ <h3 class="confirm-title">Suspend account</h3>
226
+ <p class="confirm-text">
227
+ Are you sure you want to suspend <strong>{{ confirmSuspendItem?.name }}</strong>'s account?
228
+ </p>
229
+ <div class="confirm-actions">
230
+ <button class="btn-cancel" @click="cancelSuspend">Cancel</button>
231
+ <button class="btn-confirm-danger" :disabled="confirmSuspendLoading" @click="confirmSuspend">
232
+ <span v-if="confirmSuspendLoading" class="spinner spinner-light" />
233
+ <span v-else>Suspend</span>
234
+ </button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <Teleport to="body">
240
+ <div
241
+ v-if="openMenuId"
242
+ class="dropdown"
243
+ :style="{ top: menuPos.top + 'px', left: menuPos.left + 'px' }"
244
+ >
245
+ <button class="dd-item" @click="viewClient(activeMenuItem)">
246
+ View Organization
247
+ </button>
248
+ <button
249
+ v-if="activeMenuItem?.status === 'suspended'"
250
+ class="dd-item"
251
+ @click="activateClient(activeMenuItem)"
252
+ >
253
+ Activate
254
+ </button>
255
+ <button
256
+ v-else
257
+ class="dd-item danger"
258
+ @click="askSuspend(activeMenuItem)"
259
+ >
260
+ Suspend
261
+ </button>
262
+ </div>
263
+ </Teleport>
246
264
  </div>
247
265
  </template>
248
266
 
249
267
  <script setup lang="ts">
250
- import { computed, nextTick, onMounted, ref, watch } from 'vue'
268
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
251
269
  import useOrg from '../composables/useOrg'
252
270
  import useVerification from '../composables/useVerification'
253
271
  import useRole from '../composables/useRole'
@@ -283,6 +301,9 @@ const APP_LIST = [
283
301
  const tab = ref<TabStatus>('active')
284
302
  const dialog = ref(false)
285
303
  const openMenuId = ref<string | null>(null)
304
+ const menuPos = ref({ top: 0, left: 0 })
305
+ const tableScrollEl = ref<HTMLElement | null>(null)
306
+
286
307
  // filterStatus
287
308
  const filterStatus = ref<'active' | 'suspended'>('active')
288
309
  const filterPlan = ref('') // '' = All | any APP_LIST value
@@ -300,6 +321,10 @@ const roleCache = ref<Record<string, string>>({})
300
321
  const viewMode = ref<'table' | 'detail'>('table')
301
322
  const selectedClient = ref<any>(null)
302
323
 
324
+ /* suspend confirmation */
325
+ const confirmSuspendItem = ref<any>(null)
326
+ const confirmSuspendLoading = ref(false)
327
+
303
328
  /* ── COMPUTED ── */
304
329
  const pageRange = computed(() => {
305
330
  if (!totalItems.value) return '0-0 of 0'
@@ -308,6 +333,10 @@ const pageRange = computed(() => {
308
333
  return `${start}-${end} of ${totalItems.value}`
309
334
  })
310
335
 
336
+ const activeMenuItem = computed(() =>
337
+ items.value.find(i => i._id === openMenuId.value) ?? null
338
+ )
339
+
311
340
  /* ── HELPERS ── */
312
341
  function formatDate(date?: string | Date) {
313
342
  if (date == null || date === '') return '-'
@@ -358,6 +387,28 @@ async function activateClient(item: any) {
358
387
  }
359
388
  }
360
389
 
390
+ /* ── SUSPEND CONFIRMATION ── */
391
+ function askSuspend(item: any) {
392
+ confirmSuspendItem.value = item
393
+ closeMenu()
394
+ }
395
+
396
+ function cancelSuspend() {
397
+ if (confirmSuspendLoading.value) return
398
+ confirmSuspendItem.value = null
399
+ }
400
+
401
+ async function confirmSuspend() {
402
+ if (!confirmSuspendItem.value) return
403
+ confirmSuspendLoading.value = true
404
+ try {
405
+ await suspendClient(confirmSuspendItem.value)
406
+ confirmSuspendItem.value = null
407
+ } finally {
408
+ confirmSuspendLoading.value = false
409
+ }
410
+ }
411
+
361
412
  async function fetchOrgs(status: 'active' | 'suspended') {
362
413
  loading.value = true
363
414
 
@@ -484,7 +535,6 @@ async function handleUpdatePage(p: number) {
484
535
 
485
536
  async function onTabChange(value: TabStatus) {
486
537
  tab.value = value
487
- // sync filterStatus ( active/suspended)
488
538
  if (value !== 'pending') filterStatus.value = value
489
539
  page.value = 1
490
540
  await fetchData()
@@ -509,14 +559,44 @@ function handleFilterChange() {
509
559
  fetchData()
510
560
  }
511
561
 
512
- /* ── MENU ── */
513
- function toggleMenu(id: string) {
514
- openMenuId.value = openMenuId.value === id ? null : id
562
+ const DROPDOWN_WIDTH = 130
563
+
564
+ function toggleMenu(id: string, event: MouseEvent) {
565
+ if (openMenuId.value === id) {
566
+ closeMenu()
567
+ return
568
+ }
569
+ const btn = event.currentTarget as HTMLElement
570
+ const rect = btn.getBoundingClientRect()
571
+
572
+ let left = rect.right - DROPDOWN_WIDTH
573
+ if (left < 8) left = 8
574
+ if (left + DROPDOWN_WIDTH > window.innerWidth - 8) {
575
+ left = window.innerWidth - DROPDOWN_WIDTH - 8
576
+ }
577
+
578
+ menuPos.value = {
579
+ top: rect.bottom + 4,
580
+ left,
581
+ }
582
+ openMenuId.value = id
515
583
  }
584
+
516
585
  function closeMenu() {
517
586
  openMenuId.value = null
518
587
  }
519
588
 
589
+ function handleOutsideClick(e: MouseEvent) {
590
+ const target = e.target as HTMLElement
591
+ if (!target.closest('.dropdown') && !target.closest('.dots-btn')) {
592
+ closeMenu()
593
+ }
594
+ }
595
+
596
+ function handleWindowResize() {
597
+ closeMenu()
598
+ }
599
+
520
600
  /* ── DIALOG ── */
521
601
  function openInviteDialog() { dialog.value = true }
522
602
  function handleSuccess() { dialog.value = false; fetchData() }
@@ -532,7 +612,18 @@ async function cancelInvite(item: any) {
532
612
  }
533
613
  }
534
614
 
535
- onMounted(() => fetchData())
615
+ onMounted(() => {
616
+ fetchData()
617
+ document.addEventListener('click', handleOutsideClick)
618
+ document.addEventListener('scroll', closeMenu, true)
619
+ window.addEventListener('resize', handleWindowResize)
620
+ })
621
+
622
+ onUnmounted(() => {
623
+ document.removeEventListener('click', handleOutsideClick)
624
+ document.removeEventListener('scroll', closeMenu, true)
625
+ window.removeEventListener('resize', handleWindowResize)
626
+ })
536
627
  </script>
537
628
 
538
629
  <style scoped>
@@ -823,15 +914,13 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
823
914
  .dots-btn span { display: block; width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
824
915
 
825
916
  .dropdown {
826
- position: absolute;
827
- right: 0;
828
- top: calc(100% + 4px);
917
+ position: fixed;
829
918
  background: #fff;
830
919
  border: 1px solid #E5E7EB;
831
920
  border-radius: 8px;
832
921
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
833
922
  min-width: 130px;
834
- z-index: 50;
923
+ z-index: 1100;
835
924
  overflow: hidden;
836
925
  }
837
926
  .dd-item {
@@ -874,6 +963,10 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
874
963
  border-radius: 50%;
875
964
  animation: spin 0.7s linear infinite;
876
965
  }
966
+ .spinner-light {
967
+ border: 1.5px solid rgba(255,255,255,0.5);
968
+ border-top-color: #fff;
969
+ }
877
970
 
878
971
  .client-detail {
879
972
  margin: 12px 0 0 0;
@@ -896,4 +989,61 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
896
989
  box-shadow: 0 16px 48px rgba(0,0,0,0.12);
897
990
  overflow: hidden;
898
991
  }
992
+
993
+ /* ===== SUSPEND CONFIRMATION DIALOG ===== */
994
+ .confirm-box {
995
+ background: #fff;
996
+ border-radius: 12px;
997
+ max-width: 380px;
998
+ width: 90%;
999
+ box-shadow: 0 16px 48px rgba(0,0,0,0.12);
1000
+ padding: 24px;
1001
+ }
1002
+ .confirm-title {
1003
+ font-size: 16px;
1004
+ font-weight: 600;
1005
+ color: #111827;
1006
+ margin-bottom: 8px;
1007
+ }
1008
+ .confirm-text {
1009
+ font-size: 13.5px;
1010
+ color: #4B5563;
1011
+ line-height: 1.5;
1012
+ margin-bottom: 20px;
1013
+ }
1014
+ .confirm-actions {
1015
+ display: flex;
1016
+ justify-content: flex-end;
1017
+ gap: 10px;
1018
+ }
1019
+ .btn-cancel {
1020
+ padding: 8px 16px;
1021
+ font-size: 13px;
1022
+ font-weight: 500;
1023
+ border: 1px solid #D1D5DB;
1024
+ border-radius: 8px;
1025
+ background: #fff;
1026
+ color: #374151;
1027
+ cursor: pointer;
1028
+ transition: background 0.15s;
1029
+ }
1030
+ .btn-cancel:hover { background: #F9FAFB; }
1031
+ .btn-confirm-danger {
1032
+ display: inline-flex;
1033
+ align-items: center;
1034
+ justify-content: center;
1035
+ gap: 6px;
1036
+ min-width: 78px;
1037
+ padding: 8px 16px;
1038
+ font-size: 13px;
1039
+ font-weight: 500;
1040
+ border: none;
1041
+ border-radius: 8px;
1042
+ background: #DC2626;
1043
+ color: #fff;
1044
+ cursor: pointer;
1045
+ transition: background 0.15s;
1046
+ }
1047
+ .btn-confirm-danger:hover:not(:disabled) { background: #B91C1C; }
1048
+ .btn-confirm-danger:disabled { opacity: 0.7; cursor: not-allowed; }
899
1049
  </style>
@@ -824,13 +824,7 @@
824
824
  <div class="dashboard-card dashboard-panel figma-panel">
825
825
  <div class="figma-panel-header">
826
826
  <h2>Today's Attentions</h2>
827
- <button
828
- class="figma-panel-action"
829
- type="button"
830
- @click="goToDashboardRoute('incident-reports')"
831
- >
832
- View All
833
- </button>
827
+
834
828
  </div>
835
829
  <div class="figma-panel-body">
836
830
  <div v-if="todayAttentions.length" class="figma-list">