@brickclay-org/ui 0.0.39 → 0.0.40

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.
Files changed (56) hide show
  1. package/ASSETS_SETUP.md +59 -0
  2. package/ng-package.json +29 -0
  3. package/package.json +15 -26
  4. package/src/lib/assets/icons.ts +8 -0
  5. package/src/lib/badge/badge.html +24 -0
  6. package/src/lib/badge/badge.ts +42 -0
  7. package/src/lib/brickclay-lib.spec.ts +23 -0
  8. package/src/lib/brickclay-lib.ts +15 -0
  9. package/src/lib/button-group/button-group.html +12 -0
  10. package/src/lib/button-group/button-group.ts +73 -0
  11. package/src/lib/calender/calendar.module.ts +35 -0
  12. package/src/lib/calender/components/custom-calendar/custom-calendar.component.css +698 -0
  13. package/src/lib/calender/components/custom-calendar/custom-calendar.component.html +230 -0
  14. package/src/lib/calender/components/custom-calendar/custom-calendar.component.spec.ts +23 -0
  15. package/src/lib/calender/components/custom-calendar/custom-calendar.component.ts +1554 -0
  16. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.css +373 -0
  17. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.html +210 -0
  18. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.ts +361 -0
  19. package/src/lib/calender/components/time-picker/time-picker.component.css +174 -0
  20. package/src/lib/calender/components/time-picker/time-picker.component.html +60 -0
  21. package/src/lib/calender/components/time-picker/time-picker.component.ts +283 -0
  22. package/src/lib/calender/services/calendar-manager.service.ts +45 -0
  23. package/src/lib/checkbox/checkbox.html +42 -0
  24. package/src/lib/checkbox/checkbox.ts +67 -0
  25. package/src/lib/chips/chips.html +74 -0
  26. package/src/lib/chips/chips.ts +222 -0
  27. package/src/lib/grid/components/grid/grid.html +97 -0
  28. package/src/lib/grid/components/grid/grid.ts +139 -0
  29. package/src/lib/grid/models/grid.model.ts +20 -0
  30. package/src/lib/input/input.html +127 -0
  31. package/src/lib/input/input.ts +394 -0
  32. package/src/lib/pill/pill.html +24 -0
  33. package/src/lib/pill/pill.ts +39 -0
  34. package/src/lib/radio/radio.html +58 -0
  35. package/src/lib/radio/radio.ts +72 -0
  36. package/src/lib/select/select.html +111 -0
  37. package/src/lib/select/select.ts +401 -0
  38. package/src/lib/spinner/spinner.html +5 -0
  39. package/src/lib/spinner/spinner.ts +22 -0
  40. package/src/lib/tabs/tabs.html +28 -0
  41. package/src/lib/tabs/tabs.ts +48 -0
  42. package/src/lib/textarea/textarea.html +80 -0
  43. package/src/lib/textarea/textarea.ts +172 -0
  44. package/src/lib/toggle/toggle.html +24 -0
  45. package/src/lib/toggle/toggle.ts +62 -0
  46. package/src/lib/ui-button/ui-button.html +25 -0
  47. package/src/lib/ui-button/ui-button.ts +55 -0
  48. package/src/lib/ui-icon-button/ui-icon-button.html +7 -0
  49. package/src/lib/ui-icon-button/ui-icon-button.ts +38 -0
  50. package/src/public-api.ts +43 -0
  51. package/tsconfig.lib.json +19 -0
  52. package/tsconfig.lib.prod.json +11 -0
  53. package/tsconfig.spec.json +15 -0
  54. package/fesm2022/brickclay-org-ui.mjs +0 -4035
  55. package/fesm2022/brickclay-org-ui.mjs.map +0 -1
  56. package/index.d.ts +0 -857
