@7365admin1/layer-common 3.2.2-staging.118 → 3.2.2-staging.121
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/assets/css/primitives.css +168 -0
- package/assets/css/screens.css +76 -1
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AddPassKeyToVisitor.vue +95 -73
- package/components/AppDateField.vue +15 -1
- package/components/AppField.vue +25 -0
- package/components/BuildingUnitFormEdit.vue +5 -1
- package/components/Card/DeleteConfirmation.vue +5 -2
- package/components/HidAccessLogDashboard.vue +288 -447
- package/components/HidIntercomManagement.vue +387 -521
- package/components/HidQrCodeConfiguration.vue +293 -369
- package/components/HidReaderManagement.vue +197 -320
- package/components/HidServiceSettingsPanel.vue +38 -26
- package/components/HidUserEnrollment.vue +239 -344
- package/components/IncidentReport/Authorities.vue +30 -18
- package/components/Nfc/NFCTagForm.vue +5 -1
- package/components/ScanVisitorQRCode.vue +64 -28
- package/components/SearchVehicleNumberUser.vue +39 -37
- package/components/TableMain.vue +50 -1
- package/components/VehicleAddSelection.vue +48 -32
- package/components/VehicleForm.vue +112 -129
- package/components/VehicleUpdateMoreAction.vue +59 -46
- package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
- package/components/VisitorFormSelection.vue +40 -37
- package/components/VisitorPassKeyQRScanner.vue +65 -18
- package/components/VisitorSocketPopUp.vue +21 -13
- package/components/VisitorsReportPreview.vue +44 -29
- package/package.json +1 -1
- package/tools/render-harness/baselines.json +95 -0
- package/tools/render-harness/harness-init.ps1 +4 -0
- package/utils/status.test.ts +33 -0
- package/utils/status.ts +36 -0
- package/utils/theme-aa-ledger.ts +142 -0
- package/utils/theme.test.ts +95 -34
- package/utils/theme.ts +55 -28
|
@@ -2,37 +2,39 @@
|
|
|
2
2
|
<v-row no-gutters class="px-5 pt-4">
|
|
3
3
|
<!-- Authorities (Police/SCDF/Ambulance called in at the scene) -->
|
|
4
4
|
<v-col cols="12" class="border-b pb-5 mb-5 mt-1">
|
|
5
|
-
<p class="
|
|
5
|
+
<p class="screen-card-title mb-2">
|
|
6
6
|
Authorities (Police/SCDF/Ambulance called in at the scene)?
|
|
7
7
|
</p>
|
|
8
8
|
<p
|
|
9
9
|
v-if="authorities?.authoritiesValue == 'no'"
|
|
10
|
-
class="mt-2 text-
|
|
10
|
+
class="report-answer mt-2 text-capitalize"
|
|
11
11
|
>
|
|
12
12
|
{{ authorities?.authoritiesValue }}
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
<v-
|
|
16
|
-
v-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</
|
|
15
|
+
<AppCard v-else class="table-card mt-5">
|
|
16
|
+
<v-data-table
|
|
17
|
+
:headers="authoritiesTableHeader"
|
|
18
|
+
:items="authorities?.authoritiesCalled"
|
|
19
|
+
fixed-header
|
|
20
|
+
hide-default-footer
|
|
21
|
+
:hide-default-header="false"
|
|
22
|
+
items-per-page="100"
|
|
23
|
+
style="max-height: calc(100vh - (200px))"
|
|
24
|
+
>
|
|
25
|
+
</v-data-table>
|
|
26
|
+
</AppCard>
|
|
27
27
|
</v-col>
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
<!-- `w-100` + `mx-2` made the card 16px wider than its column, so its right
|
|
29
|
+
corner was clipped off. The card supplies its own edge now. -->
|
|
30
|
+
<AppCard class="table-card w-100 my-8">
|
|
31
|
+
<v-table class="responsive-table elevation-0">
|
|
30
32
|
<thead>
|
|
31
33
|
<tr>
|
|
32
34
|
<th
|
|
33
35
|
v-for="(item, index) in concernHeader"
|
|
34
36
|
:key="index"
|
|
35
|
-
class="text-left
|
|
37
|
+
class="text-left border-e"
|
|
36
38
|
>
|
|
37
39
|
{{ item }}
|
|
38
40
|
</th>
|
|
@@ -196,10 +198,20 @@
|
|
|
196
198
|
</tr>
|
|
197
199
|
</tbody>
|
|
198
200
|
</v-table>
|
|
199
|
-
</
|
|
201
|
+
</AppCard>
|
|
200
202
|
</v-row>
|
|
201
203
|
</template>
|
|
202
204
|
|
|
205
|
+
<style scoped>
|
|
206
|
+
/* The prototype states no size for a plain answer line, so it takes the body
|
|
207
|
+
row's own type rather than Vuetify's `text-h6`. */
|
|
208
|
+
.report-answer {
|
|
209
|
+
font-size: var(--fs-cell);
|
|
210
|
+
font-weight: var(--fw-cell);
|
|
211
|
+
color: var(--text);
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
214
|
+
|
|
203
215
|
<script lang="ts" setup>
|
|
204
216
|
definePageMeta({
|
|
205
217
|
middleware: ["01-auth", "02-org"],
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
</v-col>
|
|
65
65
|
|
|
66
66
|
<v-col cols="6" class="pa-0">
|
|
67
|
+
<!-- Was `color="black"`: a fixed slab that stayed black on the dark
|
|
68
|
+
card. `primary-button` is the theme's FILL token, and its pair
|
|
69
|
+
`on-primary-button` supplies the white label - unlike `primary`,
|
|
70
|
+
which is a FOREGROUND here (links, icons, text buttons). -->
|
|
67
71
|
<v-btn
|
|
68
72
|
block
|
|
69
73
|
tile
|
|
@@ -71,7 +75,7 @@
|
|
|
71
75
|
class="text-none"
|
|
72
76
|
size="large"
|
|
73
77
|
height="48"
|
|
74
|
-
color="
|
|
78
|
+
color="primary-button"
|
|
75
79
|
@click="submit"
|
|
76
80
|
:loading="loading"
|
|
77
81
|
:disabled="!valid || loading || !hasChanges"
|
|
@@ -9,36 +9,27 @@
|
|
|
9
9
|
>
|
|
10
10
|
<v-container class="d-flex justify-center" max-height="90vh">
|
|
11
11
|
<!-- QR code reader container -->
|
|
12
|
-
<v-card
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<v-toolbar>
|
|
17
|
-
<v-card-title class="text-h5"> Scan QR Code </v-card-title>
|
|
18
|
-
<v-spacer />
|
|
19
|
-
<v-btn
|
|
20
|
-
color="grey-darken-1"
|
|
21
|
-
icon="mdi-close"
|
|
22
|
-
@click="closeDialog()"
|
|
23
|
-
/>
|
|
24
|
-
</v-toolbar>
|
|
25
|
-
|
|
26
|
-
<div
|
|
27
|
-
id="reader"
|
|
28
|
-
class="d-flex justify-center align-center w-100 h-100"
|
|
29
|
-
style="height: calc(100vh - 64px)"
|
|
30
|
-
>
|
|
31
|
-
<!-- The QR code scanner box -->
|
|
32
|
-
<!-- This is where the QR code scanner will be displayed -->
|
|
12
|
+
<v-card class="screen-modal qr-scanner">
|
|
13
|
+
<div class="qr-scanner__head">
|
|
14
|
+
<v-card-title>Scan QR Code</v-card-title>
|
|
15
|
+
<AppButton variant="icon" icon="mdi-close" @click="closeDialog()" />
|
|
33
16
|
</div>
|
|
34
|
-
|
|
35
|
-
<div
|
|
36
|
-
<
|
|
17
|
+
|
|
18
|
+
<div class="screen-modal__body qr-scanner__body">
|
|
19
|
+
<div id="reader" class="qr-scanner__reader">
|
|
20
|
+
<!-- The QR code scanner box -->
|
|
21
|
+
<!-- This is where the QR code scanner will be displayed -->
|
|
22
|
+
</div>
|
|
23
|
+
<!-- Show Error Messages -->
|
|
24
|
+
<p v-if="showNotFoundMessage" class="qr-scanner__error">{{ errorMessage }}</p>
|
|
25
|
+
<!-- Switch camera button inside the reader box -->
|
|
26
|
+
<AppButton
|
|
27
|
+
icon="mdi-camera-switch"
|
|
28
|
+
class="qr-scanner__switch"
|
|
29
|
+
aria-label="Switch camera"
|
|
30
|
+
@click="switchCamera()"
|
|
31
|
+
/>
|
|
37
32
|
</div>
|
|
38
|
-
<!-- Switch camera button inside the reader box -->
|
|
39
|
-
<v-btn color="primary" icon class="mt-4" @click="switchCamera()">
|
|
40
|
-
<v-icon icon="mdi-camera-switch" large />
|
|
41
|
-
</v-btn>
|
|
42
33
|
</v-card>
|
|
43
34
|
</v-container>
|
|
44
35
|
</v-dialog>
|
|
@@ -225,3 +216,48 @@ const stopScanner = () => {
|
|
|
225
216
|
});
|
|
226
217
|
};
|
|
227
218
|
</script>
|
|
219
|
+
|
|
220
|
+
<style scoped>
|
|
221
|
+
/* Same scanner frame as the pass-key scanner: the card used to be `w-100`
|
|
222
|
+
inside the centring container, so keep that width. */
|
|
223
|
+
.qr-scanner {
|
|
224
|
+
width: 100%;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.qr-scanner__head {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
padding-right: 12px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.qr-scanner__body {
|
|
235
|
+
display: flex;
|
|
236
|
+
flex-direction: column;
|
|
237
|
+
align-items: center;
|
|
238
|
+
gap: 16px;
|
|
239
|
+
padding: 4px 20px 20px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.qr-scanner__reader {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
width: 100%;
|
|
247
|
+
min-height: 320px;
|
|
248
|
+
border: 1px solid var(--border);
|
|
249
|
+
border-radius: var(--r-inner);
|
|
250
|
+
background: var(--thead);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.qr-scanner__error {
|
|
254
|
+
margin: 0;
|
|
255
|
+
font-size: var(--fs-cell);
|
|
256
|
+
color: var(--err);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.qr-scanner__switch {
|
|
260
|
+
width: var(--btn-h);
|
|
261
|
+
padding: 0;
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
@@ -1,44 +1,35 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-card width="100%">
|
|
3
|
-
<v-
|
|
4
|
-
<v-row no-gutters>
|
|
5
|
-
<v-col cols="12" class="text-h6 font-weight-bold px-3">Vehicle Number ({{ vehicleNumber }})</v-col>
|
|
6
|
-
</v-row>
|
|
7
|
-
</v-toolbar>
|
|
2
|
+
<v-card width="100%" class="screen-modal search-vehicle-user">
|
|
3
|
+
<v-card-title>Vehicle Number ({{ vehicleNumber }})</v-card-title>
|
|
8
4
|
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
<div class="screen-modal__body search-vehicle-user__body">
|
|
6
|
+
<!-- `table-card` is the shared table card's own class, so the header
|
|
7
|
+
type, the cell padding and the row rule are the design's - without
|
|
8
|
+
TableMain's page toolbar, which a two-row picker inside a dialog
|
|
9
|
+
has nothing to put in. -->
|
|
10
|
+
<AppCard class="table-card">
|
|
11
|
+
<v-data-table :items="prop.vehicleNumberUsersList" hide-default-footer :headers="headers" density="comfortable"
|
|
12
|
+
item-key="_id ">
|
|
13
|
+
<template #item.name="{ item }">
|
|
14
|
+
<span class="d-flex align-center ga-2">
|
|
15
|
+
<span>
|
|
16
|
+
<AvatarMain :name="item?.name" :size="20" :id="item?._id" />
|
|
17
|
+
</span>
|
|
18
|
+
<span class="text-capitalize">{{ item?.name }}</span>
|
|
16
19
|
</span>
|
|
17
|
-
|
|
18
|
-
</span>
|
|
19
|
-
</template>
|
|
20
|
+
</template>
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
<template #item.actions="{ item }">
|
|
23
|
+
<AppButton variant="ghost" @click="handleSelectUser(item)">Select</AppButton>
|
|
24
|
+
</template>
|
|
25
|
+
</v-data-table>
|
|
26
|
+
</AppCard>
|
|
27
|
+
</div>
|
|
26
28
|
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Close
|
|
32
|
-
</v-btn>
|
|
33
|
-
</v-col>
|
|
34
|
-
|
|
35
|
-
<v-col cols="6" class="pa-0">
|
|
36
|
-
<v-btn block variant="flat" color="black" class="text-none" height="48" tile @click="emit('close')">
|
|
37
|
-
Add Different User
|
|
38
|
-
</v-btn>
|
|
39
|
-
</v-col>
|
|
40
|
-
</v-row>
|
|
41
|
-
</v-toolbar>
|
|
29
|
+
<div class="search-vehicle-user__footer">
|
|
30
|
+
<AppButton variant="ghost" @click="emit('close')">Close</AppButton>
|
|
31
|
+
<AppButton @click="emit('close')">Add Different User</AppButton>
|
|
32
|
+
</div>
|
|
42
33
|
<v-dialog v-model="confirmAction" persistent max-width="450">
|
|
43
34
|
<DialogReplaceAutofillPrompt @close="confirmAction = false" @proceed="handleUpdateUserDetails"/>
|
|
44
35
|
</v-dialog>
|
|
@@ -89,4 +80,15 @@ onMounted(() => {
|
|
|
89
80
|
})
|
|
90
81
|
</script>
|
|
91
82
|
|
|
92
|
-
<style scoped
|
|
83
|
+
<style scoped>
|
|
84
|
+
.search-vehicle-user__body {
|
|
85
|
+
padding: 4px 20px 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.search-vehicle-user__footer {
|
|
89
|
+
display: flex;
|
|
90
|
+
justify-content: flex-end;
|
|
91
|
+
gap: 8px;
|
|
92
|
+
padding: 16px 20px 20px;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
package/components/TableMain.vue
CHANGED
|
@@ -155,8 +155,14 @@
|
|
|
155
155
|
</div>
|
|
156
156
|
|
|
157
157
|
<!-- Data Table -->
|
|
158
|
+
<!--
|
|
159
|
+
`tableHeaders`, not `headers`, and the two are the SAME ARRAY unless a
|
|
160
|
+
caller has opted into fixed proportions by putting `weight` on a
|
|
161
|
+
column. See `columnsAreWeighted` below.
|
|
162
|
+
-->
|
|
158
163
|
<v-data-table
|
|
159
|
-
:
|
|
164
|
+
:class="columnsAreWeighted ? 'table-card__fixed' : undefined"
|
|
165
|
+
:headers="tableHeaders"
|
|
160
166
|
:items="items"
|
|
161
167
|
:item-value="itemValue"
|
|
162
168
|
:items-per-page="itemsPerPage"
|
|
@@ -257,6 +263,49 @@ const props = defineProps({
|
|
|
257
263
|
},
|
|
258
264
|
});
|
|
259
265
|
|
|
266
|
+
/**
|
|
267
|
+
* THE DESIGN'S FIXED COLUMN PROPORTIONS, AND WHY THEY ARE OPT-IN.
|
|
268
|
+
*
|
|
269
|
+
* Every table in the prototype is a CSS grid with an explicit, per-table
|
|
270
|
+
* `grid-template-columns` - `0.9fr 1.5fr 1.7fr 0.8fr 1fr 1.1fr` on Feedbacks,
|
|
271
|
+
* `2fr 1fr 1fr` on Units, `1.6fr 1fr 1.3fr 1.6fr 48px` on Pass & Key. Those
|
|
272
|
+
* ratios are a per-screen design decision. Nothing in a caller's `headers`
|
|
273
|
+
* array carries that information, so this shared component cannot derive them,
|
|
274
|
+
* and inventing one set of proportions for 21 components and 18 app pages would
|
|
275
|
+
* be worse than the automatic layout they have now.
|
|
276
|
+
*
|
|
277
|
+
* So it is stated, not guessed: a header may carry `weight: 1.5` and this turns
|
|
278
|
+
* the weights into percentage widths and switches the table to
|
|
279
|
+
* `table-layout: fixed`, which is what makes a width binding rather than a hint.
|
|
280
|
+
* A column with NO weight is left exactly as the caller wrote it, so an
|
|
281
|
+
* existing `width` (an action column pinned at 48px, say) survives untouched
|
|
282
|
+
* and only the weighted columns share out what is left.
|
|
283
|
+
*
|
|
284
|
+
* NO caller changes. With no weights anywhere, `tableHeaders` IS `props.headers`
|
|
285
|
+
* - the same array reference - and the table renders exactly what it renders
|
|
286
|
+
* today, automatic widths and all.
|
|
287
|
+
*/
|
|
288
|
+
const columnsAreWeighted = computed(() =>
|
|
289
|
+
props.headers.some((h) => Number(h?.weight) > 0)
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const tableHeaders = computed(() => {
|
|
293
|
+
if (!columnsAreWeighted.value) return props.headers;
|
|
294
|
+
|
|
295
|
+
// Columns with no weight keep whatever width they already had (often none,
|
|
296
|
+
// e.g. a 48px action column), and only the weighted ones share what is left.
|
|
297
|
+
const total = props.headers.reduce(
|
|
298
|
+
(sum, h) => sum + (Number(h?.weight) > 0 ? Number(h.weight) : 0),
|
|
299
|
+
0
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return props.headers.map((h) =>
|
|
303
|
+
Number(h?.weight) > 0
|
|
304
|
+
? { ...h, width: `${((Number(h.weight) / total) * 100).toFixed(4)}%` }
|
|
305
|
+
: h
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
260
309
|
const emits = defineEmits([
|
|
261
310
|
"create",
|
|
262
311
|
"scan",
|
|
@@ -1,36 +1,24 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-card width="100%">
|
|
3
|
-
<v-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
tile
|
|
23
|
-
block
|
|
24
|
-
variant="text"
|
|
25
|
-
class="text-none"
|
|
26
|
-
size="48"
|
|
27
|
-
@click="cancel"
|
|
28
|
-
>
|
|
29
|
-
Cancel
|
|
30
|
-
</v-btn>
|
|
31
|
-
</v-col>
|
|
32
|
-
</v-row>
|
|
33
|
-
</v-toolbar>
|
|
2
|
+
<v-card width="100%" class="screen-modal vehicle-add">
|
|
3
|
+
<v-card-title>Add Vehicle</v-card-title>
|
|
4
|
+
|
|
5
|
+
<div class="screen-modal__body vehicle-add__body">
|
|
6
|
+
<p class="vehicle-add__hint">Please Select Status</p>
|
|
7
|
+
<AppButton
|
|
8
|
+
v-for="item in selection"
|
|
9
|
+
:key="item.value"
|
|
10
|
+
class="vehicle-add__option"
|
|
11
|
+
@click="select(item.value)"
|
|
12
|
+
>
|
|
13
|
+
{{ item.label }}
|
|
14
|
+
</AppButton>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="vehicle-add__footer">
|
|
18
|
+
<AppButton variant="ghost" class="vehicle-add__cancel" @click="cancel">
|
|
19
|
+
Cancel
|
|
20
|
+
</AppButton>
|
|
21
|
+
</div>
|
|
34
22
|
</v-card>
|
|
35
23
|
</template>
|
|
36
24
|
|
|
@@ -56,3 +44,31 @@ function select(value: TVehicleType | 'seasonpass') {
|
|
|
56
44
|
emit("select", value);
|
|
57
45
|
}
|
|
58
46
|
</script>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
/* The three choices are full-width by the screen's own design, so the only
|
|
50
|
+
thing said here is the width and the stack - the button itself is AppButton. */
|
|
51
|
+
.vehicle-add__body {
|
|
52
|
+
padding: 20px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.vehicle-add__hint {
|
|
56
|
+
margin: 0 0 14px;
|
|
57
|
+
text-align: center;
|
|
58
|
+
font-size: var(--fs-cell);
|
|
59
|
+
color: var(--muted);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.vehicle-add__option {
|
|
63
|
+
display: flex;
|
|
64
|
+
width: 100%;
|
|
65
|
+
margin-bottom: 10px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.vehicle-add__footer {
|
|
69
|
+
display: flex;
|
|
70
|
+
justify-content: flex-end;
|
|
71
|
+
gap: 8px;
|
|
72
|
+
padding: 0 20px 20px;
|
|
73
|
+
}
|
|
74
|
+
</style>
|