# @isa/ui/datepicker

A comprehensive date range picker component library for Angular applications with calendar and month/year selection views, form integration, and robust validation.

## Overview

The UI Datepicker library provides a full-featured date range picker for the ISA application. It implements Angular's `ControlValueAccessor` interface for seamless form integration, uses signals for reactive state management, and provides calendar-based date selection with month/year navigation. The library includes dedicated validators for date bounds and range order validation, making it ideal for filtering, reporting, and data entry scenarios requiring date range inputs.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Validation](#validation)
- [Form Integration](#form-integration)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)

## Features

- **Date range selection** - Select start and end dates with automatic ordering
- **ControlValueAccessor integration** - Full Angular forms compatibility (reactive and template-driven)
- **Multiple views** - Calendar view for day selection, month/year view for navigation
- **Signal-based reactivity** - Modern Angular signals for state management
- **Configurable bounds** - Min/max date constraints with injection tokens
- **Auto-sorting** - Automatically orders start/end dates when user selects them
- **Zod schema validation** - Runtime type safety with Zod schemas
- **Custom validators** - Date bounds and range order validators for forms
- **date-fns integration** - Comprehensive date manipulation and formatting
- **Keyboard navigation** - Full keyboard support for accessibility
- **OnPush change detection** - Optimized performance
- **Extensible architecture** - Abstract base class for custom datepicker implementations

## Quick Start

### 1. Import Component

```typescript
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { RangeDatepickerComponent } from '@isa/ui/datepicker';

@Component({
  selector: 'app-date-filter',
  standalone: true,
  imports: [ReactiveFormsModule, RangeDatepickerComponent],
  template: `
    <ui-range-datepicker [formControl]="dateRangeControl" />
  `
})
export class DateFilterComponent {
  dateRangeControl = new FormControl<[Date?, Date?]>([undefined, undefined]);
}
```

### 2. Use with Reactive Forms

```typescript
import { Component } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { RangeDatepickerComponent, dateBoundsValidator, dateRangeStartStopOrderValidator } from '@isa/ui/datepicker';
import { subMonths } from 'date-fns';

@Component({
  selector: 'app-report-filter',
  standalone: true,
  imports: [ReactiveFormsModule, RangeDatepickerComponent],
  template: `
    <form [formGroup]="filterForm">
      <ui-range-datepicker formControlName="dateRange" />
    </form>
  `
})
export class ReportFilterComponent {
  filterForm = this.fb.group({
    dateRange: [
      [undefined, undefined] as [Date?, Date?],
      [
        Validators.required,
        dateRangeStartStopOrderValidator('start', 'stop')
      ]
    ]
  }, {
    validators: [dateRangeStartStopOrderValidator('dateRange', 'dateRange')]
  });

  constructor(private fb: FormBuilder) {}
}
```

### 3. Configure Min/Max Bounds

```typescript
import { Component, Provider } from '@angular/core';
import { RangeDatepickerComponent, UI_DATEPICKER_DEFAULT_MIN, UI_DATEPICKER_DEFAULT_MAX } from '@isa/ui/datepicker';
import { subYears, addYears } from 'date-fns';

const CUSTOM_DATEPICKER_MIN: Provider = {
  provide: UI_DATEPICKER_DEFAULT_MIN,
  useValue: subYears(new Date(), 10) // 10 years in the past
};

const CUSTOM_DATEPICKER_MAX: Provider = {
  provide: UI_DATEPICKER_DEFAULT_MAX,
  useValue: addYears(new Date(), 2) // 2 years in the future
};

@Component({
  selector: 'app-custom-datepicker',
  standalone: true,
  imports: [RangeDatepickerComponent],
  providers: [CUSTOM_DATEPICKER_MIN, CUSTOM_DATEPICKER_MAX],
  template: `<ui-range-datepicker />`
})
export class CustomDatepickerComponent {}
```

### 4. Access Selected Dates

```typescript
import { Component, signal } from '@angular/core';
import { RangeDatepickerComponent } from '@isa/ui/datepicker';

@Component({
  selector: 'app-date-display',
  standalone: true,
  imports: [RangeDatepickerComponent],
  template: `
    <ui-range-datepicker [(ngModel)]="selectedRange" />

    @if (selectedRange()[0] && selectedRange()[1]) {
      <p>Start: {{ selectedRange()[0] | date }}</p>
      <p>End: {{ selectedRange()[1] | date }}</p>
    }
  `
})
export class DateDisplayComponent {
  selectedRange = signal<[Date?, Date?]>([undefined, undefined]);
}
```

## Core Concepts

### Component Architecture

The datepicker library uses a multi-layer architecture:

```
RangeDatepickerComponent (User-facing)
├─→ Extends RangeDatepicker (Business logic)
│   └─→ Extends DatepickerBase (Forms integration)
├─→ DatepickerViewState (View state management)
├─→ CalendarBodyComponent (Day selection view)
├─→ MonthYearBodyComponent (Month/year selection view)
├─→ SelectedRangeComponent (Header with date inputs)
└─→ SelectedMonthYearComponent (Month/year display)
```

### Date Range Value Type

The library uses a tuple type for date ranges:

```typescript
export const DateRangeSchema = z.tuple([
  z.coerce.date().optional(), // start date
  z.coerce.date().optional(), // end date
]);

export type DateRangeValue = z.infer<typeof DateRangeSchema>;
// Type: [Date?, Date?]
```

### Auto-Sorting Behavior

When users select dates, the range automatically sorts them in ascending order:

```typescript
// User selects end date first
datepicker.setDateRange(new Date('2024-12-31'));
// Value: [2024-12-31, undefined]

// User selects start date second
datepicker.setDateRange(new Date('2024-01-01'));
// Value: [2024-01-01, 2024-12-31] (automatically sorted!)
```

**Sorting Logic**:
1. If both dates are selected, clears range and starts new selection
2. If only end date exists, sorts new selection with existing date
3. If only start date exists, sorts new selection with existing date
4. Uses `date-fns` `isBefore()` for comparison

### View State Management

The datepicker maintains separate view state from selected value:

```typescript
export class DatepickerViewState {
  // Currently displayed month/year in calendar
  displayedDate = signal<Date>(new Date());

  // Which view is active
  view = signal<DatepickerView>(DatepickerView.Calendar);
}
```

**View Types**:
- `DatepickerView.Calendar` - Day selection grid
- `DatepickerView.MonthYear` - Month and year selection

### ControlValueAccessor Implementation

The datepicker implements Angular's `ControlValueAccessor` interface:

```typescript
export abstract class DatepickerBase<TValue> implements ControlValueAccessor {
  // Form control callbacks
  onChange?: (value: TValue | undefined) => void;
  onTouched?: () => void;

  // Current value signal
  value = model<TValue | undefined>();

  // Form control methods
  writeValue(obj: unknown) { /* ... */ }
  registerOnChange(fn: typeof this.onChange) { /* ... */ }
  registerOnTouched(fn: typeof this.onTouched) { /* ... */ }

  // Abstract method for type parsing
  abstract parseValue(value: unknown): TValue | undefined;
}
```

### Dependency Injection Pattern

Min/max bounds are configurable via injection tokens:

```typescript
// Default configuration (4 years past, 1 year future)
export const UI_DATEPICKER_DEFAULT_MIN = new InjectionToken<DateValue | undefined>(
  'UI_DATEPICKER_DEFAULT_MIN',
  {
    providedIn: 'root',
    factory: () => subYears(new Date(), 4)
  }
);

export const UI_DATEPICKER_DEFAULT_MAX = new InjectionToken<DateValue | undefined>(
  'UI_DATEPICKER_DEFAULT_MAX',
  {
    providedIn: 'root',
    factory: () => addYears(new Date(), 1)
  }
);

// Usage in component
min = input<DateValue | undefined>(inject(UI_DATEPICKER_DEFAULT_MIN));
max = input<DateValue | undefined>(inject(UI_DATEPICKER_DEFAULT_MAX));
```

## API Reference

### RangeDatepickerComponent

Main component for date range selection with calendar interface.

#### Selector

`ui-range-datepicker`

#### Inputs (Inherited from DatepickerBase)

| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `min` | `DateValue \| undefined` | `subYears(today, 4)` | Minimum selectable date |
| `max` | `DateValue \| undefined` | `addYears(today, 1)` | Maximum selectable date |

#### Outputs (ControlValueAccessor)

Works with Angular forms - use `formControl`, `formControlName`, or `[(ngModel)]` bindings.

#### Public Properties

| Property | Type | Description |
|----------|------|-------------|
| `value` | `Signal<[Date?, Date?]>` | Current selected date range |
| `viewState` | `DatepickerViewState` | View state management service |

#### Methods

| Method | Signature | Description |
|--------|-----------|-------------|
| `setValue` | `(value?: [Date?, Date?]) => void` | Programmatically set the date range |
| `setDateRange` | `(value: Date) => void` | Add a date to the range (auto-sorts) |
| `parseValue` | `(value: unknown) => [Date?, Date?] \| undefined` | Parse unknown value to date range |
| `sortRangeValuesAsc` | `(range: [Date?, Date?]) => [Date?, Date?]` | Sort dates in ascending order |

#### Example

```typescript
import { Component, viewChild } from '@angular/core';
import { RangeDatepickerComponent } from '@isa/ui/datepicker';

@Component({
  selector: 'app-datepicker-example',
  standalone: true,
  imports: [RangeDatepickerComponent],
  template: `
    <ui-range-datepicker #picker />
    <button (click)="setLastMonth()">Last Month</button>
    <button (click)="clearDates()">Clear</button>
  `
})
export class DatepickerExampleComponent {
  picker = viewChild.required<RangeDatepickerComponent>('picker');

  setLastMonth(): void {
    const today = new Date();
    const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
    const endOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0);

    this.picker().setValue([lastMonth, endOfLastMonth]);
  }

  clearDates(): void {
    this.picker().setValue([undefined, undefined]);
  }
}
```

### DatepickerBase<TValue>

Abstract base class for creating custom datepicker implementations.

#### Type Parameters

- `TValue` - The type of the datepicker value (e.g., `[Date?, Date?]` for range picker)

#### Abstract Methods

```typescript
abstract parseValue(value: unknown): TValue | undefined;
```

Implement this method to define how raw values are parsed into your datepicker's value type.

#### Concrete Methods

| Method | Description |
|--------|-------------|
| `setValue(value?: TValue)` | Set value with parsing and form control notification |
| `writeValue(obj: unknown)` | ControlValueAccessor method - write value from form |
| `registerOnChange(fn)` | ControlValueAccessor method - register change callback |
| `registerOnTouched(fn)` | ControlValueAccessor method - register touched callback |

#### Example: Custom Single Date Picker

```typescript
import { Directive } from '@angular/core';
import { DatepickerBase } from '@isa/ui/datepicker';
import { z } from 'zod';

const SingleDateSchema = z.coerce.date().optional();
type SingleDateValue = z.infer<typeof SingleDateSchema>;

@Directive()
export class SingleDatepicker extends DatepickerBase<SingleDateValue> {
  parseValue(value: unknown): SingleDateValue {
    return SingleDateSchema.parse(value);
  }

  setSingleDate(value: Date): void {
    this.setValue(value);
  }
}
```

### DatepickerViewState

Service managing the datepicker's view state (displayed month/year and active view).

#### Properties

| Property | Type | Description |
|----------|------|-------------|
| `displayedDate` | `WritableSignal<Date>` | Currently displayed month/year in calendar |
| `view` | `WritableSignal<DatepickerView>` | Active view (Calendar or MonthYear) |

#### Example

```typescript
import { Component, inject } from '@angular/core';
import { DatepickerViewState, DatepickerView } from '@isa/ui/datepicker';

@Component({
  selector: 'app-custom-controls',
  template: `
    <button (click)="showCalendar()">Calendar View</button>
    <button (click)="showMonthYear()">Month/Year View</button>
    <button (click)="goToToday()">Today</button>
  `
})
export class CustomControlsComponent {
  viewState = inject(DatepickerViewState);

  showCalendar(): void {
    this.viewState.view.set(DatepickerView.Calendar);
  }

  showMonthYear(): void {
    this.viewState.view.set(DatepickerView.MonthYear);
  }

  goToToday(): void {
    this.viewState.displayedDate.set(new Date());
  }
}
```

### Validators

#### `dateBoundsValidator(min?, max?): ValidatorFn`

Validates that a date falls within specified bounds.

**Parameters**:
- `min?: Date` - Minimum allowed date (inclusive)
- `max?: Date` - Maximum allowed date (inclusive)

**Returns**: `ValidationErrors | null`
- `{ invalidDate: true }` - If date is invalid
- `{ minDate: true }` - If date is before minimum
- `{ maxDate: true }` - If date is after maximum
- `null` - If validation passes

**Example**:

```typescript
import { FormControl } from '@angular/forms';
import { dateBoundsValidator } from '@isa/ui/datepicker';
import { subYears, addYears } from 'date-fns';

const dateControl = new FormControl<Date>(
  new Date(),
  [dateBoundsValidator(subYears(new Date(), 5), addYears(new Date(), 1))]
);

// Test validation
dateControl.setValue(subYears(new Date(), 10)); // Invalid - before min
console.log(dateControl.errors); // { minDate: true }
```

#### `dateRangeStartStopOrderValidator(startKey, stopKey): ValidatorFn`

Validates that a start date is not after a stop date in a form group.

**Parameters**:
- `startKey: string` - Form control key for start date
- `stopKey: string` - Form control key for stop date

**Returns**: `ValidationErrors | null`
- `{ startAfterStop: true }` - If start date is after stop date
- `null` - If validation passes (or if either date is missing/invalid)

**Example**:

```typescript
import { FormBuilder, Validators } from '@angular/forms';
import { dateRangeStartStopOrderValidator } from '@isa/ui/datepicker';

const form = this.fb.group({
  startDate: [null, Validators.required],
  endDate: [null, Validators.required]
}, {
  validators: [dateRangeStartStopOrderValidator('startDate', 'endDate')]
});

// Test validation
form.patchValue({
  startDate: new Date('2024-12-31'),
  endDate: new Date('2024-01-01')
});

console.log(form.errors); // { startAfterStop: true }
```

## Usage Examples

### Basic Date Range Selection

```typescript
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { RangeDatepickerComponent } from '@isa/ui/datepicker';

@Component({
  selector: 'app-basic-range',
  standalone: true,
  imports: [ReactiveFormsModule, RangeDatepickerComponent],
  template: `
    <div class="date-filter">
      <label>Select Date Range:</label>
      <ui-range-datepicker [formControl]="rangeControl" />

      @if (rangeControl.value?.[0] && rangeControl.value?.[1]) {
        <p>
          Selected: {{ rangeControl.value[0] | date }} to {{ rangeControl.value[1] | date }}
        </p>
      }
    </div>
  `
})
export class BasicRangeComponent {
  rangeControl = new FormControl<[Date?, Date?]>([undefined, undefined]);
}
```

### Form with Validation

```typescript
import { Component } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { RangeDatepickerComponent, dateBoundsValidator, dateRangeStartStopOrderValidator } from '@isa/ui/datepicker';
import { subMonths, addMonths } from 'date-fns';

@Component({
  selector: 'app-validated-form',
  standalone: true,
  imports: [ReactiveFormsModule, RangeDatepickerComponent],
  template: `
    <form [formGroup]="reportForm" (ngSubmit)="generateReport()">
      <div class="form-field">
        <label>Report Period:</label>
        <ui-range-datepicker formControlName="dateRange" />

        @if (reportForm.get('dateRange')?.hasError('required')) {
          <span class="error">Date range is required</span>
        }
        @if (reportForm.get('dateRange')?.hasError('startAfterStop')) {
          <span class="error">Start date must be before end date</span>
        }
        @if (reportForm.get('dateRange')?.hasError('minDate')) {
          <span class="error">Date must be within the last 6 months</span>
        }
        @if (reportForm.get('dateRange')?.hasError('maxDate')) {
          <span class="error">Date cannot be in the future</span>
        }
      </div>

      <button type="submit" [disabled]="!reportForm.valid">
        Generate Report
      </button>
    </form>
  `
})
export class ValidatedFormComponent {
  reportForm = this.fb.group({
    dateRange: [
      [undefined, undefined] as [Date?, Date?],
      [
        Validators.required,
        dateBoundsValidator(subMonths(new Date(), 6), new Date())
      ]
    ]
  }, {
    validators: [dateRangeStartStopOrderValidator('dateRange', 'dateRange')]
  });

  constructor(private fb: FormBuilder) {}

  generateReport(): void {
    if (this.reportForm.valid) {
      const [start, end] = this.reportForm.value.dateRange!;
      console.log('Generating report from', start, 'to', end);
    }
  }
}
```

### Custom Min/Max Bounds

```typescript
import { Component, Provider } from '@angular/core';
import { RangeDatepickerComponent, UI_DATEPICKER_DEFAULT_MIN, UI_DATEPICKER_DEFAULT_MAX } from '@isa/ui/datepicker';
import { subYears, endOfYear } from 'date-fns';

// Custom providers for component-specific bounds
const FISCAL_YEAR_MIN: Provider = {
  provide: UI_DATEPICKER_DEFAULT_MIN,
  useValue: new Date(2020, 0, 1) // January 1, 2020
};

const FISCAL_YEAR_MAX: Provider = {
  provide: UI_DATEPICKER_DEFAULT_MAX,
  useValue: endOfYear(new Date()) // End of current year
};

@Component({
  selector: 'app-fiscal-report',
  standalone: true,
  imports: [RangeDatepickerComponent],
  providers: [FISCAL_YEAR_MIN, FISCAL_YEAR_MAX],
  template: `
    <div class="fiscal-report">
      <h2>Fiscal Year Report</h2>
      <p>Select dates between 2020 and end of current year</p>
      <ui-range-datepicker />
    </div>
  `
})
export class FiscalReportComponent {}
```

### Programmatic Date Control

```typescript
import { Component, signal, viewChild } from '@angular/core';
import { RangeDatepickerComponent } from '@isa/ui/datepicker';
import { startOfMonth, endOfMonth, subMonths, startOfYear, endOfYear } from 'date-fns';

@Component({
  selector: 'app-quick-filters',
  standalone: true,
  imports: [RangeDatepickerComponent],
  template: `
    <div class="quick-filters">
      <button (click)="setThisMonth()">This Month</button>
      <button (click)="setLastMonth()">Last Month</button>
      <button (click)="setThisYear()">This Year</button>
      <button (click)="clearRange()">Clear</button>
    </div>

    <ui-range-datepicker #picker />

    @if (selectedRange()[0] && selectedRange()[1]) {
      <div class="selected-info">
        <p>From: {{ selectedRange()[0] | date:'fullDate' }}</p>
        <p>To: {{ selectedRange()[1] | date:'fullDate' }}</p>
      </div>
    }
  `
})
export class QuickFiltersComponent {
  picker = viewChild.required<RangeDatepickerComponent>('picker');
  selectedRange = signal<[Date?, Date?]>([undefined, undefined]);

  setThisMonth(): void {
    const today = new Date();
    const start = startOfMonth(today);
    const end = endOfMonth(today);
    this.updateRange([start, end]);
  }

  setLastMonth(): void {
    const lastMonth = subMonths(new Date(), 1);
    const start = startOfMonth(lastMonth);
    const end = endOfMonth(lastMonth);
    this.updateRange([start, end]);
  }

  setThisYear(): void {
    const today = new Date();
    const start = startOfYear(today);
    const end = endOfYear(today);
    this.updateRange([start, end]);
  }

  clearRange(): void {
    this.updateRange([undefined, undefined]);
  }

  private updateRange(range: [Date?, Date?]): void {
    this.picker().setValue(range);
    this.selectedRange.set(range);
  }
}
```

### Integration with Backend API

```typescript
import { Component, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { RangeDatepickerComponent } from '@isa/ui/datepicker';
import { format } from 'date-fns';
import { HttpClient } from '@angular/common/http';

interface ReportFilters {
  startDate: string; // ISO format
  endDate: string;   // ISO format
}

@Component({
  selector: 'app-api-integration',
  standalone: true,
  imports: [ReactiveFormsModule, RangeDatepickerComponent],
  template: `
    <form [formGroup]="filterForm" (ngSubmit)="loadData()">
      <ui-range-datepicker formControlName="dateRange" />
      <button type="submit" [disabled]="!filterForm.valid || isLoading()">
        {{ isLoading() ? 'Loading...' : 'Load Data' }}
      </button>
    </form>

    @if (data()) {
      <div class="results">
        <p>Found {{ data()?.length }} results</p>
        <!-- Display results... -->
      </div>
    }
  `
})
export class ApiIntegrationComponent {
  filterForm = this.fb.group({
    dateRange: [[undefined, undefined] as [Date?, Date?]]
  });

  isLoading = signal(false);
  data = signal<unknown[] | null>(null);

  constructor(
    private fb: FormBuilder,
    private http: HttpClient
  ) {}

  loadData(): void {
    const [start, end] = this.filterForm.value.dateRange!;

    if (!start || !end) return;

    const filters: ReportFilters = {
      startDate: format(start, 'yyyy-MM-dd'),
      endDate: format(end, 'yyyy-MM-dd')
    };

    this.isLoading.set(true);

    this.http.get<unknown[]>('/api/reports', { params: filters })
      .subscribe({
        next: (results) => {
          this.data.set(results);
          this.isLoading.set(false);
        },
        error: (error) => {
          console.error('Failed to load data:', error);
          this.isLoading.set(false);
        }
      });
  }
}
```

## Validation

### Built-in Validators

The library provides two validators for common date validation scenarios:

#### 1. Date Bounds Validator

Ensures a date falls within a specified range:

```typescript
import { dateBoundsValidator } from '@isa/ui/datepicker';
import { subYears, addYears } from 'date-fns';

// Allow dates within 5 years past and 2 years future
const control = new FormControl(
  new Date(),
  [dateBoundsValidator(subYears(new Date(), 5), addYears(new Date(), 2))]
);

// Validation errors
control.setValue(subYears(new Date(), 10));
console.log(control.errors); // { minDate: true }

control.setValue(addYears(new Date(), 5));
console.log(control.errors); // { maxDate: true }

control.setValue(new Date('invalid'));
console.log(control.errors); // { invalidDate: true }
```

#### 2. Date Range Order Validator

Ensures start date is not after end date:

```typescript
import { dateRangeStartStopOrderValidator } from '@isa/ui/datepicker';

const form = this.fb.group({
  startDate: [null],
  endDate: [null]
}, {
  validators: [dateRangeStartStopOrderValidator('startDate', 'endDate')]
});

// Valid - start before end
form.patchValue({
  startDate: new Date('2024-01-01'),
  endDate: new Date('2024-12-31')
});
console.log(form.errors); // null

// Invalid - start after end
form.patchValue({
  startDate: new Date('2024-12-31'),
  endDate: new Date('2024-01-01')
});
console.log(form.errors); // { startAfterStop: true }
```

### Custom Validators

Create custom validators for specific business rules:

```typescript
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { isWeekend } from 'date-fns';

// Validator: No weekend dates
export function noWeekendsValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const date: Date | null = control.value;

    if (!date) return null;

    if (isWeekend(date)) {
      return { weekend: true };
    }

    return null;
  };
}

// Validator: Date range must span at least N days
export function minRangeDaysValidator(minDays: number): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const start = group.get('startDate')?.value as Date | null;
    const end = group.get('endDate')?.value as Date | null;

    if (!start || !end) return null;

    const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));

    if (daysDiff < minDays) {
      return { minRangeDays: { required: minDays, actual: daysDiff } };
    }

    return null;
  };
}

// Usage
const form = this.fb.group({
  startDate: [null, [Validators.required, noWeekendsValidator()]],
  endDate: [null, [Validators.required, noWeekendsValidator()]]
}, {
  validators: [
    dateRangeStartStopOrderValidator('startDate', 'endDate'),
    minRangeDaysValidator(7) // Minimum 7 days
  ]
});
```

### Error Display

Display validation errors in the template:

```html
<form [formGroup]="form">
  <ui-range-datepicker formControlName="dateRange" />

  @if (form.get('dateRange')?.touched && form.get('dateRange')?.errors) {
    <div class="validation-errors">
      @if (form.get('dateRange')?.hasError('required')) {
        <span class="error">Date range is required</span>
      }
      @if (form.get('dateRange')?.hasError('minDate')) {
        <span class="error">Date is before minimum allowed date</span>
      }
      @if (form.get('dateRange')?.hasError('maxDate')) {
        <span class="error">Date is after maximum allowed date</span>
      }
      @if (form.get('dateRange')?.hasError('startAfterStop')) {
        <span class="error">Start date must be before end date</span>
      }
      @if (form.get('dateRange')?.hasError('invalidDate')) {
        <span class="error">Invalid date format</span>
      }
    </div>
  }
</form>
```

## Form Integration

### Reactive Forms

Full integration with Angular reactive forms:

```typescript
import { Component } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { RangeDatepickerComponent, dateRangeStartStopOrderValidator } from '@isa/ui/datepicker';

@Component({
  selector: 'app-reactive-form',
  standalone: true,
  imports: [ReactiveFormsModule, RangeDatepickerComponent],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <ui-range-datepicker formControlName="dateRange" />
      <button type="submit">Submit</button>
    </form>
  `
})
export class ReactiveFormComponent {
  form = this.fb.group({
    dateRange: [
      [undefined, undefined] as [Date?, Date?],
      [Validators.required]
    ]
  }, {
    validators: [dateRangeStartStopOrderValidator('dateRange', 'dateRange')]
  });

