@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,20 +1,17 @@
1
1
  <template>
2
- <v-card width="100%" :loading="processing">
3
- <v-toolbar>
4
- <v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
5
- <span class="font-weight-bold text-h5 text-capitalize">
6
- {{ formMode }} {{ formatVisitorType(type) }}
7
- </span>
8
- <ButtonClose @click="emit('close:all')" />
9
- </v-row>
10
- </v-toolbar>
11
- <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-3">
12
- <div class="w-100 d-flex justify-space-between ga-2">
13
- <span class="text-subtitle-1 w-100 font-weight-bold mb-3">{{
14
- formTitle
15
- }}</span>
16
- <span v-if="withStep2 || withStep3" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
17
- <span class="text-primary-button">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span>
2
+ <v-card width="100%" class="screen-modal visitor-form" :loading="processing">
3
+ <div class="visitor-form__head">
4
+ <v-card-title class="text-capitalize">
5
+ {{ formMode }} {{ formatVisitorType(type) }}
6
+ </v-card-title>
7
+ <AppButton variant="icon" icon="mdi-close" aria-label="Close" @click="emit('close:all')" />
8
+ </div>
9
+
10
+ <div class="screen-modal__body visitor-form__body filter-field">
11
+ <div class="visitor-form__section-row">
12
+ <span class="visitor-form__section">{{ formTitle }}</span>
13
+ <span v-if="withStep2 || withStep3" class="visitor-form__step">Step
14
+ <span class="visitor-form__step-now">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span>
18
15
  </div>
19
16
  <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
20
17
  <v-row no-gutters class="pt-4">
@@ -27,7 +24,8 @@
27
24
  <v-row>
28
25
  <v-col cols="12">
29
26
  <InputLabel class="text-capitalize" title="NRIC" :required="requireNRIC" />
30
- <v-menu v-model="nricSearchMenu" location="bottom" offset-y :close-on-content-click="false">
27
+ <v-menu v-model="nricSearchMenu" location="bottom" offset-y :close-on-content-click="false"
28
+ content-class="visitor-form__menu">
31
29
  <template #activator="{ props }">
32
30
  <InputNRICNumber v-bind="props" v-model="visitor.nric" density="comfortable"
33
31
  @focus="isNricFieldFocused = true" @blur="isNricFieldFocused = false"
@@ -43,7 +41,8 @@
43
41
  </v-list-item>
44
42
  </div>
45
43
  <div class="search-results-close pa-1">
46
- <v-btn block variant="flat" color="red" @click.stop="closeAutofillSearchResults('nric')">
44
+ <v-btn block variant="outlined" class="screen-btn-ghost"
45
+ @click.stop="closeAutofillSearchResults('nric')">
47
46
  Close
48
47
  </v-btn>
49
48
  </div>
@@ -57,7 +56,8 @@
57
56
  <v-row>
58
57
  <v-col cols="12">
59
58
  <InputLabel class="text-capitalize" title="Phone Number" required />
60
- <v-menu v-model="phoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
59
+ <v-menu v-model="phoneSearchMenu" location="bottom" offset-y :close-on-content-click="false"
60
+ content-class="visitor-form__menu">
61
61
  <template #activator="{ props }">
62
62
  <InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]"
63
63
  @focus="isPhoneFieldFocused = true" @blur="isPhoneFieldFocused = false" density="comfortable"
@@ -72,7 +72,8 @@
72
72
  </v-list-item>
73
73
  </div>
74
74
  <div class="search-results-close pa-1">
75
- <v-btn block variant="flat" color="red" @click.stop="closeAutofillSearchResults('contact')">
75
+ <v-btn block variant="outlined" class="screen-btn-ghost"
76
+ @click.stop="closeAutofillSearchResults('contact')">
76
77
  Close
77
78
  </v-btn>
78
79
  </div>
@@ -88,7 +89,8 @@
88
89
  <v-col cols="12">
89
90
  <InputLabel class="text-capitalize" title="Vehicle Number" :required="isVehicleRequired" />
90
91
  <!-- <v-text-field v-model.trim.uppercase="visitor.plateNumber" density="comfortable" /> -->
91
- <v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
92
+ <v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false"
93
+ content-class="visitor-form__menu">
92
94
  <template #activator="{ props }">
93
95
  <InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
94
96
  @update:model-value="handleUpdateVehicleNumber" @focus="handleFocusPlateNumber"
@@ -103,7 +105,7 @@
103
105
  </v-list-item>
104
106
  </div>
105
107
  <div class="search-results-close pa-1">
106
- <v-btn block variant="flat" color="red"
108
+ <v-btn block variant="outlined" class="screen-btn-ghost"
107
109
  @click.stop="closeAutofillSearchResults('vehicleNumber')">
108
110
  Close
109
111
  </v-btn>
@@ -150,7 +152,15 @@
150
152
  <v-col v-if="shouldShowField('deliveryType')" cols="12">
151
153
  <v-row>
152
154
  <v-col cols="12">