@@ -0,0 +1,1554 @@
1
+ import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, HostListener, OnChanges, SimpleChanges } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { CalendarManagerService } from '../../services/calendar-manager.service';
5
+ import { Subscription } from 'rxjs';
6
+ import { BkTimePicker } from '../time-picker/time-picker.component';
7
+ import moment from 'moment';
8
+ import { BrickclayIcons } from '../../../assets/icons';
9
+ export interface CalendarRange {
10
+ start: Date;
11
+ end: Date;
12
+ }
13
+
14
+ export interface CalendarSelection {
15
+ startDate: string | null;
16
+ endDate: string | null;
17
+ selectedDates?: string[]; // For multi-date selection
18
+ }
19
+
20
+ @Component({
21
+ selector: 'bk-custom-calendar',
22
+ standalone: true,
23
+ imports: [CommonModule, FormsModule, BkTimePicker],
24
+ templateUrl: './custom-calendar.component.html',
25
+ styleUrls: ['./custom-calendar.component.css']
26
+ })
27
+ export class BkCustomCalendar implements OnInit, OnDestroy, OnChanges {
28
+
29
+ // Basic Options
30
+ @Input() enableTimepicker = false;
31
+ @Input() autoApply = false;
32
+ @Input() closeOnAutoApply = false;
33
+ @Input() showCancel = true;
34
+ @Input() linkedCalendars = false;
35
+ @Input() singleDatePicker = false;
36
+ @Input() showWeekNumbers = false;
37
+ @Input() showISOWeekNumbers = false;
38
+ @Input() customRangeDirection = false;
39
+ @Input() lockStartDate = false;
40
+ @Input() position: 'center' | 'left' | 'right' = 'left';
41
+ @Input() drop: 'up' | 'down' = 'down';
42
+ @Input() dualCalendar = false;
43
+ @Input() showRanges = true;
44
+ @Input() timeFormat: 12 | 24 = 24;
45
+ @Input() enableSeconds = false;
46
+ @Input() customRanges?: Record<string, CalendarRange>;
47
+ @Input() multiDateSelection = false; // NEW: Enable multi-date selection
48
+ @Input() maxDate?: Date; // NEW: Maximum selectable date
49
+ @Input() minDate?: Date; // NEW: Minimum selectable date
50
+ @Input() placeholder = 'Select date range'; // NEW: Custom placeholder
51
+ @Input() opens: 'left' | 'right' | 'center' = 'left'; // NEW: Popup position
52
+ @Input() inline = false; // NEW: Always show calendar inline (no popup)
53
+ @Input() isDisplayCrossIcon = true; // NEW: Show/Hide clear (X) icon
54
+
55
+ @Output() selected = new EventEmitter<CalendarSelection>();
56
+ @Output() opened = new EventEmitter<void>();
57
+ @Output() closed = new EventEmitter<void>();
58
+
59
+ /**
60
+ * External value passed from parent. If provided, component will select these dates on init / change.
61
+ * Accepts { startDate: Date|null, endDate: Date|null, selectedDates?: Date[] }
62
+ */
63
+ @Input() selectedValue: CalendarSelection | null = null;
64
+ /** Optional display format for the input value. Uses moment formatting tokens. */
65
+ @Input() displayFormat = 'MM/DD/YYYY';
66
+
67
+ brickclayIcons = BrickclayIcons;
68
+
69
+ show = false;
70
+ today = new Date();
71
+ month = this.today.getMonth();
72
+ year = this.today.getFullYear();
73
+ calendar: { day: number, currentMonth: boolean }[][] = [];
74
+ leftMonth!: number;
75
+ leftYear!: number;
76
+ rightMonth!: number;
77
+ rightYear!: number;
78
+ leftCalendar: { day: number, currentMonth: boolean }[][] = [];
79
+ rightCalendar: { day: number, currentMonth: boolean }[][] = [];
80
+ @Input() startDate: Date | null = null;
81
+ @Input() endDate: Date | null = null;
82
+ selectedDates: Date[] = []; // NEW: For multi-date selection
83
+ disableHighlight = false;
84
+ hoveredDate: Date | null = null; // For hover preview
85
+
86
+ // Track raw input values for minutes to allow free typing
87
+ minuteInputValues: { [key: string]: string } = {};
88
+
89
+ // Time picker for single calendar (12-hour format: 1-12)
90
+ selectedHour = 1;
91
+ selectedMinute = 0;
92
+ selectedSecond = 0;
93
+ selectedAMPM: 'AM' | 'PM' = 'AM';
94
+
95
+ // NEW: Separate time pickers for dual calendar (12-hour format: 1-12)
96
+ startHour = 1;
97
+ startMinute = 0;
98
+ startSecond = 0;
99
+ startAMPM: 'AM' | 'PM' = 'AM';
100
+ endHour = 2;
101
+ endMinute = 0;
102
+ endSecond = 0;
103
+ endAMPM: 'AM' | 'PM' = 'AM';
104
+
105
+ // Track open time-picker within this calendar (for single-open behavior)
106
+ openTimePickerId: string | null = null;
107
+ closePickerCounter: { [key: string]: number } = {};
108
+
109
+ defaultRanges: Record<string, CalendarRange> = {};
110
+ activeRange: string | null = null; // Track which range is currently active
111
+ rangeOrder: string[] = []; // Maintain order of ranges
112
+
113
+ private unregisterFn?: () => void;
114
+ private closeAllSubscription?: Subscription;
115
+ private closeFn?: () => void;
116
+
117
+ constructor(private calendarManager: CalendarManagerService) {}
118
+
119
+ @HostListener('document:click', ['$event'])
120
+ onClickOutside(event: MouseEvent) {
121
+ // Don't handle click outside if inline mode is enabled
122
+ if (this.inline) {
123
+ return;
124
+ }
125
+ const target = event.target as HTMLElement;
126
+ if (this.show && !target.closest('.calendar-container')) {
127
+ this.close();
128
+ }
129
+ }
130
+
131
+ ngOnInit() {
132
+ if (!this.customRanges) {
133
+ this.initializeDefaultRanges();
134
+ } else {
135
+ // If customRanges is provided via @Input, set the order based on the keys
136
+ // Maintain the desired order if keys match, otherwise use provided order
137
+ const desiredOrder = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'This Month', 'Last Month', 'Custom Range'];
138
+ const providedKeys = Object.keys(this.customRanges);
139
+ // Check if Custom Range exists, if not add it
140
+ if (!this.customRanges['Custom Range']) {
141
+ this.customRanges['Custom Range'] = { start: new Date(), end: new Date() };
142
+ }
143
+ // Build order: first add desired order items that exist, then add any remaining
144
+ this.rangeOrder = desiredOrder.filter(key => providedKeys.includes(key) || key === 'Custom Range');
145
+ const remaining = providedKeys.filter(key => !this.rangeOrder.includes(key));
146
+ this.rangeOrder = [...this.rangeOrder, ...remaining];
147
+ }
148
+ if (this.dualCalendar) this.initializeDual();
149
+ else this.generateCalendar();
150
+
151
+ // Initialize time from existing dates if available
152
+ if (this.startDate) {
153
+ this.initializeTimeFromDate(this.startDate, true);
154
+ }
155
+ if (this.endDate) {
156
+ this.initializeTimeFromDate(this.endDate, false);
157
+ }
158
+
159
+ // Check if current dates match any predefined range
160
+ if (this.startDate && this.endDate) {
161
+ this.checkAndSetActiveRange();
162
+ }
163
+
164
+ // If inline mode, always show calendar
165
+ if (this.inline) {
166
+ this.show = true;
167
+ }
168
+
169
+ // Register this calendar instance with the manager service
170
+ this.closeFn = () => {
171
+ if (this.show && !this.inline) {
172
+ this.close();
173
+ }
174
+ };
175
+ this.unregisterFn = this.calendarManager.register(this.closeFn);
176
+
177
+ // Subscribe to close all events (skip if inline)
178
+ this.closeAllSubscription = this.calendarManager.closeAll$.subscribe(() => {
179
+ if (this.show && !this.inline) {
180
+ this.close();
181
+ }
182
+ });
183
+ }
184
+
185
+ ngOnChanges(changes: SimpleChanges) {
186
+ if (changes['selectedValue'] && this.selectedValue) {
187
+ // Normalize incoming values to Date or null
188
+ const s = this.selectedValue;
189
+ this.startDate = s.startDate ? new Date(s.startDate) : null;
190
+ this.endDate = s.endDate ? new Date(s.endDate) : null;
191
+ this.selectedDates = (s.selectedDates || []).map((d) => new Date(d));
192
+
193
+ // Update calendar month/year to show the start date (or end date if start missing)
194
+ const focusDate = this.startDate ?? this.endDate ?? new Date();
195
+ this.month = focusDate.getMonth();
196
+ this.year = focusDate.getFullYear();
197
+ if (this.dualCalendar) {
198
+ this.initializeDual();
199
+ } else {
200
+ this.generateCalendar();
201
+ }
202
+
203
+ // Re-evaluate active range if any
204
+ this.checkAndSetActiveRange();
205
+ }
206
+ }
207
+
208
+ ngOnDestroy() {
209
+ // Unregister this calendar instance
210
+ if (this.unregisterFn) {
211
+ this.unregisterFn();
212
+ }
213
+
214
+ // Unsubscribe from close all events
215
+ if (this.closeAllSubscription) {
216
+ this.closeAllSubscription.unsubscribe();
217
+ }
218
+ }
219
+
220
+ checkAndSetActiveRange() {
221
+ if (!this.customRanges || !this.startDate || !this.endDate) return;
222
+
223
+ // Normalize dates for comparison (ignore time)
224
+ const normalizeDate = (date: Date) => {
225
+ const d = new Date(date);
226
+ d.setHours(0, 0, 0, 0);
227
+ return d;
228
+ };
229
+
230
+ const start = normalizeDate(this.startDate);
231
+ const end = normalizeDate(this.endDate);
232
+
233
+ // Check each range (except Custom Range)
234
+ for (const key of this.rangeOrder) {
235
+ if (key === 'Custom Range') continue;
236
+ const range = this.customRanges![key];
237
+ if (range) {
238
+ const rangeStart = normalizeDate(range.start);
239
+ const rangeEnd = normalizeDate(range.end);
240
+ if (start.getTime() === rangeStart.getTime() && end.getTime() === rangeEnd.getTime()) {
241
+ this.activeRange = key;
242
+ return;
243
+ }
244
+ }
245
+ }
246
+
247
+ // If no match found, it's a custom range
248
+ this.activeRange = 'Custom Range';
249
+ }
250
+
251
+ initializeDefaultRanges() {
252
+ const today = new Date();
253
+ this.customRanges = {
254
+ 'Today': { start: new Date(today.getFullYear(), today.getMonth(), today.getDate()), end: new Date(today.getFullYear(), today.getMonth(), today.getDate()) },
255
+ 'Yesterday': { start: this.addDays(today, -1), end: this.addDays(today, -1) },
256
+ 'Last 7 Days': { start: this.addDays(today, -6), end: today },
257
+ 'Last 30 Days': { start: this.addDays(today, -29), end: today },
258
+ 'This Month': { start: new Date(today.getFullYear(), today.getMonth(), 1), end: today },
259
+ 'Last Month': { start: new Date(today.getFullYear(), today.getMonth() - 1, 1), end: new Date(today.getFullYear(), today.getMonth(), 0) },
260
+ 'Custom Range': { start: new Date(), end: new Date() }, // Placeholder, won't be used for selection
261
+ };
262
+ // Set the order of ranges
263
+ this.rangeOrder = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'This Month', 'Last Month', 'Custom Range'];
264
+ }
265
+
266
+ initializeTimeFromDate(date: Date, isStart: boolean) {
267
+ // Always use 12-hour format
268
+ const hours24 = date.getHours();
269
+ const minutes = date.getMinutes();
270
+ const seconds = date.getSeconds();
271
+
272
+ if (isStart) {
273
+ this.startMinute = minutes;
274
+ this.startSecond = seconds;
275
+ if (hours24 >= 12) {
276
+ this.startAMPM = 'PM';
277
+ this.startHour = hours24 > 12 ? hours24 - 12 : 12;
278
+ } else {
279
+ this.startAMPM = 'AM';
280
+ this.startHour = hours24 === 0 ? 12 : hours24;
281
+ }
282
+ } else {
283
+ this.endMinute = minutes;
284
+ this.endSecond = seconds;
285
+ if (hours24 >= 12) {
286
+ this.endAMPM = 'PM';
287
+ this.endHour = hours24 > 12 ? hours24 - 12 : 12;
288
+ } else {
289
+ this.endAMPM = 'AM';
290
+ this.endHour = hours24 === 0 ? 12 : hours24;
291
+ }
292
+ }
293
+ }
294
+
295
+ toggle() {
296
+ // Don't toggle if inline mode is enabled
297
+ if (this.inline) {
298
+ return;
299
+ }
300
+ const wasOpen = this.show;
301
+ this.show = !this.show;
302
+
303
+ if (this.show) {
304
+ // If opening, close all other calendars first
305
+ if (!wasOpen && this.closeFn) {
306
+ this.calendarManager.closeAllExcept(this.closeFn);
307
+ }
308
+ this.disableHighlight = false;
309
+ this.opened.emit();
310
+ } else {
311
+ this.closed.emit();
312
+ }
313
+ }
314
+
315
+ close() {
316
+ // Don't close if inline mode is enabled
317
+ if (this.inline) {
318
+ return;
319
+ }
320
+ this.show = false;
321
+ this.closed.emit();
322
+ }
323
+
324
+ onDateHover(day: number | null, fromRight = false) {
325
+ if (!day || this.singleDatePicker || this.multiDateSelection) {
326
+ this.hoveredDate = null;
327
+ return;
328
+ }
329
+
330
+ // Only show hover preview if start date is selected but end date is not
331
+ if (!this.startDate || this.endDate) {
332
+ this.hoveredDate = null;
333
+ return;
334
+ }
335
+
336
+ if (!this.dualCalendar) {
337
+ this.hoveredDate = new Date(this.year, this.month, day);
338
+ } else {
339
+ this.hoveredDate = fromRight
340
+ ? new Date(this.rightYear, this.rightMonth, day)
341
+ : new Date(this.leftYear, this.leftMonth, day);
342
+ }
343
+ }
344
+
345
+ onDateLeave() {
346
+ this.hoveredDate = null;
347
+ }
348
+
349
+ selectDate(day: number | null, fromRight = false) {
350
+ if (!day) return;
351
+
352
+ let selected: Date;
353
+ if (!this.dualCalendar) {
354
+ selected = new Date(this.year, this.month, day);
355
+ } else {
356
+ selected = fromRight
357
+ ? new Date(this.rightYear, this.rightMonth, day)
358
+ : new Date(this.leftYear, this.leftMonth, day);
359
+ }
360
+
361
+ // Clear hover on selection
362
+ this.hoveredDate = null;
363
+
364
+ // Check min/max date constraints
365
+ if (this.minDate && selected < this.minDate) return;
366
+ if (this.maxDate && selected > this.maxDate) return;
367
+
368
+ // Multi-date selection mode
369
+ if (this.multiDateSelection) {
370
+ this.handleMultiDateSelection(selected);
371
+ return;
372
+ }
373
+
374
+ // Apply time if timepicker is enabled (convert 12-hour to 24-hour)
375
+ if (this.enableTimepicker) {
376
+ if (this.dualCalendar) {
377
+ // For dual calendar, use separate start/end times
378
+ // If no startDate OR endDate exists, we're selecting start date
379
+ const isStart = !this.startDate || !!this.endDate;
380
+ this.applyTimeToDate(selected, isStart);
381
+ } else {
382
+ // For single calendar, always use selected time for start
383
+ this.applyTimeToDate(selected, true);
384
+ }
385
+ }
386
+
387
+ // Single date picker mode
388
+ if (this.singleDatePicker) {
389
+ this.startDate = selected;
390
+ this.endDate = null;
391
+ // Activate Custom Range when manually selecting dates
392
+ this.activeRange = 'Custom Range';
393
+ // Apply time immediately if timepicker is enabled
394
+ if (this.enableTimepicker) {
395
+ this.applyTimeToDate(this.startDate, true);
396
+ }
397
+ if (this.autoApply) {
398
+ this.apply();
399
+ if (this.closeOnAutoApply && !this.inline) this.close();
400
+ } else {
401
+ // Always emit selection event even if autoApply is false (especially for inline calendars)
402
+ this.emitSelection();
403
+ }
404
+ return;
405
+ }
406
+
407
+ // Range selection mode
408
+ if (!this.startDate || this.endDate) {
409
+ this.startDate = selected;
410
+ this.endDate = null;
411
+ // Activate Custom Range when manually selecting dates
412
+ this.activeRange = 'Custom Range';
413
+ // Keep left calendar on the selected month for better UX
414
+ if (this.dualCalendar) {
415
+ this.leftMonth = selected.getMonth();
416
+ this.leftYear = selected.getFullYear();
417
+ // Reset right calendar to original position (next month after left) when end date is cleared
418
+ this.rightMonth = this.leftMonth + 1;
419
+ this.rightYear = this.leftYear;
420
+ if (this.rightMonth > 11) {
421
+ this.rightMonth = 0;
422
+ this.rightYear++;
423
+ }
424
+ this.generateDualCalendars();
425
+ }
426
+ // Don't overwrite time picker values - keep current values and apply them to the date
427
+ // Time picker values are already set by user, we just apply them to the selected date
428
+ } else {
429
+ if (selected < this.startDate && !this.customRangeDirection) {
430
+ this.endDate = this.startDate;
431
+ this.startDate = selected;
432
+ // Activate Custom Range when manually selecting dates
433
+ this.activeRange = 'Custom Range';
434
+ // Swap times if needed
435
+ if (this.dualCalendar && this.enableTimepicker) {
436
+ [this.startHour, this.endHour] = [this.endHour, this.startHour];
437
+ [this.startMinute, this.endMinute] = [this.endMinute, this.startMinute];
438
+ [this.startSecond, this.endSecond] = [this.endSecond, this.startSecond];
439
+ [this.startAMPM, this.endAMPM] = [this.endAMPM, this.startAMPM];
440
+ }
441
+ // Keep left calendar on the selected month
442
+ if (this.dualCalendar) {
443
+ this.leftMonth = selected.getMonth();
444
+ this.leftYear = selected.getFullYear();
445
+ this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
446
+ }
447
+ } else {
448
+ this.endDate = selected;
449
+ // Activate Custom Range when manually selecting dates
450
+ this.activeRange = 'Custom Range';
451
+ // Only move right calendar if end date is in a different month than start date
452
+ if (this.dualCalendar) {
453
+ if (this.startDate) {
454
+ const startMonth = this.startDate.getMonth();
455
+ const startYear = this.startDate.getFullYear();
456
+ const endMonth = selected.getMonth();
457
+ const endYear = selected.getFullYear();
458
+
459
+ // Only move right calendar if end date is in a different month
460
+ if (endMonth !== startMonth || endYear !== startYear) {
461
+ this.rightMonth = endMonth;
462
+ this.rightYear = endYear;
463
+ this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
464
+ }
465
+ // If both dates are in same month, keep right calendar in its current position
466
+ } else {
467
+ // If no start date, move right calendar to end date month
468
+ this.rightMonth = selected.getMonth();
469
+ this.rightYear = selected.getFullYear();
470
+ this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
471
+ }
472
+ }
473
+ // Don't overwrite time picker values - keep current end time values
474
+ // Time picker values are already set by user
475
+ }
476
+ if (this.autoApply) {
477
+ this.apply();
478
+ if (this.closeOnAutoApply && !this.inline) this.close();
479
+ } else {
480
+ // Check if the selection matches a predefined range
481
+ this.checkAndSetActiveRange();
482
+ // Always emit selection event for inline calendars
483
+ if (this.inline) {
484
+ this.emitSelection();
485
+ }
486
+ }
487
+ }
488
+ }
489
+
490
+ handleMultiDateSelection(selected: Date) {
491
+ const dateStr = this.getDateString(selected);
492
+ const existingIndex = this.selectedDates.findIndex(d => this.getDateString(d) === dateStr);
493
+
494
+ if (existingIndex >= 0) {
495
+ // Deselect if already selected
496
+ this.selectedDates.splice(existingIndex, 1);
497
+ } else {
498
+ // Add to selection
499
+ this.selectedDates.push(new Date(selected));
500
+ this.selectedDates.sort((a, b) => a.getTime() - b.getTime());
501
+ }
502
+
503
+ // Update startDate and endDate for compatibility
504
+ if (this.selectedDates.length > 0) {
505
+ this.startDate = new Date(this.selectedDates[0]);
506
+ this.endDate = new Date(this.selectedDates[this.selectedDates.length - 1]);
507
+ // Activate Custom Range when manually selecting dates
508
+ this.activeRange = 'Custom Range';
509
+ } else {
510
+ this.startDate = null;
511
+ this.endDate = null;
512
+ this.activeRange = null;
513
+ }
514
+
515
+ // Always emit selection event for inline calendars or when autoApply is true
516
+ if (this.autoApply || this.inline) {
517
+ this.emitSelection();
518
+ if (this.closeOnAutoApply && !this.inline) this.close();
519
+ }
520
+ }
521
+
522
+ getDateString(date: Date): string {
523
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
524
+ }
525
+
526
+ isDateInMultiSelection(year: number, month: number, day: number): boolean {
527
+ if (!this.multiDateSelection || this.selectedDates.length === 0) return false;
528
+ const cellDate = new Date(year, month, day);
529
+ return this.selectedDates.some(d => this.getDateString(d) === this.getDateString(cellDate));
530
+ }
531
+
532
+ apply() {
533
+ // Format minute inputs to 2 digits before applying
534
+ this.formatAllMinuteInputs();
535
+
536
+ // Apply time to dates
537
+ if (this.enableTimepicker) {
538
+ if (this.dualCalendar) {
539
+ // Dual calendar with separate start/end times (always 12-hour format)
540
+ if (this.startDate) {
541
+ this.applyTimeToDate(this.startDate, true);
542
+ }
543
+ if (this.endDate) {
544
+ this.applyTimeToDate(this.endDate, false);
545
+ }
546
+ } else {
547
+ // Single calendar with time (always 12-hour format)
548
+ if (this.startDate) {
549
+ this.applyTimeToDate(this.startDate, true);
550
+ }
551
+ if (this.endDate && !this.singleDatePicker) {
552
+ this.applyTimeToDate(this.endDate, true);
553
+ }
554
+ }
555
+ }
556
+
557
+ // Check if the selection matches a predefined range
558
+ this.checkAndSetActiveRange();
559
+
560
+ this.emitSelection();
561
+ this.disableHighlight = true;
562
+ this.close();
563
+ }
564
+
565
+ cancel() {
566
+ this.startDate = null;
567
+ this.endDate = null;
568
+ this.selectedDates = [];
569
+ this.close();
570
+ }
571
+
572
+ clear() {
573
+ this.startDate = null;
574
+ this.endDate = null;
575
+ this.selectedDates = [];
576
+ this.activeRange = null; // Clear active range
577
+
578
+ // Reset right calendar to original position (next month after left) when end date is cleared
579
+ if (this.dualCalendar && !this.endDate) {
580
+ this.rightMonth = this.leftMonth + 1;
581
+ this.rightYear = this.leftYear;
582
+ if (this.rightMonth > 11) {
583
+ this.rightMonth = 0;
584
+ this.rightYear++;
585
+ }
586
+ this.generateDualCalendars();
587
+ }
588
+
589
+ this.emitSelection();
590
+ }
591
+
592
+ chooseRange(key: string) {
593
+ if (!this.customRanges) return;
594
+ // Don't allow selecting "Custom Range" directly - it's only activated when manually selecting dates
595
+ if (key === 'Custom Range') return;
596
+ const r = this.customRanges[key];
597
+ if (!r) return;
598
+ this.startDate = new Date(r.start);
599
+ this.endDate = new Date(r.end);
600
+ this.selectedDates = [];
601
+ this.activeRange = key; // Set active range
602
+
603
+ // Navigate calendars to show the selected date range
604
+ if (this.dualCalendar) {
605
+ // For dual calendar: left always shows start date month
606
+ if (this.startDate) {
607
+ this.leftMonth = this.startDate.getMonth();
608
+ this.leftYear = this.startDate.getFullYear();
609
+ }
610
+
611
+ // Right calendar logic
612
+ if (this.endDate && this.startDate) {
613
+ const startMonth = this.startDate.getMonth();
614
+ const startYear = this.startDate.getFullYear();
615
+ const endMonth = this.endDate.getMonth();
616
+ const endYear = this.endDate.getFullYear();
617
+
618
+ // Only move right calendar if end date is in a different month than start date
619
+ if (endMonth !== startMonth || endYear !== startYear) {
620
+ this.rightMonth = endMonth;
621
+ this.rightYear = endYear;
622
+ } else {
623
+ // If both dates are in same month, reset right calendar to default position (next month after left)
624
+ this.rightMonth = this.leftMonth + 1;
625
+ this.rightYear = this.leftYear;
626
+ if (this.rightMonth > 11) {
627
+ this.rightMonth = 0;
628
+ this.rightYear++;
629
+ }
630
+ }
631
+ } else if (this.endDate && !this.startDate) {
632
+ // If only end date exists, show it in right calendar
633
+ this.rightMonth = this.endDate.getMonth();
634
+ this.rightYear = this.endDate.getFullYear();
635
+ } else {
636
+ // If no end date, reset right calendar to default position
637
+ this.rightMonth = this.leftMonth + 1;
638
+ this.rightYear = this.leftYear;
639
+ if (this.rightMonth > 11) {
640
+ this.rightMonth = 0;
641
+ this.rightYear++;
642
+ }
643
+ }
644
+
645
+ this.generateDualCalendars();
646
+ } else {
647
+ // For single calendar: show the start date month (or end date if only end date exists)
648
+ if (this.startDate) {
649
+ this.month = this.startDate.getMonth();
650
+ this.year = this.startDate.getFullYear();
651
+ } else if (this.endDate) {
652
+ this.month = this.endDate.getMonth();
653
+ this.year = this.endDate.getFullYear();
654
+ }
655
+ this.generateCalendar();
656
+ }
657
+
658
+ this.emitSelection();
659
+ if (this.autoApply || this.closeOnAutoApply) {
660
+ this.close();
661
+ }
662
+ }
663
+
664
+ // emitSelection() {
665
+ // const selection: CalendarSelection = {
666
+ // startDate: this.startDate,
667
+ // endDate: this.endDate
668
+ // };
669
+ // if (this.multiDateSelection) {
670
+ // selection.selectedDates = [...this.selectedDates];
671
+ // }
672
+ // this.selected.emit(selection);
673
+ // }
674
+
675
+ emitSelection() {
676
+ const selection: CalendarSelection = {
677
+ startDate: this.startDate ? this.formatDateToString(this.startDate) : null,
678
+ endDate: this.endDate ? this.formatDateToString(this.endDate) : null,
679
+ };
680
+
681
+ if (this.multiDateSelection && this.selectedDates.length > 0) {
682
+ selection.selectedDates = this.selectedDates.map(d => this.formatDateToString(d));
683
+ }
684
+
685
+ this.selected.emit(selection);
686
+ }
687
+
688
+ addDays(date: Date, days: number): Date {
689
+ const d = new Date(date);
690
+ d.setDate(d.getDate() + days);
691
+ return d;
692
+ }
693
+
694
+ generateCalendar() {
695
+ this.calendar = this.buildCalendar(this.year, this.month);
696
+ }
697
+
698
+ nextMonth() {
699
+ if (!this.dualCalendar) {
700
+ this.month++;
701
+ if (this.month > 11) { this.month = 0; this.year++; }
702
+ this.generateCalendar();
703
+ return;
704
+ }
705
+ // For dual calendar, this should not be used - use nextLeftMonth or nextRightMonth instead
706
+ this.nextLeftMonth();
707
+ }
708
+
709
+ prevMonth() {
710
+ if (!this.dualCalendar) {
711
+ this.month--;
712
+ if (this.month < 0) { this.month = 11; this.year--; }
713
+ this.generateCalendar();
714
+ return;
715
+ }
716
+ // For dual calendar, this should not be used - use prevLeftMonth or prevRightMonth instead
717
+ this.prevLeftMonth();
718
+ }
719
+
720
+ // Independent navigation for left calendar
721
+ nextLeftMonth() {
722
+ this.leftMonth++;
723
+ if (this.leftMonth > 11) { this.leftMonth = 0; this.leftYear++; }
724
+ this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
725
+ }
726
+
727
+ prevLeftMonth() {
728
+ this.leftMonth--;
729
+ if (this.leftMonth < 0) { this.leftMonth = 11; this.leftYear--; }
730
+ this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
731
+ }
732
+
733
+ // Independent navigation for right calendar
734
+ nextRightMonth() {
735
+ this.rightMonth++;
736
+ if (this.rightMonth > 11) { this.rightMonth = 0; this.rightYear++; }
737
+ this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
738
+ }
739
+
740
+ prevRightMonth() {
741
+ this.rightMonth--;
742
+ if (this.rightMonth < 0) { this.rightMonth = 11; this.rightYear--; }
743
+ this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
744
+ }
745
+
746
+ initializeDual() {
747
+ this.leftMonth = this.today.getMonth();
748
+ this.leftYear = this.today.getFullYear();
749
+ // Initialize right calendar to next month, but they can move independently
750
+ this.rightMonth = this.leftMonth + 1;
751
+ this.rightYear = this.leftYear;
752
+ if (this.rightMonth > 11) { this.rightMonth = 0; this.rightYear++; }
753
+ this.generateDualCalendars();
754
+ }
755
+
756
+ generateDualCalendars() {
757
+ this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
758
+ this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
759
+ }
760
+
761
+ buildCalendar(year: number, month: number): { day: number, currentMonth: boolean }[][] {
762
+ const firstDay = new Date(year, month, 1).getDay();
763
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
764
+ const prevMonthDays = new Date(year, month, 0).getDate();
765
+
766
+ const grid: { day: number, currentMonth: boolean }[][] = [];
767
+ let row: { day: number, currentMonth: boolean }[] = [];
768
+
769
+ // Adjust first day (0 = Sunday, 1 = Monday, etc.)
770
+ const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1; // Make Monday = 0
771
+
772
+ for (let i = adjustedFirstDay - 1; i >= 0; i--) {
773
+ row.push({ day: prevMonthDays - i, currentMonth: false });
774
+ }
775
+ for (let d = 1; d <= daysInMonth; d++) {
776
+ row.push({ day: d, currentMonth: true });
777
+ if (row.length === 7) { grid.push(row); row = []; }
778
+ }
779
+ let nextMonthDay = 1;
780
+ while (row.length > 0 && row.length < 7) {
781
+ row.push({ day: nextMonthDay++, currentMonth: false });
782
+ }
783
+ if (row.length) grid.push(row);
784
+
785
+ // Ensure we always have 6 rows (42 cells total) for consistent layout
786
+ while (grid.length < 6) {
787
+ const newRow: { day: number, currentMonth: boolean }[] = [];
788
+ for (let i = 0; i < 7; i++) {
789
+ newRow.push({ day: nextMonthDay++, currentMonth: false });
790
+ }
791
+ grid.push(newRow);
792
+ }
793
+
794
+ return grid;
795
+ }
796
+
797
+ isDateSelected(year: number, month: number, day: number): boolean {
798
+ if (this.disableHighlight) return false;
799
+ if (!day) return false;
800
+
801
+ // Multi-date selection
802
+ if (this.multiDateSelection) {
803
+ return this.isDateInMultiSelection(year, month, day);
804
+ }
805
+
806
+ const cellDate = new Date(year, month, day);
807
+
808
+ // Check if it's today (highlight today by default if no date selected)
809
+ const today = new Date();
810
+ const isToday = cellDate.getFullYear() === today.getFullYear() &&
811
+ cellDate.getMonth() === today.getMonth() &&
812
+ cellDate.getDate() === today.getDate();
813
+
814
+ // If no startDate is set and it's today, highlight it
815
+ if (!this.startDate && isToday) {
816
+ return true;
817
+ }
818
+
819
+ if (!this.startDate) return false;
820
+
821
+ // Check if date is disabled
822
+ if (this.minDate && cellDate < this.minDate) return false;
823
+ if (this.maxDate && cellDate > this.maxDate) return false;
824
+
825
+ const sameDay =
826
+ cellDate.getFullYear() === this.startDate.getFullYear() &&
827
+ cellDate.getMonth() === this.startDate.getMonth() &&
828
+ cellDate.getDate() === this.startDate.getDate();
829
+
830
+ if (this.singleDatePicker) return sameDay;
831
+
832
+ // For range selection: only highlight start and end dates (not in-between)
833
+ if (this.startDate && this.endDate) {
834
+ const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
835
+ const end = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate());
836
+ return cellDate.getTime() === start.getTime() || cellDate.getTime() === end.getTime();
837
+ }
838
+
839
+ // If only start date is selected and hovering, check if this is start or hovered end
840
+ if (this.startDate && !this.endDate && this.hoveredDate) {
841
+ const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
842
+ const hovered = new Date(this.hoveredDate.getFullYear(), this.hoveredDate.getMonth(), this.hoveredDate.getDate());
843
+ // Show both start and hovered date as selected (circular black)
844
+ return cellDate.getTime() === start.getTime() || cellDate.getTime() === hovered.getTime();
845
+ }
846
+
847
+ return sameDay;
848
+ }
849
+
850
+ isDateInRange(year: number, month: number, day: number): boolean {
851
+ if (this.disableHighlight || !day) return false;
852
+ if (this.singleDatePicker) return false;
853
+ if (this.multiDateSelection) return false;
854
+
855
+ const cellDate = new Date(year, month, day);
856
+
857
+ // If both start and end are selected, show gray background for dates in between
858
+ if (this.startDate && this.endDate) {
859
+ const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
860
+ const end = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate());
861
+ return cellDate > start && cellDate < end;
862
+ }
863
+
864
+ // If only start is selected and hovering, show preview range
865
+ if (this.startDate && !this.endDate && this.hoveredDate) {
866
+ const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
867
+ const hovered = new Date(this.hoveredDate.getFullYear(), this.hoveredDate.getMonth(), this.hoveredDate.getDate());
868
+
869
+ // Determine which is earlier - show gray background for dates between them
870
+ const minDate = hovered < start ? hovered : start;
871
+ const maxDate = hovered >= start ? hovered : start;
872
+ return cellDate > minDate && cellDate < maxDate;
873
+ }
874
+
875
+ return false;
876
+ }
877
+
878
+ isDateDisabled(year: number, month: number, day: number): boolean {
879
+ if (!day) return false;
880
+ const cellDate = new Date(year, month, day);
881
+ if (this.minDate && cellDate < this.minDate) return true;
882
+ if (this.maxDate && cellDate > this.maxDate) return true;
883
+ return false;
884
+ }
885
+
886
+ isToday(year: number, month: number, day: number): boolean {
887
+ if (!day) return false;
888
+ const today = new Date();
889
+ const cellDate = new Date(year, month, day);
890
+ return cellDate.getFullYear() === today.getFullYear() &&
891
+ cellDate.getMonth() === today.getMonth() &&
892
+ cellDate.getDate() === today.getDate();
893
+ }
894
+
895
+ getDisplayValue(): string {
896
+ if (this.multiDateSelection && this.selectedDates.length > 0) {
897
+ if (this.selectedDates.length === 1) {
898
+ return moment(this.selectedDates[0]).format(this.displayFormat);
899
+ }
900
+ return `${this.selectedDates.length} dates selected`;
901
+ }
902
+
903
+ if (!this.startDate) return '';
904
+
905
+ // Prefer moment formatting for consistent display
906
+ let dateStr = moment(this.startDate).format(this.displayFormat);
907
+
908
+ if (this.enableTimepicker && !this.dualCalendar) {
909
+ const hr = this.startDate.getHours().toString().padStart(2, '0');
910
+ const min = this.startDate.getMinutes().toString().padStart(2, '0');
911
+ dateStr += ` ${hr}:${min}`;
912
+ if (this.enableSeconds) {
913
+ const sec = this.startDate.getSeconds().toString().padStart(2, '0');
914
+ dateStr += `:${sec}`;
915
+ }
916
+ }
917
+
918
+ if (this.endDate && !this.singleDatePicker) {
919
+ let endStr = moment(this.endDate).format(this.displayFormat);
920
+ if (this.enableTimepicker) {
921
+ if (this.dualCalendar) {
922
+ const startHr = this.startDate.getHours().toString().padStart(2, '0');
923
+ const startMin = this.startDate.getMinutes().toString().padStart(2, '0');
924
+ dateStr += ` ${startHr}:${startMin}`;
925
+ if (this.enableSeconds) {
926
+ const startSec = this.startDate.getSeconds().toString().padStart(2, '0');
927
+ dateStr += `:${startSec}`;
928
+ }
929
+ }
930
+ const endHr = this.endDate.getHours().toString().padStart(2, '0');
931
+ const endMin = this.endDate.getMinutes().toString().padStart(2, '0');
932
+ endStr += ` ${endHr}:${endMin}`;
933
+ if (this.enableSeconds) {
934
+ const endSec = this.endDate.getSeconds().toString().padStart(2, '0');
935
+ endStr += `:${endSec}`;
936
+ }
937
+ }
938
+ return `${dateStr} - ${endStr}`;
939
+ }
940
+ return dateStr;
941
+ }
942
+
943
+ // Time picker helpers
944
+ getTimeInputValue(isStart = true): string {
945
+ const h = isStart ? this.startHour : this.endHour;
946
+ const m = isStart ? this.startMinute : this.endMinute;
947
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
948
+ }
949
+
950
+ getSingleTimeInputValue(): string {
951
+ return `${this.selectedHour.toString().padStart(2, '0')}:${this.selectedMinute.toString().padStart(2, '0')}`;
952
+ }
953
+
954
+ // NEW: Helper to build display value for BkTimePicker (single calendar)
955
+ getSingleTimePickerDisplay(): string {
956
+ const hour = this.selectedHour || 12;
957
+ const minuteStr = this.selectedMinute.toString().padStart(2, '0');
958
+ const ampm = this.selectedAMPM || 'AM';
959
+ return `${hour}:${minuteStr} ${ampm}`;
960
+ }
961
+
962
+ // NEW: Helper to build display value for BkTimePicker (dual calendar)
963
+ getDualTimePickerDisplay(isStart = true): string {
964
+ const hour = isStart ? (this.startHour || 12) : (this.endHour || 12);
965
+ const minute = isStart ? this.startMinute : this.endMinute;
966
+ const ampm = isStart ? (this.startAMPM || 'AM') : (this.endAMPM || 'AM');
967
+ const minuteStr = minute.toString().padStart(2, '0');
968
+ return `${hour}:${minuteStr} ${ampm}`;
969
+ }
970
+
971
+ // Coordination helpers for embedded BkTimePicker instances
972
+ onTimePickerOpened(pickerId: string) {
973
+ // Close previously open picker inside this calendar
974
+ if (this.openTimePickerId && this.openTimePickerId !== pickerId) {
975
+ if (!this.closePickerCounter[this.openTimePickerId]) {
976
+ this.closePickerCounter[this.openTimePickerId] = 0;
977
+ }
978
+ this.closePickerCounter[this.openTimePickerId]++;
979
+ }
980
+ this.openTimePickerId = pickerId;
981
+ }
982
+
983
+ onTimePickerClosed(pickerId: string) {
984
+ if (this.openTimePickerId === pickerId) {
985
+ this.openTimePickerId = null;
986
+ }
987
+ }
988
+
989
+ shouldClosePicker(pickerId: string): number {
990
+ return this.closePickerCounter[pickerId] || 0;
991
+ }
992
+
993
+ // NEW: Parse "H:MM AM/PM" (or "HH:MM" 24h) from BkTimePicker
994
+ private parsePickerTimeString(timeStr: string): { hour12: number; minute: number; ampm: 'AM' | 'PM' } {
995
+ if (!timeStr) {
996
+ return { hour12: 12, minute: 0, ampm: 'AM' };
997
+ }
998
+
999
+ const parts = timeStr.trim().split(' ');
1000
+ const timePart = parts[0] || '12:00';
1001
+ let ampmPart = (parts[1] || '').toUpperCase();
1002
+
1003
+ const [hourStr, minuteStr] = timePart.split(':');
1004
+ let hour = parseInt(hourStr || '12', 10);
1005
+ const minute = parseInt(minuteStr || '0', 10);
1006
+
1007
+ if (ampmPart !== 'AM' && ampmPart !== 'PM') {
1008
+ // Interpret as 24-hour input and convert
1009
+ if (hour >= 12) {
1010
+ ampmPart = 'PM';
1011
+ if (hour > 12) hour -= 12;
1012
+ } else {
1013
+ ampmPart = 'AM';
1014
+ if (hour === 0) hour = 12;
1015
+ }
1016
+ }
1017
+
1018
+ // Clamp to 1-12 range just in case
1019
+ if (hour < 1) hour = 1;
1020
+ if (hour > 12) hour = 12;
1021
+
1022
+ return { hour12: hour, minute, ampm: ampmPart as 'AM' | 'PM' };
1023
+ }
1024
+
1025
+ // NEW: Handle BkTimePicker change for single calendar
1026
+ onSingleTimePickerChange(time: string) {
1027
+ const { hour12, minute, ampm } = this.parsePickerTimeString(time);
1028
+
1029
+ this.selectedHour = hour12;
1030
+ this.selectedMinute = minute;
1031
+ this.selectedAMPM = ampm;
1032
+
1033
+ if (this.startDate) {
1034
+ let h24 = hour12;
1035
+ if (ampm === 'PM' && h24 < 12) h24 += 12;
1036
+ if (ampm === 'AM' && h24 === 12) h24 = 0;
1037
+ this.startDate.setHours(h24, minute, this.selectedSecond);
1038
+ this.emitSelection();
1039
+ }
1040
+ }
1041
+
1042
+ // NEW: Handle BkTimePicker change for dual calendar
1043
+ onDualTimePickerChange(time: string, isStart = true) {
1044
+ const { hour12, minute, ampm } = this.parsePickerTimeString(time);
1045
+
1046
+ if (isStart) {
1047
+ this.startHour = hour12;
1048
+ this.startMinute = minute;
1049
+ this.startAMPM = ampm;
1050
+
1051
+ if (this.startDate) {
1052
+ let h24 = hour12;
1053
+ if (ampm === 'PM' && h24 < 12) h24 += 12;
1054
+ if (ampm === 'AM' && h24 === 12) h24 = 0;
1055
+ this.startDate.setHours(h24, minute, this.startSecond);
1056
+ }
1057
+ } else {
1058
+ this.endHour = hour12;
1059
+ this.endMinute = minute;
1060
+ this.endAMPM = ampm;
1061
+
1062
+ if (this.endDate) {
1063
+ let h24 = hour12;
1064
+ if (ampm === 'PM' && h24 < 12) h24 += 12;
1065
+ if (ampm === 'AM' && h24 === 12) h24 = 0;
1066
+ this.endDate.setHours(h24, minute, this.endSecond);
1067
+ }
1068
+ }
1069
+
1070
+ this.emitSelection();
1071
+ }
1072
+
1073
+ onTimeChange(event: any, isStart = true) {
1074
+ const [h, m] = event.target.value.split(':').map(Number);
1075
+ if (isStart) {
1076
+ this.startHour = h;
1077
+ this.startMinute = m;
1078
+ if (this.startDate) {
1079
+ this.startDate.setHours(h, m, this.startSecond);
1080
+ this.emitSelection();
1081
+ }
1082
+ } else {
1083
+ this.endHour = h;
1084
+ this.endMinute = m;
1085
+ if (this.endDate) {
1086
+ this.endDate.setHours(h, m, this.endSecond);
1087
+ this.emitSelection();
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ onSingleTimeChange(event: any) {
1093
+ const [h, m] = event.target.value.split(':').map(Number);
1094
+ this.selectedHour = h;
1095
+ this.selectedMinute = m;
1096
+ if (this.startDate) {
1097
+ this.startDate.setHours(h, m, this.selectedSecond);
1098
+ this.emitSelection();
1099
+ }
1100
+ }
1101
+
1102
+ // Custom time picker controls
1103
+ incrementHour(isStart = true) {
1104
+ // 12-hour format: 1-12
1105
+ if (isStart) {
1106
+ this.startHour = this.startHour >= 12 ? 1 : this.startHour + 1;
1107
+ // Toggle AM/PM at 12
1108
+ if (this.startHour === 12) {
1109
+ this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
1110
+ }
1111
+ if (this.startDate) {
1112
+ let h = this.startHour;
1113
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1114
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1115
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1116
+ }
1117
+ } else {
1118
+ this.endHour = this.endHour >= 12 ? 1 : this.endHour + 1;
1119
+ // Toggle AM/PM at 12
1120
+ if (this.endHour === 12) {
1121
+ this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
1122
+ }
1123
+ if (this.endDate) {
1124
+ let h = this.endHour;
1125
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1126
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1127
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1128
+ }
1129
+ }
1130
+ this.emitSelection();
1131
+ }
1132
+
1133
+ decrementHour(isStart = true) {
1134
+ // 12-hour format: 1-12
1135
+ if (isStart) {
1136
+ this.startHour = this.startHour <= 1 ? 12 : this.startHour - 1;
1137
+ // Toggle AM/PM at 12
1138
+ if (this.startHour === 12) {
1139
+ this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
1140
+ }
1141
+ if (this.startDate) {
1142
+ let h = this.startHour;
1143
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1144
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1145
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1146
+ }
1147
+ } else {
1148
+ this.endHour = this.endHour <= 1 ? 12 : this.endHour - 1;
1149
+ // Toggle AM/PM at 12
1150
+ if (this.endHour === 12) {
1151
+ this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
1152
+ }
1153
+ if (this.endDate) {
1154
+ let h = this.endHour;
1155
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1156
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1157
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1158
+ }
1159
+ }
1160
+ this.emitSelection();
1161
+ }
1162
+
1163
+ incrementMinute(isStart = true) {
1164
+ if (isStart) {
1165
+ this.startMinute = (this.startMinute + 1) % 60;
1166
+ if (this.startDate) {
1167
+ let h = this.startHour;
1168
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1169
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1170
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1171
+ }
1172
+ } else {
1173
+ this.endMinute = (this.endMinute + 1) % 60;
1174
+ if (this.endDate) {
1175
+ let h = this.endHour;
1176
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1177
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1178
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1179
+ }
1180
+ }
1181
+ this.emitSelection();
1182
+ }
1183
+
1184
+ decrementMinute(isStart = true) {
1185
+ if (isStart) {
1186
+ this.startMinute = this.startMinute <= 0 ? 59 : this.startMinute - 1;
1187
+ if (this.startDate) {
1188
+ let h = this.startHour;
1189
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1190
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1191
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1192
+ }
1193
+ } else {
1194
+ this.endMinute = this.endMinute <= 0 ? 59 : this.endMinute - 1;
1195
+ if (this.endDate) {
1196
+ let h = this.endHour;
1197
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1198
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1199
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1200
+ }
1201
+ }
1202
+ this.emitSelection();
1203
+ }
1204
+
1205
+ toggleAMPM(isStart = true) {
1206
+ if (isStart) {
1207
+ this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
1208
+ if (this.startDate) {
1209
+ let h = this.startHour;
1210
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1211
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1212
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1213
+ }
1214
+ } else {
1215
+ this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
1216
+ if (this.endDate) {
1217
+ let h = this.endHour;
1218
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1219
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1220
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1221
+ }
1222
+ }
1223
+ this.emitSelection();
1224
+ }
1225
+
1226
+ // Single calendar time picker controls (12-hour format: 1-12)
1227
+ incrementSingleHour() {
1228
+ this.selectedHour = this.selectedHour >= 12 ? 1 : this.selectedHour + 1;
1229
+ // Toggle AM/PM at 12
1230
+ if (this.selectedHour === 12) {
1231
+ this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
1232
+ }
1233
+ if (this.startDate) {
1234
+ let h = this.selectedHour;
1235
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1236
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1237
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1238
+ this.emitSelection();
1239
+ }
1240
+ }
1241
+
1242
+ decrementSingleHour() {
1243
+ this.selectedHour = this.selectedHour <= 1 ? 12 : this.selectedHour - 1;
1244
+ // Toggle AM/PM at 12
1245
+ if (this.selectedHour === 12) {
1246
+ this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
1247
+ }
1248
+ if (this.startDate) {
1249
+ let h = this.selectedHour;
1250
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1251
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1252
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1253
+ this.emitSelection();
1254
+ }
1255
+ }
1256
+
1257
+ incrementSingleMinute() {
1258
+ this.selectedMinute = (this.selectedMinute + 1) % 60;
1259
+ if (this.startDate) {
1260
+ let h = this.selectedHour;
1261
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1262
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1263
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1264
+ this.emitSelection();
1265
+ }
1266
+ }
1267
+
1268
+ decrementSingleMinute() {
1269
+ this.selectedMinute = this.selectedMinute <= 0 ? 59 : this.selectedMinute - 1;
1270
+ if (this.startDate) {
1271
+ let h = this.selectedHour;
1272
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1273
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1274
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1275
+ this.emitSelection();
1276
+ }
1277
+ }
1278
+
1279
+ toggleSingleAMPM() {
1280
+ this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
1281
+ if (this.startDate) {
1282
+ let h = this.selectedHour;
1283
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1284
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1285
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1286
+ this.emitSelection();
1287
+ }
1288
+ }
1289
+
1290
+ getMonthName(month: number): string {
1291
+ const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
1292
+ return months[month];
1293
+ }
1294
+
1295
+ // Input handlers for direct hour/minute input (12-hour format only)
1296
+ onHourInput(event: any, isStart = true, isSingle = false): void {
1297
+ const inputValue = event.target.value;
1298
+
1299
+ // Allow empty input while typing
1300
+ if (inputValue === '' || inputValue === null || inputValue === undefined) {
1301
+ return;
1302
+ }
1303
+
1304
+ let value = parseInt(inputValue) || 0;
1305
+
1306
+ // Validate: 1-12 for 12-hour format
1307
+ if (value < 1) value = 1;
1308
+ if (value > 12) value = 12;
1309
+
1310
+ if (isSingle) {
1311
+ this.selectedHour = value;
1312
+ if (this.startDate) {
1313
+ let h = value;
1314
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1315
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1316
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1317
+ this.emitSelection();
1318
+ }
1319
+ } else if (isStart) {
1320
+ this.startHour = value;
1321
+ if (this.startDate) {
1322
+ let h = value;
1323
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1324
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1325
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1326
+ this.emitSelection();
1327
+ }
1328
+ } else {
1329
+ this.endHour = value;
1330
+ if (this.endDate) {
1331
+ let h = value;
1332
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1333
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1334
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1335
+ this.emitSelection();
1336
+ }
1337
+ }
1338
+
1339
+ // Don't format during input, only on blur
1340
+ event.target.value = value.toString();
1341
+ }
1342
+
1343
+ onHourBlur(event: any, isStart = true, isSingle = false): void {
1344
+ const inputValue = event.target.value;
1345
+ if (inputValue === '' || inputValue === null || inputValue === undefined) {
1346
+ // If empty, set to current value
1347
+ const currentValue = isSingle ? this.selectedHour : (isStart ? this.startHour : this.endHour);
1348
+ event.target.value = currentValue.toString();
1349
+ return;
1350
+ }
1351
+
1352
+ let value = parseInt(inputValue) || 0;
1353
+ if (value < 1) value = 1;
1354
+ if (value > 12) value = 12;
1355
+
1356
+ // Format to single digit (no padding for hours in 12-hour format)
1357
+ event.target.value = value.toString();
1358
+
1359
+ // Update the value
1360
+ if (isSingle) {
1361
+ this.selectedHour = value;
1362
+ if (this.startDate) {
1363
+ let h = value;
1364
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1365
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1366
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1367
+ this.emitSelection();
1368
+ }
1369
+ } else if (isStart) {
1370
+ this.startHour = value;
1371
+ if (this.startDate) {
1372
+ let h = value;
1373
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1374
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1375
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1376
+ this.emitSelection();
1377
+ }
1378
+ } else {
1379
+ this.endHour = value;
1380
+ if (this.endDate) {
1381
+ let h = value;
1382
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1383
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1384
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1385
+ this.emitSelection();
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ onMinuteInput(event: any, isStart = true, isSingle = false): void {
1391
+ const inputValue = event.target.value;
1392
+ const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
1393
+
1394
+ // Remove any non-digit characters
1395
+ const digitsOnly = inputValue.replace(/\D/g, '');
1396
+
1397
+ // Store raw input value for display
1398
+ this.minuteInputValues[key] = digitsOnly;
1399
+
1400
+ // Allow empty input while typing
1401
+ if (digitsOnly === '' || digitsOnly === null || digitsOnly === undefined) {
1402
+ return; // Don't modify the input, let user clear it
1403
+ }
1404
+
1405
+ // Allow typing up to 2 digits without formatting
1406
+ let value = parseInt(digitsOnly) || 0;
1407
+
1408
+ // If user types more than 2 digits, take only first 2
1409
+ if (digitsOnly.length > 2) {
1410
+ value = parseInt(digitsOnly.substring(0, 2));
1411
+ this.minuteInputValues[key] = digitsOnly.substring(0, 2);
1412
+ event.target.value = digitsOnly.substring(0, 2);
1413
+ }
1414
+
1415
+ // If value exceeds 59, clamp it to 59
1416
+ if (value > 59) {
1417
+ value = 59;
1418
+ this.minuteInputValues[key] = '59';
1419
+ event.target.value = '59';
1420
+ }
1421
+
1422
+ // Update the internal value silently (don't emit during typing to avoid re-rendering)
1423
+ if (isSingle) {
1424
+ this.selectedMinute = value;
1425
+ } else if (isStart) {
1426
+ this.startMinute = value;
1427
+ } else {
1428
+ this.endMinute = value;
1429
+ }
1430
+
1431
+ // Don't update dates or emit during typing - wait for blur or apply
1432
+ }
1433
+
1434
+ onMinuteBlur(event: any, isStart = true, isSingle = false): void {
1435
+ const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
1436
+ const inputValue = event.target.value;
1437
+
1438
+ if (inputValue === '' || inputValue === null || inputValue === undefined) {
1439
+ // If empty, set to current value
1440
+ const currentValue = isSingle ? this.selectedMinute : (isStart ? this.startMinute : this.endMinute);
1441
+ event.target.value = currentValue.toString().padStart(2, '0');
1442
+ delete this.minuteInputValues[key];
1443
+ return;
1444
+ }
1445
+
1446
+ const digitsOnly = inputValue.replace(/\D/g, '');
1447
+ let value = parseInt(digitsOnly) || 0;
1448
+ if (value < 0) value = 0;
1449
+ if (value > 59) value = 59;
1450
+
1451
+ // Format to 2 digits on blur (01-09 becomes 01-09, 10-59 stays as is)
1452
+ event.target.value = value.toString().padStart(2, '0');
1453
+ delete this.minuteInputValues[key]; // Clear raw input, use formatted value
1454
+
1455
+ // Update the value
1456
+ if (isSingle) {
1457
+ this.selectedMinute = value;
1458
+ if (this.startDate) {
1459
+ let h = this.selectedHour;
1460
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1461
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1462
+ this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1463
+ this.emitSelection();
1464
+ }
1465
+ } else if (isStart) {
1466
+ this.startMinute = value;
1467
+ if (this.startDate) {
1468
+ let h = this.startHour;
1469
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1470
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1471
+ this.startDate.setHours(h, this.startMinute, this.startSecond);
1472
+ this.emitSelection();
1473
+ }
1474
+ } else {
1475
+ this.endMinute = value;
1476
+ if (this.endDate) {
1477
+ let h = this.endHour;
1478
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1479
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1480
+ this.endDate.setHours(h, this.endMinute, this.endSecond);
1481
+ this.emitSelection();
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ // Get display value for hour (always 12-hour format)
1487
+ getDisplayHour(hour: number): number {
1488
+ if (hour === 0) return 12;
1489
+ if (hour > 12) return hour - 12;
1490
+ return hour;
1491
+ }
1492
+
1493
+ // Get display value for minute (formatted only when not actively typing)
1494
+ getMinuteDisplayValue(isStart: boolean, isSingle: boolean): string {
1495
+ const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
1496
+ // If user is typing (has raw input), show that, otherwise show formatted value
1497
+ if (this.minuteInputValues[key] !== undefined) {
1498
+ return this.minuteInputValues[key];
1499
+ }
1500
+ // Otherwise return formatted value
1501
+ const value = isSingle ? this.selectedMinute : (isStart ? this.startMinute : this.endMinute);
1502
+ return value.toString().padStart(2, '0');
1503
+ }
1504
+
1505
+ // Helper method to apply time picker values to a date
1506
+ applyTimeToDate(date: Date, isStart: boolean): void {
1507
+ if (this.dualCalendar) {
1508
+ if (isStart) {
1509
+ let h = this.startHour;
1510
+ if (this.startAMPM === 'PM' && h < 12) h += 12;
1511
+ if (this.startAMPM === 'AM' && h === 12) h = 0;
1512
+ date.setHours(h, this.startMinute, this.startSecond);
1513
+ } else {
1514
+ let h = this.endHour;
1515
+ if (this.endAMPM === 'PM' && h < 12) h += 12;
1516
+ if (this.endAMPM === 'AM' && h === 12) h = 0;
1517
+ date.setHours(h, this.endMinute, this.endSecond);
1518
+ }
1519
+ } else {
1520
+ let h = this.selectedHour;
1521
+ if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1522
+ if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1523
+ date.setHours(h, this.selectedMinute, this.selectedSecond);
1524
+ }
1525
+ }
1526
+
1527
+ // Select all text on focus for easy replacement
1528
+ onTimeInputFocus(event: any): void {
1529
+ event.target.select();
1530
+ }
1531
+
1532
+ // Format all minute inputs to 2 digits (called before Apply)
1533
+ formatAllMinuteInputs(): void {
1534
+ // Format minute inputs in the DOM - find all minute inputs and format single digits
1535
+ const inputs = document.querySelectorAll('.time-input');
1536
+ inputs.forEach((input: any) => {
1537
+ const value = parseInt(input.value);
1538
+ // If it's a valid minute value (0-59) and not already formatted (single digit or 2 digits without leading zero)
1539
+ if (!isNaN(value) && value >= 0 && value <= 59) {
1540
+ // Format if it's a single digit (1-9) or if it's 2 digits but the first is not 0
1541
+ if (input.value.length === 1 || (input.value.length === 2 && !input.value.startsWith('0') && value < 10)) {
1542
+ input.value = value.toString().padStart(2, '0');
1543
+ }
1544
+ }
1545
+ });
1546
+ }
1547
+
1548
+ formatDateToString(date: Date): string {
1549
+ const yyyy = date.getFullYear();
1550
+ const mm = (date.getMonth() + 1).toString().padStart(2, '0'); // month is 0-based
1551
+ const dd = date.getDate().toString().padStart(2, '0');
1552
+ return `${yyyy}-${mm}-${dd}`;
1553
+ }
1554
+ }