  constructor(private fb: FormBuilder) {}

  onSubmit(): void {
    if (this.form.valid) {
      console.log('Selected range:', this.form.value.dateRange);
    }
  }
}
```

### Template-Driven Forms

Works with template-driven forms via ngModel:

```html
<form #form="ngForm" (ngSubmit)="onSubmit(form)">
  <ui-range-datepicker
    name="dateRange"
    [(ngModel)]="selectedRange"
    required>
  </ui-range-datepicker>

  <button type="submit" [disabled]="!form.valid">Submit</button>
</form>
```

### Form Control State

Access form control state and errors:

```typescript
// Get form control
const dateRangeControl = this.form.get('dateRange');

// Check validity
console.log(dateRangeControl?.valid);
console.log(dateRangeControl?.invalid);
console.log(dateRangeControl?.errors);

// Check state
console.log(dateRangeControl?.touched);
console.log(dateRangeControl?.dirty);
console.log(dateRangeControl?.pristine);

// Manually update
dateRangeControl?.setValue([new Date(), new Date()]);
dateRangeControl?.patchValue([new Date(), undefined]);
dateRangeControl?.markAsTouched();
dateRangeControl?.markAsDirty();
```

## Testing

The library uses **Jest** for testing.

### Running Tests

```bash
# Run tests for this library
npx nx test ui-datepicker --skip-nx-cache