153
- <v-radio-group v-model="visitor.deliveryType" inline color="primary" :rules="[requiredRule]">
155
+ <!-- The handoff never draws the delivery type, so this takes the
156
+ form's OWN established pattern rather than inventing one: an
157
+ `InputLabel` like every other field, and `accent` - the
158
+ colour the converted NRIC picker's radios already use - in
159
+ place of `primary`, which is the darker link tone. Without
160
+ the label this was the only required field in the form with
161
+ nothing naming it: just a bare Food/Parcel pair. -->
162
+ <InputLabel class="text-capitalize" title="Delivery Type" required />
163
+ <v-radio-group v-model="visitor.deliveryType" inline color="accent" :rules="[requiredRule]">
154
164
  <v-radio label="Food" value="Food"></v-radio>
155
165
  <v-radio label="Parcel" value="Parcel"></v-radio>
156
166
  </v-radio-group>
@@ -286,34 +296,23 @@
286
296
  </v-col>
287
297
  </v-row>
288
298
  </v-form>
289
- </v-card-text>
290
-
291
- <v-row no-gutters class="w-100" v-if="errorMessage">
292
- <p class="text-error w-100 text-center text-subtitle-2">
293
- {{ errorMessage }}
294
- </p>
295
- </v-row>
296
- <v-toolbar density="compact">
297
- <v-row no-gutters>
298
- <v-col cols="6">
299
- <v-btn v-if="
300
- formMode === 'add' &&
301
- (withStep2 || withStep3) &&
302
- contractorStep > 1
303
- " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
304
- <v-btn v-else-if="formMode === 'add' || formMode === 'register'" tile block variant="text" class="text-none"
305
- size="48" @click="backToSelection" text="Back to Selection" />
306
- <v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
307
- </v-col>
308
- <v-col cols="6">
309
- <v-btn v-if="showNextButton" tile block variant="flat" color="primary-button" class="text-none" size="48"
310
- :disabled="processing" @click="handleNextPage" :text="'Next'" />
311
- <v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
312
- :disabled="!validForm || processing || membersFieldIncomplete" @click="submit"
313
- :text="formMode == 'add' ? 'Submit' : 'Update'" />
314
- </v-col>
315
- </v-row>
316
- </v-toolbar>
299
+ </div>
300
+
301
+ <p v-if="errorMessage" class="visitor-form__error">
302
+ {{ errorMessage }}
303
+ </p>
304
+
305
+ <div class="visitor-form__footer">
306
+ <AppButton v-if="formMode === 'add' && (withStep2 || withStep3) && contractorStep > 1" variant="ghost"
307
+ @click="handleGoToPreviousPage">Back</AppButton>
308
+ <AppButton v-else-if="formMode === 'add' || formMode === 'register'" variant="ghost" @click="backToSelection">
309
+ Back to Selection</AppButton>
310
+ <AppButton v-else variant="ghost" @click="emit('close:all')">Close</AppButton>
311
+
312
+ <AppButton v-if="showNextButton" :disabled="processing" @click="handleNextPage">Next</AppButton>
313
+ <AppButton v-else :disabled="!validForm || processing || membersFieldIncomplete" @click="submit">
314
+ {{ formMode == 'add' ? 'Submit' : 'Update' }}</AppButton>
315
+ </div>
317
316
 
318
317
  <v-dialog v-model="dialog.vehicleNumberUsersList" v-if="vehicleNumberUserItems.length > 0" persistent
319
318
  max-width="600">
@@ -323,72 +322,73 @@
323
322
  </v-dialog>
324
323
 
325
324
  <v-dialog v-model="dialog.nricAutofillOptions" max-width="430">
326
- <v-card v-if="selectedNricSearchResult" class="nric-options-card">
327
- <v-card-actions class="justify-end pa-1">
328
- <v-btn icon="mdi-close" variant="text" size="small" @click="closeNricAutofillOptions" />
329
- </v-card-actions>
330
-
331
- <v-card-text class="pa-4 pt-1">
332
- <h2 class="nric-options-title font-weight-bold mb-4">Select</h2>
325
+ <v-card v-if="selectedNricSearchResult" class="screen-modal nric-options-card">
326
+ <div class="visitor-form__head">
327
+ <v-card-title>Select</v-card-title>
328
+ <AppButton variant="icon" icon="mdi-close" aria-label="Close" @click="closeNricAutofillOptions" />
329
+ </div>
333
330
 
331
+ <div class="screen-modal__body nric-options-body">
334
332
  <div class="nric-selected-person d-flex align-center justify-space-between ga-3 mb-5">
335
333
  <span>{{ selectedNricSearchResult.name || "Unknown" }}</span>
336
334
  <span v-if="!visitor.nric || currentAutofillSource === 'nric'" class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
337
335
  </div>
338
336
 
339
337
  <template v-if="(!visitor.contact || currentAutofillSource === 'contact') && selectedNricContactOptions.length >= 1">
340
- <h3 class="nric-options-section-title font-weight-bold mb-2">Contact</h3>
341
- <v-radio-group v-model="selectedNricContact" hide-details color="success" class="nric-option-group">
338
+ <h3 class="nric-options-section-title mb-2">Contact</h3>
339
+ <v-radio-group v-model="selectedNricContact" hide-details color="accent" class="nric-option-group">
342
340
  <v-radio v-for="contact in selectedNricContactOptions" :key="contact" :label="contact" :value="contact"
