@bcrs-shared-components/effective-date-time 1.1.43 → 1.1.44

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,525 +1,525 @@
1
- <template>
2
- <v-card
3
- id="effective-date-time-box"
4
- flat
5
- >
6
- <v-radio-group
7
- v-model="effectiveDateType"
8
- column
9
- class="pt-0 mt-0"
10
- >
11
- <v-radio
12
- label="Immediate (date and time of filing)"
13
- :value="EffectiveDateTypes.IMMEDIATE"
14
- />
15
- <v-radio
16
- label="A date and time in the future"
17
- :value="EffectiveDateTypes.FUTURE_EFFECTIVE"
18
- />
19
- </v-radio-group>
20
-
21
- <v-form
22
- ref="form"
23
- class="date-time-selectors"
24
- >
25
- <DatePicker
26
- ref="datePickerRef"
27
- title="Date"
28
- nudge-right="40"
29
- :inputRules="dateRules"
30
- :disablePicker="effectiveDateType !== EffectiveDateTypes.FUTURE_EFFECTIVE"
31
- :minDate="dateToYyyyMmDd(minDate)"
32
- :maxDate="dateToYyyyMmDd(maxDate)"
33
- @emitDate="dateText = $event"
34
- @emitCancel="dateText = ''"
35
- />
36
-
37
- <v-row>
38
- <v-col
39
- cols="12"
40
- sm="6"
41
- md="3"
42
- >
43
- <v-combobox
44
- id="hour-selector"
45
- ref="hourSelector"
46
- v-model="selectHour"
47
- filled
48
- class="mr-1"
49
- label="Hour"
50
- :items="hours"
51
- :disabled="!isFutureEffective"
52
- :rules="hourRules"
53
- />
54
- </v-col>
55
- <span
56
- class="time-colon"
57
- :class="{ 'disabled': !isFutureEffective }"
58
- >:</span>
59
- <v-col
60
- cols="12"
61
- sm="6"
62
- md="3"
63
- >
64
- <v-combobox
65
- id="minute-selector"
66
- ref="minuteSelector"
67
- v-model="selectMinute"
68
- filled
69
- class="ml-1"
70
- label="Minute"
71
- :items="minutes"
72
- :disabled="!isFutureEffective"
73
- :rules="minuteRules"
74
- />
75
- </v-col>
76
- <v-col
77
- cols="12"
78
- sm="6"
79
- md="3"
80
- >
81
- <v-select
82
- id="period-selector"
83
- v-model="selectPeriod"
84
- filled
85
- :items="timePeriod"
86
- :disabled="!isFutureEffective"
87
- />
88
- </v-col>
89
- <v-col
90
- cols="12"
91
- sm="6"
92
- md="3"
93
- class="label-col"
94
- >
95
- <span
96
- class="time-zone-label"
97
- :class="{ 'disabled': !isFutureEffective }"
98
- >Pacific time</span>
99
- </v-col>
100
- </v-row>
101
-
102
- <!-- display validation alert only after date and time have been entered -->
103
- <v-row v-if="isFutureEffective && dateText && (selectHour.length > 0) && (selectMinute.length > 0)">
104
- <v-col class="validation-alert">
105
- <p
106
- v-if="isUnderTime"
107
- class="validation-alert-msg"
108
- >
109
- The time must be at least {{ dateToPacificTime(minDate) }} for the selected date
110
- </p>
111
- <p
112
- v-if="isOverTime"
113
- class="validation-alert-msg"
114
- >
115
- The time must be at most {{ dateToPacificTime(maxDate) }} for the selected date
116
- </p>
117
- </v-col>
118
- </v-row>
119
- </v-form>
120
- </v-card>
121
- </template>
122
-
123
- <script lang="ts">
124
- import Vue from 'vue'
125
- import { Component, Emit, Mixins, Prop, Watch } from 'vue-property-decorator'
126
- import { DatePicker } from '@bcrs-shared-components/date-picker'
127
- import { DateMixin } from '@/mixins' // NB: local mixin (StoryBook can't find it otherwise)
128
- import { EffectiveDateTypes } from '@bcrs-shared-components/enums'
129
- import { EffectiveDateTimeIF, FormFieldType, FormIF } from '@bcrs-shared-components/interfaces'
130
-
131
- enum PeriodTypes {
132
- AM = 'am',
133
- PM = 'pm'
134
- }
135
-
136
- @Component({
137
- components: {
138
- DatePicker
139
- }
140
- })
141
- export default class EffectiveDateTime extends Mixins(DateMixin) {
142
- readonly MIN_DIFF_MINUTES = 3
143
- readonly MAX_DIFF_DAYS = 10
144
-
145
- // Add element types to refs
146
- $refs!: {
147
- form: FormIF,
148
- datePickerRef: any, // should be DatePicker but TS complains
149
- hourSelector: FormFieldType, // used in unit tests
150
- minuteSelector: FormFieldType // used in unit tests
151
- }
152
-
153
- /** Whether to parse the initial effective date-time into the controls. */
154
- @Prop({ default: false }) readonly parseInitial!: boolean
155
-
156
- /** Current JS date, expected to be passed in periodically. */
157
- @Prop() readonly currentJsDate!: Date
158
-
159
- /** Effective Date Time object, for initial config. */
160
- @Prop() readonly effectiveDateTime!: EffectiveDateTimeIF
161
-
162
- /** Whether to perform validation. */
163
- @Prop() readonly isAppValidate!: boolean
164
-
165
- // Declaration for template
166
- readonly EffectiveDateTypes = EffectiveDateTypes
167
-
168
- /** Whether Is Immediate is selected. */
169
- private isImmediate = false
170
-
171
- /** Whether Is Future Effective is selected. */
172
- private isFutureEffective = false
173
-
174
- /** The minimum date that can be entered (ie, now + 3 minutes). */
175
- private minDate: Date = null
176
-
177
- /** The maximum date that can be entered (ie, 10 days from now). */
178
- private maxDate: Date = null
179
-
180
- // V-model values
181
- private effectiveDateType: EffectiveDateTypes = null
182
- private datePicker = ''
183
- private dateText = ''
184
- private selectHour: string[] = []
185
- private selectMinute: string[] = []
186
- private selectPeriod = PeriodTypes.AM
187
-
188
- // Combobox items
189
- private hours = [...Array(12).keys()].map(num => (num + 1).toString())
190
- private minutes = [...Array(60).keys()].map(num => num.toString().padStart(2, '0'))
191
- private timePeriod = [PeriodTypes.AM, PeriodTypes.PM]
192
-
193
- /** Validations rules for date text field. */
194
- get dateRules (): Array<(v) => boolean | string> {
195
- // only apply rules when Future Effective is selected
196
- if (this.isFutureEffective && this.isAppValidate) {
197
- const minDateStr = this.dateToPacificDate(this.minDate, true)
198
- const maxDateStr = this.dateToPacificDate(this.maxDate, true)
199
- return [
200
- (v: string) => !!v || 'Select date',
201
- (v: string) => this.isValidDateRange(v) || `Date must be between ${minDateStr} and ${maxDateStr}`
202
- ]
203
- }
204
- return []
205
- }
206
-
207
- /**
208
- * True if date is >= the minimum (ie, today) and <= the maximum (ie, the 10th day).
209
- * This is used for Vue form validation (in Date Rules above).
210
- */
211
- private isValidDateRange (v: string): boolean {
212
- let date = new Date(v)
213
- // only compare year/month/day (ignore time)
214
- date = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
215
- const minDay = new Date(this.minDate.getFullYear(), this.minDate.getMonth(), this.minDate.getDate())
216
- const maxDay = new Date(this.maxDate.getFullYear(), this.maxDate.getMonth(), this.maxDate.getDate())
217
- return (date >= minDay && date <= maxDay)
218
- }
219
-
220
- /** Validations rules for hour selector. */
221
- get hourRules (): Array<(v) => boolean | string> {
222
- // only apply rules when Future Effective is selected
223
- if (this.isFutureEffective && this.isAppValidate) {
224
- return [
225
- (v: string[]) => (v.length > 0) || 'Select hour',
226
- (v: string) => (/^([1-9]|1[012])$/.test(v)) || ''
227
- ]
228
- }
229
- return []
230
- }
231
-
232
- /** Validations rules for minute selector. */
233
- get minuteRules (): Array<(v) => boolean | string> {
234
- // only apply rules when Future Effective is selected
235
- if (this.isFutureEffective && this.isAppValidate) {
236
- return [
237
- (v: string[]) => (v.length > 0) || 'Select minute',
238
- (v: string) => (/^([0-5]?[0-9])$/.test(v)) || ''
239
- ]
240
- }
241
- return []
242
- }
243
-
244
- /**
245
- * True if time is under the minimum (ie, for today).
246
- * This is a non-form validation - it needs to be checked for overall component validity.
247
- */
248
- get isUnderTime (): boolean {
249
- if (this.effectiveDateTime.effectiveDate) {
250
- const date = new Date(this.effectiveDateTime.effectiveDate)
251
- // use max seconds and milliseconds for comparison
252
- date.setSeconds(59, 999)
253
- return (date.getTime() < this.minDate.getTime())
254
- }
255
- return false
256
- }
257
-
258
- /**
259
- * True if time is over the maximum (ie, for 10th day).
260
- * This is a non-form validation - it needs to be checked for overall component validity.
261
- */
262
- get isOverTime (): boolean {
263
- if (this.effectiveDateTime.effectiveDate) {
264
- const date = new Date(this.effectiveDateTime.effectiveDate)
265
- // use min seconds and milliseconds for comparison
266
- date.setSeconds(0, 0)
267
- return (date.getTime() > this.maxDate.getTime())
268
- }
269
- return false
270
- }
271
-
272
- /** Called when component is mounted. */
273
- mounted (): void {
274
- if (this.parseInitial) this.parseInitialEffectiveDateTime()
275
- }
276
-
277
- /** Parses initial Effective Date Time and sets state. */
278
- private parseInitialEffectiveDateTime (): void {
279
- // set the chosen effective date option
280
- this.isFutureEffective = this.effectiveDateTime.isFutureEffective
281
- if (this.isFutureEffective === true) {
282
- this.effectiveDateType = EffectiveDateTypes.FUTURE_EFFECTIVE
283
- } else if (this.isFutureEffective === false) {
284
- this.effectiveDateType = EffectiveDateTypes.IMMEDIATE
285
- } else {
286
- this.effectiveDateType = null
287
- }
288
-
289
- // try to create Date object
290
- const effectiveDate = this.effectiveDateTime.effectiveDate
291
- const date = effectiveDate && new Date(effectiveDate)
292
-
293
- if (date) {
294
- // set model properties
295
- let hour = date.getHours()
296
- const minute = date.getMinutes()
297
- const period = hour < 12 ? PeriodTypes.AM : PeriodTypes.PM
298
-
299
- // convert 24h -> 12h and 0h -> 12h
300
- if (hour > 12) {
301
- hour -= 12
302
- } else if (hour === 0) {
303
- hour = 12
304
- }
305
-
306
- // set model values
307
- this.dateText = this.dateToYyyyMmDd(date)
308
- this.selectHour = [hour.toString()]
309
- this.selectMinute = [minute.toString().padStart(2, '0')]
310
- this.selectPeriod = period
311
- }
312
- }
313
-
314
- /** Constructs the effective date and updates the parent. */
315
- private async constructAndUpdate (): Promise<void> {
316
- // wait for form to update itself before checking validity
317
- await Vue.nextTick()
318
-
319
- const isDateValid = this.$refs.datePickerRef.validateForm()
320
- const isTimeValid = this.$refs.form.validate()
321
- if (isDateValid && isTimeValid && !!this.selectHour.length && !!this.selectMinute.length) {
322
- const year = +this.dateText.slice(0, 4)
323
- const month = (+this.dateText.slice(5, 7) - 1) // zero-relative
324
- const date = +this.dateText.slice(8, 10)
325
- let hours = +this.selectHour
326
- const minutes = +this.selectMinute
327
-
328
- // convert 12 am -> 0
329
- if (this.selectPeriod === PeriodTypes.AM && +this.selectHour === 12) {
330
- hours = 0
331
- }
332
-
333
- // convert 1-11 pm -> 13-23
334
- if (this.selectPeriod === PeriodTypes.PM && +this.selectHour !== 12) {
335
- hours += 12
336
- }
337
-
338
- // construct date in UTC using parameters in Pacific time
339
- const dateTime = this.createUtcDate(year, month, date, hours, minutes)
340
-
341
- // Set Effective Date
342
- this.emitEffectiveDate(dateTime)
343
- }
344
-
345
- // update validity every time
346
- this.emitValid()
347
- }
348
-
349
- @Watch('currentJsDate', { immediate: true })
350
- onCurrentJsDateChanged (val: Date) {
351
- // safety check (val may be null)
352
- if (val) {
353
- // set new min date
354
- const minDate = new Date()
355
- // add 3 minutes
356
- minDate.setTime(val.getTime() + this.MIN_DIFF_MINUTES * 60 * 1000)
357
- this.minDate = minDate
358
-
359
- // set new max date
360
- const maxDate = new Date()
361
- // add 10 days
362
- maxDate.setTime(val.getTime() + this.MAX_DIFF_DAYS * 24 * 60 * 60 * 1000)
363
- this.maxDate = maxDate
364
-
365
- // check if form is still valid
366
- this.emitValid()
367
- }
368
- }
369
-
370
- @Watch('datePicker')
371
- onDatePickerChanged (val: string): void {
372
- this.dateText = val
373
- // the watcher for dateText will fire next
374
- }
375
-
376
- @Watch('dateText')
377
- onDateTextChanged (val: string): void {
378
- if (this.isFutureEffective) {
379
- this.constructAndUpdate()
380
- }
381
- }
382
-
383
- @Watch('selectHour')
384
- onSelectHourChanged (val: string): void {
385
- if (this.isFutureEffective) {
386
- this.constructAndUpdate()
387
- }
388
- }
389
-
390
- @Watch('selectMinute')
391
- onSelectMinuteChanged (val: string): void {
392
- if (this.isFutureEffective) {
393
- this.constructAndUpdate()
394
- }
395
- }
396
-
397
- @Watch('selectPeriod')
398
- onSelectPeriodChanged (val: string): void {
399
- if (this.isFutureEffective) {
400
- this.constructAndUpdate()
401
- }
402
- }
403
-
404
- @Watch('effectiveDateType')
405
- onEffectiveDateTypeChanged (val: EffectiveDateTypes): void {
406
- this.isImmediate = (val === EffectiveDateTypes.IMMEDIATE)
407
- this.isFutureEffective = (val === EffectiveDateTypes.FUTURE_EFFECTIVE)
408
-
409
- // if we changed to IMMEDIATE then clear the model values (otherwise retain them)
410
- if (this.isImmediate) {
411
- this.datePicker = ''
412
- this.dateText = ''
413
- this.selectHour = []
414
- this.selectMinute = []
415
- this.selectPeriod = PeriodTypes.AM
416
- this.$refs.datePickerRef.clearDate()
417
- }
418
-
419
- // update the parent
420
- this.emitIsFutureEffective(this.isFutureEffective)
421
- this.emitEffectiveDate(null)
422
- this.emitValid()
423
- }
424
-
425
- @Emit('isFutureEffective')
426
- private emitIsFutureEffective (val: boolean): void {}
427
-
428
- @Emit('effectiveDate')
429
- private emitEffectiveDate (val: Date): void {}
430
-
431
- @Emit('valid')
432
- private async emitValid (): Promise<boolean> {
433
- // localized dateText check for future effective selections
434
- const validDateText = this.isFutureEffective ? !!this.dateText : true
435
-
436
- // wait for form to update itself before checking validity
437
- await Vue.nextTick()
438
- const isDateValid = this.$refs.datePickerRef.validateForm()
439
- const isTimeValid = this.$refs.form.validate()
440
- return this.isImmediate || (!!this.effectiveDateType &&
441
- isDateValid && isTimeValid &&
442
- !!this.selectHour.length && !!this.selectMinute.length &&
443
- !this.isUnderTime &&
444
- !this.isOverTime &&
445
- validDateText
446
- )
447
- }
448
- }
449
- </script>
450
-
451
- <style lang="scss" scoped>
452
- @import '@/assets/styles/theme.scss';
453
-
454
- #effective-date-time-box {
455
- padding: 2rem 2rem 0.5rem;
456
- line-height: 1.2rem;
457
- }
458
-
459
- :deep(.v-label) {
460
- color: $gray7;
461
- font-weight: normal;
462
- }
463
-
464
- .v-radio {
465
- padding-bottom: .5rem;
466
- }
467
-
468
- .date-time-selectors {
469
- margin-left: 2rem;
470
- }
471
-
472
- .time-colon {
473
- margin-left: -4px;
474
- margin-right: -4px;
475
- padding-top: 2rem;
476
- font-size: 25px;
477
- }
478
-
479
- @media (max-width: 768px) {
480
- .time-colon {
481
- display: none;
482
- }
483
- }
484
-
485
- .label-col {
486
- position: relative;
487
- align-self: center;
488
- }
489
-
490
- .time-zone-label {
491
- position: absolute;
492
- top: -10px;
493
- color: $gray7;
494
- }
495
-
496
- .disabled {
497
- color: $gray6;
498
- }
499
-
500
- .validation-alert {
501
- position: relative;
502
-
503
- .validation-alert-msg {
504
- line-height: 12px;
505
- position: absolute;
506
- top: -2rem;
507
- padding: 0 12px;
508
- font-size: 12px;
509
- font-weight: 500;
510
- color: $BCgovInputError !important;
511
- }
512
- }
513
-
514
- :deep() {
515
- .v-icon.v-icon.v-icon--disabled {
516
- color: $app-blue !important;
517
- }
518
- .v-input--is-disabled {
519
- opacity: 0.4;
520
- }
521
- .v-input--is-disabled .v-input__control > .v-input__slot:before {
522
- border-image: none;
523
- }
524
- }
525
- </style>
1
+ <template>
2
+ <v-card
3
+ id="effective-date-time-box"
4
+ flat
5
+ >
6
+ <v-radio-group
7
+ v-model="effectiveDateType"
8
+ column
9
+ class="pt-0 mt-0"
10
+ >
11
+ <v-radio
12
+ label="Immediate (date and time of filing)"
13
+ :value="EffectiveDateTypes.IMMEDIATE"
14
+ />
15
+ <v-radio
16
+ label="A date and time in the future"
17
+ :value="EffectiveDateTypes.FUTURE_EFFECTIVE"
18
+ />
19
+ </v-radio-group>
20
+
21
+ <v-form
22
+ ref="form"
23
+ class="date-time-selectors"
24
+ >
25
+ <DatePicker
26
+ ref="datePickerRef"
27
+ title="Date"
28
+ nudge-right="40"
29
+ :inputRules="dateRules"
30
+ :disablePicker="effectiveDateType !== EffectiveDateTypes.FUTURE_EFFECTIVE"
31
+ :minDate="dateToYyyyMmDd(minDate)"
32
+ :maxDate="dateToYyyyMmDd(maxDate)"
33
+ @emitDate="dateText = $event"
34
+ @emitCancel="dateText = ''"
35
+ />
36
+
37
+ <v-row>
38
+ <v-col
39
+ cols="12"
40
+ sm="6"
41
+ md="3"
42
+ >
43
+ <v-combobox
44
+ id="hour-selector"
45
+ ref="hourSelector"
46
+ v-model="selectHour"
47
+ filled
48
+ class="mr-1"
49
+ label="Hour"
50
+ :items="hours"
51
+ :disabled="!isFutureEffective"
52
+ :rules="hourRules"
53
+ />
54
+ </v-col>
55
+ <span
56
+ class="time-colon"
57
+ :class="{ 'disabled': !isFutureEffective }"
58
+ >:</span>
59
+ <v-col
60
+ cols="12"
61
+ sm="6"
62
+ md="3"
63
+ >
64
+ <v-combobox
65
+ id="minute-selector"
66
+ ref="minuteSelector"
67
+ v-model="selectMinute"
68
+ filled
69
+ class="ml-1"
70
+ label="Minute"
71
+ :items="minutes"
72
+ :disabled="!isFutureEffective"
73
+ :rules="minuteRules"
74
+ />
75
+ </v-col>
76
+ <v-col
77
+ cols="12"
78
+ sm="6"
79
+ md="3"
80
+ >
81
+ <v-select
82
+ id="period-selector"
83
+ v-model="selectPeriod"
84
+ filled
85
+ :items="timePeriod"
86
+ :disabled="!isFutureEffective"
87
+ />
88
+ </v-col>
89
+ <v-col
90
+ cols="12"
91
+ sm="6"
92
+ md="3"
93
+ class="label-col"
94
+ >
95
+ <span
96
+ class="time-zone-label"
97
+ :class="{ 'disabled': !isFutureEffective }"
98
+ >Pacific time</span>
99
+ </v-col>
100
+ </v-row>
101
+
102
+ <!-- display validation alert only after date and time have been entered -->
103
+ <v-row v-if="isFutureEffective && dateText && (selectHour.length > 0) && (selectMinute.length > 0)">
104
+ <v-col class="validation-alert">
105
+ <p
106
+ v-if="isUnderTime"
107
+ class="validation-alert-msg"
108
+ >
109
+ The time must be at least {{ dateToPacificTime(minDate) }} for the selected date
110
+ </p>
111
+ <p
112
+ v-if="isOverTime"
113
+ class="validation-alert-msg"
114
+ >
115
+ The time must be at most {{ dateToPacificTime(maxDate) }} for the selected date
116
+ </p>
117
+ </v-col>
118
+ </v-row>
119
+ </v-form>
120
+ </v-card>
121
+ </template>
122
+
123
+ <script lang="ts">
124
+ import Vue from 'vue'
125
+ import { Component, Emit, Mixins, Prop, Watch } from 'vue-property-decorator'
126
+ import { DatePicker } from '@bcrs-shared-components/date-picker'
127
+ import { DateMixin } from '@/mixins' // NB: local mixin (StoryBook can't find it otherwise)
128
+ import { EffectiveDateTypes } from '@bcrs-shared-components/enums'
129
+ import { EffectiveDateTimeIF, FormFieldType, FormIF } from '@bcrs-shared-components/interfaces'
130
+
131
+ enum PeriodTypes {
132
+ AM = 'am',
133
+ PM = 'pm'
134
+ }
135
+
136
+ @Component({
137
+ components: {
138
+ DatePicker
139
+ }
140
+ })
141
+ export default class EffectiveDateTime extends Mixins(DateMixin) {
142
+ readonly MIN_DIFF_MINUTES = 3
143
+ readonly MAX_DIFF_DAYS = 10
144
+
145
+ // Add element types to refs
146
+ $refs!: {
147
+ form: FormIF,
148
+ datePickerRef: any, // should be DatePicker but TS complains
149
+ hourSelector: FormFieldType, // used in unit tests
150
+ minuteSelector: FormFieldType // used in unit tests
151
+ }
152
+
153
+ /** Whether to parse the initial effective date-time into the controls. */
154
+ @Prop({ default: false }) readonly parseInitial!: boolean
155
+
156
+ /** Current JS date, expected to be passed in periodically. */
157
+ @Prop() readonly currentJsDate!: Date
158
+
159
+ /** Effective Date Time object, for initial config. */
160
+ @Prop() readonly effectiveDateTime!: EffectiveDateTimeIF
161
+
162
+ /** Whether to perform validation. */
163
+ @Prop() readonly isAppValidate!: boolean
164
+
165
+ // Declaration for template
166
+ readonly EffectiveDateTypes = EffectiveDateTypes
167
+
168
+ /** Whether Is Immediate is selected. */
169
+ private isImmediate = false
170
+
171
+ /** Whether Is Future Effective is selected. */
172
+ private isFutureEffective = false
173
+
174
+ /** The minimum date that can be entered (ie, now + 3 minutes). */
175
+ private minDate: Date = null
176
+
177
+ /** The maximum date that can be entered (ie, 10 days from now). */
178
+ private maxDate: Date = null
179
+
180
+ // V-model values
181
+ private effectiveDateType: EffectiveDateTypes = null
182
+ private datePicker = ''
183
+ private dateText = ''
184
+ private selectHour: string[] = []
185
+ private selectMinute: string[] = []
186
+ private selectPeriod = PeriodTypes.AM
187
+
188
+ // Combobox items
189
+ private hours = [...Array(12).keys()].map(num => (num + 1).toString())
190
+ private minutes = [...Array(60).keys()].map(num => num.toString().padStart(2, '0'))
191
+ private timePeriod = [PeriodTypes.AM, PeriodTypes.PM]
192
+
193
+ /** Validations rules for date text field. */
194
+ get dateRules (): Array<(v) => boolean | string> {
195
+ // only apply rules when Future Effective is selected
196
+ if (this.isFutureEffective && this.isAppValidate) {
197
+ const minDateStr = this.dateToPacificDate(this.minDate, true)
198
+ const maxDateStr = this.dateToPacificDate(this.maxDate, true)
199
+ return [
200
+ (v: string) => !!v || 'Select date',
201
+ (v: string) => this.isValidDateRange(v) || `Date must be between ${minDateStr} and ${maxDateStr}`
202
+ ]
203
+ }
204
+ return []
205
+ }
206
+
207
+ /**
208
+ * True if date is >= the minimum (ie, today) and <= the maximum (ie, the 10th day).
209
+ * This is used for Vue form validation (in Date Rules above).
210
+ */
211
+ private isValidDateRange (v: string): boolean {
212
+ let date = new Date(v)
213
+ // only compare year/month/day (ignore time)
214
+ date = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
215
+ const minDay = new Date(this.minDate.getFullYear(), this.minDate.getMonth(), this.minDate.getDate())
216
+ const maxDay = new Date(this.maxDate.getFullYear(), this.maxDate.getMonth(), this.maxDate.getDate())
217
+ return (date >= minDay && date <= maxDay)
218
+ }
219
+
220
+ /** Validations rules for hour selector. */
221
+ get hourRules (): Array<(v) => boolean | string> {
222
+ // only apply rules when Future Effective is selected
223
+ if (this.isFutureEffective && this.isAppValidate) {
224
+ return [
225
+ (v: string[]) => (v.length > 0) || 'Select hour',
226
+ (v: string) => (/^([1-9]|1[012])$/.test(v)) || ''
227
+ ]
228
+ }
229
+ return []
230
+ }
231
+
232
+ /** Validations rules for minute selector. */
233
+ get minuteRules (): Array<(v) => boolean | string> {
234
+ // only apply rules when Future Effective is selected
235
+ if (this.isFutureEffective && this.isAppValidate) {
236
+ return [
237
+ (v: string[]) => (v.length > 0) || 'Select minute',
238
+ (v: string) => (/^([0-5]?[0-9])$/.test(v)) || ''
239
+ ]
240
+ }
241
+ return []
242
+ }
243
+
244
+ /**
245
+ * True if time is under the minimum (ie, for today).
246
+ * This is a non-form validation - it needs to be checked for overall component validity.
247
+ */
248
+ get isUnderTime (): boolean {
249
+ if (this.effectiveDateTime.effectiveDate) {
250
+ const date = new Date(this.effectiveDateTime.effectiveDate)
251
+ // use max seconds and milliseconds for comparison
252
+ date.setSeconds(59, 999)
253
+ return (date.getTime() < this.minDate.getTime())
254
+ }
255
+ return false
256
+ }
257
+
258
+ /**
259
+ * True if time is over the maximum (ie, for 10th day).
260
+ * This is a non-form validation - it needs to be checked for overall component validity.
261
+ */
262
+ get isOverTime (): boolean {
263
+ if (this.effectiveDateTime.effectiveDate) {
264
+ const date = new Date(this.effectiveDateTime.effectiveDate)
265
+ // use min seconds and milliseconds for comparison
266
+ date.setSeconds(0, 0)
267
+ return (date.getTime() > this.maxDate.getTime())
268
+ }
269
+ return false
270
+ }
271
+
272
+ /** Called when component is mounted. */
273
+ mounted (): void {
274
+ if (this.parseInitial) this.parseInitialEffectiveDateTime()
275
+ }
276
+
277
+ /** Parses initial Effective Date Time and sets state. */
278
+ private parseInitialEffectiveDateTime (): void {
279
+ // set the chosen effective date option
280
+ this.isFutureEffective = this.effectiveDateTime.isFutureEffective
281
+ if (this.isFutureEffective === true) {
282
+ this.effectiveDateType = EffectiveDateTypes.FUTURE_EFFECTIVE
283
+ } else if (this.isFutureEffective === false) {
284
+ this.effectiveDateType = EffectiveDateTypes.IMMEDIATE
285
+ } else {
286
+ this.effectiveDateType = null
287
+ }
288
+
289
+ // try to create Date object
290
+ const effectiveDate = this.effectiveDateTime.effectiveDate
291
+ const date = effectiveDate && new Date(effectiveDate)
292
+
293
+ if (date) {
294
+ // set model properties
295
+ let hour = date.getHours()
296
+ const minute = date.getMinutes()
297
+ const period = hour < 12 ? PeriodTypes.AM : PeriodTypes.PM
298
+
299
+ // convert 24h -> 12h and 0h -> 12h
300
+ if (hour > 12) {
301
+ hour -= 12
302
+ } else if (hour === 0) {
303
+ hour = 12
304
+ }
305
+
306
+ // set model values
307
+ this.dateText = this.dateToYyyyMmDd(date)
308
+ this.selectHour = [hour.toString()]
309
+ this.selectMinute = [minute.toString().padStart(2, '0')]
310
+ this.selectPeriod = period
311
+ }
312
+ }
313
+
314
+ /** Constructs the effective date and updates the parent. */
315
+ private async constructAndUpdate (): Promise<void> {
316
+ // wait for form to update itself before checking validity
317
+ await Vue.nextTick()
318
+
319
+ const isDateValid = this.$refs.datePickerRef.validateForm()
320
+ const isTimeValid = this.$refs.form.validate()
321
+ if (isDateValid && isTimeValid && !!this.selectHour.length && !!this.selectMinute.length) {
322
+ const year = +this.dateText.slice(0, 4)
323
+ const month = (+this.dateText.slice(5, 7) - 1) // zero-relative
324
+ const date = +this.dateText.slice(8, 10)
325
+ let hours = +this.selectHour
326
+ const minutes = +this.selectMinute
327
+
328
+ // convert 12 am -> 0
329
+ if (this.selectPeriod === PeriodTypes.AM && +this.selectHour === 12) {
330
+ hours = 0
331
+ }
332
+
333
+ // convert 1-11 pm -> 13-23
334
+ if (this.selectPeriod === PeriodTypes.PM && +this.selectHour !== 12) {
335
+ hours += 12
336
+ }
337
+
338
+ // construct date in UTC using parameters in Pacific time
339
+ const dateTime = this.createUtcDate(year, month, date, hours, minutes)
340
+
341
+ // Set Effective Date
342
+ this.emitEffectiveDate(dateTime)
343
+ }
344
+
345
+ // update validity every time
346
+ this.emitValid()
347
+ }
348
+
349
+ @Watch('currentJsDate', { immediate: true })
350
+ onCurrentJsDateChanged (val: Date) {
351
+ // safety check (val may be null)
352
+ if (val) {
353
+ // set new min date
354
+ const minDate = new Date()
355
+ // add 3 minutes
356
+ minDate.setTime(val.getTime() + this.MIN_DIFF_MINUTES * 60 * 1000)
357
+ this.minDate = minDate
358
+
359
+ // set new max date
360
+ const maxDate = new Date()
361
+ // add 10 days
362
+ maxDate.setTime(val.getTime() + this.MAX_DIFF_DAYS * 24 * 60 * 60 * 1000)
363
+ this.maxDate = maxDate
364
+
365
+ // check if form is still valid
366
+ this.emitValid()
367
+ }
368
+ }
369
+
370
+ @Watch('datePicker')
371
+ onDatePickerChanged (val: string): void {
372
+ this.dateText = val
373
+ // the watcher for dateText will fire next
374
+ }
375
+
376
+ @Watch('dateText')
377
+ onDateTextChanged (val: string): void {
378
+ if (this.isFutureEffective) {
379
+ this.constructAndUpdate()
380
+ }
381
+ }
382
+
383
+ @Watch('selectHour')
384
+ onSelectHourChanged (val: string): void {
385
+ if (this.isFutureEffective) {
386
+ this.constructAndUpdate()
387
+ }
388
+ }
389
+
390
+ @Watch('selectMinute')
391
+ onSelectMinuteChanged (val: string): void {
392
+ if (this.isFutureEffective) {
393
+ this.constructAndUpdate()
394
+ }
395
+ }
396
+
397
+ @Watch('selectPeriod')
398
+ onSelectPeriodChanged (val: string): void {
399
+ if (this.isFutureEffective) {
400
+ this.constructAndUpdate()
401
+ }
402
+ }
403
+
404
+ @Watch('effectiveDateType')
405
+ onEffectiveDateTypeChanged (val: EffectiveDateTypes): void {
406
+ this.isImmediate = (val === EffectiveDateTypes.IMMEDIATE)
407
+ this.isFutureEffective = (val === EffectiveDateTypes.FUTURE_EFFECTIVE)
408
+
409
+ // if we changed to IMMEDIATE then clear the model values (otherwise retain them)
410
+ if (this.isImmediate) {
411
+ this.datePicker = ''
412
+ this.dateText = ''
413
+ this.selectHour = []
414
+ this.selectMinute = []
415
+ this.selectPeriod = PeriodTypes.AM
416
+ this.$refs.datePickerRef.clearDate()
417
+ }
418
+
419
+ // update the parent
420
+ this.emitIsFutureEffective(this.isFutureEffective)
421
+ this.emitEffectiveDate(null)
422
+ this.emitValid()
423
+ }
424
+
425
+ @Emit('isFutureEffective')
426
+ private emitIsFutureEffective (val: boolean): void {}
427
+
428
+ @Emit('effectiveDate')
429
+ private emitEffectiveDate (val: Date): void {}
430
+
431
+ @Emit('valid')
432
+ private async emitValid (): Promise<boolean> {
433
+ // localized dateText check for future effective selections
434
+ const validDateText = this.isFutureEffective ? !!this.dateText : true
435
+
436
+ // wait for form to update itself before checking validity
437
+ await Vue.nextTick()
438
+ const isDateValid = this.$refs.datePickerRef.validateForm()
439
+ const isTimeValid = this.$refs.form.validate()
440
+ return this.isImmediate || (!!this.effectiveDateType &&
441
+ isDateValid && isTimeValid &&
442
+ !!this.selectHour.length && !!this.selectMinute.length &&
443
+ !this.isUnderTime &&
444
+ !this.isOverTime &&
445
+ validDateText
446
+ )
447
+ }
448
+ }
449
+ </script>
450
+
451
+ <style lang="scss" scoped>
452
+ @import '@/assets/styles/theme.scss';
453
+
454
+ #effective-date-time-box {
455
+ padding: 2rem 2rem 0.5rem;
456
+ line-height: 1.2rem;
457
+ }
458
+
459
+ :deep(.v-label) {
460
+ color: $gray7;
461
+ font-weight: normal;
462
+ }
463
+
464
+ .v-radio {
465
+ padding-bottom: .5rem;
466
+ }
467
+
468
+ .date-time-selectors {
469
+ margin-left: 2rem;
470
+ }
471
+
472
+ .time-colon {
473
+ margin-left: -4px;
474
+ margin-right: -4px;
475
+ padding-top: 2rem;
476
+ font-size: 25px;
477
+ }
478
+
479
+ @media (max-width: 768px) {
480
+ .time-colon {
481
+ display: none;
482
+ }
483
+ }
484
+
485
+ .label-col {
486
+ position: relative;
487
+ align-self: center;
488
+ }
489
+
490
+ .time-zone-label {
491
+ position: absolute;
492
+ top: -10px;
493
+ color: $gray7;
494
+ }
495
+
496
+ .disabled {
497
+ color: $gray6;
498
+ }
499
+
500
+ .validation-alert {
501
+ position: relative;
502
+
503
+ .validation-alert-msg {
504
+ line-height: 12px;
505
+ position: absolute;
506
+ top: -2rem;
507
+ padding: 0 12px;
508
+ font-size: 12px;
509
+ font-weight: 500;
510
+ color: $BCgovInputError !important;
511
+ }
512
+ }
513
+
514
+ :deep() {
515
+ .v-icon.v-icon.v-icon--disabled {
516
+ color: $app-blue !important;
517
+ }
518
+ .v-input--is-disabled {
519
+ opacity: 0.4;
520
+ }
521
+ .v-input--is-disabled .v-input__control > .v-input__slot:before {
522
+ border-image: none;
523
+ }
524
+ }
525
+ </style>