@7365admin1/layer-common 3.2.2-staging.124 → 3.2.2-staging.126

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.
@@ -1,117 +1,109 @@
1
1
  <template>
2
2
  <v-dialog :model-value="modelValue" width="480" persistent>
3
- <v-card width="100%">
4
- <v-toolbar density="compact" color="black">
5
- <v-toolbar-title class="text-subtitle-1 font-weight-medium">
3
+ <v-card width="100%" class="screen-modal card-details">
4
+ <div class="card-details__head">
5
+ <v-card-title>
6
6
  Card Details — {{ card?.cardNo ?? "N/A" }}
7
- </v-toolbar-title>
8
- <v-btn icon @click="emit('update:modelValue', false)">
9
- <v-icon>mdi-close</v-icon>
10
- </v-btn>
11
- </v-toolbar>
7
+ </v-card-title>
8
+ <AppButton variant="icon" icon="mdi-close" aria-label="Close"
9
+ @click="emit('update:modelValue', false)" />
10
+ </div>
12
11
 
13
- <v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-4">
12
+ <div class="screen-modal__body card-details__body">
14
13
  <div v-if="pending" class="d-flex justify-center align-center py-8">
15
- <v-progress-circular indeterminate color="black" />
14
+ <v-progress-circular indeterminate color="accent" />
16
15
  </div>
17
16
 
18
- <div v-else-if="!details" class="text-center text-grey py-8">
17
+ <div v-else-if="!details" class="card-details__empty">
19
18
  No details available.
20
19
  </div>
21
20
 
22
21
  <div v-else class="d-flex flex-column ga-3">
23
22
  <div>
24
- <div class="text-caption text-grey">Card No</div>
25
- <div class="text-body-2 font-weight-medium">{{ details.cardNo ?? "N/A" }}</div>
23
+ <div class="card-details__label">Card No</div>
24
+ <div class="card-details__value">{{ details.cardNo ?? "N/A" }}</div>
26
25
  </div>
27
26
 
28
27
  <div>
29
- <div class="text-caption text-grey">Type</div>
30
- <div class="text-body-2 font-weight-medium">{{ details.type ?? "N/A" }}</div>
28
+ <div class="card-details__label">Type</div>
29
+ <div class="card-details__value">{{ details.type ?? "N/A" }}</div>
31
30
  </div>
32
31
 
33
32
  <div v-if="details.status">
34
- <div class="text-caption text-grey">Status</div>
35
- <v-chip
36
- :color="statusColor(details.status)"
37
- variant="flat"
38
- size="x-small"
39
- class="text-capitalize font-weight-medium mt-1"
40
- >
41
- {{ details.status ?? "N/A" }}
42
- </v-chip>
33
+ <div class="card-details__label">Status</div>
34
+ <StatusChip class="mt-1 text-capitalize" :status="details.status" />
43
35
  </div>
44
36
 
45
37
  <div>
46
- <div class="text-caption text-grey">Activated</div>
47
- <div class="text-body-2 font-weight-medium">{{ details.isActivated ? "Yes" : "No" }}</div>
38
+ <div class="card-details__label">Activated</div>
39
+ <div class="card-details__value">{{ details.isActivated ? "Yes" : "No" }}</div>
48
40
  </div>
49
41
 
50
42
  <div v-if="details.site?.name">
51
- <div class="text-caption text-grey">Site</div>
52
- <div class="text-body-2 font-weight-medium">{{ details.site?.name ?? "N/A" }}</div>
43
+ <div class="card-details__label">Site</div>
44
+ <div class="card-details__value">{{ details.site?.name ?? "N/A" }}</div>
53
45
  </div>
54
46
 
55
47
  <div>
56
- <div class="text-caption text-grey">User</div>
57
- <div class="text-body-2 font-weight-medium">
48
+ <div class="card-details__label">User</div>
49
+ <div class="card-details__value">
58
50
  {{ details.userCred?.name ?? details.user?.name ?? "N/A" }}
59
51
  <template v-if="details.user?.email">({{ details.user.email }})</template>
60
52
  </div>
61
53
  </div>
62
54
 
63
55
  <div v-if="details.userType">