343
341
  class="nric-option-radio" />
344
342
  </v-radio-group>
345
343
  </template>
346
344
 
347
345
  <template v-if="(!visitor.plateNumber || currentAutofillSource === 'vehicleNumber') && selectedNricPlateOptions.length >= 1">
348
- <h3 class="nric-options-section-title font-weight-bold mb-2 mt-5">Vehicle</h3>
349
- <v-radio-group v-model="selectedNricPlate" hide-details color="success" class="nric-option-group">
346
+ <h3 class="nric-options-section-title mb-2 mt-5">Vehicle</h3>
347
+ <v-radio-group v-model="selectedNricPlate" hide-details color="accent" class="nric-option-group">
350
348
  <v-radio v-for="plate in selectedNricPlateOptions" :key="plate" :label="plate" :value="plate"
351
349
  class="nric-option-radio" />
352
350
  </v-radio-group>
353
351
  </template>
354
- </v-card-text>
352
+ </div>
355
353
 
356
- <v-card-actions class="justify-end px-4 pb-4">
357
- <v-btn variant="text" class="text-none" size="small" @click="closeNricAutofillOptions">Cancel</v-btn>
358
- <v-btn color="primary-button" variant="flat" class="text-none px-5" size="small"
359
- @click="confirmNricAutofillOptions">
360
- Select
361
- </v-btn>
362
- </v-card-actions>
354
+ <div class="visitor-form__footer">
355
+ <AppButton variant="ghost" @click="closeNricAutofillOptions">Cancel</AppButton>
356
+ <AppButton @click="confirmNricAutofillOptions">Select</AppButton>
357
+ </div>
363
358
  </v-card>
364
359
  </v-dialog>
365
360
 
366
361
  <v-dialog v-model="dialog.showNonCheckedOutDialog" max-width="1200">
367
- <v-card :loading="loading.checkingOut || loading.searchingNonCheckedOut">
368
- <v-card-actions class="justify-end bg-grey-lighten-3 pa-4">
369
- <v-btn icon="mdi-close" variant="text" size="large" @click="dialog.showNonCheckedOutDialog = false" />
370
- </v-card-actions>
371
-
372
- <v-card-text class="pa-6">
373
- <p class="text-subtitle-1 font-weight-medium mb-8">
362
+ <v-card class="screen-modal"
363
+ :loading="loading.checkingOut || loading.searchingNonCheckedOut">
364
+ <!-- No heading: the product gives this dialog none, and inventing copy
365
+ is not this batch's call. The design's modal frame does draw a
366
+ title, so that gap is raised on the PR as a copy decision. -->
367
+ <div class="visitor-form__head visitor-form__head--bare">
368
+ <AppButton variant="icon" icon="mdi-close" aria-label="Close"
369
+ @click="dialog.showNonCheckedOutDialog = false" />
370
+ </div>
371
+
372
+ <div class="screen-modal__body checkout-dialog__body">
373
+ <p class="checkout-dialog__lead">
374
374
  The following list needs to check-out before proceeding to add related to the NRIC/Phone Number below:
375
375
  </p>
376
376
 
377
- <div class="text-subtitle-1 mb-8">
377
+ <div class="checkout-dialog__subject">
378
378
  <div v-if="!skipNonCheckedOutNric">NRIC: {{ visitor.nric || "N/A" }}</div>
379
379
  <div>Phone Number: {{ visitor.contact || "N/A" }}</div>
380
380
  </div>
381
381
 
382
- <v-data-table class="border rounded" :headers="nonCheckedOutVisitorHeaders"
382
+ <v-data-table class="table-card" :headers="nonCheckedOutVisitorHeaders"
383
383
  :items="matchingNonCheckedOutVisitors" item-value="_id" :items-per-page="5" hide-default-footer>
384
384
  <template #item.name="{ item }">
385
385
  <div class="d-flex align-center ga-3">
386
- <v-avatar color="red" size="32">
387
- <span class="text-white text-subtitle-2">{{ getVisitorInitial(item.name) }}</span>
386
+ <v-avatar class="checkout-dialog__avatar" size="32">
387
+ <span>{{ getVisitorInitial(item.name) }}</span>
388
388
  </v-avatar>
389
389
  <div>
390
- <div class="font-weight-medium">{{ item.name || "N/A" }}</div>
391
- <div class="text-caption">
390
+ <div class="checkout-dialog__name">{{ item.name || "N/A" }}</div>
391
+ <div class="checkout-dialog__meta">
392
392
  <v-icon icon="mdi-card-account-details-outline" size="16" class="mr-1" />
393
393
  {{ maskNRIC(item.nric) || "N/A" }}
394
394
  </div>
@@ -425,11 +425,11 @@
425
425
 
426
426
  <template #item.checkInOut="{ item }">
427
427
  <div class="py-2">
428
- <div class="mb-2">
429
- <v-icon icon="mdi-clock-check-outline" color="green" size="20" class="mr-1" />
428
+ <div class="checkout-dialog__in mb-2">
429
+ <v-icon icon="mdi-clock-check-outline" size="20" class="mr-1" />
430
430
  {{ formatNonCheckedOutDate(item.checkIn) }}
