/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Component, ElementRef, ViewChild, forwardRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; /** * Drop-in replacement for `` used throughout the * Investment module. Keeps the DD-MM-YYYY string every date field/helper in investment-data.ts * already expects (parseDDMMYYYY, addPeriod, etc.), but adds a calendar button that opens the * browser's native date picker and converts the picked value back to DD-MM-YYYY — so users can * either type the date or pick it, without changing any consuming component's data model. * * Usage: */ @Component({ selector: 'mifosx-date-ddmmyyyy', standalone: true, imports: [ CommonModule, FormsModule ], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateDdmmyyyyPickerComponent), multi: true } ], template: `
`, styles: [ ` /* Angular view encapsulation stops the host page's .inv-form-field styles from reaching * inside this component's own template, so the look of the surrounding form fields * (height, border, radius, font) is deliberately re-declared here instead of inherited. */ :host { display: block; width: 100%; } .inv-date-field { position: relative; display: flex; align-items: center; gap: 6px; width: 100%; } .inv-date-field__text { flex: 1; min-width: 0; width: 100%; height: 36px; padding: 0 10px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 13px; color: #0d1b2a; font-family: inherit; background: #fff; box-sizing: border-box; } .inv-date-field__btn { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; height: 36px; width: 36px; padding: 0; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; cursor: pointer; font-size: 14px; line-height: 1; } .inv-date-field__btn:hover:not(:disabled) { background: #f2f4f7; } .inv-date-field__btn:disabled { cursor: not-allowed; opacity: 0.5; } .inv-date-field__native { position: absolute; width: 0; height: 0; padding: 0; border: none; opacity: 0; pointer-events: none; } ` ] }) export class DateDdmmyyyyPickerComponent implements ControlValueAccessor { @ViewChild('native') private nativeInput?: ElementRef; value = ''; disabled = false; private onChangeFn: (v: string) => void = () => {}; onTouched: () => void = () => {}; /** DD-MM-YYYY -> yyyy-MM-dd, the format requires. */ get isoValue(): string { const parts = (this.value || '').split('-'); if (parts.length !== 3) { return ''; } const [ dd, mm, yyyy ] = parts; if (!/^\d{2}$/.test(dd) || !/^\d{2}$/.test(mm) || !/^\d{4}$/.test(yyyy)) { return ''; } return `${yyyy}-${mm}-${dd}`; } writeValue(v: string): void { this.value = v ?? ''; } registerOnChange(fn: (v: string) => void): void { this.onChangeFn = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } onTextChange(v: string): void { this.value = v; this.onChangeFn(v); } /** yyyy-MM-dd (from the native picker) -> DD-MM-YYYY, the format the rest of the app expects. */ onPickerChange(iso: string): void { const parts = (iso || '').split('-'); if (parts.length !== 3) { return; } const [ yyyy, mm, dd ] = parts; const ddmmyyyy = `${dd}-${mm}-${yyyy}`; this.value = ddmmyyyy; this.onChangeFn(ddmmyyyy); this.onTouched(); } openPicker(): void { const el = this.nativeInput?.nativeElement; if (!el || this.disabled) { return; } const showPicker = (el as HTMLInputElement & { showPicker?: () => void }).showPicker; if (typeof showPicker === 'function') { try { showPicker.call(el); return; } catch { // Some browsers throw if not called from a "user gesture" context they recognise — // fall back to a plain click(), which still opens the native picker. } } el.click(); } }