# Run tests with coverage
npx nx test ui-datepicker --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test ui-datepicker --watch
```

### Test Coverage

The library includes comprehensive unit tests covering:

- **Component rendering** - Calendar and month/year views
- **Date selection** - Single date, range selection, auto-sorting
- **Form integration** - ControlValueAccessor implementation
- **Validators** - Date bounds and range order validation
- **State management** - View state transitions
- **Date bounds** - Min/max constraint enforcement
- **Edge cases** - Invalid dates, empty values, out-of-bounds dates

### Example Tests

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from '@jest/globals';
import { RangeDatepickerComponent } from './range-datepicker.component';

describe('RangeDatepickerComponent', () => {
  let component: RangeDatepickerComponent;
  let fixture: ComponentFixture<RangeDatepickerComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [RangeDatepickerComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(RangeDatepickerComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should auto-sort date range', () => {
    const startDate = new Date('2024-01-01');
    const endDate = new Date('2024-12-31');

    // Select end date first
    component.setDateRange(endDate);
    // Select start date second
    component.setDateRange(startDate);

    const value = component.value();
    expect(value?.[0]).toEqual(startDate);
    expect(value?.[1]).toEqual(endDate);
  });

  it('should parse date range from unknown value', () => {
    const dateStrings = ['2024-01-01', '2024-12-31'];
    const parsed = component.parseValue(dateStrings);

    expect(parsed).toBeDefined();
    expect(parsed?.[0]).toBeInstanceOf(Date);
    expect(parsed?.[1]).toBeInstanceOf(Date);
  });
});
```