431
431
  </div>
432
- <v-btn size="small" class="text-none" color="red" variant="flat"
432
+ <v-btn class="screen-btn-primary" variant="flat"
433
433
  :loading="loading.checkingOut && selectedCheckoutVisitorId === item._id" :disabled="!item._id"
434
434
  @click.stop="item._id && handleCheckout(item._id)">
435
435
  Check Out
@@ -437,35 +437,36 @@
437
437
  </div>
438
438
  </template>
439
439
  </v-data-table>
440
+ </div>
440
441
 
441
- <div class="d-flex justify-end mt-8">
442
- <v-btn v-if="!skipNonCheckedOutNric" color="red" variant="flat" size="large" class="text-none px-10"
443
- :loading="loading.searchingNonCheckedOut" @click="handleSkipNricNonCheckedOut">
444
- Skip NRIC: {{ visitor.nric || "N/A" }}
445
- </v-btn>
446
- </div>
447
- </v-card-text>
442
+ <div class="visitor-form__footer" v-if="!skipNonCheckedOutNric">
443
+ <v-btn class="screen-btn-primary" variant="flat" :loading="loading.searchingNonCheckedOut"
444
+ @click="handleSkipNricNonCheckedOut">
445
+ Skip NRIC: {{ visitor.nric || "N/A" }}
446
+ </v-btn>
447
+ </div>
448
448
  </v-card>
449
449
  </v-dialog>
450
450
 
451
451
  <v-dialog v-if="enableUnregistered" v-model="dialog.showUnregisteredPlateDialog" max-width="900" persistent>
452
- <v-card color="background" class="pa-4">
453
- <v-card-actions class="justify-end pa-0">
454
- <v-btn icon="mdi-close" variant="text" size="large" @click="closeUnregisteredPlateDialog" />
455
- </v-card-actions>
452
+ <v-card class="screen-modal">
453
+ <!-- No heading in the product; see the note on the check-out dialog. -->
454
+ <div class="visitor-form__head visitor-form__head--bare">
455
+ <AppButton variant="icon" icon="mdi-close" aria-label="Close" @click="closeUnregisteredPlateDialog" />
456
+ </div>
456
457
 
457
- <v-card-text class="text-center pt-2">
458
- <v-icon color="orange" size="96">mdi-alert-circle-outline</v-icon>
458
+ <div class="screen-modal__body unreg-dialog__body">
459
+ <v-icon class="unreg-dialog__alert" size="96">mdi-alert-circle-outline</v-icon>
459
460
 
460
- <div :class="`text-body-${$vuetify.display.xs ? 'mobile' : 'tablet'} mt-8`">
461
+ <p class="unreg-dialog__lead">
461
462
  Vehicle Number {{ unregisteredPlateSearchText }} is similar to below
462
463
  <br />
463
464
  Unregistered visitors last 24 hours. Please select to
464
465
  <br />
465
466
  Update
466
- </div>
467
+ </p>
467
468
 
468
- <v-table class="mt-8 border rounded">
469
+ <v-table class="table-card unreg-dialog__table">
469
470
  <thead>
470
471
  <tr>
471
472
  <th class="text-left">Plate Number</th>
@@ -475,45 +476,38 @@
475
476
  </thead>
476
477
  <tbody>
477
478
  <tr v-for="match in unregisteredPlateMatches" :key="match._id || match.plateNumber">
478
- <td class="text-left font-weight-bold text-uppercase">
479
+ <td class="text-left text-uppercase unreg-dialog__plate">
479
480
  {{ match.plateNumber || "N/A" }}
480
481
  </td>
481
482
  <td class="text-left">
482
483
  {{ formatUnregisteredCheckIn(match.checkIn) }}
483
484
  </td>
484
485
  <td class="text-center">
485
- <v-btn color="red" class="text-capitalize" density="compact" size="large"
486
- @click="selectUnregisteredPlateMatch(match)">
487
- Update
488
- </v-btn>
486
+ <AppButton @click="selectUnregisteredPlateMatch(match)">Update</AppButton>
489
487
  </td>
490
488
  </tr>
491
489
  </tbody>
492
490
  </v-table>
493
- </v-card-text>
491
+ </div>
494
492
 
495
- <v-card-actions>
496
- <v-btn block color="red" variant="flat" size="x-large" @click="closeUnregisteredPlateDialog">
497
- Cancel & Add New
498
- </v-btn>
499
- </v-card-actions>
493
+ <div class="visitor-form__footer">
494
+ <AppButton variant="ghost" @click="closeUnregisteredPlateDialog">Cancel &amp; Add New</AppButton>
495
+ </div>
500
496
  </v-card>
501
497
  </v-dialog>
502
498
 
503
499
  <v-dialog v-model="hidQrPreviewDialog" max-width="440" persistent>
