Merge pull request 'new design implement for login screen, organization onboarding form changes, new filter implement in all screens, remove icons from validation,' (#8) from feature/onboarding-workflow-sc into dev
Reviewed-on: sc/syscom-master-admin-ui#8
This commit is contained in:
+2
-1
@@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+22
@@ -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",
|
||||
|
||||
+3
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,29 @@
|
||||
<input
|
||||
#nativeInput
|
||||
[id]="id()"
|
||||
type="text"
|
||||
[placeholder]="placeholder()"
|
||||
[ngModel]="value"
|
||||
(ngModelChange)="onValueChange($event)"
|
||||
(focus)="onInputFocus()"
|
||||
(blur)="onInputBlur()"
|
||||
mwlFlatpickr
|
||||
(flatpickrReady)="onReady($event)"
|
||||
(flatpickrChange)="onFlatpickrChange($event)"
|
||||
(flatpickrOpen)="onOpen()"
|
||||
(flatpickrClose)="onClose()"
|
||||
(flatpickrMonthChange)="onMonthChange()"
|
||||
(flatpickrYearChange)="onYearChange()"
|
||||
[class]="class()"
|
||||
[mode]="mode()"
|
||||
[inline]="inline()"
|
||||
[noCalendar]="noCalendar()"
|
||||
[altInput]="altInput()"
|
||||
[convertModelValue]="false"
|
||||
[enableTime]="enableTime()"
|
||||
[dateFormat]="dateFormat()"
|
||||
[minDate]="minDate()!"
|
||||
[maxDate]="maxDate()!"
|
||||
[disabled]="disabled"
|
||||
[readonly]="readonly()"
|
||||
/>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLInputElement>;
|
||||
|
||||
readonly id = input<string>('');
|
||||
readonly altInput = input<boolean>(false);
|
||||
readonly convertModelValue = input<boolean>(false);
|
||||
readonly enableTime = input<boolean>(false);
|
||||
readonly noCalendar = input<boolean>(false);
|
||||
readonly inline = input<boolean>(false);
|
||||
readonly class = input<string>('');
|
||||
readonly dateFormat = input<string>('Y-m-d');
|
||||
readonly placeholder = input<string>('');
|
||||
readonly mode = input<SpkFlatpickrMode>('single');
|
||||
readonly minDate = input<string | Date | undefined>(undefined);
|
||||
readonly maxDate = input<string | Date | undefined>(undefined);
|
||||
readonly readonly = input<boolean>(false);
|
||||
|
||||
@Output() focus = new EventEmitter<void>();
|
||||
@Output() blur = new EventEmitter<void>();
|
||||
|
||||
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 = `<span>${monthName}</span> <i class="ri-arrow-down-s-line text-sm opacity-70"></i>`;
|
||||
}
|
||||
if (yearPillBtn) {
|
||||
yearPillBtn.innerHTML = `<span>${currentYear}</span> <i class="ri-arrow-down-s-line text-sm opacity-70"></i>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 += `
|
||||
<button type="button" class="spk-month-btn py-1 px-1 text-[0.75rem] rounded-md font-medium transition-all text-center border ${activeClass} ${disabledClass}" data-action="select-month" data-month="${m.index}">
|
||||
${m.shortName}
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
|
||||
this.viewContainerEl.innerHTML = `
|
||||
<div class="flex items-center justify-between px-0.5 pb-1 mb-1 border-b border-slate-200 dark:border-white/10">
|
||||
<span class="text-[0.7rem] font-bold text-primary dark:text-primary-light uppercase tracking-wider ps-1">Select Month</span>
|
||||
<button type="button" class="spk-close-btn p-0.5 text-slate-400 hover:text-red-500 dark:hover:text-red-400 rounded-md cursor-pointer transition-colors" data-action="close">
|
||||
<i class="ri-close-line text-xs pointer-events-none"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-1 p-1 bg-slate-50/70 dark:bg-white/5 rounded-lg border border-slate-200/60 dark:border-white/10">
|
||||
${monthsHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 += `
|
||||
<button type="button" class="spk-year-btn py-1 px-1 text-[0.75rem] rounded-md font-medium transition-all text-center border ${activeClass} ${disabledClass}" data-action="select-year" data-year="${y}">
|
||||
${y}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
this.viewContainerEl.innerHTML = `
|
||||
<div class="flex items-center justify-between px-0.5 pb-1 mb-1 border-b border-slate-200 dark:border-white/10">
|
||||
<div class="flex items-center gap-1.5 mx-auto">
|
||||
<button type="button" class="spk-prev-years-btn px-1.5 py-0.5 text-primary bg-primary/10 dark:bg-primary/20 hover:bg-primary hover:text-white rounded-md transition-all cursor-pointer flex items-center justify-center border border-primary/20" data-action="prev-year-page" title="Previous 12 years">
|
||||
<i class="ri-arrow-left-s-line text-[0.65rem] font-bold pointer-events-none"></i>
|
||||
</button>
|
||||
<span class="text-[0.7rem] font-bold text-primary dark:text-primary-light uppercase tracking-wider px-1.5 py-0.5 rounded-md bg-primary/5 dark:bg-primary/10 border border-primary/15">${startYear} - ${endYear}</span>
|
||||
<button type="button" class="spk-next-years-btn px-1.5 py-0.5 text-primary bg-primary/10 dark:bg-primary/20 hover:bg-primary hover:text-white rounded-md transition-all cursor-pointer flex items-center justify-center border border-primary/20" data-action="next-year-page" title="Next 12 years">
|
||||
<i class="ri-arrow-right-s-line text-[0.65rem] font-bold pointer-events-none"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="spk-close-btn p-0.5 text-slate-400 hover:text-red-500 dark:hover:text-red-400 rounded-md cursor-pointer transition-colors" data-action="close">
|
||||
<i class="ri-close-line text-xs pointer-events-none"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-1 p-1 bg-slate-50/70 dark:bg-white/5 rounded-lg border border-slate-200/60 dark:border-white/10">
|
||||
${yearsHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +1,119 @@
|
||||
<div
|
||||
class="grid grid-cols-12 h-screen min-h-screen overflow-hidden bg-bodybg dark:bg-bodybg text-defaulttextcolor text-defaultsize">
|
||||
<div class="hidden xl:block xl:col-span-6 h-screen"> <img src="./assets/images/authentication/auth-cover.png"
|
||||
class="w-full h-full object-cover object-left" alt="login cover" /> </div>
|
||||
<div class="xl:col-span-6 col-span-12 min-h-[100dvh] flex items-center justify-center px-6 bg-bodybg dark:bg-bodybg">
|
||||
<div class="authentication authentication-basic w-full max-w-[420px]">
|
||||
class="grid grid-cols-12 min-h-screen h-screen overflow-hidden bg-[#f4f2fb] text-defaulttextcolor font-sans relative p-0">
|
||||
|
||||
<div class="my-[2.5rem] flex justify-center">
|
||||
<img src="./assets/images/brand-logos/erp-logo-icon.png" alt="logo" class="desktop-logo">
|
||||
<img src="./assets/images/brand-logos/erp-logo-icon.png" alt="logo" class="desktop-dark">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="box !mb-0">
|
||||
<div class="box-body !p-[3rem]">
|
||||
<p class="h5 font-semibold mb-2 text-center">Sign In</p>
|
||||
<p class="mb-4 text-[#8c9097] dark:text-white/50 opacity-[0.7] font-normal text-center">
|
||||
Welcome back !
|
||||
</p>
|
||||
|
||||
<form [formGroup]="adminLoginForm" (ngSubmit)="login()">
|
||||
@if (loginError) {
|
||||
<div class="text-danger mb-3">{{ loginError }}</div>
|
||||
}
|
||||
<!-- Extreme Lower Right Light Purple Spark Graphic -->
|
||||
<div class="fixed bottom-8 right-10 pointer-events-none opacity-60 z-0">
|
||||
<svg width="42" height="42" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0L14.59 9.41L24 12L14.59 14.59L12 24L9.41 14.59L0 12L9.41 9.41L12 0Z" fill="#c3b4f5" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-y-4">
|
||||
<div class="xl:col-span-12 col-span-12">
|
||||
<label for="signin-username" class="form-label text-default">User Name</label>
|
||||
<input type="text" class="form-control form-control-lg w-full !rounded-md"
|
||||
id="signin-username" placeholder="user name" formControlName="username"
|
||||
autocomplete="username">
|
||||
</div>
|
||||
<!-- Left Marketing Panel (60% Width) - Full Banner Image (Straight left, rounded right corners) -->
|
||||
<div
|
||||
class="hidden lg:flex lg:col-span-7 xl:col-span-7 relative h-full w-full overflow-hidden items-center justify-center bg-white rounded-r-[32px]">
|
||||
<img src="assets/images/authentication/auth-cover.png" alt="Biz360 Banner"
|
||||
class="w-full h-full object-cover object-center rounded-r-[32px]" />
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-12 col-span-12 mb-2">
|
||||
<label for="signin-password" class="form-label text-default block">Password</label>
|
||||
<!-- Right Login Panel (40% Width) -->
|
||||
<div
|
||||
class="lg:col-span-5 xl:col-span-5 col-span-12 min-h-[100dvh] h-full flex items-center justify-center p-4 sm:p-6 relative overflow-y-auto z-10 bg-[#f4f2fb]">
|
||||
|
||||
<a routerLink="/authentication/reset-password/basic"
|
||||
class="ltr:float-right rtl:float-left text-danger">
|
||||
Forgot password ?
|
||||
</a>
|
||||
<!-- Central Floating Card Encased in Lavender-Purple Frame -->
|
||||
<div
|
||||
class="w-full max-w-[420px] bg-[#c3b4f5] p-[14px] sm:p-[16px] rounded-[34px] shadow-xl transition-all duration-300 relative z-10">
|
||||
|
||||
<div class="input-group">
|
||||
<input class="form-control form-control-lg !rounded-s-md" id="signin-password"
|
||||
placeholder="password" formControlName="password"
|
||||
autocomplete="current-password"
|
||||
[type]="visibilityMap['Angular'] ? 'text' : 'password'">
|
||||
<!-- Inner White Card Area -->
|
||||
<div class="bg-white rounded-[24px] px-6 sm:px-8 py-8 sm:py-9 shadow-inner flex flex-col">
|
||||
|
||||
<button aria-label="button" class="ti-btn ti-btn-light !rounded-s-none !mb-0"
|
||||
type="button" (click)="toggleVisibility('Angular')" id="button-addon2">
|
||||
<i class="{{ iconMap['Angular'] }} align-middle"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="form-check !ps-0">
|
||||
<input class="form-check-input" type="checkbox" id="defaultCheck1"
|
||||
formControlName="rememberMe">
|
||||
|
||||
<label class="form-check-label text-[#8c9097] dark:text-white/50 font-normal"
|
||||
for="defaultCheck1">
|
||||
Remember password ?
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-12 col-span-12 grid mt-2">
|
||||
<button type="submit" [disabled]="adminLoginForm.invalid || isSubmitting"
|
||||
class="ti-btn ti-btn-primary !bg-primary !text-white !font-medium inline-flex items-center justify-center gap-2">
|
||||
|
||||
<span [style.display]="isSubmitting ? '' : 'none'" class="ti-spinner text-white"
|
||||
role="status" aria-label="loading"></span>
|
||||
<span [style.display]="isSubmitting ? '' : 'none'">Signing In...</span>
|
||||
<span [style.display]="isSubmitting ? 'none' : ''">Sign In</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Stylized Gradient Purple-and-Indigo Winged 'V' Logo & Header -->
|
||||
<div class="flex flex-col items-center text-center mb-6">
|
||||
<div class="mb-3 flex justify-center">
|
||||
<img src="./assets/images/brand-logos/erp-logo-icon.png" alt="logo" class="desktop-logo">
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-bold text-slate-900 mb-1">Sign In</h3>
|
||||
<p class="text-xs sm:text-sm text-slate-400 font-normal">Welcome back! Please enter your details.</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form [formGroup]="adminLoginForm" (ngSubmit)="login()" class="space-y-4">
|
||||
@if (loginError) {
|
||||
<div class="p-3 text-xs text-red-600 bg-red-50 border border-red-200 rounded-lg">
|
||||
{{ loginError }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- User Name Field (White Input Background) -->
|
||||
<div>
|
||||
<label for="signin-username" class="block text-sm font-semibold text-slate-800 mb-1.5">User Name</label>
|
||||
<input type="text" id="signin-username" placeholder="user name" formControlName="username"
|
||||
autocomplete="username"
|
||||
class="login-input w-full px-4 py-3 bg-white border border-slate-300 rounded-xl text-base font-semibold text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-400/50 focus:border-purple-500 focus:bg-white transition-all duration-200" />
|
||||
</div>
|
||||
|
||||
<!-- Password Field (White Input Background & Visibility Toggle) -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label for="signin-password" class="block text-sm font-semibold text-slate-800">Password</label>
|
||||
<a routerLink="/authentication/reset-password/basic"
|
||||
class="text-xs sm:text-sm font-semibold text-[#7c3aed] hover:text-[#6d28d9] transition-colors">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
<div class="relative flex items-center">
|
||||
<input [type]="visibilityMap['Angular'] ? 'text' : 'password'" id="signin-password" placeholder="password"
|
||||
formControlName="password" autocomplete="current-password"
|
||||
class="login-input w-full pl-4 pr-11 py-3 bg-white border border-slate-300 rounded-xl text-base font-semibold text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-400/50 focus:border-purple-500 focus:bg-white transition-all duration-200" />
|
||||
<button type="button" (click)="toggleVisibility('Angular')"
|
||||
class="absolute right-3.5 text-slate-400 hover:text-slate-600 focus:outline-none p-1 transition-colors"
|
||||
aria-label="Toggle password visibility">
|
||||
@if (visibilityMap['Angular']) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path
|
||||
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24">
|
||||
</path>
|
||||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Password Checkbox -->
|
||||
<div class="flex items-center gap-2 pt-1 pb-1">
|
||||
<input type="checkbox" id="rememberMe" formControlName="rememberMe"
|
||||
class="w-4 h-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500 cursor-pointer" />
|
||||
<label for="rememberMe" class="text-xs sm:text-sm text-slate-700 font-semibold cursor-pointer select-none">
|
||||
Remember password?
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Action Button: Gradient Purple-to-Pink -->
|
||||
<div class="pt-2">
|
||||
<button type="submit" [disabled]="isSubmitting"
|
||||
class="w-full py-3.5 px-4 bg-gradient-to-r from-[#7c3aed] via-[#a855f7] to-[#d946ef] text-white font-bold text-base rounded-xl shadow-lg shadow-purple-500/30 hover:shadow-purple-500/45 hover:opacity-95 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center gap-2">
|
||||
@if (isSubmitting) {
|
||||
<span class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
|
||||
<span>Signing In...</span>
|
||||
} @else {
|
||||
<span>Sign In</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
});
|
||||
|
||||
@@ -1,63 +1,3 @@
|
||||
<app-filter-card title="Filter" titleIcon="ti ti-filter" headerClass="!py-2" bodyClass="!px-4 !py-2.5">
|
||||
<form [formGroup]="filterForm" (ngSubmit)="onApplyFilter($event)" autocomplete="off" class="grid w-full grid-cols-12 items-end gap-3">
|
||||
<div class="col-span-12 sm:col-span-5 lg:col-span-2">
|
||||
<app-autocomplete
|
||||
formControlName="countryId"
|
||||
inputId="city-country-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="Country"
|
||||
placeholder="Search country"
|
||||
[searchFn]="filterCountrySearchFn"
|
||||
[valueWith]="filterCountryValueFn"
|
||||
[displayWith]="filterCountryDisplayFn"
|
||||
[selectedItem]="selectedCountry()"
|
||||
[minSearchLength]="0"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterCountryChanged($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 sm:col-span-5 lg:col-span-2">
|
||||
<app-autocomplete
|
||||
formControlName="stateId"
|
||||
inputId="city-state-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="State"
|
||||
placeholder="Search state"
|
||||
[searchFn]="filterStateSearchFn"
|
||||
[valueWith]="filterStateValueFn"
|
||||
[displayWith]="filterStateDisplayFn"
|
||||
[selectedItem]="selectedFilterState()"
|
||||
[minSearchLength]="0"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterStateChanged($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-2 lg:col-span-1">
|
||||
<app-button
|
||||
action="custom"
|
||||
label="Filter"
|
||||
icon="ti ti-filter"
|
||||
variant="primary-full"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8 w-full md:!w-auto"
|
||||
(buttonClicked)="onApplyFilter($event)"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</app-filter-card>
|
||||
|
||||
<app-data-table
|
||||
[columns]="columns()"
|
||||
[rows]="tableStore.rows()"
|
||||
@@ -69,6 +9,8 @@
|
||||
buttonTitle="Add"
|
||||
[showSearch]="true"
|
||||
[showAddButton]="true"
|
||||
[showFilterButton]="true"
|
||||
[filterActive]="showFilters()"
|
||||
searchPlaceholder="Search cities..."
|
||||
[searchDebounceTime]="300"
|
||||
toolTip="Add City"
|
||||
@@ -77,7 +19,77 @@
|
||||
(pageChanged)="tableStore.onPageChange($event)"
|
||||
(sortChanged)="tableStore.onSortChange($event)"
|
||||
(actionClicked)="onActionClick($event)"
|
||||
/>
|
||||
(filterClicked)="onToggleFilters()">
|
||||
<ng-template appDataTableToolbar>
|
||||
<form [formGroup]="filterForm" (ngSubmit)="onApplyFilter($event)" autocomplete="off"
|
||||
class="flex flex-wrap items-end gap-3 w-full">
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete
|
||||
formControlName="countryId"
|
||||
inputId="city-country-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="Country"
|
||||
placeholder="Search country"
|
||||
[searchFn]="filterCountrySearchFn"
|
||||
[valueWith]="filterCountryValueFn"
|
||||
[displayWith]="filterCountryDisplayFn"
|
||||
[selectedItem]="selectedCountry()"
|
||||
[minSearchLength]="0"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterCountryChanged($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete
|
||||
formControlName="stateId"
|
||||
inputId="city-state-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="State"
|
||||
placeholder="Search state"
|
||||
[searchFn]="filterStateSearchFn"
|
||||
[valueWith]="filterStateValueFn"
|
||||
[displayWith]="filterStateDisplayFn"
|
||||
[selectedItem]="selectedFilterState()"
|
||||
[minSearchLength]="0"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterStateChanged($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<app-button
|
||||
action="custom"
|
||||
label="Apply"
|
||||
icon="ti ti-filter"
|
||||
variant="primary-full"
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8"
|
||||
/>
|
||||
<app-button
|
||||
action="custom"
|
||||
label="Reset"
|
||||
icon="ti ti-refresh"
|
||||
variant="outline-primary"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8"
|
||||
(buttonClicked)="onResetFilter()"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
<app-confirm-dialog
|
||||
title="Delete City"
|
||||
|
||||
@@ -11,7 +11,7 @@ import { StateLookupDto } from '../../../states/models/state.model';
|
||||
import { CityService } from '../../data-access/city.service';
|
||||
import { CountryService } from '../../../countries/data-access/country.service';
|
||||
import { StateService } from '../../../states/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,
|
||||
@@ -25,7 +25,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 { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog';
|
||||
import { CityFormModalComponent } from '../../components/city-form-modal/city-form-modal';
|
||||
@@ -51,9 +50,9 @@ interface CityTableRow extends DataTableRecord {
|
||||
standalone: true,
|
||||
imports: [
|
||||
DataTable,
|
||||
DataTableToolbarDirective,
|
||||
ReactiveFormsModule,
|
||||
Autocomplete,
|
||||
FilterCard,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
CityFormModalComponent
|
||||
@@ -78,6 +77,8 @@ export class CityList implements OnInit {
|
||||
readonly pendingDeleteCity = signal<CityTableRow | null>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,34 @@
|
||||
<app-filter-card title="Filter" titleIcon="ti ti-filter" headerClass="!py-2" bodyClass="!px-4 !py-2.5">
|
||||
<form [formGroup]="countryFilterForm" (ngSubmit)="onApplyFilter()" autocomplete="off" class="grid w-full grid-cols-12 items-end gap-3">
|
||||
<div class="col-span-12 sm:col-span-5 lg:col-span-2">
|
||||
<app-autocomplete
|
||||
formControlName="countryId"
|
||||
inputId="state-country-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="Country"
|
||||
placeholder="Search"
|
||||
[searchFn]="searchCountries"
|
||||
[displayWith]="displayCountry"
|
||||
[valueWith]="countryValue"
|
||||
[selectedItem]="selectedCountryLookup()"
|
||||
[minSearchLength]="1"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterCountrySelected($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-3 lg:col-span-1">
|
||||
<app-button
|
||||
action="custom"
|
||||
label="Filter"
|
||||
icon="ti ti-filter"
|
||||
variant="primary-full"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8 w-full md:!w-auto"
|
||||
(buttonClicked)="onApplyFilter()"
|
||||
></app-button>
|
||||
</div>
|
||||
</form>
|
||||
</app-filter-card>
|
||||
<app-data-table [columns]="columns()" [rows]="tableStore.rows()" [actions]="actions()"
|
||||
[totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()"
|
||||
[pageSize]="tableStore.queryState.pageSize()" tableTitle="States" buttonTitle="Add" [showSearch]="true"
|
||||
[showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()" searchPlaceholder="Search states..."
|
||||
[searchDebounceTime]="300" toolTip="Add State" (addClicked)="onAddState()"
|
||||
(searchChanged)="tableStore.onSearch($event)" (pageChanged)="tableStore.onPageChange($event)"
|
||||
(sortChanged)="tableStore.onSortChange($event)" (actionClicked)="onActionClick($event)"
|
||||
(filterClicked)="onToggleFilters()">
|
||||
<ng-template appDataTableToolbar>
|
||||
<form [formGroup]="countryFilterForm" (ngSubmit)="onApplyFilter()" autocomplete="off"
|
||||
class="flex flex-wrap items-end gap-3 w-full">
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete formControlName="countryId" inputId="state-country-filter" variant="floating" size="sm"
|
||||
label="Country" placeholder="Search country" [searchFn]="searchCountries" [displayWith]="displayCountry"
|
||||
[valueWith]="countryValue" [selectedItem]="selectedCountryLookup()" [minSearchLength]="1" [debounceTime]="300"
|
||||
[limit]="50" [clearable]="true" [hideValidation]="true" wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterCountrySelected($event)" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<app-button action="custom" label="Apply" icon="ti ti-filter" variant="primary-full" type="submit" size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8" />
|
||||
<app-button action="custom" label="Reset" icon="ti ti-refresh" variant="outline-primary" type="button" size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8" (buttonClicked)="onResetFilter()" />
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
<app-data-table
|
||||
[columns]="columns()"
|
||||
[rows]="tableStore.rows()"
|
||||
[actions]="actions()"
|
||||
[totalRecords]="tableStore.totalRecords()"
|
||||
[pageIndex]="tableStore.queryState.pageIndex()"
|
||||
[pageSize]="tableStore.queryState.pageSize()"
|
||||
tableTitle="States"
|
||||
buttonTitle="Add"
|
||||
[showSearch]="true"
|
||||
[showAddButton]="true"
|
||||
searchPlaceholder="Search states..."
|
||||
[searchDebounceTime]="300"
|
||||
toolTip="Add State"
|
||||
(addClicked)="onAddState()"
|
||||
(searchChanged)="tableStore.onSearch($event)"
|
||||
(pageChanged)="tableStore.onPageChange($event)"
|
||||
(sortChanged)="tableStore.onSortChange($event)"
|
||||
(actionClicked)="onActionClick($event)"
|
||||
/>
|
||||
<app-confirm-dialog title="Delete State" text="Do you really want to delete this state?" confirmButtonText="Delete"
|
||||
cancelButtonText="Cancel" (confirmed)="onDeleteConfirmed()" (cancelled)="onDeleteCancelled()" />
|
||||
|
||||
<app-confirm-dialog
|
||||
title="Delete State"
|
||||
text="Do you really want to delete this state?"
|
||||
confirmButtonText="Delete"
|
||||
cancelButtonText="Cancel"
|
||||
(confirmed)="onDeleteConfirmed()"
|
||||
(cancelled)="onDeleteCancelled()"
|
||||
/>
|
||||
|
||||
<app-state-form-modal
|
||||
[open]="tableStore.showModal()"
|
||||
[mode]="tableStore.modalMode()"
|
||||
[stateId]="tableStore.selectedItem()?.id ?? null"
|
||||
(saved)="tableStore.refresh(); tableStore.closeModal()"
|
||||
(closed)="tableStore.closeModal()"
|
||||
/>
|
||||
<app-state-form-modal [open]="tableStore.showModal()" [mode]="tableStore.modalMode()"
|
||||
[stateId]="tableStore.selectedItem()?.id ?? null" (saved)="tableStore.refresh(); tableStore.closeModal()"
|
||||
(closed)="tableStore.closeModal()" />
|
||||
@@ -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<StateTableRow | null>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<div class="xl:col-span-12 col-span-12">
|
||||
<app-filter-card title="Filter" titleIcon="ti ti-filter" headerClass="!py-2" bodyClass="!px-4 !py-2.5">
|
||||
<form [formGroup]="countryFilterForm" (ngSubmit)="applyCountryFilter()" autocomplete="off" class="grid w-full grid-cols-12 items-end gap-3">
|
||||
<div class="col-span-12 sm:col-span-5 lg:col-span-2">
|
||||
<app-autocomplete
|
||||
formControlName="countryId"
|
||||
inputId="organization-country-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="Country"
|
||||
placeholder="Search"
|
||||
[searchFn]="searchCountries"
|
||||
[displayWith]="displayCountry"
|
||||
[valueWith]="countryValue"
|
||||
[selectedItem]="selectedCountryLookup()"
|
||||
[minSearchLength]="1"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onCountryLookupSelected($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-3 lg:col-span-1">
|
||||
<app-button
|
||||
action="custom"
|
||||
label="Filter"
|
||||
icon="ti ti-filter"
|
||||
variant="primary-full"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8 w-full md:!w-auto"
|
||||
(buttonClicked)="applyCountryFilter()"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</app-filter-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-data-table
|
||||
[columns]="columns()"
|
||||
[rows]="tableStore.rows()"
|
||||
@@ -52,6 +10,8 @@
|
||||
[showSearch]="true"
|
||||
rowColorMode="none"
|
||||
[showAddButton]="true"
|
||||
[showFilterButton]="true"
|
||||
[filterActive]="showFilters()"
|
||||
searchPlaceholder="Search..."
|
||||
[searchDebounceTime]="300"
|
||||
(addClicked)="onAddOrganization()"
|
||||
@@ -59,5 +19,54 @@
|
||||
(pageChanged)="tableStore.onPageChange($event)"
|
||||
(sortChanged)="tableStore.onSortChange($event)"
|
||||
(actionClicked)="onActionClick($event)"
|
||||
toolTip="Add Organization"
|
||||
/>
|
||||
(filterClicked)="onToggleFilters()"
|
||||
toolTip="Add Organization">
|
||||
<ng-template appDataTableToolbar>
|
||||
<form [formGroup]="countryFilterForm" (ngSubmit)="applyCountryFilter()" autocomplete="off"
|
||||
class="flex flex-wrap items-end gap-3 w-full">
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete
|
||||
formControlName="countryId"
|
||||
inputId="organization-country-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="Country"
|
||||
placeholder="Search country"
|
||||
[searchFn]="searchCountries"
|
||||
[displayWith]="displayCountry"
|
||||
[valueWith]="countryValue"
|
||||
[selectedItem]="selectedCountryLookup()"
|
||||
[minSearchLength]="1"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onCountryLookupSelected($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<app-button
|
||||
action="custom"
|
||||
label="Apply"
|
||||
icon="ti ti-filter"
|
||||
variant="primary-full"
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8"
|
||||
/>
|
||||
<app-button
|
||||
action="custom"
|
||||
label="Reset"
|
||||
icon="ti ti-refresh"
|
||||
variant="outline-primary"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="!rounded-full shadow-sm !mb-0 min-h-8"
|
||||
(buttonClicked)="onResetFilter()"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
|
||||
@@ -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<CountryLookupDto | null>(null);
|
||||
readonly appliedCountryId = signal<string | null>(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<OrganizationListRow>): 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+107
-201
@@ -2,12 +2,15 @@
|
||||
<!-- Custom Stepper Navigation Card -->
|
||||
<div class="stepper-header-card mb-4 sm:mb-6">
|
||||
<!-- Mobile Current Step Indicator Banner (< 640px) -->
|
||||
<div class="mb-3 flex items-center justify-between rounded-lg bg-primary/5 px-3 py-2 text-xs font-medium text-primary sm:hidden">
|
||||
<div
|
||||
class="stepper-mobile-banner mb-3 flex items-center justify-between rounded-lg px-3 py-2 text-xs font-medium sm:hidden">
|
||||
<span class="flex items-center gap-1.5 min-w-0 truncate">
|
||||
<span class="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary text-[0.65rem] font-bold text-white">
|
||||
<span
|
||||
class="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary text-[0.65rem] font-bold text-white">
|
||||
{{ currentStepIndex() + 1 }}
|
||||
</span>
|
||||
<span class="truncate">Step {{ currentStepIndex() + 1 }} of {{ steps().length }}: <strong class="font-semibold">{{ currentStepLabel() }}</strong></span>
|
||||
<span class="truncate">Step {{ currentStepIndex() + 1 }} of {{ steps().length }}: <strong
|
||||
class="font-semibold">{{ currentStepLabel() }}</strong></span>
|
||||
</span>
|
||||
<span class="shrink-0 rounded bg-primary/10 px-2 py-0.5 text-[0.6875rem] font-semibold text-primary ms-2">
|
||||
{{ getProgressPercent() }}%
|
||||
@@ -18,85 +21,80 @@
|
||||
<div class="py-1">
|
||||
<nav aria-label="Organization onboarding progress" class="relative w-full">
|
||||
<!-- Background Track Line -->
|
||||
<div
|
||||
class="absolute top-4.5 sm:top-8 h-1 bg-gray-200 dark:bg-slate-700/60 z-0 transition-all duration-300"
|
||||
[style.left]="getTrackLineLeft()"
|
||||
[style.right]="getTrackLineLeft()"
|
||||
></div>
|
||||
<div class="stepper-bg-track absolute top-4.5 sm:top-8 h-1 z-0 transition-all duration-300"
|
||||
[style.left]="getTrackLineLeft()" [style.right]="getTrackLineLeft()"></div>
|
||||
|
||||
<!-- Gradient Progress Line -->
|
||||
<div
|
||||
class="stepper-progress-line absolute top-4.5 sm:top-8 h-1 z-0 transition-all duration-500 ease-in-out"
|
||||
[style.left]="getTrackLineLeft()"
|
||||
[style.width]="getProgressLineWidth()"
|
||||
></div>
|
||||
<div class="stepper-progress-line absolute top-4.5 sm:top-8 h-1 z-0 transition-all duration-500 ease-in-out"
|
||||
[style.left]="getTrackLineLeft()" [style.width]="getProgressLineWidth()"></div>
|
||||
|
||||
<!-- Stepper Nodes Grid -->
|
||||
<ul class="relative z-10 flex w-full justify-between items-start">
|
||||
@for (step of steps(); track step.key; let index = $index) {
|
||||
<li class="flex flex-1 flex-col items-center text-center">
|
||||
<button
|
||||
type="button"
|
||||
class="step-node-btn group flex flex-col items-center focus:outline-none"
|
||||
[class.cursor-pointer]="canOpenStep(index)"
|
||||
[class.cursor-default]="!canOpenStep(index)"
|
||||
[disabled]="!canOpenStep(index)"
|
||||
(click)="selectStep(index)"
|
||||
>
|
||||
<!-- Step Circle Node -->
|
||||
<div
|
||||
class="step-circle"
|
||||
[class.active]="isCurrentStep(index)"
|
||||
[class.completed]="isCompletedStep(index) && !isCurrentStep(index)"
|
||||
[class.pending]="isPendingStep(index)"
|
||||
>
|
||||
<!-- Icons by Step Key / Index -->
|
||||
@switch (step.key) {
|
||||
@case ('basics') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5 sm:size-7">
|
||||
<path d="M19 4h-3.5c-.27-1.16-1.31-2-2.5-2h-2c-1.19 0-2.23.84-2.5 2H5c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h8.08c-.05-.33-.08-.66-.08-1 0-3.31 2.69-6 6-6 .34 0 .67.03 1 .08V6c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM7 9h7v2H7V9zm0 4h5v2H7v-2z"/>
|
||||
<path d="M18 13c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm-1 7.2-2.2-2.2 1.4-1.4 0.8 0.8 3.2-3.2 1.4 1.4-4.6 4.6z"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('localization') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5 sm:size-7">
|
||||
<circle cx="10" cy="10" r="7.5"/>
|
||||
<path d="M2.5 10h15"/>
|
||||
<path d="M10 2.5a11 11 0 0 1 0 15a11 11 0 0 1 0-15"/>
|
||||
<path d="M12 21.5v-7.5h3.5v7.5M15.5 21.5v-9.5h3.5v9.5M19 21.5v-6h3v6" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('plan-limits') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5 sm:size-7">
|
||||
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" fill="currentColor" stroke="none" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
<path d="M8.5 13.5l2.2-2.2 1.8 1.8 3-3.2m0 0h-2.5m2.5 0v2.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('admin-user') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5 sm:size-7">
|
||||
<path d="M12 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-5 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm10 0a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||
<path d="M12 7.5c-3 0-8 1.5-8 4v2.5h8.5c-.32-.73-.5-1.54-.5-2.4 0-1.48.55-2.83 1.46-3.87C13.01 7.6 12.45 7.5 12 7.5zM4 11.5c-1.8 0-5 .9-5 2.5V16h4v-2c0-.94.32-1.8.85-2.5H4zm13.15-4c.45.64.75 1.4.81 2.22C19.06 10.28 20 11.23 20 12.5v1.5h4v-1.5c0-1.6-3.2-2.5-5-2.5h-1.85z"/>
|
||||
<path d="M17 12c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0 2a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm0 7c-1.5 0-2.8-.78-3.5-1.95.04-1.16 2.33-1.8 3.5-1.8s3.46.64 3.5 1.8c-.7 1.17-2 1.95-3.5 1.95z"/>
|
||||
</svg>
|
||||
}
|
||||
@default {
|
||||
<span>{{ index + 1 }}</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<li class="flex flex-1 flex-col items-center text-center">
|
||||
<button type="button" class="step-node-btn group flex flex-col items-center focus:outline-none"
|
||||
[class.cursor-pointer]="canOpenStep(index)" [class.cursor-default]="!canOpenStep(index)"
|
||||
[disabled]="!canOpenStep(index)" (click)="selectStep(index)">
|
||||
<!-- Step Circle Node -->
|
||||
<div class="step-circle" [class.active]="isCurrentStep(index)"
|
||||
[class.completed]="isCompletedStep(index) && !isCurrentStep(index)"
|
||||
[class.pending]="isPendingStep(index)">
|
||||
<!-- Icons by Step Key / Index -->
|
||||
@switch (step.key) {
|
||||
@case ('basics') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="size-5 sm:size-7">
|
||||
<path
|
||||
d="M19 4h-3.5c-.27-1.16-1.31-2-2.5-2h-2c-1.19 0-2.23.84-2.5 2H5c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h8.08c-.05-.33-.08-.66-.08-1 0-3.31 2.69-6 6-6 .34 0 .67.03 1 .08V6c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM7 9h7v2H7V9zm0 4h5v2H7v-2z" />
|
||||
<path
|
||||
d="M18 13c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm-1 7.2-2.2-2.2 1.4-1.4 0.8 0.8 3.2-3.2 1.4 1.4-4.6 4.6z" />
|
||||
</svg>
|
||||
}
|
||||
@case ('localization') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5 sm:size-7">
|
||||
<circle cx="10" cy="10" r="7.5" />
|
||||
<path d="M2.5 10h15" />
|
||||
<path d="M10 2.5a11 11 0 0 1 0 15a11 11 0 0 1 0-15" />
|
||||
<path d="M12 21.5v-7.5h3.5v7.5M15.5 21.5v-9.5h3.5v9.5M19 21.5v-6h3v6" fill="currentColor"
|
||||
stroke="none" />
|
||||
</svg>
|
||||
}
|
||||
@case ('plan-limits') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5 sm:size-7">
|
||||
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" fill="currentColor" stroke="none" />
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
<path d="M8.5 13.5l2.2-2.2 1.8 1.8 3-3.2m0 0h-2.5m2.5 0v2.5" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
@case ('admin-user') {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="size-5 sm:size-7">
|
||||
<path
|
||||
d="M12 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-5 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm10 0a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z" />
|
||||
<path
|
||||
d="M12 7.5c-3 0-8 1.5-8 4v2.5h8.5c-.32-.73-.5-1.54-.5-2.4 0-1.48.55-2.83 1.46-3.87C13.01 7.6 12.45 7.5 12 7.5zM4 11.5c-1.8 0-5 .9-5 2.5V16h4v-2c0-.94.32-1.8.85-2.5H4zm13.15-4c.45.64.75 1.4.81 2.22C19.06 10.28 20 11.23 20 12.5v1.5h4v-1.5c0-1.6-3.2-2.5-5-2.5h-1.85z" />
|
||||
<path
|
||||
d="M17 12c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0 2a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm0 7c-1.5 0-2.8-.78-3.5-1.95.04-1.16 2.33-1.8 3.5-1.8s3.46.64 3.5 1.8c-.7 1.17-2 1.95-3.5 1.95z" />
|
||||
</svg>
|
||||
}
|
||||
@default {
|
||||
<span>{{ index + 1 }}</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Step Label -->
|
||||
<span
|
||||
class="step-label hidden sm:block"
|
||||
[class.active]="isCurrentStep(index)"
|
||||
[class.completed]="isCompletedStep(index) && !isCurrentStep(index)"
|
||||
[class.pending]="isPendingStep(index)"
|
||||
>
|
||||
{{ index + 1 }}. {{ step.label }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<!-- Step Label -->
|
||||
<span class="step-label hidden sm:block" [class.active]="isCurrentStep(index)"
|
||||
[class.completed]="isCompletedStep(index) && !isCurrentStep(index)"
|
||||
[class.pending]="isPendingStep(index)">
|
||||
{{ index + 1 }}. {{ step.label }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -106,144 +104,52 @@
|
||||
|
||||
<!-- Stepper Form Content -->
|
||||
<div>
|
||||
<div class="rounded-xl border border-dashed border-gray-200 bg-white p-3.5 sm:p-6 dark:border-white/10 dark:bg-bodybg">
|
||||
<div class="stepper-content-box rounded-xl p-3.5 sm:p-6">
|
||||
<ng-content />
|
||||
</div>
|
||||
|
||||
<!-- Button Group Footer -->
|
||||
<footer class="modern-modal-footer mt-5 px-6">
|
||||
<!-- Mobile View (< 640px) -->
|
||||
<div class="flex flex-col gap-2.5 sm:hidden">
|
||||
<!-- Back or Cancel button -->
|
||||
<div class="w-full">
|
||||
@if (isFirstStep()) {
|
||||
<app-button
|
||||
action="cancel"
|
||||
label="Cancel"
|
||||
[showIcon]="true"
|
||||
[disabled]="navigationDisabled()"
|
||||
[fullWidth]="true"
|
||||
className="w-full"
|
||||
(buttonClicked)="requestCancel()"
|
||||
></app-button>
|
||||
} @else {
|
||||
<app-button
|
||||
action="previous"
|
||||
label="Back"
|
||||
[showIcon]="true"
|
||||
[disabled]="navigationDisabled()"
|
||||
[fullWidth]="true"
|
||||
className="onboarding-nav-btn w-full"
|
||||
(buttonClicked)="requestBack()"
|
||||
></app-button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Save Draft & Next/Finish side by side -->
|
||||
<div class="grid grid-cols-2 gap-2.5 w-full max-[420px]:grid-cols-1">
|
||||
<div class="min-w-0">
|
||||
<app-button
|
||||
action="save"
|
||||
[variant]="'custom'"
|
||||
label="Save Draft"
|
||||
loadingLabel="Saving..."
|
||||
[showIcon]="true"
|
||||
[loading]="savingDraft()"
|
||||
[disabled]="navigationDisabled() || savingDraft() || finishing()"
|
||||
[fullWidth]="true"
|
||||
className="onboarding-save-btn ti-btn-outline-primary whitespace-nowrap w-full"
|
||||
(buttonClicked)="requestSaveDraft()"
|
||||
></app-button>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
@if (!isLastStep()) {
|
||||
<app-button
|
||||
action="next"
|
||||
label="Next"
|
||||
[showIcon]="true"
|
||||
[iconPosition]="'right'"
|
||||
[disabled]="navigationDisabled() || savingDraft() || finishing()"
|
||||
[fullWidth]="true"
|
||||
className="onboarding-nav-btn w-full"
|
||||
(buttonClicked)="requestNext()"
|
||||
></app-button>
|
||||
<footer class="modern-modal-footer mt-5 px-2 sm:px-6">
|
||||
<div class="flex flex-col-reverse gap-2.5 sm:flex-row sm:items-center sm:justify-end sm:gap-3">
|
||||
<!-- Secondary Actions: Cancel/Back & Save Draft (Left group on Desktop, Bottom row on Mobile) -->
|
||||
<div class="grid grid-cols-2 gap-2.5 w-full sm:flex sm:w-auto sm:items-center sm:gap-3">
|
||||
<!-- Cancel on Step 1, or Back on Step > 1 -->
|
||||
<div class="min-w-0 sm:w-auto">
|
||||
@if (isFirstStep()) {
|
||||
<app-button action="cancel" [variant]="'custom'" label="Cancel" [showIcon]="true"
|
||||
[disabled]="navigationDisabled()" [fullWidth]="true"
|
||||
className="onboarding-nav-btn onboarding-save-btn ti-btn-outline-primary whitespace-nowrap justify-center sm:w-32"
|
||||
(buttonClicked)="requestCancel()"></app-button>
|
||||
} @else {
|
||||
<app-button
|
||||
action="approve"
|
||||
label="Finish & Provision"
|
||||
loadingLabel="Preparing..."
|
||||
[showIcon]="true"
|
||||
[loading]="finishing()"
|
||||
[disabled]="navigationDisabled() || savingDraft() || finishing()"
|
||||
[fullWidth]="true"
|
||||
className="w-full"
|
||||
(buttonClicked)="requestFinish()"
|
||||
></app-button>
|
||||
|
||||
<app-button action="previous" [variant]="'custom'" label="Back" [showIcon]="true"
|
||||
[disabled]="navigationDisabled()" [fullWidth]="true"
|
||||
className="onboarding-nav-btn onboarding-save-btn ti-btn-outline-primary whitespace-nowrap justify-center sm:w-32"
|
||||
(buttonClicked)="requestBack()"></app-button>
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop View (>= 640px) -->
|
||||
<div class="hidden w-full sm:flex sm:items-center sm:justify-between sm:gap-3">
|
||||
<!-- Left: Cancel or Back -->
|
||||
<div>
|
||||
@if (isFirstStep()) {
|
||||
<app-button
|
||||
action="cancel"
|
||||
label="Cancel"
|
||||
[showIcon]="true"
|
||||
[disabled]="navigationDisabled()"
|
||||
(buttonClicked)="requestCancel()"
|
||||
></app-button>
|
||||
} @else {
|
||||
<app-button
|
||||
action="previous"
|
||||
label="Back"
|
||||
[showIcon]="true"
|
||||
[disabled]="navigationDisabled()"
|
||||
className="onboarding-nav-btn"
|
||||
(buttonClicked)="requestBack()"
|
||||
></app-button>
|
||||
}
|
||||
<!-- Save Draft -->
|
||||
<div class="min-w-0 sm:w-auto">
|
||||
<app-button action="save" [variant]="'custom'" label="Save Draft" loadingLabel="Saving..." [showIcon]="true"
|
||||
[loading]="savingDraft()" [disabled]="navigationDisabled() || savingDraft() || finishing()"
|
||||
[fullWidth]="true"
|
||||
className="onboarding-nav-btn onboarding-save-btn ti-btn-outline-primary whitespace-nowrap justify-center sm:w-36"
|
||||
(buttonClicked)="requestSaveDraft()"></app-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Save Draft & Next/Finish -->
|
||||
<div class="flex items-center gap-2.5 sm:gap-3">
|
||||
<app-button
|
||||
action="save"
|
||||
[variant]="'custom'"
|
||||
label="Save Draft"
|
||||
loadingLabel="Saving..."
|
||||
[showIcon]="true"
|
||||
[loading]="savingDraft()"
|
||||
[disabled]="navigationDisabled() || savingDraft() || finishing()"
|
||||
className="onboarding-save-btn ti-btn-outline-primary whitespace-nowrap px-4"
|
||||
(buttonClicked)="requestSaveDraft()"
|
||||
></app-button>
|
||||
|
||||
<!-- Primary Action: Next or Finish & Provision (Top row on Mobile, Far Right on Desktop) -->
|
||||
<div class="w-full sm:w-auto">
|
||||
@if (!isLastStep()) {
|
||||
<app-button
|
||||
action="next"
|
||||
label="Next"
|
||||
[showIcon]="true"
|
||||
[iconPosition]="'right'"
|
||||
[disabled]="navigationDisabled() || savingDraft() || finishing()"
|
||||
className="onboarding-nav-btn"
|
||||
(buttonClicked)="requestNext()"
|
||||
></app-button>
|
||||
<app-button action="next" label="Next" [showIcon]="true" [iconPosition]="'right'"
|
||||
[disabled]="navigationDisabled() || savingDraft() || finishing()" [fullWidth]="true"
|
||||
className="onboarding-nav-btn justify-center sm:w-36" (buttonClicked)="requestNext()"></app-button>
|
||||
} @else {
|
||||
<app-button
|
||||
action="approve"
|
||||
label="Finish & Provision"
|
||||
loadingLabel="Preparing..."
|
||||
[showIcon]="true"
|
||||
[loading]="finishing()"
|
||||
[disabled]="navigationDisabled() || savingDraft() || finishing()"
|
||||
className="sm:w-44"
|
||||
(buttonClicked)="requestFinish()"
|
||||
></app-button>
|
||||
<app-button action="approve" label="Finish & Provision" loadingLabel="Preparing..." [showIcon]="true"
|
||||
[loading]="finishing()" [disabled]="navigationDisabled() || savingDraft() || finishing()" [fullWidth]="true"
|
||||
className="onboarding-nav-btn justify-center sm:w-44" (buttonClicked)="requestFinish()"></app-button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+48
-16
@@ -9,8 +9,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-nav-scroll-wrapper {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.stepper-bg-track {
|
||||
background-color: #e2e8f0;
|
||||
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-progress-line {
|
||||
@@ -19,6 +24,29 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.stepper-mobile-banner {
|
||||
background-color: rgba(124, 61, 235, 0.08);
|
||||
color: var(--primary, #7c3deb);
|
||||
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
background-color: rgba(168, 85, 247, 0.15) !important;
|
||||
color: #c084fc !important;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-content-box {
|
||||
background-color: #ffffff;
|
||||
border: 1px dashed #e2e8f0;
|
||||
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
background-color: var(--bg-body, #1a1c1e) !important;
|
||||
border-color: rgba(255, 255, 255, 0.12) !important;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.step-node-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -48,14 +76,15 @@
|
||||
|
||||
// Pending Step
|
||||
&.pending {
|
||||
background-color: #f0f2f5;
|
||||
background-color: #f1f5f9;
|
||||
border: 2px solid #cbd5e1;
|
||||
color: #94a3b8;
|
||||
color: #64748b;
|
||||
|
||||
:host-context(.dark) & {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: #64748b;
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
background-color: var(--bg-body, #181920) !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.25) !important;
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +94,7 @@
|
||||
background: linear-gradient(135deg, var(--primary, #7c3deb) 0%, #a855f7 100%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(124, 61, 235, 0.25);
|
||||
box-shadow: 0 4px 12px rgba(124, 61, 235, 0.35);
|
||||
}
|
||||
|
||||
// Active Step with Glowing Aura in Theme Primary
|
||||
@@ -101,11 +130,12 @@
|
||||
}
|
||||
|
||||
&.pending {
|
||||
color: #94a3b8;
|
||||
color: #64748b;
|
||||
font-weight: 400;
|
||||
|
||||
:host-context(.dark) & {
|
||||
color: #64748b;
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +143,9 @@
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
|
||||
:host-context(.dark) & {
|
||||
color: #cbd5e1;
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
color: #cbd5e1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +153,9 @@
|
||||
color: var(--primary, #7c3deb);
|
||||
font-weight: 700;
|
||||
|
||||
:host-context(.dark) & {
|
||||
color: #ffffff;
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
color: #c084fc !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -87,10 +87,7 @@ export class OnboardingStepper {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
index < this.currentStepIndex() ||
|
||||
this.isCompletedStep(index)
|
||||
);
|
||||
return index >= 0 && index < this.steps().length;
|
||||
}
|
||||
|
||||
selectStep(index: number): void {
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ export class OrganizationOnboardingStateService {
|
||||
}
|
||||
|
||||
canOpenStep(index: number): boolean {
|
||||
return index <= this.currentStepIndexState() || this.completedStepIndexesState().includes(index);
|
||||
return index >= 0 && index <= 3;
|
||||
}
|
||||
|
||||
isStepCompleted(index: number): boolean {
|
||||
|
||||
+93
-20
@@ -24,25 +24,82 @@ import {
|
||||
import { ORGANIZATION_TYPE_OPTIONS } from '../../constants/organization-onboarding.constants';
|
||||
|
||||
const DATE_FORMAT_OPTIONS: readonly FormSelectOption<DateFormatValue>[] = [
|
||||
{ value: 'DD/MM/YYYY', label: 'DD/MM/YYYY' },
|
||||
{ value: 'MM/DD/YYYY', label: 'MM/DD/YYYY' },
|
||||
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' },
|
||||
// --- Standard Slashes ---
|
||||
{ value: 'DD/MM/YYYY', label: 'DD/MM/YYYY (e.g., 29/07/2026 - UK, India, Europe)' },
|
||||
{ value: 'MM/DD/YYYY', label: 'MM/DD/YYYY (e.g., 07/29/2026 - US)' },
|
||||
{ value: 'YYYY/MM/DD', label: 'YYYY/MM/DD (e.g., 2026/07/29 - East Asia)' },
|
||||
|
||||
// --- ISO / Standard Hyphens ---
|
||||
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD (e.g., 2026-07-29 - ISO 8601 Standard)' },
|
||||
{ value: 'DD-MM-YYYY', label: 'DD-MM-YYYY (e.g., 29-07-2026)' },
|
||||
{ value: 'MM-DD-YYYY', label: 'MM-DD-YYYY (e.g., 07-29-2026)' },
|
||||
|
||||
// --- European Dots ---
|
||||
{ value: 'DD.MM.YYYY', label: 'DD.MM.YYYY (e.g., 29.07.2026 - Germany, Russia, Latin Europe)' },
|
||||
{ value: 'YYYY.MM.DD', label: 'YYYY.MM.DD (e.g., 2026.07.29 - Hungary, China)' },
|
||||
|
||||
// --- Text-Based Formats ---
|
||||
{ value: 'DD MMM YYYY', label: 'DD MMM YYYY (e.g., 29 Jul 2026)' },
|
||||
{ value: 'MMM DD, YYYY', label: 'MMM DD, YYYY (e.g., Jul 29, 2026)' },
|
||||
{ value: 'DD MMMM YYYY', label: 'DD MMMM YYYY (e.g., 29 July 2026)' },
|
||||
{ value: 'MMMM DD, YYYY', label: 'MMMM DD, YYYY (e.g., July 29, 2026)' },
|
||||
];
|
||||
|
||||
const TIME_FORMAT_OPTIONS: readonly FormSelectOption<TimeFormatValue>[] = [
|
||||
{ value: 0, label: '12 hour' },
|
||||
{ value: 1, label: '24 hour' },
|
||||
export const TIME_FORMAT_OPTIONS: readonly FormSelectOption<TimeFormatValue>[] = [
|
||||
{
|
||||
value: 'TwelveHour',
|
||||
label: '12-Hour (e.g., 01:30 PM)',
|
||||
},
|
||||
{
|
||||
value: 'TwentyFourHour',
|
||||
label: '24-Hour (e.g., 13:30)',
|
||||
},
|
||||
];
|
||||
|
||||
const NUMBER_FORMAT_OPTIONS: readonly FormSelectOption<NumberFormatValue>[] = [
|
||||
{ value: 'OneTwoThreeCommaFourFiveSixPointSevenEight', label: '123,456.78' },
|
||||
{ value: 'OneTwoThreePointFourFiveSixCommaSevenEight', label: '123.456,78' },
|
||||
{
|
||||
value: 'OneTwoThreeCommaFourFiveSixPointSevenEight',
|
||||
label: '1,234,567.89 (Western Standard)'
|
||||
},
|
||||
{
|
||||
value: 'OneTwoThreePointFourFiveSixCommaSevenEight',
|
||||
label: '1.234.567,89 (European Standard)'
|
||||
},
|
||||
{
|
||||
value: 'OneTwoThreeSpaceFourFiveSixCommaSevenEight',
|
||||
label: '1 234 567,89 (International - Space with Comma)'
|
||||
},
|
||||
{
|
||||
value: 'OneTwoThreeSpaceFourFiveSixPointSevenEight',
|
||||
label: '1 234 567.89 (International - Space with Period)'
|
||||
},
|
||||
{
|
||||
value: 'TwelveLakhThirtyFourThousandFiveHundredSixtySevenPointEightNine',
|
||||
label: '12,34,567.89 (South Asian / Indian Standard)'
|
||||
},
|
||||
];
|
||||
|
||||
const FISCAL_YEAR_OPTIONS: readonly FormSelectOption<FiscalYearConventionValue>[] = [
|
||||
{ value: 'CalendarYear', label: 'Calendar Year (Jan-Dec)' },
|
||||
{ value: 'AprilToMarch', label: 'April - March' },
|
||||
{ value: 'JulyToJune', label: 'July - June' },
|
||||
{
|
||||
value: 'JanuaryToDecember',
|
||||
label: 'January 1 – December 31 (Calendar Year)',
|
||||
},
|
||||
{
|
||||
value: 'AprilToMarch',
|
||||
label: 'April 1 – March 31 (e.g., India, UK, Canada, Japan)',
|
||||
},
|
||||
{
|
||||
value: 'JulyToJune',
|
||||
label: 'July 1 – June 30 (e.g., Australia, New Zealand, Egypt)',
|
||||
},
|
||||
{
|
||||
value: 'OctoberToSeptember',
|
||||
label: 'October 1 – September 30 (e.g., US Federal, Thailand)',
|
||||
},
|
||||
{
|
||||
value: 'March21ToMarch20',
|
||||
label: 'March 21 – March 20 (e.g., Iran, Afghanistan)',
|
||||
},
|
||||
];
|
||||
|
||||
const LICENSE_TYPE_OPTIONS: readonly FormSelectOption<OrganizationLicenseType>[] = [
|
||||
@@ -58,7 +115,7 @@ const LOCALIZATION_DEFAULTS: readonly CountryLocalizationDefaults[] = [
|
||||
defaultLanguage: { id: 'en-in', label: 'English', secondaryLabel: 'en-IN' },
|
||||
dateFormat: 'DD/MM/YYYY',
|
||||
timeFormat: 0,
|
||||
numberFormat: 'OneTwoThreeCommaFourFiveSixPointSevenEight',
|
||||
numberFormat: 'TwelveLakhThirtyFourThousandFiveHundredSixtySevenPointEightNine',
|
||||
fiscalYearConvention: 'AprilToMarch',
|
||||
},
|
||||
{
|
||||
@@ -69,7 +126,7 @@ const LOCALIZATION_DEFAULTS: readonly CountryLocalizationDefaults[] = [
|
||||
dateFormat: 'MM/DD/YYYY',
|
||||
timeFormat: 0,
|
||||
numberFormat: 'OneTwoThreeCommaFourFiveSixPointSevenEight',
|
||||
fiscalYearConvention: 'CalendarYear',
|
||||
fiscalYearConvention: 'JanuaryToDecember', // or 'OctoberToSeptember' if defaulting to US Federal fiscal year
|
||||
},
|
||||
];
|
||||
|
||||
@@ -127,13 +184,29 @@ export class OrganizationOnboardingService {
|
||||
|
||||
getSubscriptionPlans(term = '', limit = 50): Observable<readonly SubscriptionPlanDto[]> {
|
||||
const params = new HttpParams().set('term', term).set('limit', limit);
|
||||
return this.http.get<readonly SubscriptionPlanDto[]>(ORGANIZATION_ONBOARDING_ENDPOINTS.subscriptionPlans, { params }).pipe(
|
||||
catchError(() => of([
|
||||
{ id: 'starter', name: 'Starter', code: 'STARTER', maxCompanies: 1, maxUsers: 10, maxStorageGb: 10, defaultTrialDays: 14 },
|
||||
{ id: 'growth', name: 'Growth', code: 'GROWTH', maxCompanies: 5, maxUsers: 100, maxStorageGb: 100, defaultTrialDays: 30 },
|
||||
{ id: 'enterprise', name: 'Enterprise', code: 'ENTERPRISE', maxCompanies: 25, maxUsers: 1000, maxStorageGb: 500, defaultTrialDays: 30 },
|
||||
]))
|
||||
);
|
||||
return this.http
|
||||
.get<SubscriptionPlanDto[] | { value?: SubscriptionPlanDto[]; data?: SubscriptionPlanDto[] }>(
|
||||
ORGANIZATION_ONBOARDING_ENDPOINTS.subscriptionPlans,
|
||||
{ params }
|
||||
)
|
||||
.pipe(
|
||||
map((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
return res;
|
||||
}
|
||||
if (res && Array.isArray(res.value)) {
|
||||
return res.value;
|
||||
}
|
||||
if (res && Array.isArray(res.data)) {
|
||||
return res.data;
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Error fetching subscription plans:', error);
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
searchSubscriptionPlans(term: string | null, limit = 10): Observable<readonly OrganizationPlanLookupValue[]> {
|
||||
|
||||
+39
-11
@@ -34,16 +34,30 @@ export type TimeFormatValue = 0 | 1 | 'TwelveHour' | 'TwentyFourHour';
|
||||
export type DateFormatValue =
|
||||
| 'DD/MM/YYYY'
|
||||
| 'MM/DD/YYYY'
|
||||
| 'YYYY-MM-DD';
|
||||
| 'YYYY/MM/DD'
|
||||
| 'YYYY-MM-DD'
|
||||
| 'DD-MM-YYYY'
|
||||
| 'MM-DD-YYYY'
|
||||
| 'DD.MM.YYYY'
|
||||
| 'YYYY.MM.DD'
|
||||
| 'DD MMM YYYY'
|
||||
| 'MMM DD, YYYY'
|
||||
| 'DD MMMM YYYY'
|
||||
| 'MMMM DD, YYYY';
|
||||
|
||||
export type NumberFormatValue =
|
||||
| 'OneTwoThreeCommaFourFiveSixPointSevenEight'
|
||||
| 'OneTwoThreePointFourFiveSixCommaSevenEight';
|
||||
| 'OneTwoThreeCommaFourFiveSixPointSevenEight'
|
||||
| 'OneTwoThreePointFourFiveSixCommaSevenEight'
|
||||
| 'OneTwoThreeSpaceFourFiveSixCommaSevenEight'
|
||||
| 'OneTwoThreeSpaceFourFiveSixPointSevenEight'
|
||||
| 'TwelveLakhThirtyFourThousandFiveHundredSixtySevenPointEightNine';
|
||||
|
||||
export type FiscalYearConventionValue =
|
||||
| 'CalendarYear'
|
||||
| 'JanuaryToDecember'
|
||||
| 'AprilToMarch'
|
||||
| 'JulyToJune';
|
||||
| 'JulyToJune'
|
||||
| 'OctoberToSeptember'
|
||||
| 'March21ToMarch20';
|
||||
|
||||
export function mapFiscalYearToMmDd(fiscalYear: string | null | undefined): string {
|
||||
if (!fiscalYear) return '01-01';
|
||||
@@ -60,11 +74,22 @@ export function mapFiscalYearToMmDd(fiscalYear: string | null | undefined): stri
|
||||
}
|
||||
|
||||
export function mapMmDdToFiscalYear(mmDd: string | null | undefined): FiscalYearConventionValue {
|
||||
if (!mmDd) return 'CalendarYear';
|
||||
if (mmDd === '04-01') return 'AprilToMarch';
|
||||
if (mmDd === '07-01') return 'JulyToJune';
|
||||
if (mmDd === '01-01') return 'CalendarYear';
|
||||
return (mmDd as FiscalYearConventionValue) ?? 'CalendarYear';
|
||||
if (!mmDd) return 'JanuaryToDecember';
|
||||
|
||||
switch (mmDd) {
|
||||
case '01-01':
|
||||
return 'JanuaryToDecember';
|
||||
case '04-01':
|
||||
return 'AprilToMarch';
|
||||
case '07-01':
|
||||
return 'JulyToJune';
|
||||
case '10-01':
|
||||
return 'OctoberToSeptember';
|
||||
case '03-21':
|
||||
return 'March21ToMarch20';
|
||||
default:
|
||||
return 'JanuaryToDecember';
|
||||
}
|
||||
}
|
||||
|
||||
export function mapNumberFormatToApi(numberFormat: string | null | undefined): string {
|
||||
@@ -112,8 +137,11 @@ export function mapApiToLicenseType(licenseType: unknown): OrganizationLicenseTy
|
||||
|
||||
export interface SubscriptionPlanDto {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
name: string;
|
||||
price?: number;
|
||||
currencyId?: string | null;
|
||||
billingCycle?: number;
|
||||
maxCompanies?: number;
|
||||
maxUsers?: number;
|
||||
maxStorageGb?: number;
|
||||
|
||||
@@ -107,7 +107,7 @@ export class OrganizationOnboarding {
|
||||
}
|
||||
|
||||
onStepSelected(index: number): void {
|
||||
if (!this.stateService.canOpenStep(index)) {
|
||||
if (!this.stateService.canOpenStep(index) || index === this.currentStepIndex()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,15 +135,10 @@ export class OrganizationOnboarding {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeStep = this.getActiveStep();
|
||||
if (!activeStep?.validate()) {
|
||||
this.stateService.unmarkStepCompleted(currentIndex);
|
||||
if (!this.validateStepsUpTo(currentIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storeValidatedActiveStep(activeStep);
|
||||
this.stateService.markStepCompleted(currentIndex);
|
||||
|
||||
this.persistStepToServer(currentIndex, true)
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
@@ -165,16 +160,13 @@ export class OrganizationOnboarding {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeStep = this.getActiveStep();
|
||||
if (!activeStep?.validate()) {
|
||||
const currentIndex = this.currentStepIndex();
|
||||
if (!this.validateStepsUpTo(currentIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storeValidatedActiveStep(activeStep);
|
||||
|
||||
this.savingDraft.set(true);
|
||||
|
||||
const currentIndex = this.currentStepIndex();
|
||||
this.persistStepToServer(currentIndex, false)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
@@ -199,16 +191,10 @@ export class OrganizationOnboarding {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeStep = this.adminStep();
|
||||
if (!activeStep?.validate()) {
|
||||
this.stateService.unmarkStepCompleted(3);
|
||||
if (!this.validateStepsUpTo(this.steps.length - 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adminValue = activeStep.getValue();
|
||||
this.stateService.updateAdmin(adminValue);
|
||||
this.stateService.markStepCompleted(3);
|
||||
|
||||
const data = this.stateService.onboardingData();
|
||||
if (!data.basics || !data.localization || !data.planLimits || !data.admin) {
|
||||
this.toastr.error('Complete and validate every onboarding step before provisioning.');
|
||||
@@ -337,8 +323,42 @@ export class OrganizationOnboarding {
|
||||
}
|
||||
}
|
||||
|
||||
private getActiveStep(): OnboardingStepForm<unknown> | undefined {
|
||||
switch (this.currentStepIndex()) {
|
||||
private validateStepsUpTo(targetIndex: number): boolean {
|
||||
this.captureActiveStepDraft();
|
||||
|
||||
for (let i = 0; i <= targetIndex; i++) {
|
||||
const stepComp = this.getStepComponent(i);
|
||||
if (!stepComp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stepComp.validate()) {
|
||||
this.stateService.unmarkStepCompleted(i);
|
||||
|
||||
if (i !== this.currentStepIndex()) {
|
||||
this.stateService.setCurrentStepIndex(i);
|
||||
this.hydrateActiveStep();
|
||||
queueMicrotask(() => {
|
||||
this.getStepComponent(i)?.validate();
|
||||
});
|
||||
const stepLabel = this.steps[i]?.label ?? `Step ${i + 1}`;
|
||||
this.toastr.warning(
|
||||
`Please complete all required fields in Step ${i + 1} (${stepLabel}) before proceeding.`,
|
||||
'Validation Required'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
this.storeValidatedStep(i, stepComp);
|
||||
this.stateService.markStepCompleted(i);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private getStepComponent(index: number): OnboardingStepForm<unknown> | undefined {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return this.basicsStep();
|
||||
case 1:
|
||||
@@ -352,8 +372,12 @@ export class OrganizationOnboarding {
|
||||
}
|
||||
}
|
||||
|
||||
private storeValidatedActiveStep(step: OnboardingStepForm<unknown>): void {
|
||||
switch (this.currentStepIndex()) {
|
||||
private getActiveStep(): OnboardingStepForm<unknown> | undefined {
|
||||
return this.getStepComponent(this.currentStepIndex());
|
||||
}
|
||||
|
||||
private storeValidatedStep(index: number, step: OnboardingStepForm<unknown>): void {
|
||||
switch (index) {
|
||||
case 0:
|
||||
this.stateService.updateBasics((step as OrganizationBasicsStepComponent).getValue());
|
||||
break;
|
||||
@@ -369,6 +393,10 @@ export class OrganizationOnboarding {
|
||||
}
|
||||
}
|
||||
|
||||
private storeValidatedActiveStep(step: OnboardingStepForm<unknown>): void {
|
||||
this.storeValidatedStep(this.currentStepIndex(), step);
|
||||
}
|
||||
|
||||
private captureActiveStepDraft(): void {
|
||||
const currentIndex = this.currentStepIndex();
|
||||
const validatedValue = this.getValidatedStepValue(currentIndex);
|
||||
|
||||
+7
-21
@@ -105,8 +105,8 @@
|
||||
inputId="organization-go-live-date"
|
||||
variant="floating"
|
||||
label="Go-Live Date"
|
||||
mode="single"
|
||||
[required]="true"
|
||||
[variant]="'floating'"
|
||||
[submitAttempted]="submitAttempted()"
|
||||
[validationMessages]="{ required: 'Go-Live Date is required.' }"
|
||||
/>
|
||||
@@ -114,30 +114,16 @@
|
||||
|
||||
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
||||
<app-form-date-picker
|
||||
formControlName="systemAccessStartDate"
|
||||
inputId="organization-system-access-start-date"
|
||||
formControlName="systemAccessPeriod"
|
||||
inputId="organization-system-access-period"
|
||||
variant="floating"
|
||||
label="System Access From Date"
|
||||
label="System Access Period"
|
||||
mode="range"
|
||||
[required]="true"
|
||||
[variant]="'floating'"
|
||||
[submitAttempted]="submitAttempted()"
|
||||
[validationMessages]="{ required: 'System Access Start Date is required.' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
||||
<app-form-date-picker
|
||||
formControlName="systemAccessEndDate"
|
||||
inputId="organization-system-access-end-date"
|
||||
variant="floating"
|
||||
label="System Access To Date"
|
||||
[required]="requiresAccessEndDate()"
|
||||
[submitAttempted]="submitAttempted()"
|
||||
[validationMessages]="{
|
||||
required: 'System Access End Date is required for Trial.',
|
||||
trialEndDateRequired: 'System Access End Date is required for Trial.',
|
||||
accessDateRange: 'System Access End Date must be on or after the access start date.',
|
||||
trialEndDateOrder: 'Trial end date must be later than the access start date.'
|
||||
required: 'System Access Period is required.',
|
||||
trialEndDateRequired: 'System Access End Date is required for Trial.'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+38
-28
@@ -18,7 +18,7 @@ import {
|
||||
AutocompleteSearchFn,
|
||||
AutocompleteValueFn,
|
||||
} from '../../../../../shared/components/form/autocomplete/autocomplete.types';
|
||||
import { FormDatePicker } from '../../../../../shared/components/form/form-date-picker/form-date-picker';
|
||||
import { DateRangeValue, FormDatePicker } from '../../../../../shared/components/form/form-date-picker/form-date-picker';
|
||||
import { FormInput } from '../../../../../shared/components/form/form-input/form-input';
|
||||
import { FormSelect } from '../../../../../shared/components/form/form-select/form-select';
|
||||
import {
|
||||
@@ -38,8 +38,7 @@ interface OrganizationPlanLimitsFormModel {
|
||||
readonly maximumUsers: number | null;
|
||||
readonly maximumStorageGb: number | null;
|
||||
readonly goLiveDate: string | null;
|
||||
readonly systemAccessStartDate: string | null;
|
||||
readonly systemAccessEndDate: string | null;
|
||||
readonly systemAccessPeriod: DateRangeValue | null;
|
||||
}
|
||||
|
||||
type ThemeSelectValue =
|
||||
@@ -59,17 +58,19 @@ const accessDateValidator: ValidatorFn = (control: AbstractControl): ValidationE
|
||||
}
|
||||
|
||||
const errors: ValidationErrors = {};
|
||||
const { licenseType, systemAccessStartDate, systemAccessEndDate } = value;
|
||||
const { licenseType, systemAccessPeriod } = value;
|
||||
const from = systemAccessPeriod?.from ?? null;
|
||||
const to = systemAccessPeriod?.to ?? null;
|
||||
|
||||
if (licenseType === 'Trial' && !systemAccessEndDate) {
|
||||
if (licenseType === 'Trial' && (!systemAccessPeriod || !to)) {
|
||||
errors['trialEndDateRequired'] = true;
|
||||
}
|
||||
|
||||
if (systemAccessStartDate && systemAccessEndDate && systemAccessStartDate > systemAccessEndDate) {
|
||||
if (from && to && from > to) {
|
||||
errors['accessDateRange'] = true;
|
||||
}
|
||||
|
||||
if (licenseType === 'Trial' && systemAccessStartDate && systemAccessEndDate && systemAccessStartDate >= systemAccessEndDate) {
|
||||
if (licenseType === 'Trial' && from && to && from >= to) {
|
||||
errors['trialEndDateOrder'] = true;
|
||||
}
|
||||
|
||||
@@ -106,8 +107,7 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
maximumUsers: this.formBuilder.control<number | null>(null, [Validators.required, Validators.min(1)]),
|
||||
maximumStorageGb: this.formBuilder.control<number | null>(null, [Validators.required, Validators.min(1)]),
|
||||
goLiveDate: this.formBuilder.control<string | null>(null, [Validators.required]),
|
||||
systemAccessStartDate: this.formBuilder.control<string | null>(null, [Validators.required]),
|
||||
systemAccessEndDate: this.formBuilder.control<string | null>(null),
|
||||
systemAccessPeriod: this.formBuilder.control<DateRangeValue | null>(null, [Validators.required]),
|
||||
}, { validators: accessDateValidator });
|
||||
|
||||
readonly requiresAccessEndDate = computed(() => this.form.controls.licenseType.value === 'Trial');
|
||||
@@ -120,8 +120,7 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
})
|
||||
);
|
||||
|
||||
readonly displayPlan: AutocompleteDisplayFn<OrganizationPlanLookupValue> = item =>
|
||||
[item.label, item.code].filter(Boolean).join(' - ');
|
||||
readonly displayPlan: AutocompleteDisplayFn<OrganizationPlanLookupValue> = item => item.label;
|
||||
|
||||
readonly planValue: AutocompleteValueFn<OrganizationPlanLookupValue, string> = item => item.id;
|
||||
|
||||
@@ -133,12 +132,12 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(licenseType => {
|
||||
if (licenseType === 'Trial') {
|
||||
this.form.controls.systemAccessEndDate.addValidators([Validators.required]);
|
||||
this.form.controls.systemAccessPeriod.addValidators([Validators.required]);
|
||||
} else {
|
||||
this.form.controls.systemAccessEndDate.removeValidators([Validators.required]);
|
||||
this.form.controls.systemAccessPeriod.removeValidators([Validators.required]);
|
||||
}
|
||||
|
||||
this.form.controls.systemAccessEndDate.updateValueAndValidity({ emitEvent: false });
|
||||
this.form.controls.systemAccessPeriod.updateValueAndValidity({ emitEvent: false });
|
||||
});
|
||||
|
||||
this.form.controls.subscriptionPlanId.valueChanges
|
||||
@@ -186,8 +185,11 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
getValue(): OrganizationPlanLimitsValue {
|
||||
const value = this.form.getRawValue() as OrganizationPlanLimitsFormModel;
|
||||
const subscriptionPlan = this.planSelection();
|
||||
const period = value.systemAccessPeriod;
|
||||
const systemAccessStartDate = period?.from ?? null;
|
||||
const systemAccessEndDate = period?.to ?? null;
|
||||
|
||||
if (!subscriptionPlan || !value.licenseType || !value.maximumCompanies || !value.maximumUsers || !value.maximumStorageGb || !value.goLiveDate || !value.systemAccessStartDate) {
|
||||
if (!subscriptionPlan || !value.licenseType || !value.maximumCompanies || !value.maximumUsers || !value.maximumStorageGb || !value.goLiveDate || !systemAccessStartDate) {
|
||||
throw new Error('Organization plan and limits values are incomplete.');
|
||||
}
|
||||
|
||||
@@ -198,14 +200,15 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
maximumUsers: value.maximumUsers,
|
||||
maximumStorageGb: value.maximumStorageGb,
|
||||
goLiveDate: value.goLiveDate,
|
||||
systemAccessStartDate: value.systemAccessStartDate,
|
||||
systemAccessEndDate: value.systemAccessEndDate,
|
||||
systemAccessStartDate,
|
||||
systemAccessEndDate,
|
||||
limitsEditable: this.limitsEditable(),
|
||||
};
|
||||
}
|
||||
|
||||
getDraftValue(): Partial<OrganizationPlanLimitsValue> {
|
||||
const value = this.form.getRawValue() as OrganizationPlanLimitsFormModel;
|
||||
const period = value.systemAccessPeriod;
|
||||
|
||||
return {
|
||||
subscriptionPlan: this.planSelection() ?? undefined,
|
||||
@@ -214,8 +217,8 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
maximumUsers: value.maximumUsers ?? undefined,
|
||||
maximumStorageGb: value.maximumStorageGb ?? undefined,
|
||||
goLiveDate: value.goLiveDate ?? undefined,
|
||||
systemAccessStartDate: value.systemAccessStartDate ?? undefined,
|
||||
systemAccessEndDate: value.systemAccessEndDate,
|
||||
systemAccessStartDate: period?.from ?? undefined,
|
||||
systemAccessEndDate: period?.to ?? undefined,
|
||||
limitsEditable: this.limitsEditable(),
|
||||
};
|
||||
}
|
||||
@@ -232,6 +235,10 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
code: value['SubscriptionPlan']?.code ?? '',
|
||||
} : planId ? { id: planId, label: '', code: '' } : null);
|
||||
|
||||
const from = value.systemAccessStartDate ?? value['SystemAccessFrom'] ?? null;
|
||||
const to = value.systemAccessEndDate ?? value['SystemAccessTo'] ?? null;
|
||||
const systemAccessPeriod: DateRangeValue | null = (from || to) ? { from, to } : null;
|
||||
|
||||
this.form.patchValue({
|
||||
subscriptionPlanId: planId,
|
||||
licenseType: normalizedLicenseType,
|
||||
@@ -239,8 +246,7 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
maximumUsers: value.maximumUsers ?? value['MaxUsers'] ?? null,
|
||||
maximumStorageGb: value.maximumStorageGb ?? value['MaxStorageGb'] ?? null,
|
||||
goLiveDate: value.goLiveDate ?? value['GoLiveDate'] ?? null,
|
||||
systemAccessStartDate: value.systemAccessStartDate ?? value['SystemAccessFrom'] ?? null,
|
||||
systemAccessEndDate: value.systemAccessEndDate ?? value['SystemAccessTo'] ?? null,
|
||||
systemAccessPeriod,
|
||||
}, { emitEvent: false });
|
||||
|
||||
this.planSelection.set(subPlan ?? value.subscriptionPlan ?? null);
|
||||
@@ -265,20 +271,24 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
|
||||
this.planSelection.set(defaults.plan);
|
||||
this.limitsEditable.set(defaults.limitsEditable);
|
||||
|
||||
const currentPeriod = this.form.controls.systemAccessPeriod.value;
|
||||
let from = currentPeriod?.from ?? this.form.controls.goLiveDate.value ?? new Date().toISOString().split('T')[0];
|
||||
let to = currentPeriod?.to ?? null;
|
||||
|
||||
if (defaults.licenseType === 'Trial' && defaults.defaultTrialDays && !to) {
|
||||
const startDate = new Date(from);
|
||||
startDate.setDate(startDate.getDate() + defaults.defaultTrialDays);
|
||||
to = startDate.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
const patch: Record<string, unknown> = {
|
||||
licenseType: defaults.licenseType,
|
||||
maximumCompanies: defaults.maximumCompanies,
|
||||
maximumUsers: defaults.maximumUsers,
|
||||
maximumStorageGb: defaults.maximumStorageGb,
|
||||
systemAccessPeriod: (from || to) ? { from, to } : null,
|
||||
};
|
||||
|
||||
if (defaults.licenseType === 'Trial' && defaults.defaultTrialDays && !this.form.controls.systemAccessEndDate.value) {
|
||||
const startDateStr = this.form.controls.systemAccessStartDate.value || new Date().toISOString().split('T')[0];
|
||||
const startDate = new Date(startDateStr);
|
||||
startDate.setDate(startDate.getDate() + defaults.defaultTrialDays);
|
||||
patch['systemAccessEndDate'] = startDate.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
this.form.patchValue(patch, { emitEvent: false });
|
||||
this.applyLimitsEditableState();
|
||||
}
|
||||
|
||||
@@ -215,12 +215,17 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box-footer modern-modal-footer flex flex-col-reverse sm:flex-row sm:justify-between sm:items-center gap-3">
|
||||
<div class="w-full sm:w-auto">
|
||||
<app-button action="cancel" label="Cancel" [showIcon]="true" [fullWidth]="true" (buttonClicked)="onDiscard()" />
|
||||
</div>
|
||||
<div class="w-full sm:w-auto">
|
||||
<app-button action="save" label="Save" [showIcon]="true" [fullWidth]="true" (buttonClicked)="onSave()" />
|
||||
<div class="box-footer modern-modal-footer flex flex-col sm:flex-row sm:justify-end sm:items-center gap-3">
|
||||
<div class="grid grid-cols-2 gap-3 w-full sm:flex sm:w-auto sm:items-center sm:gap-3">
|
||||
<div class="w-full sm:w-auto">
|
||||
<app-button action="cancel" [variant]="'custom'" label="Cancel" [showIcon]="true" [fullWidth]="true"
|
||||
className="onboarding-nav-btn onboarding-save-btn ti-btn-outline-primary whitespace-nowrap justify-center sm:w-32"
|
||||
(buttonClicked)="onDiscard()"></app-button>
|
||||
</div>
|
||||
<div class="w-full sm:w-auto">
|
||||
<app-button action="save" label="Save" [showIcon]="true" [fullWidth]="true"
|
||||
className="sm:w-32 justify-center" (buttonClicked)="onSave()"></app-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,36 +1,75 @@
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
|
||||
&.w-full {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.w-full,
|
||||
:host-context(.onboarding-action-footer) button.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button,
|
||||
:host-context(.onboarding-action-footer) button {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: .3s ease;
|
||||
height: 36px !important;
|
||||
min-height: 36px !important;
|
||||
max-height: 36px !important;
|
||||
padding: 0 16px !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1 !important;
|
||||
font-weight: 600 !important;
|
||||
transition: .3s ease !important;
|
||||
box-sizing: border-box !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
vertical-align: middle !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-light,
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-light {
|
||||
border: 1px solid #dbe3ef;
|
||||
border: 1.5px solid #dbe3ef !important;
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
border-color: rgba(255, 255, 255, 0.15) !important;
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
color: #cbd5e1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-light:hover:not(:disabled),
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-light:hover:not(:disabled) {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
border-color: #cbd5e1 !important;
|
||||
transform: translateY(-2px);
|
||||
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
border-color: rgba(255, 255, 255, 0.25) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-primary-full,
|
||||
:host-context(.modern-modal-footer) button.ti-btn-success-full {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border: 1.5px solid transparent !important;
|
||||
color: #ffffff;
|
||||
background: #7c3deb;
|
||||
box-shadow: 0 8px 20px rgba(125, 61, 235, 0.468);
|
||||
transition: .35s ease;
|
||||
transition: .35s ease !important;
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-primary-full:hover:not(:disabled),
|
||||
@@ -39,8 +78,35 @@
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, .45);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Single Solid Button Rule using CSS :has()
|
||||
When an outline button (Cancel/Save Draft) is hovered, demote the default
|
||||
solid button ('Next') to outline style and remove its glow/box-shadow.
|
||||
========================================================================== */
|
||||
:host-context(.modern-modal-footer:has(.ti-btn-outline-primary:hover:not(:disabled))) button.ti-btn-primary-full:not(:hover),
|
||||
:host-context(.modern-modal-footer:has(.onboarding-save-btn:hover:not(:disabled))) button.ti-btn-primary-full:not(:hover) {
|
||||
background: #ffffff !important;
|
||||
color: var(--primary, #7c3deb) !important;
|
||||
border: 1.5px solid var(--primary, #7c3deb) !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
|
||||
&::before {
|
||||
opacity: 0 !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
background: transparent !important;
|
||||
color: #c084fc !important;
|
||||
border-color: #a855f7 !important;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-primary-full::before,
|
||||
:host-context(.modern-modal-footer) button.ti-btn-success-full::before{
|
||||
:host-context(.modern-modal-footer) button.ti-btn-outline-primary::before,
|
||||
:host-context(.modern-modal-footer) button.ti-btn-success-full::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -60,14 +126,34 @@
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.onboarding-nav-btn {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.onboarding-save-btn {
|
||||
border: 1.5px solid var(--primary, #7c3deb) !important;
|
||||
background: #ffffff;
|
||||
color: var(--primary, #7c3deb);
|
||||
transition: all 0.35s ease !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
:host-context(.dark) &,
|
||||
:host-context([data-theme-mode="dark"]) & {
|
||||
background: transparent;
|
||||
color: #c084fc;
|
||||
border-color: #a855f7;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.onboarding-save-btn:hover:not(:disabled) {
|
||||
background: var(--primary, #7c3deb) !important;
|
||||
background: linear-gradient(135deg, var(--primary, #7c3deb) 0%, #a855f7 100%) !important;
|
||||
color: #ffffff !important;
|
||||
border-color: transparent !important;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, .45);
|
||||
box-shadow: 0 12px 28px rgba(124, 61, 235, 0.45);
|
||||
}
|
||||
|
||||
@keyframes saveButtonShine {
|
||||
|
||||
@@ -15,6 +15,7 @@ export type IconPosition = 'left' | 'right';
|
||||
export type ButtonVariant =
|
||||
| 'primary'
|
||||
| 'primary-full'
|
||||
| 'outline-primary'
|
||||
| 'secondary'
|
||||
| 'secondary-full'
|
||||
| 'success'
|
||||
@@ -201,6 +202,9 @@ const BUTTON_ACTION_CONFIG: Record<ButtonAction, ButtonActionConfig> = {
|
||||
standalone: true,
|
||||
templateUrl: './button.html',
|
||||
styleUrl: './button.scss',
|
||||
host: {
|
||||
'[class.w-full]': 'fullWidth()'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class Button {
|
||||
@@ -293,6 +297,7 @@ export class Button {
|
||||
const variants: Record<ButtonVariant, string> = {
|
||||
primary: 'ti-btn-primary',
|
||||
'primary-full': 'ti-btn-primary-full',
|
||||
'outline-primary': 'ti-btn-outline-primary',
|
||||
|
||||
secondary: 'ti-btn-secondary',
|
||||
'secondary-full': 'ti-btn-secondary-full',
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
{{ tableTitle () }}
|
||||
</h5>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
@if(showFilterButton()){
|
||||
<div animate.enter="filter-btn-enter" animate.leave="filter-btn-leave">
|
||||
<app-button action="custom" icon="ti ti-filter" label="Filter" size="sm" iconClass="!text-[1rem]"
|
||||
[variant]="filterActive() ? 'primary-full' : 'outline-primary'"
|
||||
className="!rounded-full shadow-sm !whitespace-nowrap" (buttonClicked)="onFilterClick($event)" />
|
||||
</div>
|
||||
}
|
||||
@if(showAddButton()){
|
||||
<div>
|
||||
<app-button action="add" [label]="buttonTitle()" size="sm" iconClass="!text-[1rem]"
|
||||
@@ -30,6 +37,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (toolbarTemplate(); as toolbar) {
|
||||
<div class="table-filter-row" [class.is-open]="filterActive()">
|
||||
<div class="table-filter-row-inner">
|
||||
<div class="table-filter-row-content" #filterRowContent>
|
||||
<ng-container [ngTemplateOutlet]="toolbar.templateRef" />
|
||||
@if (filterActionsTemplate(); as filterActions) {
|
||||
<div class="table-filter-row-actions" [class.is-pinned]="filterRowOverflowing()">
|
||||
<ng-container [ngTemplateOutlet]="filterActions.templateRef" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="ti-btn ti-btn-icon ti-btn-sm ti-btn-light !rounded-full !m-0 shrink-0 filter-close-btn"
|
||||
aria-label="Close filter" (click)="onFilterClick($event)">
|
||||
<i class="ti ti-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="box-body !p-0 modern-table-body">
|
||||
<div class="table-responsive overflow-x-auto overflow-y-visible modern-table-wrapper">
|
||||
<table [class]="tableClass()">
|
||||
|
||||
@@ -253,4 +253,158 @@
|
||||
|
||||
.data-table-modern .custom-floating-input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
|
||||
/* Filter toggle button (in the toolbar) */
|
||||
.filter-btn-enter {
|
||||
animation: filterButtonIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.filter-btn-leave {
|
||||
animation: filterButtonOut 0.15s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes filterButtonIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes filterButtonOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/* Full-width filter row, joined to the table card */
|
||||
.table-filter-row {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
border-bottom-width: 0;
|
||||
background-color: color-mix(in srgb, var(--color-primary) 3%, transparent);
|
||||
transition: grid-template-rows 0.38s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-bottom-width 0.38s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.table-filter-row.is-open {
|
||||
grid-template-rows: 1fr;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--color-defaultborder);
|
||||
}
|
||||
|
||||
:host-context(.dark) .table-filter-row {
|
||||
border-color: color-mix(in srgb, #fff 10%, transparent);
|
||||
background-color: color-mix(in srgb, #fff 3%, transparent);
|
||||
}
|
||||
|
||||
.table-filter-row-inner {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1.25rem;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.28s ease 0.06s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.table-filter-row:not(.is-open) .table-filter-row-inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-filter-row.is-open .table-filter-row-inner {
|
||||
padding: 0.875rem 1.25rem;
|
||||
opacity: 1;
|
||||
transition: opacity 0.32s ease 0.08s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.table-filter-row-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: -0.5rem;
|
||||
padding-top: 10px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--color-primary) 40%, transparent) transparent;
|
||||
}
|
||||
|
||||
.table-filter-row-content>* {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.table-filter-row-content::-webkit-scrollbar {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.table-filter-row-content::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
|
||||
border-radius: 999px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.table-filter-row-content:hover::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 65%, transparent);
|
||||
}
|
||||
|
||||
.table-filter-row-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.table-filter-row-actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.table-filter-row-actions.is-pinned {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
padding-left: 0.75rem;
|
||||
background: linear-gradient(to right, transparent, color-mix(in srgb, var(--color-primary) 3%, var(--color-white)) 1rem);
|
||||
}
|
||||
|
||||
:host-context(.dark) .table-filter-row-actions.is-pinned {
|
||||
background: linear-gradient(to right, transparent, color-mix(in srgb, #fff 3%, var(--color-bodybg)) 1rem);
|
||||
}
|
||||
|
||||
.table-filter-row .filter-close-btn {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.filter-close-btn {
|
||||
transition: transform 0.15s ease, background-color 0.15s ease;
|
||||
border-color: #7c3deb;
|
||||
}
|
||||
|
||||
.filter-close-btn:hover {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.filter-close-btn:active {
|
||||
transform: rotate(90deg) scale(0.9);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, DestroyRef, HostListener, computed, contentChildren, effect, inject, input, output, signal } from '@angular/core';
|
||||
import { Component, DestroyRef, ElementRef, HostListener, computed, contentChildren, effect, inject, input, output, signal, viewChild, contentChild } from '@angular/core';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
@@ -10,9 +10,9 @@ import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angul
|
||||
import { DataTableAction, DataTableActionEvent, DataTableCellContext, DataTableColumn, DataTablePageEvent, DataTableRecord, DataTableSortEvent } from './data-table.types';
|
||||
|
||||
import { Button } from '../button/button';
|
||||
import { contentChild } from '@angular/core';
|
||||
|
||||
import { DataTableToolbarDirective } from '../../directives/data-table-toolbar/data-table-toolbar.directive';
|
||||
import { DataTableFilterActionsDirective } from '../../directives/data-table-filter-actions/data-table-filter-actions.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-data-table',
|
||||
@@ -34,6 +34,34 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
readonly cellTemplates = contentChildren(DataTableCellDirective);
|
||||
readonly toolbarTemplate = contentChild(DataTableToolbarDirective);
|
||||
|
||||
readonly filterActionsTemplate = contentChild(DataTableFilterActionsDirective);
|
||||
|
||||
readonly filterRowContent = viewChild<ElementRef<HTMLElement>>('filterRowContent');
|
||||
readonly filterRowOverflowing = signal(false);
|
||||
private filterRowResizeObserver?: ResizeObserver;
|
||||
|
||||
private readonly filterRowContentEffect = effect(onCleanup => {
|
||||
const element = this.filterRowContent()?.nativeElement;
|
||||
|
||||
this.filterRowResizeObserver?.disconnect();
|
||||
|
||||
if (!element) {
|
||||
this.filterRowOverflowing.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkOverflow = () => {
|
||||
this.filterRowOverflowing.set(element.scrollWidth > element.clientWidth + 1);
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
|
||||
this.filterRowResizeObserver = new ResizeObserver(checkOverflow);
|
||||
this.filterRowResizeObserver.observe(element);
|
||||
|
||||
onCleanup(() => this.filterRowResizeObserver?.disconnect());
|
||||
});
|
||||
|
||||
private readonly defaultRowClasses = [
|
||||
'table-primary',
|
||||
'table-secondary',
|
||||
@@ -110,6 +138,8 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
searchDebounceTime = input(300);
|
||||
emptyMessage = input('No records found');
|
||||
emptyDescription = input('There is currently no data to display.');
|
||||
showFilterButton = input<boolean>(false);
|
||||
filterActive = input<boolean>(false);
|
||||
/*---------------------------*/
|
||||
|
||||
/* --------- Permission inputs ---- */
|
||||
@@ -141,6 +171,7 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
actionHeaderClass = input<string>('!text-center');
|
||||
actionCellClass = input<string>('!text-center');
|
||||
addClicked = output<void>();
|
||||
filterClicked = output<void>();
|
||||
|
||||
totalVisibleColumns = computed(() => this.columns().length + (this.actions().length > 0 ? 1 : 0));
|
||||
|
||||
@@ -563,6 +594,16 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
return this.composeClass(baseClass, '');
|
||||
}
|
||||
|
||||
onFilterClick(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.closeActionMenu();
|
||||
this.filterClicked.emit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { DataTableCellDirective } from '../../directives/data-table-cell.directive';
|
||||
export { DataTableToolbarDirective } from '../../directives/data-table-toolbar/data-table-toolbar.directive';
|
||||
export { DataTableFilterActionsDirective } from '../../directives/data-table-filter-actions/data-table-filter-actions.directive';
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
<app-form-field
|
||||
[label]="label()"
|
||||
[inputId]="inputId()"
|
||||
[control]="control()"
|
||||
[required]="required()"
|
||||
[disabled]="isDisabled()"
|
||||
[description]="description()"
|
||||
[hint]="hint()"
|
||||
[help]="help()"
|
||||
[floating]="isFloating()"
|
||||
[forceFloat]="shouldFloat()"
|
||||
[hideValidation]="hideValidation()"
|
||||
[showValidationWhenDirty]="showValidationWhenDirty()"
|
||||
[submitAttempted]="submitAttempted()"
|
||||
<app-form-field [label]="label()" [inputId]="inputId()" [control]="control()" [required]="required()"
|
||||
[disabled]="isDisabled()" [description]="description()" [hint]="hint()" [help]="help()" [floating]="isFloating()"
|
||||
[forceFloat]="shouldFloat()" [hideValidation]="hideValidation()"
|
||||
[showValidationWhenDirty]="showValidationWhenDirty()" [submitAttempted]="submitAttempted()"
|
||||
[validationMessages]="validationMessages()"
|
||||
[wrapperClass]="(isFloating() ? 'date-picker-floating ' : '') + wrapperClass()"
|
||||
[labelClass]="labelClass()"
|
||||
[contentClass]="fieldContentClass()"
|
||||
>
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
data-form-control
|
||||
[id]="inputId()"
|
||||
type="date"
|
||||
[class]="inputClass()"
|
||||
[value]="value() ?? ''"
|
||||
[placeholder]="isFloating() ? ' ' : placeholder()"
|
||||
[required]="required()"
|
||||
[readOnly]="readonly()"
|
||||
[disabled]="isDisabled()"
|
||||
[attr.min]="min()"
|
||||
[attr.max]="max()"
|
||||
[attr.aria-label]="label()"
|
||||
[attr.aria-invalid]="showError() ? 'true' : null"
|
||||
[attr.aria-required]="required() ? 'true' : null"
|
||||
(focus)="onFocus()"
|
||||
(input)="onInput($event)"
|
||||
(blur)="onBlur()"
|
||||
/>
|
||||
[wrapperClass]="(isFloating() ? 'date-picker-floating ' : '') + wrapperClass()" [labelClass]="labelClass()"
|
||||
[contentClass]="fieldContentClass()">
|
||||
<div #wrapperDiv class="relative w-full flex items-center cursor-pointer" (click)="openCalendar($event)">
|
||||
<spk-flatpickr #flatpickrComponent [id]="inputId()" [placeholder]="isFloating() ? ' ' : placeholder()" [mode]="mode()"
|
||||
[minDate]="min() || undefined" [maxDate]="max() || undefined" [readonly]="readonly()" [disabled]="isDisabled()"
|
||||
[altInput]="false" [convertModelValue]="true" [enableTime]="false" [dateFormat]="'Y-m-d'"
|
||||
[class]="inputClass() + ' !pe-14 ' + (mode() === 'range' && isRangeSelected() && !focused() ? '!text-transparent' : '')"
|
||||
[ngModel]="flatpickrValue()" (ngModelChange)="onDateChange($event)" (focus)="onFocus()" (blur)="onBlur()" />
|
||||
|
||||
@if (mode() === 'range' && isRangeSelected() && !focused()) {
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3 text-xs sm:text-sm font-medium text-defaulttextcolor dark:text-white/90">
|
||||
<span>{{ rangeFrom() }}</span>
|
||||
<span
|
||||
class="badge bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-light font-bold text-[0.7rem] uppercase tracking-wider mx-2 px-2 py-0.5 rounded border border-primary/20">
|
||||
TO
|
||||
</span>
|
||||
<span>{{ rangeTo() }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="absolute inset-y-0 end-0 flex items-center pe-3 gap-1 z-10">
|
||||
@if (clearable() && hasValue() && !isDisabled() && !readonly()) {
|
||||
<button #clearBtn type="button"
|
||||
class="spk-date-picker-clear-btn text-[#8c9097] dark:text-white/50 hover:text-red-500 dark:hover:text-red-400 p-0.5 rounded-full transition-colors cursor-pointer flex items-center justify-center"
|
||||
(click)="clearDate($event)" aria-label="Clear date">
|
||||
<i class="ri-close-line text-[1.1rem]"></i>
|
||||
</button>
|
||||
}
|
||||
<span #calendarIcon class="pointer-events-none text-[#8c9097] dark:text-white/50 flex items-center justify-center" aria-hidden="true">
|
||||
<i class="ri-calendar-line text-[1.1rem]"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</app-form-field>
|
||||
</app-form-field>
|
||||
@@ -4,107 +4,82 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:host ::ng-deep .date-picker-floating .floating-label .form-control {
|
||||
min-height: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
border: 1px solid var(--color-inputborder);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--color-white);
|
||||
color: transparent !important;
|
||||
font-size: 0.8125rem;
|
||||
padding-block: 0.5rem 0.15rem !important;
|
||||
padding-inline: 0.75rem !important;
|
||||
box-shadow: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
:host ::ng-deep {
|
||||
spk-flatpickr {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide native datetime format (dd/mm/yyyy) when un-floated and not focused */
|
||||
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-fields-wrapper,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-text,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-month-field,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-day-field,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-year-field {
|
||||
color: transparent !important;
|
||||
}
|
||||
.date-picker-floating .floating-label .form-control {
|
||||
min-height: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
border: 1px solid var(--color-inputborder, #e2e8f0);
|
||||
border-radius: 0.5rem !important;
|
||||
background-color: var(--color-white, #ffffff);
|
||||
color: var(--color-defaulttextcolor, #334155);
|
||||
font-size: 0.8125rem;
|
||||
padding-block: 0.5rem 0.15rem !important;
|
||||
padding-inline: 0.75rem 2.5rem !important;
|
||||
box-shadow: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
:host ::ng-deep .date-picker-floating .floating-label > label {
|
||||
position: absolute !important;
|
||||
inset-inline-start: 0.75rem !important;
|
||||
padding-inline: 0.35rem !important;
|
||||
background-color: var(--color-white) !important;
|
||||
font-weight: 400 !important;
|
||||
color: #8c9097 !important;
|
||||
z-index: 9999 !important;
|
||||
overflow: visible !important;
|
||||
white-space: nowrap !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.date-picker-floating .floating-label > label {
|
||||
position: absolute !important;
|
||||
inset-inline-start: 0.75rem !important;
|
||||
padding-inline: 0.35rem !important;
|
||||
background-color: var(--color-white, #ffffff) !important;
|
||||
font-weight: 400 !important;
|
||||
color: #8c9097 !important;
|
||||
z-index: 10 !important;
|
||||
overflow: visible !important;
|
||||
white-space: nowrap !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Float label and highlight purple on focus or when date is selected */
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float > label,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within > label {
|
||||
top: 0 !important;
|
||||
transform: translateY(-50%) scale(0.85) !important;
|
||||
transform-origin: left center !important;
|
||||
font-weight: 400 !important;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
/* Float label and highlight purple on focus or when date is selected */
|
||||
.date-picker-floating .floating-label--float > label,
|
||||
.date-picker-floating .floating-label:focus-within > label {
|
||||
top: 0 !important;
|
||||
transform: translateY(-50%) scale(0.85) !important;
|
||||
transform-origin: left center !important;
|
||||
font-weight: 400 !important;
|
||||
color: var(--color-primary, #7c3deb) !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float .form-control,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control {
|
||||
color: var(--color-defaulttextcolor) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
/* Show native datetime format / selected date value when floated or focused */
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit,
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-fields-wrapper,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-fields-wrapper,
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-text,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-text,
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-month-field,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-month-field,
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-day-field,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-day-field,
|
||||
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-year-field,
|
||||
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-year-field {
|
||||
color: var(--color-defaulttextcolor) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .date-picker-floating .form-control,
|
||||
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label > label {
|
||||
background-color: var(--color-bodybg) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label--float .form-control,
|
||||
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label:focus-within .form-control {
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit,
|
||||
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit {
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
/* Calendar icon picker indicator styling */
|
||||
:host ::ng-deep .form-control::-webkit-calendar-picker-indicator {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease, filter 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
.date-picker-floating .floating-label--float .form-control,
|
||||
.date-picker-floating .floating-label:focus-within .form-control {
|
||||
color: var(--color-defaulttextcolor, #334155) !important;
|
||||
border-color: var(--color-primary, #7c3deb) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Invert calendar picker icon color to white in dark mode */
|
||||
:host-context(.dark) ::ng-deep .form-control::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1) !important;
|
||||
opacity: 0.8;
|
||||
/* Dark mode styling replacing deprecated :host-context */
|
||||
.dark,
|
||||
[data-theme-mode="dark"] {
|
||||
:host ::ng-deep {
|
||||
.date-picker-floating .floating-label .form-control {
|
||||
background-color: var(--color-bodybg, #1a1c1e) !important;
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
.date-picker-floating .floating-label > label {
|
||||
background-color: var(--color-bodybg, #1a1c1e) !important;
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
|
||||
.date-picker-floating .floating-label--float > label,
|
||||
.date-picker-floating .floating-label:focus-within > label {
|
||||
color: #c084fc !important;
|
||||
}
|
||||
|
||||
.date-picker-floating .floating-label--float .form-control,
|
||||
.date-picker-floating .floating-label:focus-within .form-control {
|
||||
color: #ffffff !important;
|
||||
border-color: #a855f7 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,52 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Injector,
|
||||
ViewChild,
|
||||
computed,
|
||||
effect,
|
||||
forwardRef,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
NgControl,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms';
|
||||
|
||||
import { FormField } from '../form-field/form-field';
|
||||
import { ValidationMessageMap } from '../form-validation-message/form-validation-message';
|
||||
import {
|
||||
SpkFlatpickr,
|
||||
SpkFlatpickrMode,
|
||||
formatSingleDate,
|
||||
fromFlatpickrValue,
|
||||
toFlatpickrValue,
|
||||
} from '../../../../@spk/plugins&reusable/spk-flatpickr/spk-flatpickr';
|
||||
|
||||
export interface DateRangeValue {
|
||||
from: string | null;
|
||||
to: string | null;
|
||||
}
|
||||
|
||||
export type DatePickerValue = string | DateRangeValue | null;
|
||||
|
||||
export function formatSingleDateValue(raw: any): string | null {
|
||||
return formatSingleDate(raw);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-form-date-picker',
|
||||
standalone: true,
|
||||
imports: [FormField],
|
||||
imports: [FormsModule, ReactiveFormsModule, FormField, SpkFlatpickr],
|
||||
templateUrl: './form-date-picker.html',
|
||||
styleUrl: './form-date-picker.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -34,17 +58,25 @@ import { ValidationMessageMap } from '../form-validation-message/form-validation
|
||||
},
|
||||
],
|
||||
})
|
||||
export class FormDatePicker implements ControlValueAccessor {
|
||||
export class FormDatePicker implements ControlValueAccessor, AfterViewInit {
|
||||
private readonly injector = inject(Injector);
|
||||
|
||||
readonly flatpickrComponent = viewChild<SpkFlatpickr>('flatpickrComponent');
|
||||
|
||||
@ViewChild('wrapperDiv', { static: false }) wrapperDivRef?: ElementRef<HTMLElement>;
|
||||
@ViewChild('calendarIcon', { static: false }) calendarIconRef?: ElementRef<HTMLElement>;
|
||||
@ViewChild('clearBtn', { static: false }) clearBtnRef?: ElementRef<HTMLElement>;
|
||||
|
||||
readonly inputId = input.required<string>();
|
||||
readonly label = input.required<string>();
|
||||
readonly placeholder = input('');
|
||||
readonly mode = input<SpkFlatpickrMode>('single');
|
||||
readonly required = input(false);
|
||||
readonly readonly = input(false);
|
||||
readonly disabled = input(false);
|
||||
readonly min = input<string | null>(null);
|
||||
readonly max = input<string | null>(null);
|
||||
readonly clearable = input(true);
|
||||
readonly min = input<string | undefined>(undefined);
|
||||
readonly max = input<string | undefined>(undefined);
|
||||
readonly hint = input<string | null>(null);
|
||||
readonly help = input<string | null>(null);
|
||||
readonly description = input<string | null>(null);
|
||||
@@ -57,12 +89,12 @@ export class FormDatePicker implements ControlValueAccessor {
|
||||
readonly labelClass = input('');
|
||||
readonly fieldContentClass = input('');
|
||||
|
||||
readonly value = signal<string | null>(null);
|
||||
readonly value = signal<DatePickerValue>(null);
|
||||
readonly formDisabled = signal(false);
|
||||
readonly focused = signal(false);
|
||||
private readonly controlStateVersion = signal(0);
|
||||
|
||||
private onChange: (value: string | null) => void = () => {};
|
||||
private onChange: (value: DatePickerValue) => void = () => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
readonly control = computed<AbstractControl | null>(() =>
|
||||
@@ -71,7 +103,16 @@ export class FormDatePicker implements ControlValueAccessor {
|
||||
|
||||
readonly isFloating = computed(() => this.variant() === 'floating');
|
||||
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
|
||||
readonly hasValue = computed(() => !!this.value());
|
||||
readonly hasValue = computed(() => {
|
||||
const val = this.value();
|
||||
if (!val) return false;
|
||||
if (typeof val === 'string') return !!val.trim();
|
||||
if (typeof val === 'object' && val !== null && 'from' in val) {
|
||||
return !!(val.from || val.to);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
readonly shouldFloat = computed(() => this.focused() || this.hasValue());
|
||||
readonly showError = computed(() => {
|
||||
this.controlStateVersion();
|
||||
@@ -84,6 +125,30 @@ export class FormDatePicker implements ControlValueAccessor {
|
||||
this.showError() ? 'is-invalid' : '',
|
||||
].filter(Boolean).join(' '));
|
||||
|
||||
readonly flatpickrValue = computed(() => {
|
||||
return toFlatpickrValue(this.value(), this.mode());
|
||||
});
|
||||
|
||||
readonly rangeFrom = computed<string | null>(() => {
|
||||
const val = this.value();
|
||||
if (this.mode() === 'range' && val && typeof val === 'object' && 'from' in val) {
|
||||
return val.from ?? null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
readonly rangeTo = computed<string | null>(() => {
|
||||
const val = this.value();
|
||||
if (this.mode() === 'range' && val && typeof val === 'object' && 'to' in val) {
|
||||
return val.to ?? null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
readonly isRangeSelected = computed<boolean>(() => {
|
||||
return !!(this.rangeFrom() && this.rangeTo());
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect((onCleanup) => {
|
||||
const control = this.control();
|
||||
@@ -93,15 +158,15 @@ export class FormDatePicker implements ControlValueAccessor {
|
||||
}
|
||||
|
||||
const statusSubscription = control.statusChanges.subscribe(() => {
|
||||
this.controlStateVersion.update(value => value + 1);
|
||||
this.controlStateVersion.update(val => val + 1);
|
||||
});
|
||||
|
||||
const valueSubscription = control.valueChanges.subscribe(() => {
|
||||
this.controlStateVersion.update(value => value + 1);
|
||||
this.controlStateVersion.update(val => val + 1);
|
||||
});
|
||||
|
||||
const eventsSubscription = control.events?.subscribe(() => {
|
||||
this.controlStateVersion.update(value => value + 1);
|
||||
this.controlStateVersion.update(val => val + 1);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
@@ -112,11 +177,48 @@ export class FormDatePicker implements ControlValueAccessor {
|
||||
});
|
||||
}
|
||||
|
||||
writeValue(value: string | null): void {
|
||||
this.value.set(value ?? null);
|
||||
ngAfterViewInit(): void {
|
||||
const fp = this.flatpickrComponent();
|
||||
if (fp) {
|
||||
if (this.wrapperDivRef?.nativeElement) {
|
||||
fp.registerIgnoredElement(this.wrapperDivRef.nativeElement);
|
||||
}
|
||||
if (this.calendarIconRef?.nativeElement) {
|
||||
fp.registerIgnoredElement(this.calendarIconRef.nativeElement);
|
||||
}
|
||||
if (this.clearBtnRef?.nativeElement) {
|
||||
fp.registerIgnoredElement(this.clearBtnRef.nativeElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: string | null) => void): void {
|
||||
writeValue(val: DatePickerValue): void {
|
||||
if (!val) {
|
||||
this.value.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mode() === 'range') {
|
||||
if (typeof val === 'object' && val !== null && ('from' in val || 'to' in val)) {
|
||||
const range = val as DateRangeValue;
|
||||
const from = formatSingleDate(range.from);
|
||||
const to = formatSingleDate(range.to);
|
||||
this.value.set(from || to ? { from, to } : null);
|
||||
} else if (typeof val === 'string') {
|
||||
const parts = val.split(/\s+to\s+|\s+-\s+/i);
|
||||
const from = formatSingleDate(parts[0]);
|
||||
const to = formatSingleDate(parts[1]);
|
||||
this.value.set(from || to ? { from, to } : null);
|
||||
} else {
|
||||
this.value.set(null);
|
||||
}
|
||||
} else {
|
||||
const normalized = formatSingleDate(val);
|
||||
this.value.set(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: DatePickerValue) => void): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
@@ -128,11 +230,50 @@ export class FormDatePicker implements ControlValueAccessor {
|
||||
this.formDisabled.set(isDisabled);
|
||||
}
|
||||
|
||||
onInput(event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const nextValue = target.value.trim() || null;
|
||||
this.value.set(nextValue);
|
||||
this.onChange(nextValue);
|
||||
openCalendar(event?: MouseEvent): void {
|
||||
if (this.isDisabled() || this.readonly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event?.target as HTMLElement | undefined;
|
||||
if (target && (target.tagName === 'INPUT' || target.closest('.spk-date-picker-clear-btn'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const fp = this.flatpickrComponent();
|
||||
if (fp) {
|
||||
fp.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
clearDate(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.isDisabled() || this.readonly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value.set(null);
|
||||
this.onChange(null);
|
||||
this.onTouched();
|
||||
|
||||
const fp = this.flatpickrComponent();
|
||||
if (fp) {
|
||||
fp.clear();
|
||||
}
|
||||
}
|
||||
|
||||
onDateChange(nextRawValue: any): void {
|
||||
const normalized = fromFlatpickrValue(nextRawValue, this.mode());
|
||||
this.value.set(normalized);
|
||||
this.onChange(normalized);
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
onFocus(): void {
|
||||
@@ -143,5 +284,4 @@ export class FormDatePicker implements ControlValueAccessor {
|
||||
this.focused.set(false);
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
>
|
||||
<i
|
||||
<!-- <i
|
||||
class="ri-error-warning-line me-1 align-middle"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
></i> -->
|
||||
|
||||
{{ message() }}
|
||||
</p>
|
||||
|
||||
@@ -146,12 +146,10 @@
|
||||
<div class="modern-modal-footer">
|
||||
|
||||
@if (showCancelButton()) {
|
||||
<app-button
|
||||
action="cancel"
|
||||
[label]="cancelLabel()"
|
||||
[showIcon]="true"
|
||||
[disabled]="loading()"
|
||||
(buttonClicked)="close()" />
|
||||
|
||||
<app-button action="cancel" [variant]="'custom'" [label]="cancelLabel()" [showIcon]="true"
|
||||
className="onboarding-nav-btn onboarding-save-btn ti-btn-outline-primary whitespace-nowrap justify-center"
|
||||
(buttonClicked)="close()"></app-button>
|
||||
}
|
||||
|
||||
@if (showSubmitButton()) {
|
||||
|
||||
@@ -460,6 +460,126 @@
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ==========================================================
|
||||
Dark Mode
|
||||
========================================================== */
|
||||
|
||||
:host-context(.dark) .modern-modal {
|
||||
background: var(--color-bodybg) !important;
|
||||
box-shadow:
|
||||
0 15px 40px rgba(0, 0, 0, .25),
|
||||
0 30px 80px rgba(0, 0, 0, .35);
|
||||
}
|
||||
|
||||
:host-context(.dark) .modern-modal-body {
|
||||
color: var(--color-defaulttextcolor) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) .modern-modal-body::-webkit-scrollbar-thumb {
|
||||
background: var(--color-inputborder) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) .modern-modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-textmuted) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) .modern-modal-footer {
|
||||
background: var(--color-bodybg) !important;
|
||||
border-top: 1px solid var(--color-inputborder) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:first-child button {
|
||||
background: var(--color-light) !important;
|
||||
border-color: var(--color-inputborder) !important;
|
||||
color: var(--color-defaulttextcolor) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:first-child button:hover {
|
||||
background: var(--color-bodybg) !important;
|
||||
border-color: var(--color-textmuted) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:last-child button {
|
||||
box-shadow:
|
||||
0 8px 20px rgba(125, 61, 235, 0.55) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:last-child button:hover {
|
||||
box-shadow:
|
||||
0 12px 28px rgba(125, 61, 235, .65) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================
|
||||
Modal form-control borders in dark mode
|
||||
========================================================== */
|
||||
|
||||
:host-context(.dark) ::ng-deep .modern-modal {
|
||||
|
||||
.shared-form-input,
|
||||
.custom-floating-input,
|
||||
.form-control,
|
||||
.form-select,
|
||||
.ti-form-input,
|
||||
.ti-form-select,
|
||||
.ti-form-control,
|
||||
.ng-select-container,
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input:not([type]),
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="search"],
|
||||
input[type="date"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"],
|
||||
input[type="time"],
|
||||
input[type="week"] {
|
||||
border-color: rgb(115 122 130) !important;
|
||||
}
|
||||
|
||||
.form-check-input:not(:checked) {
|
||||
border-color: rgb(115 122 130) !important;
|
||||
}
|
||||
|
||||
/* keep focus / validation colors */
|
||||
.shared-form-control.is-focused .shared-form-input,
|
||||
.shared-form-input:focus,
|
||||
.custom-floating-input:focus,
|
||||
.form-control:focus,
|
||||
.form-select:focus,
|
||||
.ti-form-input:focus,
|
||||
.ti-form-select:focus,
|
||||
.ti-form-control:focus,
|
||||
.ng-select.ng-select-focused .ng-select-container,
|
||||
textarea:focus {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.form-control.is-invalid,
|
||||
.form-select.is-invalid,
|
||||
.ti-form-input.is-invalid,
|
||||
.ti-form-select.is-invalid,
|
||||
.ti-form-control.is-invalid,
|
||||
textarea.is-invalid,
|
||||
.has-error .shared-form-input,
|
||||
.ng-invalid.ng-touched,
|
||||
.custom-floating-input.ng-invalid.ng-touched {
|
||||
border-color: var(--color-danger) !important;
|
||||
}
|
||||
|
||||
.form-check-input:checked,
|
||||
.form-check-input.is-invalid {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.form-check-input.is-invalid:not(:checked) {
|
||||
border-color: var(--color-danger) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================
|
||||
Mobile
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { DataTableFilterActionsDirective } from './data-table-filter-actions.directive';
|
||||
|
||||
describe('DataTableFilterActionsDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new DataTableFilterActionsDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
Directive,
|
||||
TemplateRef,
|
||||
inject
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[appDataTableFilterActions]',
|
||||
standalone: true
|
||||
})
|
||||
export class DataTableFilterActionsDirective {
|
||||
|
||||
readonly templateRef = inject(
|
||||
TemplateRef<unknown>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
+90
-8
@@ -1,18 +1,11 @@
|
||||
@forward "tailwindcss";
|
||||
@forward "../public/assets/css/style.css";
|
||||
|
||||
|
||||
/* ====================== ngx-toastr ====================== */
|
||||
@forward "../node_modules/ngx-toastr/toastr.css";
|
||||
|
||||
|
||||
/* inter */
|
||||
@import "flatpickr/dist/flatpickr.css";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
/* Montserrat */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&display=swap');
|
||||
|
||||
|
||||
/* Etihad Altis Text (local font) */
|
||||
@font-face {
|
||||
font-family: 'EtihadAltis-Text';
|
||||
@@ -68,4 +61,93 @@ body {
|
||||
|
||||
.ti-form-select {
|
||||
border: 1px solid #7c3deb !important;
|
||||
}
|
||||
|
||||
/* ====================== Flatpickr Calendar Header Styling ====================== */
|
||||
.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;
|
||||
background-color: #ffffff !important;
|
||||
border: 1px solid var(--color-inputborder, #e2e8f0) !important;
|
||||
z-index: 99999 !important;
|
||||
|
||||
&.open {
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
|
||||
.dark &,
|
||||
&.dark,
|
||||
[data-theme-mode="dark"] & {
|
||||
background-color: #1a1c1e !important;
|
||||
border-color: rgba(255, 255, 255, 0.12) !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.flatpickr-months {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
height: 3.2rem !important;
|
||||
padding: 0.35rem 0.65rem !important;
|
||||
position: relative !important;
|
||||
background-color: rgba(124, 61, 235, 0.08) !important;
|
||||
border-bottom: 1px solid rgba(124, 61, 235, 0.12) !important;
|
||||
border-top-left-radius: 0.5rem !important;
|
||||
border-top-right-radius: 0.5rem !important;
|
||||
|
||||
.dark &,
|
||||
[data-theme-mode="dark"] & {
|
||||
background-color: rgba(124, 61, 235, 0.16) !important;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08) !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.15) !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.5rem !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user