## Architecture Notes

### Component Hierarchy

```
RangeDatepickerComponent
├─→ RangeDatepicker (extends DatepickerBase<DateRangeValue>)
│   ├─→ Implements date range logic
│   ├─→ Auto-sorting behavior
│   └─→ Zod schema parsing
├─→ DatepickerBase<TValue>
│   ├─→ ControlValueAccessor implementation
│   ├─→ Form integration
│   └─→ Min/max bounds management
├─→ DatepickerViewState (DI service)
│   ├─→ Displayed date signal
│   └─→ Active view signal
├─→ CalendarBodyComponent
│   ├─→ Day grid rendering
│   ├─→ Month navigation
│   └─→ Date selection handling
├─→ MonthYearBodyComponent
│   ├─→ Month selection grid
│   ├─→ Year selection grid
│   └─→ Bounds filtering
├─→ SelectedRangeComponent (header)
│   └─→ Date input fields
└─→ SelectedMonthYearComponent (header)
    └─→ Month/year display
```

### Design Patterns

#### 1. Abstract Base Class

`DatepickerBase` provides extensibility for custom implementations:

```typescript
@Directive()
export abstract class DatepickerBase<TValue> implements ControlValueAccessor {
  // Common form integration
  // Min/max bounds handling
  // Abstract parsing method
  abstract parseValue(value: unknown): TValue | undefined;
}

// Implementations
export class RangeDatepicker extends DatepickerBase<DateRangeValue> { /* ... */ }
export class SingleDatepicker extends DatepickerBase<Date | undefined> { /* ... */ }
```