504
- <v-card class="hid-qr-preview-card">
505
- <v-toolbar color="transparent" density="comfortable" class="px-2">
506
- <v-toolbar-title class="text-subtitle-1 font-weight-bold">
507
- Visitor QR Code
508
- </v-toolbar-title>
509
- <v-btn icon="mdi-close" variant="text" title="Close" @click="closeHidQrPreview" />
510
- </v-toolbar>
511
-
512
- <v-card-text class="px-8 pb-6 text-center">
513
- <div class="text-h6 font-weight-bold">
500
+ <v-card class="screen-modal hid-qr-preview-card">
501
+ <div class="visitor-form__head">
502
+ <v-card-title>Visitor QR Code</v-card-title>
503
+ <AppButton variant="icon" icon="mdi-close" aria-label="Close" @click="closeHidQrPreview" />
504
+ </div>
505
+
506
+ <div class="screen-modal__body hid-qr__body">
507
+ <div class="hid-qr__header">
514
508
  {{ hidQrTemplate.header || "Visitor Access Pass" }}
515
509
  </div>
516
- <div class="text-body-2 text-medium-emphasis mt-1">
510
+ <div class="hid-qr__subtext">
517
511
  {{ hidQrTemplate.subtext || "Scan at the HID reader for entry" }}
518
512
  </div>
519
513
 
@@ -525,38 +519,32 @@
525
519
  </button>
526
520
  </div>
527
521
 
528
- <v-btn
529
- variant="text"
530
- density="comfortable"
531
- class="text-none"
532
- :prepend-icon="hidQrVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
522
+ <AppButton
523
+ variant="ghost"
524
+ :icon="hidQrVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
533
525
  @click="hidQrVisible = !hidQrVisible"
534
526
  >
535
527
  {{ hidQrVisible ? "Hide QR code" : "Show QR code" }}
536
- </v-btn>
528
+ </AppButton>
537
529
 
538
- <div class="text-h6 font-weight-bold">{{ visitor.name }}</div>
539
- <div v-if="hidQrReader?.portalName" class="text-caption text-medium-emphasis mt-1">
530
+ <div class="hid-qr__name">{{ visitor.name }}</div>
531
+ <div v-if="hidQrReader?.portalName" class="hid-qr__meta">
540
532
  {{ hidQrReader.name || "HID Reader" }} · {{ hidQrReader.portalName }}
541
533
  </div>
542
- <div v-if="hidQrPreview.expiresAt" class="text-caption text-medium-emphasis mt-1">
534
+ <div v-if="hidQrPreview.expiresAt" class="hid-qr__meta">
543
535
  Valid until {{ formatHidQrExpiry(hidQrPreview.expiresAt) }}
544
536
  </div>
545
- <v-chip
537
+ <StatusChip
546
538
  v-if="hidQrPreview.expiresAt"
547
539
  class="mt-2"
548
- size="small"
549
- :color="hidQrExpired ? 'error' : 'success'"
550
- variant="tonal"
551
- >
552
- {{ hidQrExpired ? "Expired" : `Expires in ${formatHidQrCountdown(hidQrRemainingSeconds)}` }}
553
- </v-chip>
540
+ :tone="hidQrExpired ? 'err' : 'ok'"
541
+ :label="hidQrExpired ? 'Expired' : `Expires in ${formatHidQrCountdown(hidQrRemainingSeconds)}`"
542
+ />
554
543
 
555
544
  <v-btn
556
545
  v-if="hidQrExpired"
557
546
  block
558
- class="mt-4 text-none"
559
- color="primary-button"
547
+ class="mt-4 screen-btn-primary"
560
548
  variant="flat"
561
549
  prepend-icon="mdi-refresh"
562
550
  :loading="hidQrRegenerating"
@@ -568,42 +556,22 @@
568
556
  <v-alert v-if="hidQrPreviewError" type="error" variant="tonal" density="compact" class="mt-4 text-left">
569
557
  {{ hidQrPreviewError }}
570
558
  </v-alert>
571
- </v-card-text>
559
+ </div>
572
560
 
573
- <v-divider />
574
- <v-card-actions class="pa-0">
575
- <v-btn
576
- variant="text"
577
- height="52"
578
- class="flex-grow-1 text-none"
579
- prepend-icon="mdi-download"
580
- @click="downloadHidQrCode"
581
- >
561
+ <div class="visitor-form__footer">
562
+ <AppButton variant="ghost" icon="mdi-download" @click="downloadHidQrCode">
582
563
  Download
583
- </v-btn>
584
- <v-btn
564
+ </AppButton>
565
+ <AppButton
585
566
  v-if="hasHidQrPrinterConfig"
586
- variant="flat"
587
- color="primary-button"
588
- height="52"
589
- class="flex-grow-1 text-none"
590
- prepend-icon="mdi-printer"
591
- :loading="hidQrPrinting"
567
+ icon="mdi-printer"
568
+ :disabled="hidQrPrinting"
592
569
  @click="printHidQrCodePass"
593
570
  >
594
571
  Print
595
- </v-btn>
596
- <v-btn
597
- v-else
598
- variant="flat"
599
- color="primary-button"
600
- height="52"
601
- class="flex-grow-1 text-none"
602
- @click="closeHidQrPreview"
603
- >
604
- Close
605
- </v-btn>
606
- </v-card-actions>
572
+ </AppButton>
573
+ <AppButton v-else @click="closeHidQrPreview">Close</AppButton>
574
+ </div>
607
575
  </v-card>
608
576
  </v-dialog>
609
577
  </v-card>