64
- <div class="text-caption text-grey">User Type</div>
65
- <div class="text-body-2 font-weight-medium">{{ details.userType }}</div>
56
+ <div class="card-details__label">User Type</div>
57
+ <div class="card-details__value">{{ details.userType }}</div>
66
58
  </div>
67
59
 
68
60
  <!-- Visitor / Resident Credentials -->
69
61
  <template v-if="details.userCred">
70
62
  <v-divider />
71
- <div class="text-caption font-weight-bold text-grey-darken-2 text-uppercase">
63
+ <div class="card-details__section">
72
64
  {{ details.userType ?? "Visitor Information" }}
73
65
  </div>
74
66
 
75
67
  <div v-if="details.userCred.email">
76
- <div class="text-caption text-grey">Email</div>
77
- <div class="text-body-2 font-weight-medium">{{ details.userCred.email }}</div>
68
+ <div class="card-details__label">Email</div>
69
+ <div class="card-details__value">{{ details.userCred.email }}</div>
78
70
  </div>
79
71
 
80
72
  <div v-if="details.userCred.nric">
81
- <div class="text-caption text-grey">NRIC</div>
82
- <div class="text-body-2 font-weight-medium">{{ details.userCred.nric }}</div>
73
+ <div class="card-details__label">NRIC</div>
74
+ <div class="card-details__value">{{ details.userCred.nric }}</div>
83
75
  </div>
84
76
 
85
77
  <div v-if="details.userCred.contact">
86
- <div class="text-caption text-grey">Contact</div>
87
- <div class="text-body-2 font-weight-medium">{{ details.userCred.contact }}</div>
78
+ <div class="card-details__label">Contact</div>
79
+ <div class="card-details__value">{{ details.userCred.contact }}</div>
88
80
  </div>
89
81
 
90
82
  <div v-if="details.userCred.company">
91
- <div class="text-caption text-grey">Company</div>
92
- <div class="text-body-2 font-weight-medium">{{ details.userCred.company }}</div>
83
+ <div class="card-details__label">Company</div>
84
+ <div class="card-details__value">{{ details.userCred.company }}</div>
93
85
  </div>
94
86
 
95
87
  <div v-if="details.userCred.plateNumber">
96
- <div class="text-caption text-grey">Plate Number</div>
97
- <div class="text-body-2 font-weight-medium">{{ details.userCred.plateNumber }}</div>
88
+ <div class="card-details__label">Plate Number</div>
89
+ <div class="card-details__value">{{ details.userCred.plateNumber }}</div>
98
90
  </div>
99
91
  <v-divider />
100
92
  </template>
101
93
 
102
94
  <div>
103
- <div class="text-caption text-grey">Created At</div>
104
- <div class="text-body-2 font-weight-medium">{{ formatDate(details.createdAt) }}</div>
95
+ <div class="card-details__label">Created At</div>
96
+ <div class="card-details__value">{{ formatDate(details.createdAt) }}</div>
105
97
  </div>
106
98
 
107
99
  <div>
108
- <div class="text-caption text-grey">Updated At</div>
109
- <div class="text-body-2 font-weight-medium">{{ formatDate(details.updatedAt) }}</div>
100
+ <div class="card-details__label">Updated At</div>
101
+ <div class="card-details__value">{{ formatDate(details.updatedAt) }}</div>
110
102
  </div>
111
103
 
112
104
  <div v-if="details.remarks">
113
- <div class="text-caption text-grey">Remarks</div>
114
- <div class="text-body-2 font-weight-medium">{{ details.remarks }}</div>
105
+ <div class="card-details__label">Remarks</div>
106
+ <div class="card-details__value">{{ details.remarks }}</div>
115
107
  </div>
116
108
 
117
109
  <!-- History -->
@@ -121,7 +113,7 @@
121
113
  class="d-flex align-center justify-space-between cursor-pointer"
122
114
  @click="historyOpen = !historyOpen"
123
115
  >
124
- <div class="text-caption font-weight-bold text-grey-darken-2 text-uppercase">
116
+ <div class="card-details__section">
125
117
  History ({{ details.history.length }})
126
118
  </div>
127
119
  <v-icon size="16" :icon="historyOpen ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