**Benefits**:
- Code reuse for form integration
- Consistent API across datepicker types
- Easy to create custom datepicker variants

#### 2. Dependency Injection for Configuration

Injection tokens allow flexible configuration:

```typescript
export const UI_DATEPICKER_DEFAULT_MIN = new InjectionToken<DateValue>(
  'UI_DATEPICKER_DEFAULT_MIN',
  { providedIn: 'root', factory: () => subYears(new Date(), 4) }
);

// Override per component
providers: [
  { provide: UI_DATEPICKER_DEFAULT_MIN, useValue: customMin }
]
```

**Benefits**:
- Global defaults with local overrides
- Type-safe configuration
- Testability (easy to mock)

#### 3. Signal-Based State Management

Uses Angular signals for reactive state:

```typescript
// View state
displayedDate = signal<Date>(new Date());
view = signal<DatepickerView>(DatepickerView.Calendar);

// Component value
value = model<DateRangeValue | undefined>();

// Computed properties
calendarDays = computed(() => {
  const month = this.viewState.displayedDate();
  return eachDayOfInterval(/* ... */);
});
```

**Benefits**:
- Fine-grained reactivity
- Automatic dependency tracking
- Minimal re-renders with OnPush

#### 4. Zod Schema Validation

Runtime type safety with Zod:

```typescript
export const DateRangeSchema = z.tuple([
  z.coerce.date().optional(),
  z.coerce.date().optional(),
]);

parseValue(value: unknown): DateRangeValue | undefined {
  return DateRangeSchema.optional().parse(value);
}
```

**Benefits**:
- Runtime type validation
- Type coercion (strings → dates)
- Clear validation errors

### Performance Considerations

1. **OnPush Change Detection**: All components use `ChangeDetectionStrategy.OnPush`
2. **Signal-Based Reactivity**: Computed properties only recalculate when dependencies change
3. **date-fns Tree Shaking**: Only import needed date functions
4. **Lazy View Rendering**: Calendar/month-year views render only when active

### Known Architectural Considerations

#### 1. Single Date Picker Support (Medium Priority)

**Current State**:
- Library only provides `RangeDatepicker`
- No built-in single date picker component

**Consideration**:
- Add `SingleDatepickerComponent` extending `DatepickerBase<Date>`
- Share calendar view components

**Impact**: Medium - common use case currently requires workaround

#### 2. Validator Utilities Location (Low Priority)

**Current State**:
- Validators have `TODO` comments about extracting to utils
- Currently in library but could be shared

**Consideration**:
- Move to `@isa/utils/validators` for broader reuse
- Keep datepicker-specific validators in library

**Impact**: Low - current location is acceptable

#### 3. Time Selection Support (Future Enhancement)

**Current State**:
- Date-only selection
- No time picker functionality

**Consideration**:
- Add time selection component
- Create `DateTimeRangePicker` variant

**Impact**: Future - no current requirement

## Dependencies

### Required Libraries

- `@angular/core` - Angular framework
- `@angular/common` - Angular common utilities (DatePipe)
- `@angular/forms` - Forms integration (ControlValueAccessor)
- `date-fns` - Date manipulation and formatting
- `zod` - Runtime type validation
- `@ng-icons/core` - Icon rendering
- `@isa/icons` - ISA icon library (chevrons, check icon)
- `@isa/ui/buttons` - Button components (month/year selection)

### Path Alias

Import from: `@isa/ui/datepicker`

### Project Configuration

- **Project Name**: `ui-datepicker`
- **Prefix**: `ui`
- **Testing**: Jest
- **Source Root**: `libs/ui/datepicker/src`

## License

Internal ISA Frontend library - not for external distribution.