@@ -891,28 +859,6 @@ type HidQrReaderSummary = {
891
859
  };
892
860
  const hidQrReader = ref<HidQrReaderSummary | null>(null);
893
861
 
894
- watch(
895
- [hidQrReaderId, () => prop.site],
896
- async ([readerId, site]) => {
897
- hidQrReader.value = null;
898
- if (!readerId || !site) return;
899
- try {
900
- const response = await getReaders({ site, page: 1, limit: 100 });
901
- const readers: unknown = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
902
- hidQrReader.value = Array.isArray(readers)
903
- ? readers.find((reader: unknown): reader is HidQrReaderSummary => (
904
- typeof reader === "object"
905
- && reader !== null
906
- && String((reader as Record<string, unknown>)._id || "") === readerId
907
- )) || null
908
- : null;
909
- } catch {
910
- hidQrReader.value = null;
911
- }
912
- },
913
- { immediate: true },
914
- );
915
-
916
862
  const hidQrCodePassAllowed = computed(() => {
917
863
  return (
918
864
  hidQrCodePassEnabled.value &&
@@ -1472,6 +1418,33 @@ const {
1472
1418
  getSiteById(prop.site)
1473
1419
  );
1474
1420
 
1421
+ // Has to sit BELOW `siteData`, not beside `hidQrReaderId` up at the HID block.
1422
+ // `hidQrReaderId` reads `hidQrCodePassConfig`, which reads `siteData`, and Vue
1423
+ // runs a watcher's source getter the moment `watch()` is called - so declared
1424
+ // any earlier this throws `Cannot access 'siteData' before initialization` and
1425
+ // the whole form fails to mount.
1426
+ watch(
1427
+ [hidQrReaderId, () => prop.site],
1428
+ async ([readerId, site]) => {
1429
+ hidQrReader.value = null;
1430
+ if (!readerId || !site) return;
1431
+ try {
1432
+ const response = await getReaders({ site, page: 1, limit: 100 });
1433
+ const readers: unknown = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
1434
+ hidQrReader.value = Array.isArray(readers)
1435
+ ? readers.find((reader: unknown): reader is HidQrReaderSummary => (
1436
+ typeof reader === "object"
1437
+ && reader !== null
1438
+ && String((reader as Record<string, unknown>)._id || "") === readerId
1439
+ )) || null
1440
+ : null;
1441
+ } catch {
1442
+ hidQrReader.value = null;
1443
+ }
1444
+ },
1445
+ { immediate: true },
1446
+ );
1447
+
1475
1448
  const {
1476
1449
  data: blocksListData,
1477
1450
  refresh: refreshBlockListData,
@@ -2379,11 +2352,81 @@ onMounted(() => {
2379
2352
  pointer-events: none;
2380
2353
  }
2381
2354
 
2355
+ .visitor-form__head {
2356
+ display: flex;
2357
+ align-items: center;
2358
+ justify-content: space-between;
2359
+ padding-right: 12px;
2360
+ }
2361
+
2362
+ /* The card must not scroll as a whole - that takes the Back/Submit footer off
2363
+ screen with it. The BODY scrolls; `.screen-modal` caps the card's height. */
2364
+ .visitor-form__body {
2365
+ padding: 4px 20px 0;
2366
+ max-height: calc(100vh - 160px);
2367
+ }
2368
+
2369
+ .visitor-form__section-row {
2370
+ display: flex;
2371
+ align-items: baseline;
2372
+ justify-content: space-between;
2373
+ gap: 8px;
2374
+ margin-bottom: 4px;
2375
+ }
2376
+
2377
+ .visitor-form__section {
2378
+ font-size: var(--fs-cell);
2379
+ font-weight: 800;
2380
+ color: var(--text);
2381
+ }
2382
+
2383
+ /* The handoff draws no step counter, so this takes the established type: the
2384
+ cell size, the accent for the step you are on, `--text2` for the rest. */
2385
+ .visitor-form__step {
2386
+ font-size: var(--fs-cell);
2387
+ font-weight: 700;
2388
+ color: var(--text2);
2389
+ white-space: nowrap;
2390
+ }
2391
+
2392
+ /* `--accent-text`, not `--accent-strong`: the strong one is a FILL for a white
2393
+ label. Used as ink on `--card` it reads 3.64:1 in dark. `--accent-text` is
2394
+ the accent's text form and `utils/theme.test.ts` asserts it >= 4.5:1 there. */
2395
+ .visitor-form__step-now {
2396
+ color: var(--accent-text);
2397
+ }
2398
+
2399
+ .visitor-form__error {
2400
+ margin: 0;
2401
+ padding: 0 20px;
2402
+ font-size: var(--fs-cell);
2403
+ font-weight: 500;
2404
+ color: var(--err);
2405
+ text-align: center;
2406
+ }
2407
+
2408
+ /* The design's dialog footer: ghost secondary, then one primary, right-aligned.
2409
+ Deliberately NOT the shared `.screen-modal__footer` - that class is on the
2410
+ batch-D branch (#1150) and not on this base, and adding a second copy of it
2411
+ to `screens.css` here would collide with that PR. */
2412
+ .visitor-form__footer {
2413
+ display: flex;
2414
+ justify-content: flex-end;
2415
+ gap: 8px;
2416
+ padding: 16px 20px 20px;
2417
+ }
2418
+
2419
+ /* `!important` only because Vuetify's own is: a `v-list-item` with no `rounded`
2420
+ prop emits `rounded-0`, and that utility is `border-radius: 0 !important`.
2421
+ The original `border-radius: 4px` here lost the same fight and never drew -
2422
+ measured 0px on the glass before this change. Same pattern `.screen-modal`
2423
+ already uses for `--r-modal`. */
2382
2424
  .nric-search-item {
2383
- border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2384
- border-radius: 4px;
2425
+ border: 1px solid var(--border);
2426
+ border-radius: var(--r-inner) !important;
2385
2427
  margin: 4px;
2386
2428
  min-height: 72px;
2429
+ font-size: var(--fs-cell);
2387
2430
  }
2388
2431
 
2389
2432
  .phone-search-results {
@@ -2391,6 +2434,9 @@ onMounted(() => {
2391
2434
  flex-direction: column;
2392
2435
  max-height: 400px;
2393
2436
  overflow: hidden;
2437
+ background: var(--card);
2438
+ border: 1px solid var(--border);
2439
+ box-shadow: var(--shadow-menu);
2394
2440
  }
2395
2441
 
2396
2442
  .search-results-scroll {
@@ -2403,55 +2449,164 @@ onMounted(() => {
2403
2449
  flex: 0 0 auto;
2404
2450
  }
2405
2451
 
2406
- .nric-options-card {
2407
- border-radius: 16px !important;
2452
+ .nric-options-body {
2453
+ padding: 0 20px;
2454
+ }
2455
+
2456
+ .visitor-form__head--bare {
2457
+ justify-content: flex-end;
2458
+ padding-top: 12px;
2459
+ }
2460
+
2461
+ .checkout-dialog__body {
2462
+ padding: 0 20px;
2463
+ }
2464
+
2465
+ .checkout-dialog__lead {
2466
+ margin-bottom: 20px;
2467
+ font-size: var(--fs-cell);
2468
+ font-weight: 700;
2469
+ color: var(--text);
2408
2470
  }
2409
2471
 
2410
- .nric-options-title {
2411
- color: #06253b;
2412
- font-size: 1.45rem;
2413
- line-height: 1.2;
2472
+ .checkout-dialog__subject {
2473
+ margin-bottom: 20px;
2474
+ font-size: var(--fs-cell);
2475
+ color: var(--text2);
2414
2476
  }
2415
2477
 
2478
+ /* The initial badge was a flat `red` avatar with a white letter - 3.99:1 in
2479
+ both themes, and red is the design's ERROR tone, which a person's initial is
2480
+ not. It takes the accent tag: `--accent-soft` fill, `--accent-text` ink. */
2481
+ .checkout-dialog__avatar {
2482
+ background: var(--accent-soft);
2483
+ color: var(--accent-text);
2484
+ font-size: var(--fs-chip);
2485
+ font-weight: var(--fw-chip);
2486
+ }
2487
+
2488
+ .checkout-dialog__name {
2489
+ font-size: var(--fs-cell);
2490
+ font-weight: var(--fw-cell);
2491
+ color: var(--text);
2492
+ }
2493
+
2494
+ .checkout-dialog__meta {
2495
+ font-size: var(--fs-breadcrumb);
2496
+ color: var(--text2);
2497
+ }
2498
+
2499
+ /* Checked-in IS a success state, so this one keeps its green - as the token,
2500
+ not Vuetify's `green`, which does not follow the theme. */
2501
+ .checkout-dialog__in {
2502
+ font-size: var(--fs-cell);
2503
+ color: var(--text);
2504
+ }
2505
+
2506
+ .checkout-dialog__in :deep(.v-icon) {
2507
+ color: var(--ok);
2508
+ }
2509
+
2510
+ .unreg-dialog__body {
2511
+ padding: 0 20px;
2512
+ text-align: center;
2513
+ }
2514
+
2515
+ /* A warning IS the design's warning tone; Vuetify's `orange` is not it and
2516
+ does not follow the theme. */
2517
+ .unreg-dialog__alert {
2518
+ color: var(--warn);
2519
+ }
2520
+
2521
+ .unreg-dialog__lead {
2522
+ margin-top: 20px;
2523
+ font-size: var(--fs-cell);
2524
+ color: var(--text);
2525
+ }
2526
+
2527
+ .unreg-dialog__table {
2528
+ margin-top: 20px;
2529
+ }
2530
+
2531
+ .unreg-dialog__plate {
2532
+ font-weight: 800;
2533
+ }
2534
+
2535
+ /* `#06253b` was a literal navy on every line of this dialog - unreadable on a
2536
+ dark card. All of it is `--text` / `--text2` now. */
2416
2537
  .nric-options-section-title {
2417
- color: #06253b;
2418
- font-size: 1.05rem;
2538
+ font-size: var(--fs-cell);
2539
+ font-weight: 800;
2419
2540
  line-height: 1.25;
2541
+ color: var(--text);
2420
2542
  }
2421
2543
 
2422
2544
  .nric-selected-person {
2423
- border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2424
- border-radius: 12px;
2425
- color: #06253b;
2426
- font-size: 0.95rem;
2545
+ border: 1px solid var(--border);
2546
+ border-radius: var(--r-inner-lg);
2547
+ color: var(--text);
2548
+ font-size: var(--fs-cell);
2427
2549
  min-height: 52px;
2428
2550
  padding: 10px 14px;
2429
2551
  }
2430
2552
 
2431
2553
  .nric-option-group {
2432
- border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2554
+ border-top: 1px solid var(--border);
2433
2555
  }
2434
2556
 
2435
2557
  .nric-option-radio {
2436
- border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2437
- color: #06253b;
2438
- font-size: 0.95rem;
2558
+ border-bottom: 1px solid var(--border);
2559
+ color: var(--text);
2560
+ font-size: var(--fs-cell);
2439
2561
  min-height: 46px;
2440
2562
  }
2441
2563
 
2442
2564
  .nric-option-radio :deep(.v-label) {
2443
- font-size: 0.95rem;
2565
+ font-size: var(--fs-cell);
2566
+ color: var(--text);
2444
2567
  opacity: 1;
2445
2568
  }
2446
2569
 
2447
- .hid-qr-preview-card {
2448
- border-radius: 6px;
2570
+ .hid-qr__body {
2571
+ padding: 0 20px;
2572
+ text-align: center;
2573
+ }
2574
+
2575
+ .hid-qr__header {
2576
+ font-size: var(--fs-card-title);
2577
+ font-weight: var(--fw-card-title);
2578
+ color: var(--text);
2579
+ }
2580
+
2581
+ .hid-qr__subtext {
2582
+ margin-top: 4px;
2583
+ font-size: var(--fs-cell);
2584
+ color: var(--text2);
2449
2585
  }
2450
2586
 
2587
+ .hid-qr__name {
2588
+ margin-top: 12px;
2589
+ font-size: var(--fs-card-title);
2590
+ font-weight: var(--fw-card-title);
2591
+ color: var(--text);
2592
+ }
2593
+
2594
+ .hid-qr__meta {
2595
+ margin-top: 4px;
2596
+ font-size: var(--fs-breadcrumb);
2597
+ color: var(--text2);
2598
+ }
2599
+
2600
+ /* The QR itself STAYS on a white plate with dark ink, in both themes, on
2601
+ purpose: a phone camera and an HID reader need the light-on-dark contrast the
2602
+ code is printed with, and inverting it in dark mode would stop it scanning.
2603
+ The frame around it follows the theme; the plate does not. */
2451
2604
  .hid-qr-image-shell {
2452
2605
  display: inline-flex;
2606
+ margin: 20px 0;
2453
2607
  padding: 12px;
2454
- border: 1px solid #dce3e8;
2608
+ border: 1px solid var(--border);
2609
+ border-radius: var(--r-inner);
2455
2610
  background: #ffffff;
2456
2611
  }
2457
2612
 
@@ -2462,11 +2617,40 @@ onMounted(() => {
2462
2617
  align-items: center;
2463
2618
  justify-content: center;
2464
2619
  border: 0;
2465
- background: #f5f7f9;
2466
- color: #344054;
2620
+ border-radius: var(--r-inner-sm);
2621
+ /* The white plate above is a deliberate exception FOR THE PRINTED CODE. This
2622
+ is the tap-to-reveal button that sits in its place beforehand, so it is
2623
+ ordinary chrome and follows the theme: a literal `#f5f7f9`/`#344054` drew a
2624
+ light slab with dark ink inside the dark modal. `--bg` reads as a recess
2625
+ against the modal's `--card` in both themes. Measured `--text2` on `--bg`:
2626
+ 8.88:1 light, 11.21:1 dark. */
2627
+ background: var(--bg);
2628
+ color: var(--text2);
2467
2629
  cursor: pointer;
2468
2630
  flex-direction: column;
2469
2631
  gap: 10px;
2470
2632
  font: inherit;
2633
+ font-size: var(--fs-cell);
2634
+ }
2635
+ </style>
2636
+
2637
+ <!--
2638
+ UNSCOPED on purpose, and only for the menu surface.
2639
+
2640
+ A `v-menu`'s panel is TELEPORTED to the overlay container, so it never
2641
+ carries this component's scope attribute - a scoped rule cannot reach it.
2642
+ And Vuetify owns the radius from outside at a specificity a scoped class
2643
+ cannot beat: `.v-menu > .v-overlay__content > .v-list { border-radius:
2644
+ inherit }` is 0,3,0 against a scoped class's 0,2,0, so the list inherits the
2645
+ overlay's 4px however the list itself is styled. Measured: 4px on the glass
2646
+ with the scoped rule in place.
2647
+
2648
+ So the radius is set where Vuetify sets it - on the overlay content, reached
2649
+ by the menu's own `content-class`. The class name is component-prefixed, so
2650
+ nothing else in the layer can pick it up. No `!important`.
2651
+ -->
2652
+ <style>
2653
+ .v-menu > .v-overlay__content.visitor-form__menu {
2654
+ border-radius: var(--r-menu);
2471
2655
  }
2472
2656
  </style>