@@ -137,37 +129,36 @@
137
129
  <div class="d-flex flex-column align-center" style="width: 20px; flex-shrink: 0;">
138
130
  <div
139
131
  class="timeline-dot rounded-circle mt-1"
140
- :class="(entry.type === 'assigned' || entry.visitorId) ? 'bg-blue' : 'bg-orange'"
132
+ :class="isAssignment(entry) ? 'timeline-dot--assigned' : 'timeline-dot--returned'"
141
133
  />
142
134
  <div
143
135
  v-if="idx < sortedHistory.length - 1"
144
- class="timeline-line bg-grey-lighten-2 mt-1"
136
+ class="timeline-line mt-1"
145
137
  />
146
138
  </div>
147
139
 
148
140
  <!-- Entry content -->
149
141
  <div class="d-flex flex-column ga-1 pb-4 flex-grow-1">
150
142
  <div class="d-flex align-center justify-space-between">
151
- <v-chip
152
- size="x-small"
153
- variant="tonal"
154
- :color="(entry.type === 'assigned' || entry.visitorId) ? 'blue' : 'orange'"
155
- :prepend-icon="(entry.type === 'assigned' || entry.visitorId) ? 'mdi-account-arrow-right' : 'mdi-arrow-u-left-top'"
156
- >
157
- {{ (entry.type === 'assigned' || entry.visitorId) ? "Assigned" : "Returned" }}
158
- </v-chip>
159
- <span class="text-caption text-grey">{{ formatDate(entry.createdAt) }}</span>
143
+ <StatusChip :status="isAssignment(entry) ? 'Assigned' : 'Returned'">
144
+ <v-icon
145
+ size="13"
146
+ class="mr-1"
147
+ :icon="isAssignment(entry) ? 'mdi-account-arrow-right' : 'mdi-arrow-u-left-top'"
148
+ />{{ isAssignment(entry) ? "Assigned" : "Returned" }}
149
+ </StatusChip>
150
+ <span class="card-details__label">{{ formatDate(entry.createdAt) }}</span>
160
151
  </div>
161
152
 
162
153
  <!-- Assignment entry -->
163
154
  <template v-if="entry.type === 'assigned' || entry.visitorId">
164
- <div class="text-body-2 font-weight-medium">{{ entry.name ?? "N/A" }}</div>
165
- <div class="text-caption text-grey-darken-1">
155
+ <div class="card-details__value">{{ entry.name ?? "N/A" }}</div>
156
+ <div class="card-details__sub">
166
157
  <span v-if="entry.nric">NRIC: {{ entry.nric }}</span>
167
158
  <span v-if="entry.contact" class="ml-2">· {{ entry.contact }}</span>
168
159
  <span v-if="entry.email && !entry.nric && !entry.contact">{{ entry.email }}</span>
169
160
  </div>
170
- <div class="text-caption text-grey-darken-1">
161
+ <div class="card-details__sub">
171
162
  <span v-if="entry.company">{{ entry.company }}</span>
172
163
  <span v-if="entry.plateNumber" class="ml-2">· {{ entry.plateNumber }}</span>
173
164
  </div>
@@ -176,12 +167,10 @@
176
167
  <!-- Return/checkout entry -->
177
168
  <template v-else>
178
169
  <div class="d-flex align-center ga-2">
179
- <v-chip size="x-small" variant="flat" :color="statusColor(entry.status?.toLowerCase())">
180
- {{ entry.status ?? "N/A" }}
181
- </v-chip>
182
- <span v-if="entry.damage" class="text-caption text-error">Damaged</span>
170
+ <StatusChip :status="entry.status" :label="entry.status ?? 'N/A'" />
171
+ <span v-if="entry.damage" class="card-details__damage">Damaged</span>
183
172
  </div>
184
- <div v-if="entry.remarks" class="text-caption text-grey-darken-1">
173
+ <div v-if="entry.remarks" class="card-details__sub">
185
174
  Remarks: {{ entry.remarks }}
186
175
  </div>
187
176
  </template>
@@ -190,7 +179,7 @@
190
179
  </template>
191
180
  </template>
192
181
  </div>
193
- </v-card-text>
182
+ </div>
194
183
  </v-card>
195
184
  </v-dialog>
196
185
  </template>
