@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101
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/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +51 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/StockCard.vue +17 -12
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE CARD.
|
|
3
|
+
|
|
4
|
+
14px radius, a 1px `--border` rule and the design's one-pixel shadow, on
|
|
5
|
+
`--card`. A plain element rather than a `v-card`, because `v-card` brings its
|
|
6
|
+
own elevation, radius and surface colour at a specificity that a class has to
|
|
7
|
+
fight - which is why the radius kept coming back as 4px in Phase 3.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<div class="app-card" :class="{ 'app-card--flush': flush }">
|
|
11
|
+
<slot />
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
defineProps({
|
|
17
|
+
/** Let content (a menu, a dropdown) escape the card's rounded corners. */
|
|
18
|
+
flush: { type: Boolean, default: false },
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE DATE FIELD.
|
|
3
|
+
|
|
4
|
+
The same 40px box with a calendar glyph, as the handoff draws the two date
|
|
5
|
+
ends of a filter row. `<input type="date">` - the browser's own picker, which
|
|
6
|
+
is the one a phone user already knows and the one that is localised, keyboard
|
|
7
|
+
accessible and free.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<label class="app-field" :class="{ 'app-field--compact': compact }">
|
|
11
|
+
<v-icon class="app-field__icon" icon="mdi-calendar-month-outline" size="17" />
|
|
12
|
+
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
13
|
+
<input
|
|
14
|
+
class="app-field__input"
|
|
15
|
+
type="date"
|
|
16
|
+
:value="model"
|
|
17
|
+
:min="min || undefined"
|
|
18
|
+
:max="max || undefined"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
@input="model = ($event.target as HTMLInputElement).value"
|
|
21
|
+
/>
|
|
22
|
+
</label>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
const model = defineModel({ type: String, default: "" });
|
|
27
|
+
|
|
28
|
+
defineProps({
|
|
29
|
+
label: { type: String, default: "" },
|
|
30
|
+
min: { type: String, default: "" },
|
|
31
|
+
max: { type: String, default: "" },
|
|
32
|
+
compact: { type: Boolean, default: false },
|
|
33
|
+
disabled: { type: Boolean, default: false },
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE TEXT FIELD.
|
|
3
|
+
|
|
4
|
+
40px (34px compact), 10px radius, a 1px border, 13.5px/500 - and the label
|
|
5
|
+
OUTSIDE the box as a small muted word, not Vuetify's outlined field with a
|
|
6
|
+
label that floats up through the border. A native `<input>`, so the browser
|
|
7
|
+
keeps the keyboard, autofill and accessibility for free.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<label
|
|
11
|
+
class="app-field"
|
|
12
|
+
:class="[{ 'app-field--compact': compact, 'app-field--search': search }, klass]"
|
|
13
|
+
>
|
|
14
|
+
<v-icon
|
|
15
|
+
v-if="icon || search"
|
|
16
|
+
class="app-field__icon"
|
|
17
|
+
:icon="icon || 'mdi-magnify'"
|
|
18
|
+
size="18"
|
|
19
|
+
/>
|
|
20
|
+
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
21
|
+
<input
|
|
22
|
+
class="app-field__input"
|
|
23
|
+
:type="type"
|
|
24
|
+
:value="model"
|
|
25
|
+
:placeholder="placeholder"
|
|
26
|
+
:disabled="disabled"
|
|
27
|
+
:readonly="readonly"
|
|
28
|
+
@input="model = ($event.target as HTMLInputElement).value"
|
|
29
|
+
/>
|
|
30
|
+
</label>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
const model = defineModel({ type: String, default: "" });
|
|
35
|
+
|
|
36
|
+
defineProps({
|
|
37
|
+
label: { type: String, default: "" },
|
|
38
|
+
placeholder: { type: String, default: "" },
|
|
39
|
+
icon: { type: String, default: "" },
|
|
40
|
+
type: { type: String, default: "text" },
|
|
41
|
+
/** The design's search field: a magnifier and `flex:1` in a filter row. */
|
|
42
|
+
search: { type: Boolean, default: false },
|
|
43
|
+
compact: { type: Boolean, default: false },
|
|
44
|
+
disabled: { type: Boolean, default: false },
|
|
45
|
+
/** A field that prints a derived value - a preview, a computed reference -
|
|
46
|
+
and is not typed into. Kept distinct from `disabled`, which also greys
|
|
47
|
+
the box and takes it out of the tab order. */
|
|
48
|
+
readonly: { type: Boolean, default: false },
|
|
49
|
+
klass: { type: String, default: "" },
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
@@ -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>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE SELECT.
|
|
3
|
+
|
|
4
|
+
The handoff draws it as the text field with the filter's name in muted 12px
|
|
5
|
+
on the left, the chosen value next to it and a chevron on the right. A native
|
|
6
|
+
`<select>` under our chrome: the option list, the keyboard and the mobile
|
|
7
|
+
wheel are the browser's, so there is no menu to build and nothing to get
|
|
8
|
+
wrong on a phone.
|
|
9
|
+
|
|
10
|
+
`multiple` is the ONE case where native is the wrong answer. A native
|
|
11
|
+
`<select multiple>` is not a dropdown at all - it is an always-open scrolling
|
|
12
|
+
LIST BOX - so adopting it would turn the daily filter bars into list boxes
|
|
13
|
+
and change how the staff work. So multi opens a POPOVER in the same field
|
|
14
|
+
chrome, with the chosen values as the design's soft removable pills.
|
|
15
|
+
|
|
16
|
+
The single-value path below is byte-for-byte the one it has always been: a
|
|
17
|
+
consumer that passes neither `multiple` nor `return-object` still emits
|
|
18
|
+
exactly the `<select>` element's own string.
|
|
19
|
+
-->
|
|
20
|
+
<template>
|
|
21
|
+
<label
|
|
22
|
+
v-if="!multiple"
|
|
23
|
+
v-bind="$attrs"
|
|
24
|
+
class="app-field"
|
|
25
|
+
:class="{ 'app-field--compact': compact }"
|
|
26
|
+
>
|
|
27
|
+
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
28
|
+
<select
|
|
29
|
+
class="app-field__input"
|
|
30
|
+
:value="singleValue"
|
|
31
|
+
:disabled="disabled"
|
|
32
|
+
@change="onNativeChange"
|
|
33
|
+
>
|
|
34
|
+
<!-- `clearable` on a native select is the empty option, not an x. -->
|
|
35
|
+
<option v-if="clearable" value="">{{ placeholder }}</option>
|
|
36
|
+
<option v-for="opt in normalized" :key="String(opt.value)" :value="opt.value">
|
|
37
|
+
{{ opt.title }}
|
|
38
|
+
</option>
|
|
39
|
+
</select>
|
|
40
|
+
<v-icon class="app-field__icon" icon="mdi-chevron-down" size="17" />
|
|
41
|
+
</label>
|
|
42
|
+
|
|
43
|
+
<v-menu
|
|
44
|
+
v-else
|
|
45
|
+
:close-on-content-click="false"
|
|
46
|
+
:disabled="disabled"
|
|
47
|
+
location="bottom start"
|
|
48
|
+
offset="4"
|
|
49
|
+
>
|
|
50
|
+
<template #activator="{ props: activator }">
|
|
51
|
+
<div
|
|
52
|
+
v-bind="{ ...activator, ...$attrs }"
|
|
53
|
+
class="app-field app-select"
|
|
54
|
+
:class="{ 'app-field--compact': compact, 'app-select--disabled': disabled }"
|
|
55
|
+
role="button"
|
|
56
|
+
:aria-disabled="disabled"
|
|
57
|
+
:tabindex="disabled ? -1 : 0"
|
|
58
|
+
>
|
|
59
|
+
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
60
|
+
<span class="app-select__values">
|
|
61
|
+
<StatusChip
|
|
62
|
+
v-for="opt in selectedOptions"
|
|
63
|
+
:key="String(opt.value)"
|
|
64
|
+
tone="neutral"
|
|
65
|
+
pill
|
|
66
|
+
:dot="false"
|
|
67
|
+
>
|
|
68
|
+
{{ opt.title }}
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
class="app-select__x"
|
|
72
|
+
:aria-label="`Remove ${opt.title}`"
|
|
73
|
+
@click.stop="toggle(opt)"
|
|
74
|
+
>
|
|
75
|
+
<v-icon icon="mdi-close" size="12" />
|
|
76
|
+
</button>
|
|
77
|
+
</StatusChip>
|
|
78
|
+
<span v-if="!selectedOptions.length" class="app-select__placeholder">
|
|
79
|
+
{{ placeholder }}
|
|
80
|
+
</span>
|
|
81
|
+
</span>
|
|
82
|
+
<button
|
|
83
|
+
v-if="clearable && selectedOptions.length"
|
|
84
|
+
type="button"
|
|
85
|
+
class="app-select__clear"
|
|
86
|
+
aria-label="Clear"
|
|
87
|
+
@click.stop="clear"
|
|
88
|
+
>
|
|
89
|
+
<v-icon icon="mdi-close-circle" size="15" />
|
|
90
|
+
</button>
|
|
91
|
+
<v-icon class="app-field__icon" icon="mdi-chevron-down" size="17" />
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<div class="app-select__menu">
|
|
96
|
+
<button
|
|
97
|
+
v-for="opt in normalized"
|
|
98
|
+
:key="String(opt.value)"
|
|
99
|
+
type="button"
|
|
100
|
+
class="app-select__option"
|
|
101
|
+
:aria-pressed="isOn(opt)"
|
|
102
|
+
@click="toggle(opt)"
|
|
103
|
+
>
|
|
104
|
+
<v-icon
|
|
105
|
+
class="app-select__box"
|
|
106
|
+
:class="{ 'app-select__box--on': isOn(opt) }"
|
|
107
|
+
:icon="isOn(opt) ? 'mdi-checkbox-marked' : 'mdi-checkbox-blank-outline'"
|
|
108
|
+
size="17"
|
|
109
|
+
/>
|
|
110
|
+
<span class="app-select__option-text">{{ opt.title }}</span>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
</v-menu>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<script setup lang="ts">
|
|
117
|
+
/* `v-menu` has no single root node, so a class on `<AppSelect multiple>` has
|
|
118
|
+
to be put on the activator by hand. The single branch lands where it always
|
|
119
|
+
did - on the `<label>`. */
|
|
120
|
+
defineOptions({ inheritAttrs: false });
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* One model for both shapes: a string when single, an array when multiple.
|
|
124
|
+
* The default stays `""` so an existing single consumer that never sets its
|
|
125
|
+
* v-model starts on the same empty string it starts on today.
|
|
126
|
+
*/
|
|
127
|
+
const model = defineModel<any>({ default: "" });
|
|
128
|
+
|
|
129
|
+
const props = defineProps({
|
|
130
|
+
label: { type: String, default: "" },
|
|
131
|
+
/** Strings, or the `{ title, value }` shape `v-select` already takes. */
|
|
132
|
+
items: { type: Array as PropType<any[]>, default: () => [] },
|
|
133
|
+
itemTitle: { type: String, default: "title" },
|
|
134
|
+
itemValue: { type: String, default: "value" },
|
|
135
|
+
compact: { type: Boolean, default: false },
|
|
136
|
+
disabled: { type: Boolean, default: false },
|
|
137
|
+
/** Popover multi-select with removable pills. Never a native list box. */
|
|
138
|
+
multiple: { type: Boolean, default: false },
|
|
139
|
+
/** Emit the source item rather than its `itemValue`. */
|
|
140
|
+
returnObject: { type: Boolean, default: false },
|
|
141
|
+
clearable: { type: Boolean, default: false },
|
|
142
|
+
placeholder: { type: String, default: "" },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const normalized = computed(() =>
|
|
146
|
+
props.items.map((item: any) =>
|
|
147
|
+
typeof item === "object" && item !== null
|
|
148
|
+
? { title: item[props.itemTitle], value: item[props.itemValue], raw: item }
|
|
149
|
+
: { title: String(item), value: item, raw: item }
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
/* A model entry is either the plain value or - under `return-object` - the
|
|
154
|
+
source item, so everything compares on this one key. */
|
|
155
|
+
const keyOf = (v: any) =>
|
|
156
|
+
String(v !== null && typeof v === "object" ? v[props.itemValue] : v);
|
|
157
|
+
|
|
158
|
+
/* ---- single ---------------------------------------------------------- */
|
|
159
|
+
|
|
160
|
+
const singleValue = computed(() =>
|
|
161
|
+
props.returnObject ? keyOf(model.value) : model.value
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
function onNativeChange(event: Event) {
|
|
165
|
+
const value = (event.target as HTMLSelectElement).value;
|
|
166
|
+
model.value = props.returnObject
|
|
167
|
+
? normalized.value.find((o) => String(o.value) === value)?.raw ?? value
|
|
168
|
+
: value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ---- multiple -------------------------------------------------------- */
|
|
172
|
+
|
|
173
|
+
const selected = computed<any[]>(() =>
|
|
174
|
+
Array.isArray(model.value) ? model.value : []
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const selectedKeys = computed(() => new Set(selected.value.map(keyOf)));
|
|
178
|
+
|
|
179
|
+
const isOn = (opt: any) => selectedKeys.value.has(String(opt.value));
|
|
180
|
+
|
|
181
|
+
/* Fall back to the raw key when a chosen value is not in `items` - an async
|
|
182
|
+
item list can arrive after the model does, and the chip must still print. */
|
|
183
|
+
const selectedOptions = computed(() =>
|
|
184
|
+
selected.value.map(
|
|
185
|
+
(v) =>
|
|
186
|
+
normalized.value.find((o) => String(o.value) === keyOf(v)) ?? {
|
|
187
|
+
title: keyOf(v),
|
|
188
|
+
value: keyOf(v),
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
function toggle(opt: any) {
|
|
194
|
+
if (props.disabled) return;
|
|
195
|
+
model.value = isOn(opt)
|
|
196
|
+
? selected.value.filter((v) => keyOf(v) !== String(opt.value))
|
|
197
|
+
: [...selected.value, props.returnObject ? opt.raw : opt.value];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function clear() {
|
|
201
|
+
model.value = [];
|
|
202
|
+
}
|
|
203
|
+
</script>
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters align="center" justify="center">
|
|
3
3
|
<v-col cols="12">
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
</
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<v-card-text class="pa-0">
|
|
4
|
+
<!-- The screen's name and its Back move out of the `grey-lighten-4` bar -
|
|
5
|
+
a slab that stayed white in dark mode - onto the design's title row.
|
|
6
|
+
All five service apps mount this component bare, so the layer is the
|
|
7
|
+
only place the page name can come from. -->
|
|
8
|
+
<PageHeader title="Area Checklist">
|
|
9
|
+
<template #actions>
|
|
10
|
+
<AppButton variant="ghost" icon="mdi-chevron-left" @click="back()">
|
|
11
|
+
Back
|
|
12
|
+
</AppButton>
|
|
13
|
+
</template>
|
|
14
|
+
</PageHeader>
|
|
15
|
+
|
|
16
|
+
<AppCard>
|
|
17
|
+
<div class="pa-0">
|
|
19
18
|
<v-row no-gutters>
|
|
20
19
|
<v-col
|
|
21
20
|
cols="12"
|
|
22
21
|
md="6"
|
|
23
22
|
lg="5"
|
|
24
23
|
xl="4"
|
|
25
|
-
|
|
24
|
+
class="checklist-detail__split"
|
|
26
25
|
>
|
|
27
26
|
<v-row no-gutters class="pa-6">
|
|
28
27
|
<v-col cols="12" class="mb-4">
|
|
@@ -47,11 +46,11 @@
|
|
|
47
46
|
|
|
48
47
|
<v-col cols="12" class="mb-3">
|
|
49
48
|
<div class="text-body-2 mb-1">Status:</div>
|
|
50
|
-
<
|
|
51
|
-
class="text-capitalize
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
<StatusChip
|
|
50
|
+
class="text-capitalize"
|
|
51
|
+
:status="status"
|
|
52
|
+
:label="status || 'N/A'"
|
|
53
|
+
/>
|
|
55
54
|
</v-col>
|
|
56
55
|
</v-row>
|
|
57
56
|
</v-col>
|
|
@@ -119,15 +118,15 @@
|
|
|
119
118
|
class="text-subtitle-2 font-weight-bold px-0"
|
|
120
119
|
>
|
|
121
120
|
Set {{ checklistSet.set }}
|
|
122
|
-
<
|
|
121
|
+
<StatusChip
|
|
123
122
|
v-if="checklistSet.isScheduleTask"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class="
|
|
123
|
+
tone="info"
|
|
124
|
+
:dot="false"
|
|
125
|
+
pill
|
|
126
|
+
class="ml-2"
|
|
128
127
|
>
|
|
129
128
|
Schedule Task
|
|
130
|
-
</
|
|
129
|
+
</StatusChip>
|
|
131
130
|
</v-list-subheader>
|
|
132
131
|
|
|
133
132
|
<v-list-item
|
|
@@ -161,22 +160,14 @@
|
|
|
161
160
|
</div>
|
|
162
161
|
</v-col>
|
|
163
162
|
<v-col cols="12" sm="2" class="text-right">
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
: 'grey'
|
|
169
|
-
"
|
|
170
|
-
:variant="
|
|
171
|
-
unit.status?.toLowerCase() === 'completed'
|
|
172
|
-
? 'flat'
|
|
173
|
-
: 'outlined'
|
|
174
|
-
"
|
|
175
|
-
size="small"
|
|
163
|
+
<!-- Same word decides the colour, only now through the
|
|
164
|
+
shared status map, so "Completed" here is the same
|
|
165
|
+
green as on every other screen. -->
|
|
166
|
+
<StatusChip
|
|
176
167
|
class="text-capitalize"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
168
|
+
:status="unit.status || 'Open'"
|
|
169
|
+
:label="unit.status || 'Open'"
|
|
170
|
+
/>
|
|
180
171
|
</v-col>
|
|
181
172
|
</v-row>
|
|
182
173
|
</v-list-item>
|
|
@@ -191,8 +182,8 @@
|
|
|
191
182
|
</v-row>
|
|
192
183
|
</v-col>
|
|
193
184
|
</v-row>
|
|
194
|
-
</
|
|
195
|
-
</
|
|
185
|
+
</div>
|
|
186
|
+
</AppCard>
|
|
196
187
|
</v-col>
|
|
197
188
|
</v-row>
|
|
198
189
|
</template>
|
|
@@ -242,3 +233,19 @@ const timeOut = ref<string>("N/A");
|
|
|
242
233
|
const status = ref<string>("N/A");
|
|
243
234
|
const signatureUrl = ref<string>("");
|
|
244
235
|
</script>
|
|
236
|
+
|
|
237
|
+
<style scoped>
|
|
238
|
+
/* Was an inline `rgba(0,0,0,.12)`, which is the same near-black rule in dark
|
|
239
|
+
mode. The token follows the theme. At `md` and below the two halves stack, so
|
|
240
|
+
the rule moves under the first one. */
|
|
241
|
+
.checklist-detail__split {
|
|
242
|
+
border-bottom: 1px solid var(--border);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@media (min-width: 960px) {
|
|
246
|
+
.checklist-detail__split {
|
|
247
|
+
border-bottom: none;
|
|
248
|
+
border-right: 1px solid var(--border);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
<template #extension>
|
|
34
34
|
<v-row no-gutters class="w-100 d-flex flex-column">
|
|
35
35
|
<v-card
|
|
36
|
-
class="w-100
|
|
36
|
+
class="w-100 filter-bar d-flex align-center ga-5"
|
|
37
37
|
flat
|
|
38
38
|
:height="60"
|
|
39
39
|
>
|
|
@@ -80,7 +80,11 @@
|
|
|
80
80
|
</div>
|
|
81
81
|
</template>
|
|
82
82
|
<template #item.status="{ value }">
|
|
83
|
-
<
|
|
83
|
+
<StatusChip
|
|
84
|
+
class="text-capitalize"
|
|
85
|
+
:status="value"
|
|
86
|
+
:label="value || 'No Status'"
|
|
87
|
+
/>
|
|
84
88
|
</template>
|
|
85
89
|
</TableMain></v-col
|
|
86
90
|
>
|
package/components/AreaMain.vue
CHANGED
|
@@ -49,10 +49,8 @@
|
|
|
49
49
|
<!-- Primary action -->
|
|
50
50
|
<v-btn
|
|
51
51
|
v-if="canCreateArea"
|
|
52
|
-
class="text-none mr-3"
|
|
53
|
-
|
|
54
|
-
variant="tonal"
|
|
55
|
-
size="large"
|
|
52
|
+
class="text-none mr-3 screen-btn-primary"
|
|
53
|
+
variant="flat"
|
|
56
54
|
@click="onCreateItem"
|
|
57
55
|
>
|
|
58
56
|
{{ createLabel }}
|
|
@@ -63,10 +61,8 @@
|
|
|
63
61
|
<template #activator="{ props: t }">
|
|
64
62
|
<v-btn
|
|
65
63
|
v-bind="t"
|
|
66
|
-
class="ma-1"
|
|
67
|
-
variant="
|
|
68
|
-
size="large"
|
|
69
|
-
rounded="pill"
|
|
64
|
+
class="ma-1 screen-icon-btn"
|
|
65
|
+
variant="flat"
|
|
70
66
|
href="/files/area-sample-data.xlsx"
|
|
71
67
|
target="_blank"
|
|
72
68
|
download
|
|
@@ -81,10 +77,8 @@
|
|
|
81
77
|
<template #activator="{ props: t }">
|
|
82
78
|
<v-btn
|
|
83
79
|
v-bind="t"
|
|
84
|
-
class="ma-1"
|
|
85
|
-
variant="
|
|
86
|
-
size="large"
|
|
87
|
-
rounded="pill"
|
|
80
|
+
class="ma-1 screen-icon-btn"
|
|
81
|
+
variant="flat"
|
|
88
82
|
@click="onImportData"
|
|
89
83
|
>
|
|
90
84
|
<v-icon>mdi-upload</v-icon>
|
|
@@ -97,10 +91,8 @@
|
|
|
97
91
|
<template #activator="{ props: t }">
|
|
98
92
|
<v-btn
|
|
99
93
|
v-bind="t"
|
|
100
|
-
class="ma-1"
|
|
101
|
-
variant="
|
|
102
|
-
size="large"
|
|
103
|
-
rounded="pill"
|
|
94
|
+
class="ma-1 screen-icon-btn"
|
|
95
|
+
variant="flat"
|
|
104
96
|
@click="handleDownloadExcel"
|
|
105
97
|
:loading="isDownloading"
|
|
106
98
|
>
|
|
@@ -114,7 +106,7 @@
|
|
|
114
106
|
|
|
115
107
|
<v-spacer />
|
|
116
108
|
|
|
117
|
-
<v-col cols="auto">
|
|
109
|
+
<v-col cols="auto" class="filter-field">
|
|
118
110
|
<v-text-field
|
|
119
111
|
v-model="searchInput"
|
|
120
112
|
density="compact"
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
@row-click="handleRowClick"
|
|
16
16
|
>
|
|
17
17
|
<template #actions>
|
|
18
|
+
<!-- Was `color="black"`: a black slab in both themes, and the only
|
|
19
|
+
black surface on the page. Same button, the design's ghost. -->
|
|
18
20
|
<v-btn
|
|
19
21
|
v-if="canManageAttendanceSettings"
|
|
20
|
-
variant="
|
|
21
|
-
|
|
22
|
-
class="text-none"
|
|
22
|
+
variant="outlined"
|
|
23
|
+
class="text-none screen-btn-ghost"
|
|
23
24
|
@click="dialogShowSettings = true"
|
|
24
25
|
>
|
|
25
26
|
<v-icon class="mr-2">mdi-cog</v-icon>
|