@7365admin1/layer-common 3.2.2-staging.91 → 3.2.2-staging.93
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/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/assets/css/primitives.css +147 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/Avatar/Main.vue +13 -1
- package/components/DashboardMain.vue +69 -124
- package/components/FeedbackMain.vue +33 -36
- package/components/InvitationMain.vue +45 -62
- package/components/Layout/Header.vue +16 -2
- package/components/Layout/NavigationDrawer.vue +4 -3
- package/components/ServiceProviderMain.vue +105 -135
- package/components/SwitchContext.vue +33 -1
- package/components/WorkOrder/Main.vue +42 -51
- package/package.json +1 -1
- package/utils/nav-sections.test.ts +70 -11
- package/utils/nav-sections.ts +48 -25
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/layer-common": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Group the sidebar's sections and give the list screens the design's page
|
|
6
|
+
scaffold.
|
|
7
|
+
|
|
8
|
+
- `withSections()` walked each application's menu in the order it was given and
|
|
9
|
+
printed a heading whenever the section changed, so Property Management drew
|
|
10
|
+
SERVICE DESK, PROPERTY and SECURITY OPERATIONS **twice each**. Items are now
|
|
11
|
+
grouped under one heading per section, in the order the design lists them,
|
|
12
|
+
with the application's own order kept inside each section. No menu item is
|
|
13
|
+
added, hidden, renamed, re-routed or re-gated.
|
|
14
|
+
- `ServiceProviderMain`, `FeedbackMain`, `InvitationMain` and `WorkOrder/Main`
|
|
15
|
+
had no page title, put their primary action top-left, floated the search
|
|
16
|
+
outside the card, and stacked the refresh/pager on a bar above the tabs. They
|
|
17
|
+
now use `PageHeader` / `AppCard` / `CardToolbar` / `AppButton` / `AppField`:
|
|
18
|
+
a 22px title with the action on the right, then one in-card row with the tabs
|
|
19
|
+
on the left and search, count and pager on the right.
|
|
20
|
+
- `SwitchContext` and the rail's application name print in full instead of
|
|
21
|
+
truncating to "Seventh Cond…" / "Property Managem…".
|
|
22
|
+
- The security dashboard's `ACTIVE PATROL` / `STAFF STATUS` panel headings are
|
|
23
|
+
written the way the design writes them, matching their sibling panels.
|
|
24
|
+
|
|
25
|
+
Placement and wording only: the same buttons open the same dialogs under the
|
|
26
|
+
same permissions, the same tabs load the same lists, and the tables, columns,
|
|
27
|
+
filters and row menus are untouched.
|
|
@@ -108,6 +108,9 @@
|
|
|
108
108
|
.app-toolbar {
|
|
109
109
|
display: flex;
|
|
110
110
|
align-items: center;
|
|
111
|
+
/* One row at desktop widths, as drawn. On a phone the tabs and the
|
|
112
|
+
search/pager group drop onto their own lines rather than overflow. */
|
|
113
|
+
flex-wrap: wrap;
|
|
111
114
|
gap: 22px;
|
|
112
115
|
padding: 0 var(--cellpad-x);
|
|
113
116
|
border-bottom: 1px solid var(--border);
|
|
@@ -129,6 +132,14 @@
|
|
|
129
132
|
gap: 8px;
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
/* The list toolbar's own search box. `--search` alone stretches (it is written
|
|
136
|
+
for the filter row), which would push the pager off the end of the row. */
|
|
137
|
+
.app-toolbar__search.app-field--search {
|
|
138
|
+
flex: 0 0 auto;
|
|
139
|
+
width: 200px;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
.app-toolbar__count {
|
|
133
144
|
font-size: 12.5px;
|
|
134
145
|
font-weight: 600;
|
|
@@ -161,6 +172,9 @@
|
|
|
161
172
|
color: var(--muted);
|
|
162
173
|
white-space: nowrap;
|
|
163
174
|
cursor: pointer;
|
|
175
|
+
/* A tab is a <button> on the screens that switch a list in place and a link
|
|
176
|
+
on the ones that route (Invitations), so the underline has to be ours. */
|
|
177
|
+
text-decoration: none;
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
.app-tab:hover {
|
|
@@ -408,6 +422,139 @@ select.app-field__input {
|
|
|
408
422
|
}
|
|
409
423
|
}
|
|
410
424
|
|
|
425
|
+
/* ------------------------------------------------------------------ */
|
|
426
|
+
/* SegmentedControl - Today | Week | Month. */
|
|
427
|
+
/* ------------------------------------------------------------------ */
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The handoff draws this as a 3px-padded bordered tray holding 8px-radius
|
|
431
|
+
* segments; the SELECTED one is an ACCENT FILL, not a grey one. Vuetify's
|
|
432
|
+
* `v-btn-toggle` cannot be talked into that shape (its own height scale, its
|
|
433
|
+
* dividers, its overlay layer), so the tray is written as markup.
|
|
434
|
+
*
|
|
435
|
+
* The fill is `--accent-strong`, not `--accent`: white on `--accent` measures
|
|
436
|
+
* 3.23:1, the same Phase-1 rule every other filled label follows.
|
|
437
|
+
*/
|
|
438
|
+
.app-seg {
|
|
439
|
+
display: flex;
|
|
440
|
+
gap: 2px;
|
|
441
|
+
padding: 3px;
|
|
442
|
+
border: 1px solid var(--border);
|
|
443
|
+
border-radius: var(--r-inner);
|
|
444
|
+
background: var(--card);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.app-seg__item {
|
|
448
|
+
padding: 6px 12px;
|
|
449
|
+
border: none;
|
|
450
|
+
border-radius: 8px;
|
|
451
|
+
background: transparent;
|
|
452
|
+
color: var(--muted);
|
|
453
|
+
font-family: inherit;
|
|
454
|
+
font-size: 12.5px;
|
|
455
|
+
font-weight: 700;
|
|
456
|
+
line-height: 1.2;
|
|
457
|
+
white-space: nowrap;
|
|
458
|
+
cursor: pointer;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.app-seg__item:hover {
|
|
462
|
+
background: var(--hover);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.app-seg__item--on,
|
|
466
|
+
.app-seg__item--on:hover {
|
|
467
|
+
background: var(--accent-strong);
|
|
468
|
+
color: var(--on-accent-strong);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ------------------------------------------------------------------ */
|
|
472
|
+
/* KpiTile - the dashboard's headline figure. */
|
|
473
|
+
/* ------------------------------------------------------------------ */
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Three rows, in this order, straight from the handoff:
|
|
477
|
+
* 1. a 34px soft icon square and the uppercase label, side by side
|
|
478
|
+
* 2. the 30px tabular number with its percentage pill BESIDE it
|
|
479
|
+
* 3. the sub-line
|
|
480
|
+
* The product had the icon alone on row 1 and the pill stranded at the
|
|
481
|
+
* bottom, which is why the tile read as a different component.
|
|
482
|
+
*/
|
|
483
|
+
.app-kpi {
|
|
484
|
+
padding: 18px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.app-kpi__head {
|
|
488
|
+
display: flex;
|
|
489
|
+
align-items: center;
|
|
490
|
+
gap: 10px;
|
|
491
|
+
margin-bottom: 14px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.app-kpi__icon {
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
justify-content: center;
|
|
498
|
+
width: 34px;
|
|
499
|
+
height: 34px;
|
|
500
|
+
flex: 0 0 34px;
|
|
501
|
+
border-radius: var(--r-inner);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.app-kpi__label {
|
|
505
|
+
font-size: var(--fs-table-head);
|
|
506
|
+
font-weight: var(--fw-table-head);
|
|
507
|
+
letter-spacing: var(--ls-table-head);
|
|
508
|
+
text-transform: uppercase;
|
|
509
|
+
color: var(--muted);
|
|
510
|
+
min-width: 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.app-kpi__action {
|
|
514
|
+
margin-left: auto;
|
|
515
|
+
flex: 0 0 auto;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.app-kpi__figure {
|
|
519
|
+
display: flex;
|
|
520
|
+
align-items: center;
|
|
521
|
+
gap: 10px;
|
|
522
|
+
flex-wrap: wrap;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.app-kpi__value {
|
|
526
|
+
font-size: var(--fs-kpi);
|
|
527
|
+
font-weight: var(--fw-kpi);
|
|
528
|
+
letter-spacing: var(--ls-kpi);
|
|
529
|
+
line-height: 1.1;
|
|
530
|
+
color: var(--text);
|
|
531
|
+
font-variant-numeric: tabular-nums;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.app-kpi__chip {
|
|
535
|
+
display: inline-flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
padding: 3px 8px;
|
|
538
|
+
border-radius: 999px;
|
|
539
|
+
font-size: 11.5px;
|
|
540
|
+
font-weight: 800;
|
|
541
|
+
line-height: 1.2;
|
|
542
|
+
background: var(--ok-bg);
|
|
543
|
+
color: var(--ok);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.app-kpi__chip--down {
|
|
547
|
+
background: var(--err-bg);
|
|
548
|
+
color: var(--err);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.app-kpi__sub {
|
|
552
|
+
margin-top: 6px;
|
|
553
|
+
font-size: 12.5px;
|
|
554
|
+
font-weight: 600;
|
|
555
|
+
color: var(--muted);
|
|
556
|
+
}
|
|
557
|
+
|
|
411
558
|
/* ------------------------------------------------------------------ */
|
|
412
559
|
/* Responsive - the prototype has no reference, this follows Phase 1. */
|
|
413
560
|
/* ------------------------------------------------------------------ */
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE KPI TILE.
|
|
3
|
+
|
|
4
|
+
The handoff's tile is three rows: the soft icon square AND the uppercase
|
|
5
|
+
label together on the first, the big tabular number with its percentage pill
|
|
6
|
+
BESIDE it on the second, the sub-line on the third. The product had the icon
|
|
7
|
+
alone on a row of its own, the label under it, and the pill stranded at the
|
|
8
|
+
bottom next to the sub-line - the same figures in a different shape.
|
|
9
|
+
|
|
10
|
+
It draws what it is given and nothing else: no fetch, no formatting, no
|
|
11
|
+
threshold logic. `#action` is the far end of the first row (a filter menu, a
|
|
12
|
+
"View All"); `#default`, when given, replaces the figure rows entirely for
|
|
13
|
+
the tiles that list items instead of counting them.
|
|
14
|
+
-->
|
|
15
|
+
<template>
|
|
16
|
+
<AppCard class="app-kpi d-flex flex-column flex-grow-1">
|
|
17
|
+
<div class="app-kpi__head">
|
|
18
|
+
<span
|
|
19
|
+
v-if="icon"
|
|
20
|
+
class="app-kpi__icon"
|
|
21
|
+
:style="tint ? { backgroundColor: tint } : undefined"
|
|
22
|
+
>
|
|
23
|
+
<v-icon :icon="icon" :color="color || undefined" size="19" />
|
|
24
|
+
</span>
|
|
25
|
+
|
|
26
|
+
<span class="app-kpi__label">{{ label }}</span>
|
|
27
|
+
|
|
28
|
+
<span v-if="$slots.action" class="app-kpi__action">
|
|
29
|
+
<slot name="action" />
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<slot>
|
|
34
|
+
<div class="app-kpi__figure">
|
|
35
|
+
<span class="app-kpi__value">{{ value }}</span>
|
|
36
|
+
|
|
37
|
+
<span
|
|
38
|
+
v-if="chip"
|
|
39
|
+
class="app-kpi__chip"
|
|
40
|
+
:class="{ 'app-kpi__chip--down': down }"
|
|
41
|
+
>
|
|
42
|
+
{{ chip }}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div v-if="sub" class="app-kpi__sub">{{ sub }}</div>
|
|
47
|
+
</slot>
|
|
48
|
+
</AppCard>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup lang="ts">
|
|
52
|
+
defineProps({
|
|
53
|
+
icon: { type: String, default: "" },
|
|
54
|
+
/** The icon square's soft fill - a token-derived colour from the caller. */
|
|
55
|
+
tint: { type: String, default: "" },
|
|
56
|
+
color: { type: String, default: "" },
|
|
57
|
+
label: { type: String, default: "" },
|
|
58
|
+
value: { type: [String, Number], default: "" },
|
|
59
|
+
/** The percentage pill's text, already formatted by the caller. */
|
|
60
|
+
chip: { type: String, default: "" },
|
|
61
|
+
/** Draws the pill in the error tone rather than the success one. */
|
|
62
|
+
down: { type: Boolean, default: false },
|
|
63
|
+
sub: { type: String, default: "" },
|
|
64
|
+
});
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE SEGMENTED CONTROL - `Today | Week | Month`.
|
|
3
|
+
|
|
4
|
+
A tray of plain `<button>`s, not a `v-btn-toggle`. The toggle brings its own
|
|
5
|
+
height scale, dividers, ripple and overlay layer, and its selected state is
|
|
6
|
+
drawn from `on-surface` - which is why the product's selected segment reads
|
|
7
|
+
grey where the design fills it with the accent.
|
|
8
|
+
|
|
9
|
+
Selection only: it writes the same `v-model` the toggle wrote, and the value
|
|
10
|
+
set is whatever the caller passes. No behaviour is added here.
|
|
11
|
+
-->
|
|
12
|
+
<template>
|
|
13
|
+
<div class="app-seg" role="group">
|
|
14
|
+
<button
|
|
15
|
+
v-for="option in options"
|
|
16
|
+
:key="String(option.value)"
|
|
17
|
+
type="button"
|
|
18
|
+
class="app-seg__item"
|
|
19
|
+
:class="{ 'app-seg__item--on': model === option.value }"
|
|
20
|
+
:aria-pressed="model === option.value"
|
|
21
|
+
@click="model = option.value"
|
|
22
|
+
>
|
|
23
|
+
{{ option.label }}
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
defineProps({
|
|
30
|
+
/** `{ label, value }` - the same option list the toggle was given. */
|
|
31
|
+
options: {
|
|
32
|
+
type: Array as PropType<Array<{ label: string; value: any }>>,
|
|
33
|
+
default: () => [],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const model = defineModel<any>({ default: "" });
|
|
38
|
+
</script>
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-avatar :color="getColor(name || id)" :size="size">
|
|
3
|
-
|
|
3
|
+
<!--
|
|
4
|
+
A stored profile photo that 404s left the circle EMPTY - `v-img`
|
|
5
|
+
draws nothing when the source fails, and there was no fallback under
|
|
6
|
+
it. The initials the avatar already computes stand in, so the circle
|
|
7
|
+
is never blank.
|
|
8
|
+
-->
|
|
9
|
+
<v-img v-if="imageSrc" :alt="name" :src="imageUrl">
|
|
10
|
+
<template #error>
|
|
11
|
+
<div class="d-flex align-center justify-center fill-height">
|
|
12
|
+
<span :style="{ fontSize: `${fontSize}px` }">{{ initials }}</span>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
</v-img>
|
|
4
16
|
<span v-else :style="{
|
|
5
17
|
fontSize: `${fontSize}px`,
|
|
6
18
|
lineHeight: `${size}px`
|
|
@@ -203,44 +203,36 @@
|
|
|
203
203
|
|
|
204
204
|
<section v-else class="unified-dashboard pb-8">
|
|
205
205
|
<!-- ── Header ──────────────────────────────────────────────────────────── -->
|
|
206
|
-
<
|
|
207
|
-
|
|
206
|
+
<PageHeader
|
|
207
|
+
:title="dashboardTitle"
|
|
208
|
+
:subtitle="currentSiteName || 'Selected site'"
|
|
209
|
+
subtitle-icon="mdi-map-marker"
|
|
208
210
|
>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<div
|
|
220
|
-
v-if="showHeaderActions"
|
|
221
|
-
class="d-flex flex-wrap align-center justify-md-end ga-3"
|
|
222
|
-
>
|
|
223
|
-
<v-btn
|
|
224
|
-
variant="outlined"
|
|
225
|
-
class="text-none font-weight-bold"
|
|
226
|
-
prepend-icon="mdi-tune-variant"
|
|
211
|
+
<!--
|
|
212
|
+
The design's three actions: two 38px ghost buttons and the accent
|
|
213
|
+
segmented control. Same dialog, same export menu, same range model -
|
|
214
|
+
`Customize Dashboard` and `Export` shorten to the labels the prototype
|
|
215
|
+
prints, and the export menu keeps both of its formats.
|
|
216
|
+
-->
|
|
217
|
+
<template v-if="showHeaderActions" #actions>
|
|
218
|
+
<AppButton
|
|
219
|
+
variant="ghost"
|
|
220
|
+
icon="mdi-tune-variant"
|
|
227
221
|
@click="openCustomizeDialog"
|
|
228
222
|
>
|
|
229
|
-
Customize
|
|
230
|
-
</
|
|
223
|
+
Customize
|
|
224
|
+
</AppButton>
|
|
231
225
|
|
|
232
226
|
<v-menu>
|
|
233
227
|
<template #activator="{ props: menuProps }">
|
|
234
|
-
<
|
|
228
|
+
<AppButton
|
|
235
229
|
v-bind="menuProps"
|
|
236
|
-
variant="
|
|
237
|
-
|
|
238
|
-
append-icon="mdi-chevron-down"
|
|
239
|
-
:loading="isExporting"
|
|
230
|
+
variant="ghost"
|
|
231
|
+
icon="mdi-download-outline"
|
|
240
232
|
:disabled="isExporting"
|
|
241
233
|
>
|
|
242
234
|
Export
|
|
243
|
-
</
|
|
235
|
+
</AppButton>
|
|
244
236
|
</template>
|
|
245
237
|
|
|
246
238
|
<v-list density="compact" border rounded="lg" elevation="3">
|
|
@@ -249,36 +241,9 @@
|
|
|
249
241
|
</v-list>
|
|
250
242
|
</v-menu>
|
|
251
243
|
|
|
252
|
-
<v-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
density="comfortable"
|
|
256
|
-
divided
|
|
257
|
-
variant="outlined"
|
|
258
|
-
class="bg-surface"
|
|
259
|
-
style="border-radius: 8px"
|
|
260
|
-
>
|
|
261
|
-
<v-btn
|
|
262
|
-
v-for="range in rangeOptions"
|
|
263
|
-
:key="range.value"
|
|
264
|
-
:value="range.value"
|
|
265
|
-
class="text-none"
|
|
266
|
-
size="small"
|
|
267
|
-
:style="
|
|
268
|
-
selectedRange === range.value
|
|
269
|
-
? {
|
|
270
|
-
backgroundColor: 'rgba(var(--v-theme-on-surface), 0.12)',
|
|
271
|
-
color: 'rgb(var(--v-theme-on-surface))',
|
|
272
|
-
fontWeight: '600',
|
|
273
|
-
}
|
|
274
|
-
: { backgroundColor: 'transparent', color: 'rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))' }
|
|
275
|
-
"
|
|
276
|
-
>
|
|
277
|
-
{{ range.label }}
|
|
278
|
-
</v-btn>
|
|
279
|
-
</v-btn-toggle>
|
|
280
|
-
</div>
|
|
281
|
-
</div>
|
|
244
|
+
<AppSegmented v-model="selectedRange" :options="rangeOptions" />
|
|
245
|
+
</template>
|
|
246
|
+
</PageHeader>
|
|
282
247
|
|
|
283
248
|
<v-progress-linear
|
|
284
249
|
v-if="finalLoading"
|
|
@@ -298,20 +263,24 @@
|
|
|
298
263
|
lg="3"
|
|
299
264
|
class="d-flex"
|
|
300
265
|
>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
266
|
+
<!--
|
|
267
|
+
The same figures the tile has always shown, in the arrangement the
|
|
268
|
+
handoff draws them: icon and label on one row, the number with its
|
|
269
|
+
percentage pill beside it, the sub-line under both. The filter menu
|
|
270
|
+
and the facility "View All" keep their place at the end of the first
|
|
271
|
+
row and do exactly what they did.
|
|
272
|
+
-->
|
|
273
|
+
<AppKpiTile
|
|
274
|
+
:icon="card.icon"
|
|
275
|
+
:tint="card.tint"
|
|
276
|
+
:color="card.color"
|
|
277
|
+
:label="card.title"
|
|
278
|
+
:value="card.value"
|
|
279
|
+
:chip="kpiChipText(card)"
|
|
280
|
+
:down="Number(card.percentage) < 0"
|
|
281
|
+
:sub="card.meta"
|
|
305
282
|
>
|
|
306
|
-
<
|
|
307
|
-
<div
|
|
308
|
-
class="d-inline-flex align-center justify-center rounded-lg"
|
|
309
|
-
style="height: 28px; width: 28px; flex-shrink: 0"
|
|
310
|
-
:style="{ backgroundColor: card.tint }"
|
|
311
|
-
>
|
|
312
|
-
<v-icon :icon="card.icon" :color="card.color" size="15" />
|
|
313
|
-
</div>
|
|
314
|
-
|
|
283
|
+
<template v-if="card.filter || card.key === 'facilityBookings'" #action>
|
|
315
284
|
<v-menu v-if="card.filter" v-model="filterMenus[card.key]">
|
|
316
285
|
<template #activator="{ props: menuProps }">
|
|
317
286
|
<v-btn
|
|
@@ -355,59 +324,24 @@
|
|
|
355
324
|
>
|
|
356
325
|
View All
|
|
357
326
|
</button>
|
|
358
|
-
</
|
|
327
|
+
</template>
|
|
359
328
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
:key="item._id || item.name"
|
|
373
|
-
class="d-flex justify-space-between text-body-2 text-high-emphasis font-weight-medium"
|
|
374
|
-
>
|
|
375
|
-
<span>{{ item.name }}</span>
|
|
376
|
-
<span class="text-medium-emphasis">Balance: {{ item.qty }}</span>
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
</template>
|
|
380
|
-
<template v-else>
|
|
381
|
-
<div class="d-flex align-center mb-3" style="min-height: 31px">
|
|
382
|
-
<span class="dashboard-kpi-value">
|
|
383
|
-
{{ card.value }}
|
|
384
|
-
</span>
|
|
385
|
-
</div>
|
|
386
|
-
<div class="d-flex align-center flex-wrap ga-2">
|
|
387
|
-
<span v-if="card.meta" class="text-caption text-medium-emphasis">{{
|
|
388
|
-
card.meta
|
|
389
|
-
}}</span>
|
|
390
|
-
<v-chip
|
|
391
|
-
:color="card.percentage >= 0 ? 'success' : 'error'"
|
|
392
|
-
size="x-small"
|
|
393
|
-
variant="flat"
|
|
394
|
-
class="font-weight-bold"
|
|
395
|
-
>
|
|
396
|
-
<v-icon
|
|
397
|
-
:icon="
|
|
398
|
-
card.percentage >= 0
|
|
399
|
-
? 'mdi-arrow-up-right'
|
|
400
|
-
: 'mdi-arrow-down-right'
|
|
401
|
-
"
|
|
402
|
-
size="13"
|
|
403
|
-
start
|
|
404
|
-
/>
|
|
405
|
-
{{ Math.abs(card.percentage) }}%
|
|
406
|
-
</v-chip>
|
|
329
|
+
<!-- The supply tile lists items instead of counting one figure. -->
|
|
330
|
+
<template
|
|
331
|
+
v-if="card.key === 'supplyAlert' && Array.isArray(card.raw?.items)"
|
|
332
|
+
>
|
|
333
|
+
<div class="d-flex flex-column ga-2">
|
|
334
|
+
<div
|
|
335
|
+
v-for="item in card.raw.items"
|
|
336
|
+
:key="item._id || item.name"
|
|
337
|
+
class="d-flex justify-space-between text-body-2 text-high-emphasis font-weight-medium"
|
|
338
|
+
>
|
|
339
|
+
<span>{{ item.name }}</span>
|
|
340
|
+
<span class="text-medium-emphasis">Balance: {{ item.qty }}</span>
|
|
407
341
|
</div>
|
|
408
|
-
</
|
|
409
|
-
</
|
|
410
|
-
</
|
|
342
|
+
</div>
|
|
343
|
+
</template>
|
|
344
|
+
</AppKpiTile>
|
|
411
345
|
</v-col>
|
|
412
346
|
</v-row>
|
|
413
347
|
|
|
@@ -954,7 +888,7 @@
|
|
|
954
888
|
<v-col v-if="isWidgetVisible('activePatrol')" cols="12" lg="6">
|
|
955
889
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
956
890
|
<div class="figma-panel-header">
|
|
957
|
-
<h2>
|
|
891
|
+
<h2>Active Patrol</h2>
|
|
958
892
|
<button
|
|
959
893
|
class="figma-panel-action"
|
|
960
894
|
type="button"
|
|
@@ -1034,7 +968,7 @@
|
|
|
1034
968
|
<v-col v-if="isWidgetVisible('staffStatus')" cols="12" lg="6">
|
|
1035
969
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
1036
970
|
<div class="figma-panel-header">
|
|
1037
|
-
<h2>
|
|
971
|
+
<h2>Staff Status</h2>
|
|
1038
972
|
<button
|
|
1039
973
|
class="figma-panel-action"
|
|
1040
974
|
type="button"
|
|
@@ -2491,6 +2425,17 @@ const visibleKpiCards = computed(() =>
|
|
|
2491
2425
|
kpiCards.value.filter((card) => isWidgetVisible(card.key))
|
|
2492
2426
|
);
|
|
2493
2427
|
|
|
2428
|
+
/**
|
|
2429
|
+
* The percentage pill's label. Same number the chip has always shown, written
|
|
2430
|
+
* the way the handoff prints it - `+0%` / `-6.4%`, the sign carrying what the
|
|
2431
|
+
* arrow icon used to. A card without a percentage gets no pill.
|
|
2432
|
+
*/
|
|
2433
|
+
function kpiChipText(card: Record<string, any>): string {
|
|
2434
|
+
const value = Number(card?.percentage);
|
|
2435
|
+
if (!Number.isFinite(value)) return "";
|
|
2436
|
+
return `${value < 0 ? "-" : "+"}${Math.abs(value)}%`;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2494
2439
|
// ─── Dashboard Title ──────────────────────────────────────────────────────────
|
|
2495
2440
|
|
|
2496
2441
|
const dashboardTitle = computed(() => {
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<div>
|
|
3
|
+
<!-- The design's title row: the page's name at 22px and its primary
|
|
4
|
+
action on the right of that same line. Placement only - the same
|
|
5
|
+
button opens the same dialog under the same permission. -->
|
|
6
|
+
<PageHeader title="Feedbacks">
|
|
7
|
+
<template v-if="canCreateFeedback" #actions>
|
|
8
|
+
<AppButton @click="showCreateDialog = true">Create Feedback</AppButton>
|
|
9
|
+
</template>
|
|
10
|
+
</PageHeader>
|
|
11
|
+
|
|
12
|
+
<v-row no-gutters>
|
|
3
13
|
<v-col cols="12" class="mb-2">
|
|
4
14
|
<v-row no-gutters align="center" class="filter-bar">
|
|
5
15
|
<v-text-field
|
|
@@ -50,47 +60,32 @@
|
|
|
50
60
|
<v-date-picker v-model="endDate" width="320" :locale="locale" />
|
|
51
61
|
</v-card>
|
|
52
62
|
</v-dialog>
|
|
53
|
-
<v-btn
|
|
54
|
-
v-if="canCreateFeedback"
|
|
55
|
-
text="create feedback"
|
|
56
|
-
class="text-none text-capitalize ml-2 screen-btn-primary filter-bar__fit"
|
|
57
|
-
variant="flat"
|
|
58
|
-
@click="showCreateDialog = true"
|
|
59
|
-
/>
|
|
60
63
|
</v-row>
|
|
61
64
|
</v-col>
|
|
62
65
|
|
|
63
66
|
<v-col cols="12">
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
density="comfortable"
|
|
67
|
+
<AppCard class="table-card">
|
|
68
|
+
<!-- One row inside the card - refresh, the range and the pager - in
|
|
69
|
+
place of a `v-toolbar` that stacked them above the table. Same
|
|
70
|
+
three controls, same handlers. -->
|
|
71
|
+
<CardToolbar :count="pageRange">
|
|
72
|
+
<template #left>
|
|
73
|
+
<AppButton
|
|
74
|
+
variant="icon"
|
|
75
|
+
icon="mdi-refresh"
|
|
76
|
+
aria-label="Refresh"
|
|
75
77
|
@click="(page = 1), refreshFeedbacks()"
|
|
76
|
-
|
|
77
|
-
<v-icon icon="mdi-refresh" />
|
|
78
|
-
</v-btn>
|
|
78
|
+
/>
|
|
79
79
|
</template>
|
|
80
80
|
|
|
81
|
-
<template #
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
v-model="page"
|
|
88
|
-
:length="pages"
|
|
89
|
-
@update:value="updatePage"
|
|
90
|
-
/>
|
|
91
|
-
</v-row>
|
|
81
|
+
<template #right>
|
|
82
|
+
<local-pagination
|
|
83
|
+
v-model="page"
|
|
84
|
+
:length="pages"
|
|
85
|
+
@update:value="updatePage"
|
|
86
|
+
/>
|
|
92
87
|
</template>
|
|
93
|
-
</
|
|
88
|
+
</CardToolbar>
|
|
94
89
|
|
|
95
90
|
<v-data-table
|
|
96
91
|
:headers="headers"
|
|
@@ -100,6 +95,7 @@
|
|
|
100
95
|
fixed-header
|
|
101
96
|
hide-default-footer
|
|
102
97
|
:hide-default-header="false"
|
|
98
|
+
:loading="loading"
|
|
103
99
|
@click:row="tableRowClickHandler"
|
|
104
100
|
style="max-height: calc(100vh - (200px))"
|
|
105
101
|
>
|
|
@@ -173,9 +169,10 @@
|
|
|
173
169
|
<span v-else class="mt-1 text-wrap"> Not Assigned </span>
|
|
174
170
|
</template>
|
|
175
171
|
</v-data-table>
|
|
176
|
-
</
|
|
172
|
+
</AppCard>
|
|
177
173
|
</v-col>
|
|
178
|
-
|
|
174
|
+
</v-row>
|
|
175
|
+
</div>
|
|
179
176
|
|
|
180
177
|
<!-- <FeedbackForm
|
|
181
178
|
v-model="showCreateDialog"
|