@@ -245,15 +234,12 @@ watch(
245
234
  }
246
235
  );
247
236
 
248
- function statusColor(status: string) {
249
- const map: Record<string, string> = {
250
- active: "success",
251
- assigned: "primary",
252
- replaced: "orange",
253
- deleted: "error",
254
- available: "grey",
255
- };
256
- return map[status] ?? "grey";
237
+ /**
238
+ * A history entry is an ASSIGNMENT if it says so or carries a visitor; the
239
+ * alternative is a return. Was written out three times inline.
240
+ */
241
+ function isAssignment(entry: Record<string, any>) {
242
+ return entry.type === "assigned" || Boolean(entry.visitorId);
257
243
  }
258
244
 
259
245
  function formatDate(date: string) {
@@ -270,14 +256,74 @@ function formatDate(date: string) {
270
256
  </script>
271
257
 
272
258
  <style scoped>
259
+ .card-details__head {
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: space-between;
263
+ padding-right: 12px;
264
+ }
265
+
266
+ .card-details__body {
267
+ padding: 0 20px 20px;
268
+ max-height: 70vh;
269
+ }
270
+
271
+ .card-details__label {
272
+ font-size: var(--fs-breadcrumb);
273
+ color: var(--text2);
274
+ }
275
+
276
+ .card-details__value {
277
+ font-size: var(--fs-cell);
278
+ font-weight: var(--fw-cell);
279
+ color: var(--text);
280
+ }
281
+
282
+ .card-details__section {
283
+ font-size: var(--fs-section-label);
284
+ font-weight: var(--fw-section-label);
285
+ letter-spacing: var(--ls-section-label);
286
+ text-transform: uppercase;
287
+ color: var(--text2);
288
+ }
289
+
290
+ .card-details__sub {
291
+ font-size: var(--fs-breadcrumb);
292
+ color: var(--text2);
293
+ }
294
+
295
+ .card-details__empty {
296
+ padding: 32px 0;
297
+ text-align: center;
298
+ font-size: var(--fs-cell);
299
+ color: var(--muted);
300
+ }
301
+
302
+ .card-details__damage {
303
+ font-size: var(--fs-breadcrumb);
304
+ color: var(--err);
305
+ }
306
+
273
307
  .timeline-dot {
274
308
  width: 10px;
275
309
  height: 10px;
276
310
  flex-shrink: 0;
277
311
  }
312
+
313
+ /* Were `bg-blue` / `bg-orange` / `bg-grey-lighten-2` - three fixed Vuetify hues
314
+ that do not follow the theme. Same meanings, as tokens. */
315
+ .timeline-dot--assigned {
316
+ background: var(--info);
317
+ }
318
+
319
+ .timeline-dot--returned {
320
+ background: var(--warn);
321
+ }
322
+
278
323
  .timeline-line {
279
324
  width: 2px;
280
325
  flex-grow: 1;
281
326
  min-height: 16px;
327
+ background: var(--border);
282
328
  }
283
329
  </style>
@@ -1,13 +1,11 @@
1
1
  <template>
2
2
  <v-dialog v-model="showDialog" max-width="500" persistent>
3
- <v-card>
4
- <v-toolbar>
5
- <v-row no-gutters class="fill-height px-6" align="center">
6
- <span class="font-weight-bold text-h6">Search Location</span>
7
- </v-row>
8
- </v-toolbar>
9
-
10
- <v-card-text class="pa-6">
3
+ <v-card class="screen-modal map-search">
4
+ <div class="map-search__head">
5
+ <v-card-title>Search Location</v-card-title>
6
+ </div>
7
+
8
+ <div class="screen-modal__body map-search__body filter-field">
11
9
  <v-form ref="formRef" @submit.prevent="handleSearch">
12
10
  <v-row dense>
13
11
  <v-col cols="12" md="6">
@@ -64,47 +62,23 @@
64
62
  </v-col>
65
63
  </v-row>
66
64
  </v-form>
67
- </v-card-text>
68
-
69
- <v-toolbar class="pa-0" density="compact">
70
- <v-row no-gutters>
71
- <v-col cols="6" class="pa-0">
72
- <v-btn
73
- block
74
- variant="text"
75
- class="text-none"
76
- size="large"
77
- height="56"
78
- :disabled="searching"
79
- @click="handleClose"
80
- >
81
- Cancel
82
- </v-btn>
83
- </v-col>
84
- <v-col cols="6" class="pa-0">
85
- <v-btn
86
- block
87
- variant="flat"
88
- color="black"
89
- class="text-none font-weight-bold rounded-0"
90
- height="56"
91
- size="large"
92
- :loading="searching"
93
- :disabled="!formData.postalCode"
94
- @click="handleSearch"
95
- >
96
- <v-icon class="mr-2">mdi-magnify</v-icon>
97
- Search
98
- </v-btn>
99
- </v-col>
100
- </v-row>
101
- </v-toolbar>
65
+ </div>
66
+
67
+ <div class="map-search__footer">
68
+ <AppButton variant="ghost" :disabled="searching" @click="handleClose">Cancel</AppButton>
69
+ <AppButton
70
+ icon="mdi-magnify"
71
+ :disabled="searching || !formData.postalCode"
72
+ @click="handleSearch"
73
+ >
74
+ Search
75
+ </AppButton>
76
+ </div>
102
77
  </v-card>
103
78
  </v-dialog>
104
79
 
105
80
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
106
81
  </template>
107
- That is
108
82
 
109
83
  <script setup lang="ts">
110
84
  const showDialog = defineModel({ type: Boolean, default: false });
@@ -391,3 +365,27 @@ watch(showDialog, (newVal) => {
391
365
  }
392
366
  });
393
367
  </script>
368
+
369
+ <style scoped>
370
+ .map-search__head {
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: space-between;
374
+ padding-right: 12px;
375
+ }
376
+
377
+ .map-search__body {
378
+ padding: 0 20px;
379
+ }
380
+
381
+ /* The design's dialog footer - ghost secondary, then one primary, right
382
+ aligned. Deliberately not the shared `.screen-modal__footer`: that class is
383
+ on the batch-D branch (#1150), not on this base, and adding a second copy of
384
+ it to `screens.css` here would collide with that PR. */
385
+ .map-search__footer {
386
+ display: flex;
387
+ justify-content: flex-end;
388
+ gap: 8px;
389
+ padding: 16px 20px 20px;
390
+ }
391
+ </style>
@@ -1,13 +1,11 @@
1
1
  <template>
2
2
  <v-dialog v-model="showDialog" max-width="600" persistent>
3
- <v-card>
4
- <v-toolbar>
5
- <v-row no-gutters class="fill-height px-6" align="center">
6
- <span class="font-weight-bold text-h6">Attendance Settings</span>
7
- </v-row>
8
- </v-toolbar>
9
-
10
- <v-card-text class="pa-6">
3
+ <v-card class="screen-modal attendance-settings">
4
+ <div class="attendance-settings__head">
5
+ <v-card-title>Attendance Settings</v-card-title>
6
+ </div>
7
+
8
+ <div class="screen-modal__body attendance-settings__body filter-field">
11
9
  <v-alert
12
10
  v-if="error"
13
11
  type="error"
@@ -26,12 +24,13 @@
26
24
  <v-checkbox
27
25
  v-model="isGeofencingEnabled"
28
26
  label="Enable Geofencing"
27
+ color="accent"
29
28
  density="comfortable"
30
29
  hide-details
31
30
  />
32
31
  <span
33
32
  v-if="isGeofencingEnabled"
34
- class="text-body-2 text-medium-emphasis"
33
+ class="attendance-settings__hint"
35
34
  >
36
35
  Enabling this setting will require users to take attendance
37
36
  within a certain radius via QR code or within the site location.
@@ -43,44 +42,32 @@
43
42
  <v-checkbox
44
43
  v-model="isQrEnabled"
45
44
  label="Enable QR Code"
45
+ color="accent"
46
46
  density="comfortable"
47
47
  hide-details
48
48
  />
49
49
  </v-col>
50
50
 
51
51
  <v-col v-if="isQrEnabled" cols="12">
52
- <v-card
53
- variant="flat"
54
- class="pa-4 d-flex flex-column align-center"
55
- >
52
+ <div class="attendance-settings__qr">
56
53
  <QrcodeVue
57
54
  :value="site"
58
55
  :level="'M'"
59
56
  :render-as="'svg'"
60
57
  :size="300"
61
58
  />
62
- <v-btn
63
- variant="text"
64
- color="primary"
65
- class="mt-2"
66
- @click="downloadQrCode"
67
- >
68
- <v-icon class="mr-2">mdi-download</v-icon>
59
+ <AppButton variant="ghost" icon="mdi-download" class="mt-2" @click="downloadQrCode">
69
60
  Download QR Code
70
- </v-btn>
71
- </v-card>
61
+ </AppButton>
62
+ </div>
72
63
  </v-col>
73
64
 
74
65
  <v-col cols="12" class="mt-4">
75
66
  <InputLabel title="Site Location" />
76
- <v-btn
77
- variant="outlined"
78
- class="mt-2"
79
- @click="isMapSearchVisible = true"
80
- >
81
- <v-icon class="mr-2">mdi-map-search</v-icon>
67
+ <AppButton variant="ghost" icon="mdi-map-search" class="mt-2"
68
+ @click="isMapSearchVisible = true">
82
69
  Search Location
83
- </v-btn>
70
+ </AppButton>
84
71
  </v-col>
85
72
 
86
73
  <v-col v-if="geolocation" cols="12">
@@ -125,40 +112,12 @@
125
112
  </template>
126
113
  </v-row>
127
114
  </v-form>
128
- </v-card-text>
129
-
130
- <v-toolbar class="pa-0" density="compact">
131
- <v-row no-gutters>
132
- <v-col cols="6" class="pa-0">
133
- <v-btn
134
- block
135
- variant="text"
136
- class="text-none"
137
- size="large"
138
- @click="close"
139
- height="56"
140
- :disabled="submitting"
141
- >
142
- Cancel
143
- </v-btn>
144
- </v-col>
145
- <v-col cols="6" class="pa-0">
146
- <v-btn
147
- block
148
- variant="flat"
149
- color="black"
150
- class="text-none font-weight-bold rounded-0"
151
- height="56"
152
- size="large"
153
- :loading="submitting"
154
- @click="onSubmit"
155
- >
156
- <v-icon class="mr-2">mdi-check</v-icon>
157
- Update
158
- </v-btn>
159
- </v-col>
160
- </v-row>
161
- </v-toolbar>
115
+ </div>
116
+
117
+ <div class="attendance-settings__footer">
118
+ <AppButton variant="ghost" :disabled="submitting" @click="close">Cancel</AppButton>
119
+ <AppButton icon="mdi-check" :disabled="submitting" @click="onSubmit">Update</AppButton>
120
+ </div>
162
121
  </v-card>
163
122
  </v-dialog>
164
123
 
@@ -397,3 +356,43 @@ watch(showDialog, (newVal) => {
397
356
  }
398
357
  });
399
358
  </script>
359
+
360
+ <style scoped>
361
+ .attendance-settings__head {
362
+ display: flex;
363
+ align-items: center;
364
+ justify-content: space-between;
365
+ padding-right: 12px;
366
+ }
367
+
368
+ .attendance-settings__body {
369
+ padding: 0 20px;
370
+ }
371
+
372
+ .attendance-settings__hint {
373
+ font-size: var(--fs-cell);
374
+ color: var(--text2);
375
+ }
376
+
377
+ /* Was a `variant="flat"` v-card, i.e. a second card inside the modal card. It
378
+ is a frame around the QR, so it takes the design's inner surface. */
379
+ .attendance-settings__qr {
380
+ display: flex;
381
+ flex-direction: column;
382
+ align-items: center;
383
+ padding: 16px;
384
+ border: 1px solid var(--border);
385
+ border-radius: var(--r-inner);
386
+ background: var(--thead);
387
+ }
388
+
389
+ /* The design's dialog footer. Deliberately not the shared
390
+ `.screen-modal__footer` - that class is on the batch-D branch (#1150), not
391
+ on this base, and a second copy in `screens.css` would collide with it. */
392
+ .attendance-settings__footer {
393
+ display: flex;
394
+ justify-content: flex-end;
395
+ gap: 8px;
396
+ padding: 16px 20px 20px;
397
+ }
398
+ </style>