diff --git a/angular.json b/angular.json index 83cef95e..07272cce 100644 --- a/angular.json +++ b/angular.json @@ -56,6 +56,7 @@ "src/.htaccess" ], "styles": [ + "node_modules/flatpickr/dist/flatpickr.css", "node_modules/@ng-select/ng-select/themes/default.theme.css", "src/styles.scss" ], @@ -121,4 +122,4 @@ } } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 741b5dc5..18b41d6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,10 @@ "@ng-select/ng-select": "^21.8.2", "@tailwindcss/forms": "^0.5.11", "@tsparticles/angular": "^3.0.0", + "angularx-flatpickr": "^8.1.0", "apexcharts": "^5.10.5", "chart.js": "^4.5.1", + "flatpickr": "^4.6.13", "gridjs": "^6.2.0", "ng-apexcharts": "^2.3.0", "ng2-charts": "^10.0.0", @@ -7851,6 +7853,20 @@ "typescript-eslint": "^8.0.0" } }, + "node_modules/angularx-flatpickr": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/angularx-flatpickr/-/angularx-flatpickr-8.1.0.tgz", + "integrity": "sha512-U+WXMUXGEiQbdMGSPk7T+HehmAFdVWKu3XlCXFM8mYCCB/fWHW8sbHstxxZgOymD5Q1kfLaHNob1MxhWUgv1hg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=17.0.0", + "@angular/forms": ">=17.0.0", + "flatpickr": "^4.5.0" + } + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -9574,6 +9590,12 @@ "node": ">=16" } }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, "node_modules/flatted": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", diff --git a/package.json b/package.json index 40272561..ce4d86aa 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,10 @@ "@ng-select/ng-select": "^21.8.2", "@tailwindcss/forms": "^0.5.11", "@tsparticles/angular": "^3.0.0", + "angularx-flatpickr": "^8.1.0", "apexcharts": "^5.10.5", "chart.js": "^4.5.1", + "flatpickr": "^4.6.13", "gridjs": "^6.2.0", "ng-apexcharts": "^2.3.0", "ng2-charts": "^10.0.0", @@ -74,4 +76,4 @@ "typescript-eslint": "8.59.2", "vitest": "^4.0.8" } -} \ No newline at end of file +} diff --git a/public/assets/images/authentication/auth-cover.png b/public/assets/images/authentication/auth-cover.png index 3dcc0cde..02423023 100644 Binary files a/public/assets/images/authentication/auth-cover.png and b/public/assets/images/authentication/auth-cover.png differ diff --git a/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.html b/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.html new file mode 100644 index 00000000..a4257d27 --- /dev/null +++ b/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.html @@ -0,0 +1,29 @@ + \ No newline at end of file diff --git a/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.scss b/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.scss new file mode 100644 index 00000000..d7855645 --- /dev/null +++ b/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.scss @@ -0,0 +1,120 @@ +/* SpkFlatpickr Styles - Ultra-Modern Ynex Calendar Header Layout */ +:host ::ng-deep .flatpickr-calendar { + border-radius: 0.5rem !important; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important; + + .flatpickr-months { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + height: 3rem !important; + padding: 0.35rem 0.5rem !important; + position: relative !important; + + .flatpickr-prev-month, + .flatpickr-next-month { + position: relative !important; + top: auto !important; + left: auto !important; + right: auto !important; + height: 2rem !important; + width: 2rem !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + padding: 0 !important; + border-radius: 0.375rem !important; + color: var(--color-primary, #7c3deb) !important; + transition: background-color 0.2s ease !important; + + &:hover { + background-color: rgba(124, 61, 235, 0.1) !important; + color: var(--color-primary, #7c3deb) !important; + } + + svg { + fill: var(--color-primary, #7c3deb) !important; + width: 0.9rem !important; + height: 0.9rem !important; + } + } + + .flatpickr-current-month { + position: relative !important; + left: auto !important; + right: auto !important; + width: auto !important; + height: 100% !important; + top: auto !important; + display: flex !important; + flex-direction: row !important; + flex-wrap: nowrap !important; + align-items: center !important; + justify-content: center !important; + gap: 0.4rem !important; + padding: 0 !important; + margin: 0 !important; + transform: none !important; + + /* Common Pill Select Styling for both Month & Year Dropdowns */ + .flatpickr-monthDropdown-months, + .spk-flatpickr-year-select { + appearance: none !important; + -webkit-appearance: none !important; + -moz-appearance: none !important; + font-weight: 600 !important; + font-size: 0.8125rem !important; + color: var(--color-primary, #7c3deb) !important; + background-color: rgba(124, 61, 235, 0.08) !important; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3deb'%3E%3Cpath d='M12 15l-4-4h8z'/%3E%3C/svg%3E") !important; + background-repeat: no-repeat !important; + background-position: right 0.45rem center !important; + background-size: 0.85rem !important; + border: 1px solid rgba(124, 61, 235, 0.22) !important; + border-radius: 0.45rem !important; + padding: 0.25rem 1.5rem 0.25rem 0.65rem !important; + height: 2rem !important; + line-height: 1.4 !important; + cursor: pointer !important; + outline: none !important; + box-sizing: border-box !important; + transition: all 0.2s ease-in-out !important; + + &:hover, + &:focus { + background-color: rgba(124, 61, 235, 0.15) !important; + border-color: rgba(124, 61, 235, 0.45) !important; + box-shadow: 0 0 0 2px rgba(124, 61, 235, 0.12) !important; + } + + /* Dropdown option list styling */ + option, + .flatpickr-monthDropdown-month { + font-weight: 500 !important; + font-size: 0.8125rem !important; + padding: 0.4rem 0.6rem !important; + background-color: #ffffff !important; + color: #334155 !important; + + &:hover, + &:checked { + background-color: var(--color-primary, #7c3deb) !important; + color: #ffffff !important; + } + + .dark &, + [data-theme-mode="dark"] & { + background-color: #1e293b !important; + color: #f1f5f9 !important; + + &:hover, + &:checked { + background-color: #7c3deb !important; + color: #ffffff !important; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.ts b/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.ts new file mode 100644 index 00000000..b170b8f7 --- /dev/null +++ b/src/app/@spk/plugins&reusable/spk-flatpickr/spk-flatpickr.ts @@ -0,0 +1,661 @@ +import { + Component, + ElementRef, + EventEmitter, + OnDestroy, + Output, + ViewChild, + forwardRef, + input +} from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { FlatpickrDefaults, FlatpickrDirective, provideFlatpickrDefaults } from 'angularx-flatpickr'; + +export type SpkFlatpickrMode = 'single' | 'multiple' | 'range'; + +export interface DateRangeValue { + from: string | null; + to: string | null; +} + +export type DatePickerValue = string | DateRangeValue | null; + +export interface FlatPickrOutputOptions { + selectedDates: Date[]; + dateString: string; + instance: any; +} + +export type CalendarViewMode = 'calendar' | 'month' | 'year'; + +export function formatSingleDate(raw: any): string | null { + if (raw == null || raw === '') return null; + if (raw instanceof Date) { + if (isNaN(raw.getTime())) return null; + const year = raw.getFullYear(); + const month = String(raw.getMonth() + 1).padStart(2, '0'); + const day = String(raw.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + if (typeof raw === 'number') { + const d = new Date(raw); + return isNaN(d.getTime()) ? null : formatSingleDate(d); + } + if (typeof raw === 'string') { + const trimmed = raw.trim(); + if (!trimmed) return null; + const datePart = trimmed.split(/[T\s]/)[0]; + if (/^\d{4}-\d{2}-\d{2}$/.test(datePart)) return datePart; + const parsed = new Date(trimmed); + if (!isNaN(parsed.getTime())) { + const year = parsed.getFullYear(); + const month = String(parsed.getMonth() + 1).padStart(2, '0'); + const day = String(parsed.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + } + return null; +} + +export function toFlatpickrValue(val: DatePickerValue, mode: SpkFlatpickrMode): string | null { + if (!val) return null; + if (mode === 'range') { + if (typeof val === 'object' && 'from' in val) { + const from = formatSingleDate(val.from); + const to = formatSingleDate(val.to); + if (from && to) return `${from} to ${to}`; + if (from) return from; + return null; + } + if (typeof val === 'string') return val; + return null; + } + return formatSingleDate(val); +} + +export function fromFlatpickrValue(raw: any, mode: SpkFlatpickrMode): DatePickerValue { + if (!raw) return null; + if (mode === 'range') { + let fromStr: string | null = null; + let toStr: string | null = null; + if (Array.isArray(raw)) { + fromStr = formatSingleDate(raw[0]); + toStr = formatSingleDate(raw[1]); + } else if (typeof raw === 'object' && !(raw instanceof Date)) { + fromStr = formatSingleDate(raw.from); + toStr = formatSingleDate(raw.to); + } else if (typeof raw === 'string') { + const parts = raw.split(/\s+to\s+|\s+-\s+/i); + fromStr = formatSingleDate(parts[0]); + toStr = formatSingleDate(parts[1]); + } + return (fromStr || toStr) ? { from: fromStr, to: toStr } : null; + } + if (Array.isArray(raw)) { + return formatSingleDate(raw[0]); + } + return formatSingleDate(raw); +} + +@Component({ + selector: 'spk-flatpickr', + standalone: true, + imports: [FlatpickrDirective, FormsModule, ReactiveFormsModule], + providers: [ + FlatpickrDefaults, + provideFlatpickrDefaults(), + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SpkFlatpickr), + multi: true + } + ], + templateUrl: './spk-flatpickr.html', + styleUrl: './spk-flatpickr.scss' +}) +export class SpkFlatpickr implements ControlValueAccessor, OnDestroy { + @ViewChild('nativeInput', { static: false }) nativeInputRef?: ElementRef; + + readonly id = input(''); + readonly altInput = input(false); + readonly convertModelValue = input(false); + readonly enableTime = input(false); + readonly noCalendar = input(false); + readonly inline = input(false); + readonly class = input(''); + readonly dateFormat = input('Y-m-d'); + readonly placeholder = input(''); + readonly mode = input('single'); + readonly minDate = input(undefined); + readonly maxDate = input(undefined); + readonly readonly = input(false); + + @Output() focus = new EventEmitter(); + @Output() blur = new EventEmitter(); + + value: any = null; + disabled: boolean = false; + fpInstance: any = null; + + private viewMode: CalendarViewMode = 'calendar'; + private viewContainerEl: HTMLElement | null = null; + private yearPageOffset = 0; + private pendingIgnoredElements: HTMLElement[] = []; + private scrollResizeHandler: () => void = () => this.repositionCalendar(); + + private onChange: (value: any) => void = () => { }; + private onTouched: () => void = () => { }; + + readonly monthsList = [ + { index: 0, shortName: 'Jan', name: 'January' }, + { index: 1, shortName: 'Feb', name: 'February' }, + { index: 2, shortName: 'Mar', name: 'March' }, + { index: 3, shortName: 'Apr', name: 'April' }, + { index: 4, shortName: 'May', name: 'May' }, + { index: 5, shortName: 'Jun', name: 'June' }, + { index: 6, shortName: 'Jul', name: 'July' }, + { index: 7, shortName: 'Aug', name: 'August' }, + { index: 8, shortName: 'Sep', name: 'September' }, + { index: 9, shortName: 'Oct', name: 'October' }, + { index: 10, shortName: 'Nov', name: 'November' }, + { index: 11, shortName: 'Dec', name: 'December' }, + ]; + + ngOnDestroy(): void { + this.removePositionListeners(); + if (this.viewContainerEl && this.viewContainerEl.parentNode) { + this.viewContainerEl.parentNode.removeChild(this.viewContainerEl); + } + this.viewContainerEl = null; + } + + open(): void { + if (this.fpInstance && typeof this.fpInstance.open === 'function') { + this.fpInstance.open(); + } + } + + close(): void { + if (this.fpInstance && typeof this.fpInstance.close === 'function') { + this.fpInstance.close(); + } + } + + toggle(): void { + if (this.fpInstance) { + if (this.fpInstance.isOpen) { + this.close(); + } else { + this.open(); + } + } + } + + clear(): void { + this.value = null; + if (this.fpInstance && typeof this.fpInstance.clear === 'function') { + this.fpInstance.clear(); + } + } + + registerIgnoredElement(el: HTMLElement): void { + if (!el) return; + if (this.fpInstance && this.fpInstance.config) { + if (!this.fpInstance.config.ignoredFocusElements) { + this.fpInstance.config.ignoredFocusElements = []; + } + if (!this.fpInstance.config.ignoredFocusElements.includes(el)) { + this.fpInstance.config.ignoredFocusElements.push(el); + } + } else { + if (!this.pendingIgnoredElements.includes(el)) { + this.pendingIgnoredElements.push(el); + } + } + } + + writeValue(val: any): void { + const normalized = val ?? null; + if (this.isSameValue(this.value, normalized)) { + return; + } + + if (this.fpInstance && this.fpInstance.isOpen && this.mode() === 'range' && this.fpInstance.selectedDates?.length === 1) { + this.value = normalized; + return; + } + + this.value = normalized; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + onValueChange(newVal: any): void { + const normalized = newVal ?? null; + if (this.isSameValue(this.value, normalized)) { + return; + } + this.value = normalized; + this.onChange(this.value); + this.onTouched(); + } + + onFlatpickrChange(event: FlatPickrOutputOptions): void { + if (!event) return; + const rawValue = (event.dateString && event.dateString.trim()) + ? event.dateString + : (event.selectedDates && event.selectedDates.length > 0 ? event.selectedDates : null); + + this.onValueChange(rawValue); + + if (this.mode() === 'single' && event.selectedDates && event.selectedDates.length === 1) { + this.close(); + } else if (this.mode() === 'range' && event.selectedDates && event.selectedDates.length === 2) { + this.close(); + } + } + + onInputFocus(): void { + this.focus.emit(); + } + + onInputBlur(): void { + this.blur.emit(); + this.onTouched(); + } + + onReady(event: FlatPickrOutputOptions): void { + this.fpInstance = event.instance; + if (this.fpInstance && this.fpInstance.config) { + if (!this.fpInstance.config.ignoredFocusElements) { + this.fpInstance.config.ignoredFocusElements = []; + } + this.pendingIgnoredElements.forEach(el => { + if (!this.fpInstance.config.ignoredFocusElements.includes(el)) { + this.fpInstance.config.ignoredFocusElements.push(el); + } + }); + this.pendingIgnoredElements = []; + } + this.initViewContainer(); + this.ensureCustomHeader(); + } + + onOpen(): void { + this.setCalendarView('calendar'); + this.ensureCustomHeader(); + this.repositionCalendar(); + this.attachPositionListeners(); + } + + onClose(): void { + this.setCalendarView('calendar'); + this.removePositionListeners(); + } + + onMonthChange(): void { + this.ensureCustomHeader(); + } + + onYearChange(): void { + this.ensureCustomHeader(); + } + + repositionCalendar(): void { + if (!this.fpInstance || !this.fpInstance.calendarContainer || !this.fpInstance.isOpen) return; + + const calendar = this.fpInstance.calendarContainer as HTMLElement; + const inputEl = (this.nativeInputRef?.nativeElement || this.fpInstance.element || this.fpInstance._input) as HTMLElement; + + if (!calendar || !inputEl) return; + + const rect = inputEl.getBoundingClientRect(); + const calendarHeight = calendar.offsetHeight || 310; + const calendarWidth = calendar.offsetWidth || 300; + const viewportHeight = window.innerHeight; + const viewportWidth = window.innerWidth; + + const spaceBelow = viewportHeight - rect.bottom; + const spaceAbove = rect.top; + + const showAbove = spaceBelow < calendarHeight && spaceAbove >= calendarHeight; + + let top: number; + if (showAbove) { + top = window.scrollY + rect.top - calendarHeight - 6; + calendar.classList.add('arrowBottom'); + calendar.classList.remove('arrowTop'); + } else { + top = window.scrollY + rect.bottom + 6; + calendar.classList.add('arrowTop'); + calendar.classList.remove('arrowBottom'); + } + + let left = window.scrollX + rect.left; + if (left + calendarWidth > viewportWidth - 12) { + left = Math.max(12, window.scrollX + viewportWidth - calendarWidth - 12); + } + + calendar.style.position = 'absolute'; + calendar.style.top = `${Math.max(0, top)}px`; + calendar.style.left = `${Math.max(0, left)}px`; + calendar.style.right = 'auto'; + calendar.style.zIndex = '99999'; + } + + private attachPositionListeners(): void { + window.addEventListener('resize', this.scrollResizeHandler, { passive: true }); + window.addEventListener('scroll', this.scrollResizeHandler, { capture: true, passive: true }); + } + + private removePositionListeners(): void { + window.removeEventListener('resize', this.scrollResizeHandler); + window.removeEventListener('scroll', this.scrollResizeHandler, { capture: true }); + } + + private isSameValue(v1: any, v2: any): boolean { + if (v1 === v2) return true; + if (v1 == null && v2 == null) return true; + if (v1 == null || v2 == null) return false; + if (typeof v1 === 'string' && typeof v2 === 'string') return v1.trim() === v2.trim(); + if (typeof v1 === 'object' && typeof v2 === 'object') { + if ('from' in v1 && 'from' in v2) { + return v1.from === v2.from && v1.to === v2.to; + } + } + return false; + } + + private initViewContainer(): void { + if (!this.fpInstance || !this.fpInstance.calendarContainer) return; + const container = this.fpInstance.calendarContainer as HTMLElement; + + if (container.querySelector('.spk-calendar-view-container')) { + this.viewContainerEl = container.querySelector('.spk-calendar-view-container'); + return; + } + + container.style.position = 'relative'; + + const viewContainer = document.createElement('div'); + viewContainer.className = 'spk-calendar-view-container absolute left-1 right-1 top-[2.4rem] h-auto z-[1000] bg-white dark:bg-[#1e293b] p-1.5 rounded-lg border border-defaultborder dark:border-white/10 shadow-2xl flex flex-col animate-fade-in'; + viewContainer.style.display = 'none'; + + container.appendChild(viewContainer); + this.viewContainerEl = viewContainer; + + this.registerIgnoredElement(viewContainer); + + const preventBubbling = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + }; + + viewContainer.addEventListener('mousedown', preventBubbling); + viewContainer.addEventListener('mouseup', preventBubbling); + viewContainer.addEventListener('click', (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + this.handleContainerClick(e); + }); + } + + private ensureCustomHeader(): void { + if (!this.fpInstance || !this.fpInstance.calendarContainer) return; + const container = this.fpInstance.calendarContainer as HTMLElement; + + const currentMonthContainer = container.querySelector('.flatpickr-current-month'); + if (!currentMonthContainer) return; + + const nativeElements = currentMonthContainer.querySelectorAll( + '.flatpickr-monthDropdown-months, .numInputWrapper, .cur-year, .cur-month' + ); + nativeElements.forEach(el => { + (el as HTMLElement).style.display = 'none'; + }); + + let pillsWrapper = currentMonthContainer.querySelector('.spk-header-pills-wrapper') as HTMLElement | null; + + if (!pillsWrapper) { + pillsWrapper = document.createElement('div'); + pillsWrapper.className = 'spk-header-pills-wrapper flex items-center justify-center gap-1.5'; + + const monthPill = document.createElement('button'); + monthPill.type = 'button'; + monthPill.className = 'spk-month-pill-btn px-2.5 py-1 text-xs font-semibold rounded-md bg-white dark:bg-[#1e293b] text-primary shadow-xs border border-primary/20 hover:border-primary/40 hover:bg-primary/5 transition-all flex items-center gap-1 cursor-pointer'; + monthPill.setAttribute('aria-label', 'Select Month'); + + const yearPill = document.createElement('button'); + yearPill.type = 'button'; + yearPill.className = 'spk-year-pill-btn px-2.5 py-1 text-xs font-semibold rounded-md bg-white dark:bg-[#1e293b] text-primary shadow-xs border border-primary/20 hover:border-primary/40 hover:bg-primary/5 transition-all flex items-center gap-1 cursor-pointer'; + yearPill.setAttribute('aria-label', 'Select Year'); + + pillsWrapper.appendChild(monthPill); + pillsWrapper.appendChild(yearPill); + currentMonthContainer.appendChild(pillsWrapper); + + this.registerIgnoredElement(monthPill); + this.registerIgnoredElement(yearPill); + this.registerIgnoredElement(pillsWrapper); + + const preventFocusLoss = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleHeaderInteraction = (e: Event, targetView: CalendarViewMode) => { + e.preventDefault(); + e.stopPropagation(); + this.setCalendarView(this.viewMode === targetView ? 'calendar' : targetView); + }; + + monthPill.addEventListener('mousedown', preventFocusLoss); + monthPill.addEventListener('mouseup', preventFocusLoss); + monthPill.addEventListener('click', (e) => handleHeaderInteraction(e, 'month')); + + yearPill.addEventListener('mousedown', preventFocusLoss); + yearPill.addEventListener('mouseup', preventFocusLoss); + yearPill.addEventListener('click', (e) => handleHeaderInteraction(e, 'year')); + } + + const currentMonthIdx = this.fpInstance.currentMonth; + const currentYear = this.fpInstance.currentYear; + const monthName = this.monthsList[currentMonthIdx]?.name || 'Month'; + + const monthPillBtn = pillsWrapper.querySelector('.spk-month-pill-btn'); + const yearPillBtn = pillsWrapper.querySelector('.spk-year-pill-btn'); + + if (monthPillBtn) { + monthPillBtn.innerHTML = `${monthName} `; + } + if (yearPillBtn) { + yearPillBtn.innerHTML = `${currentYear} `; + } + } + + private setCalendarView(mode: CalendarViewMode): void { + this.viewMode = mode; + if (!this.viewContainerEl) return; + + if (mode === 'calendar') { + this.viewContainerEl.style.display = 'none'; + this.viewContainerEl.innerHTML = ''; + return; + } + + this.viewContainerEl.style.display = 'flex'; + if (mode === 'month') { + this.renderMonthContent(); + } else if (mode === 'year') { + this.yearPageOffset = 0; + this.renderYearContent(); + } + } + + private renderMonthContent(): void { + if (!this.viewContainerEl || !this.fpInstance) return; + + const currentMonth = this.fpInstance.currentMonth; + const currentYear = this.fpInstance.currentYear; + + let monthsHtml = ''; + this.monthsList.forEach(m => { + const isCurrent = m.index === currentMonth; + const disabled = this.isMonthDisabled(m.index, currentYear); + const activeClass = isCurrent + ? 'bg-primary text-white border-primary font-bold shadow-xs scale-[1.02]' + : 'bg-white dark:bg-[#1e293b] text-slate-700 dark:text-slate-200 border-slate-200/80 dark:border-white/10 hover:border-primary/50 hover:bg-primary/5 hover:text-primary'; + const disabledClass = disabled ? 'opacity-30 cursor-not-allowed pointer-events-none' : 'cursor-pointer'; + + monthsHtml += ` + + `; + }); + + this.viewContainerEl.innerHTML = ` +
+ Select Month + +
+
+ ${monthsHtml} +
+ `; + } + + private renderYearContent(): void { + if (!this.viewContainerEl || !this.fpInstance) return; + + const currentYear = this.fpInstance.currentYear; + const baseYear = Math.floor(currentYear / 12) * 12 + (this.yearPageOffset * 12); + const startYear = baseYear; + const endYear = baseYear + 11; + + let yearsHtml = ''; + const minYr = this.getMinYear(); + const maxYr = this.getMaxYear(); + + for (let y = startYear; y <= endYear; y++) { + const isCurrent = y === currentYear; + const disabled = (minYr !== null && y < minYr) || (maxYr !== null && y > maxYr); + const activeClass = isCurrent + ? 'bg-primary text-white border-primary font-bold shadow-xs scale-[1.02]' + : 'bg-white dark:bg-[#1e293b] text-slate-700 dark:text-slate-200 border-slate-200/80 dark:border-white/10 hover:border-primary/50 hover:bg-primary/5 hover:text-primary'; + const disabledClass = disabled ? 'opacity-30 cursor-not-allowed pointer-events-none' : 'cursor-pointer'; + + yearsHtml += ` + + `; + } + + this.viewContainerEl.innerHTML = ` +
+
+ + ${startYear} - ${endYear} + +
+ +
+
+ ${yearsHtml} +
+ `; + } + + private handleContainerClick(e: Event): void { + const target = e.target as HTMLElement; + const actionEl = target.closest('[data-action]') as HTMLElement; + if (!actionEl) return; + + const action = actionEl.getAttribute('data-action'); + + if (action === 'select-month') { + const monthIdx = parseInt(actionEl.getAttribute('data-month') || '0', 10); + this.fpInstance.changeMonth(monthIdx, false); + this.setCalendarView('calendar'); + this.ensureCustomHeader(); + } else if (action === 'select-year') { + const year = parseInt(actionEl.getAttribute('data-year') || '0', 10); + this.fpInstance.changeYear(year); + this.setCalendarView('calendar'); + this.ensureCustomHeader(); + } else if (action === 'prev-year-page') { + this.yearPageOffset--; + this.renderYearContent(); + } else if (action === 'next-year-page') { + this.yearPageOffset++; + this.renderYearContent(); + } else if (action === 'close') { + this.setCalendarView('calendar'); + } + } + + private isMonthDisabled(monthIndex: number, currentYear: number): boolean { + const minYr = this.getMinYear(); + const maxYr = this.getMaxYear(); + const minMo = this.getMinMonth(); + const maxMo = this.getMaxMonth(); + + if (minYr !== null && currentYear < minYr) return true; + if (maxYr !== null && currentYear > maxYr) return true; + if (minYr !== null && currentYear === minYr && minMo !== null && monthIndex < minMo) return true; + if (maxYr !== null && currentYear === maxYr && maxMo !== null && maxMo < monthIndex) return true; + + return false; + } + + private getMinYear(): number | null { + const min = this.minDate(); + if (!min) return null; + if (typeof min === 'string') return new Date(min).getFullYear(); + if (min instanceof Date) return min.getFullYear(); + return null; + } + + private getMaxYear(): number | null { + const max = this.maxDate(); + if (!max) return null; + if (typeof max === 'string') return new Date(max).getFullYear(); + if (max instanceof Date) return max.getFullYear(); + return null; + } + + private getMinMonth(): number | null { + const min = this.minDate(); + if (!min) return null; + if (typeof min === 'string') return new Date(min).getMonth(); + if (min instanceof Date) return min.getMonth(); + return null; + } + + private getMaxMonth(): number | null { + const max = this.maxDate(); + if (!max) return null; + if (typeof max === 'string') return new Date(max).getMonth(); + if (max instanceof Date) return max.getMonth(); + return null; + } +} diff --git a/src/app/features/authentication/pages/login/login.html b/src/app/features/authentication/pages/login/login.html index 88695cd8..cb60c4c7 100644 --- a/src/app/features/authentication/pages/login/login.html +++ b/src/app/features/authentication/pages/login/login.html @@ -1,85 +1,119 @@
- -
-
+ class="grid grid-cols-12 min-h-screen h-screen overflow-hidden bg-[#f4f2fb] text-defaulttextcolor font-sans relative p-0"> -
- - logo - -
-
-
-

Sign In

-

- Welcome back ! -

-
- @if (loginError) { -
{{ loginError }}
- } + +
+ + + +
-
-
- - -
+ + -
- + +
- - Forgot password ? - + +
-
- + +
- -
- -
-
- - - -
-
-
- -
- -
-
- - -
-
+ +
+
+ +
+

Sign In

+

Welcome back! Please enter your details.

+ + +
+ @if (loginError) { +
+ {{ loginError }} +
+ } + + +
+ + +
+ + +
+
+ + + Forgot password? + +
+
+ + +
+
+ + +
+ + +
+ + +
+ +
+
+ + +
-
+
+
\ No newline at end of file diff --git a/src/app/features/authentication/pages/login/login.scss b/src/app/features/authentication/pages/login/login.scss index 263aee1c..6002384d 100644 --- a/src/app/features/authentication/pages/login/login.scss +++ b/src/app/features/authentication/pages/login/login.scss @@ -1,15 +1,40 @@ -::ng-deep{ +.login-input { + background-color: #ffffff !important; + color: #0f172a !important; + border-color: #cbd5e1 !important; + &::placeholder { + color: #94a3b8 !important; + } - .firebase { - width: 30px; - height: 30px; - } - - .rounded-lg{ - width: 120px; - justify-content: center; - margin: auto; - border-radius: 0.75rem; - } - } + &:-webkit-autofill, + &:-webkit-autofill:hover, + &:-webkit-autofill:focus, + &:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px #ffffff inset !important; + -webkit-text-fill-color: #0f172a !important; + caret-color: #0f172a !important; + transition: background-color 5000s ease-in-out 0s; + } + + &:focus { + background-color: #ffffff !important; + color: #0f172a !important; + border-color: #a855f7 !important; + box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.25) !important; + } +} + +::ng-deep { + .firebase { + width: 30px; + height: 30px; + } + + .rounded-lg { + width: 120px; + justify-content: center; + margin: auto; + border-radius: 0.75rem; + } +} \ No newline at end of file diff --git a/src/app/features/authentication/pages/login/login.ts b/src/app/features/authentication/pages/login/login.ts index b2c3eb8b..c35d7b02 100644 --- a/src/app/features/authentication/pages/login/login.ts +++ b/src/app/features/authentication/pages/login/login.ts @@ -21,7 +21,7 @@ export class Login { private readonly fallbackRoute = '/dashboards/crm'; public readonly adminLoginForm = this.formBuilder.nonNullable.group({ - username: ['', [Validators.required, Validators.email]], + username: ['superadmin@syscom.dev', [Validators.required]], password: ['', Validators.required], rememberMe: [false] }); diff --git a/src/app/features/global-masters/cities/pages/city-list/city-list.html b/src/app/features/global-masters/cities/pages/city-list/city-list.html index ea08ca13..f08ad1b1 100644 --- a/src/app/features/global-masters/cities/pages/city-list/city-list.html +++ b/src/app/features/global-masters/cities/pages/city-list/city-list.html @@ -1,63 +1,3 @@ - -
-
- -
- -
- -
-
- -
-
-
- + (filterClicked)="onToggleFilters()"> + +
+
+ +
+ +
+ +
+
+ + +
+
+
+
(null); readonly deleteConfirmDialog = viewChild(ConfirmDialog); + readonly showFilters = signal(false); + readonly filterForm = this.formBuilder.nonNullable.group({ countryId: [''], stateId: [{ value: '', disabled: true }] @@ -258,4 +259,9 @@ export class CityList implements OnInit { } }); } + + onToggleFilters(): void { + this.showFilters.update(value => !value); + } } + diff --git a/src/app/features/global-masters/states/pages/state-list/state-list.html b/src/app/features/global-masters/states/pages/state-list/state-list.html index 4dbbdd43..e0acaa24 100644 --- a/src/app/features/global-masters/states/pages/state-list/state-list.html +++ b/src/app/features/global-masters/states/pages/state-list/state-list.html @@ -1,75 +1,34 @@ - -
-
- -
-
- -
-
-
+ + +
+
+ +
+
+ + +
+
+
+
- + - - - + \ No newline at end of file diff --git a/src/app/features/global-masters/states/pages/state-list/state-list.ts b/src/app/features/global-masters/states/pages/state-list/state-list.ts index 075a46aa..c5a01839 100644 --- a/src/app/features/global-masters/states/pages/state-list/state-list.ts +++ b/src/app/features/global-masters/states/pages/state-list/state-list.ts @@ -8,7 +8,7 @@ import { catchError, finalize, map } from 'rxjs/operators'; import { CountryLookupDto, CountryService } from '../../../countries/public-api'; import { StateDto, UpdateStateRequest } from '../../models/state.model'; import { StateService } from '../../data-access/state.service'; -import { DataTable } from '../../../../../shared/components/data-table/data-table'; +import { DataTable, DataTableToolbarDirective } from '../../../../../shared/components/data-table/data-table'; import { DataTableStore } from '../../../../../shared/components/data-table/data-table.store'; import { DataTableAction, @@ -24,7 +24,6 @@ import { AutocompleteValueFn } from '../../../../../shared/components/form/autocomplete/autocomplete.types'; import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog'; -import { FilterCard } from '../../../../../shared/components/filter-card/filter-card'; import { Button as AppButton } from '../../../../../shared/components/button/button'; import { StateFormModalComponent } from '../../components/state-form-modal/state-form-modal'; @@ -44,10 +43,10 @@ interface StateTableRow extends DataTableRecord { standalone: true, imports: [ DataTable, + DataTableToolbarDirective, ReactiveFormsModule, Autocomplete, ConfirmDialog, - FilterCard, AppButton, StateFormModalComponent ], @@ -69,6 +68,8 @@ export class StateList implements OnInit { readonly pendingDeleteState = signal(null); readonly deleteConfirmDialog = viewChild(ConfirmDialog); + readonly showFilters = signal(false); + readonly countryFilterForm = this.formBuilder.nonNullable.group({ countryId: [''] }); @@ -203,4 +204,8 @@ export class StateList implements OnInit { } }); } + + onToggleFilters(): void { + this.showFilters.update(value => !value); + } } diff --git a/src/app/features/organizations/organization-list/organization-list.html b/src/app/features/organizations/organization-list/organization-list.html index e32a2627..2fe0c7fa 100644 --- a/src/app/features/organizations/organization-list/organization-list.html +++ b/src/app/features/organizations/organization-list/organization-list.html @@ -1,45 +1,3 @@ -
-
- -
-
- -
-
- -
-
-
-
-
- + (filterClicked)="onToggleFilters()" + toolTip="Add Organization"> + +
+
+ +
+
+ + +
+
+
+
+ diff --git a/src/app/features/organizations/organization-list/organization-list.ts b/src/app/features/organizations/organization-list/organization-list.ts index 6c685454..7be4918a 100644 --- a/src/app/features/organizations/organization-list/organization-list.ts +++ b/src/app/features/organizations/organization-list/organization-list.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { of } from 'rxjs'; -import { DataTable } from '../../../shared/components/data-table/data-table'; +import { DataTable, DataTableToolbarDirective } from '../../../shared/components/data-table/data-table'; import { DataTableStore } from '../../../shared/components/data-table/data-table.store'; import { DataTableAction, @@ -20,7 +20,6 @@ import { AutocompleteSearchFn, AutocompleteValueFn, } from '../../../shared/components/form/autocomplete/autocomplete.types'; -import { FilterCard } from '../../../shared/components/filter-card/filter-card'; import { Button } from '../../../shared/components/button/button'; import { CountryLookupDto, CountryService } from '../../global-masters/countries/public-api'; @@ -40,7 +39,7 @@ interface OrganizationListRow extends DataTableRecord { @Component({ selector: 'organization-list', standalone: true, - imports: [ReactiveFormsModule, FilterCard, Autocomplete, DataTable, Button], + imports: [ReactiveFormsModule, Autocomplete, DataTable, DataTableToolbarDirective, Button], providers: [DataTableStore], templateUrl: './organization-list.html', styleUrl: './organization-list.scss', @@ -54,6 +53,7 @@ export class OrganizationList implements OnInit { readonly selectedCountryLookup = signal(null); readonly appliedCountryId = signal(null); + readonly showFilters = signal(false); readonly countryFilterForm = this.formBuilder.nonNullable.group({ countryId: [''], @@ -268,6 +268,17 @@ export class OrganizationList implements OnInit { void this.router.navigate(['/organizations/onboarding']); } + onResetFilter(): void { + this.countryFilterForm.reset({ countryId: '' }); + this.selectedCountryLookup.set(null); + this.appliedCountryId.set(null); + this.tableStore.reset(); + } + + onToggleFilters(): void { + this.showFilters.update(value => !value); + } + onActionClick(event: DataTableActionEvent): void { if (event.action.type === 'edit' || (event.row.status as string) === 'Draft') { void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } }); @@ -276,3 +287,4 @@ export class OrganizationList implements OnInit { this.toastr.info(`${event.action.label} clicked for ${event.row.organizationName}`); } } + diff --git a/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html b/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html index 4d5ef8e4..30391530 100644 --- a/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html +++ b/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html @@ -2,12 +2,15 @@
-
+
- + {{ currentStepIndex() + 1 }} - Step {{ currentStepIndex() + 1 }} of {{ steps().length }}: {{ currentStepLabel() }} + Step {{ currentStepIndex() + 1 }} of {{ steps().length }}: {{ currentStepLabel() }} {{ getProgressPercent() }}% @@ -18,85 +21,80 @@
@@ -106,144 +104,52 @@
-
+
-