commons-shared-web-ui 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -15,6 +15,7 @@ import * as i2 from '@angular/material/icon';
|
|
|
15
15
|
import { MatIconModule } from '@angular/material/icon';
|
|
16
16
|
import * as i10 from '@angular/material/button';
|
|
17
17
|
import { MatButtonModule } from '@angular/material/button';
|
|
18
|
+
import * as i7$1 from '@angular/material/menu';
|
|
18
19
|
import { MatMenuModule } from '@angular/material/menu';
|
|
19
20
|
import * as i7 from '@angular/material/datepicker';
|
|
20
21
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
|
@@ -39,14 +40,14 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
|
|
39
40
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
40
41
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
|
41
42
|
import * as i1$2 from '@angular/forms';
|
|
42
|
-
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
43
|
+
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators, FormControl, FormArray, FormGroup } from '@angular/forms';
|
|
43
44
|
import * as i1$1 from '@angular/router';
|
|
44
45
|
import * as i2$1 from '@angular/cdk/scrolling';
|
|
45
46
|
import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
|
|
46
47
|
import { Subject, BehaviorSubject, combineLatest, forkJoin, of } from 'rxjs';
|
|
47
48
|
import { debounceTime, distinctUntilChanged, takeUntil, map, finalize, catchError } from 'rxjs/operators';
|
|
48
49
|
import * as i3 from '@angular/common/http';
|
|
49
|
-
import {
|
|
50
|
+
import { HttpHeaders, HttpParams } from '@angular/common/http';
|
|
50
51
|
|
|
51
52
|
class MaterialModule {
|
|
52
53
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MaterialModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
@@ -2836,6 +2837,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2836
2837
|
class SummaryCardComponent {
|
|
2837
2838
|
config;
|
|
2838
2839
|
theme;
|
|
2840
|
+
labels;
|
|
2839
2841
|
cardClick = new EventEmitter();
|
|
2840
2842
|
constructor() { }
|
|
2841
2843
|
onCardClick() {
|
|
@@ -2895,15 +2897,17 @@ class SummaryCardComponent {
|
|
|
2895
2897
|
return styles;
|
|
2896
2898
|
}
|
|
2897
2899
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SummaryCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2898
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SummaryCardComponent, isStandalone: false, selector: "lib-summary-card", inputs: { config: "config", theme: "theme" }, outputs: { cardClick: "cardClick" }, ngImport: i0, template: "<div class=\"cc-summary-card\" (click)=\"onCardClick()\">\r\n <!-- Icon Section (Left Side) -->\r\n <div class=\"icon-section\" *ngIf=\"config.icon || config.iconImage\" [ngClass]=\"config.iconClass\"\r\n [ngStyle]=\"iconStyles\">\r\n <mat-icon *ngIf=\"config.icon\">{{ config.icon }}</mat-icon>\r\n <img *ngIf=\"!config.icon && config.iconImage\" [src]=\"config.iconImage\" alt=\"
|
|
2900
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SummaryCardComponent, isStandalone: false, selector: "lib-summary-card", inputs: { config: "config", theme: "theme", labels: "labels" }, outputs: { cardClick: "cardClick" }, ngImport: i0, template: "<div class=\"cc-summary-card\" (click)=\"onCardClick()\">\r\n <!-- Icon Section (Left Side) -->\r\n <div class=\"icon-section\" *ngIf=\"config.icon || config.iconImage\" [ngClass]=\"config.iconClass\"\r\n [ngStyle]=\"iconStyles\">\r\n <mat-icon *ngIf=\"config.icon\">{{ config.icon }}</mat-icon>\r\n <img *ngIf=\"!config.icon && config.iconImage\" [src]=\"config.iconImage\" [alt]=\"labels.iconAlt\">\r\n </div>\r\n\r\n <!-- Right Section (Header + Value/Meta) -->\r\n <div class=\"right-section\">\r\n <!-- Header (Full Width on Right) -->\r\n <div class=\"header\" [ngClass]=\"config.headerClass\" [ngStyle]=\"headerStyles\">\r\n {{ config.header }}\r\n </div>\r\n\r\n <!-- Value and Meta Row -->\r\n <div class=\"value-meta-row\">\r\n <!-- Content Section -->\r\n <div class=\"content-section\">\r\n <!-- Value Row (with optional inline description) -->\r\n <div class=\"value-row\" [class.inline-layout]=\"isDescriptionInline\">\r\n <div class=\"value-container\">\r\n <div class=\"value\" [ngClass]=\"config.valueClass\" [ngStyle]=\"valueStyles\">\r\n {{ config.value }}\r\n <span *ngIf=\"config.valueSubtext\" class=\"value-subtext\">{{ config.valueSubtext }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Description (inline or bottom based on config) -->\r\n <div class=\"description\" *ngIf=\"config.description\" [ngClass]=\"config.descriptionClass\"\r\n [ngStyle]=\"descriptionStyles\">\r\n {{ config.description }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Meta/Status Section (Aligned with Value) -->\r\n <div class=\"meta-section\" *ngIf=\"config.metaData && config.metaData.length > 0\">\r\n <div *ngFor=\"let meta of config.metaData\" class=\"meta-item\"\r\n [ngClass]=\"[meta.type === 'pill' ? 'meta-pill' : 'meta-text', meta.cssClass || '']\"\r\n [ngStyle]=\"getMetaStyles(meta)\">\r\n {{ meta.text }}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [":host{display:block;width:100%}.cc-summary-card{display:flex;flex-direction:row;align-items:stretch;box-sizing:border-box;font-family:var(--cc-sc-font-family, \"Inter\", sans-serif);background-color:var(--cc-sc-bg-color, #ffffff);border-radius:var(--cc-sc-border-radius, 8px);border:var(--cc-sc-border, 1px solid #e0e0e0);padding:var(--cc-sc-padding, 16px);box-shadow:var(--cc-sc-shadow, 0 2px 4px rgba(0, 0, 0, .05));transition:all var(--cc-sc-transition-duration, .2s) ease;height:100%;width:100%;overflow:hidden;gap:var(--cc-sc-icon-margin, 1rem)}.cc-summary-card.clickable{cursor:pointer}.cc-summary-card.clickable:hover{transform:var(--cc-sc-hover-transform);box-shadow:var(--cc-sc-hover-shadow)}.cc-summary-card.disabled{opacity:var(--cc-sc-disabled-opacity);cursor:not-allowed;pointer-events:none}.cc-summary-card .icon-section{width:var(--cc-sc-icon-box-size);height:var(--cc-sc-icon-box-size);min-width:var(--cc-sc-icon-box-size);display:flex;align-items:center;justify-content:center;border-radius:var(--cc-sc-icon-radius);background-color:var(--cc-sc-icon-bg);color:var(--cc-sc-icon-color);flex-shrink:0;align-self:center}.cc-summary-card .icon-section mat-icon{width:var(--cc-sc-icon-size);height:var(--cc-sc-icon-size);font-size:var(--cc-sc-icon-size);line-height:var(--cc-sc-icon-size)}.cc-summary-card .icon-section img{width:var(--cc-sc-icon-size);height:var(--cc-sc-icon-size);object-fit:contain}.cc-summary-card .right-section{display:flex;flex-direction:column;flex:1;min-width:0;gap:var(--cc-sc-content-gap, .5rem)}.cc-summary-card .right-section .header{font-size:var(--cc-sc-header-size);font-weight:var(--cc-sc-header-weight);text-transform:var(--cc-sc-header-transform);color:var(--cc-sc-header-color);letter-spacing:var(--cc-sc-header-letter-spacing);line-height:var(--cc-sc-header-line-height);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%}.cc-summary-card .right-section .value-meta-row{display:flex;flex-direction:row;align-items:flex-start;justify-content:space-between;gap:1rem;flex:1}.cc-summary-card .right-section .value-meta-row .content-section{display:flex;flex-direction:column;justify-content:flex-start;flex:1;min-width:0}.cc-summary-card .right-section .value-meta-row .content-section .value-row{display:flex;flex-direction:column;gap:var(--cc-sc-value-desc-gap)}.cc-summary-card .right-section .value-meta-row .content-section .value-row.inline-layout{flex-direction:row;align-items:baseline;flex-wrap:wrap}.cc-summary-card .right-section .value-meta-row .content-section .value-row .value{font-size:var(--cc-sc-value-size);font-weight:var(--cc-sc-value-weight);color:var(--cc-sc-value-color);line-height:var(--cc-sc-value-line-height);white-space:nowrap}.cc-summary-card .right-section .value-meta-row .content-section .value-row .value .value-subtext{font-size:var(--cc-sc-desc-size);font-weight:var(--cc-sc-desc-weight);color:var(--cc-sc-desc-color);margin-left:4px}.cc-summary-card .right-section .value-meta-row .content-section .value-row .description{font-size:var(--cc-sc-desc-size);font-weight:var(--cc-sc-desc-weight);color:var(--cc-sc-desc-color);line-height:var(--cc-sc-desc-line-height)}.cc-summary-card .right-section .value-meta-row .meta-section{display:flex;flex-direction:column;align-items:flex-end;justify-content:flex-start;flex-shrink:0;gap:6px}.cc-summary-card .right-section .value-meta-row .meta-section .meta-item{font-size:.8125rem;line-height:1.4;white-space:nowrap;text-align:right;flex-shrink:0}.cc-summary-card .right-section .value-meta-row .meta-section .meta-item.meta-text{color:var(--cc-sc-desc-color, #64748b);font-weight:500}.cc-summary-card .right-section .value-meta-row .meta-section .meta-item.meta-pill{display:inline-flex;align-items:center;justify-content:center;padding:var(--cc-sc-meta-pill-padding, 4px 12px);border-radius:var(--cc-sc-meta-pill-radius, 20px);font-size:var(--cc-sc-meta-pill-font-size, .75rem);font-weight:var(--cc-sc-meta-pill-font-weight, 600);line-height:1.2;background-color:var(--cc-sc-meta-pill-bg, #f1f5f9);color:var(--cc-sc-meta-pill-color, #475569)}@media(max-width:600px){.cc-summary-card .icon-section{margin-right:var(--cc-sc-icon-margin-mobile, .75rem)}.cc-summary-card .content-section .header{font-size:var(--cc-sc-header-size-mobile, .7rem)}.cc-summary-card .content-section .value-row .value{font-size:var(--cc-sc-value-size-mobile, 1.25rem)}.cc-summary-card .content-section .value-row .description{font-size:var(--cc-sc-desc-size-mobile, .7rem)}}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] });
|
|
2899
2901
|
}
|
|
2900
2902
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SummaryCardComponent, decorators: [{
|
|
2901
2903
|
type: Component,
|
|
2902
|
-
args: [{ selector: 'lib-summary-card', standalone: false, template: "<div class=\"cc-summary-card\" (click)=\"onCardClick()\">\r\n <!-- Icon Section (Left Side) -->\r\n <div class=\"icon-section\" *ngIf=\"config.icon || config.iconImage\" [ngClass]=\"config.iconClass\"\r\n [ngStyle]=\"iconStyles\">\r\n <mat-icon *ngIf=\"config.icon\">{{ config.icon }}</mat-icon>\r\n <img *ngIf=\"!config.icon && config.iconImage\" [src]=\"config.iconImage\" alt=\"
|
|
2904
|
+
args: [{ selector: 'lib-summary-card', standalone: false, template: "<div class=\"cc-summary-card\" (click)=\"onCardClick()\">\r\n <!-- Icon Section (Left Side) -->\r\n <div class=\"icon-section\" *ngIf=\"config.icon || config.iconImage\" [ngClass]=\"config.iconClass\"\r\n [ngStyle]=\"iconStyles\">\r\n <mat-icon *ngIf=\"config.icon\">{{ config.icon }}</mat-icon>\r\n <img *ngIf=\"!config.icon && config.iconImage\" [src]=\"config.iconImage\" [alt]=\"labels.iconAlt\">\r\n </div>\r\n\r\n <!-- Right Section (Header + Value/Meta) -->\r\n <div class=\"right-section\">\r\n <!-- Header (Full Width on Right) -->\r\n <div class=\"header\" [ngClass]=\"config.headerClass\" [ngStyle]=\"headerStyles\">\r\n {{ config.header }}\r\n </div>\r\n\r\n <!-- Value and Meta Row -->\r\n <div class=\"value-meta-row\">\r\n <!-- Content Section -->\r\n <div class=\"content-section\">\r\n <!-- Value Row (with optional inline description) -->\r\n <div class=\"value-row\" [class.inline-layout]=\"isDescriptionInline\">\r\n <div class=\"value-container\">\r\n <div class=\"value\" [ngClass]=\"config.valueClass\" [ngStyle]=\"valueStyles\">\r\n {{ config.value }}\r\n <span *ngIf=\"config.valueSubtext\" class=\"value-subtext\">{{ config.valueSubtext }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Description (inline or bottom based on config) -->\r\n <div class=\"description\" *ngIf=\"config.description\" [ngClass]=\"config.descriptionClass\"\r\n [ngStyle]=\"descriptionStyles\">\r\n {{ config.description }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Meta/Status Section (Aligned with Value) -->\r\n <div class=\"meta-section\" *ngIf=\"config.metaData && config.metaData.length > 0\">\r\n <div *ngFor=\"let meta of config.metaData\" class=\"meta-item\"\r\n [ngClass]=\"[meta.type === 'pill' ? 'meta-pill' : 'meta-text', meta.cssClass || '']\"\r\n [ngStyle]=\"getMetaStyles(meta)\">\r\n {{ meta.text }}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [":host{display:block;width:100%}.cc-summary-card{display:flex;flex-direction:row;align-items:stretch;box-sizing:border-box;font-family:var(--cc-sc-font-family, \"Inter\", sans-serif);background-color:var(--cc-sc-bg-color, #ffffff);border-radius:var(--cc-sc-border-radius, 8px);border:var(--cc-sc-border, 1px solid #e0e0e0);padding:var(--cc-sc-padding, 16px);box-shadow:var(--cc-sc-shadow, 0 2px 4px rgba(0, 0, 0, .05));transition:all var(--cc-sc-transition-duration, .2s) ease;height:100%;width:100%;overflow:hidden;gap:var(--cc-sc-icon-margin, 1rem)}.cc-summary-card.clickable{cursor:pointer}.cc-summary-card.clickable:hover{transform:var(--cc-sc-hover-transform);box-shadow:var(--cc-sc-hover-shadow)}.cc-summary-card.disabled{opacity:var(--cc-sc-disabled-opacity);cursor:not-allowed;pointer-events:none}.cc-summary-card .icon-section{width:var(--cc-sc-icon-box-size);height:var(--cc-sc-icon-box-size);min-width:var(--cc-sc-icon-box-size);display:flex;align-items:center;justify-content:center;border-radius:var(--cc-sc-icon-radius);background-color:var(--cc-sc-icon-bg);color:var(--cc-sc-icon-color);flex-shrink:0;align-self:center}.cc-summary-card .icon-section mat-icon{width:var(--cc-sc-icon-size);height:var(--cc-sc-icon-size);font-size:var(--cc-sc-icon-size);line-height:var(--cc-sc-icon-size)}.cc-summary-card .icon-section img{width:var(--cc-sc-icon-size);height:var(--cc-sc-icon-size);object-fit:contain}.cc-summary-card .right-section{display:flex;flex-direction:column;flex:1;min-width:0;gap:var(--cc-sc-content-gap, .5rem)}.cc-summary-card .right-section .header{font-size:var(--cc-sc-header-size);font-weight:var(--cc-sc-header-weight);text-transform:var(--cc-sc-header-transform);color:var(--cc-sc-header-color);letter-spacing:var(--cc-sc-header-letter-spacing);line-height:var(--cc-sc-header-line-height);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%}.cc-summary-card .right-section .value-meta-row{display:flex;flex-direction:row;align-items:flex-start;justify-content:space-between;gap:1rem;flex:1}.cc-summary-card .right-section .value-meta-row .content-section{display:flex;flex-direction:column;justify-content:flex-start;flex:1;min-width:0}.cc-summary-card .right-section .value-meta-row .content-section .value-row{display:flex;flex-direction:column;gap:var(--cc-sc-value-desc-gap)}.cc-summary-card .right-section .value-meta-row .content-section .value-row.inline-layout{flex-direction:row;align-items:baseline;flex-wrap:wrap}.cc-summary-card .right-section .value-meta-row .content-section .value-row .value{font-size:var(--cc-sc-value-size);font-weight:var(--cc-sc-value-weight);color:var(--cc-sc-value-color);line-height:var(--cc-sc-value-line-height);white-space:nowrap}.cc-summary-card .right-section .value-meta-row .content-section .value-row .value .value-subtext{font-size:var(--cc-sc-desc-size);font-weight:var(--cc-sc-desc-weight);color:var(--cc-sc-desc-color);margin-left:4px}.cc-summary-card .right-section .value-meta-row .content-section .value-row .description{font-size:var(--cc-sc-desc-size);font-weight:var(--cc-sc-desc-weight);color:var(--cc-sc-desc-color);line-height:var(--cc-sc-desc-line-height)}.cc-summary-card .right-section .value-meta-row .meta-section{display:flex;flex-direction:column;align-items:flex-end;justify-content:flex-start;flex-shrink:0;gap:6px}.cc-summary-card .right-section .value-meta-row .meta-section .meta-item{font-size:.8125rem;line-height:1.4;white-space:nowrap;text-align:right;flex-shrink:0}.cc-summary-card .right-section .value-meta-row .meta-section .meta-item.meta-text{color:var(--cc-sc-desc-color, #64748b);font-weight:500}.cc-summary-card .right-section .value-meta-row .meta-section .meta-item.meta-pill{display:inline-flex;align-items:center;justify-content:center;padding:var(--cc-sc-meta-pill-padding, 4px 12px);border-radius:var(--cc-sc-meta-pill-radius, 20px);font-size:var(--cc-sc-meta-pill-font-size, .75rem);font-weight:var(--cc-sc-meta-pill-font-weight, 600);line-height:1.2;background-color:var(--cc-sc-meta-pill-bg, #f1f5f9);color:var(--cc-sc-meta-pill-color, #475569)}@media(max-width:600px){.cc-summary-card .icon-section{margin-right:var(--cc-sc-icon-margin-mobile, .75rem)}.cc-summary-card .content-section .header{font-size:var(--cc-sc-header-size-mobile, .7rem)}.cc-summary-card .content-section .value-row .value{font-size:var(--cc-sc-value-size-mobile, 1.25rem)}.cc-summary-card .content-section .value-row .description{font-size:var(--cc-sc-desc-size-mobile, .7rem)}}\n"] }]
|
|
2903
2905
|
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
2904
2906
|
type: Input
|
|
2905
2907
|
}], theme: [{
|
|
2906
2908
|
type: Input
|
|
2909
|
+
}], labels: [{
|
|
2910
|
+
type: Input
|
|
2907
2911
|
}], cardClick: [{
|
|
2908
2912
|
type: Output
|
|
2909
2913
|
}] } });
|
|
@@ -2931,6 +2935,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2931
2935
|
}]
|
|
2932
2936
|
}] });
|
|
2933
2937
|
|
|
2938
|
+
/**
|
|
2939
|
+
* Recursively translates the Configuration object using the provided labels map.
|
|
2940
|
+
* This function processes:
|
|
2941
|
+
* - Section Titles
|
|
2942
|
+
* - Field Labels
|
|
2943
|
+
* - Placeholders
|
|
2944
|
+
* - Help Texts
|
|
2945
|
+
* - Option Labels (for static options)
|
|
2946
|
+
* - Repeater Labels (addLabel, repeaterItemLabel)
|
|
2947
|
+
*
|
|
2948
|
+
* @param config The FormConfig object to translate
|
|
2949
|
+
* @param labelsMap A map of key-value pairs for translation (Flattened JSON)
|
|
2950
|
+
* @returns A new FormConfig object with translated strings
|
|
2951
|
+
*/
|
|
2952
|
+
function translateConfig(config, labelsMap) {
|
|
2953
|
+
if (!config || !labelsMap) {
|
|
2954
|
+
return config;
|
|
2955
|
+
}
|
|
2956
|
+
// Deep clone to avoid mutating original config, if not already cloned
|
|
2957
|
+
// Note: Assuming config passed here is already a clone to be modified or we clone it.
|
|
2958
|
+
// To be safe, let's clone it if it's the root call, but this function is helper.
|
|
2959
|
+
// In component we do: processedConfig = JSON.parse(JSON.stringify(config)); -> translateConfig(processedConfig, map)
|
|
2960
|
+
// So distinct reference is passed.
|
|
2961
|
+
// Helper to translate a single key
|
|
2962
|
+
const t = (key) => {
|
|
2963
|
+
return labelsMap[key] || key;
|
|
2964
|
+
};
|
|
2965
|
+
if (config.sections) {
|
|
2966
|
+
config.sections.forEach((section) => {
|
|
2967
|
+
if (section.sectionTitle) {
|
|
2968
|
+
section.sectionTitle = t(section.sectionTitle);
|
|
2969
|
+
}
|
|
2970
|
+
if (section.repeaterItemLabel) {
|
|
2971
|
+
section.repeaterItemLabel = t(section.repeaterItemLabel);
|
|
2972
|
+
}
|
|
2973
|
+
if (section.addLabel) {
|
|
2974
|
+
section.addLabel = t(section.addLabel);
|
|
2975
|
+
}
|
|
2976
|
+
if (section.fields) {
|
|
2977
|
+
section.fields.forEach((field) => {
|
|
2978
|
+
translateField(field, t);
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
return config;
|
|
2984
|
+
}
|
|
2985
|
+
function translateField(field, t) {
|
|
2986
|
+
if (field.label) {
|
|
2987
|
+
field.label = t(field.label);
|
|
2988
|
+
}
|
|
2989
|
+
if (field.placeholder) {
|
|
2990
|
+
field.placeholder = t(field.placeholder);
|
|
2991
|
+
}
|
|
2992
|
+
if (field.helpText) {
|
|
2993
|
+
field.helpText = t(field.helpText);
|
|
2994
|
+
}
|
|
2995
|
+
if (field.suffixText) {
|
|
2996
|
+
field.suffixText = t(field.suffixText);
|
|
2997
|
+
}
|
|
2998
|
+
if (field.prefixText) { // Assuming prefixText might exist or be added
|
|
2999
|
+
field.prefixText = t(field.prefixText);
|
|
3000
|
+
}
|
|
3001
|
+
// Translate options if they exist
|
|
3002
|
+
if (field.options) {
|
|
3003
|
+
field.options.forEach((opt) => {
|
|
3004
|
+
if (opt.label) {
|
|
3005
|
+
opt.label = t(opt.label);
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
// Handle subFields for composite type
|
|
3010
|
+
if (field.subFields) {
|
|
3011
|
+
field.subFields.forEach((sub) => {
|
|
3012
|
+
translateField(sub, t);
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
|
|
2934
3017
|
class ConfigurableFormComponent {
|
|
2935
3018
|
fb;
|
|
2936
3019
|
snackBar;
|
|
@@ -2939,6 +3022,7 @@ class ConfigurableFormComponent {
|
|
|
2939
3022
|
jsonConfig;
|
|
2940
3023
|
data = {};
|
|
2941
3024
|
baseApiUrl = '';
|
|
3025
|
+
labels;
|
|
2942
3026
|
optionsLoad = new EventEmitter();
|
|
2943
3027
|
form;
|
|
2944
3028
|
processedConfig;
|
|
@@ -2971,6 +3055,10 @@ class ConfigurableFormComponent {
|
|
|
2971
3055
|
else {
|
|
2972
3056
|
return;
|
|
2973
3057
|
}
|
|
3058
|
+
// Apply translations if labels and labelsObject are provided
|
|
3059
|
+
if (this.labels && this.labels.labelsObject) {
|
|
3060
|
+
translateConfig(this.processedConfig, this.labels.labelsObject);
|
|
3061
|
+
}
|
|
2974
3062
|
this.normalizeFields();
|
|
2975
3063
|
this.initializeFieldVisibility();
|
|
2976
3064
|
this.buildForm();
|
|
@@ -3197,7 +3285,7 @@ class ConfigurableFormComponent {
|
|
|
3197
3285
|
const formArray = this.getFormArray(arrayName);
|
|
3198
3286
|
// Check max items
|
|
3199
3287
|
if (section.maxItems && formArray.length >= section.maxItems) {
|
|
3200
|
-
this.snackBar.open(
|
|
3288
|
+
this.snackBar.open(this.labels.errorMaxItemsAllowed.replace('{0}', String(section.maxItems)), this.labels.closeSnackBar, { duration: 3000 });
|
|
3201
3289
|
return;
|
|
3202
3290
|
}
|
|
3203
3291
|
formArray.push(this.createGroup(section.fields));
|
|
@@ -3223,14 +3311,6 @@ class ConfigurableFormComponent {
|
|
|
3223
3311
|
setupDependencies() {
|
|
3224
3312
|
this.processedConfig.sections.forEach(section => {
|
|
3225
3313
|
section.fields.forEach(field => {
|
|
3226
|
-
if (field.dependsOn) {
|
|
3227
|
-
const control = this.form.get(field.dependsOn);
|
|
3228
|
-
if (control) {
|
|
3229
|
-
control.valueChanges.subscribe(value => {
|
|
3230
|
-
this.onFieldValueChange(field, value);
|
|
3231
|
-
});
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
3314
|
// Also setup for fields with dependent array
|
|
3235
3315
|
if (field.dependent && field.dependent.length > 0) {
|
|
3236
3316
|
const control = this.form.get(field.name);
|
|
@@ -3383,7 +3463,7 @@ class ConfigurableFormComponent {
|
|
|
3383
3463
|
const file = files[i];
|
|
3384
3464
|
// Check file size (5MB limit)
|
|
3385
3465
|
if (file.size > 5 * 1024 * 1024) {
|
|
3386
|
-
this.snackBar.open(
|
|
3466
|
+
this.snackBar.open(this.labels.errorFileLimitExceeded.replace('{0}', file.name), this.labels.closeSnackBar, { duration: 3000 });
|
|
3387
3467
|
continue;
|
|
3388
3468
|
}
|
|
3389
3469
|
uploadedFiles.push({
|
|
@@ -3453,11 +3533,11 @@ class ConfigurableFormComponent {
|
|
|
3453
3533
|
return this.passwordFieldState.get(fieldName) || false;
|
|
3454
3534
|
}
|
|
3455
3535
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConfigurableFormComponent, deps: [{ token: i1$2.FormBuilder }, { token: i2$2.MatSnackBar }, { token: i3.HttpClient }], target: i0.ɵɵFactoryTarget.Component });
|
|
3456
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: ConfigurableFormComponent, isStandalone: true, selector: "lib-configurable-form", inputs: { config: "config", jsonConfig: "jsonConfig", data: "data", baseApiUrl: "baseApiUrl" }, outputs: { optionsLoad: "optionsLoad" }, usesOnChanges: true, ngImport: i0, template: "<form [formGroup]=\"form\" class=\"configurable-form-container\" *ngIf=\"form\">\r\n\r\n <ng-container *ngFor=\"let section of sections\">\r\n\r\n <!-- Repeater Section -->\r\n <div *ngIf=\"section.isRepeater; else normalSection\"\r\n [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div [formArrayName]=\"section.formArrayName || 'items'\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let item of getFormArray(section.formArrayName!).controls; let i = index\"\r\n [formGroupName]=\"i\" class=\"repeater-item\">\r\n <div class=\"repeater-item-header\" *ngIf=\"getFormArray(section.formArrayName!).length > 1\">\r\n <span class=\"text-small\">{{ section.repeaterItemLabel || 'Item' }} {{i + 1}}</span>\r\n <button mat-icon-button color=\"warn\" type=\"button\"\r\n (click)=\"removeRepeaterItem(section.formArrayName!, i)\"\r\n [disabled]=\"section.minItems && getFormArray(section.formArrayName!).length <= section.minItems\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <!-- Fields Grid -->\r\n <div class=\"form-grid\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: item}\"></ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"repeater-separator\" *ngIf=\"i < getFormArray(section.formArrayName!).length - 1\"></div>\r\n </div>\r\n </div>\r\n\r\n <button mat-button color=\"primary\" type=\"button\" (click)=\"addRepeaterItem(section)\" class=\"add-btn\"\r\n *ngIf=\"!section.collapsed\"\r\n [disabled]=\"section.maxItems && getFormArray(section.formArrayName!).length >= section.maxItems\">\r\n <mat-icon>add_circle_outline</mat-icon> {{ section.addLabel || 'Add More' }}\r\n </button>\r\n </div>\r\n\r\n <!-- Normal Section -->\r\n <ng-template #normalSection>\r\n <div [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n <div class=\"form-grid\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <!-- Pass the root form group to the template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: form}\"></ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n </ng-container>\r\n\r\n</form>\r\n\r\n<!-- Reusable Field Template -->\r\n<ng-template #fieldTemplate let-field=\"field\" let-group=\"group\">\r\n <div [formGroup]=\"group\">\r\n\r\n <!-- Text / Email / Number / Tel / URL (Password removed) -->\r\n <div *ngIf=\"['text', 'email', 'number', 'tel', 'url'].includes(field.type || '')\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"field.type\" [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\"\r\n [readonly]=\"field.readonly\" class=\"form-input\" [name]=\"field.name\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} Characters\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Password Field (Separated) -->\r\n <div *ngIf=\"field.type === 'password'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"isPasswordVisible(field.name) ? 'text' : 'password'\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n <mat-icon class=\"password-toggle-icon\" (click)=\"togglePassword(field.name)\">\r\n {{ isPasswordVisible(field.name) ? 'visibility' : 'visibility_off' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} Characters\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Textarea -->\r\n <div *ngIf=\"field.type === 'textarea'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <textarea [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\"\r\n class=\"form-input form-textarea\" rows=\"4\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\"></textarea>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} Characters\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Select / Dropdown -->\r\n <div *ngIf=\"field.type === 'select' || field.type === 'dropdown'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <select [formControlName]=\"field.name\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <option value=\"\" disabled selected *ngIf=\"field.placeholder\">{{ field.placeholder }}</option>\r\n <option value=\"\" disabled selected *ngIf=\"!field.placeholder\">Select</option>\r\n <option *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\">{{ opt.label }}</option>\r\n </select>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Date -->\r\n <div *ngIf=\"field.type === 'date'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"relative\">\r\n <input matInput [matDatepicker]=\"picker\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <mat-datepicker-toggle matSuffix [for]=\"picker\" class=\"date-toggle\"></mat-datepicker-toggle>\r\n <mat-datepicker #picker></mat-datepicker>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Radio -->\r\n <div *ngIf=\"field.type === 'radio'\" class=\"radio-group-container\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <label class=\"field-label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </label>\r\n <mat-radio-group [formControlName]=\"field.name\" class=\"radio-group\">\r\n <mat-radio-button *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\" class=\"radio-button\">\r\n {{ opt.label }}\r\n </mat-radio-button>\r\n </mat-radio-group>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Composite Field (Sub-groups) -->\r\n <div *ngIf=\"field.type === 'composite'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"composite-container\">\r\n <ng-container *ngFor=\"let sub of field.subFields; let last = last\">\r\n <div class=\"composite-sub-field\" style=\"flex: 1;\">\r\n <div class=\"field-label\" *ngIf=\"sub.label\"\r\n style=\"font-size: 0.75rem; font-weight: normal; margin-bottom: 0.25rem;\">\r\n {{ sub.label }}\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"sub.type\" [formControlName]=\"sub.name\" [placeholder]=\"sub.placeholder || ''\"\r\n [readonly]=\"sub.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(sub.name)?.touched && group.get(sub.name)?.invalid\">\r\n <div class=\"input-suffix\" [class.color-suffix]=\"sub.suffixText === '%'\">\r\n <span class=\"suffix-text\" *ngIf=\"sub.suffixText\">{{ sub.suffixText }}</span>\r\n <mat-icon *ngIf=\"sub.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- Adjust separator alignment if labels are present -->\r\n <span class=\"separator\" *ngIf=\"!last && field.separator\"\r\n [style.padding-top]=\"sub.label ? '1.25rem' : '0'\">{{ field.separator }}</span>\r\n </ng-container>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"error-text text-danger text-small mt-1\" *ngIf=\"group.errors?.['invalid_minMax_' + field.name]\">\r\n Min value cannot be greater than Max value.\r\n </div>\r\n <div class=\"error-text text-danger text-small mt-1\"\r\n *ngIf=\"group.errors?.['invalid_percentageTotal_' + field.name]\">\r\n Total percentage must be 100%.\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- File Upload -->\r\n <div *ngIf=\"field.type === 'file'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n\r\n <!-- Upload Box -->\r\n <div class=\"upload-box\" (click)=\"fileInput.click()\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n *ngIf=\"!field.uploadedFiles || field.uploadedFiles.length === 0 || field.multiple\">\r\n <mat-icon class=\"upload-icon\">cloud_upload</mat-icon>\r\n <div class=\"upload-text\">Click to upload File</div>\r\n <div class=\"upload-hint\">PDF, DOCX, JPG, PNG (Max 5MB)</div>\r\n <input #fileInput type=\"file\" [attr.multiple]=\"field.multiple ? true : null\" [attr.accept]=\"field.accept\"\r\n (change)=\"onFileChange($event, field)\" style=\"display: none;\">\r\n </div>\r\n\r\n <!-- Uploaded Files List -->\r\n <div class=\"uploaded-files-list\" *ngIf=\"field.uploadedFiles && field.uploadedFiles.length > 0\">\r\n <div class=\"uploaded-file-item\" *ngFor=\"let file of field.uploadedFiles; let i = index\">\r\n <div class=\"file-icon\">\r\n <mat-icon class=\"pdf-icon\" *ngIf=\"file.type === 'application/pdf'\">picture_as_pdf</mat-icon>\r\n <mat-icon class=\"doc-icon\"\r\n *ngIf=\"file.type.includes('word') || file.type.includes('document')\">description</mat-icon>\r\n <mat-icon class=\"img-icon\" *ngIf=\"file.type.includes('image')\">image</mat-icon>\r\n <mat-icon class=\"file-icon-default\"\r\n *ngIf=\"!file.type.includes('pdf') && !file.type.includes('word') && !file.type.includes('document') && !file.type.includes('image')\">insert_drive_file</mat-icon>\r\n </div>\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ file.name }}</div>\r\n <div class=\"file-size\">{{ (file.size / 1024).toFixed(2) }} KB</div>\r\n </div>\r\n <button mat-icon-button color=\"warn\" type=\"button\" (click)=\"removeFile(field, i)\"\r\n class=\"remove-file-btn\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n</ng-template>", styles: [":host{display:block;width:100%}.configurable-form-container{padding:0;font-family:var(--cf-font-family, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif)}.configurable-form-container .section-card{background:var(--cf-surface-background, #ffffff);border:1px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:var(--cf-section-padding, 2rem);margin-bottom:var(--cf-section-spacing, 1.5rem);box-shadow:var(--cf-section-shadow, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06))}.configurable-form-container .section-card:last-of-type{margin-bottom:0}.configurable-form-container .section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--cf-section-spacing, 1.5rem);padding-bottom:1rem;border-bottom:2px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .section-title{font-size:var(--cf-section-title-size, 1.25rem);font-weight:var(--cf-section-title-weight, 600);color:var(--cf-text-primary, #111827);margin:0;letter-spacing:-.025em}.configurable-form-container .form-grid{display:flex;flex-wrap:wrap;margin-left:-.625rem;margin-right:-.625rem}.configurable-form-container .form-col{position:relative;width:100%;padding-left:.625rem;padding-right:.625rem;margin-bottom:1.25rem}.configurable-form-container .col-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-12{flex:0 0 100%;max-width:100%}@media(min-width:769px){.configurable-form-container .col-md-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-md-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-md-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-md-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-md-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-md-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-md-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-md-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-md-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-md-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-md-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-md-12{flex:0 0 100%;max-width:100%}}.configurable-form-container .field-container{margin-bottom:0}.configurable-form-container .field-label{display:block;font-size:var(--cf-label-size, .875rem);font-weight:var(--cf-label-weight, 600);color:var(--cf-text-primary, #111827);margin-bottom:.5rem;line-height:1.25rem}.configurable-form-container .field-label .text-danger{color:var(--cf-error-color, #DC2626);margin-left:.125rem}.configurable-form-container .input-wrapper{position:relative;display:flex;align-items:center}.configurable-form-container .form-input{width:100%;padding:var(--cf-input-padding-y, .625rem) var(--cf-input-padding-x, .875rem);font-size:var(--cf-input-font-size, .875rem);line-height:1.5;color:var(--cf-text-primary, #111827);background-color:var(--cf-input-bg, #ffffff);border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-input-radius, 8px);transition:all .2s ease;font-family:inherit}.configurable-form-container .form-input:hover:not(:disabled):not([readonly]){border-color:var(--cf-input-hover-border-color, #9CA3AF)}.configurable-form-container .form-input:focus{outline:none;border-color:var(--cf-primary-color, #3B82F6);box-shadow:0 0 0 3px #3b82f61a}.configurable-form-container .form-input::placeholder{color:#9ca3af}.configurable-form-container .form-input:disabled,.configurable-form-container .form-input[readonly]{background-color:var(--cf-disabled-background, #F3F4F6);color:var(--cf-text-secondary, #6B7280);cursor:not-allowed;border-color:#e5e7eb}.configurable-form-container .form-input.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .form-input.is-invalid:focus{box-shadow:0 0 0 3px #dc26261a}.configurable-form-container select.form-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}.configurable-form-container select.form-input:disabled{cursor:not-allowed}.configurable-form-container .form-textarea{resize:vertical;min-height:100px;font-family:inherit}.configurable-form-container .input-suffix{position:absolute;right:.75rem;display:flex;align-items:center;gap:.5rem;pointer-events:none}.configurable-form-container .input-suffix .password-toggle-icon{pointer-events:auto;cursor:pointer}.configurable-form-container .input-suffix .password-toggle-icon:hover{color:var(--cf-text-primary, #111827)}.configurable-form-container .input-suffix .suffix-text{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .input-suffix mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .input-suffix .lock-icon{color:#9ca3af}.configurable-form-container .input-suffix.color-suffix .suffix-text{color:var(--cf-primary-color, #3B82F6);font-weight:600}.configurable-form-container .char-count-row{display:flex;justify-content:space-between;align-items:center;margin-top:.5rem}.configurable-form-container .char-count{font-size:.75rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .hint-text{font-size:var(--cf-hint-size, .75rem);color:var(--cf-text-secondary, #6B7280);margin-top:.375rem;line-height:1.25rem}.configurable-form-container .mt-2{margin-top:.5rem}.configurable-form-container .radio-group-container.is-invalid .field-label{color:var(--cf-error-color, #DC2626)}.configurable-form-container .radio-group{display:flex;gap:2rem;flex-wrap:wrap;margin-top:.5rem}.configurable-form-container .radio-button{margin:0}.configurable-form-container .composite-container{display:flex;align-items:center;gap:.75rem}.configurable-form-container .composite-container .separator{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .composite-container .input-wrapper{flex:1}.configurable-form-container .upload-box{border:2px dashed var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:2.5rem;text-align:center;cursor:pointer;transition:all .2s ease;background-color:var(--cf-hover-background, #F9FAFB)}.configurable-form-container .upload-box:hover{border-color:var(--cf-primary-color, #3B82F6);background-color:#3b82f608}.configurable-form-container .upload-box.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .upload-box .upload-icon{font-size:3rem;width:3rem;height:3rem;color:var(--cf-primary-color, #3B82F6);margin-bottom:.75rem}.configurable-form-container .upload-box .upload-text{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.375rem}.configurable-form-container .upload-box .upload-hint{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-files-list{margin-top:1rem}.configurable-form-container .uploaded-file-item{display:flex;align-items:center;gap:.75rem;padding:.875rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:8px;margin-bottom:.5rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .uploaded-file-item:hover{border-color:#9ca3af;box-shadow:0 1px 3px #0000001a}.configurable-form-container .uploaded-file-item .file-icon mat-icon{font-size:2rem;width:2rem;height:2rem}.configurable-form-container .uploaded-file-item .file-icon mat-icon.pdf-icon{color:#dc2626}.configurable-form-container .uploaded-file-item .file-icon mat-icon.doc-icon{color:#2563eb}.configurable-form-container .uploaded-file-item .file-icon mat-icon.img-icon{color:#059669}.configurable-form-container .uploaded-file-item .file-icon mat-icon.file-icon-default{color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .file-info{flex:1}.configurable-form-container .uploaded-file-item .file-info .file-name{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.125rem}.configurable-form-container .uploaded-file-item .file-info .file-size{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .remove-file-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.configurable-form-container .relative{position:relative}.configurable-form-container .date-toggle{position:absolute;right:0;top:50%;transform:translateY(-50%)}.configurable-form-container .repeater-item{padding:1.5rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:10px;margin-bottom:1rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .repeater-item:hover{border-color:#9ca3af;box-shadow:0 2px 4px #0000000d}.configurable-form-container .repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;padding-bottom:.75rem;border-bottom:1.5px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .repeater-item-header .text-small{font-size:.875rem;font-weight:600;color:var(--cf-text-primary, #111827);text-transform:uppercase;letter-spacing:.025em}.configurable-form-container .repeater-separator{height:1px;background-color:var(--cf-border-color, #D1D5DB);margin:1.5rem 0}.configurable-form-container .add-btn{margin-top:1rem;display:inline-flex;align-items:center;gap:.5rem;padding:.625rem 1.25rem;background-color:transparent;color:var(--cf-primary-color, #3B82F6);border:1.5px solid var(--cf-primary-color, #3B82F6);border-radius:8px;font-size:.875rem;font-weight:600;cursor:pointer;transition:all .2s ease}.configurable-form-container .add-btn:hover:not(:disabled){background-color:var(--cf-primary-color, #3B82F6);color:#fff}.configurable-form-container .add-btn:disabled{opacity:.5;cursor:not-allowed;border-color:var(--cf-disabled-background, #E5E7EB)}.configurable-form-container .add-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}@media(max-width:768px){.configurable-form-container .section-card{padding:1.25rem;border-radius:8px}.configurable-form-container .form-grid{margin-left:-.5rem;margin-right:-.5rem}.configurable-form-container .form-col{padding-left:.5rem;padding-right:.5rem;margin-bottom:1rem}.configurable-form-container .radio-group{flex-direction:column;gap:.75rem}.configurable-form-container .composite-container{flex-direction:column;align-items:stretch}.configurable-form-container .composite-container .separator{text-align:center}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i1$2.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "ngmodule", type: MaterialModule }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i6.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i6.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "component", type: i7.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i7.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i7.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "directive", type: i8.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i10.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i10.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }] });
|
|
3536
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: ConfigurableFormComponent, isStandalone: true, selector: "lib-configurable-form", inputs: { config: "config", jsonConfig: "jsonConfig", data: "data", baseApiUrl: "baseApiUrl", labels: "labels" }, outputs: { optionsLoad: "optionsLoad" }, usesOnChanges: true, ngImport: i0, template: "<form [formGroup]=\"form\" class=\"configurable-form-container\" *ngIf=\"form\">\r\n\r\n <ng-container *ngFor=\"let section of sections\">\r\n\r\n <!-- Repeater Section -->\r\n <div *ngIf=\"section.isRepeater; else normalSection\"\r\n [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div [formArrayName]=\"section.formArrayName || 'items'\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let item of getFormArray(section.formArrayName!).controls; let i = index\"\r\n [formGroupName]=\"i\" class=\"repeater-item\">\r\n <div class=\"repeater-item-header\" *ngIf=\"getFormArray(section.formArrayName!).length > 1\">\r\n <span class=\"text-small\">{{ section.repeaterItemLabel || labels.repeaterItemDefaultLabel }} {{i\r\n + 1}}</span>\r\n <button mat-icon-button color=\"warn\" type=\"button\"\r\n (click)=\"removeRepeaterItem(section.formArrayName!, i)\"\r\n [disabled]=\"section.minItems && getFormArray(section.formArrayName!).length <= section.minItems\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <!-- Fields Grid -->\r\n <div class=\"form-grid\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: item}\"></ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"repeater-separator\" *ngIf=\"i < getFormArray(section.formArrayName!).length - 1\"></div>\r\n </div>\r\n </div>\r\n\r\n <button mat-button color=\"primary\" type=\"button\" (click)=\"addRepeaterItem(section)\" class=\"add-btn\"\r\n *ngIf=\"!section.collapsed\"\r\n [disabled]=\"section.maxItems && getFormArray(section.formArrayName!).length >= section.maxItems\">\r\n <mat-icon>add_circle_outline</mat-icon> {{ section.addLabel || labels.addMoreDefaultLabel }}\r\n </button>\r\n </div>\r\n\r\n <!-- Normal Section -->\r\n <ng-template #normalSection>\r\n <div [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n <div class=\"form-grid\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <!-- Pass the root form group to the template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: form}\"></ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n </ng-container>\r\n\r\n</form>\r\n\r\n<!-- Reusable Field Template -->\r\n<ng-template #fieldTemplate let-field=\"field\" let-group=\"group\">\r\n <div [formGroup]=\"group\">\r\n\r\n <!-- Text / Email / Number / Tel / URL (Password removed) -->\r\n <div *ngIf=\"['text', 'email', 'number', 'tel', 'url'].includes(field.type || '')\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"field.type\" [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\"\r\n [readonly]=\"field.readonly\" class=\"form-input\" [name]=\"field.name\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} {{ labels.charactersLabel }}\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Password Field (Separated) -->\r\n <div *ngIf=\"field.type === 'password'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"isPasswordVisible(field.name) ? 'text' : 'password'\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n <mat-icon class=\"password-toggle-icon\" (click)=\"togglePassword(field.name)\">\r\n {{ isPasswordVisible(field.name) ? 'visibility' : 'visibility_off' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} {{ labels.charactersLabel }}\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Textarea -->\r\n <div *ngIf=\"field.type === 'textarea'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <textarea [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\"\r\n class=\"form-input form-textarea\" rows=\"4\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\"></textarea>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} {{ labels.charactersLabel }}\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Select / Dropdown -->\r\n <div *ngIf=\"field.type === 'select' || field.type === 'dropdown'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <select [formControlName]=\"field.name\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <option value=\"\" disabled selected *ngIf=\"field.placeholder\">{{ field.placeholder }}</option>\r\n <option value=\"\" disabled selected *ngIf=\"!field.placeholder\">{{ labels.selectDefaultPlaceholder }}\r\n </option>\r\n <option *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\">{{ opt.label }}</option>\r\n </select>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Date -->\r\n <div *ngIf=\"field.type === 'date'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"relative\">\r\n <input matInput [matDatepicker]=\"picker\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <mat-datepicker-toggle matSuffix [for]=\"picker\" class=\"date-toggle\"></mat-datepicker-toggle>\r\n <mat-datepicker #picker></mat-datepicker>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Radio -->\r\n <div *ngIf=\"field.type === 'radio'\" class=\"radio-group-container\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <label class=\"field-label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </label>\r\n <mat-radio-group [formControlName]=\"field.name\" class=\"radio-group\">\r\n <mat-radio-button *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\" class=\"radio-button\">\r\n {{ opt.label }}\r\n </mat-radio-button>\r\n </mat-radio-group>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Composite Field (Sub-groups) -->\r\n <div *ngIf=\"field.type === 'composite'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"composite-container\">\r\n <ng-container *ngFor=\"let sub of field.subFields; let last = last\">\r\n <div class=\"composite-sub-field\" style=\"flex: 1;\">\r\n <div class=\"field-label\" *ngIf=\"sub.label\"\r\n style=\"font-size: 0.75rem; font-weight: normal; margin-bottom: 0.25rem;\">\r\n {{ sub.label }}\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"sub.type\" [formControlName]=\"sub.name\" [placeholder]=\"sub.placeholder || ''\"\r\n [readonly]=\"sub.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(sub.name)?.touched && group.get(sub.name)?.invalid\">\r\n <div class=\"input-suffix\" [class.color-suffix]=\"sub.suffixText === '%'\">\r\n <span class=\"suffix-text\" *ngIf=\"sub.suffixText\">{{ sub.suffixText }}</span>\r\n <mat-icon *ngIf=\"sub.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- Adjust separator alignment if labels are present -->\r\n <span class=\"separator\" *ngIf=\"!last && field.separator\"\r\n [style.padding-top]=\"sub.label ? '1.25rem' : '0'\">{{ field.separator }}</span>\r\n </ng-container>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"error-text text-danger text-small mt-1\" *ngIf=\"group.errors?.['invalid_minMax_' + field.name]\">\r\n {{ labels.errorMinValue }}\r\n </div>\r\n <div class=\"error-text text-danger text-small mt-1\"\r\n *ngIf=\"group.errors?.['invalid_percentageTotal_' + field.name]\">\r\n {{ labels.errorPercentageTotal }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- File Upload -->\r\n <div *ngIf=\"field.type === 'file'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n\r\n <!-- Upload Box -->\r\n <div class=\"upload-box\" (click)=\"fileInput.click()\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n *ngIf=\"!field.uploadedFiles || field.uploadedFiles.length === 0 || field.multiple\">\r\n <mat-icon class=\"upload-icon\">cloud_upload</mat-icon>\r\n <div class=\"upload-text\">{{ labels.uploadDragDropText }}</div>\r\n <div class=\"upload-hint\">{{ labels.uploadHint }}</div>\r\n <input #fileInput type=\"file\" [attr.multiple]=\"field.multiple ? true : null\" [attr.accept]=\"field.accept\"\r\n (change)=\"onFileChange($event, field)\" style=\"display: none;\">\r\n </div>\r\n\r\n <!-- Uploaded Files List -->\r\n <div class=\"uploaded-files-list\" *ngIf=\"field.uploadedFiles && field.uploadedFiles.length > 0\">\r\n <div class=\"uploaded-file-item\" *ngFor=\"let file of field.uploadedFiles; let i = index\">\r\n <div class=\"file-icon\">\r\n <mat-icon class=\"pdf-icon\" *ngIf=\"file.type === 'application/pdf'\">picture_as_pdf</mat-icon>\r\n <mat-icon class=\"doc-icon\"\r\n *ngIf=\"file.type.includes('word') || file.type.includes('document')\">description</mat-icon>\r\n <mat-icon class=\"img-icon\" *ngIf=\"file.type.includes('image')\">image</mat-icon>\r\n <mat-icon class=\"file-icon-default\"\r\n *ngIf=\"!file.type.includes('pdf') && !file.type.includes('word') && !file.type.includes('document') && !file.type.includes('image')\">insert_drive_file</mat-icon>\r\n </div>\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ file.name }}</div>\r\n <div class=\"file-size\">{{ (file.size / 1024).toFixed(2) }} KB</div>\r\n </div>\r\n <button mat-icon-button color=\"warn\" type=\"button\" (click)=\"removeFile(field, i)\"\r\n class=\"remove-file-btn\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n</ng-template>", styles: [":host{display:block;width:100%}.configurable-form-container{padding:0;font-family:var(--cf-font-family, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif)}.configurable-form-container .section-card{background:var(--cf-surface-background, #ffffff);border:1px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:var(--cf-section-padding, 2rem);margin-bottom:var(--cf-section-spacing, 1.5rem);box-shadow:var(--cf-section-shadow, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06))}.configurable-form-container .section-card:last-of-type{margin-bottom:0}.configurable-form-container .section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--cf-section-spacing, 1.5rem);padding-bottom:1rem;border-bottom:2px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .section-title{font-size:var(--cf-section-title-size, 1.25rem);font-weight:var(--cf-section-title-weight, 600);color:var(--cf-text-primary, #111827);margin:0;letter-spacing:-.025em}.configurable-form-container .form-grid{display:flex;flex-wrap:wrap;margin-left:-.625rem;margin-right:-.625rem}.configurable-form-container .form-col{position:relative;width:100%;padding-left:.625rem;padding-right:.625rem;margin-bottom:1.25rem}.configurable-form-container .col-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-12{flex:0 0 100%;max-width:100%}@media(min-width:769px){.configurable-form-container .col-md-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-md-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-md-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-md-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-md-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-md-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-md-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-md-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-md-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-md-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-md-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-md-12{flex:0 0 100%;max-width:100%}}.configurable-form-container .field-container{margin-bottom:0}.configurable-form-container .field-label{display:block;font-size:var(--cf-label-size, .875rem);font-weight:var(--cf-label-weight, 600);color:var(--cf-text-primary, #111827);margin-bottom:.5rem;line-height:1.25rem}.configurable-form-container .field-label .text-danger{color:var(--cf-error-color, #DC2626);margin-left:.125rem}.configurable-form-container .input-wrapper{position:relative;display:flex;align-items:center}.configurable-form-container .form-input{width:100%;padding:var(--cf-input-padding-y, .625rem) var(--cf-input-padding-x, .875rem);font-size:var(--cf-input-font-size, .875rem);line-height:1.5;color:var(--cf-text-primary, #111827);background-color:var(--cf-input-bg, #ffffff);border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-input-radius, 8px);transition:all .2s ease;font-family:inherit}.configurable-form-container .form-input:hover:not(:disabled):not([readonly]){border-color:var(--cf-input-hover-border-color, #9CA3AF)}.configurable-form-container .form-input:focus{outline:none;border-color:var(--cf-primary-color, #3B82F6);box-shadow:0 0 0 3px #3b82f61a}.configurable-form-container .form-input::placeholder{color:#9ca3af}.configurable-form-container .form-input:disabled,.configurable-form-container .form-input[readonly]{background-color:var(--cf-disabled-background, #F3F4F6);color:var(--cf-text-secondary, #6B7280);cursor:not-allowed;border-color:#e5e7eb}.configurable-form-container .form-input.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .form-input.is-invalid:focus{box-shadow:0 0 0 3px #dc26261a}.configurable-form-container select.form-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}.configurable-form-container select.form-input:disabled{cursor:not-allowed}.configurable-form-container .form-textarea{resize:vertical;min-height:100px;font-family:inherit}.configurable-form-container .input-suffix{position:absolute;right:.75rem;display:flex;align-items:center;gap:.5rem;pointer-events:none}.configurable-form-container .input-suffix .password-toggle-icon{pointer-events:auto;cursor:pointer}.configurable-form-container .input-suffix .password-toggle-icon:hover{color:var(--cf-text-primary, #111827)}.configurable-form-container .input-suffix .suffix-text{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .input-suffix mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .input-suffix .lock-icon{color:#9ca3af}.configurable-form-container .input-suffix.color-suffix .suffix-text{color:var(--cf-primary-color, #3B82F6);font-weight:600}.configurable-form-container .char-count-row{display:flex;justify-content:space-between;align-items:center;margin-top:.5rem}.configurable-form-container .char-count{font-size:.75rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .hint-text{font-size:var(--cf-hint-size, .75rem);color:var(--cf-text-secondary, #6B7280);margin-top:.375rem;line-height:1.25rem}.configurable-form-container .mt-2{margin-top:.5rem}.configurable-form-container .radio-group-container.is-invalid .field-label{color:var(--cf-error-color, #DC2626)}.configurable-form-container .radio-group{display:flex;gap:2rem;flex-wrap:wrap;margin-top:.5rem}.configurable-form-container .radio-button{margin:0}.configurable-form-container .composite-container{display:flex;align-items:center;gap:.75rem}.configurable-form-container .composite-container .separator{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .composite-container .input-wrapper{flex:1}.configurable-form-container .upload-box{border:2px dashed var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:2.5rem;text-align:center;cursor:pointer;transition:all .2s ease;background-color:var(--cf-hover-background, #F9FAFB)}.configurable-form-container .upload-box:hover{border-color:var(--cf-primary-color, #3B82F6);background-color:#3b82f608}.configurable-form-container .upload-box.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .upload-box .upload-icon{font-size:3rem;width:3rem;height:3rem;color:var(--cf-primary-color, #3B82F6);margin-bottom:.75rem}.configurable-form-container .upload-box .upload-text{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.375rem}.configurable-form-container .upload-box .upload-hint{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-files-list{margin-top:1rem}.configurable-form-container .uploaded-file-item{display:flex;align-items:center;gap:.75rem;padding:.875rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:8px;margin-bottom:.5rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .uploaded-file-item:hover{border-color:#9ca3af;box-shadow:0 1px 3px #0000001a}.configurable-form-container .uploaded-file-item .file-icon mat-icon{font-size:2rem;width:2rem;height:2rem}.configurable-form-container .uploaded-file-item .file-icon mat-icon.pdf-icon{color:#dc2626}.configurable-form-container .uploaded-file-item .file-icon mat-icon.doc-icon{color:#2563eb}.configurable-form-container .uploaded-file-item .file-icon mat-icon.img-icon{color:#059669}.configurable-form-container .uploaded-file-item .file-icon mat-icon.file-icon-default{color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .file-info{flex:1}.configurable-form-container .uploaded-file-item .file-info .file-name{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.125rem}.configurable-form-container .uploaded-file-item .file-info .file-size{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .remove-file-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.configurable-form-container .relative{position:relative}.configurable-form-container .date-toggle{position:absolute;right:0;top:50%;transform:translateY(-50%)}.configurable-form-container .repeater-item{padding:1.5rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:10px;margin-bottom:1rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .repeater-item:hover{border-color:#9ca3af;box-shadow:0 2px 4px #0000000d}.configurable-form-container .repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;padding-bottom:.75rem;border-bottom:1.5px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .repeater-item-header .text-small{font-size:.875rem;font-weight:600;color:var(--cf-text-primary, #111827);text-transform:uppercase;letter-spacing:.025em}.configurable-form-container .repeater-separator{height:1px;background-color:var(--cf-border-color, #D1D5DB);margin:1.5rem 0}.configurable-form-container .add-btn{margin-top:1rem;display:inline-flex;align-items:center;gap:.5rem;padding:.625rem 1.25rem;background-color:transparent;color:var(--cf-primary-color, #3B82F6);border:1.5px solid var(--cf-primary-color, #3B82F6);border-radius:8px;font-size:.875rem;font-weight:600;cursor:pointer;transition:all .2s ease}.configurable-form-container .add-btn:hover:not(:disabled){background-color:var(--cf-primary-color, #3B82F6);color:#fff}.configurable-form-container .add-btn:disabled{opacity:.5;cursor:not-allowed;border-color:var(--cf-disabled-background, #E5E7EB)}.configurable-form-container .add-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}@media(max-width:768px){.configurable-form-container .section-card{padding:1.25rem;border-radius:8px}.configurable-form-container .form-grid{margin-left:-.5rem;margin-right:-.5rem}.configurable-form-container .form-col{padding-left:.5rem;padding-right:.5rem;margin-bottom:1rem}.configurable-form-container .radio-group{flex-direction:column;gap:.75rem}.configurable-form-container .composite-container{flex-direction:column;align-items:stretch}.configurable-form-container .composite-container .separator{text-align:center}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i1$2.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "ngmodule", type: MaterialModule }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i6.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i6.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "component", type: i7.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i7.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i7.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "directive", type: i8.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i10.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i10.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }] });
|
|
3457
3537
|
}
|
|
3458
3538
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConfigurableFormComponent, decorators: [{
|
|
3459
3539
|
type: Component,
|
|
3460
|
-
args: [{ selector: 'lib-configurable-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, MaterialModule], template: "<form [formGroup]=\"form\" class=\"configurable-form-container\" *ngIf=\"form\">\r\n\r\n <ng-container *ngFor=\"let section of sections\">\r\n\r\n <!-- Repeater Section -->\r\n <div *ngIf=\"section.isRepeater; else normalSection\"\r\n [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div [formArrayName]=\"section.formArrayName || 'items'\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let item of getFormArray(section.formArrayName!).controls; let i = index\"\r\n [formGroupName]=\"i\" class=\"repeater-item\">\r\n <div class=\"repeater-item-header\" *ngIf=\"getFormArray(section.formArrayName!).length > 1\">\r\n <span class=\"text-small\">{{ section.repeaterItemLabel || 'Item' }} {{i + 1}}</span>\r\n <button mat-icon-button color=\"warn\" type=\"button\"\r\n (click)=\"removeRepeaterItem(section.formArrayName!, i)\"\r\n [disabled]=\"section.minItems && getFormArray(section.formArrayName!).length <= section.minItems\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <!-- Fields Grid -->\r\n <div class=\"form-grid\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: item}\"></ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"repeater-separator\" *ngIf=\"i < getFormArray(section.formArrayName!).length - 1\"></div>\r\n </div>\r\n </div>\r\n\r\n <button mat-button color=\"primary\" type=\"button\" (click)=\"addRepeaterItem(section)\" class=\"add-btn\"\r\n *ngIf=\"!section.collapsed\"\r\n [disabled]=\"section.maxItems && getFormArray(section.formArrayName!).length >= section.maxItems\">\r\n <mat-icon>add_circle_outline</mat-icon> {{ section.addLabel || 'Add More' }}\r\n </button>\r\n </div>\r\n\r\n <!-- Normal Section -->\r\n <ng-template #normalSection>\r\n <div [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n <div class=\"form-grid\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <!-- Pass the root form group to the template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: form}\"></ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n </ng-container>\r\n\r\n</form>\r\n\r\n<!-- Reusable Field Template -->\r\n<ng-template #fieldTemplate let-field=\"field\" let-group=\"group\">\r\n <div [formGroup]=\"group\">\r\n\r\n <!-- Text / Email / Number / Tel / URL (Password removed) -->\r\n <div *ngIf=\"['text', 'email', 'number', 'tel', 'url'].includes(field.type || '')\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"field.type\" [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\"\r\n [readonly]=\"field.readonly\" class=\"form-input\" [name]=\"field.name\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} Characters\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Password Field (Separated) -->\r\n <div *ngIf=\"field.type === 'password'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"isPasswordVisible(field.name) ? 'text' : 'password'\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n <mat-icon class=\"password-toggle-icon\" (click)=\"togglePassword(field.name)\">\r\n {{ isPasswordVisible(field.name) ? 'visibility' : 'visibility_off' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} Characters\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Textarea -->\r\n <div *ngIf=\"field.type === 'textarea'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <textarea [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\"\r\n class=\"form-input form-textarea\" rows=\"4\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\"></textarea>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} Characters\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Select / Dropdown -->\r\n <div *ngIf=\"field.type === 'select' || field.type === 'dropdown'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <select [formControlName]=\"field.name\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <option value=\"\" disabled selected *ngIf=\"field.placeholder\">{{ field.placeholder }}</option>\r\n <option value=\"\" disabled selected *ngIf=\"!field.placeholder\">Select</option>\r\n <option *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\">{{ opt.label }}</option>\r\n </select>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Date -->\r\n <div *ngIf=\"field.type === 'date'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"relative\">\r\n <input matInput [matDatepicker]=\"picker\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <mat-datepicker-toggle matSuffix [for]=\"picker\" class=\"date-toggle\"></mat-datepicker-toggle>\r\n <mat-datepicker #picker></mat-datepicker>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Radio -->\r\n <div *ngIf=\"field.type === 'radio'\" class=\"radio-group-container\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <label class=\"field-label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </label>\r\n <mat-radio-group [formControlName]=\"field.name\" class=\"radio-group\">\r\n <mat-radio-button *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\" class=\"radio-button\">\r\n {{ opt.label }}\r\n </mat-radio-button>\r\n </mat-radio-group>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Composite Field (Sub-groups) -->\r\n <div *ngIf=\"field.type === 'composite'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"composite-container\">\r\n <ng-container *ngFor=\"let sub of field.subFields; let last = last\">\r\n <div class=\"composite-sub-field\" style=\"flex: 1;\">\r\n <div class=\"field-label\" *ngIf=\"sub.label\"\r\n style=\"font-size: 0.75rem; font-weight: normal; margin-bottom: 0.25rem;\">\r\n {{ sub.label }}\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"sub.type\" [formControlName]=\"sub.name\" [placeholder]=\"sub.placeholder || ''\"\r\n [readonly]=\"sub.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(sub.name)?.touched && group.get(sub.name)?.invalid\">\r\n <div class=\"input-suffix\" [class.color-suffix]=\"sub.suffixText === '%'\">\r\n <span class=\"suffix-text\" *ngIf=\"sub.suffixText\">{{ sub.suffixText }}</span>\r\n <mat-icon *ngIf=\"sub.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- Adjust separator alignment if labels are present -->\r\n <span class=\"separator\" *ngIf=\"!last && field.separator\"\r\n [style.padding-top]=\"sub.label ? '1.25rem' : '0'\">{{ field.separator }}</span>\r\n </ng-container>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"error-text text-danger text-small mt-1\" *ngIf=\"group.errors?.['invalid_minMax_' + field.name]\">\r\n Min value cannot be greater than Max value.\r\n </div>\r\n <div class=\"error-text text-danger text-small mt-1\"\r\n *ngIf=\"group.errors?.['invalid_percentageTotal_' + field.name]\">\r\n Total percentage must be 100%.\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- File Upload -->\r\n <div *ngIf=\"field.type === 'file'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n\r\n <!-- Upload Box -->\r\n <div class=\"upload-box\" (click)=\"fileInput.click()\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n *ngIf=\"!field.uploadedFiles || field.uploadedFiles.length === 0 || field.multiple\">\r\n <mat-icon class=\"upload-icon\">cloud_upload</mat-icon>\r\n <div class=\"upload-text\">Click to upload File</div>\r\n <div class=\"upload-hint\">PDF, DOCX, JPG, PNG (Max 5MB)</div>\r\n <input #fileInput type=\"file\" [attr.multiple]=\"field.multiple ? true : null\" [attr.accept]=\"field.accept\"\r\n (change)=\"onFileChange($event, field)\" style=\"display: none;\">\r\n </div>\r\n\r\n <!-- Uploaded Files List -->\r\n <div class=\"uploaded-files-list\" *ngIf=\"field.uploadedFiles && field.uploadedFiles.length > 0\">\r\n <div class=\"uploaded-file-item\" *ngFor=\"let file of field.uploadedFiles; let i = index\">\r\n <div class=\"file-icon\">\r\n <mat-icon class=\"pdf-icon\" *ngIf=\"file.type === 'application/pdf'\">picture_as_pdf</mat-icon>\r\n <mat-icon class=\"doc-icon\"\r\n *ngIf=\"file.type.includes('word') || file.type.includes('document')\">description</mat-icon>\r\n <mat-icon class=\"img-icon\" *ngIf=\"file.type.includes('image')\">image</mat-icon>\r\n <mat-icon class=\"file-icon-default\"\r\n *ngIf=\"!file.type.includes('pdf') && !file.type.includes('word') && !file.type.includes('document') && !file.type.includes('image')\">insert_drive_file</mat-icon>\r\n </div>\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ file.name }}</div>\r\n <div class=\"file-size\">{{ (file.size / 1024).toFixed(2) }} KB</div>\r\n </div>\r\n <button mat-icon-button color=\"warn\" type=\"button\" (click)=\"removeFile(field, i)\"\r\n class=\"remove-file-btn\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n</ng-template>", styles: [":host{display:block;width:100%}.configurable-form-container{padding:0;font-family:var(--cf-font-family, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif)}.configurable-form-container .section-card{background:var(--cf-surface-background, #ffffff);border:1px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:var(--cf-section-padding, 2rem);margin-bottom:var(--cf-section-spacing, 1.5rem);box-shadow:var(--cf-section-shadow, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06))}.configurable-form-container .section-card:last-of-type{margin-bottom:0}.configurable-form-container .section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--cf-section-spacing, 1.5rem);padding-bottom:1rem;border-bottom:2px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .section-title{font-size:var(--cf-section-title-size, 1.25rem);font-weight:var(--cf-section-title-weight, 600);color:var(--cf-text-primary, #111827);margin:0;letter-spacing:-.025em}.configurable-form-container .form-grid{display:flex;flex-wrap:wrap;margin-left:-.625rem;margin-right:-.625rem}.configurable-form-container .form-col{position:relative;width:100%;padding-left:.625rem;padding-right:.625rem;margin-bottom:1.25rem}.configurable-form-container .col-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-12{flex:0 0 100%;max-width:100%}@media(min-width:769px){.configurable-form-container .col-md-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-md-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-md-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-md-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-md-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-md-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-md-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-md-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-md-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-md-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-md-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-md-12{flex:0 0 100%;max-width:100%}}.configurable-form-container .field-container{margin-bottom:0}.configurable-form-container .field-label{display:block;font-size:var(--cf-label-size, .875rem);font-weight:var(--cf-label-weight, 600);color:var(--cf-text-primary, #111827);margin-bottom:.5rem;line-height:1.25rem}.configurable-form-container .field-label .text-danger{color:var(--cf-error-color, #DC2626);margin-left:.125rem}.configurable-form-container .input-wrapper{position:relative;display:flex;align-items:center}.configurable-form-container .form-input{width:100%;padding:var(--cf-input-padding-y, .625rem) var(--cf-input-padding-x, .875rem);font-size:var(--cf-input-font-size, .875rem);line-height:1.5;color:var(--cf-text-primary, #111827);background-color:var(--cf-input-bg, #ffffff);border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-input-radius, 8px);transition:all .2s ease;font-family:inherit}.configurable-form-container .form-input:hover:not(:disabled):not([readonly]){border-color:var(--cf-input-hover-border-color, #9CA3AF)}.configurable-form-container .form-input:focus{outline:none;border-color:var(--cf-primary-color, #3B82F6);box-shadow:0 0 0 3px #3b82f61a}.configurable-form-container .form-input::placeholder{color:#9ca3af}.configurable-form-container .form-input:disabled,.configurable-form-container .form-input[readonly]{background-color:var(--cf-disabled-background, #F3F4F6);color:var(--cf-text-secondary, #6B7280);cursor:not-allowed;border-color:#e5e7eb}.configurable-form-container .form-input.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .form-input.is-invalid:focus{box-shadow:0 0 0 3px #dc26261a}.configurable-form-container select.form-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}.configurable-form-container select.form-input:disabled{cursor:not-allowed}.configurable-form-container .form-textarea{resize:vertical;min-height:100px;font-family:inherit}.configurable-form-container .input-suffix{position:absolute;right:.75rem;display:flex;align-items:center;gap:.5rem;pointer-events:none}.configurable-form-container .input-suffix .password-toggle-icon{pointer-events:auto;cursor:pointer}.configurable-form-container .input-suffix .password-toggle-icon:hover{color:var(--cf-text-primary, #111827)}.configurable-form-container .input-suffix .suffix-text{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .input-suffix mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .input-suffix .lock-icon{color:#9ca3af}.configurable-form-container .input-suffix.color-suffix .suffix-text{color:var(--cf-primary-color, #3B82F6);font-weight:600}.configurable-form-container .char-count-row{display:flex;justify-content:space-between;align-items:center;margin-top:.5rem}.configurable-form-container .char-count{font-size:.75rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .hint-text{font-size:var(--cf-hint-size, .75rem);color:var(--cf-text-secondary, #6B7280);margin-top:.375rem;line-height:1.25rem}.configurable-form-container .mt-2{margin-top:.5rem}.configurable-form-container .radio-group-container.is-invalid .field-label{color:var(--cf-error-color, #DC2626)}.configurable-form-container .radio-group{display:flex;gap:2rem;flex-wrap:wrap;margin-top:.5rem}.configurable-form-container .radio-button{margin:0}.configurable-form-container .composite-container{display:flex;align-items:center;gap:.75rem}.configurable-form-container .composite-container .separator{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .composite-container .input-wrapper{flex:1}.configurable-form-container .upload-box{border:2px dashed var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:2.5rem;text-align:center;cursor:pointer;transition:all .2s ease;background-color:var(--cf-hover-background, #F9FAFB)}.configurable-form-container .upload-box:hover{border-color:var(--cf-primary-color, #3B82F6);background-color:#3b82f608}.configurable-form-container .upload-box.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .upload-box .upload-icon{font-size:3rem;width:3rem;height:3rem;color:var(--cf-primary-color, #3B82F6);margin-bottom:.75rem}.configurable-form-container .upload-box .upload-text{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.375rem}.configurable-form-container .upload-box .upload-hint{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-files-list{margin-top:1rem}.configurable-form-container .uploaded-file-item{display:flex;align-items:center;gap:.75rem;padding:.875rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:8px;margin-bottom:.5rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .uploaded-file-item:hover{border-color:#9ca3af;box-shadow:0 1px 3px #0000001a}.configurable-form-container .uploaded-file-item .file-icon mat-icon{font-size:2rem;width:2rem;height:2rem}.configurable-form-container .uploaded-file-item .file-icon mat-icon.pdf-icon{color:#dc2626}.configurable-form-container .uploaded-file-item .file-icon mat-icon.doc-icon{color:#2563eb}.configurable-form-container .uploaded-file-item .file-icon mat-icon.img-icon{color:#059669}.configurable-form-container .uploaded-file-item .file-icon mat-icon.file-icon-default{color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .file-info{flex:1}.configurable-form-container .uploaded-file-item .file-info .file-name{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.125rem}.configurable-form-container .uploaded-file-item .file-info .file-size{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .remove-file-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.configurable-form-container .relative{position:relative}.configurable-form-container .date-toggle{position:absolute;right:0;top:50%;transform:translateY(-50%)}.configurable-form-container .repeater-item{padding:1.5rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:10px;margin-bottom:1rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .repeater-item:hover{border-color:#9ca3af;box-shadow:0 2px 4px #0000000d}.configurable-form-container .repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;padding-bottom:.75rem;border-bottom:1.5px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .repeater-item-header .text-small{font-size:.875rem;font-weight:600;color:var(--cf-text-primary, #111827);text-transform:uppercase;letter-spacing:.025em}.configurable-form-container .repeater-separator{height:1px;background-color:var(--cf-border-color, #D1D5DB);margin:1.5rem 0}.configurable-form-container .add-btn{margin-top:1rem;display:inline-flex;align-items:center;gap:.5rem;padding:.625rem 1.25rem;background-color:transparent;color:var(--cf-primary-color, #3B82F6);border:1.5px solid var(--cf-primary-color, #3B82F6);border-radius:8px;font-size:.875rem;font-weight:600;cursor:pointer;transition:all .2s ease}.configurable-form-container .add-btn:hover:not(:disabled){background-color:var(--cf-primary-color, #3B82F6);color:#fff}.configurable-form-container .add-btn:disabled{opacity:.5;cursor:not-allowed;border-color:var(--cf-disabled-background, #E5E7EB)}.configurable-form-container .add-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}@media(max-width:768px){.configurable-form-container .section-card{padding:1.25rem;border-radius:8px}.configurable-form-container .form-grid{margin-left:-.5rem;margin-right:-.5rem}.configurable-form-container .form-col{padding-left:.5rem;padding-right:.5rem;margin-bottom:1rem}.configurable-form-container .radio-group{flex-direction:column;gap:.75rem}.configurable-form-container .composite-container{flex-direction:column;align-items:stretch}.configurable-form-container .composite-container .separator{text-align:center}}\n"] }]
|
|
3540
|
+
args: [{ selector: 'lib-configurable-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, MaterialModule], template: "<form [formGroup]=\"form\" class=\"configurable-form-container\" *ngIf=\"form\">\r\n\r\n <ng-container *ngFor=\"let section of sections\">\r\n\r\n <!-- Repeater Section -->\r\n <div *ngIf=\"section.isRepeater; else normalSection\"\r\n [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div [formArrayName]=\"section.formArrayName || 'items'\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let item of getFormArray(section.formArrayName!).controls; let i = index\"\r\n [formGroupName]=\"i\" class=\"repeater-item\">\r\n <div class=\"repeater-item-header\" *ngIf=\"getFormArray(section.formArrayName!).length > 1\">\r\n <span class=\"text-small\">{{ section.repeaterItemLabel || labels.repeaterItemDefaultLabel }} {{i\r\n + 1}}</span>\r\n <button mat-icon-button color=\"warn\" type=\"button\"\r\n (click)=\"removeRepeaterItem(section.formArrayName!, i)\"\r\n [disabled]=\"section.minItems && getFormArray(section.formArrayName!).length <= section.minItems\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <!-- Fields Grid -->\r\n <div class=\"form-grid\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: item}\"></ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"repeater-separator\" *ngIf=\"i < getFormArray(section.formArrayName!).length - 1\"></div>\r\n </div>\r\n </div>\r\n\r\n <button mat-button color=\"primary\" type=\"button\" (click)=\"addRepeaterItem(section)\" class=\"add-btn\"\r\n *ngIf=\"!section.collapsed\"\r\n [disabled]=\"section.maxItems && getFormArray(section.formArrayName!).length >= section.maxItems\">\r\n <mat-icon>add_circle_outline</mat-icon> {{ section.addLabel || labels.addMoreDefaultLabel }}\r\n </button>\r\n </div>\r\n\r\n <!-- Normal Section -->\r\n <ng-template #normalSection>\r\n <div [ngClass]=\"section.noCardLayout ? 'section-no-card' : 'section-card'\">\r\n <div class=\"section-header\" *ngIf=\"section.sectionTitle\">\r\n <h3 class=\"section-title\">{{ section.sectionTitle }}</h3>\r\n <button *ngIf=\"section.collapsible\" mat-icon-button type=\"button\" (click)=\"toggleSection(section)\">\r\n <mat-icon>{{ section.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\r\n </button>\r\n </div>\r\n <div class=\"form-grid\" *ngIf=\"!section.collapsed\">\r\n <div *ngFor=\"let field of section.fields\" [ngClass]=\"['form-col', field.class || 'col-12']\"\r\n [hidden]=\"!isFieldVisible(field)\">\r\n <!-- Pass the root form group to the template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"fieldTemplate; context: {field: field, group: form}\"></ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n </ng-container>\r\n\r\n</form>\r\n\r\n<!-- Reusable Field Template -->\r\n<ng-template #fieldTemplate let-field=\"field\" let-group=\"group\">\r\n <div [formGroup]=\"group\">\r\n\r\n <!-- Text / Email / Number / Tel / URL (Password removed) -->\r\n <div *ngIf=\"['text', 'email', 'number', 'tel', 'url'].includes(field.type || '')\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"field.type\" [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\"\r\n [readonly]=\"field.readonly\" class=\"form-input\" [name]=\"field.name\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} {{ labels.charactersLabel }}\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Password Field (Separated) -->\r\n <div *ngIf=\"field.type === 'password'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"isPasswordVisible(field.name) ? 'text' : 'password'\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\">\r\n <div class=\"input-suffix\">\r\n <span class=\"suffix-text\" *ngIf=\"field.suffixText\">{{ field.suffixText }}</span>\r\n <mat-icon *ngIf=\"field.suffixIcon\">{{ field.suffixIcon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.icon\">{{ field.icon }}</mat-icon>\r\n <mat-icon *ngIf=\"field.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n <mat-icon class=\"password-toggle-icon\" (click)=\"togglePassword(field.name)\">\r\n {{ isPasswordVisible(field.name) ? 'visibility' : 'visibility_off' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} {{ labels.charactersLabel }}\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Textarea -->\r\n <div *ngIf=\"field.type === 'textarea'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <textarea [formControlName]=\"field.name\" [placeholder]=\"field.placeholder || ''\" [readonly]=\"field.readonly\"\r\n class=\"form-input form-textarea\" rows=\"4\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n [maxlength]=\"field.validationRules?.maxLength || field.uiConfig?.maxCharacters\"></textarea>\r\n <!-- Character count -->\r\n <div class=\"char-count-row\" *ngIf=\"field.uiConfig?.maxCharacters || field.validationRules?.maxLength\">\r\n <div class=\"hint-text\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"char-count\">\r\n {{ getCharacterCount(field.name) }} / {{ field.uiConfig?.maxCharacters ||\r\n field.validationRules?.maxLength }} {{ labels.charactersLabel }}\r\n </div>\r\n </div>\r\n <div class=\"hint-text mt-2\"\r\n *ngIf=\"(field.hint || field.helpText) && !field.uiConfig?.maxCharacters && !field.validationRules?.maxLength\">\r\n {{ field.hint || field.helpText }}\r\n </div>\r\n </div>\r\n\r\n <!-- Select / Dropdown -->\r\n <div *ngIf=\"field.type === 'select' || field.type === 'dropdown'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <select [formControlName]=\"field.name\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <option value=\"\" disabled selected *ngIf=\"field.placeholder\">{{ field.placeholder }}</option>\r\n <option value=\"\" disabled selected *ngIf=\"!field.placeholder\">{{ labels.selectDefaultPlaceholder }}\r\n </option>\r\n <option *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\">{{ opt.label }}</option>\r\n </select>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Date -->\r\n <div *ngIf=\"field.type === 'date'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"relative\">\r\n <input matInput [matDatepicker]=\"picker\" [formControlName]=\"field.name\"\r\n [placeholder]=\"field.placeholder || ''\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <mat-datepicker-toggle matSuffix [for]=\"picker\" class=\"date-toggle\"></mat-datepicker-toggle>\r\n <mat-datepicker #picker></mat-datepicker>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Radio -->\r\n <div *ngIf=\"field.type === 'radio'\" class=\"radio-group-container\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\">\r\n <label class=\"field-label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </label>\r\n <mat-radio-group [formControlName]=\"field.name\" class=\"radio-group\">\r\n <mat-radio-button *ngFor=\"let opt of getFieldOptions(field)\" [value]=\"opt.value\" class=\"radio-button\">\r\n {{ opt.label }}\r\n </mat-radio-button>\r\n </mat-radio-group>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n </div>\r\n\r\n <!-- Composite Field (Sub-groups) -->\r\n <div *ngIf=\"field.type === 'composite'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n <div class=\"composite-container\">\r\n <ng-container *ngFor=\"let sub of field.subFields; let last = last\">\r\n <div class=\"composite-sub-field\" style=\"flex: 1;\">\r\n <div class=\"field-label\" *ngIf=\"sub.label\"\r\n style=\"font-size: 0.75rem; font-weight: normal; margin-bottom: 0.25rem;\">\r\n {{ sub.label }}\r\n </div>\r\n <div class=\"input-wrapper\">\r\n <input [type]=\"sub.type\" [formControlName]=\"sub.name\" [placeholder]=\"sub.placeholder || ''\"\r\n [readonly]=\"sub.readonly\" class=\"form-input\"\r\n [class.is-invalid]=\"group.get(sub.name)?.touched && group.get(sub.name)?.invalid\">\r\n <div class=\"input-suffix\" [class.color-suffix]=\"sub.suffixText === '%'\">\r\n <span class=\"suffix-text\" *ngIf=\"sub.suffixText\">{{ sub.suffixText }}</span>\r\n <mat-icon *ngIf=\"sub.readonly\" class=\"lock-icon\">lock_outline</mat-icon>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- Adjust separator alignment if labels are present -->\r\n <span class=\"separator\" *ngIf=\"!last && field.separator\"\r\n [style.padding-top]=\"sub.label ? '1.25rem' : '0'\">{{ field.separator }}</span>\r\n </ng-container>\r\n </div>\r\n <div class=\"hint-text mt-2\" *ngIf=\"field.hint || field.helpText\">{{ field.hint || field.helpText }}</div>\r\n <div class=\"error-text text-danger text-small mt-1\" *ngIf=\"group.errors?.['invalid_minMax_' + field.name]\">\r\n {{ labels.errorMinValue }}\r\n </div>\r\n <div class=\"error-text text-danger text-small mt-1\"\r\n *ngIf=\"group.errors?.['invalid_percentageTotal_' + field.name]\">\r\n {{ labels.errorPercentageTotal }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- File Upload -->\r\n <div *ngIf=\"field.type === 'file'\" class=\"field-container\">\r\n <div class=\"field-label\" *ngIf=\"field.label\">\r\n {{ field.label }}\r\n <span class=\"text-danger\" *ngIf=\"field.required || field.mandatory\">*</span>\r\n </div>\r\n\r\n <!-- Upload Box -->\r\n <div class=\"upload-box\" (click)=\"fileInput.click()\"\r\n [class.is-invalid]=\"group.get(field.name)?.touched && group.get(field.name)?.invalid\"\r\n *ngIf=\"!field.uploadedFiles || field.uploadedFiles.length === 0 || field.multiple\">\r\n <mat-icon class=\"upload-icon\">cloud_upload</mat-icon>\r\n <div class=\"upload-text\">{{ labels.uploadDragDropText }}</div>\r\n <div class=\"upload-hint\">{{ labels.uploadHint }}</div>\r\n <input #fileInput type=\"file\" [attr.multiple]=\"field.multiple ? true : null\" [attr.accept]=\"field.accept\"\r\n (change)=\"onFileChange($event, field)\" style=\"display: none;\">\r\n </div>\r\n\r\n <!-- Uploaded Files List -->\r\n <div class=\"uploaded-files-list\" *ngIf=\"field.uploadedFiles && field.uploadedFiles.length > 0\">\r\n <div class=\"uploaded-file-item\" *ngFor=\"let file of field.uploadedFiles; let i = index\">\r\n <div class=\"file-icon\">\r\n <mat-icon class=\"pdf-icon\" *ngIf=\"file.type === 'application/pdf'\">picture_as_pdf</mat-icon>\r\n <mat-icon class=\"doc-icon\"\r\n *ngIf=\"file.type.includes('word') || file.type.includes('document')\">description</mat-icon>\r\n <mat-icon class=\"img-icon\" *ngIf=\"file.type.includes('image')\">image</mat-icon>\r\n <mat-icon class=\"file-icon-default\"\r\n *ngIf=\"!file.type.includes('pdf') && !file.type.includes('word') && !file.type.includes('document') && !file.type.includes('image')\">insert_drive_file</mat-icon>\r\n </div>\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ file.name }}</div>\r\n <div class=\"file-size\">{{ (file.size / 1024).toFixed(2) }} KB</div>\r\n </div>\r\n <button mat-icon-button color=\"warn\" type=\"button\" (click)=\"removeFile(field, i)\"\r\n class=\"remove-file-btn\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n</ng-template>", styles: [":host{display:block;width:100%}.configurable-form-container{padding:0;font-family:var(--cf-font-family, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif)}.configurable-form-container .section-card{background:var(--cf-surface-background, #ffffff);border:1px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:var(--cf-section-padding, 2rem);margin-bottom:var(--cf-section-spacing, 1.5rem);box-shadow:var(--cf-section-shadow, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06))}.configurable-form-container .section-card:last-of-type{margin-bottom:0}.configurable-form-container .section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--cf-section-spacing, 1.5rem);padding-bottom:1rem;border-bottom:2px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .section-title{font-size:var(--cf-section-title-size, 1.25rem);font-weight:var(--cf-section-title-weight, 600);color:var(--cf-text-primary, #111827);margin:0;letter-spacing:-.025em}.configurable-form-container .form-grid{display:flex;flex-wrap:wrap;margin-left:-.625rem;margin-right:-.625rem}.configurable-form-container .form-col{position:relative;width:100%;padding-left:.625rem;padding-right:.625rem;margin-bottom:1.25rem}.configurable-form-container .col-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-12{flex:0 0 100%;max-width:100%}@media(min-width:769px){.configurable-form-container .col-md-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.configurable-form-container .col-md-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.configurable-form-container .col-md-3{flex:0 0 25%;max-width:25%}.configurable-form-container .col-md-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.configurable-form-container .col-md-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.configurable-form-container .col-md-6{flex:0 0 50%;max-width:50%}.configurable-form-container .col-md-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.configurable-form-container .col-md-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.configurable-form-container .col-md-9{flex:0 0 75%;max-width:75%}.configurable-form-container .col-md-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.configurable-form-container .col-md-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.configurable-form-container .col-md-12{flex:0 0 100%;max-width:100%}}.configurable-form-container .field-container{margin-bottom:0}.configurable-form-container .field-label{display:block;font-size:var(--cf-label-size, .875rem);font-weight:var(--cf-label-weight, 600);color:var(--cf-text-primary, #111827);margin-bottom:.5rem;line-height:1.25rem}.configurable-form-container .field-label .text-danger{color:var(--cf-error-color, #DC2626);margin-left:.125rem}.configurable-form-container .input-wrapper{position:relative;display:flex;align-items:center}.configurable-form-container .form-input{width:100%;padding:var(--cf-input-padding-y, .625rem) var(--cf-input-padding-x, .875rem);font-size:var(--cf-input-font-size, .875rem);line-height:1.5;color:var(--cf-text-primary, #111827);background-color:var(--cf-input-bg, #ffffff);border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:var(--cf-input-radius, 8px);transition:all .2s ease;font-family:inherit}.configurable-form-container .form-input:hover:not(:disabled):not([readonly]){border-color:var(--cf-input-hover-border-color, #9CA3AF)}.configurable-form-container .form-input:focus{outline:none;border-color:var(--cf-primary-color, #3B82F6);box-shadow:0 0 0 3px #3b82f61a}.configurable-form-container .form-input::placeholder{color:#9ca3af}.configurable-form-container .form-input:disabled,.configurable-form-container .form-input[readonly]{background-color:var(--cf-disabled-background, #F3F4F6);color:var(--cf-text-secondary, #6B7280);cursor:not-allowed;border-color:#e5e7eb}.configurable-form-container .form-input.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .form-input.is-invalid:focus{box-shadow:0 0 0 3px #dc26261a}.configurable-form-container select.form-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}.configurable-form-container select.form-input:disabled{cursor:not-allowed}.configurable-form-container .form-textarea{resize:vertical;min-height:100px;font-family:inherit}.configurable-form-container .input-suffix{position:absolute;right:.75rem;display:flex;align-items:center;gap:.5rem;pointer-events:none}.configurable-form-container .input-suffix .password-toggle-icon{pointer-events:auto;cursor:pointer}.configurable-form-container .input-suffix .password-toggle-icon:hover{color:var(--cf-text-primary, #111827)}.configurable-form-container .input-suffix .suffix-text{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .input-suffix mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .input-suffix .lock-icon{color:#9ca3af}.configurable-form-container .input-suffix.color-suffix .suffix-text{color:var(--cf-primary-color, #3B82F6);font-weight:600}.configurable-form-container .char-count-row{display:flex;justify-content:space-between;align-items:center;margin-top:.5rem}.configurable-form-container .char-count{font-size:.75rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .hint-text{font-size:var(--cf-hint-size, .75rem);color:var(--cf-text-secondary, #6B7280);margin-top:.375rem;line-height:1.25rem}.configurable-form-container .mt-2{margin-top:.5rem}.configurable-form-container .radio-group-container.is-invalid .field-label{color:var(--cf-error-color, #DC2626)}.configurable-form-container .radio-group{display:flex;gap:2rem;flex-wrap:wrap;margin-top:.5rem}.configurable-form-container .radio-button{margin:0}.configurable-form-container .composite-container{display:flex;align-items:center;gap:.75rem}.configurable-form-container .composite-container .separator{font-size:.875rem;color:var(--cf-text-secondary, #6B7280);font-weight:500}.configurable-form-container .composite-container .input-wrapper{flex:1}.configurable-form-container .upload-box{border:2px dashed var(--cf-border-color, #D1D5DB);border-radius:var(--cf-section-radius, 12px);padding:2.5rem;text-align:center;cursor:pointer;transition:all .2s ease;background-color:var(--cf-hover-background, #F9FAFB)}.configurable-form-container .upload-box:hover{border-color:var(--cf-primary-color, #3B82F6);background-color:#3b82f608}.configurable-form-container .upload-box.is-invalid{border-color:var(--cf-error-color, #DC2626);background-color:#fef2f2}.configurable-form-container .upload-box .upload-icon{font-size:3rem;width:3rem;height:3rem;color:var(--cf-primary-color, #3B82F6);margin-bottom:.75rem}.configurable-form-container .upload-box .upload-text{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.375rem}.configurable-form-container .upload-box .upload-hint{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-files-list{margin-top:1rem}.configurable-form-container .uploaded-file-item{display:flex;align-items:center;gap:.75rem;padding:.875rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:8px;margin-bottom:.5rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .uploaded-file-item:hover{border-color:#9ca3af;box-shadow:0 1px 3px #0000001a}.configurable-form-container .uploaded-file-item .file-icon mat-icon{font-size:2rem;width:2rem;height:2rem}.configurable-form-container .uploaded-file-item .file-icon mat-icon.pdf-icon{color:#dc2626}.configurable-form-container .uploaded-file-item .file-icon mat-icon.doc-icon{color:#2563eb}.configurable-form-container .uploaded-file-item .file-icon mat-icon.img-icon{color:#059669}.configurable-form-container .uploaded-file-item .file-icon mat-icon.file-icon-default{color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .file-info{flex:1}.configurable-form-container .uploaded-file-item .file-info .file-name{font-size:.875rem;color:var(--cf-text-primary, #111827);font-weight:500;margin-bottom:.125rem}.configurable-form-container .uploaded-file-item .file-info .file-size{font-size:.75rem;color:var(--cf-text-secondary, #6B7280)}.configurable-form-container .uploaded-file-item .remove-file-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.configurable-form-container .relative{position:relative}.configurable-form-container .date-toggle{position:absolute;right:0;top:50%;transform:translateY(-50%)}.configurable-form-container .repeater-item{padding:1.5rem;border:1.5px solid var(--cf-border-color, #D1D5DB);border-radius:10px;margin-bottom:1rem;background-color:var(--cf-hover-background, #F9FAFB);transition:all .2s ease}.configurable-form-container .repeater-item:hover{border-color:#9ca3af;box-shadow:0 2px 4px #0000000d}.configurable-form-container .repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;padding-bottom:.75rem;border-bottom:1.5px solid var(--cf-border-color, #D1D5DB)}.configurable-form-container .repeater-item-header .text-small{font-size:.875rem;font-weight:600;color:var(--cf-text-primary, #111827);text-transform:uppercase;letter-spacing:.025em}.configurable-form-container .repeater-separator{height:1px;background-color:var(--cf-border-color, #D1D5DB);margin:1.5rem 0}.configurable-form-container .add-btn{margin-top:1rem;display:inline-flex;align-items:center;gap:.5rem;padding:.625rem 1.25rem;background-color:transparent;color:var(--cf-primary-color, #3B82F6);border:1.5px solid var(--cf-primary-color, #3B82F6);border-radius:8px;font-size:.875rem;font-weight:600;cursor:pointer;transition:all .2s ease}.configurable-form-container .add-btn:hover:not(:disabled){background-color:var(--cf-primary-color, #3B82F6);color:#fff}.configurable-form-container .add-btn:disabled{opacity:.5;cursor:not-allowed;border-color:var(--cf-disabled-background, #E5E7EB)}.configurable-form-container .add-btn mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}@media(max-width:768px){.configurable-form-container .section-card{padding:1.25rem;border-radius:8px}.configurable-form-container .form-grid{margin-left:-.5rem;margin-right:-.5rem}.configurable-form-container .form-col{padding-left:.5rem;padding-right:.5rem;margin-bottom:1rem}.configurable-form-container .radio-group{flex-direction:column;gap:.75rem}.configurable-form-container .composite-container{flex-direction:column;align-items:stretch}.configurable-form-container .composite-container .separator{text-align:center}}\n"] }]
|
|
3461
3541
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: i2$2.MatSnackBar }, { type: i3.HttpClient }], propDecorators: { config: [{
|
|
3462
3542
|
type: Input
|
|
3463
3543
|
}], jsonConfig: [{
|
|
@@ -3466,6 +3546,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
3466
3546
|
type: Input
|
|
3467
3547
|
}], baseApiUrl: [{
|
|
3468
3548
|
type: Input
|
|
3549
|
+
}], labels: [{
|
|
3550
|
+
type: Input
|
|
3469
3551
|
}], optionsLoad: [{
|
|
3470
3552
|
type: Output
|
|
3471
3553
|
}] } });
|
|
@@ -3499,6 +3581,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
3499
3581
|
|
|
3500
3582
|
class SmartFormController {
|
|
3501
3583
|
formData = {};
|
|
3584
|
+
/** Auth token sourced from the FormSchema configJSON (e.g. "Bearer eyJ…") */
|
|
3585
|
+
token;
|
|
3586
|
+
/** HTTP header name for the token (default: "Authorization") */
|
|
3587
|
+
tokenHeader;
|
|
3588
|
+
/** Flat map of translated i18n labels passed from SmartFormComponent */
|
|
3589
|
+
labels = {};
|
|
3590
|
+
/** Custom label keys for form actions (Next, Submit, Add, etc.) */
|
|
3591
|
+
actionLabels;
|
|
3502
3592
|
fieldSubjects = new Map();
|
|
3503
3593
|
initialize(initialData) {
|
|
3504
3594
|
this.formData = { ...initialData };
|
|
@@ -3542,38 +3632,112 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
3542
3632
|
type: Injectable
|
|
3543
3633
|
}] });
|
|
3544
3634
|
|
|
3635
|
+
/**
|
|
3636
|
+
* Utility class for translating Smart Form schemas.
|
|
3637
|
+
*/
|
|
3638
|
+
class SmartFormTranslationUtils {
|
|
3639
|
+
/**
|
|
3640
|
+
* Recursively walks the schema and replaces i18n keys with values from labels map.
|
|
3641
|
+
* @param schema The FormSchema to translate.
|
|
3642
|
+
* @param labels The labels map (can be flat or contain labelsObject property).
|
|
3643
|
+
*/
|
|
3644
|
+
static translateSchema(schema, labels) {
|
|
3645
|
+
if (!schema || !labels)
|
|
3646
|
+
return;
|
|
3647
|
+
// Support both flat labels map and the pattern used in ConfigurableForm which passes { labelsObject: ... }
|
|
3648
|
+
const labelsMap = labels.labelsObject || labels;
|
|
3649
|
+
const translate = (key) => labelsMap[key] || key;
|
|
3650
|
+
// Root properties
|
|
3651
|
+
if (schema.label)
|
|
3652
|
+
schema.label = translate(schema.label);
|
|
3653
|
+
if (schema.description)
|
|
3654
|
+
schema.description = translate(schema.description);
|
|
3655
|
+
// Submit config
|
|
3656
|
+
if (schema.submitConfig) {
|
|
3657
|
+
if (schema.submitConfig.successMessage)
|
|
3658
|
+
schema.submitConfig.successMessage = translate(schema.submitConfig.successMessage);
|
|
3659
|
+
if (schema.submitConfig.errorMessage)
|
|
3660
|
+
schema.submitConfig.errorMessage = translate(schema.submitConfig.errorMessage);
|
|
3661
|
+
}
|
|
3662
|
+
// Section config
|
|
3663
|
+
if (schema.sectionConfig) {
|
|
3664
|
+
this.translateSection(schema.sectionConfig, translate);
|
|
3665
|
+
}
|
|
3666
|
+
// Stepper config
|
|
3667
|
+
if (schema.stepperConfig?.children) {
|
|
3668
|
+
schema.stepperConfig.children.forEach(field => this.translateField(field, translate));
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
static translateSection(section, translate) {
|
|
3672
|
+
if (section.label)
|
|
3673
|
+
section.label = translate(section.label);
|
|
3674
|
+
if (section.children) {
|
|
3675
|
+
section.children.forEach((field) => this.translateField(field, translate));
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
static translateField(field, translate) {
|
|
3679
|
+
if (field.label)
|
|
3680
|
+
field.label = translate(field.label);
|
|
3681
|
+
if (field.placeholder)
|
|
3682
|
+
field.placeholder = translate(field.placeholder);
|
|
3683
|
+
if (field.hint)
|
|
3684
|
+
field.hint = translate(field.hint);
|
|
3685
|
+
if (field.textConfig?.patternMessage) {
|
|
3686
|
+
field.textConfig.patternMessage = translate(field.textConfig.patternMessage);
|
|
3687
|
+
}
|
|
3688
|
+
if (field.attachmentConfig?.acceptLabel) {
|
|
3689
|
+
field.attachmentConfig.acceptLabel = translate(field.attachmentConfig.acceptLabel);
|
|
3690
|
+
}
|
|
3691
|
+
if (field.optionConfig?.optionList) {
|
|
3692
|
+
field.optionConfig.optionList.forEach(opt => {
|
|
3693
|
+
if (opt.label)
|
|
3694
|
+
opt.label = translate(opt.label);
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
// Recurse into children (ROW or GROUP)
|
|
3698
|
+
if (field.children) {
|
|
3699
|
+
field.children.forEach(child => this.translateField(child, translate));
|
|
3700
|
+
}
|
|
3701
|
+
// Recurse into nested section config
|
|
3702
|
+
if (field.sectionConfig) {
|
|
3703
|
+
this.translateSection(field.sectionConfig, translate);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3545
3708
|
class ExpressionService {
|
|
3546
3709
|
loadedFunctions = new Map();
|
|
3547
3710
|
evaluate(expression, context, variables) {
|
|
3548
3711
|
try {
|
|
3549
|
-
const
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
return func(...Object.values(filteredContext));
|
|
3712
|
+
const vars = variables || this.extractVariables(expression);
|
|
3713
|
+
const args = vars.map(v => context[v]);
|
|
3714
|
+
const func = new Function(...vars, `return ${expression};`);
|
|
3715
|
+
return func(...args);
|
|
3554
3716
|
}
|
|
3555
3717
|
catch (e) {
|
|
3556
|
-
console.error('Expression evaluation error:', e);
|
|
3718
|
+
console.error('Expression evaluation error:', e, { expression, vars: variables || this.extractVariables(expression), context });
|
|
3557
3719
|
return null;
|
|
3558
3720
|
}
|
|
3559
3721
|
}
|
|
3560
3722
|
evaluateCondition(expression, context) {
|
|
3723
|
+
const vars = this.extractVariables(expression);
|
|
3561
3724
|
try {
|
|
3562
|
-
const
|
|
3563
|
-
const
|
|
3725
|
+
const args = vars.map(v => context[v]);
|
|
3726
|
+
const func = new Function(...vars, `return ${expression};`);
|
|
3727
|
+
const result = func(...args);
|
|
3564
3728
|
return !!result;
|
|
3565
3729
|
}
|
|
3566
3730
|
catch (e) {
|
|
3567
|
-
console.error('Condition evaluation error:', e);
|
|
3731
|
+
console.error('Condition evaluation error:', e, { expression, variables: vars, context });
|
|
3568
3732
|
return false;
|
|
3569
3733
|
}
|
|
3570
3734
|
}
|
|
3571
3735
|
evaluateFormula(formula, functionName, context, variables) {
|
|
3572
3736
|
try {
|
|
3573
3737
|
if (!this.loadedFunctions.has(functionName)) {
|
|
3574
|
-
const func = new Function('context', `
|
|
3575
|
-
${formula}
|
|
3576
|
-
return ${functionName};
|
|
3738
|
+
const func = new Function('context', `
|
|
3739
|
+
${formula}
|
|
3740
|
+
return ${functionName};
|
|
3577
3741
|
`)(context);
|
|
3578
3742
|
this.loadedFunctions.set(functionName, func);
|
|
3579
3743
|
}
|
|
@@ -3589,9 +3753,21 @@ class ExpressionService {
|
|
|
3589
3753
|
}
|
|
3590
3754
|
}
|
|
3591
3755
|
extractVariables(expression) {
|
|
3756
|
+
if (!expression)
|
|
3757
|
+
return [];
|
|
3758
|
+
// Remove strings from expression to avoid picking up variables inside quotes
|
|
3759
|
+
const cleanExpr = expression
|
|
3760
|
+
.replace(/'[^']*'/g, '')
|
|
3761
|
+
.replace(/"[^"]*"/g, '')
|
|
3762
|
+
.replace(/`[^`]*`/g, '')
|
|
3763
|
+
// Remove properties (e.g., .max) to only pick up top-level identifiers
|
|
3764
|
+
.replace(/\.\w+/g, '');
|
|
3592
3765
|
const regex = /\b[a-zA-Z_]\w*\b/g;
|
|
3593
|
-
const matches =
|
|
3594
|
-
const keywords = [
|
|
3766
|
+
const matches = cleanExpr.match(regex) || [];
|
|
3767
|
+
const keywords = [
|
|
3768
|
+
'true', 'false', 'null', 'undefined', 'return', 'if', 'else', 'for', 'while', 'function', 'var', 'let', 'const', 'this',
|
|
3769
|
+
'Math', 'JSON', 'console', 'window', 'document', 'Date', 'Object', 'Array', 'Number', 'String', 'Boolean', 'Symbol'
|
|
3770
|
+
];
|
|
3595
3771
|
return [...new Set(matches.filter(m => !keywords.includes(m)))];
|
|
3596
3772
|
}
|
|
3597
3773
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ExpressionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -3604,174 +3780,303 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
3604
3780
|
}]
|
|
3605
3781
|
}] });
|
|
3606
3782
|
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
return (control) => {
|
|
3618
|
-
if (!control.value)
|
|
3619
|
-
return null;
|
|
3620
|
-
const phoneRegex = /^\+?[\d\s\-\(\)]{10,}$/;
|
|
3621
|
-
return phoneRegex.test(control.value) ? null : { phone: true };
|
|
3622
|
-
};
|
|
3623
|
-
}
|
|
3624
|
-
static url() {
|
|
3625
|
-
return (control) => {
|
|
3626
|
-
if (!control.value)
|
|
3627
|
-
return null;
|
|
3628
|
-
const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
|
|
3629
|
-
return urlRegex.test(control.value) ? null : { url: true };
|
|
3630
|
-
};
|
|
3631
|
-
}
|
|
3632
|
-
static minLength(min) {
|
|
3633
|
-
return (control) => {
|
|
3634
|
-
if (!control.value)
|
|
3635
|
-
return null;
|
|
3636
|
-
return control.value.length >= min ? null : { minLength: { min, actual: control.value.length } };
|
|
3637
|
-
};
|
|
3638
|
-
}
|
|
3639
|
-
static maxLength(max) {
|
|
3640
|
-
return (control) => {
|
|
3641
|
-
if (!control.value)
|
|
3642
|
-
return null;
|
|
3643
|
-
return control.value.length <= max ? null : { maxLength: { max, actual: control.value.length } };
|
|
3644
|
-
};
|
|
3645
|
-
}
|
|
3646
|
-
static pattern(pattern, message) {
|
|
3647
|
-
return (control) => {
|
|
3648
|
-
if (!control.value)
|
|
3649
|
-
return null;
|
|
3650
|
-
const regex = new RegExp(pattern);
|
|
3651
|
-
return regex.test(control.value) ? null : { pattern: { message: message || 'Invalid format' } };
|
|
3652
|
-
};
|
|
3653
|
-
}
|
|
3654
|
-
static numberRange(min, max) {
|
|
3655
|
-
return (control) => {
|
|
3656
|
-
if (!control.value && control.value !== 0)
|
|
3657
|
-
return null;
|
|
3658
|
-
const value = Number(control.value);
|
|
3659
|
-
if (min !== undefined && value < min) {
|
|
3660
|
-
return { min: { min, actual: value } };
|
|
3661
|
-
}
|
|
3662
|
-
if (max !== undefined && value > max) {
|
|
3663
|
-
return { max: { max, actual: value } };
|
|
3664
|
-
}
|
|
3665
|
-
return null;
|
|
3666
|
-
};
|
|
3667
|
-
}
|
|
3668
|
-
static dateRange(minDate, maxDate) {
|
|
3669
|
-
return (control) => {
|
|
3670
|
-
if (!control.value)
|
|
3671
|
-
return null;
|
|
3672
|
-
const date = new Date(control.value);
|
|
3673
|
-
if (minDate && date < new Date(minDate)) {
|
|
3674
|
-
return { minDate: { minDate } };
|
|
3675
|
-
}
|
|
3676
|
-
if (maxDate && date > new Date(maxDate)) {
|
|
3677
|
-
return { maxDate: { maxDate } };
|
|
3678
|
-
}
|
|
3679
|
-
return null;
|
|
3680
|
-
};
|
|
3681
|
-
}
|
|
3682
|
-
static getErrorMessage(errors) {
|
|
3683
|
-
if (!errors)
|
|
3783
|
+
/**
|
|
3784
|
+
* Utility functions for string manipulation
|
|
3785
|
+
*/
|
|
3786
|
+
class StringUtils {
|
|
3787
|
+
/**
|
|
3788
|
+
* Converts a string to camelCase.
|
|
3789
|
+
* Example: "First Name" -> "firstName", "Profile Picture" -> "profilePicture"
|
|
3790
|
+
*/
|
|
3791
|
+
static toCamelCase(str) {
|
|
3792
|
+
if (!str)
|
|
3684
3793
|
return '';
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
if (errors['minLength'])
|
|
3694
|
-
return `Minimum length is ${errors['minLength'].min} characters`;
|
|
3695
|
-
if (errors['maxLength'])
|
|
3696
|
-
return `Maximum length is ${errors['maxLength'].max} characters`;
|
|
3697
|
-
if (errors['min'])
|
|
3698
|
-
return `Minimum value is ${errors['min'].min}`;
|
|
3699
|
-
if (errors['max'])
|
|
3700
|
-
return `Maximum value is ${errors['max'].max}`;
|
|
3701
|
-
if (errors['minDate'])
|
|
3702
|
-
return `Date must be after ${errors['minDate'].minDate}`;
|
|
3703
|
-
if (errors['maxDate'])
|
|
3704
|
-
return `Date must be before ${errors['maxDate'].maxDate}`;
|
|
3705
|
-
if (errors['pattern'])
|
|
3706
|
-
return errors['pattern'].message || 'Invalid format';
|
|
3707
|
-
return 'Invalid value';
|
|
3794
|
+
return str
|
|
3795
|
+
.replace(/[^a-zA-Z0-9 ]/g, '')
|
|
3796
|
+
.split(' ')
|
|
3797
|
+
.filter(Boolean)
|
|
3798
|
+
.map((word, i) => i === 0
|
|
3799
|
+
? word.charAt(0).toLowerCase() + word.slice(1)
|
|
3800
|
+
: word.charAt(0).toUpperCase() + word.slice(1))
|
|
3801
|
+
.join('');
|
|
3708
3802
|
}
|
|
3709
3803
|
}
|
|
3710
3804
|
|
|
3711
3805
|
class FormFieldComponent {
|
|
3806
|
+
fb;
|
|
3712
3807
|
expressionService;
|
|
3713
3808
|
http;
|
|
3714
3809
|
config;
|
|
3715
3810
|
controller;
|
|
3716
|
-
|
|
3811
|
+
/**
|
|
3812
|
+
* The FormGroup that THIS field's control should be registered in.
|
|
3813
|
+
* For repeater instances this is the instance's own isolated FormGroup.
|
|
3814
|
+
* For flat (non-repeater) fields this is the root formGroup.
|
|
3815
|
+
*/
|
|
3816
|
+
formGroup;
|
|
3817
|
+
/**
|
|
3818
|
+
* Set to TRUE when this field is part of a repeatable group (config.sectionConfig.allowMulti = true).
|
|
3819
|
+
* When true, the field does NOT sync with the global controller to prevent data collision
|
|
3820
|
+
* between different instances of the same repeater row.
|
|
3821
|
+
*/
|
|
3822
|
+
allowMulti = false;
|
|
3717
3823
|
value;
|
|
3718
3824
|
isVisible = true;
|
|
3719
|
-
|
|
3825
|
+
showPassword = false; // password show/hide toggle
|
|
3826
|
+
isDragOver = false; // file upload drag-over state
|
|
3827
|
+
fileUploadError = ''; // per-field file validation error
|
|
3720
3828
|
destroy$ = new Subject();
|
|
3721
|
-
|
|
3829
|
+
// ── GROUP / allowMulti support ──────────────────────────────────────────
|
|
3830
|
+
/** For GROUP fields with allowMulti = true */
|
|
3831
|
+
groupFormArray;
|
|
3832
|
+
/** For GROUP fields with allowMulti = false — single nested FormGroup */
|
|
3833
|
+
groupFormGroup;
|
|
3834
|
+
/**
|
|
3835
|
+
* Tracked list of repeater instances.
|
|
3836
|
+
* Using a separate array (not FormArray.controls) + trackBy(id) ensures
|
|
3837
|
+
* Angular creates FRESH child components for every new row, preventing
|
|
3838
|
+
* cached values from bleeding into new instances.
|
|
3839
|
+
*/
|
|
3840
|
+
instanceList = [];
|
|
3841
|
+
_nextInstanceId = 0;
|
|
3842
|
+
/**
|
|
3843
|
+
* Key used to register the GROUP control on the parent formGroup.
|
|
3844
|
+
* Priority: sectionConfig.name > field.name > camelCase(label) > '__group__'
|
|
3845
|
+
*/
|
|
3846
|
+
get groupKey() {
|
|
3847
|
+
return (this.config.sectionConfig?.name ||
|
|
3848
|
+
this.config.name ||
|
|
3849
|
+
StringUtils.toCamelCase(this.config.sectionConfig?.label) ||
|
|
3850
|
+
'__group__');
|
|
3851
|
+
}
|
|
3852
|
+
constructor(fb, expressionService, http) {
|
|
3853
|
+
this.fb = fb;
|
|
3722
3854
|
this.expressionService = expressionService;
|
|
3723
3855
|
this.http = http;
|
|
3724
3856
|
}
|
|
3725
3857
|
ngOnInit() {
|
|
3726
|
-
this.
|
|
3858
|
+
if (this.isGroup) {
|
|
3859
|
+
this.initGroupField();
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
this.registerControl();
|
|
3727
3863
|
this.setupVisibility();
|
|
3728
3864
|
this.setupGeneratedField();
|
|
3865
|
+
this.setupFormulaValidation(); // Generic formula-based validation
|
|
3729
3866
|
this.setupDependencies();
|
|
3730
|
-
//
|
|
3867
|
+
this.setupMatchValidation(); // cross-field match (e.g. confirmPassword)
|
|
3731
3868
|
if (!this.config.optionConfig?.dependencies) {
|
|
3732
3869
|
this.loadDropdownOptions();
|
|
3733
3870
|
}
|
|
3734
3871
|
}
|
|
3872
|
+
// ── GROUP initialisation ──────────────────────────────────────────────────
|
|
3873
|
+
initGroupField() {
|
|
3874
|
+
if (this.config.sectionConfig?.allowMulti) {
|
|
3875
|
+
this.groupFormArray = this.fb.array([]);
|
|
3876
|
+
this.formGroup.addControl(this.groupKey, this.groupFormArray);
|
|
3877
|
+
this.addGroupInstance();
|
|
3878
|
+
}
|
|
3879
|
+
else {
|
|
3880
|
+
this.groupFormGroup = this.fb.group({});
|
|
3881
|
+
this.formGroup.addControl(this.groupKey, this.groupFormGroup);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Sets up cross-field validation based on the `onValidate` formula.
|
|
3886
|
+
* Watches all variables mentioned in the formula and updates the field's
|
|
3887
|
+
* validity whenever any of them change.
|
|
3888
|
+
*/
|
|
3889
|
+
setupFormulaValidation() {
|
|
3890
|
+
if (!this.config.onValidate)
|
|
3891
|
+
return;
|
|
3892
|
+
const expression = this.config.onValidate;
|
|
3893
|
+
const variables = this.expressionService.extractVariables(expression);
|
|
3894
|
+
// Subscribe to all fields mentioned in the formula
|
|
3895
|
+
const observables = variables.map(v => this.controller.getFieldObservable(v));
|
|
3896
|
+
combineLatest(observables).pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
3897
|
+
const context = this.controller.getAllData();
|
|
3898
|
+
const isValid = this.expressionService.evaluateCondition(expression, context);
|
|
3899
|
+
const control = this.formGroup.get(this.config.name);
|
|
3900
|
+
if (control) {
|
|
3901
|
+
if (!isValid) {
|
|
3902
|
+
control.setErrors({ ...control.errors, formulaError: true });
|
|
3903
|
+
}
|
|
3904
|
+
else {
|
|
3905
|
+
// Clear only the formulaError
|
|
3906
|
+
if (control.hasError('formulaError')) {
|
|
3907
|
+
const errors = { ...control.errors };
|
|
3908
|
+
delete errors['formulaError'];
|
|
3909
|
+
control.setErrors(Object.keys(errors).length ? errors : null);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
});
|
|
3914
|
+
}
|
|
3915
|
+
addGroupInstance() {
|
|
3916
|
+
const fg = this.fb.group({});
|
|
3917
|
+
this.groupFormArray.push(fg);
|
|
3918
|
+
// Spread to a new array reference so *ngFor always detects the change
|
|
3919
|
+
this.instanceList = [...this.instanceList, { id: this._nextInstanceId++, fg }];
|
|
3920
|
+
}
|
|
3921
|
+
removeGroupInstance(index) {
|
|
3922
|
+
if (this.instanceList.length > 1) {
|
|
3923
|
+
this.groupFormArray.removeAt(index);
|
|
3924
|
+
this.instanceList = this.instanceList.filter((_, i) => i !== index);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
trackByInstanceId(_, item) {
|
|
3928
|
+
return item.id;
|
|
3929
|
+
}
|
|
3930
|
+
// ── Leaf control ─────────────────────────────────────────────────────────
|
|
3735
3931
|
ngOnDestroy() {
|
|
3932
|
+
// Always complete so any subscriptions (e.g. subfields valueChanges) are cleaned up
|
|
3736
3933
|
this.destroy$.next();
|
|
3737
3934
|
this.destroy$.complete();
|
|
3935
|
+
if (this.isGroup) {
|
|
3936
|
+
if (this.formGroup?.contains(this.groupKey)) {
|
|
3937
|
+
this.formGroup.removeControl(this.groupKey);
|
|
3938
|
+
}
|
|
3939
|
+
return;
|
|
3940
|
+
}
|
|
3941
|
+
const fieldName = this.config.name;
|
|
3942
|
+
if (fieldName && this.formGroup?.contains(fieldName)) {
|
|
3943
|
+
this.formGroup.removeControl(fieldName);
|
|
3944
|
+
}
|
|
3738
3945
|
}
|
|
3739
|
-
|
|
3740
|
-
if (this.config.name)
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3946
|
+
registerControl() {
|
|
3947
|
+
if (!this.config.name || !this.formGroup)
|
|
3948
|
+
return;
|
|
3949
|
+
const fieldName = this.config.name;
|
|
3950
|
+
const validators = this.getValidators();
|
|
3951
|
+
// When inside a repeater instance, ALWAYS start with defaultValue (never
|
|
3952
|
+
// read from the shared controller — prevents cross-instance value copying).
|
|
3953
|
+
const initialValue = this.allowMulti
|
|
3954
|
+
? (this.config.defaultValue ?? null)
|
|
3955
|
+
: (this.controller.getFieldValue(fieldName) ?? this.config.defaultValue ?? null);
|
|
3956
|
+
let control = this.formGroup.get(fieldName);
|
|
3957
|
+
if (!control) {
|
|
3958
|
+
control = new FormControl({ value: initialValue, disabled: !!this.config.disabled }, validators);
|
|
3959
|
+
this.formGroup.addControl(fieldName, control);
|
|
3960
|
+
}
|
|
3961
|
+
this.value = control.value;
|
|
3962
|
+
if (!this.allowMulti) {
|
|
3963
|
+
// ── Flat field: keep in sync with shared controller ──────────────────
|
|
3964
|
+
control.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(val => {
|
|
3745
3965
|
this.value = val;
|
|
3966
|
+
this.controller.updateField(fieldName, val);
|
|
3967
|
+
});
|
|
3968
|
+
this.controller.getFieldObservable(fieldName).pipe(takeUntil(this.destroy$)).subscribe(val => {
|
|
3969
|
+
if (val !== control.value) {
|
|
3970
|
+
control.setValue(val, { emitEvent: false });
|
|
3971
|
+
this.value = val;
|
|
3972
|
+
}
|
|
3746
3973
|
});
|
|
3747
3974
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
const observables = variables.map(v => this.controller.getFieldObservable(v));
|
|
3753
|
-
combineLatest(observables)
|
|
3754
|
-
.pipe(takeUntil(this.destroy$))
|
|
3755
|
-
.subscribe(() => {
|
|
3756
|
-
const context = this.controller.getAllData();
|
|
3757
|
-
this.isVisible = this.expressionService.evaluateCondition(this.config.visibilityExpression, context);
|
|
3975
|
+
else {
|
|
3976
|
+
// ── Repeater field: local value tracking only ─────────────────────────
|
|
3977
|
+
control.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(val => {
|
|
3978
|
+
this.value = val;
|
|
3758
3979
|
});
|
|
3759
3980
|
}
|
|
3760
3981
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
this.controller.updateField(this.getFieldName(), result);
|
|
3772
|
-
}
|
|
3773
|
-
});
|
|
3982
|
+
getValidators() {
|
|
3983
|
+
const validators = [];
|
|
3984
|
+
if (this.config.required)
|
|
3985
|
+
validators.push(Validators.required);
|
|
3986
|
+
if (this.config.subType === 'EMAIL')
|
|
3987
|
+
validators.push(Validators.email);
|
|
3988
|
+
if (this.config.subType === 'PASSWORD') {
|
|
3989
|
+
// Minimum 8 chars by default; honour explicit textConfig.length overrides
|
|
3990
|
+
const minLen = this.config.textConfig?.length?.min ?? 8;
|
|
3991
|
+
validators.push(Validators.minLength(minLen));
|
|
3774
3992
|
}
|
|
3993
|
+
if (this.config.textConfig?.length) {
|
|
3994
|
+
const { min, max } = this.config.textConfig.length;
|
|
3995
|
+
if (min && this.config.subType !== 'PASSWORD')
|
|
3996
|
+
validators.push(Validators.minLength(min));
|
|
3997
|
+
if (max)
|
|
3998
|
+
validators.push(Validators.maxLength(max));
|
|
3999
|
+
}
|
|
4000
|
+
if (this.config.textConfig?.pattern)
|
|
4001
|
+
validators.push(Validators.pattern(this.config.textConfig.pattern));
|
|
4002
|
+
if (this.config.numberConfig) {
|
|
4003
|
+
const { min, max } = this.config.numberConfig;
|
|
4004
|
+
if (min !== undefined)
|
|
4005
|
+
validators.push(Validators.min(min));
|
|
4006
|
+
if (max !== undefined)
|
|
4007
|
+
validators.push(Validators.max(max));
|
|
4008
|
+
}
|
|
4009
|
+
return validators;
|
|
4010
|
+
}
|
|
4011
|
+
// ── Cross-field match validation (password === confirmPassword) ───────────
|
|
4012
|
+
/**
|
|
4013
|
+
* When `textConfig.matchField` is configured, subscribes to value changes on
|
|
4014
|
+
* BOTH this control and the referenced control so the mismatch error updates
|
|
4015
|
+
* instantly whichever field the user edits last.
|
|
4016
|
+
*
|
|
4017
|
+
* - Values differ → sets `{ passwordMismatch: true }` on THIS control.
|
|
4018
|
+
* - Values match → clears `passwordMismatch` from THIS control.
|
|
4019
|
+
*/
|
|
4020
|
+
setupMatchValidation() {
|
|
4021
|
+
const matchFieldName = this.config.textConfig?.matchField;
|
|
4022
|
+
if (!matchFieldName || !this.config.name || !this.formGroup)
|
|
4023
|
+
return;
|
|
4024
|
+
const thisControl = this.formGroup.get(this.config.name);
|
|
4025
|
+
const otherControl = this.formGroup.get(matchFieldName);
|
|
4026
|
+
if (!thisControl || !otherControl)
|
|
4027
|
+
return;
|
|
4028
|
+
const runCheck = () => {
|
|
4029
|
+
const thisVal = thisControl.value;
|
|
4030
|
+
const otherVal = otherControl.value;
|
|
4031
|
+
if (thisVal && otherVal && thisVal !== otherVal) {
|
|
4032
|
+
// Both have a value but they differ — flag the mismatch
|
|
4033
|
+
thisControl.setErrors({ ...thisControl.errors, passwordMismatch: true }, { emitEvent: false });
|
|
4034
|
+
}
|
|
4035
|
+
else {
|
|
4036
|
+
// Either one is empty OR they now match — clear the mismatch error
|
|
4037
|
+
if (thisControl.hasError('passwordMismatch')) {
|
|
4038
|
+
const errors = { ...thisControl.errors };
|
|
4039
|
+
delete errors['passwordMismatch'];
|
|
4040
|
+
thisControl.setErrors(Object.keys(errors).length ? errors : null, { emitEvent: false });
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
// Fire when THIS (confirmPassword) field changes
|
|
4045
|
+
thisControl.valueChanges
|
|
4046
|
+
.pipe(takeUntil(this.destroy$))
|
|
4047
|
+
.subscribe(() => runCheck());
|
|
4048
|
+
// Also fire when the OTHER (password) field changes so the error clears
|
|
4049
|
+
// as soon as the user corrects the source value
|
|
4050
|
+
otherControl.valueChanges
|
|
4051
|
+
.pipe(takeUntil(this.destroy$))
|
|
4052
|
+
.subscribe(() => runCheck());
|
|
4053
|
+
}
|
|
4054
|
+
setupVisibility() {
|
|
4055
|
+
if (!this.config.visibilityExpression)
|
|
4056
|
+
return;
|
|
4057
|
+
const variables = this.expressionService.extractVariables(this.config.visibilityExpression);
|
|
4058
|
+
const observables = variables.map(v => this.controller.getFieldObservable(v));
|
|
4059
|
+
combineLatest(observables).pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
4060
|
+
const context = this.controller.getAllData();
|
|
4061
|
+
this.isVisible = this.expressionService.evaluateCondition(this.config.visibilityExpression, context);
|
|
4062
|
+
const control = this.formGroup?.get(this.config.name);
|
|
4063
|
+
if (control) {
|
|
4064
|
+
this.isVisible ? control.enable({ emitEvent: false }) : control.disable({ emitEvent: false });
|
|
4065
|
+
}
|
|
4066
|
+
});
|
|
4067
|
+
}
|
|
4068
|
+
setupGeneratedField() {
|
|
4069
|
+
if (this.config.type !== 'GENERATED' || !this.config.generatedConfig)
|
|
4070
|
+
return;
|
|
4071
|
+
const variables = this.config.generatedConfig.variables || [];
|
|
4072
|
+
const observables = variables.map(v => this.controller.getFieldObservable(v));
|
|
4073
|
+
combineLatest(observables).pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
4074
|
+
const context = this.controller.getAllData();
|
|
4075
|
+
const result = this.evaluateFormula(context);
|
|
4076
|
+
if (result !== null && this.config.name) {
|
|
4077
|
+
this.controller.updateField(this.config.name, result);
|
|
4078
|
+
}
|
|
4079
|
+
});
|
|
3775
4080
|
}
|
|
3776
4081
|
evaluateFormula(context) {
|
|
3777
4082
|
if (!this.config.generatedConfig)
|
|
@@ -3792,74 +4097,59 @@ class FormFieldComponent {
|
|
|
3792
4097
|
return;
|
|
3793
4098
|
const dependencies = this.config.optionConfig.dependencies;
|
|
3794
4099
|
const observables = Object.values(dependencies).map(fieldName => this.controller.getFieldObservable(fieldName));
|
|
3795
|
-
combineLatest(observables)
|
|
3796
|
-
.pipe(takeUntil(this.destroy$))
|
|
3797
|
-
.subscribe((values) => {
|
|
3798
|
-
// Create a map of dependency values
|
|
4100
|
+
combineLatest(observables).pipe(takeUntil(this.destroy$)).subscribe(values => {
|
|
3799
4101
|
const dependencyValues = {};
|
|
3800
4102
|
Object.keys(dependencies).forEach((paramKey, index) => {
|
|
3801
4103
|
dependencyValues[paramKey] = values[index];
|
|
3802
4104
|
});
|
|
3803
|
-
// Check if all required dependencies have values (optional: could skip this if partial dependencies are allowed)
|
|
3804
4105
|
const allPresent = Object.values(dependencyValues).every(v => v !== null && v !== undefined && v !== '');
|
|
3805
4106
|
if (allPresent) {
|
|
3806
4107
|
this.loadDropdownOptions(dependencyValues);
|
|
3807
4108
|
}
|
|
3808
|
-
else {
|
|
3809
|
-
|
|
3810
|
-
if (this.config.optionConfig) {
|
|
3811
|
-
this.config.optionConfig.optionList = [];
|
|
3812
|
-
}
|
|
4109
|
+
else if (this.config.optionConfig) {
|
|
4110
|
+
this.config.optionConfig.optionList = [];
|
|
3813
4111
|
}
|
|
3814
4112
|
});
|
|
3815
4113
|
}
|
|
3816
4114
|
loadDropdownOptions(queryParams = {}) {
|
|
3817
4115
|
const optionConfig = this.config.optionConfig;
|
|
3818
|
-
const urls = optionConfig?.apiUrls ||
|
|
4116
|
+
const urls = optionConfig?.apiUrls ||
|
|
4117
|
+
(optionConfig?.apiUrl ? [optionConfig.apiUrl] :
|
|
4118
|
+
optionConfig?.optionUrl ? [optionConfig.optionUrl] : []);
|
|
3819
4119
|
if (!urls || urls.length === 0)
|
|
3820
4120
|
return;
|
|
3821
|
-
// Create observables for each URL with query params
|
|
3822
4121
|
const observables = urls.map(url => {
|
|
3823
4122
|
let fullUrl = url;
|
|
3824
4123
|
const params = new URLSearchParams();
|
|
3825
4124
|
Object.entries(queryParams).forEach(([key, value]) => {
|
|
3826
|
-
if (value !== null && value !== undefined)
|
|
4125
|
+
if (value !== null && value !== undefined)
|
|
3827
4126
|
params.append(key, String(value));
|
|
3828
|
-
}
|
|
3829
4127
|
});
|
|
3830
4128
|
const queryString = params.toString();
|
|
3831
|
-
if (queryString)
|
|
4129
|
+
if (queryString)
|
|
3832
4130
|
fullUrl += (fullUrl.includes('?') ? '&' : '?') + queryString;
|
|
3833
|
-
}
|
|
3834
|
-
return this.http.get(fullUrl);
|
|
4131
|
+
return this.http.get(fullUrl, { headers: this.getHeaders() });
|
|
3835
4132
|
});
|
|
3836
|
-
forkJoin(observables)
|
|
3837
|
-
|
|
3838
|
-
.subscribe({
|
|
3839
|
-
next: (responses) => {
|
|
4133
|
+
forkJoin(observables).pipe(takeUntil(this.destroy$)).subscribe({
|
|
4134
|
+
next: responses => {
|
|
3840
4135
|
let mergedData = [];
|
|
3841
4136
|
responses.forEach(response => {
|
|
3842
|
-
// Identify array source for each response
|
|
3843
4137
|
const data = optionConfig?.dataPath
|
|
3844
4138
|
? this.getValueByPath(response, optionConfig.dataPath)
|
|
3845
4139
|
: (Array.isArray(response) ? response : response.data || response.items || []);
|
|
3846
|
-
if (Array.isArray(data))
|
|
4140
|
+
if (Array.isArray(data))
|
|
3847
4141
|
mergedData = [...mergedData, ...data];
|
|
3848
|
-
}
|
|
3849
4142
|
});
|
|
3850
|
-
// Sort merged data BEFORE mapping
|
|
3851
4143
|
if (optionConfig?.sortBy) {
|
|
3852
4144
|
const sortKey = optionConfig.sortBy;
|
|
3853
4145
|
const direction = optionConfig.sortDirection === 'DESC' ? -1 : 1;
|
|
3854
4146
|
mergedData.sort((a, b) => {
|
|
3855
4147
|
const valA = this.getValueByPath(a, sortKey);
|
|
3856
4148
|
const valB = this.getValueByPath(b, sortKey);
|
|
3857
|
-
if (typeof valA === 'string' && typeof valB === 'string')
|
|
4149
|
+
if (typeof valA === 'string' && typeof valB === 'string')
|
|
3858
4150
|
return direction * valA.localeCompare(valB);
|
|
3859
|
-
|
|
3860
|
-
if (typeof valA === 'number' && typeof valB === 'number') {
|
|
4151
|
+
if (typeof valA === 'number' && typeof valB === 'number')
|
|
3861
4152
|
return direction * (valA - valB);
|
|
3862
|
-
}
|
|
3863
4153
|
return 0;
|
|
3864
4154
|
});
|
|
3865
4155
|
}
|
|
@@ -3870,14 +4160,10 @@ class FormFieldComponent {
|
|
|
3870
4160
|
const code = optionConfig?.valuePath
|
|
3871
4161
|
? this.getValueByPath(item, optionConfig.valuePath)
|
|
3872
4162
|
: (item.code || item.id || item.value);
|
|
3873
|
-
return {
|
|
3874
|
-
label: String(label),
|
|
3875
|
-
code: code,
|
|
3876
|
-
value: item
|
|
3877
|
-
};
|
|
4163
|
+
return { label: String(label), code, value: item };
|
|
3878
4164
|
});
|
|
3879
4165
|
},
|
|
3880
|
-
error:
|
|
4166
|
+
error: err => console.error('Failed to load dropdown options:', err)
|
|
3881
4167
|
});
|
|
3882
4168
|
}
|
|
3883
4169
|
getValueByPath(obj, path) {
|
|
@@ -3885,138 +4171,97 @@ class FormFieldComponent {
|
|
|
3885
4171
|
return obj;
|
|
3886
4172
|
return path.split('.').reduce((acc, part) => {
|
|
3887
4173
|
const match = part.match(/(\w+)\[(\d+)\]/);
|
|
3888
|
-
if (match)
|
|
4174
|
+
if (match)
|
|
3889
4175
|
return acc?.[match[1]]?.[parseInt(match[2])];
|
|
3890
|
-
}
|
|
3891
4176
|
return acc?.[part];
|
|
3892
4177
|
}, obj);
|
|
3893
4178
|
}
|
|
3894
|
-
|
|
4179
|
+
/** Builds HttpHeaders using the token stored in the SmartFormController (sourced from configJSON). */
|
|
4180
|
+
getHeaders() {
|
|
4181
|
+
let headers = new HttpHeaders();
|
|
4182
|
+
if (this.controller.token) {
|
|
4183
|
+
const headerName = this.controller.tokenHeader || 'Authorization';
|
|
4184
|
+
headers = headers.set(headerName, this.controller.token);
|
|
4185
|
+
}
|
|
4186
|
+
return headers;
|
|
4187
|
+
}
|
|
4188
|
+
updateValue(newValue) {
|
|
3895
4189
|
if (!this.config.name)
|
|
3896
4190
|
return;
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
newValue = target.checked;
|
|
3903
|
-
}
|
|
3904
|
-
else if (target.type === 'number') {
|
|
3905
|
-
newValue = target.value ? Number(target.value) : null;
|
|
3906
|
-
}
|
|
3907
|
-
else {
|
|
3908
|
-
newValue = target.value;
|
|
3909
|
-
}
|
|
3910
|
-
}
|
|
3911
|
-
else {
|
|
3912
|
-
newValue = event;
|
|
4191
|
+
const control = this.formGroup.get(this.config.name);
|
|
4192
|
+
if (control) {
|
|
4193
|
+
control.setValue(newValue);
|
|
4194
|
+
control.markAsDirty();
|
|
4195
|
+
control.markAsTouched();
|
|
3913
4196
|
}
|
|
3914
|
-
this.controller.updateField(fieldName, newValue);
|
|
3915
|
-
this.validate(newValue);
|
|
3916
4197
|
}
|
|
3917
4198
|
onCheckboxListChange(code, checked) {
|
|
3918
4199
|
if (!this.config.name)
|
|
3919
4200
|
return;
|
|
3920
|
-
const
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
newValue = currentValue.filter((c) => c !== code);
|
|
3928
|
-
}
|
|
3929
|
-
this.controller.updateField(fieldName, newValue);
|
|
3930
|
-
this.validate(newValue);
|
|
4201
|
+
const currentValue = (this.allowMulti
|
|
4202
|
+
? (this.formGroup.get(this.config.name)?.value)
|
|
4203
|
+
: this.controller.getFieldValue(this.config.name)) || [];
|
|
4204
|
+
const newValue = checked
|
|
4205
|
+
? [...currentValue, code]
|
|
4206
|
+
: currentValue.filter((c) => c !== code);
|
|
4207
|
+
this.updateValue(newValue);
|
|
3931
4208
|
}
|
|
3932
4209
|
isChecked(code) {
|
|
3933
4210
|
const value = this.value || [];
|
|
3934
4211
|
return Array.isArray(value) && value.includes(code);
|
|
3935
4212
|
}
|
|
3936
|
-
|
|
3937
|
-
this.
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
if (
|
|
3948
|
-
this.errorMessage
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
if (
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
if (this.sectionIndex !== undefined && this.config.name) {
|
|
3989
|
-
return `${this.config.name}_${this.sectionIndex}`;
|
|
3990
|
-
}
|
|
3991
|
-
return this.config.name || '';
|
|
3992
|
-
}
|
|
3993
|
-
get isTextField() {
|
|
3994
|
-
return this.config.type === 'TEXT_INPUT';
|
|
3995
|
-
}
|
|
3996
|
-
get isNumberField() {
|
|
3997
|
-
return this.config.type === 'NUMBER_INPUT';
|
|
3998
|
-
}
|
|
3999
|
-
get isDateField() {
|
|
4000
|
-
return this.config.type === 'DATE';
|
|
4001
|
-
}
|
|
4002
|
-
get isDropdown() {
|
|
4003
|
-
return this.config.type === 'DROPDOWN';
|
|
4004
|
-
}
|
|
4005
|
-
get isRadio() {
|
|
4006
|
-
return this.config.type === 'RADIO';
|
|
4007
|
-
}
|
|
4008
|
-
get isCheckbox() {
|
|
4009
|
-
return this.config.type === 'CHECKBOX';
|
|
4010
|
-
}
|
|
4011
|
-
get isChip() {
|
|
4012
|
-
return this.config.type === 'CHIP';
|
|
4013
|
-
}
|
|
4014
|
-
get isSwitch() {
|
|
4015
|
-
return this.config.type === 'SWITCH';
|
|
4016
|
-
}
|
|
4017
|
-
get isRating() {
|
|
4018
|
-
return this.config.type === 'RATING';
|
|
4019
|
-
}
|
|
4213
|
+
get errorMessage() {
|
|
4214
|
+
if (!this.config.name || !this.formGroup)
|
|
4215
|
+
return '';
|
|
4216
|
+
const control = this.formGroup.get(this.config.name);
|
|
4217
|
+
if (control && control.invalid && (control.touched || control.dirty)) {
|
|
4218
|
+
if (control.hasError('required'))
|
|
4219
|
+
return 'This field is required';
|
|
4220
|
+
if (control.hasError('email'))
|
|
4221
|
+
return 'Invalid email format';
|
|
4222
|
+
if (control.hasError('passwordMismatch'))
|
|
4223
|
+
return 'Passwords do not match';
|
|
4224
|
+
if (control.hasError('formulaError'))
|
|
4225
|
+
return this.config.errorMessage || 'Invalid value';
|
|
4226
|
+
if (control.hasError('minlength'))
|
|
4227
|
+
return `Minimum length is ${control.errors?.['minlength'].requiredLength} characters`;
|
|
4228
|
+
if (control.hasError('maxlength'))
|
|
4229
|
+
return `Maximum length is ${control.errors?.['maxlength'].requiredLength} characters`;
|
|
4230
|
+
if (control.hasError('min'))
|
|
4231
|
+
return `Minimum value is ${control.errors?.['min'].min}`;
|
|
4232
|
+
if (control.hasError('max'))
|
|
4233
|
+
return `Maximum value is ${control.errors?.['max'].max}`;
|
|
4234
|
+
if (control.hasError('pattern'))
|
|
4235
|
+
return this.config.textConfig?.patternMessage || 'Invalid format';
|
|
4236
|
+
}
|
|
4237
|
+
return '';
|
|
4238
|
+
}
|
|
4239
|
+
// ── Type guards ──────────────────────────────────────────────────────────
|
|
4240
|
+
get isTextField() { return this.config.type === 'TEXT_INPUT'; }
|
|
4241
|
+
get isNumberField() { return this.config.type === 'NUMBER_INPUT'; }
|
|
4242
|
+
get isDateField() { return this.config.type === 'DATE'; }
|
|
4243
|
+
get isDropdown() { return this.config.type === 'DROPDOWN'; }
|
|
4244
|
+
get isFileUpload() { return this.config.type === 'FILE_UPLOAD'; }
|
|
4245
|
+
get isRadio() { return this.config.type === 'RADIO'; }
|
|
4246
|
+
get isCheckbox() { return this.config.type === 'CHECKBOX'; }
|
|
4247
|
+
get isChip() { return this.config.type === 'CHIP'; }
|
|
4248
|
+
get isSwitch() { return this.config.type === 'SWITCH'; }
|
|
4249
|
+
get isRating() { return this.config.type === 'RATING'; }
|
|
4250
|
+
get isGenerated() { return this.config.type === 'GENERATED'; }
|
|
4251
|
+
get isRow() { return this.config.type === 'ROW'; }
|
|
4252
|
+
get isGroup() { return this.config.type === 'GROUP'; }
|
|
4253
|
+
/**
|
|
4254
|
+
* Returns the effective grid column span for a child inside a ROW.
|
|
4255
|
+
* If the child declares an explicit colSpan, use it.
|
|
4256
|
+
* Otherwise divide 12 equally among all children (floor, min 1).
|
|
4257
|
+
*/
|
|
4258
|
+
getChildColSpan(child) {
|
|
4259
|
+
if (child.colSpan)
|
|
4260
|
+
return child.colSpan;
|
|
4261
|
+
const count = this.config.children?.length || 1;
|
|
4262
|
+
return Math.max(1, Math.floor(12 / count));
|
|
4263
|
+
}
|
|
4264
|
+
// ── Rating helpers ───────────────────────────────────────────────────────
|
|
4020
4265
|
onRatingChange(star, event) {
|
|
4021
4266
|
if (!this.config.name || this.config.disabled)
|
|
4022
4267
|
return;
|
|
@@ -4024,78 +4269,214 @@ class FormFieldComponent {
|
|
|
4024
4269
|
if (this.config.ratingConfig?.allowHalf && event) {
|
|
4025
4270
|
const target = event.target;
|
|
4026
4271
|
const rect = target.getBoundingClientRect();
|
|
4027
|
-
|
|
4028
|
-
// If click is in the first 50% of the star, it's a half star
|
|
4029
|
-
if (x < rect.width / 2) {
|
|
4272
|
+
if (event.clientX - rect.left < rect.width / 2)
|
|
4030
4273
|
newValue = star - 0.5;
|
|
4031
|
-
}
|
|
4032
4274
|
}
|
|
4033
|
-
|
|
4034
|
-
if (this.value === newValue) {
|
|
4275
|
+
if (this.value === newValue)
|
|
4035
4276
|
newValue = 0;
|
|
4036
|
-
|
|
4037
|
-
this.controller.updateField(this.getFieldName(), newValue);
|
|
4038
|
-
this.validate(newValue);
|
|
4277
|
+
this.updateValue(newValue);
|
|
4039
4278
|
}
|
|
4040
4279
|
getStarArray() {
|
|
4041
4280
|
const max = this.config.ratingConfig?.maxRating || 5;
|
|
4042
4281
|
return Array.from({ length: max }, (_, i) => i + 1);
|
|
4043
4282
|
}
|
|
4044
|
-
isStarHalf(star) {
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: FormFieldComponent, isStandalone: false, selector: "lib-form-field", inputs: { config: "config", controller: "controller", sectionIndex: "sectionIndex" }, ngImport: i0, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\n <!-- ROW Layout -->\n <div *ngIf=\"isRow\" class=\"form-row\" [class.horizontal]=\"config.subType === 'HORIZONTAL'\">\n <ng-container *ngFor=\"let child of config.children\">\n <div class=\"row-field\">\n <lib-form-field\n [config]=\"child\"\n [controller]=\"controller\"\n [sectionIndex]=\"sectionIndex\">\n </lib-form-field>\n </div>\n </ng-container>\n </div>\n\n <!-- GROUP (Section) -->\n <div *ngIf=\"isGroup && config.sectionConfig\" class=\"form-group\">\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\n <lib-form-field\n [config]=\"field\"\n [controller]=\"controller\"\n [sectionIndex]=\"sectionIndex\">\n </lib-form-field>\n </ng-container>\n </div>\n\n <!-- Text Input -->\n <div *ngIf=\"isTextField\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <textarea\n *ngIf=\"config.subType === 'LONG'\"\n class=\"field-input textarea\"\n [placeholder]=\"config.hint || ''\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n (input)=\"onValueChange($event)\"\n rows=\"4\">\n </textarea>\n \n <input\n *ngIf=\"config.subType !== 'LONG'\"\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\"\n class=\"field-input\"\n [placeholder]=\"config.hint || ''\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n (input)=\"onValueChange($event)\">\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Number Input -->\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <input\n type=\"number\"\n class=\"field-input\"\n [placeholder]=\"config.hint || ''\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n [min]=\"config.numberConfig?.min\"\n [max]=\"config.numberConfig?.max\"\n [step]=\"config.numberConfig?.step || 1\"\n (input)=\"onValueChange($event)\">\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Date Input -->\n <div *ngIf=\"isDateField\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <input\n type=\"date\"\n class=\"field-input\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n [min]=\"config.dateConfig?.minDate\"\n [max]=\"config.dateConfig?.maxDate\"\n (input)=\"onValueChange($event)\">\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Dropdown -->\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <select\n *ngIf=\"config.subType === 'SINGLE'\"\n class=\"field-input\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <option value=\"\">Select...</option>\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\n {{ option.label }}\n </option>\n </select>\n \n <select\n *ngIf=\"config.subType === 'MULTIPLE'\"\n class=\"field-input\"\n multiple\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\n {{ option.label }}\n </option>\n </select>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Radio -->\n <div *ngIf=\"isRadio\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div class=\"radio-group\">\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\n <input\n type=\"radio\"\n [name]=\"getFieldName()\"\n [value]=\"option.code\"\n [checked]=\"value === option.code\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange(option.code)\">\n <span>{{ option.label }}</span>\n </label>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Checkbox -->\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\">\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\n <label class=\"checkbox-label\">\n <input\n type=\"checkbox\"\n [checked]=\"value === true\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <span>{{ config.label }}</span>\n </label>\n </div>\n \n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\">\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(option.code)\"\n [disabled]=\"config.disabled\"\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\n <span>{{ option.label }}</span>\n </label>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Chip -->\n <div *ngIf=\"isChip\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div class=\"chip-group\">\n <label *ngFor=\"let option of config.optionConfig?.optionList\" \n class=\"chip-label\"\n [class.selected]=\"isChecked(option.code)\">\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(option.code)\"\n [disabled]=\"config.disabled\"\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\"\n style=\"display: none;\">\n <span>{{ option.label }}</span>\n </label>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Switch -->\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\">\n <label class=\"switch-container\">\n <span class=\"field-label\">{{ config.label }}</span>\n <div class=\"switch\">\n <input\n type=\"checkbox\"\n [checked]=\"value === true\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <span class=\"slider\"></span>\n </div>\n </label>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Rating -->\n <div *ngIf=\"isRating\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div class=\"rating-group\">\n <span *ngFor=\"let star of getStarArray()\" \n class=\"star\"\n [class.filled]=\"isStarFilled(star)\"\n [class.half]=\"isStarHalf(star)\"\n (click)=\"onRatingChange(star, $event)\">\n \u2605\n </span>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Generated Field (Read-only) -->\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\n <div class=\"generated-value\">{{ value || '-' }}</div>\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\n </div>\n</div>\n", styles: [".form-field{margin-bottom:16px}.form-field.has-error .field-input{border-color:#f44336}.form-row{display:flex;gap:16px}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{font-size:14px;font-weight:500;color:#333}.field-label .required{color:#f44336;margin-left:4px}.field-input{padding:10px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;transition:border-color .3s}.field-input:focus{outline:none;border-color:#2196f3}.field-input:disabled{background:#f5f5f5;cursor:not-allowed}.field-input.textarea{resize:vertical;font-family:inherit}.field-hint{font-size:12px;color:#666}.field-error{font-size:12px;color:#f44336}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px}.radio-label input,.checkbox-label input{cursor:pointer}.checkbox-single .checkbox-label{font-weight:500}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:8px 16px;border:1px solid #ddd;border-radius:20px;cursor:pointer;font-size:14px;transition:all .3s}.chip-label:hover{background:#f5f5f5}.chip-label.selected{background:#2196f3;color:#fff;border-color:#2196f3}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:#2196f3}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:#ccc;transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{font-size:28px;display:inline-block;background:#ddd;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.rating-group .star.filled{background:#ffc107;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.rating-group .star.half{background:linear-gradient(90deg,#ffc107 50%,#ddd 50%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.rating-group .star:hover{background:#ffc107;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.generated-value{padding:10px 12px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;font-size:14px;color:#666}select[multiple]{min-height:120px}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: FormFieldComponent, selector: "lib-form-field", inputs: ["config", "controller", "sectionIndex"] }] });
|
|
4283
|
+
isStarHalf(star) { return (this.value || 0) === star - 0.5; }
|
|
4284
|
+
isStarFilled(star) { return (this.value || 0) >= star; }
|
|
4285
|
+
// ── File Upload helpers ──────────────────────────────────────────────────
|
|
4286
|
+
onDragOver(event) {
|
|
4287
|
+
event.preventDefault();
|
|
4288
|
+
event.stopPropagation();
|
|
4289
|
+
this.isDragOver = true;
|
|
4290
|
+
}
|
|
4291
|
+
onDragLeave(event) {
|
|
4292
|
+
event.preventDefault();
|
|
4293
|
+
event.stopPropagation();
|
|
4294
|
+
this.isDragOver = false;
|
|
4295
|
+
}
|
|
4296
|
+
onFileDrop(event) {
|
|
4297
|
+
event.preventDefault();
|
|
4298
|
+
event.stopPropagation();
|
|
4299
|
+
this.isDragOver = false;
|
|
4300
|
+
const files = event.dataTransfer?.files;
|
|
4301
|
+
if (files && files.length > 0) {
|
|
4302
|
+
this.processFiles(files);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
onFileSelected(event) {
|
|
4306
|
+
const input = event.target;
|
|
4307
|
+
if (input.files && input.files.length > 0) {
|
|
4308
|
+
this.processFiles(input.files);
|
|
4309
|
+
}
|
|
4310
|
+
// Reset input so the same file can be re-selected if removed
|
|
4311
|
+
input.value = '';
|
|
4312
|
+
}
|
|
4313
|
+
processFiles(files) {
|
|
4314
|
+
this.fileUploadError = '';
|
|
4315
|
+
const cfg = this.config.attachmentConfig;
|
|
4316
|
+
const maxSizeBytes = (cfg?.maxSizeMB ?? 10) * 1024 * 1024;
|
|
4317
|
+
const currentFiles = this.value || [];
|
|
4318
|
+
const maxFiles = cfg?.maxFiles ?? (cfg?.multiple ? 10 : 1);
|
|
4319
|
+
const isMultiple = cfg?.multiple ?? false;
|
|
4320
|
+
const incoming = Array.from(files);
|
|
4321
|
+
for (const file of incoming) {
|
|
4322
|
+
// Size validation
|
|
4323
|
+
if (file.size > maxSizeBytes) {
|
|
4324
|
+
this.fileUploadError = `"${file.name}" exceeds the maximum allowed size of ${cfg?.maxSizeMB ?? 10} MB.`;
|
|
4325
|
+
continue;
|
|
4326
|
+
}
|
|
4327
|
+
// Max-files validation
|
|
4328
|
+
if (isMultiple && currentFiles.length >= maxFiles) {
|
|
4329
|
+
this.fileUploadError = `Maximum ${maxFiles} file${maxFiles !== 1 ? 's' : ''} allowed.`;
|
|
4330
|
+
break;
|
|
4331
|
+
}
|
|
4332
|
+
const reader = new FileReader();
|
|
4333
|
+
reader.onload = (e) => {
|
|
4334
|
+
const entry = {
|
|
4335
|
+
name: file.name,
|
|
4336
|
+
size: file.size,
|
|
4337
|
+
type: file.type,
|
|
4338
|
+
dataUrl: e.target?.result,
|
|
4339
|
+
file
|
|
4340
|
+
};
|
|
4341
|
+
const updated = isMultiple ? [...currentFiles, entry] : [entry];
|
|
4342
|
+
this.updateValue(updated);
|
|
4343
|
+
};
|
|
4344
|
+
reader.readAsDataURL(file);
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
removeUploadedFile(index) {
|
|
4348
|
+
const current = this.value || [];
|
|
4349
|
+
const updated = current.filter((_, i) => i !== index);
|
|
4350
|
+
this.updateValue(updated);
|
|
4351
|
+
}
|
|
4352
|
+
getFileIcon(mimeType) {
|
|
4353
|
+
if (mimeType.includes('pdf'))
|
|
4354
|
+
return 'picture_as_pdf';
|
|
4355
|
+
if (mimeType.includes('image'))
|
|
4356
|
+
return 'image';
|
|
4357
|
+
if (mimeType.includes('word') || mimeType.includes('document'))
|
|
4358
|
+
return 'description';
|
|
4359
|
+
if (mimeType.includes('sheet') || mimeType.includes('excel') || mimeType.includes('csv'))
|
|
4360
|
+
return 'table_chart';
|
|
4361
|
+
if (mimeType.includes('zip') || mimeType.includes('compressed'))
|
|
4362
|
+
return 'folder_zip';
|
|
4363
|
+
return 'attach_file';
|
|
4364
|
+
}
|
|
4365
|
+
formatFileSize(bytes) {
|
|
4366
|
+
if (bytes < 1024)
|
|
4367
|
+
return `${bytes} B`;
|
|
4368
|
+
if (bytes < 1024 * 1024)
|
|
4369
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
4370
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
4371
|
+
}
|
|
4372
|
+
// ── Action Labels ──────────────────────────────────────────────────────────
|
|
4373
|
+
get addLabel() {
|
|
4374
|
+
const key = this.controller.actionLabels?.addLabel || 'Add';
|
|
4375
|
+
return this.controller.labels[key] || key;
|
|
4376
|
+
}
|
|
4377
|
+
get removeLabel() {
|
|
4378
|
+
const key = this.controller.actionLabels?.removeLabel || 'Remove';
|
|
4379
|
+
return this.controller.labels[key] || key;
|
|
4380
|
+
}
|
|
4381
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FormFieldComponent, deps: [{ token: i1$2.FormBuilder }, { token: ExpressionService }, { token: i3.HttpClient }], target: i0.ɵɵFactoryTarget.Component });
|
|
4382
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: FormFieldComponent, isStandalone: false, selector: "lib-form-field", inputs: { config: "config", controller: "controller", formGroup: "formGroup", allowMulti: "allowMulti" }, ngImport: i0, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\r\n\r\n <!-- \u2550\u2550 ROW Layout \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRow\" class=\"form-row grid-row\">\r\n <ng-container *ngFor=\"let child of config.children\">\r\n <div class=\"row-field\" [style.gridColumn]=\"'span ' + getChildColSpan(child)\">\r\n <lib-form-field [config]=\"child\" [controller]=\"controller\" [formGroup]=\"formGroup\" [allowMulti]=\"allowMulti\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 allowMulti (repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig?.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label\">{{ config.sectionConfig!.label }}</h3>\r\n\r\n <div *ngFor=\"let instance of instanceList; trackBy: trackByInstanceId; let i = index\" class=\"group-instance\">\r\n <!-- Instance header \u2014 show remove only when more than 1 instance -->\r\n <div class=\"group-header\" *ngIf=\"instanceList.length > 1\">\r\n <span class=\"group-number\">{{ config.sectionConfig!.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" [icon]=\"{type: 'material', value: 'delete_outline'}\"\r\n (click)=\"removeGroupInstance(i)\">\r\n {{ removeLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Fields \u2014 each child receives the *instance* FormGroup and the group's allowMulti config -->\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig!.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instance.fg\" [allowMulti]=\"true\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <lib-button [variant]=\"'outline'\" [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\"\r\n class=\"btn-add-group-wrapper\">\r\n {{ addLabel }} {{ config.sectionConfig!.label }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 single (non-repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig && !config.sectionConfig.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig.label\">{{ config.sectionConfig.label }}</h3>\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"groupFormGroup\" [allowMulti]=\"false\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Text Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTextField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <textarea *ngIf=\"config.subType === 'LONG'\" class=\"field-input textarea\" [placeholder]=\"config.placeholder || ''\"\r\n [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\" rows=\"4\">\r\n </textarea>\r\n\r\n <!-- Password input with show/hide toggle -->\r\n <div *ngIf=\"config.subType === 'PASSWORD'\" class=\"password-wrapper\">\r\n <input [type]=\"showPassword ? 'text' : 'password'\" class=\"field-input password-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <button type=\"button\" class=\"password-toggle\" (click)=\"showPassword = !showPassword\" tabindex=\"-1\"\r\n [attr.aria-label]=\"showPassword ? 'Hide password' : 'Show password'\">\r\n <mat-icon class=\"eye-icon\">{{ showPassword ? 'visibility' : 'visibility_off' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input *ngIf=\"config.subType !== 'LONG' && config.subType !== 'PASSWORD'\"\r\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\" class=\"field-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\"\r\n [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Number Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input type=\"number\" class=\"field-input\" [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\"\r\n [min]=\"config.numberConfig?.min\" [max]=\"config.numberConfig?.max\" [step]=\"config.numberConfig?.step || 1\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Date Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDateField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <input type=\"date\" class=\"field-input\" [formControlName]=\"config.name!\" [min]=\"config.dateConfig?.minDate\"\r\n [max]=\"config.dateConfig?.maxDate\" [class.is-invalid]=\"errorMessage\">\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Dropdown \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <select *ngIf=\"config.subType === 'SINGLE'\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option [ngValue]=\"null\" disabled selected>{{ config.placeholder || 'Select' }}</option>\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <select *ngIf=\"config.subType === 'MULTIPLE'\" class=\"field-input\" multiple [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Radio \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRadio\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"radio-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\r\n <input type=\"radio\" [formControlName]=\"config.name!\" [value]=\"option.code\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Checkbox \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\r\n <label class=\"checkbox-label\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span>{{ config.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Chip \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isChip\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"chip-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"chip-label\"\r\n [class.selected]=\"isChecked(option.code)\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\" style=\"display: none;\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Switch \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label class=\"switch-container\">\r\n <span class=\"field-label\">{{ config.label }}</span>\r\n <div class=\"switch\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span class=\"slider\"></span>\r\n </div>\r\n </label>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rating \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRating\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rating-group\" [class.is-invalid]=\"errorMessage\">\r\n <span *ngFor=\"let star of getStarArray()\" class=\"star\" [class.filled]=\"isStarFilled(star)\"\r\n [class.half]=\"isStarHalf(star)\" (click)=\"onRatingChange(star, $event)\">\r\n <mat-icon>{{ isStarFilled(star) || isStarHalf(star) ? 'star' : 'star_border' }}</mat-icon>\r\n </span>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Generated Field (read-only) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\r\n <div class=\"generated-value\">{{ value || '-' }}</div>\r\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 File Upload \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isFileUpload\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Drop Zone -->\r\n <div class=\"upload-drop-zone\" [class.drag-over]=\"isDragOver\" [class.has-files]=\"value?.length\"\r\n [class.is-invalid]=\"errorMessage\" (dragover)=\"onDragOver($event)\" (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onFileDrop($event)\" (click)=\"fileInput.click()\">\r\n\r\n <div class=\"upload-icon-wrap\">\r\n <mat-icon class=\"upload-cloud-icon\">cloud_upload</mat-icon>\r\n </div>\r\n\r\n <p class=\"upload-main-text\">Drag and drop files here or <span class=\"upload-link\">click to upload</span></p>\r\n <p class=\"upload-hint-text\" *ngIf=\"config.attachmentConfig?.acceptLabel\">\r\n Supported formats:\r\n <span class=\"upload-formats\">{{ config.attachmentConfig!.acceptLabel }}</span>\r\n </p>\r\n <p class=\"upload-hint-text\" *ngIf=\"!config.attachmentConfig?.acceptLabel && config.hint\">\r\n {{ config.hint }}\r\n </p>\r\n\r\n <!-- Hidden native file input -->\r\n <input #fileInput type=\"file\" style=\"display:none\"\r\n [attr.multiple]=\"config.attachmentConfig?.multiple ? true : null\"\r\n [attr.accept]=\"config.attachmentConfig?.accept || null\" (change)=\"onFileSelected($event)\">\r\n </div>\r\n\r\n <!-- Uploaded file list -->\r\n <div class=\"uploaded-list\" *ngIf=\"value?.length\">\r\n <div *ngFor=\"let f of value; let i = index\" class=\"uploaded-item\">\r\n <!-- File type icon -->\r\n <mat-icon class=\"file-type-icon\">{{ getFileIcon(f.type) }}</mat-icon>\r\n\r\n <!-- Image thumbnail (only for images) -->\r\n <img *ngIf=\"f.type?.startsWith('image') && f.dataUrl\" [src]=\"f.dataUrl\" class=\"file-thumb\" alt=\"preview\">\r\n\r\n <!-- Name & size -->\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size\">{{ formatFileSize(f.size) }}</span>\r\n </div>\r\n <lib-button [variant]=\"'danger-outline'\" (click)=\"removeUploadedFile(i)\">\r\n <mat-icon>close</mat-icon>\r\n </lib-button>\r\n </div>\r\n </div>\r\n\r\n <!-- Validation / file errors -->\r\n <span class=\"field-error\" *ngIf=\"fileUploadError\">{{ fileUploadError }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage && !fileUploadError\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\"\r\n *ngIf=\"config.hint && !errorMessage && !fileUploadError && !config.attachmentConfig?.acceptLabel\">\r\n {{ config.hint }}\r\n </span>\r\n </div>\r\n\r\n</div>", styles: [".form-field{margin-bottom:var(--cc-sf-grid-gap, 16px)}.form-field.has-error .field-input{border-color:var(--cc-sf-error-border, #DC2626)}.form-row{display:flex;gap:var(--cc-sf-grid-gap, 16px)}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.form-row.grid-row{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-row.grid-row{grid-template-columns:1fr}.form-row.grid-row .row-field{grid-column:span 12!important}}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:var(--cc-sf-label-weight, 500);color:var(--cc-sf-label-color, #111827);margin-bottom:.5rem;line-height:1.25rem}.field-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.field-input{width:100%;padding:var(--cc-sf-input-padding, .625rem .875rem);font-size:var(--cc-sf-input-font-size, .875rem);line-height:1.5;color:var(--cc-sf-input-color, #111827);background-color:var(--cc-sf-input-bg, #ffffff);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);border-radius:var(--cc-sf-input-radius, 8px);box-shadow:var(--cc-sf-input-shadow, none);transition:var(--cc-sf-input-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.field-input::placeholder{color:var(--cc-sf-input-placeholder, #9CA3AF)}.field-input:hover:not(:disabled):not([readonly]){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.field-input:focus{outline:none;border-color:var(--cc-sf-input-focus-border, #3B82F6);box-shadow:var(--cc-sf-input-focus-shadow, 0 0 0 3px rgba(59, 130, 246, .12))}.field-input:disabled,.field-input[readonly]{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.field-input.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.field-input.is-invalid:focus{box-shadow:var(--cc-sf-error-focus-shadow, 0 0 0 3px rgba(220, 38, 38, .1))}.field-input.textarea{resize:vertical;min-height:100px;font-family:var(--cc-sf-font-family, inherit)}select.field-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}select.field-input:disabled{cursor:not-allowed}.field-hint{font-size:var(--cc-sf-hint-size, .75rem);color:var(--cc-sf-hint-color, #6B7280)}.field-error{font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--cc-sf-label-size, .875rem);color:var(--cc-sf-label-color, #111827)}.radio-label input,.checkbox-label input{cursor:pointer;accent-color:var(--cc-sf-chip-selected-bg, #3B82F6)}.checkbox-single .checkbox-label{font-weight:var(--cc-sf-label-weight, 500)}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:var(--cc-sf-chip-padding, 6px 14px);background:var(--cc-sf-chip-bg, #ffffff);color:var(--cc-sf-chip-color, #374151);border:var(--cc-sf-chip-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-chip-radius, 20px);cursor:pointer;font-size:var(--cc-sf-font-size-base, .875rem);transition:var(--cc-sf-input-transition, all .2s ease)}.chip-label:hover{background:var(--cc-sf-chip-hover-bg, #F3F4F6)}.chip-label.selected{background:var(--cc-sf-chip-selected-bg, #3B82F6);color:var(--cc-sf-chip-selected-color, #ffffff);border-color:var(--cc-sf-chip-selected-border, #3B82F6)}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:var(--cc-sf-switch-track-on, #3B82F6)}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--cc-sf-switch-track-off, #D1D5DB);transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:var(--cc-sf-switch-thumb, #ffffff);transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{display:inline-flex;align-items:center;cursor:pointer;transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star mat-icon{font-size:var(--cc-sf-star-size, 28px);width:var(--cc-sf-star-size, 28px);height:var(--cc-sf-star-size, 28px);line-height:var(--cc-sf-star-size, 28px);color:var(--cc-sf-star-empty, #D1D5DB);transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star.filled mat-icon,.rating-group .star.half mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.rating-group .star:hover mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.password-wrapper{position:relative;display:flex;align-items:center}.password-wrapper .password-input{padding-right:2.75rem;width:100%}.password-wrapper .password-toggle{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:.25rem;line-height:1;color:var(--cc-sf-hint-color, #6B7280);display:flex;align-items:center;justify-content:center;transition:color var(--cc-sf-input-transition, .2s ease)}.password-wrapper .password-toggle mat-icon.eye-icon{font-size:1.125rem;width:1.125rem;height:1.125rem;line-height:1.125rem}.password-wrapper .password-toggle:hover{color:var(--cc-sf-label-color, #374151)}.password-wrapper .password-toggle:focus{outline:none}.generated-value{padding:var(--cc-sf-generated-padding, .625rem .875rem);background:var(--cc-sf-generated-bg, #F3F4F6);border:var(--cc-sf-generated-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-generated-radius, 8px);font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-generated-color, #6B7280);font-family:var(--cc-sf-font-family, inherit)}.group-section-wrapper{margin-bottom:var(--cc-sf-section-gap, 20px);border:var(--cc-sf-section-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-section-radius, 10px);padding:var(--cc-sf-section-padding, 20px);background-color:var(--cc-sf-section-bg, #ffffff);box-shadow:var(--cc-sf-section-shadow, 0 1px 4px rgba(0, 0, 0, .05))}.group-section-wrapper .group-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.group-section-wrapper .group-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.group-section-wrapper .group-fields.sf-grid{grid-template-columns:1fr}.group-section-wrapper .group-fields.sf-grid .sf-col{grid-column:span 12!important}}.group-section-wrapper .group-instance{position:relative;margin-bottom:16px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px)}.group-section-wrapper .group-instance:last-child{margin-bottom:0}.group-section-wrapper .group-instance .group-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:10px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.group-section-wrapper .group-instance .group-header .group-number{font-weight:500;color:var(--cc-sf-instance-num-color, #4B5563);font-size:var(--cc-sf-instance-num-size, .8125rem)}.btn-remove{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:var(--cc-sf-error-text-size, .75rem);transition:var(--cc-sf-btn-transition, all .2s ease)}.btn-remove mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.btn-add-group{display:flex;align-items:center;justify-content:center;gap:4px;width:100%;padding:8px 16px;margin-top:12px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.btn-add-group mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.btn-add-group:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}.upload-drop-zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 24px;border:var(--cc-sf-dropzone-border, 1.5px dashed #CBD5E1);border-radius:var(--cc-sf-dropzone-radius, 12px);background-color:var(--cc-sf-dropzone-bg, #F8FAFC);cursor:pointer;transition:background-color .2s ease,border-color .2s ease;text-align:center;-webkit-user-select:none;user-select:none}.upload-drop-zone:hover{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-hover-border, #93C5FD)}.upload-drop-zone.drag-over{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-over-border, #3B82F6);box-shadow:var(--cc-sf-dropzone-over-shadow, 0 0 0 4px rgba(59, 130, 246, .12))}.upload-drop-zone.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.upload-icon-wrap{margin-bottom:4px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-icon-wrap mat-icon.upload-cloud-icon{font-size:56px;width:56px;height:56px;line-height:56px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-main-text{font-size:.9rem;font-weight:600;color:var(--cc-sf-label-color, #1E293B);margin:0}.upload-main-text .upload-link{color:var(--cc-sf-dropzone-link-color, #3B82F6)}.upload-hint-text{font-size:.78rem;color:var(--cc-sf-dropzone-hint-color, #64748B);margin:0}.upload-hint-text .upload-formats{color:var(--cc-sf-dropzone-link-color, #3B82F6);font-weight:500}.uploaded-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}.uploaded-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cc-sf-uploaded-item-bg, #ffffff);border:var(--cc-sf-uploaded-item-border, 1px solid #E2E8F0);border-radius:var(--cc-sf-uploaded-item-radius, 8px);transition:box-shadow .15s ease}.uploaded-item:hover{box-shadow:0 2px 6px #0000000f}.uploaded-item mat-icon.file-type-icon{font-size:20px;width:20px;height:20px;line-height:20px;flex-shrink:0;color:var(--cc-sf-hint-color, #64748B)}.uploaded-item .file-thumb{width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0}.uploaded-item .file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.uploaded-item .file-info .file-name{font-size:.85rem;font-weight:500;color:var(--cc-sf-label-color, #1E293B);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uploaded-item .file-info .file-size{font-size:.72rem;color:var(--cc-sf-hint-color, #94A3B8)}.uploaded-item .file-remove-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:var(--cc-sf-uploaded-remove-color, #94A3B8);padding:4px;border-radius:4px;flex-shrink:0;transition:color .15s ease,background .15s ease}.uploaded-item .file-remove-btn mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.uploaded-item .file-remove-btn:hover{color:var(--cc-sf-uploaded-remove-hover-color, #DC2626);background:var(--cc-sf-uploaded-remove-hover-bg, #FEF2F2)}.input-group{position:relative;display:flex;align-items:stretch;width:100%}.input-group .field-input{flex:1;border-radius:0}.input-group .field-input:first-child{border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-group .field-input:last-child{border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px)}.input-group.readonly .field-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);cursor:default;padding-right:3.5rem}.input-group.readonly .input-prefix,.input-group.readonly .input-suffix{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6)}.input-prefix,.input-suffix{display:flex;align-items:center;padding:0 .875rem;background-color:var(--cc-sf-input-bg, #FFFFFF);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);color:var(--cc-sf-hint-color, #6B7280);font-size:var(--cc-sf-input-font-size, .875rem);white-space:nowrap;-webkit-user-select:none;user-select:none}.input-prefix{border-right:none;border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-suffix{border-left:none;border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px);color:var(--cc-sf-chip-selected-bg, #3B82F6);font-weight:500}.readonly-icons{position:absolute;right:.875rem;top:50%;transform:translateY(-50%);display:flex;gap:8px;pointer-events:none}.readonly-icons mat-icon.lock-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem;opacity:.5;color:var(--cc-sf-hint-color, #6B7280)}.subfields-group-wrapper{margin-bottom:var(--cc-sf-grid-gap, 16px)}.subfields-group-wrapper .group-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:600;color:var(--cc-sf-label-color, #111827);margin-bottom:.75rem}.subfields-group-wrapper .group-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.subfields-group-wrapper .subfields-row{display:flex;align-items:flex-end;gap:12px;border-radius:var(--cc-sf-input-radius, 8px);transition:all .2s ease}.subfields-group-wrapper .subfields-row.is-invalid .subfield-item ::ng-deep .field-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.subfields-group-wrapper .subfields-row .subfield-item{flex:1;min-width:0}.subfields-group-wrapper .subfields-row .subfield-item ::ng-deep .field-label{font-size:.75rem!important;margin-bottom:4px!important;font-weight:500!important;color:var(--cc-sf-hint-color, #6B7280)!important}.subfields-group-wrapper .subfields-row .subfield-separator{margin-bottom:24px;font-weight:700;color:#94a3b8}.subfields-group-wrapper .subfields-group-error{display:block;margin-top:6px;font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ButtonComponent, selector: "lib-button", inputs: ["variant", "type", "disabled", "width", "height", "borderRadius", "fontSize", "fontWeight", "backgroundColor", "color", "border", "icon", "labels"] }, { kind: "component", type: FormFieldComponent, selector: "lib-form-field", inputs: ["config", "controller", "formGroup", "allowMulti"] }] });
|
|
4063
4383
|
}
|
|
4064
4384
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FormFieldComponent, decorators: [{
|
|
4065
4385
|
type: Component,
|
|
4066
|
-
args: [{ selector: 'lib-form-field', standalone: false, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\n <!-- ROW Layout -->\n <div *ngIf=\"isRow\" class=\"form-row\" [class.horizontal]=\"config.subType === 'HORIZONTAL'\">\n <ng-container *ngFor=\"let child of config.children\">\n <div class=\"row-field\">\n <lib-form-field\n [config]=\"child\"\n [controller]=\"controller\"\n [sectionIndex]=\"sectionIndex\">\n </lib-form-field>\n </div>\n </ng-container>\n </div>\n\n <!-- GROUP (Section) -->\n <div *ngIf=\"isGroup && config.sectionConfig\" class=\"form-group\">\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\n <lib-form-field\n [config]=\"field\"\n [controller]=\"controller\"\n [sectionIndex]=\"sectionIndex\">\n </lib-form-field>\n </ng-container>\n </div>\n\n <!-- Text Input -->\n <div *ngIf=\"isTextField\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <textarea\n *ngIf=\"config.subType === 'LONG'\"\n class=\"field-input textarea\"\n [placeholder]=\"config.hint || ''\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n (input)=\"onValueChange($event)\"\n rows=\"4\">\n </textarea>\n \n <input\n *ngIf=\"config.subType !== 'LONG'\"\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\"\n class=\"field-input\"\n [placeholder]=\"config.hint || ''\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n (input)=\"onValueChange($event)\">\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Number Input -->\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <input\n type=\"number\"\n class=\"field-input\"\n [placeholder]=\"config.hint || ''\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n [min]=\"config.numberConfig?.min\"\n [max]=\"config.numberConfig?.max\"\n [step]=\"config.numberConfig?.step || 1\"\n (input)=\"onValueChange($event)\">\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Date Input -->\n <div *ngIf=\"isDateField\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <input\n type=\"date\"\n class=\"field-input\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n [min]=\"config.dateConfig?.minDate\"\n [max]=\"config.dateConfig?.maxDate\"\n (input)=\"onValueChange($event)\">\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Dropdown -->\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <select\n *ngIf=\"config.subType === 'SINGLE'\"\n class=\"field-input\"\n [value]=\"value || ''\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <option value=\"\">Select...</option>\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\n {{ option.label }}\n </option>\n </select>\n \n <select\n *ngIf=\"config.subType === 'MULTIPLE'\"\n class=\"field-input\"\n multiple\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\n {{ option.label }}\n </option>\n </select>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Radio -->\n <div *ngIf=\"isRadio\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div class=\"radio-group\">\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\n <input\n type=\"radio\"\n [name]=\"getFieldName()\"\n [value]=\"option.code\"\n [checked]=\"value === option.code\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange(option.code)\">\n <span>{{ option.label }}</span>\n </label>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Checkbox -->\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\">\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\n <label class=\"checkbox-label\">\n <input\n type=\"checkbox\"\n [checked]=\"value === true\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <span>{{ config.label }}</span>\n </label>\n </div>\n \n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\">\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(option.code)\"\n [disabled]=\"config.disabled\"\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\n <span>{{ option.label }}</span>\n </label>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Chip -->\n <div *ngIf=\"isChip\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div class=\"chip-group\">\n <label *ngFor=\"let option of config.optionConfig?.optionList\" \n class=\"chip-label\"\n [class.selected]=\"isChecked(option.code)\">\n <input\n type=\"checkbox\"\n [checked]=\"isChecked(option.code)\"\n [disabled]=\"config.disabled\"\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\"\n style=\"display: none;\">\n <span>{{ option.label }}</span>\n </label>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Switch -->\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\">\n <label class=\"switch-container\">\n <span class=\"field-label\">{{ config.label }}</span>\n <div class=\"switch\">\n <input\n type=\"checkbox\"\n [checked]=\"value === true\"\n [disabled]=\"config.disabled\"\n (change)=\"onValueChange($event)\">\n <span class=\"slider\"></span>\n </div>\n </label>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Rating -->\n <div *ngIf=\"isRating\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">\n {{ config.label }}\n <span class=\"required\" *ngIf=\"config.required\">*</span>\n </label>\n \n <div class=\"rating-group\">\n <span *ngFor=\"let star of getStarArray()\" \n class=\"star\"\n [class.filled]=\"isStarFilled(star)\"\n [class.half]=\"isStarHalf(star)\"\n (click)=\"onRatingChange(star, $event)\">\n \u2605\n </span>\n </div>\n \n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\n </div>\n\n <!-- Generated Field (Read-only) -->\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\n <div class=\"generated-value\">{{ value || '-' }}</div>\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\n </div>\n</div>\n", styles: [".form-field{margin-bottom:16px}.form-field.has-error .field-input{border-color:#f44336}.form-row{display:flex;gap:16px}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{font-size:14px;font-weight:500;color:#333}.field-label .required{color:#f44336;margin-left:4px}.field-input{padding:10px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;transition:border-color .3s}.field-input:focus{outline:none;border-color:#2196f3}.field-input:disabled{background:#f5f5f5;cursor:not-allowed}.field-input.textarea{resize:vertical;font-family:inherit}.field-hint{font-size:12px;color:#666}.field-error{font-size:12px;color:#f44336}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px}.radio-label input,.checkbox-label input{cursor:pointer}.checkbox-single .checkbox-label{font-weight:500}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:8px 16px;border:1px solid #ddd;border-radius:20px;cursor:pointer;font-size:14px;transition:all .3s}.chip-label:hover{background:#f5f5f5}.chip-label.selected{background:#2196f3;color:#fff;border-color:#2196f3}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:#2196f3}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:#ccc;transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{font-size:28px;display:inline-block;background:#ddd;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.rating-group .star.filled{background:#ffc107;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.rating-group .star.half{background:linear-gradient(90deg,#ffc107 50%,#ddd 50%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.rating-group .star:hover{background:#ffc107;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.generated-value{padding:10px 12px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;font-size:14px;color:#666}select[multiple]{min-height:120px}\n"] }]
|
|
4067
|
-
}], ctorParameters: () => [{ type: ExpressionService }, { type: i3.HttpClient }], propDecorators: { config: [{
|
|
4386
|
+
args: [{ selector: 'lib-form-field', standalone: false, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\r\n\r\n <!-- \u2550\u2550 ROW Layout \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRow\" class=\"form-row grid-row\">\r\n <ng-container *ngFor=\"let child of config.children\">\r\n <div class=\"row-field\" [style.gridColumn]=\"'span ' + getChildColSpan(child)\">\r\n <lib-form-field [config]=\"child\" [controller]=\"controller\" [formGroup]=\"formGroup\" [allowMulti]=\"allowMulti\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 allowMulti (repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig?.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label\">{{ config.sectionConfig!.label }}</h3>\r\n\r\n <div *ngFor=\"let instance of instanceList; trackBy: trackByInstanceId; let i = index\" class=\"group-instance\">\r\n <!-- Instance header \u2014 show remove only when more than 1 instance -->\r\n <div class=\"group-header\" *ngIf=\"instanceList.length > 1\">\r\n <span class=\"group-number\">{{ config.sectionConfig!.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" [icon]=\"{type: 'material', value: 'delete_outline'}\"\r\n (click)=\"removeGroupInstance(i)\">\r\n {{ removeLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Fields \u2014 each child receives the *instance* FormGroup and the group's allowMulti config -->\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig!.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instance.fg\" [allowMulti]=\"true\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <lib-button [variant]=\"'outline'\" [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\"\r\n class=\"btn-add-group-wrapper\">\r\n {{ addLabel }} {{ config.sectionConfig!.label }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 single (non-repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig && !config.sectionConfig.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig.label\">{{ config.sectionConfig.label }}</h3>\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"groupFormGroup\" [allowMulti]=\"false\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Text Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTextField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <textarea *ngIf=\"config.subType === 'LONG'\" class=\"field-input textarea\" [placeholder]=\"config.placeholder || ''\"\r\n [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\" rows=\"4\">\r\n </textarea>\r\n\r\n <!-- Password input with show/hide toggle -->\r\n <div *ngIf=\"config.subType === 'PASSWORD'\" class=\"password-wrapper\">\r\n <input [type]=\"showPassword ? 'text' : 'password'\" class=\"field-input password-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <button type=\"button\" class=\"password-toggle\" (click)=\"showPassword = !showPassword\" tabindex=\"-1\"\r\n [attr.aria-label]=\"showPassword ? 'Hide password' : 'Show password'\">\r\n <mat-icon class=\"eye-icon\">{{ showPassword ? 'visibility' : 'visibility_off' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input *ngIf=\"config.subType !== 'LONG' && config.subType !== 'PASSWORD'\"\r\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\" class=\"field-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\"\r\n [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Number Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input type=\"number\" class=\"field-input\" [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\"\r\n [min]=\"config.numberConfig?.min\" [max]=\"config.numberConfig?.max\" [step]=\"config.numberConfig?.step || 1\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Date Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDateField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <input type=\"date\" class=\"field-input\" [formControlName]=\"config.name!\" [min]=\"config.dateConfig?.minDate\"\r\n [max]=\"config.dateConfig?.maxDate\" [class.is-invalid]=\"errorMessage\">\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Dropdown \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <select *ngIf=\"config.subType === 'SINGLE'\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option [ngValue]=\"null\" disabled selected>{{ config.placeholder || 'Select' }}</option>\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <select *ngIf=\"config.subType === 'MULTIPLE'\" class=\"field-input\" multiple [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Radio \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRadio\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"radio-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\r\n <input type=\"radio\" [formControlName]=\"config.name!\" [value]=\"option.code\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Checkbox \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\r\n <label class=\"checkbox-label\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span>{{ config.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Chip \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isChip\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"chip-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"chip-label\"\r\n [class.selected]=\"isChecked(option.code)\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\" style=\"display: none;\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Switch \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label class=\"switch-container\">\r\n <span class=\"field-label\">{{ config.label }}</span>\r\n <div class=\"switch\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span class=\"slider\"></span>\r\n </div>\r\n </label>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rating \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRating\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rating-group\" [class.is-invalid]=\"errorMessage\">\r\n <span *ngFor=\"let star of getStarArray()\" class=\"star\" [class.filled]=\"isStarFilled(star)\"\r\n [class.half]=\"isStarHalf(star)\" (click)=\"onRatingChange(star, $event)\">\r\n <mat-icon>{{ isStarFilled(star) || isStarHalf(star) ? 'star' : 'star_border' }}</mat-icon>\r\n </span>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Generated Field (read-only) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\r\n <div class=\"generated-value\">{{ value || '-' }}</div>\r\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 File Upload \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isFileUpload\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Drop Zone -->\r\n <div class=\"upload-drop-zone\" [class.drag-over]=\"isDragOver\" [class.has-files]=\"value?.length\"\r\n [class.is-invalid]=\"errorMessage\" (dragover)=\"onDragOver($event)\" (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onFileDrop($event)\" (click)=\"fileInput.click()\">\r\n\r\n <div class=\"upload-icon-wrap\">\r\n <mat-icon class=\"upload-cloud-icon\">cloud_upload</mat-icon>\r\n </div>\r\n\r\n <p class=\"upload-main-text\">Drag and drop files here or <span class=\"upload-link\">click to upload</span></p>\r\n <p class=\"upload-hint-text\" *ngIf=\"config.attachmentConfig?.acceptLabel\">\r\n Supported formats:\r\n <span class=\"upload-formats\">{{ config.attachmentConfig!.acceptLabel }}</span>\r\n </p>\r\n <p class=\"upload-hint-text\" *ngIf=\"!config.attachmentConfig?.acceptLabel && config.hint\">\r\n {{ config.hint }}\r\n </p>\r\n\r\n <!-- Hidden native file input -->\r\n <input #fileInput type=\"file\" style=\"display:none\"\r\n [attr.multiple]=\"config.attachmentConfig?.multiple ? true : null\"\r\n [attr.accept]=\"config.attachmentConfig?.accept || null\" (change)=\"onFileSelected($event)\">\r\n </div>\r\n\r\n <!-- Uploaded file list -->\r\n <div class=\"uploaded-list\" *ngIf=\"value?.length\">\r\n <div *ngFor=\"let f of value; let i = index\" class=\"uploaded-item\">\r\n <!-- File type icon -->\r\n <mat-icon class=\"file-type-icon\">{{ getFileIcon(f.type) }}</mat-icon>\r\n\r\n <!-- Image thumbnail (only for images) -->\r\n <img *ngIf=\"f.type?.startsWith('image') && f.dataUrl\" [src]=\"f.dataUrl\" class=\"file-thumb\" alt=\"preview\">\r\n\r\n <!-- Name & size -->\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size\">{{ formatFileSize(f.size) }}</span>\r\n </div>\r\n <lib-button [variant]=\"'danger-outline'\" (click)=\"removeUploadedFile(i)\">\r\n <mat-icon>close</mat-icon>\r\n </lib-button>\r\n </div>\r\n </div>\r\n\r\n <!-- Validation / file errors -->\r\n <span class=\"field-error\" *ngIf=\"fileUploadError\">{{ fileUploadError }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage && !fileUploadError\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\"\r\n *ngIf=\"config.hint && !errorMessage && !fileUploadError && !config.attachmentConfig?.acceptLabel\">\r\n {{ config.hint }}\r\n </span>\r\n </div>\r\n\r\n</div>", styles: [".form-field{margin-bottom:var(--cc-sf-grid-gap, 16px)}.form-field.has-error .field-input{border-color:var(--cc-sf-error-border, #DC2626)}.form-row{display:flex;gap:var(--cc-sf-grid-gap, 16px)}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.form-row.grid-row{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-row.grid-row{grid-template-columns:1fr}.form-row.grid-row .row-field{grid-column:span 12!important}}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:var(--cc-sf-label-weight, 500);color:var(--cc-sf-label-color, #111827);margin-bottom:.5rem;line-height:1.25rem}.field-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.field-input{width:100%;padding:var(--cc-sf-input-padding, .625rem .875rem);font-size:var(--cc-sf-input-font-size, .875rem);line-height:1.5;color:var(--cc-sf-input-color, #111827);background-color:var(--cc-sf-input-bg, #ffffff);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);border-radius:var(--cc-sf-input-radius, 8px);box-shadow:var(--cc-sf-input-shadow, none);transition:var(--cc-sf-input-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.field-input::placeholder{color:var(--cc-sf-input-placeholder, #9CA3AF)}.field-input:hover:not(:disabled):not([readonly]){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.field-input:focus{outline:none;border-color:var(--cc-sf-input-focus-border, #3B82F6);box-shadow:var(--cc-sf-input-focus-shadow, 0 0 0 3px rgba(59, 130, 246, .12))}.field-input:disabled,.field-input[readonly]{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.field-input.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.field-input.is-invalid:focus{box-shadow:var(--cc-sf-error-focus-shadow, 0 0 0 3px rgba(220, 38, 38, .1))}.field-input.textarea{resize:vertical;min-height:100px;font-family:var(--cc-sf-font-family, inherit)}select.field-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}select.field-input:disabled{cursor:not-allowed}.field-hint{font-size:var(--cc-sf-hint-size, .75rem);color:var(--cc-sf-hint-color, #6B7280)}.field-error{font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--cc-sf-label-size, .875rem);color:var(--cc-sf-label-color, #111827)}.radio-label input,.checkbox-label input{cursor:pointer;accent-color:var(--cc-sf-chip-selected-bg, #3B82F6)}.checkbox-single .checkbox-label{font-weight:var(--cc-sf-label-weight, 500)}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:var(--cc-sf-chip-padding, 6px 14px);background:var(--cc-sf-chip-bg, #ffffff);color:var(--cc-sf-chip-color, #374151);border:var(--cc-sf-chip-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-chip-radius, 20px);cursor:pointer;font-size:var(--cc-sf-font-size-base, .875rem);transition:var(--cc-sf-input-transition, all .2s ease)}.chip-label:hover{background:var(--cc-sf-chip-hover-bg, #F3F4F6)}.chip-label.selected{background:var(--cc-sf-chip-selected-bg, #3B82F6);color:var(--cc-sf-chip-selected-color, #ffffff);border-color:var(--cc-sf-chip-selected-border, #3B82F6)}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:var(--cc-sf-switch-track-on, #3B82F6)}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--cc-sf-switch-track-off, #D1D5DB);transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:var(--cc-sf-switch-thumb, #ffffff);transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{display:inline-flex;align-items:center;cursor:pointer;transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star mat-icon{font-size:var(--cc-sf-star-size, 28px);width:var(--cc-sf-star-size, 28px);height:var(--cc-sf-star-size, 28px);line-height:var(--cc-sf-star-size, 28px);color:var(--cc-sf-star-empty, #D1D5DB);transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star.filled mat-icon,.rating-group .star.half mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.rating-group .star:hover mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.password-wrapper{position:relative;display:flex;align-items:center}.password-wrapper .password-input{padding-right:2.75rem;width:100%}.password-wrapper .password-toggle{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:.25rem;line-height:1;color:var(--cc-sf-hint-color, #6B7280);display:flex;align-items:center;justify-content:center;transition:color var(--cc-sf-input-transition, .2s ease)}.password-wrapper .password-toggle mat-icon.eye-icon{font-size:1.125rem;width:1.125rem;height:1.125rem;line-height:1.125rem}.password-wrapper .password-toggle:hover{color:var(--cc-sf-label-color, #374151)}.password-wrapper .password-toggle:focus{outline:none}.generated-value{padding:var(--cc-sf-generated-padding, .625rem .875rem);background:var(--cc-sf-generated-bg, #F3F4F6);border:var(--cc-sf-generated-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-generated-radius, 8px);font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-generated-color, #6B7280);font-family:var(--cc-sf-font-family, inherit)}.group-section-wrapper{margin-bottom:var(--cc-sf-section-gap, 20px);border:var(--cc-sf-section-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-section-radius, 10px);padding:var(--cc-sf-section-padding, 20px);background-color:var(--cc-sf-section-bg, #ffffff);box-shadow:var(--cc-sf-section-shadow, 0 1px 4px rgba(0, 0, 0, .05))}.group-section-wrapper .group-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.group-section-wrapper .group-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.group-section-wrapper .group-fields.sf-grid{grid-template-columns:1fr}.group-section-wrapper .group-fields.sf-grid .sf-col{grid-column:span 12!important}}.group-section-wrapper .group-instance{position:relative;margin-bottom:16px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px)}.group-section-wrapper .group-instance:last-child{margin-bottom:0}.group-section-wrapper .group-instance .group-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:10px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.group-section-wrapper .group-instance .group-header .group-number{font-weight:500;color:var(--cc-sf-instance-num-color, #4B5563);font-size:var(--cc-sf-instance-num-size, .8125rem)}.btn-remove{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:var(--cc-sf-error-text-size, .75rem);transition:var(--cc-sf-btn-transition, all .2s ease)}.btn-remove mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.btn-add-group{display:flex;align-items:center;justify-content:center;gap:4px;width:100%;padding:8px 16px;margin-top:12px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.btn-add-group mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.btn-add-group:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}.upload-drop-zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 24px;border:var(--cc-sf-dropzone-border, 1.5px dashed #CBD5E1);border-radius:var(--cc-sf-dropzone-radius, 12px);background-color:var(--cc-sf-dropzone-bg, #F8FAFC);cursor:pointer;transition:background-color .2s ease,border-color .2s ease;text-align:center;-webkit-user-select:none;user-select:none}.upload-drop-zone:hover{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-hover-border, #93C5FD)}.upload-drop-zone.drag-over{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-over-border, #3B82F6);box-shadow:var(--cc-sf-dropzone-over-shadow, 0 0 0 4px rgba(59, 130, 246, .12))}.upload-drop-zone.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.upload-icon-wrap{margin-bottom:4px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-icon-wrap mat-icon.upload-cloud-icon{font-size:56px;width:56px;height:56px;line-height:56px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-main-text{font-size:.9rem;font-weight:600;color:var(--cc-sf-label-color, #1E293B);margin:0}.upload-main-text .upload-link{color:var(--cc-sf-dropzone-link-color, #3B82F6)}.upload-hint-text{font-size:.78rem;color:var(--cc-sf-dropzone-hint-color, #64748B);margin:0}.upload-hint-text .upload-formats{color:var(--cc-sf-dropzone-link-color, #3B82F6);font-weight:500}.uploaded-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}.uploaded-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cc-sf-uploaded-item-bg, #ffffff);border:var(--cc-sf-uploaded-item-border, 1px solid #E2E8F0);border-radius:var(--cc-sf-uploaded-item-radius, 8px);transition:box-shadow .15s ease}.uploaded-item:hover{box-shadow:0 2px 6px #0000000f}.uploaded-item mat-icon.file-type-icon{font-size:20px;width:20px;height:20px;line-height:20px;flex-shrink:0;color:var(--cc-sf-hint-color, #64748B)}.uploaded-item .file-thumb{width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0}.uploaded-item .file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.uploaded-item .file-info .file-name{font-size:.85rem;font-weight:500;color:var(--cc-sf-label-color, #1E293B);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uploaded-item .file-info .file-size{font-size:.72rem;color:var(--cc-sf-hint-color, #94A3B8)}.uploaded-item .file-remove-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:var(--cc-sf-uploaded-remove-color, #94A3B8);padding:4px;border-radius:4px;flex-shrink:0;transition:color .15s ease,background .15s ease}.uploaded-item .file-remove-btn mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.uploaded-item .file-remove-btn:hover{color:var(--cc-sf-uploaded-remove-hover-color, #DC2626);background:var(--cc-sf-uploaded-remove-hover-bg, #FEF2F2)}.input-group{position:relative;display:flex;align-items:stretch;width:100%}.input-group .field-input{flex:1;border-radius:0}.input-group .field-input:first-child{border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-group .field-input:last-child{border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px)}.input-group.readonly .field-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);cursor:default;padding-right:3.5rem}.input-group.readonly .input-prefix,.input-group.readonly .input-suffix{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6)}.input-prefix,.input-suffix{display:flex;align-items:center;padding:0 .875rem;background-color:var(--cc-sf-input-bg, #FFFFFF);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);color:var(--cc-sf-hint-color, #6B7280);font-size:var(--cc-sf-input-font-size, .875rem);white-space:nowrap;-webkit-user-select:none;user-select:none}.input-prefix{border-right:none;border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-suffix{border-left:none;border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px);color:var(--cc-sf-chip-selected-bg, #3B82F6);font-weight:500}.readonly-icons{position:absolute;right:.875rem;top:50%;transform:translateY(-50%);display:flex;gap:8px;pointer-events:none}.readonly-icons mat-icon.lock-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem;opacity:.5;color:var(--cc-sf-hint-color, #6B7280)}.subfields-group-wrapper{margin-bottom:var(--cc-sf-grid-gap, 16px)}.subfields-group-wrapper .group-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:600;color:var(--cc-sf-label-color, #111827);margin-bottom:.75rem}.subfields-group-wrapper .group-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.subfields-group-wrapper .subfields-row{display:flex;align-items:flex-end;gap:12px;border-radius:var(--cc-sf-input-radius, 8px);transition:all .2s ease}.subfields-group-wrapper .subfields-row.is-invalid .subfield-item ::ng-deep .field-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.subfields-group-wrapper .subfields-row .subfield-item{flex:1;min-width:0}.subfields-group-wrapper .subfields-row .subfield-item ::ng-deep .field-label{font-size:.75rem!important;margin-bottom:4px!important;font-weight:500!important;color:var(--cc-sf-hint-color, #6B7280)!important}.subfields-group-wrapper .subfields-row .subfield-separator{margin-bottom:24px;font-weight:700;color:#94a3b8}.subfields-group-wrapper .subfields-group-error{display:block;margin-top:6px;font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}\n"] }]
|
|
4387
|
+
}], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: ExpressionService }, { type: i3.HttpClient }], propDecorators: { config: [{
|
|
4068
4388
|
type: Input
|
|
4069
4389
|
}], controller: [{
|
|
4070
4390
|
type: Input
|
|
4071
|
-
}],
|
|
4391
|
+
}], formGroup: [{
|
|
4392
|
+
type: Input
|
|
4393
|
+
}], allowMulti: [{
|
|
4072
4394
|
type: Input
|
|
4073
4395
|
}] } });
|
|
4074
4396
|
|
|
4075
4397
|
class FormSectionComponent {
|
|
4398
|
+
fb;
|
|
4076
4399
|
config;
|
|
4077
4400
|
controller;
|
|
4078
|
-
|
|
4079
|
-
|
|
4401
|
+
formGroup;
|
|
4402
|
+
/**
|
|
4403
|
+
* For allowMulti sections: the FormArray registered on the root formGroup.
|
|
4404
|
+
* Each element is a FormGroup representing one repeater instance.
|
|
4405
|
+
*/
|
|
4406
|
+
repeaterFormArray;
|
|
4407
|
+
/**
|
|
4408
|
+
* The key under which the FormArray is registered in the root formGroup.
|
|
4409
|
+
* Falls back to config.name or a generated key.
|
|
4410
|
+
*/
|
|
4411
|
+
get arrayKey() {
|
|
4412
|
+
return this.config.name || '__repeater__';
|
|
4413
|
+
}
|
|
4414
|
+
constructor(fb) {
|
|
4415
|
+
this.fb = fb;
|
|
4416
|
+
}
|
|
4417
|
+
ngOnInit() {
|
|
4080
4418
|
if (this.config.allowMulti) {
|
|
4081
|
-
this.
|
|
4419
|
+
this.repeaterFormArray = this.fb.array([]);
|
|
4420
|
+
this.formGroup.addControl(this.arrayKey, this.repeaterFormArray);
|
|
4421
|
+
// Start with one empty instance
|
|
4422
|
+
this.addInstance();
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
ngOnDestroy() {
|
|
4426
|
+
if (this.config.allowMulti && this.formGroup.contains(this.arrayKey)) {
|
|
4427
|
+
this.formGroup.removeControl(this.arrayKey);
|
|
4082
4428
|
}
|
|
4083
4429
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4430
|
+
// ── Repeater helpers ──────────────────────────────────────────────────────
|
|
4431
|
+
/** Creates a fresh FormGroup for one repeater instance */
|
|
4432
|
+
createInstanceGroup() {
|
|
4433
|
+
return this.fb.group({});
|
|
4434
|
+
}
|
|
4435
|
+
addInstance() {
|
|
4436
|
+
this.repeaterFormArray.push(this.createInstanceGroup());
|
|
4437
|
+
}
|
|
4438
|
+
removeInstance(index) {
|
|
4439
|
+
if (this.repeaterFormArray.length > 1) {
|
|
4440
|
+
this.repeaterFormArray.removeAt(index);
|
|
4087
4441
|
}
|
|
4088
4442
|
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4443
|
+
getInstanceGroup(index) {
|
|
4444
|
+
return this.repeaterFormArray.at(index);
|
|
4445
|
+
}
|
|
4446
|
+
get instanceGroups() {
|
|
4447
|
+
return this.repeaterFormArray.controls;
|
|
4448
|
+
}
|
|
4449
|
+
// ── Non-repeater: flat single FormGroup (root formGroup passed through) ──
|
|
4450
|
+
/** For non-allowMulti sections we simply pass the root formGroup down */
|
|
4451
|
+
get flatFormGroup() {
|
|
4452
|
+
return this.formGroup;
|
|
4453
|
+
}
|
|
4454
|
+
// ── Collect nested fields for a given child ───────────────────────────────
|
|
4455
|
+
/** Flatten a field tree to get all leaf fields (for ROW children etc.) */
|
|
4456
|
+
getFlatFields(fields) {
|
|
4457
|
+
const result = [];
|
|
4458
|
+
fields.forEach(f => {
|
|
4459
|
+
if (f.type === 'ROW' && f.children) {
|
|
4460
|
+
result.push(...this.getFlatFields(f.children));
|
|
4461
|
+
}
|
|
4462
|
+
else {
|
|
4463
|
+
result.push(f);
|
|
4464
|
+
}
|
|
4465
|
+
});
|
|
4466
|
+
return result;
|
|
4467
|
+
}
|
|
4468
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FormSectionComponent, deps: [{ token: i1$2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component });
|
|
4469
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: FormSectionComponent, isStandalone: false, selector: "lib-form-section", inputs: { config: "config", controller: "controller", formGroup: "formGroup" }, ngImport: i0, template: "<div class=\"form-section-container\">\r\n <h3 class=\"section-label\" *ngIf=\"config.label && !config.allowMulti\">{{ config.label }}</h3>\r\n\r\n <!-- \u2550\u2550 Repeater (allowMulti) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <ng-container *ngIf=\"config.allowMulti\">\r\n <h3 class=\"section-label\">{{ config.label }}</h3>\r\n\r\n <div *ngFor=\"let instanceGroup of instanceGroups; let i = index\" class=\"section-instance\">\r\n <!-- Instance header (remove button) -->\r\n <div class=\"section-header\" *ngIf=\"instanceGroups.length > 1\">\r\n <span class=\"section-number\">{{ config.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" (click)=\"removeInstance(i)\">\r\n <mat-icon>delete_outline</mat-icon> Remove\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Fields \u2013 each child rendered with the *instance* FormGroup -->\r\n <div class=\"section-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instanceGroup\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <lib-button [variant]=\"'primary'\" (click)=\"addInstance()\">\r\n <mat-icon>add</mat-icon> Add {{ config.label }}\r\n </lib-button>\r\n </ng-container>\r\n\r\n <!-- \u2550\u2550 Non-repeater (single instance) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <ng-container *ngIf=\"!config.allowMulti\">\r\n <div class=\"section-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"flatFormGroup\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n\r\n</div>", styles: [".form-section-container{margin-bottom:var(--cc-sf-section-gap, 20px)}.form-section-container .section-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.form-section-container .section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:12px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.form-section-container .section-header .section-number{font-weight:600;font-size:var(--cc-sf-instance-num-size, .8125rem);color:var(--cc-sf-instance-num-color, #4B5563)}.form-section-container .section-header .btn-remove{padding:6px 12px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:12px;transition:var(--cc-sf-btn-transition, all .2s ease)}.form-section-container .section-header .btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.form-section-container .section-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-section-container .section-fields.sf-grid{grid-template-columns:1fr}.form-section-container .section-fields.sf-grid .sf-col{grid-column:span 12!important}}.form-section-container .section-fields .section-instance{margin-bottom:12px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px);transition:var(--cc-sf-btn-transition, all .2s ease)}.form-section-container .section-fields .section-instance:last-of-type{margin-bottom:0}.form-section-container .btn-add-section{display:flex;align-items:center;justify-content:center;width:100%;padding:10px 20px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);margin-top:16px;transition:var(--cc-sf-btn-transition, all .2s ease)}.form-section-container .btn-add-section:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ButtonComponent, selector: "lib-button", inputs: ["variant", "type", "disabled", "width", "height", "borderRadius", "fontSize", "fontWeight", "backgroundColor", "color", "border", "icon", "labels"] }, { kind: "component", type: FormFieldComponent, selector: "lib-form-field", inputs: ["config", "controller", "formGroup", "allowMulti"] }] });
|
|
4091
4470
|
}
|
|
4092
4471
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FormSectionComponent, decorators: [{
|
|
4093
4472
|
type: Component,
|
|
4094
|
-
args: [{ selector: 'lib-form-section', standalone: false, template: "<div class=\"form-section-container\">\n <h3 class=\"section-label\" *ngIf=\"config.label\">{{ config.label }}</h3>\n \n <div *ngFor=\"let
|
|
4095
|
-
}], propDecorators: { config: [{
|
|
4473
|
+
args: [{ selector: 'lib-form-section', standalone: false, template: "<div class=\"form-section-container\">\r\n <h3 class=\"section-label\" *ngIf=\"config.label && !config.allowMulti\">{{ config.label }}</h3>\r\n\r\n <!-- \u2550\u2550 Repeater (allowMulti) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <ng-container *ngIf=\"config.allowMulti\">\r\n <h3 class=\"section-label\">{{ config.label }}</h3>\r\n\r\n <div *ngFor=\"let instanceGroup of instanceGroups; let i = index\" class=\"section-instance\">\r\n <!-- Instance header (remove button) -->\r\n <div class=\"section-header\" *ngIf=\"instanceGroups.length > 1\">\r\n <span class=\"section-number\">{{ config.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" (click)=\"removeInstance(i)\">\r\n <mat-icon>delete_outline</mat-icon> Remove\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Fields \u2013 each child rendered with the *instance* FormGroup -->\r\n <div class=\"section-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instanceGroup\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <lib-button [variant]=\"'primary'\" (click)=\"addInstance()\">\r\n <mat-icon>add</mat-icon> Add {{ config.label }}\r\n </lib-button>\r\n </ng-container>\r\n\r\n <!-- \u2550\u2550 Non-repeater (single instance) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <ng-container *ngIf=\"!config.allowMulti\">\r\n <div class=\"section-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"flatFormGroup\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n\r\n</div>", styles: [".form-section-container{margin-bottom:var(--cc-sf-section-gap, 20px)}.form-section-container .section-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.form-section-container .section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:12px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.form-section-container .section-header .section-number{font-weight:600;font-size:var(--cc-sf-instance-num-size, .8125rem);color:var(--cc-sf-instance-num-color, #4B5563)}.form-section-container .section-header .btn-remove{padding:6px 12px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:12px;transition:var(--cc-sf-btn-transition, all .2s ease)}.form-section-container .section-header .btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.form-section-container .section-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-section-container .section-fields.sf-grid{grid-template-columns:1fr}.form-section-container .section-fields.sf-grid .sf-col{grid-column:span 12!important}}.form-section-container .section-fields .section-instance{margin-bottom:12px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px);transition:var(--cc-sf-btn-transition, all .2s ease)}.form-section-container .section-fields .section-instance:last-of-type{margin-bottom:0}.form-section-container .btn-add-section{display:flex;align-items:center;justify-content:center;width:100%;padding:10px 20px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);margin-top:16px;transition:var(--cc-sf-btn-transition, all .2s ease)}.form-section-container .btn-add-section:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}\n"] }]
|
|
4474
|
+
}], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
|
|
4096
4475
|
type: Input
|
|
4097
4476
|
}], controller: [{
|
|
4098
4477
|
type: Input
|
|
4478
|
+
}], formGroup: [{
|
|
4479
|
+
type: Input
|
|
4099
4480
|
}] } });
|
|
4100
4481
|
|
|
4101
4482
|
class SmartFormComponent {
|
|
@@ -4106,6 +4487,12 @@ class SmartFormComponent {
|
|
|
4106
4487
|
formJson;
|
|
4107
4488
|
initialValues;
|
|
4108
4489
|
enableDraftAutoSave = false;
|
|
4490
|
+
/** Flat i18n labels map passed by the consuming app.
|
|
4491
|
+
* After JSON parse the schema is walked and every string value that
|
|
4492
|
+
* matches a key in this map is replaced with the translated value.
|
|
4493
|
+
* Mirrors the pattern used by ConfigurableFormComponent + translateConfig.
|
|
4494
|
+
*/
|
|
4495
|
+
labels = {};
|
|
4109
4496
|
submit = new EventEmitter();
|
|
4110
4497
|
draftSave = new EventEmitter();
|
|
4111
4498
|
formSchema;
|
|
@@ -4121,23 +4508,41 @@ class SmartFormComponent {
|
|
|
4121
4508
|
this.http = http;
|
|
4122
4509
|
}
|
|
4123
4510
|
ngOnInit() {
|
|
4511
|
+
this.parseFormJson();
|
|
4124
4512
|
if (this.initialValues) {
|
|
4125
4513
|
this.controller.initialize(this.initialValues);
|
|
4126
4514
|
}
|
|
4127
|
-
this.parseFormJson();
|
|
4128
4515
|
}
|
|
4129
4516
|
ngOnChanges(changes) {
|
|
4130
4517
|
if (changes['formJson'] && !changes['formJson'].isFirstChange()) {
|
|
4131
4518
|
this.parseFormJson();
|
|
4132
4519
|
}
|
|
4520
|
+
// Re-translate if labels arrive after the JSON was already parsed
|
|
4521
|
+
if (changes['labels'] && !changes['labels'].isFirstChange() && this.formSchema) {
|
|
4522
|
+
this.parseFormJson();
|
|
4523
|
+
}
|
|
4133
4524
|
}
|
|
4134
4525
|
ngOnDestroy() {
|
|
4135
4526
|
this.controller.destroy();
|
|
4136
4527
|
}
|
|
4137
4528
|
parseFormJson() {
|
|
4529
|
+
if (!this.formJson) {
|
|
4530
|
+
return;
|
|
4531
|
+
} // guard: labels may arrive before formJson
|
|
4138
4532
|
try {
|
|
4139
4533
|
const jsonData = JSON.parse(this.formJson);
|
|
4140
4534
|
this.formSchema = jsonData;
|
|
4535
|
+
// Push token from configJSON into the shared controller so all child
|
|
4536
|
+
// components (form-field, etc.) can read it without @Input prop-drilling.
|
|
4537
|
+
this.controller.token = this.formSchema.token;
|
|
4538
|
+
this.controller.tokenHeader = this.formSchema.tokenHeader;
|
|
4539
|
+
// Translate i18n keys before rendering
|
|
4540
|
+
if (this.labels && Object.keys(this.labels).length) {
|
|
4541
|
+
SmartFormTranslationUtils.translateSchema(this.formSchema, this.labels);
|
|
4542
|
+
}
|
|
4543
|
+
// Share translated labels with child components via controller
|
|
4544
|
+
this.controller.labels = this.labels;
|
|
4545
|
+
this.controller.actionLabels = this.formSchema.labels;
|
|
4141
4546
|
this.isStepper = this.formSchema.formType === 'STEPPER';
|
|
4142
4547
|
this.fieldList = this.isStepper
|
|
4143
4548
|
? this.formSchema.stepperConfig?.children || []
|
|
@@ -4154,21 +4559,34 @@ class SmartFormComponent {
|
|
|
4154
4559
|
}
|
|
4155
4560
|
collectFields(fields) {
|
|
4156
4561
|
fields.forEach(field => {
|
|
4157
|
-
|
|
4158
|
-
|
|
4562
|
+
// Flat leaf fields: seed controller with default values
|
|
4563
|
+
if (field.name && field.type !== 'GROUP' && field.type !== 'ROW') {
|
|
4564
|
+
// Check if initialValues already has a value for this field
|
|
4565
|
+
const existingValue = this.initialValues?.[field.name];
|
|
4566
|
+
const value = existingValue !== undefined
|
|
4567
|
+
? existingValue
|
|
4568
|
+
: (field.defaultValue !== undefined ? field.defaultValue : null);
|
|
4159
4569
|
this.controller.updateField(field.name, value);
|
|
4160
4570
|
}
|
|
4161
|
-
|
|
4571
|
+
// Recurse into ROW children
|
|
4572
|
+
if (field.type === 'ROW' && field.children?.length) {
|
|
4162
4573
|
this.collectFields(field.children);
|
|
4163
4574
|
}
|
|
4575
|
+
// GROUP children will be handled dynamically by FormFieldComponent
|
|
4164
4576
|
});
|
|
4165
4577
|
}
|
|
4578
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
4579
|
+
// Submit
|
|
4580
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
4166
4581
|
handleSubmit() {
|
|
4167
4582
|
if (this.isStepper && this.currentStep < this.fieldList.length - 1) {
|
|
4168
|
-
this.
|
|
4583
|
+
if (this.validate())
|
|
4584
|
+
this.nextStep();
|
|
4169
4585
|
return;
|
|
4170
4586
|
}
|
|
4171
|
-
|
|
4587
|
+
if (!this.validate())
|
|
4588
|
+
return;
|
|
4589
|
+
const formData = this.collectFormData();
|
|
4172
4590
|
this.isLoading = true;
|
|
4173
4591
|
if (this.formSchema.submitConfig?.apiUrl) {
|
|
4174
4592
|
this.submitToApi(formData);
|
|
@@ -4178,54 +4596,116 @@ class SmartFormComponent {
|
|
|
4178
4596
|
this.isLoading = false;
|
|
4179
4597
|
}
|
|
4180
4598
|
}
|
|
4599
|
+
/**
|
|
4600
|
+
* Recursively extracts values from the formGroup, converting FormArrays to
|
|
4601
|
+
* arrays of objects so repeater groups come out as expected.
|
|
4602
|
+
*/
|
|
4603
|
+
collectFormData() {
|
|
4604
|
+
return this.extractGroupValue(this.formGroup);
|
|
4605
|
+
}
|
|
4606
|
+
extractGroupValue(group) {
|
|
4607
|
+
const result = {};
|
|
4608
|
+
Object.keys(group.controls).forEach(key => {
|
|
4609
|
+
const ctrl = group.get(key);
|
|
4610
|
+
if (ctrl instanceof FormArray) {
|
|
4611
|
+
// Repeater → array of objects
|
|
4612
|
+
result[key] = ctrl.controls.map(fg => this.extractGroupValue(fg));
|
|
4613
|
+
}
|
|
4614
|
+
else if (ctrl instanceof FormGroup) {
|
|
4615
|
+
// Nested single group → nested object
|
|
4616
|
+
result[key] = this.extractGroupValue(ctrl);
|
|
4617
|
+
}
|
|
4618
|
+
else {
|
|
4619
|
+
result[key] = ctrl?.value ?? null;
|
|
4620
|
+
}
|
|
4621
|
+
});
|
|
4622
|
+
return result;
|
|
4623
|
+
}
|
|
4624
|
+
validate() {
|
|
4625
|
+
if (this.formGroup.invalid) {
|
|
4626
|
+
this.formGroup.markAllAsTouched();
|
|
4627
|
+
this.scrollToFirstInvalidControl();
|
|
4628
|
+
return false;
|
|
4629
|
+
}
|
|
4630
|
+
return true;
|
|
4631
|
+
}
|
|
4632
|
+
scrollToFirstInvalidControl() {
|
|
4633
|
+
setTimeout(() => {
|
|
4634
|
+
const firstInvalidControl = document.querySelector('.is-invalid, .ng-invalid.ng-touched');
|
|
4635
|
+
if (firstInvalidControl) {
|
|
4636
|
+
firstInvalidControl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
4637
|
+
}
|
|
4638
|
+
}, 100);
|
|
4639
|
+
}
|
|
4181
4640
|
submitToApi(formData) {
|
|
4182
4641
|
const config = this.formSchema.submitConfig;
|
|
4183
4642
|
const method = config.method || 'POST';
|
|
4184
|
-
this.
|
|
4185
|
-
|
|
4186
|
-
next:
|
|
4643
|
+
const headers = this.getHeaders();
|
|
4644
|
+
this.http.request(method, config.apiUrl, { body: formData, headers }).subscribe({
|
|
4645
|
+
next: response => {
|
|
4187
4646
|
alert(config.successMessage || 'Form submitted successfully');
|
|
4188
4647
|
this.submit.emit(response);
|
|
4189
4648
|
this.isLoading = false;
|
|
4190
4649
|
},
|
|
4191
|
-
error:
|
|
4650
|
+
error: err => {
|
|
4192
4651
|
alert(config.errorMessage || 'Failed to submit form');
|
|
4193
4652
|
console.error('Submit error:', err);
|
|
4194
4653
|
this.isLoading = false;
|
|
4195
4654
|
}
|
|
4196
4655
|
});
|
|
4197
4656
|
}
|
|
4657
|
+
/** Builds HttpHeaders from the token stored in the controller (sourced from configJSON). */
|
|
4658
|
+
getHeaders() {
|
|
4659
|
+
let headers = new HttpHeaders();
|
|
4660
|
+
if (this.controller.token) {
|
|
4661
|
+
const headerName = this.controller.tokenHeader || 'Authorization';
|
|
4662
|
+
headers = headers.set(headerName, this.controller.token);
|
|
4663
|
+
}
|
|
4664
|
+
return headers;
|
|
4665
|
+
}
|
|
4666
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
4667
|
+
// Stepper
|
|
4668
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
4198
4669
|
nextStep() {
|
|
4199
|
-
if (this.currentStep < this.fieldList.length - 1)
|
|
4670
|
+
if (this.currentStep < this.fieldList.length - 1)
|
|
4200
4671
|
this.currentStep++;
|
|
4201
|
-
}
|
|
4202
4672
|
}
|
|
4203
4673
|
previousStep() {
|
|
4204
|
-
if (this.currentStep > 0)
|
|
4674
|
+
if (this.currentStep > 0)
|
|
4205
4675
|
this.currentStep--;
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
4208
|
-
get canGoNext() {
|
|
4209
|
-
return this.currentStep < this.fieldList.length - 1;
|
|
4210
|
-
}
|
|
4211
|
-
get canGoPrevious() {
|
|
4212
|
-
return this.currentStep > 0;
|
|
4213
4676
|
}
|
|
4677
|
+
get canGoNext() { return this.currentStep < this.fieldList.length - 1; }
|
|
4678
|
+
get canGoPrevious() { return this.currentStep > 0; }
|
|
4214
4679
|
get currentStepConfig() {
|
|
4215
4680
|
return this.isStepper ? this.fieldList[this.currentStep] : undefined;
|
|
4216
4681
|
}
|
|
4682
|
+
// ── Action Labels ──────────────────────────────────────────────────────────
|
|
4683
|
+
get nextLabel() {
|
|
4684
|
+
const key = this.formSchema?.labels?.nextLabel || 'Next';
|
|
4685
|
+
return this.labels[key] || key;
|
|
4686
|
+
}
|
|
4687
|
+
get submitLabel() {
|
|
4688
|
+
const key = this.formSchema?.labels?.submitLabel || 'Submit';
|
|
4689
|
+
return this.labels[key] || key;
|
|
4690
|
+
}
|
|
4691
|
+
get previousLabel() {
|
|
4692
|
+
const key = this.formSchema?.labels?.previousLabel || 'Previous';
|
|
4693
|
+
return this.labels[key] || key;
|
|
4694
|
+
}
|
|
4217
4695
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartFormComponent, deps: [{ token: i1$2.FormBuilder }, { token: SmartFormController }, { token: ExpressionService }, { token: i3.HttpClient }], target: i0.ɵɵFactoryTarget.Component });
|
|
4218
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SmartFormComponent, isStandalone: false, selector: "lib-smart-form", inputs: { formJson: "formJson", initialValues: "initialValues", enableDraftAutoSave: "enableDraftAutoSave" }, outputs: { submit: "submit", draftSave: "draftSave" }, providers: [SmartFormController], usesOnChanges: true, ngImport: i0, template: "<div class=\"smart-form-container\">\n <div class=\"smart-form-wrapper\" *ngIf=\"formSchema\">\n <!-- Form Header -->\n <div class=\"form-header\" *ngIf=\"formSchema.showTitle !== false\">\n <h2 class=\"form-title\">{{ formSchema.label }}</h2>\n <p class=\"form-description\" *ngIf=\"formSchema.description\">{{ formSchema.description }}</p>\n </div>\n\n <!-- Stepper Navigation -->\n <div class=\"stepper-nav\" *ngIf=\"isStepper && formSchema.stepperConfig?.showStep !== false\">\n <div class=\"stepper-steps\" [class.horizontal]=\"formSchema.stepperConfig?.isHorizontal !== false\">\n <div
|
|
4696
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SmartFormComponent, isStandalone: false, selector: "lib-smart-form", inputs: { formJson: "formJson", initialValues: "initialValues", enableDraftAutoSave: "enableDraftAutoSave", labels: "labels" }, outputs: { submit: "submit", draftSave: "draftSave" }, providers: [SmartFormController], usesOnChanges: true, ngImport: i0, template: "<div class=\"smart-form-container\">\r\n <div class=\"smart-form-wrapper\" *ngIf=\"formSchema\">\r\n <!-- Form Header -->\r\n <div class=\"form-header\" *ngIf=\"formSchema.showTitle !== false\">\r\n <h2 class=\"form-title\">{{ formSchema.label }}</h2>\r\n <p class=\"form-description\" *ngIf=\"formSchema.description\">{{ formSchema.description }}</p>\r\n </div>\r\n\r\n <!-- Stepper Navigation -->\r\n <div class=\"stepper-nav\" *ngIf=\"isStepper && formSchema.stepperConfig?.showStep !== false\">\r\n <div class=\"stepper-steps\" [class.horizontal]=\"formSchema.stepperConfig?.isHorizontal !== false\">\r\n <div *ngFor=\"let step of fieldList; let i = index\" class=\"stepper-step\" [class.active]=\"i === currentStep\"\r\n [class.completed]=\"i < currentStep\">\r\n <div class=\"step-number\">{{ i + 1 }}</div>\r\n <div class=\"step-label\">{{ step.sectionConfig?.label || 'Step ' + (i + 1) }}</div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Form Content -->\r\n <form [formGroup]=\"formGroup\" class=\"smart-form\">\r\n <!-- Section Form -->\r\n <div *ngIf=\"!isStepper && formSchema.sectionConfig\" class=\"form-section\">\r\n <lib-form-section [config]=\"formSchema.sectionConfig\" [controller]=\"controller\" [formGroup]=\"formGroup\">\r\n </lib-form-section>\r\n </div>\r\n\r\n <!-- Stepper Form -->\r\n <div *ngIf=\"isStepper && currentStepConfig\" class=\"form-stepper\">\r\n <lib-form-section [config]=\"currentStepConfig.sectionConfig!\" [controller]=\"controller\" [formGroup]=\"formGroup\">\r\n </lib-form-section>\r\n </div>\r\n </form>\r\n\r\n <!-- Form Actions -->\r\n <div class=\"form-actions\" *ngIf=\"formSchema.showActions !== false\">\r\n <lib-button *ngIf=\"isStepper && canGoPrevious\" [variant]=\"'outline'\" (click)=\"previousStep()\">\r\n {{ previousLabel }}\r\n </lib-button>\r\n\r\n <lib-button [variant]=\"'warning'\" [disabled]=\"isLoading\" (click)=\"handleSubmit()\">\r\n {{ isStepper && canGoNext ? nextLabel : submitLabel }}\r\n </lib-button>\r\n </div>\r\n </div>\r\n</div>", styles: [".smart-form-container{width:100%;max-width:var(--cc-sf-form-max-width, 1200px);margin:0 auto;padding:var(--cc-sf-form-padding, 24px);font-family:var(--cc-sf-font-family, \"Inter\", sans-serif)}.smart-form-wrapper{background:var(--cc-sf-form-bg, #ffffff);border-radius:var(--cc-sf-form-border-radius, 12px);border:var(--cc-sf-form-border, none);box-shadow:var(--cc-sf-form-shadow, 0 1px 3px rgba(0, 0, 0, .06))}.form-header{margin-bottom:24px}.form-header .form-title{font-size:var(--cc-sf-form-title-size, 1.5rem);font-weight:var(--cc-sf-form-title-weight, 700);color:var(--cc-sf-form-title-color, #111827);margin:0 0 8px;line-height:1.25}.form-header .form-description{font-size:var(--cc-sf-form-desc-size, .875rem);color:var(--cc-sf-form-desc-color, #6B7280);margin:0}.stepper-nav{margin-bottom:32px}.stepper-nav .stepper-steps{display:flex;gap:16px}.stepper-nav .stepper-steps.horizontal{flex-direction:row;justify-content:space-between}.stepper-nav .stepper-steps:not(.horizontal){flex-direction:column}.stepper-nav .stepper-step{display:flex;align-items:center;gap:12px;flex:1;position:relative}.stepper-nav .stepper-step:not(:last-child):after{content:\"\";position:absolute;top:calc(var(--cc-sf-step-number-size, 40px) / 2);left:calc(100% + 8px);width:calc(100% - 40px);height:2px;background:var(--cc-sf-step-connector-color, #E5E7EB);transition:background var(--cc-sf-btn-transition, .2s ease)}.stepper-nav .stepper-step.completed:after{background:var(--cc-sf-step-connector-done, #22C55E)}.stepper-nav .stepper-step .step-number{width:var(--cc-sf-step-number-size, 40px);height:var(--cc-sf-step-number-size, 40px);min-width:var(--cc-sf-step-number-size, 40px);border-radius:50%;background:var(--cc-sf-step-number-bg, #E5E7EB);color:var(--cc-sf-step-number-color, #6B7280);display:flex;align-items:center;justify-content:center;font-size:var(--cc-sf-step-number-font-size, .875rem);font-weight:var(--cc-sf-step-number-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease)}.stepper-nav .stepper-step .step-label{font-size:var(--cc-sf-step-label-size, .875rem);color:var(--cc-sf-step-label-color, #6B7280);font-weight:var(--cc-sf-step-label-weight, 500);transition:var(--cc-sf-btn-transition, all .2s ease)}.stepper-nav .stepper-step.active .step-number{background:var(--cc-sf-step-active-bg, #3B82F6);color:var(--cc-sf-step-active-color, #ffffff)}.stepper-nav .stepper-step.active .step-label{color:var(--cc-sf-step-active-label, #1D4ED8);font-weight:var(--cc-sf-step-active-label-weight, 700)}.stepper-nav .stepper-step.completed .step-number{background:var(--cc-sf-step-done-bg, #22C55E);color:var(--cc-sf-step-done-color, #ffffff)}.smart-form{margin-bottom:24px}.form-actions{display:flex;justify-content:flex-end;gap:var(--cc-sf-actions-gap, 12px);padding:var(--cc-sf-actions-padding, 20px 0 0);border-top:var(--cc-sf-actions-border, 1px solid #E5E7EB)}.form-actions .btn{font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);border:none;cursor:pointer;transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit);line-height:1.5}.form-actions .btn.btn-primary{background:var(--cc-sf-btn-primary-bg, #3B82F6);color:var(--cc-sf-btn-primary-color, #ffffff);border-radius:var(--cc-sf-btn-primary-radius, 8px);padding:var(--cc-sf-btn-primary-padding, .625rem 1.5rem)}.form-actions .btn.btn-primary:hover:not(:disabled){background:var(--cc-sf-btn-primary-hover-bg, #2563EB)}.form-actions .btn.btn-primary:disabled{opacity:var(--cc-sf-btn-disabled-opacity, .55);cursor:not-allowed}.form-actions .btn.btn-secondary{background:var(--cc-sf-btn-secondary-bg, #F3F4F6);color:var(--cc-sf-btn-secondary-color, #374151);border-radius:var(--cc-sf-btn-secondary-radius, 8px);padding:var(--cc-sf-btn-secondary-padding, .625rem 1.5rem)}.form-actions .btn.btn-secondary:hover{background:var(--cc-sf-btn-secondary-hover-bg, #E5E7EB)}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: ButtonComponent, selector: "lib-button", inputs: ["variant", "type", "disabled", "width", "height", "borderRadius", "fontSize", "fontWeight", "backgroundColor", "color", "border", "icon", "labels"] }, { kind: "component", type: FormSectionComponent, selector: "lib-form-section", inputs: ["config", "controller", "formGroup"] }] });
|
|
4219
4697
|
}
|
|
4220
4698
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartFormComponent, decorators: [{
|
|
4221
4699
|
type: Component,
|
|
4222
|
-
args: [{ selector: 'lib-smart-form', providers: [SmartFormController], standalone: false, template: "<div class=\"smart-form-container\">\n <div class=\"smart-form-wrapper\" *ngIf=\"formSchema\">\n <!-- Form Header -->\n <div class=\"form-header\" *ngIf=\"formSchema.showTitle !== false\">\n <h2 class=\"form-title\">{{ formSchema.label }}</h2>\n <p class=\"form-description\" *ngIf=\"formSchema.description\">{{ formSchema.description }}</p>\n </div>\n\n <!-- Stepper Navigation -->\n <div class=\"stepper-nav\" *ngIf=\"isStepper && formSchema.stepperConfig?.showStep !== false\">\n <div class=\"stepper-steps\" [class.horizontal]=\"formSchema.stepperConfig?.isHorizontal !== false\">\n <div
|
|
4700
|
+
args: [{ selector: 'lib-smart-form', providers: [SmartFormController], standalone: false, template: "<div class=\"smart-form-container\">\r\n <div class=\"smart-form-wrapper\" *ngIf=\"formSchema\">\r\n <!-- Form Header -->\r\n <div class=\"form-header\" *ngIf=\"formSchema.showTitle !== false\">\r\n <h2 class=\"form-title\">{{ formSchema.label }}</h2>\r\n <p class=\"form-description\" *ngIf=\"formSchema.description\">{{ formSchema.description }}</p>\r\n </div>\r\n\r\n <!-- Stepper Navigation -->\r\n <div class=\"stepper-nav\" *ngIf=\"isStepper && formSchema.stepperConfig?.showStep !== false\">\r\n <div class=\"stepper-steps\" [class.horizontal]=\"formSchema.stepperConfig?.isHorizontal !== false\">\r\n <div *ngFor=\"let step of fieldList; let i = index\" class=\"stepper-step\" [class.active]=\"i === currentStep\"\r\n [class.completed]=\"i < currentStep\">\r\n <div class=\"step-number\">{{ i + 1 }}</div>\r\n <div class=\"step-label\">{{ step.sectionConfig?.label || 'Step ' + (i + 1) }}</div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Form Content -->\r\n <form [formGroup]=\"formGroup\" class=\"smart-form\">\r\n <!-- Section Form -->\r\n <div *ngIf=\"!isStepper && formSchema.sectionConfig\" class=\"form-section\">\r\n <lib-form-section [config]=\"formSchema.sectionConfig\" [controller]=\"controller\" [formGroup]=\"formGroup\">\r\n </lib-form-section>\r\n </div>\r\n\r\n <!-- Stepper Form -->\r\n <div *ngIf=\"isStepper && currentStepConfig\" class=\"form-stepper\">\r\n <lib-form-section [config]=\"currentStepConfig.sectionConfig!\" [controller]=\"controller\" [formGroup]=\"formGroup\">\r\n </lib-form-section>\r\n </div>\r\n </form>\r\n\r\n <!-- Form Actions -->\r\n <div class=\"form-actions\" *ngIf=\"formSchema.showActions !== false\">\r\n <lib-button *ngIf=\"isStepper && canGoPrevious\" [variant]=\"'outline'\" (click)=\"previousStep()\">\r\n {{ previousLabel }}\r\n </lib-button>\r\n\r\n <lib-button [variant]=\"'warning'\" [disabled]=\"isLoading\" (click)=\"handleSubmit()\">\r\n {{ isStepper && canGoNext ? nextLabel : submitLabel }}\r\n </lib-button>\r\n </div>\r\n </div>\r\n</div>", styles: [".smart-form-container{width:100%;max-width:var(--cc-sf-form-max-width, 1200px);margin:0 auto;padding:var(--cc-sf-form-padding, 24px);font-family:var(--cc-sf-font-family, \"Inter\", sans-serif)}.smart-form-wrapper{background:var(--cc-sf-form-bg, #ffffff);border-radius:var(--cc-sf-form-border-radius, 12px);border:var(--cc-sf-form-border, none);box-shadow:var(--cc-sf-form-shadow, 0 1px 3px rgba(0, 0, 0, .06))}.form-header{margin-bottom:24px}.form-header .form-title{font-size:var(--cc-sf-form-title-size, 1.5rem);font-weight:var(--cc-sf-form-title-weight, 700);color:var(--cc-sf-form-title-color, #111827);margin:0 0 8px;line-height:1.25}.form-header .form-description{font-size:var(--cc-sf-form-desc-size, .875rem);color:var(--cc-sf-form-desc-color, #6B7280);margin:0}.stepper-nav{margin-bottom:32px}.stepper-nav .stepper-steps{display:flex;gap:16px}.stepper-nav .stepper-steps.horizontal{flex-direction:row;justify-content:space-between}.stepper-nav .stepper-steps:not(.horizontal){flex-direction:column}.stepper-nav .stepper-step{display:flex;align-items:center;gap:12px;flex:1;position:relative}.stepper-nav .stepper-step:not(:last-child):after{content:\"\";position:absolute;top:calc(var(--cc-sf-step-number-size, 40px) / 2);left:calc(100% + 8px);width:calc(100% - 40px);height:2px;background:var(--cc-sf-step-connector-color, #E5E7EB);transition:background var(--cc-sf-btn-transition, .2s ease)}.stepper-nav .stepper-step.completed:after{background:var(--cc-sf-step-connector-done, #22C55E)}.stepper-nav .stepper-step .step-number{width:var(--cc-sf-step-number-size, 40px);height:var(--cc-sf-step-number-size, 40px);min-width:var(--cc-sf-step-number-size, 40px);border-radius:50%;background:var(--cc-sf-step-number-bg, #E5E7EB);color:var(--cc-sf-step-number-color, #6B7280);display:flex;align-items:center;justify-content:center;font-size:var(--cc-sf-step-number-font-size, .875rem);font-weight:var(--cc-sf-step-number-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease)}.stepper-nav .stepper-step .step-label{font-size:var(--cc-sf-step-label-size, .875rem);color:var(--cc-sf-step-label-color, #6B7280);font-weight:var(--cc-sf-step-label-weight, 500);transition:var(--cc-sf-btn-transition, all .2s ease)}.stepper-nav .stepper-step.active .step-number{background:var(--cc-sf-step-active-bg, #3B82F6);color:var(--cc-sf-step-active-color, #ffffff)}.stepper-nav .stepper-step.active .step-label{color:var(--cc-sf-step-active-label, #1D4ED8);font-weight:var(--cc-sf-step-active-label-weight, 700)}.stepper-nav .stepper-step.completed .step-number{background:var(--cc-sf-step-done-bg, #22C55E);color:var(--cc-sf-step-done-color, #ffffff)}.smart-form{margin-bottom:24px}.form-actions{display:flex;justify-content:flex-end;gap:var(--cc-sf-actions-gap, 12px);padding:var(--cc-sf-actions-padding, 20px 0 0);border-top:var(--cc-sf-actions-border, 1px solid #E5E7EB)}.form-actions .btn{font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);border:none;cursor:pointer;transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit);line-height:1.5}.form-actions .btn.btn-primary{background:var(--cc-sf-btn-primary-bg, #3B82F6);color:var(--cc-sf-btn-primary-color, #ffffff);border-radius:var(--cc-sf-btn-primary-radius, 8px);padding:var(--cc-sf-btn-primary-padding, .625rem 1.5rem)}.form-actions .btn.btn-primary:hover:not(:disabled){background:var(--cc-sf-btn-primary-hover-bg, #2563EB)}.form-actions .btn.btn-primary:disabled{opacity:var(--cc-sf-btn-disabled-opacity, .55);cursor:not-allowed}.form-actions .btn.btn-secondary{background:var(--cc-sf-btn-secondary-bg, #F3F4F6);color:var(--cc-sf-btn-secondary-color, #374151);border-radius:var(--cc-sf-btn-secondary-radius, 8px);padding:var(--cc-sf-btn-secondary-padding, .625rem 1.5rem)}.form-actions .btn.btn-secondary:hover{background:var(--cc-sf-btn-secondary-hover-bg, #E5E7EB)}\n"] }]
|
|
4223
4701
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: SmartFormController }, { type: ExpressionService }, { type: i3.HttpClient }], propDecorators: { formJson: [{
|
|
4224
4702
|
type: Input
|
|
4225
4703
|
}], initialValues: [{
|
|
4226
4704
|
type: Input
|
|
4227
4705
|
}], enableDraftAutoSave: [{
|
|
4228
4706
|
type: Input
|
|
4707
|
+
}], labels: [{
|
|
4708
|
+
type: Input
|
|
4229
4709
|
}], submit: [{
|
|
4230
4710
|
type: Output
|
|
4231
4711
|
}], draftSave: [{
|
|
@@ -4238,12 +4718,16 @@ class SmartFormModule {
|
|
|
4238
4718
|
FormSectionComponent,
|
|
4239
4719
|
FormFieldComponent], imports: [CommonModule,
|
|
4240
4720
|
ReactiveFormsModule,
|
|
4241
|
-
FormsModule
|
|
4721
|
+
FormsModule,
|
|
4722
|
+
MaterialModule,
|
|
4723
|
+
ButtonModule], exports: [SmartFormComponent] });
|
|
4242
4724
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartFormModule, providers: [
|
|
4243
4725
|
ExpressionService
|
|
4244
4726
|
], imports: [CommonModule,
|
|
4245
4727
|
ReactiveFormsModule,
|
|
4246
|
-
FormsModule
|
|
4728
|
+
FormsModule,
|
|
4729
|
+
MaterialModule,
|
|
4730
|
+
ButtonModule] });
|
|
4247
4731
|
}
|
|
4248
4732
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartFormModule, decorators: [{
|
|
4249
4733
|
type: NgModule,
|
|
@@ -4256,7 +4740,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
4256
4740
|
imports: [
|
|
4257
4741
|
CommonModule,
|
|
4258
4742
|
ReactiveFormsModule,
|
|
4259
|
-
FormsModule
|
|
4743
|
+
FormsModule,
|
|
4744
|
+
MaterialModule,
|
|
4745
|
+
ButtonModule
|
|
4260
4746
|
],
|
|
4261
4747
|
exports: [
|
|
4262
4748
|
SmartFormComponent
|
|
@@ -5264,17 +5750,93 @@ class SmartTableComponent {
|
|
|
5264
5750
|
if (!confirm(message))
|
|
5265
5751
|
return;
|
|
5266
5752
|
}
|
|
5267
|
-
|
|
5753
|
+
if (action.apiUrl) {
|
|
5754
|
+
this.executeApiAction(action, row);
|
|
5755
|
+
}
|
|
5756
|
+
else {
|
|
5757
|
+
this.action.emit({ action, row });
|
|
5758
|
+
}
|
|
5268
5759
|
}
|
|
5269
5760
|
else {
|
|
5270
5761
|
this.action.emit({ action, row });
|
|
5271
5762
|
}
|
|
5272
5763
|
}
|
|
5764
|
+
onActionItemClick(item, row, event) {
|
|
5765
|
+
event.stopPropagation();
|
|
5766
|
+
if (item.type === 'callback' && item.callback) {
|
|
5767
|
+
item.callback(row);
|
|
5768
|
+
}
|
|
5769
|
+
if (item.type === 'route' && item.route) {
|
|
5770
|
+
const url = this.replaceParams(item.route, row);
|
|
5771
|
+
this.router.navigateByUrl(url);
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
if (item.type === 'api') {
|
|
5775
|
+
if (item.confirmationNeeded) {
|
|
5776
|
+
const message = item.confirmationMessage || this.config.labels?.defaultConfirmationMessage || 'Are you sure?';
|
|
5777
|
+
if (!confirm(message))
|
|
5778
|
+
return;
|
|
5779
|
+
}
|
|
5780
|
+
if (item.apiUrl) {
|
|
5781
|
+
this.executeApiAction(item, row);
|
|
5782
|
+
}
|
|
5783
|
+
else {
|
|
5784
|
+
this.action.emit({ action: item, row });
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
else {
|
|
5788
|
+
this.action.emit({ action: item, row });
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5273
5791
|
onTopAction(action) {
|
|
5274
5792
|
if (action.type === 'callback' && action.callback) {
|
|
5275
5793
|
action.callback(null); // No row for top action
|
|
5276
5794
|
}
|
|
5277
|
-
|
|
5795
|
+
if (action.type === 'route' && action.route) {
|
|
5796
|
+
// Since it's a top action, replaceParams with an empty object or handle statically
|
|
5797
|
+
const url = this.replaceParams(action.route, {});
|
|
5798
|
+
this.router.navigateByUrl(url);
|
|
5799
|
+
return;
|
|
5800
|
+
}
|
|
5801
|
+
if (action.type === 'api') {
|
|
5802
|
+
if (action.confirmationNeeded) {
|
|
5803
|
+
const message = action.confirmationMessage || this.config.labels?.defaultConfirmationMessage || 'Are you sure?';
|
|
5804
|
+
if (!confirm(message))
|
|
5805
|
+
return;
|
|
5806
|
+
}
|
|
5807
|
+
if (action.apiUrl) {
|
|
5808
|
+
this.executeApiAction(action, null);
|
|
5809
|
+
}
|
|
5810
|
+
else {
|
|
5811
|
+
this.topAction.emit(action);
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
else {
|
|
5815
|
+
this.topAction.emit(action);
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
executeApiAction(action, row) {
|
|
5819
|
+
if (!action.apiUrl)
|
|
5820
|
+
return;
|
|
5821
|
+
const url = row ? this.replaceParams(action.apiUrl, row) : action.apiUrl;
|
|
5822
|
+
const method = action.apiMethod || 'POST';
|
|
5823
|
+
const headers = this.getHeaders();
|
|
5824
|
+
const body = row ? row : {};
|
|
5825
|
+
this.loading = true;
|
|
5826
|
+
this.http.request(method, url, { body, headers }).pipe(finalize(() => this.loading = false)).subscribe({
|
|
5827
|
+
next: () => {
|
|
5828
|
+
this.loadData(); // reload on success
|
|
5829
|
+
if (row) {
|
|
5830
|
+
this.action.emit({ action, row });
|
|
5831
|
+
}
|
|
5832
|
+
else {
|
|
5833
|
+
this.topAction.emit(action);
|
|
5834
|
+
}
|
|
5835
|
+
},
|
|
5836
|
+
error: (err) => {
|
|
5837
|
+
console.error('API Action Error', err);
|
|
5838
|
+
}
|
|
5839
|
+
});
|
|
5278
5840
|
}
|
|
5279
5841
|
// --- Selection ---
|
|
5280
5842
|
onSelectAll(event) {
|
|
@@ -5450,11 +6012,11 @@ class SmartTableComponent {
|
|
|
5450
6012
|
return headers;
|
|
5451
6013
|
}
|
|
5452
6014
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartTableComponent, deps: [{ token: i3.HttpClient }, { token: i1$1.Router }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
|
|
5453
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SmartTableComponent, isStandalone: false, selector: "lib-smart-table", inputs: { config: "config" }, outputs: { action: "action", topAction: "topAction", filterChange: "filterChange", rowSelect: "rowSelect", columnClick: "columnClick" }, viewQueries: [{ propertyName: "stickyHeaders", predicate: ["stickyHeader"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"smart-table-wrapper\">\n <!-- Top Toolbar -->\n <div class=\"st-toolbar\" *ngIf=\"config.searchConfig?.enabled || (config.filters && config.filters.length > 0) || (config.topBarButtons && config.topBarButtons.length > 0)\">\n \n <!-- Search -->\n <div class=\"st-search\" *ngIf=\"config.searchConfig?.enabled\">\n <i class=\"fa fa-search\"></i>\n <input type=\"text\" [placeholder]=\"config.labels?.searchPlaceholder || 'Search'\" (input)=\"onSearch($event)\">\n </div>\n\n <!-- Filters -->\n <div class=\"st-filters\" *ngIf=\"config.filters\">\n <div class=\"st-filter-item\" *ngFor=\"let filter of config.filters\">\n <select (change)=\"onFilterChange(filter.key, $event)\">\n <option value=\"\">{{ filter.label }}</option>\n <option *ngFor=\"let opt of filter.options\" [value]=\"opt.value\">{{ opt.label }}</option>\n </select>\n </div>\n </div>\n\n <!-- Top Bar Buttons -->\n <div class=\"st-actions\" *ngIf=\"config.topBarButtons\">\n <lib-button *ngFor=\"let btn of config.topBarButtons\" \n [variant]=\"btn.btnVariant || 'primary'\"\n [icon]=\"btn.icon || ''\"\n (click)=\"onTopAction(btn)\">\n {{ btn.label }}\n </lib-button>\n </div>\n </div>\n\n <!-- Table Container -->\n <div class=\"st-table-container\">\n <div class=\"st-check-loader\" *ngIf=\"loading\">\n <div class=\"spinner\"></div>\n </div>\n <table class=\"st-table\" [class.loading-data]=\"loading\">\n <thead>\n <tr>\n <th *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" (change)=\"onSelectAll($event)\">\n </th>\n <th *ngFor=\"let col of config.columns\" \n #stickyHeader\n [class.sortable]=\"col.sortable\"\n [class.sticky-col]=\"col.sticky\"\n [ngStyle]=\"stickyColumnStyles[col.key]\"\n (click)=\"onSort(col)\">\n {{ col.label }}\n <span *ngIf=\"col.sortable\" class=\"sort-icon\">\n <i class=\"fa\" [ngClass]=\"getSortIcon(col.key)\"></i>\n </span>\n </th>\n <th *ngIf=\"config.actions && config.actions.length > 0\">{{ config.labels?.actionColumnHeader || 'Actions' }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <td *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" [(ngModel)]=\"row.selected\" (change)=\"onRowSelect(row)\">\n </td>\n <td *ngFor=\"let col of config.columns\" \n [class.sticky-col]=\"col.sticky\" \n [ngStyle]=\"stickyColumnStyles[col.key]\"\n [class.clickable-cell]=\"col.clickAction\"\n (click)=\"onColumnClick(row, col)\">\n <!-- Text/Number/Date -->\n <span *ngIf=\"col.type !== 'custom' && col.type !== 'html' && col.type !== 'badge'\">\n {{ getCellValue(row, col) }}\n </span>\n <!-- HTML -->\n <div *ngIf=\"col.type === 'html'\" [innerHTML]=\"getCellValue(row, col)\"></div>\n <!-- Badge -->\n <span *ngIf=\"col.type === 'badge'\" class=\"st-badge\" [ngClass]=\"getBadgeClass(row, col)\">\n {{ getCellValue(row, col) }}\n </span>\n </td>\n \n <!-- Row Actions -->\n <td *ngIf=\"config.actions && config.actions.length > 0\" class=\"st-row-actions\">\n <div class=\"action-buttons\">\n <ng-container *ngFor=\"let action of config.actions\">\n <lib-button \n [variant]=\"action.btnVariant || 'secondary'\"\n [icon]=\"action.icon || ''\"\n (click)=\"onAction(action, row)\">\n {{ action.label }}\n </lib-button>\n </ng-container>\n </div>\n <!-- Alternatively use specific icons if needed, but button component is requested -->\n </td>\n </tr>\n <tr *ngIf=\"data.length === 0 && !loading\">\n <td [attr.colspan]=\"columnCount + (config.selectable ? 1 : 0) + (config.actions ? 1 : 0)\" class=\"no-data\">\n {{ config.labels?.noDataMessage || 'No data available' }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n <div class=\"st-pagination\" *ngIf=\"config.pagination && config.pagination.enabled\">\n <lib-pagination\n [totalItems]=\"totalItems\"\n [itemsPerPage]=\"config.pagination.pageSize\"\n [currentPage]=\"currentPage\"\n [pageSizeOptions]=\"config.pagination.pageSizeOptions\"\n (pageChange)=\"onPageChange($event)\"\n (itemsPerPageChange)=\"onPageSizeChange($event)\">\n </lib-pagination>\n </div>\n</div>\n", styles: [".smart-table-wrapper{font-family:var(--st-font-family, \"Roboto\", sans-serif);background:var(--st-table-bg, #fff);border-radius:var(--st-border-radius, 8px);box-shadow:var(--st-box-shadow, 0 2px 4px rgba(0, 0, 0, .05));display:flex;flex-direction:column;gap:0;padding:0;border:var(--st-table-border, 1px solid #e0e0e0);overflow:hidden}.st-toolbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;padding:var(--st-toolbar-padding, 1rem);background:var(--st-toolbar-bg, #fff);border-bottom:var(--st-toolbar-border-bottom, 1px solid #eee);gap:var(--st-toolbar-gap, 1rem)}.st-toolbar .st-search{position:relative;width:var(--st-search-width, auto)}.st-toolbar .st-search input{padding:var(--st-search-padding, .5rem .5rem .5rem 2rem);border:var(--st-search-border, 1px solid #ccc);border-radius:var(--st-search-radius, 4px);background:var(--st-search-bg, #fff);font-size:var(--st-font-size, 14px);width:100%;color:var(--st-text-color, #333)}.st-toolbar .st-search i{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--st-search-icon-color, #999)}.st-toolbar .st-filters{display:flex;gap:1rem}.st-toolbar .st-filters select{padding:var(--st-filter-padding, .5rem);border:var(--st-filter-border, 1px solid #ccc);border-radius:var(--st-filter-radius, 4px);font-size:var(--st-filter-font-size, 14px);background:var(--st-filter-bg, #fff);color:var(--st-filter-color, #333)}.st-toolbar .st-actions{display:flex;gap:.5rem}.st-table-container{overflow-x:auto;overflow-y:auto;padding:var(--st-table-padding, 1rem)}.st-table-container::-webkit-scrollbar{width:var(--st-scrollbar-width, 8px);height:var(--st-scrollbar-height, 8px)}.st-table-container::-webkit-scrollbar-track{background:var(--st-scrollbar-track-bg, #f1f1f1);border-radius:var(--st-scrollbar-track-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb{background:var(--st-scrollbar-thumb-bg, #c1c1c1);border-radius:var(--st-scrollbar-thumb-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb:hover{background:var(--st-scrollbar-thumb-hover-bg, #a8a8a8)}.st-table-container.has-sticky-header .st-table thead th{position:sticky;top:0;z-index:10;background:var(--st-header-bg, #f9f9f9);box-shadow:0 1px 2px -1px #0000001a}.st-table-container table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--st-font-size, 14px)}.st-table-container table thead{background:var(--st-header-bg, #f9f9f9)}.st-table-container table thead th{padding:.75rem 1rem;text-align:left;color:var(--st-header-color, #333);font-weight:var(--st-header-weight, 500);font-size:var(--st-header-size, 14px);text-transform:var(--st-header-transform, none);border-bottom:var(--st-header-border, 1px solid #eee);white-space:nowrap}.st-table-container table thead th.sortable{cursor:pointer}.st-table-container table thead th.sortable:hover{opacity:.8}.st-table-container table thead th .sort-icon{margin-left:.5rem}.st-table-container table thead th .sort-icon .sort-icon{margin-left:.5rem;font-size:var(--st-sort-icon-size, .8em)}.st-table-container table thead th.st-checkbox-col{width:40px}.st-table-container table thead th.sticky-col{position:sticky;z-index:3;background:var(--st-header-bg, #f9f9f9);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table thead th.sticky-col:first-child{left:0}.st-table-container table tbody tr{background:var(--st-row-bg, #fff)}.st-table-container table tbody tr td{padding:var(--st-cell-padding, 1rem);color:var(--st-text-color, #333);vertical-align:middle;border-bottom:var(--st-row-border, 1px solid #eee)}.st-table-container table tbody tr td.sticky-col{position:sticky;z-index:2;background:var(--st-row-bg, #fff);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table tbody tr td.sticky-col:first-child{left:0}.st-table-container table tbody tr:hover td,.st-table-container table tbody tr:hover td.sticky-col{background:var(--st-row-hover-bg, #f9f9f9)}.st-table-container table tbody tr.selected td,.st-table-container table tbody tr.selected td.sticky-col{background:var(--st-row-selected-bg, #f3e5f5)}.st-table-container table tbody tr .clickable-cell{cursor:pointer;transition:background .2s}.st-table-container table tbody tr .clickable-cell:hover{background:var(--st-cell-hover-bg, #f0f0f0)!important}input[type=checkbox]{accent-color:var(--st-checkbox-color, #6200EE);width:var(--st-checkbox-size, 16px);height:var(--st-checkbox-size, 16px);cursor:pointer}.st-badge{display:inline-block;padding:var(--st-badge-padding, 4px 12px);border-radius:var(--st-badge-radius, 12px);font-size:var(--st-badge-font-size, 12px);font-weight:var(--st-badge-font-weight, 500);text-align:center;white-space:nowrap}.st-badge.badge-success{background:var(--st-badge-success-bg, #e8f5e9);color:var(--st-badge-success-color, #2e7d32)}.st-badge.badge-warning{background:var(--st-badge-warning-bg, #fff3e0);color:var(--st-badge-warning-color, #ef6c00)}.st-badge.badge-danger{background:var(--st-badge-danger-bg, #ffebee);color:var(--st-badge-danger-color, #c62828)}.st-badge.badge-info{background:var(--st-badge-info-bg, #e3f2fd);color:var(--st-badge-info-color, #1565c0)}.st-badge.badge-neutral{background:var(--st-badge-neutral-bg, #f5f5f5);color:var(--st-badge-neutral-color, #616161)}.st-row-actions .action-buttons{display:flex;gap:.5rem;align-items:center}.no-data{text-align:center;padding:2rem;color:var(--st-no-data-color, #888)}.st-pagination{padding:var(--st-pagination-padding, 1rem);border-top:var(--st-pagination-border-top, none)}@media(max-width:768px){.st-toolbar{flex-direction:column;align-items:stretch}.st-toolbar .st-search,.st-toolbar .st-filters,.st-toolbar .st-actions,.st-toolbar .st-search input{width:100%}}.st-table-container{position:relative;min-height:200px}.st-table-container .st-table.loading-data{opacity:.5;pointer-events:none}.st-table-container .st-check-loader{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;z-index:10}.st-table-container .st-check-loader .spinner{width:40px;height:40px;border:4px solid var(--st-spinner-border-color, rgba(0, 0, 0, .1));border-left-color:var(--st-loader-color);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PaginationComponent, selector: "lib-pagination", inputs: ["totalItems", "itemsPerPage", "currentPage", "pageSizeOptions", "theme", "labels"], outputs: ["pageChange", "itemsPerPageChange"] }, { kind: "component", type: ButtonComponent, selector: "lib-button", inputs: ["variant", "type", "disabled", "width", "height", "borderRadius", "fontSize", "fontWeight", "backgroundColor", "color", "border", "icon", "labels"] }] });
|
|
6015
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SmartTableComponent, isStandalone: false, selector: "lib-smart-table", inputs: { config: "config" }, outputs: { action: "action", topAction: "topAction", filterChange: "filterChange", rowSelect: "rowSelect", columnClick: "columnClick" }, viewQueries: [{ propertyName: "stickyHeaders", predicate: ["stickyHeader"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"smart-table-wrapper\">\n <!-- Top Toolbar -->\n <div class=\"st-toolbar\" *ngIf=\"config.searchConfig?.enabled || (config.filters && config.filters.length > 0) || (config.topBarButtons && config.topBarButtons.length > 0)\">\n \n <!-- Search -->\n <div class=\"st-search\" *ngIf=\"config.searchConfig?.enabled\">\n <i class=\"fa fa-search\"></i>\n <input type=\"text\" [placeholder]=\"config.labels?.searchPlaceholder || 'Search'\" (input)=\"onSearch($event)\">\n </div>\n\n <!-- Filters -->\n <div class=\"st-filters\" *ngIf=\"config.filters\">\n <div class=\"st-filter-item\" *ngFor=\"let filter of config.filters\">\n <select (change)=\"onFilterChange(filter.key, $event)\">\n <option value=\"\">{{ filter.label }}</option>\n <option *ngFor=\"let opt of filter.options\" [value]=\"opt.value\">{{ opt.label }}</option>\n </select>\n </div>\n </div>\n\n <!-- Top Bar Buttons -->\n <div class=\"st-actions\" *ngIf=\"config.topBarButtons\">\n <lib-button *ngFor=\"let btn of config.topBarButtons\" \n [variant]=\"btn.btnVariant || 'primary'\"\n [icon]=\"btn.icon || ''\"\n (click)=\"onTopAction(btn)\">\n {{ btn.label }}\n </lib-button>\n </div>\n </div>\n\n <!-- Table Container -->\n <div class=\"st-table-container\">\n <div class=\"st-check-loader\" *ngIf=\"loading\">\n <div class=\"spinner\"></div>\n </div>\n <table class=\"st-table\" [class.loading-data]=\"loading\">\n <thead>\n <tr>\n <th *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" (change)=\"onSelectAll($event)\">\n </th>\n <th *ngFor=\"let col of config.columns\" \n #stickyHeader\n [class.sortable]=\"col.sortable\"\n [class.sticky-col]=\"col.sticky\"\n [ngStyle]=\"stickyColumnStyles[col.key]\"\n (click)=\"onSort(col)\">\n {{ col.label }}\n <span *ngIf=\"col.sortable\" class=\"sort-icon\">\n <i class=\"fa\" [ngClass]=\"getSortIcon(col.key)\"></i>\n </span>\n </th>\n <th *ngIf=\"config.actions && config.actions.length > 0\">{{ config.labels?.actionColumnHeader || 'Actions' }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <td *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" [(ngModel)]=\"row.selected\" (change)=\"onRowSelect(row)\">\n </td>\n <td *ngFor=\"let col of config.columns\" \n [class.sticky-col]=\"col.sticky\" \n [ngStyle]=\"stickyColumnStyles[col.key]\"\n [class.clickable-cell]=\"col.clickAction\"\n (click)=\"onColumnClick(row, col)\">\n <!-- Text/Number/Date -->\n <span *ngIf=\"col.type !== 'custom' && col.type !== 'html' && col.type !== 'badge'\">\n {{ getCellValue(row, col) }}\n </span>\n <!-- HTML -->\n <div *ngIf=\"col.type === 'html'\" [innerHTML]=\"getCellValue(row, col)\"></div>\n <!-- Badge -->\n <span *ngIf=\"col.type === 'badge'\" class=\"st-badge\" [ngClass]=\"getBadgeClass(row, col)\">\n {{ getCellValue(row, col) }}\n </span>\n </td>\n \n <!-- Row Actions -->\n <td *ngIf=\"config.actions && config.actions.length > 0\" class=\"st-row-actions\">\n <div class=\"action-buttons\">\n <ng-container *ngFor=\"let action of config.actions\">\n <ng-container *ngIf=\"action.type === 'dropdown'\">\n <button class=\"st-dropdown-btn\" [matMenuTriggerFor]=\"menu\" (click)=\"$event.stopPropagation()\">\n <i [class]=\"action.icon || 'fa fa-ellipsis-h'\"></i>\n </button>\n <mat-menu #menu=\"matMenu\" xPosition=\"before\" class=\"st-action-menu\">\n <button mat-menu-item *ngFor=\"let item of action.items\" (click)=\"onActionItemClick(item, row, $event)\">\n <i *ngIf=\"item.icon\" [class]=\"item.icon\" style=\"margin-right: 8px;\"></i>\n <span>{{ item.label }}</span>\n </button>\n </mat-menu>\n </ng-container>\n <ng-container *ngIf=\"action.type !== 'dropdown'\">\n <lib-button \n [variant]=\"action.btnVariant || 'secondary'\"\n [icon]=\"action.icon || ''\"\n (click)=\"onAction(action, row)\">\n {{ action.label }}\n </lib-button>\n </ng-container>\n </ng-container>\n </div>\n </td>\n </tr>\n <tr *ngIf=\"data.length === 0 && !loading\">\n <td [attr.colspan]=\"columnCount + (config.selectable ? 1 : 0) + (config.actions ? 1 : 0)\" class=\"no-data\">\n {{ config.labels?.noDataMessage || 'No data available' }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n <div class=\"st-pagination\" *ngIf=\"config.pagination && config.pagination.enabled\">\n <lib-pagination\n [totalItems]=\"totalItems\"\n [itemsPerPage]=\"config.pagination.pageSize\"\n [currentPage]=\"currentPage\"\n [pageSizeOptions]=\"config.pagination.pageSizeOptions\"\n (pageChange)=\"onPageChange($event)\"\n (itemsPerPageChange)=\"onPageSizeChange($event)\">\n </lib-pagination>\n </div>\n</div>\n", styles: [".smart-table-wrapper{font-family:var(--st-font-family, \"Roboto\", sans-serif);background:var(--st-table-bg, #fff);border-radius:var(--st-border-radius, 8px);box-shadow:var(--st-box-shadow, 0 2px 4px rgba(0, 0, 0, .05));display:flex;flex-direction:column;gap:0;padding:0;border:var(--st-table-border, 1px solid #e0e0e0);overflow:hidden}.st-toolbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;padding:var(--st-toolbar-padding, 1rem);background:var(--st-toolbar-bg, #fff);border-bottom:var(--st-toolbar-border-bottom, 1px solid #eee);gap:var(--st-toolbar-gap, 1rem)}.st-toolbar .st-search{position:relative;width:var(--st-search-width, auto)}.st-toolbar .st-search input{padding:var(--st-search-padding, .5rem .5rem .5rem 2rem);border:var(--st-search-border, 1px solid #ccc);border-radius:var(--st-search-radius, 4px);background:var(--st-search-bg, #fff);font-size:var(--st-font-size, 14px);width:100%;color:var(--st-text-color, #333)}.st-toolbar .st-search i{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--st-search-icon-color, #999)}.st-toolbar .st-filters{display:flex;gap:1rem}.st-toolbar .st-filters select{padding:var(--st-filter-padding, .5rem);border:var(--st-filter-border, 1px solid #ccc);border-radius:var(--st-filter-radius, 4px);font-size:var(--st-filter-font-size, 14px);background:var(--st-filter-bg, #fff);color:var(--st-filter-color, #333)}.st-toolbar .st-actions{display:flex;gap:.5rem}.st-table-container{overflow-x:auto;overflow-y:auto;padding:var(--st-table-padding, 1rem)}.st-table-container::-webkit-scrollbar{width:var(--st-scrollbar-width, 8px);height:var(--st-scrollbar-height, 8px)}.st-table-container::-webkit-scrollbar-track{background:var(--st-scrollbar-track-bg, #f1f1f1);border-radius:var(--st-scrollbar-track-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb{background:var(--st-scrollbar-thumb-bg, #c1c1c1);border-radius:var(--st-scrollbar-thumb-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb:hover{background:var(--st-scrollbar-thumb-hover-bg, #a8a8a8)}.st-table-container.has-sticky-header .st-table thead th{position:sticky;top:0;z-index:10;background:var(--st-header-bg, #f9f9f9);box-shadow:0 1px 2px -1px #0000001a}.st-table-container table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--st-font-size, 14px)}.st-table-container table thead{background:var(--st-header-bg, #f9f9f9)}.st-table-container table thead th{padding:.75rem 1rem;text-align:left;color:var(--st-header-color, #333);font-weight:var(--st-header-weight, 500);font-size:var(--st-header-size, 14px);text-transform:var(--st-header-transform, none);border-bottom:var(--st-header-border, 1px solid #eee);white-space:nowrap}.st-table-container table thead th.sortable{cursor:pointer}.st-table-container table thead th.sortable:hover{opacity:.8}.st-table-container table thead th .sort-icon{margin-left:.5rem}.st-table-container table thead th .sort-icon .sort-icon{margin-left:.5rem;font-size:var(--st-sort-icon-size, .8em)}.st-table-container table thead th.st-checkbox-col{width:40px}.st-table-container table thead th.sticky-col{position:sticky;z-index:3;background:var(--st-header-bg, #f9f9f9);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table thead th.sticky-col:first-child{left:0}.st-table-container table tbody tr{background:var(--st-row-bg, #fff)}.st-table-container table tbody tr td{padding:var(--st-cell-padding, 1rem);color:var(--st-text-color, #333);vertical-align:middle;border-bottom:var(--st-row-border, 1px solid #eee)}.st-table-container table tbody tr td.sticky-col{position:sticky;z-index:2;background:var(--st-row-bg, #fff);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table tbody tr td.sticky-col:first-child{left:0}.st-table-container table tbody tr:hover td,.st-table-container table tbody tr:hover td.sticky-col{background:var(--st-row-hover-bg, #f9f9f9)}.st-table-container table tbody tr.selected td,.st-table-container table tbody tr.selected td.sticky-col{background:var(--st-row-selected-bg, #f3e5f5)}.st-table-container table tbody tr .clickable-cell{cursor:pointer;transition:background .2s}.st-table-container table tbody tr .clickable-cell:hover{background:var(--st-cell-hover-bg, #f0f0f0)!important}input[type=checkbox]{accent-color:var(--st-checkbox-color, #6200EE);width:var(--st-checkbox-size, 16px);height:var(--st-checkbox-size, 16px);cursor:pointer}.st-badge{display:inline-block;padding:var(--st-badge-padding, 4px 12px);border-radius:var(--st-badge-radius, 12px);font-size:var(--st-badge-font-size, 12px);font-weight:var(--st-badge-font-weight, 500);text-align:center;white-space:nowrap}.st-badge.badge-success{background:var(--st-badge-success-bg, #e8f5e9);color:var(--st-badge-success-color, #2e7d32)}.st-badge.badge-warning{background:var(--st-badge-warning-bg, #fff3e0);color:var(--st-badge-warning-color, #ef6c00)}.st-badge.badge-danger{background:var(--st-badge-danger-bg, #ffebee);color:var(--st-badge-danger-color, #c62828)}.st-badge.badge-info{background:var(--st-badge-info-bg, #e3f2fd);color:var(--st-badge-info-color, #1565c0)}.st-badge.badge-neutral{background:var(--st-badge-neutral-bg, #f5f5f5);color:var(--st-badge-neutral-color, #616161)}.st-row-actions .action-buttons{display:flex;gap:.5rem;align-items:center}.no-data{text-align:center;padding:2rem;color:var(--st-no-data-color, #888)}.st-pagination{padding:var(--st-pagination-padding, 1rem);border-top:var(--st-pagination-border-top, none)}@media(max-width:768px){.st-toolbar{flex-direction:column;align-items:stretch}.st-toolbar .st-search,.st-toolbar .st-filters,.st-toolbar .st-actions,.st-toolbar .st-search input{width:100%}}.st-table-container{position:relative;min-height:200px}.st-table-container .st-table.loading-data{opacity:.5;pointer-events:none}.st-table-container .st-check-loader{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;z-index:10}.st-table-container .st-check-loader .spinner{width:40px;height:40px;border:4px solid var(--st-spinner-border-color, rgba(0, 0, 0, .1));border-left-color:var(--st-loader-color);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PaginationComponent, selector: "lib-pagination", inputs: ["totalItems", "itemsPerPage", "currentPage", "pageSizeOptions", "theme", "labels"], outputs: ["pageChange", "itemsPerPageChange"] }, { kind: "component", type: ButtonComponent, selector: "lib-button", inputs: ["variant", "type", "disabled", "width", "height", "borderRadius", "fontSize", "fontWeight", "backgroundColor", "color", "border", "icon", "labels"] }, { kind: "component", type: i7$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }] });
|
|
5454
6016
|
}
|
|
5455
6017
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartTableComponent, decorators: [{
|
|
5456
6018
|
type: Component,
|
|
5457
|
-
args: [{ selector: 'lib-smart-table', standalone: false, template: "<div class=\"smart-table-wrapper\">\n <!-- Top Toolbar -->\n <div class=\"st-toolbar\" *ngIf=\"config.searchConfig?.enabled || (config.filters && config.filters.length > 0) || (config.topBarButtons && config.topBarButtons.length > 0)\">\n \n <!-- Search -->\n <div class=\"st-search\" *ngIf=\"config.searchConfig?.enabled\">\n <i class=\"fa fa-search\"></i>\n <input type=\"text\" [placeholder]=\"config.labels?.searchPlaceholder || 'Search'\" (input)=\"onSearch($event)\">\n </div>\n\n <!-- Filters -->\n <div class=\"st-filters\" *ngIf=\"config.filters\">\n <div class=\"st-filter-item\" *ngFor=\"let filter of config.filters\">\n <select (change)=\"onFilterChange(filter.key, $event)\">\n <option value=\"\">{{ filter.label }}</option>\n <option *ngFor=\"let opt of filter.options\" [value]=\"opt.value\">{{ opt.label }}</option>\n </select>\n </div>\n </div>\n\n <!-- Top Bar Buttons -->\n <div class=\"st-actions\" *ngIf=\"config.topBarButtons\">\n <lib-button *ngFor=\"let btn of config.topBarButtons\" \n [variant]=\"btn.btnVariant || 'primary'\"\n [icon]=\"btn.icon || ''\"\n (click)=\"onTopAction(btn)\">\n {{ btn.label }}\n </lib-button>\n </div>\n </div>\n\n <!-- Table Container -->\n <div class=\"st-table-container\">\n <div class=\"st-check-loader\" *ngIf=\"loading\">\n <div class=\"spinner\"></div>\n </div>\n <table class=\"st-table\" [class.loading-data]=\"loading\">\n <thead>\n <tr>\n <th *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" (change)=\"onSelectAll($event)\">\n </th>\n <th *ngFor=\"let col of config.columns\" \n #stickyHeader\n [class.sortable]=\"col.sortable\"\n [class.sticky-col]=\"col.sticky\"\n [ngStyle]=\"stickyColumnStyles[col.key]\"\n (click)=\"onSort(col)\">\n {{ col.label }}\n <span *ngIf=\"col.sortable\" class=\"sort-icon\">\n <i class=\"fa\" [ngClass]=\"getSortIcon(col.key)\"></i>\n </span>\n </th>\n <th *ngIf=\"config.actions && config.actions.length > 0\">{{ config.labels?.actionColumnHeader || 'Actions' }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <td *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" [(ngModel)]=\"row.selected\" (change)=\"onRowSelect(row)\">\n </td>\n <td *ngFor=\"let col of config.columns\" \n [class.sticky-col]=\"col.sticky\" \n [ngStyle]=\"stickyColumnStyles[col.key]\"\n [class.clickable-cell]=\"col.clickAction\"\n (click)=\"onColumnClick(row, col)\">\n <!-- Text/Number/Date -->\n <span *ngIf=\"col.type !== 'custom' && col.type !== 'html' && col.type !== 'badge'\">\n {{ getCellValue(row, col) }}\n </span>\n <!-- HTML -->\n <div *ngIf=\"col.type === 'html'\" [innerHTML]=\"getCellValue(row, col)\"></div>\n <!-- Badge -->\n <span *ngIf=\"col.type === 'badge'\" class=\"st-badge\" [ngClass]=\"getBadgeClass(row, col)\">\n {{ getCellValue(row, col) }}\n </span>\n </td>\n \n <!-- Row Actions -->\n <td *ngIf=\"config.actions && config.actions.length > 0\" class=\"st-row-actions\">\n <div class=\"action-buttons\">\n <ng-container *ngFor=\"let action of config.actions\">\n <lib-button \n [variant]=\"action.btnVariant || 'secondary'\"\n [icon]=\"action.icon || ''\"\n (click)=\"onAction(action, row)\">\n {{ action.label }}\n </lib-button>\n </ng-container>\n </div>\n <!-- Alternatively use specific icons if needed, but button component is requested -->\n </td>\n </tr>\n <tr *ngIf=\"data.length === 0 && !loading\">\n <td [attr.colspan]=\"columnCount + (config.selectable ? 1 : 0) + (config.actions ? 1 : 0)\" class=\"no-data\">\n {{ config.labels?.noDataMessage || 'No data available' }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n <div class=\"st-pagination\" *ngIf=\"config.pagination && config.pagination.enabled\">\n <lib-pagination\n [totalItems]=\"totalItems\"\n [itemsPerPage]=\"config.pagination.pageSize\"\n [currentPage]=\"currentPage\"\n [pageSizeOptions]=\"config.pagination.pageSizeOptions\"\n (pageChange)=\"onPageChange($event)\"\n (itemsPerPageChange)=\"onPageSizeChange($event)\">\n </lib-pagination>\n </div>\n</div>\n", styles: [".smart-table-wrapper{font-family:var(--st-font-family, \"Roboto\", sans-serif);background:var(--st-table-bg, #fff);border-radius:var(--st-border-radius, 8px);box-shadow:var(--st-box-shadow, 0 2px 4px rgba(0, 0, 0, .05));display:flex;flex-direction:column;gap:0;padding:0;border:var(--st-table-border, 1px solid #e0e0e0);overflow:hidden}.st-toolbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;padding:var(--st-toolbar-padding, 1rem);background:var(--st-toolbar-bg, #fff);border-bottom:var(--st-toolbar-border-bottom, 1px solid #eee);gap:var(--st-toolbar-gap, 1rem)}.st-toolbar .st-search{position:relative;width:var(--st-search-width, auto)}.st-toolbar .st-search input{padding:var(--st-search-padding, .5rem .5rem .5rem 2rem);border:var(--st-search-border, 1px solid #ccc);border-radius:var(--st-search-radius, 4px);background:var(--st-search-bg, #fff);font-size:var(--st-font-size, 14px);width:100%;color:var(--st-text-color, #333)}.st-toolbar .st-search i{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--st-search-icon-color, #999)}.st-toolbar .st-filters{display:flex;gap:1rem}.st-toolbar .st-filters select{padding:var(--st-filter-padding, .5rem);border:var(--st-filter-border, 1px solid #ccc);border-radius:var(--st-filter-radius, 4px);font-size:var(--st-filter-font-size, 14px);background:var(--st-filter-bg, #fff);color:var(--st-filter-color, #333)}.st-toolbar .st-actions{display:flex;gap:.5rem}.st-table-container{overflow-x:auto;overflow-y:auto;padding:var(--st-table-padding, 1rem)}.st-table-container::-webkit-scrollbar{width:var(--st-scrollbar-width, 8px);height:var(--st-scrollbar-height, 8px)}.st-table-container::-webkit-scrollbar-track{background:var(--st-scrollbar-track-bg, #f1f1f1);border-radius:var(--st-scrollbar-track-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb{background:var(--st-scrollbar-thumb-bg, #c1c1c1);border-radius:var(--st-scrollbar-thumb-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb:hover{background:var(--st-scrollbar-thumb-hover-bg, #a8a8a8)}.st-table-container.has-sticky-header .st-table thead th{position:sticky;top:0;z-index:10;background:var(--st-header-bg, #f9f9f9);box-shadow:0 1px 2px -1px #0000001a}.st-table-container table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--st-font-size, 14px)}.st-table-container table thead{background:var(--st-header-bg, #f9f9f9)}.st-table-container table thead th{padding:.75rem 1rem;text-align:left;color:var(--st-header-color, #333);font-weight:var(--st-header-weight, 500);font-size:var(--st-header-size, 14px);text-transform:var(--st-header-transform, none);border-bottom:var(--st-header-border, 1px solid #eee);white-space:nowrap}.st-table-container table thead th.sortable{cursor:pointer}.st-table-container table thead th.sortable:hover{opacity:.8}.st-table-container table thead th .sort-icon{margin-left:.5rem}.st-table-container table thead th .sort-icon .sort-icon{margin-left:.5rem;font-size:var(--st-sort-icon-size, .8em)}.st-table-container table thead th.st-checkbox-col{width:40px}.st-table-container table thead th.sticky-col{position:sticky;z-index:3;background:var(--st-header-bg, #f9f9f9);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table thead th.sticky-col:first-child{left:0}.st-table-container table tbody tr{background:var(--st-row-bg, #fff)}.st-table-container table tbody tr td{padding:var(--st-cell-padding, 1rem);color:var(--st-text-color, #333);vertical-align:middle;border-bottom:var(--st-row-border, 1px solid #eee)}.st-table-container table tbody tr td.sticky-col{position:sticky;z-index:2;background:var(--st-row-bg, #fff);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table tbody tr td.sticky-col:first-child{left:0}.st-table-container table tbody tr:hover td,.st-table-container table tbody tr:hover td.sticky-col{background:var(--st-row-hover-bg, #f9f9f9)}.st-table-container table tbody tr.selected td,.st-table-container table tbody tr.selected td.sticky-col{background:var(--st-row-selected-bg, #f3e5f5)}.st-table-container table tbody tr .clickable-cell{cursor:pointer;transition:background .2s}.st-table-container table tbody tr .clickable-cell:hover{background:var(--st-cell-hover-bg, #f0f0f0)!important}input[type=checkbox]{accent-color:var(--st-checkbox-color, #6200EE);width:var(--st-checkbox-size, 16px);height:var(--st-checkbox-size, 16px);cursor:pointer}.st-badge{display:inline-block;padding:var(--st-badge-padding, 4px 12px);border-radius:var(--st-badge-radius, 12px);font-size:var(--st-badge-font-size, 12px);font-weight:var(--st-badge-font-weight, 500);text-align:center;white-space:nowrap}.st-badge.badge-success{background:var(--st-badge-success-bg, #e8f5e9);color:var(--st-badge-success-color, #2e7d32)}.st-badge.badge-warning{background:var(--st-badge-warning-bg, #fff3e0);color:var(--st-badge-warning-color, #ef6c00)}.st-badge.badge-danger{background:var(--st-badge-danger-bg, #ffebee);color:var(--st-badge-danger-color, #c62828)}.st-badge.badge-info{background:var(--st-badge-info-bg, #e3f2fd);color:var(--st-badge-info-color, #1565c0)}.st-badge.badge-neutral{background:var(--st-badge-neutral-bg, #f5f5f5);color:var(--st-badge-neutral-color, #616161)}.st-row-actions .action-buttons{display:flex;gap:.5rem;align-items:center}.no-data{text-align:center;padding:2rem;color:var(--st-no-data-color, #888)}.st-pagination{padding:var(--st-pagination-padding, 1rem);border-top:var(--st-pagination-border-top, none)}@media(max-width:768px){.st-toolbar{flex-direction:column;align-items:stretch}.st-toolbar .st-search,.st-toolbar .st-filters,.st-toolbar .st-actions,.st-toolbar .st-search input{width:100%}}.st-table-container{position:relative;min-height:200px}.st-table-container .st-table.loading-data{opacity:.5;pointer-events:none}.st-table-container .st-check-loader{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;z-index:10}.st-table-container .st-check-loader .spinner{width:40px;height:40px;border:4px solid var(--st-spinner-border-color, rgba(0, 0, 0, .1));border-left-color:var(--st-loader-color);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
|
|
6019
|
+
args: [{ selector: 'lib-smart-table', standalone: false, template: "<div class=\"smart-table-wrapper\">\n <!-- Top Toolbar -->\n <div class=\"st-toolbar\" *ngIf=\"config.searchConfig?.enabled || (config.filters && config.filters.length > 0) || (config.topBarButtons && config.topBarButtons.length > 0)\">\n \n <!-- Search -->\n <div class=\"st-search\" *ngIf=\"config.searchConfig?.enabled\">\n <i class=\"fa fa-search\"></i>\n <input type=\"text\" [placeholder]=\"config.labels?.searchPlaceholder || 'Search'\" (input)=\"onSearch($event)\">\n </div>\n\n <!-- Filters -->\n <div class=\"st-filters\" *ngIf=\"config.filters\">\n <div class=\"st-filter-item\" *ngFor=\"let filter of config.filters\">\n <select (change)=\"onFilterChange(filter.key, $event)\">\n <option value=\"\">{{ filter.label }}</option>\n <option *ngFor=\"let opt of filter.options\" [value]=\"opt.value\">{{ opt.label }}</option>\n </select>\n </div>\n </div>\n\n <!-- Top Bar Buttons -->\n <div class=\"st-actions\" *ngIf=\"config.topBarButtons\">\n <lib-button *ngFor=\"let btn of config.topBarButtons\" \n [variant]=\"btn.btnVariant || 'primary'\"\n [icon]=\"btn.icon || ''\"\n (click)=\"onTopAction(btn)\">\n {{ btn.label }}\n </lib-button>\n </div>\n </div>\n\n <!-- Table Container -->\n <div class=\"st-table-container\">\n <div class=\"st-check-loader\" *ngIf=\"loading\">\n <div class=\"spinner\"></div>\n </div>\n <table class=\"st-table\" [class.loading-data]=\"loading\">\n <thead>\n <tr>\n <th *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" (change)=\"onSelectAll($event)\">\n </th>\n <th *ngFor=\"let col of config.columns\" \n #stickyHeader\n [class.sortable]=\"col.sortable\"\n [class.sticky-col]=\"col.sticky\"\n [ngStyle]=\"stickyColumnStyles[col.key]\"\n (click)=\"onSort(col)\">\n {{ col.label }}\n <span *ngIf=\"col.sortable\" class=\"sort-icon\">\n <i class=\"fa\" [ngClass]=\"getSortIcon(col.key)\"></i>\n </span>\n </th>\n <th *ngIf=\"config.actions && config.actions.length > 0\">{{ config.labels?.actionColumnHeader || 'Actions' }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <td *ngIf=\"config.selectable\" class=\"st-checkbox-col\">\n <input type=\"checkbox\" [(ngModel)]=\"row.selected\" (change)=\"onRowSelect(row)\">\n </td>\n <td *ngFor=\"let col of config.columns\" \n [class.sticky-col]=\"col.sticky\" \n [ngStyle]=\"stickyColumnStyles[col.key]\"\n [class.clickable-cell]=\"col.clickAction\"\n (click)=\"onColumnClick(row, col)\">\n <!-- Text/Number/Date -->\n <span *ngIf=\"col.type !== 'custom' && col.type !== 'html' && col.type !== 'badge'\">\n {{ getCellValue(row, col) }}\n </span>\n <!-- HTML -->\n <div *ngIf=\"col.type === 'html'\" [innerHTML]=\"getCellValue(row, col)\"></div>\n <!-- Badge -->\n <span *ngIf=\"col.type === 'badge'\" class=\"st-badge\" [ngClass]=\"getBadgeClass(row, col)\">\n {{ getCellValue(row, col) }}\n </span>\n </td>\n \n <!-- Row Actions -->\n <td *ngIf=\"config.actions && config.actions.length > 0\" class=\"st-row-actions\">\n <div class=\"action-buttons\">\n <ng-container *ngFor=\"let action of config.actions\">\n <ng-container *ngIf=\"action.type === 'dropdown'\">\n <button class=\"st-dropdown-btn\" [matMenuTriggerFor]=\"menu\" (click)=\"$event.stopPropagation()\">\n <i [class]=\"action.icon || 'fa fa-ellipsis-h'\"></i>\n </button>\n <mat-menu #menu=\"matMenu\" xPosition=\"before\" class=\"st-action-menu\">\n <button mat-menu-item *ngFor=\"let item of action.items\" (click)=\"onActionItemClick(item, row, $event)\">\n <i *ngIf=\"item.icon\" [class]=\"item.icon\" style=\"margin-right: 8px;\"></i>\n <span>{{ item.label }}</span>\n </button>\n </mat-menu>\n </ng-container>\n <ng-container *ngIf=\"action.type !== 'dropdown'\">\n <lib-button \n [variant]=\"action.btnVariant || 'secondary'\"\n [icon]=\"action.icon || ''\"\n (click)=\"onAction(action, row)\">\n {{ action.label }}\n </lib-button>\n </ng-container>\n </ng-container>\n </div>\n </td>\n </tr>\n <tr *ngIf=\"data.length === 0 && !loading\">\n <td [attr.colspan]=\"columnCount + (config.selectable ? 1 : 0) + (config.actions ? 1 : 0)\" class=\"no-data\">\n {{ config.labels?.noDataMessage || 'No data available' }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n <div class=\"st-pagination\" *ngIf=\"config.pagination && config.pagination.enabled\">\n <lib-pagination\n [totalItems]=\"totalItems\"\n [itemsPerPage]=\"config.pagination.pageSize\"\n [currentPage]=\"currentPage\"\n [pageSizeOptions]=\"config.pagination.pageSizeOptions\"\n (pageChange)=\"onPageChange($event)\"\n (itemsPerPageChange)=\"onPageSizeChange($event)\">\n </lib-pagination>\n </div>\n</div>\n", styles: [".smart-table-wrapper{font-family:var(--st-font-family, \"Roboto\", sans-serif);background:var(--st-table-bg, #fff);border-radius:var(--st-border-radius, 8px);box-shadow:var(--st-box-shadow, 0 2px 4px rgba(0, 0, 0, .05));display:flex;flex-direction:column;gap:0;padding:0;border:var(--st-table-border, 1px solid #e0e0e0);overflow:hidden}.st-toolbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;padding:var(--st-toolbar-padding, 1rem);background:var(--st-toolbar-bg, #fff);border-bottom:var(--st-toolbar-border-bottom, 1px solid #eee);gap:var(--st-toolbar-gap, 1rem)}.st-toolbar .st-search{position:relative;width:var(--st-search-width, auto)}.st-toolbar .st-search input{padding:var(--st-search-padding, .5rem .5rem .5rem 2rem);border:var(--st-search-border, 1px solid #ccc);border-radius:var(--st-search-radius, 4px);background:var(--st-search-bg, #fff);font-size:var(--st-font-size, 14px);width:100%;color:var(--st-text-color, #333)}.st-toolbar .st-search i{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--st-search-icon-color, #999)}.st-toolbar .st-filters{display:flex;gap:1rem}.st-toolbar .st-filters select{padding:var(--st-filter-padding, .5rem);border:var(--st-filter-border, 1px solid #ccc);border-radius:var(--st-filter-radius, 4px);font-size:var(--st-filter-font-size, 14px);background:var(--st-filter-bg, #fff);color:var(--st-filter-color, #333)}.st-toolbar .st-actions{display:flex;gap:.5rem}.st-table-container{overflow-x:auto;overflow-y:auto;padding:var(--st-table-padding, 1rem)}.st-table-container::-webkit-scrollbar{width:var(--st-scrollbar-width, 8px);height:var(--st-scrollbar-height, 8px)}.st-table-container::-webkit-scrollbar-track{background:var(--st-scrollbar-track-bg, #f1f1f1);border-radius:var(--st-scrollbar-track-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb{background:var(--st-scrollbar-thumb-bg, #c1c1c1);border-radius:var(--st-scrollbar-thumb-radius, 4px)}.st-table-container::-webkit-scrollbar-thumb:hover{background:var(--st-scrollbar-thumb-hover-bg, #a8a8a8)}.st-table-container.has-sticky-header .st-table thead th{position:sticky;top:0;z-index:10;background:var(--st-header-bg, #f9f9f9);box-shadow:0 1px 2px -1px #0000001a}.st-table-container table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--st-font-size, 14px)}.st-table-container table thead{background:var(--st-header-bg, #f9f9f9)}.st-table-container table thead th{padding:.75rem 1rem;text-align:left;color:var(--st-header-color, #333);font-weight:var(--st-header-weight, 500);font-size:var(--st-header-size, 14px);text-transform:var(--st-header-transform, none);border-bottom:var(--st-header-border, 1px solid #eee);white-space:nowrap}.st-table-container table thead th.sortable{cursor:pointer}.st-table-container table thead th.sortable:hover{opacity:.8}.st-table-container table thead th .sort-icon{margin-left:.5rem}.st-table-container table thead th .sort-icon .sort-icon{margin-left:.5rem;font-size:var(--st-sort-icon-size, .8em)}.st-table-container table thead th.st-checkbox-col{width:40px}.st-table-container table thead th.sticky-col{position:sticky;z-index:3;background:var(--st-header-bg, #f9f9f9);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table thead th.sticky-col:first-child{left:0}.st-table-container table tbody tr{background:var(--st-row-bg, #fff)}.st-table-container table tbody tr td{padding:var(--st-cell-padding, 1rem);color:var(--st-text-color, #333);vertical-align:middle;border-bottom:var(--st-row-border, 1px solid #eee)}.st-table-container table tbody tr td.sticky-col{position:sticky;z-index:2;background:var(--st-row-bg, #fff);box-shadow:var(--st-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, .1));border-right:var(--st-sticky-border-right, 1px solid rgba(0, 0, 0, .05))}.st-table-container table tbody tr td.sticky-col:first-child{left:0}.st-table-container table tbody tr:hover td,.st-table-container table tbody tr:hover td.sticky-col{background:var(--st-row-hover-bg, #f9f9f9)}.st-table-container table tbody tr.selected td,.st-table-container table tbody tr.selected td.sticky-col{background:var(--st-row-selected-bg, #f3e5f5)}.st-table-container table tbody tr .clickable-cell{cursor:pointer;transition:background .2s}.st-table-container table tbody tr .clickable-cell:hover{background:var(--st-cell-hover-bg, #f0f0f0)!important}input[type=checkbox]{accent-color:var(--st-checkbox-color, #6200EE);width:var(--st-checkbox-size, 16px);height:var(--st-checkbox-size, 16px);cursor:pointer}.st-badge{display:inline-block;padding:var(--st-badge-padding, 4px 12px);border-radius:var(--st-badge-radius, 12px);font-size:var(--st-badge-font-size, 12px);font-weight:var(--st-badge-font-weight, 500);text-align:center;white-space:nowrap}.st-badge.badge-success{background:var(--st-badge-success-bg, #e8f5e9);color:var(--st-badge-success-color, #2e7d32)}.st-badge.badge-warning{background:var(--st-badge-warning-bg, #fff3e0);color:var(--st-badge-warning-color, #ef6c00)}.st-badge.badge-danger{background:var(--st-badge-danger-bg, #ffebee);color:var(--st-badge-danger-color, #c62828)}.st-badge.badge-info{background:var(--st-badge-info-bg, #e3f2fd);color:var(--st-badge-info-color, #1565c0)}.st-badge.badge-neutral{background:var(--st-badge-neutral-bg, #f5f5f5);color:var(--st-badge-neutral-color, #616161)}.st-row-actions .action-buttons{display:flex;gap:.5rem;align-items:center}.no-data{text-align:center;padding:2rem;color:var(--st-no-data-color, #888)}.st-pagination{padding:var(--st-pagination-padding, 1rem);border-top:var(--st-pagination-border-top, none)}@media(max-width:768px){.st-toolbar{flex-direction:column;align-items:stretch}.st-toolbar .st-search,.st-toolbar .st-filters,.st-toolbar .st-actions,.st-toolbar .st-search input{width:100%}}.st-table-container{position:relative;min-height:200px}.st-table-container .st-table.loading-data{opacity:.5;pointer-events:none}.st-table-container .st-check-loader{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;z-index:10}.st-table-container .st-check-loader .spinner{width:40px;height:40px;border:4px solid var(--st-spinner-border-color, rgba(0, 0, 0, .1));border-left-color:var(--st-loader-color);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
|
|
5458
6020
|
}], ctorParameters: () => [{ type: i3.HttpClient }, { type: i1$1.Router }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { config: [{
|
|
5459
6021
|
type: Input
|
|
5460
6022
|
}], action: [{
|
|
@@ -5477,11 +6039,15 @@ class SmartTableModule {
|
|
|
5477
6039
|
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: SmartTableModule, declarations: [SmartTableComponent], imports: [CommonModule,
|
|
5478
6040
|
FormsModule,
|
|
5479
6041
|
PaginationModule,
|
|
5480
|
-
ButtonModule
|
|
6042
|
+
ButtonModule,
|
|
6043
|
+
MatMenuModule,
|
|
6044
|
+
MatIconModule], exports: [SmartTableComponent] });
|
|
5481
6045
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartTableModule, imports: [CommonModule,
|
|
5482
6046
|
FormsModule,
|
|
5483
6047
|
PaginationModule,
|
|
5484
|
-
ButtonModule
|
|
6048
|
+
ButtonModule,
|
|
6049
|
+
MatMenuModule,
|
|
6050
|
+
MatIconModule] });
|
|
5485
6051
|
}
|
|
5486
6052
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartTableModule, decorators: [{
|
|
5487
6053
|
type: NgModule,
|
|
@@ -5493,7 +6059,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
5493
6059
|
CommonModule,
|
|
5494
6060
|
FormsModule,
|
|
5495
6061
|
PaginationModule,
|
|
5496
|
-
ButtonModule
|
|
6062
|
+
ButtonModule,
|
|
6063
|
+
MatMenuModule,
|
|
6064
|
+
MatIconModule
|
|
5497
6065
|
],
|
|
5498
6066
|
exports: [
|
|
5499
6067
|
SmartTableComponent
|
|
@@ -5501,382 +6069,1324 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
5501
6069
|
}]
|
|
5502
6070
|
}] });
|
|
5503
6071
|
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
6072
|
+
class ValidationUtils {
|
|
6073
|
+
static email() {
|
|
6074
|
+
return (control) => {
|
|
6075
|
+
if (!control.value)
|
|
6076
|
+
return null;
|
|
6077
|
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
6078
|
+
return emailRegex.test(control.value) ? null : { email: true };
|
|
6079
|
+
};
|
|
6080
|
+
}
|
|
6081
|
+
static phone() {
|
|
6082
|
+
return (control) => {
|
|
6083
|
+
if (!control.value)
|
|
6084
|
+
return null;
|
|
6085
|
+
const phoneRegex = /^\+?[\d\s\-\(\)]{10,}$/;
|
|
6086
|
+
return phoneRegex.test(control.value) ? null : { phone: true };
|
|
6087
|
+
};
|
|
6088
|
+
}
|
|
6089
|
+
static url() {
|
|
6090
|
+
return (control) => {
|
|
6091
|
+
if (!control.value)
|
|
6092
|
+
return null;
|
|
6093
|
+
const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
|
|
6094
|
+
return urlRegex.test(control.value) ? null : { url: true };
|
|
6095
|
+
};
|
|
6096
|
+
}
|
|
6097
|
+
static minLength(min) {
|
|
6098
|
+
return (control) => {
|
|
6099
|
+
if (!control.value)
|
|
6100
|
+
return null;
|
|
6101
|
+
return control.value.length >= min ? null : { minLength: { min, actual: control.value.length } };
|
|
6102
|
+
};
|
|
6103
|
+
}
|
|
6104
|
+
static maxLength(max) {
|
|
6105
|
+
return (control) => {
|
|
6106
|
+
if (!control.value)
|
|
6107
|
+
return null;
|
|
6108
|
+
return control.value.length <= max ? null : { maxLength: { max, actual: control.value.length } };
|
|
6109
|
+
};
|
|
6110
|
+
}
|
|
6111
|
+
static pattern(pattern, message) {
|
|
6112
|
+
return (control) => {
|
|
6113
|
+
if (!control.value)
|
|
6114
|
+
return null;
|
|
6115
|
+
const regex = new RegExp(pattern);
|
|
6116
|
+
return regex.test(control.value) ? null : { pattern: { message: message || 'Invalid format' } };
|
|
6117
|
+
};
|
|
6118
|
+
}
|
|
6119
|
+
static numberRange(min, max) {
|
|
6120
|
+
return (control) => {
|
|
6121
|
+
if (!control.value && control.value !== 0)
|
|
6122
|
+
return null;
|
|
6123
|
+
const value = Number(control.value);
|
|
6124
|
+
if (min !== undefined && value < min) {
|
|
6125
|
+
return { min: { min, actual: value } };
|
|
5539
6126
|
}
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
6127
|
+
if (max !== undefined && value > max) {
|
|
6128
|
+
return { max: { max, actual: value } };
|
|
6129
|
+
}
|
|
6130
|
+
return null;
|
|
6131
|
+
};
|
|
6132
|
+
}
|
|
6133
|
+
static dateRange(minDate, maxDate) {
|
|
6134
|
+
return (control) => {
|
|
6135
|
+
if (!control.value)
|
|
6136
|
+
return null;
|
|
6137
|
+
const date = new Date(control.value);
|
|
6138
|
+
if (minDate && date < new Date(minDate)) {
|
|
6139
|
+
return { minDate: { minDate } };
|
|
6140
|
+
}
|
|
6141
|
+
if (maxDate && date > new Date(maxDate)) {
|
|
6142
|
+
return { maxDate: { maxDate } };
|
|
6143
|
+
}
|
|
6144
|
+
return null;
|
|
6145
|
+
};
|
|
6146
|
+
}
|
|
6147
|
+
static getErrorMessage(errors) {
|
|
6148
|
+
if (!errors)
|
|
6149
|
+
return '';
|
|
6150
|
+
if (errors['required'])
|
|
6151
|
+
return 'This field is required';
|
|
6152
|
+
if (errors['email'])
|
|
6153
|
+
return 'Please enter a valid email address';
|
|
6154
|
+
if (errors['phone'])
|
|
6155
|
+
return 'Please enter a valid phone number';
|
|
6156
|
+
if (errors['url'])
|
|
6157
|
+
return 'Please enter a valid URL';
|
|
6158
|
+
if (errors['minLength'])
|
|
6159
|
+
return `Minimum length is ${errors['minLength'].min} characters`;
|
|
6160
|
+
if (errors['maxLength'])
|
|
6161
|
+
return `Maximum length is ${errors['maxLength'].max} characters`;
|
|
6162
|
+
if (errors['min'])
|
|
6163
|
+
return `Minimum value is ${errors['min'].min}`;
|
|
6164
|
+
if (errors['max'])
|
|
6165
|
+
return `Maximum value is ${errors['max'].max}`;
|
|
6166
|
+
if (errors['minDate'])
|
|
6167
|
+
return `Date must be after ${errors['minDate'].minDate}`;
|
|
6168
|
+
if (errors['maxDate'])
|
|
6169
|
+
return `Date must be before ${errors['maxDate'].maxDate}`;
|
|
6170
|
+
if (errors['pattern'])
|
|
6171
|
+
return errors['pattern'].message || 'Invalid format';
|
|
6172
|
+
return 'Invalid value';
|
|
5543
6173
|
}
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
const SAMPLE_FORMS = {
|
|
6177
|
+
// Simple Contact Form
|
|
6178
|
+
contactForm: `{
|
|
6179
|
+
"entityType": "CONTACT",
|
|
6180
|
+
"label": "Contact Us",
|
|
6181
|
+
"description": "Send us a message",
|
|
6182
|
+
"formType": "SECTION",
|
|
6183
|
+
"sectionConfig": {
|
|
6184
|
+
"children": [
|
|
6185
|
+
{
|
|
6186
|
+
"name": "name",
|
|
6187
|
+
"label": "Full Name",
|
|
6188
|
+
"type": "TEXT_INPUT",
|
|
6189
|
+
"subType": "SHORT",
|
|
6190
|
+
"required": true,
|
|
6191
|
+
"hint": "Enter your full name"
|
|
6192
|
+
},
|
|
6193
|
+
{
|
|
6194
|
+
"name": "email",
|
|
6195
|
+
"label": "Email Address",
|
|
6196
|
+
"type": "TEXT_INPUT",
|
|
6197
|
+
"subType": "EMAIL",
|
|
6198
|
+
"required": true,
|
|
6199
|
+
"hint": "your.email@example.com"
|
|
6200
|
+
},
|
|
6201
|
+
{
|
|
6202
|
+
"name": "message",
|
|
6203
|
+
"label": "Message",
|
|
6204
|
+
"type": "TEXT_INPUT",
|
|
6205
|
+
"subType": "LONG",
|
|
6206
|
+
"required": true,
|
|
6207
|
+
"textConfig": {
|
|
6208
|
+
"length": {
|
|
6209
|
+
"min": 10,
|
|
6210
|
+
"max": 500
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
]
|
|
6215
|
+
}
|
|
5544
6216
|
}`,
|
|
5545
6217
|
// User Registration with Stepper
|
|
5546
|
-
registrationForm: `{
|
|
5547
|
-
"entityType": "USER",
|
|
5548
|
-
"label": "User Registration",
|
|
5549
|
-
"description": "Create your account",
|
|
5550
|
-
"formType": "STEPPER",
|
|
5551
|
-
"stepperConfig": {
|
|
5552
|
-
"children": [
|
|
5553
|
-
{
|
|
5554
|
-
"type": "GROUP",
|
|
5555
|
-
"subType": "SECTION",
|
|
5556
|
-
"sectionConfig": {
|
|
5557
|
-
"label": "Personal Information",
|
|
5558
|
-
"children": [
|
|
5559
|
-
{
|
|
5560
|
-
"type": "ROW",
|
|
5561
|
-
"subType": "HORIZONTAL",
|
|
5562
|
-
"children": [
|
|
5563
|
-
{
|
|
5564
|
-
"name": "firstName",
|
|
5565
|
-
"label": "First Name",
|
|
5566
|
-
"type": "TEXT_INPUT",
|
|
5567
|
-
"subType": "SHORT",
|
|
5568
|
-
"required": true
|
|
5569
|
-
},
|
|
5570
|
-
{
|
|
5571
|
-
"name": "lastName",
|
|
5572
|
-
"label": "Last Name",
|
|
5573
|
-
"type": "TEXT_INPUT",
|
|
5574
|
-
"subType": "SHORT",
|
|
5575
|
-
"required": true
|
|
5576
|
-
}
|
|
5577
|
-
]
|
|
5578
|
-
},
|
|
5579
|
-
{
|
|
5580
|
-
"name": "fullName",
|
|
5581
|
-
"label": "Full Name",
|
|
5582
|
-
"type": "GENERATED",
|
|
5583
|
-
"subType": "FORMULA",
|
|
5584
|
-
"generatedConfig": {
|
|
5585
|
-
"formula": "function fullName(first, last) { return (first || '') + ' ' + (last || ''); }",
|
|
5586
|
-
"variables": ["firstName", "lastName"]
|
|
5587
|
-
}
|
|
5588
|
-
},
|
|
5589
|
-
{
|
|
5590
|
-
"name": "dateOfBirth",
|
|
5591
|
-
"label": "Date of Birth",
|
|
5592
|
-
"type": "DATE",
|
|
5593
|
-
"subType": "SINGLE",
|
|
5594
|
-
"required": true,
|
|
5595
|
-
"dateConfig": {
|
|
5596
|
-
"allowFuture": false
|
|
5597
|
-
}
|
|
5598
|
-
}
|
|
5599
|
-
]
|
|
5600
|
-
}
|
|
5601
|
-
},
|
|
5602
|
-
{
|
|
5603
|
-
"type": "GROUP",
|
|
5604
|
-
"subType": "SECTION",
|
|
5605
|
-
"sectionConfig": {
|
|
5606
|
-
"label": "Contact Information",
|
|
5607
|
-
"children": [
|
|
5608
|
-
{
|
|
5609
|
-
"name": "email",
|
|
5610
|
-
"label": "Email",
|
|
5611
|
-
"type": "TEXT_INPUT",
|
|
5612
|
-
"subType": "EMAIL",
|
|
5613
|
-
"required": true
|
|
5614
|
-
},
|
|
5615
|
-
{
|
|
5616
|
-
"name": "phone",
|
|
5617
|
-
"label": "Phone Number",
|
|
5618
|
-
"type": "TEXT_INPUT",
|
|
5619
|
-
"subType": "PHONE",
|
|
5620
|
-
"required": true
|
|
5621
|
-
}
|
|
5622
|
-
]
|
|
5623
|
-
}
|
|
5624
|
-
},
|
|
5625
|
-
{
|
|
5626
|
-
"type": "GROUP",
|
|
5627
|
-
"subType": "SECTION",
|
|
5628
|
-
"sectionConfig": {
|
|
5629
|
-
"label": "Preferences",
|
|
5630
|
-
"children": [
|
|
5631
|
-
{
|
|
5632
|
-
"name": "notifications",
|
|
5633
|
-
"label": "Enable Email Notifications",
|
|
5634
|
-
"type": "SWITCH",
|
|
5635
|
-
"subType": "BOOL",
|
|
5636
|
-
"defaultValue": true
|
|
5637
|
-
},
|
|
5638
|
-
{
|
|
5639
|
-
"name": "interests",
|
|
5640
|
-
"label": "Interests",
|
|
5641
|
-
"type": "CHIP",
|
|
5642
|
-
"subType": "MULTIPLE",
|
|
5643
|
-
"optionConfig": {
|
|
5644
|
-
"optionList": [
|
|
5645
|
-
{ "label": "Technology", "code": "TECH" },
|
|
5646
|
-
{ "label": "Sports", "code": "SPORTS" },
|
|
5647
|
-
{ "label": "Music", "code": "MUSIC" },
|
|
5648
|
-
{ "label": "Travel", "code": "TRAVEL" }
|
|
5649
|
-
]
|
|
5650
|
-
}
|
|
5651
|
-
}
|
|
5652
|
-
]
|
|
5653
|
-
}
|
|
5654
|
-
}
|
|
5655
|
-
],
|
|
5656
|
-
"showStep": true,
|
|
5657
|
-
"isHorizontal": true
|
|
5658
|
-
}
|
|
6218
|
+
registrationForm: `{
|
|
6219
|
+
"entityType": "USER",
|
|
6220
|
+
"label": "User Registration",
|
|
6221
|
+
"description": "Create your account",
|
|
6222
|
+
"formType": "STEPPER",
|
|
6223
|
+
"stepperConfig": {
|
|
6224
|
+
"children": [
|
|
6225
|
+
{
|
|
6226
|
+
"type": "GROUP",
|
|
6227
|
+
"subType": "SECTION",
|
|
6228
|
+
"sectionConfig": {
|
|
6229
|
+
"label": "Personal Information",
|
|
6230
|
+
"children": [
|
|
6231
|
+
{
|
|
6232
|
+
"type": "ROW",
|
|
6233
|
+
"subType": "HORIZONTAL",
|
|
6234
|
+
"children": [
|
|
6235
|
+
{
|
|
6236
|
+
"name": "firstName",
|
|
6237
|
+
"label": "First Name",
|
|
6238
|
+
"type": "TEXT_INPUT",
|
|
6239
|
+
"subType": "SHORT",
|
|
6240
|
+
"required": true
|
|
6241
|
+
},
|
|
6242
|
+
{
|
|
6243
|
+
"name": "lastName",
|
|
6244
|
+
"label": "Last Name",
|
|
6245
|
+
"type": "TEXT_INPUT",
|
|
6246
|
+
"subType": "SHORT",
|
|
6247
|
+
"required": true
|
|
6248
|
+
}
|
|
6249
|
+
]
|
|
6250
|
+
},
|
|
6251
|
+
{
|
|
6252
|
+
"name": "fullName",
|
|
6253
|
+
"label": "Full Name",
|
|
6254
|
+
"type": "GENERATED",
|
|
6255
|
+
"subType": "FORMULA",
|
|
6256
|
+
"generatedConfig": {
|
|
6257
|
+
"formula": "function fullName(first, last) { return (first || '') + ' ' + (last || ''); }",
|
|
6258
|
+
"variables": ["firstName", "lastName"]
|
|
6259
|
+
}
|
|
6260
|
+
},
|
|
6261
|
+
{
|
|
6262
|
+
"name": "dateOfBirth",
|
|
6263
|
+
"label": "Date of Birth",
|
|
6264
|
+
"type": "DATE",
|
|
6265
|
+
"subType": "SINGLE",
|
|
6266
|
+
"required": true,
|
|
6267
|
+
"dateConfig": {
|
|
6268
|
+
"allowFuture": false
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
]
|
|
6272
|
+
}
|
|
6273
|
+
},
|
|
6274
|
+
{
|
|
6275
|
+
"type": "GROUP",
|
|
6276
|
+
"subType": "SECTION",
|
|
6277
|
+
"sectionConfig": {
|
|
6278
|
+
"label": "Contact Information",
|
|
6279
|
+
"children": [
|
|
6280
|
+
{
|
|
6281
|
+
"name": "email",
|
|
6282
|
+
"label": "Email",
|
|
6283
|
+
"type": "TEXT_INPUT",
|
|
6284
|
+
"subType": "EMAIL",
|
|
6285
|
+
"required": true
|
|
6286
|
+
},
|
|
6287
|
+
{
|
|
6288
|
+
"name": "phone",
|
|
6289
|
+
"label": "Phone Number",
|
|
6290
|
+
"type": "TEXT_INPUT",
|
|
6291
|
+
"subType": "PHONE",
|
|
6292
|
+
"required": true
|
|
6293
|
+
}
|
|
6294
|
+
]
|
|
6295
|
+
}
|
|
6296
|
+
},
|
|
6297
|
+
{
|
|
6298
|
+
"type": "GROUP",
|
|
6299
|
+
"subType": "SECTION",
|
|
6300
|
+
"sectionConfig": {
|
|
6301
|
+
"label": "Preferences",
|
|
6302
|
+
"children": [
|
|
6303
|
+
{
|
|
6304
|
+
"name": "notifications",
|
|
6305
|
+
"label": "Enable Email Notifications",
|
|
6306
|
+
"type": "SWITCH",
|
|
6307
|
+
"subType": "BOOL",
|
|
6308
|
+
"defaultValue": true
|
|
6309
|
+
},
|
|
6310
|
+
{
|
|
6311
|
+
"name": "interests",
|
|
6312
|
+
"label": "Interests",
|
|
6313
|
+
"type": "CHIP",
|
|
6314
|
+
"subType": "MULTIPLE",
|
|
6315
|
+
"optionConfig": {
|
|
6316
|
+
"optionList": [
|
|
6317
|
+
{ "label": "Technology", "code": "TECH" },
|
|
6318
|
+
{ "label": "Sports", "code": "SPORTS" },
|
|
6319
|
+
{ "label": "Music", "code": "MUSIC" },
|
|
6320
|
+
{ "label": "Travel", "code": "TRAVEL" }
|
|
6321
|
+
]
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
6324
|
+
]
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
],
|
|
6328
|
+
"showStep": true,
|
|
6329
|
+
"isHorizontal": true
|
|
6330
|
+
}
|
|
5659
6331
|
}`,
|
|
5660
6332
|
// Survey Form with Conditional Fields
|
|
5661
|
-
surveyForm: `{
|
|
5662
|
-
"entityType": "SURVEY",
|
|
5663
|
-
"label": "Customer Satisfaction Survey",
|
|
5664
|
-
"formType": "SECTION",
|
|
5665
|
-
"sectionConfig": {
|
|
5666
|
-
"children": [
|
|
5667
|
-
{
|
|
5668
|
-
"name": "overallRating",
|
|
5669
|
-
"label": "Overall Experience",
|
|
5670
|
-
"type": "RATING",
|
|
5671
|
-
"subType": "STAR",
|
|
5672
|
-
"required": true,
|
|
5673
|
-
"ratingConfig": {
|
|
5674
|
-
"maxRating": 5,
|
|
5675
|
-
"allowHalf": false
|
|
5676
|
-
}
|
|
5677
|
-
},
|
|
5678
|
-
{
|
|
5679
|
-
"name": "wouldRecommend",
|
|
5680
|
-
"label": "Would you recommend us?",
|
|
5681
|
-
"type": "RADIO",
|
|
5682
|
-
"subType": "SINGLE",
|
|
5683
|
-
"required": true,
|
|
5684
|
-
"optionConfig": {
|
|
5685
|
-
"optionList": [
|
|
5686
|
-
{ "label": "Yes", "code": "YES" },
|
|
5687
|
-
{ "label": "No", "code": "NO" },
|
|
5688
|
-
{ "label": "Maybe", "code": "MAYBE" }
|
|
5689
|
-
]
|
|
5690
|
-
}
|
|
5691
|
-
},
|
|
5692
|
-
{
|
|
5693
|
-
"name": "reasonForNo",
|
|
5694
|
-
"label": "Why not?",
|
|
5695
|
-
"type": "TEXT_INPUT",
|
|
5696
|
-
"subType": "LONG",
|
|
5697
|
-
"visibilityExpression": "wouldRecommend === 'NO'",
|
|
5698
|
-
"required": true
|
|
5699
|
-
},
|
|
5700
|
-
{
|
|
5701
|
-
"name": "improvements",
|
|
5702
|
-
"label": "What can we improve?",
|
|
5703
|
-
"type": "CHECKBOX",
|
|
5704
|
-
"subType": "LIST",
|
|
5705
|
-
"optionConfig": {
|
|
5706
|
-
"optionList": [
|
|
5707
|
-
{ "label": "Customer Service", "code": "SERVICE" },
|
|
5708
|
-
{ "label": "Product Quality", "code": "QUALITY" },
|
|
5709
|
-
{ "label": "Pricing", "code": "PRICE" },
|
|
5710
|
-
{ "label": "Delivery Speed", "code": "DELIVERY" }
|
|
5711
|
-
]
|
|
5712
|
-
}
|
|
5713
|
-
},
|
|
5714
|
-
{
|
|
5715
|
-
"name": "additionalComments",
|
|
5716
|
-
"label": "Additional Comments",
|
|
5717
|
-
"type": "TEXT_INPUT",
|
|
5718
|
-
"subType": "LONG",
|
|
5719
|
-
"textConfig": {
|
|
5720
|
-
"length": {
|
|
5721
|
-
"max": 1000
|
|
5722
|
-
}
|
|
5723
|
-
}
|
|
5724
|
-
}
|
|
5725
|
-
]
|
|
5726
|
-
}
|
|
6333
|
+
surveyForm: `{
|
|
6334
|
+
"entityType": "SURVEY",
|
|
6335
|
+
"label": "Customer Satisfaction Survey",
|
|
6336
|
+
"formType": "SECTION",
|
|
6337
|
+
"sectionConfig": {
|
|
6338
|
+
"children": [
|
|
6339
|
+
{
|
|
6340
|
+
"name": "overallRating",
|
|
6341
|
+
"label": "Overall Experience",
|
|
6342
|
+
"type": "RATING",
|
|
6343
|
+
"subType": "STAR",
|
|
6344
|
+
"required": true,
|
|
6345
|
+
"ratingConfig": {
|
|
6346
|
+
"maxRating": 5,
|
|
6347
|
+
"allowHalf": false
|
|
6348
|
+
}
|
|
6349
|
+
},
|
|
6350
|
+
{
|
|
6351
|
+
"name": "wouldRecommend",
|
|
6352
|
+
"label": "Would you recommend us?",
|
|
6353
|
+
"type": "RADIO",
|
|
6354
|
+
"subType": "SINGLE",
|
|
6355
|
+
"required": true,
|
|
6356
|
+
"optionConfig": {
|
|
6357
|
+
"optionList": [
|
|
6358
|
+
{ "label": "Yes", "code": "YES" },
|
|
6359
|
+
{ "label": "No", "code": "NO" },
|
|
6360
|
+
{ "label": "Maybe", "code": "MAYBE" }
|
|
6361
|
+
]
|
|
6362
|
+
}
|
|
6363
|
+
},
|
|
6364
|
+
{
|
|
6365
|
+
"name": "reasonForNo",
|
|
6366
|
+
"label": "Why not?",
|
|
6367
|
+
"type": "TEXT_INPUT",
|
|
6368
|
+
"subType": "LONG",
|
|
6369
|
+
"visibilityExpression": "wouldRecommend === 'NO'",
|
|
6370
|
+
"required": true
|
|
6371
|
+
},
|
|
6372
|
+
{
|
|
6373
|
+
"name": "improvements",
|
|
6374
|
+
"label": "What can we improve?",
|
|
6375
|
+
"type": "CHECKBOX",
|
|
6376
|
+
"subType": "LIST",
|
|
6377
|
+
"optionConfig": {
|
|
6378
|
+
"optionList": [
|
|
6379
|
+
{ "label": "Customer Service", "code": "SERVICE" },
|
|
6380
|
+
{ "label": "Product Quality", "code": "QUALITY" },
|
|
6381
|
+
{ "label": "Pricing", "code": "PRICE" },
|
|
6382
|
+
{ "label": "Delivery Speed", "code": "DELIVERY" }
|
|
6383
|
+
]
|
|
6384
|
+
}
|
|
6385
|
+
},
|
|
6386
|
+
{
|
|
6387
|
+
"name": "additionalComments",
|
|
6388
|
+
"label": "Additional Comments",
|
|
6389
|
+
"type": "TEXT_INPUT",
|
|
6390
|
+
"subType": "LONG",
|
|
6391
|
+
"textConfig": {
|
|
6392
|
+
"length": {
|
|
6393
|
+
"max": 1000
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
]
|
|
6398
|
+
}
|
|
5727
6399
|
}`,
|
|
5728
6400
|
// Job Application Form
|
|
5729
|
-
jobApplicationForm: `{
|
|
5730
|
-
"entityType": "JOB_APPLICATION",
|
|
5731
|
-
"label": "Job Application",
|
|
5732
|
-
"description": "Apply for a position at our company",
|
|
5733
|
-
"formType": "STEPPER",
|
|
5734
|
-
"stepperConfig": {
|
|
5735
|
-
"children": [
|
|
5736
|
-
{
|
|
5737
|
-
"type": "GROUP",
|
|
5738
|
-
"subType": "SECTION",
|
|
5739
|
-
"sectionConfig": {
|
|
5740
|
-
"label": "Personal Details",
|
|
5741
|
-
"children": [
|
|
5742
|
-
{
|
|
5743
|
-
"type": "ROW",
|
|
5744
|
-
"subType": "HORIZONTAL",
|
|
5745
|
-
"children": [
|
|
5746
|
-
{
|
|
5747
|
-
"name": "firstName",
|
|
5748
|
-
"label": "First Name",
|
|
5749
|
-
"type": "TEXT_INPUT",
|
|
5750
|
-
"subType": "SHORT",
|
|
5751
|
-
"required": true
|
|
5752
|
-
},
|
|
5753
|
-
{
|
|
5754
|
-
"name": "lastName",
|
|
5755
|
-
"label": "Last Name",
|
|
5756
|
-
"type": "TEXT_INPUT",
|
|
5757
|
-
"subType": "SHORT",
|
|
5758
|
-
"required": true
|
|
5759
|
-
}
|
|
5760
|
-
]
|
|
5761
|
-
},
|
|
5762
|
-
{
|
|
5763
|
-
"name": "email",
|
|
5764
|
-
"label": "Email",
|
|
5765
|
-
"type": "TEXT_INPUT",
|
|
5766
|
-
"subType": "EMAIL",
|
|
5767
|
-
"required": true
|
|
5768
|
-
},
|
|
5769
|
-
{
|
|
5770
|
-
"name": "phone",
|
|
5771
|
-
"label": "Phone",
|
|
5772
|
-
"type": "TEXT_INPUT",
|
|
5773
|
-
"subType": "PHONE",
|
|
5774
|
-
"required": true
|
|
5775
|
-
}
|
|
5776
|
-
]
|
|
5777
|
-
}
|
|
5778
|
-
},
|
|
5779
|
-
{
|
|
5780
|
-
"type": "GROUP",
|
|
5781
|
-
"subType": "SECTION",
|
|
5782
|
-
"sectionConfig": {
|
|
5783
|
-
"label": "Experience",
|
|
5784
|
-
"allowMulti": true,
|
|
5785
|
-
"name": "experienceList",
|
|
5786
|
-
"children": [
|
|
5787
|
-
{
|
|
5788
|
-
"name": "company",
|
|
5789
|
-
"label": "Company Name",
|
|
5790
|
-
"type": "TEXT_INPUT",
|
|
5791
|
-
"subType": "SHORT",
|
|
5792
|
-
"required": true
|
|
5793
|
-
},
|
|
5794
|
-
{
|
|
5795
|
-
"name": "position",
|
|
5796
|
-
"label": "Position",
|
|
5797
|
-
"type": "TEXT_INPUT",
|
|
5798
|
-
"subType": "SHORT",
|
|
5799
|
-
"required": true
|
|
5800
|
-
},
|
|
5801
|
-
{
|
|
5802
|
-
"type": "ROW",
|
|
5803
|
-
"subType": "HORIZONTAL",
|
|
5804
|
-
"children": [
|
|
5805
|
-
{
|
|
5806
|
-
"name": "startDate",
|
|
5807
|
-
"label": "Start Date",
|
|
5808
|
-
"type": "DATE",
|
|
5809
|
-
"subType": "SINGLE",
|
|
5810
|
-
"required": true
|
|
5811
|
-
},
|
|
5812
|
-
{
|
|
5813
|
-
"name": "endDate",
|
|
5814
|
-
"label": "End Date",
|
|
5815
|
-
"type": "DATE",
|
|
5816
|
-
"subType": "SINGLE"
|
|
5817
|
-
}
|
|
5818
|
-
]
|
|
5819
|
-
},
|
|
5820
|
-
{
|
|
5821
|
-
"name": "responsibilities",
|
|
5822
|
-
"label": "Key Responsibilities",
|
|
5823
|
-
"type": "TEXT_INPUT",
|
|
5824
|
-
"subType": "LONG"
|
|
5825
|
-
}
|
|
5826
|
-
]
|
|
5827
|
-
}
|
|
5828
|
-
},
|
|
5829
|
-
{
|
|
5830
|
-
"type": "GROUP",
|
|
5831
|
-
"subType": "SECTION",
|
|
5832
|
-
"sectionConfig": {
|
|
5833
|
-
"label": "Skills & Qualifications",
|
|
5834
|
-
"children": [
|
|
5835
|
-
{
|
|
5836
|
-
"name": "skills",
|
|
5837
|
-
"label": "Technical Skills",
|
|
5838
|
-
"type": "CHIP",
|
|
5839
|
-
"subType": "MULTIPLE",
|
|
5840
|
-
"optionConfig": {
|
|
5841
|
-
"optionList": [
|
|
5842
|
-
{ "label": "JavaScript", "code": "JS" },
|
|
5843
|
-
{ "label": "TypeScript", "code": "TS" },
|
|
5844
|
-
{ "label": "Angular", "code": "ANGULAR" },
|
|
5845
|
-
{ "label": "React", "code": "REACT" },
|
|
5846
|
-
{ "label": "Node.js", "code": "NODE" },
|
|
5847
|
-
{ "label": "Python", "code": "PYTHON" }
|
|
5848
|
-
]
|
|
5849
|
-
}
|
|
5850
|
-
},
|
|
5851
|
-
{
|
|
5852
|
-
"name": "yearsOfExperience",
|
|
5853
|
-
"label": "Years of Experience",
|
|
5854
|
-
"type": "NUMBER_INPUT",
|
|
5855
|
-
"subType": "INTEGER",
|
|
5856
|
-
"required": true,
|
|
5857
|
-
"numberConfig": {
|
|
5858
|
-
"min": 0,
|
|
5859
|
-
"max": 50
|
|
5860
|
-
}
|
|
5861
|
-
},
|
|
5862
|
-
{
|
|
5863
|
-
"name": "availableToStart",
|
|
5864
|
-
"label": "Available to Start",
|
|
5865
|
-
"type": "DATE",
|
|
5866
|
-
"subType": "SINGLE",
|
|
5867
|
-
"required": true,
|
|
5868
|
-
"dateConfig": {
|
|
5869
|
-
"allowFuture": true
|
|
5870
|
-
}
|
|
5871
|
-
}
|
|
5872
|
-
]
|
|
5873
|
-
}
|
|
5874
|
-
}
|
|
5875
|
-
],
|
|
5876
|
-
"showStep": true,
|
|
5877
|
-
"isHorizontal": true
|
|
5878
|
-
}
|
|
5879
|
-
}
|
|
6401
|
+
jobApplicationForm: `{
|
|
6402
|
+
"entityType": "JOB_APPLICATION",
|
|
6403
|
+
"label": "Job Application",
|
|
6404
|
+
"description": "Apply for a position at our company",
|
|
6405
|
+
"formType": "STEPPER",
|
|
6406
|
+
"stepperConfig": {
|
|
6407
|
+
"children": [
|
|
6408
|
+
{
|
|
6409
|
+
"type": "GROUP",
|
|
6410
|
+
"subType": "SECTION",
|
|
6411
|
+
"sectionConfig": {
|
|
6412
|
+
"label": "Personal Details",
|
|
6413
|
+
"children": [
|
|
6414
|
+
{
|
|
6415
|
+
"type": "ROW",
|
|
6416
|
+
"subType": "HORIZONTAL",
|
|
6417
|
+
"children": [
|
|
6418
|
+
{
|
|
6419
|
+
"name": "firstName",
|
|
6420
|
+
"label": "First Name",
|
|
6421
|
+
"type": "TEXT_INPUT",
|
|
6422
|
+
"subType": "SHORT",
|
|
6423
|
+
"required": true
|
|
6424
|
+
},
|
|
6425
|
+
{
|
|
6426
|
+
"name": "lastName",
|
|
6427
|
+
"label": "Last Name",
|
|
6428
|
+
"type": "TEXT_INPUT",
|
|
6429
|
+
"subType": "SHORT",
|
|
6430
|
+
"required": true
|
|
6431
|
+
}
|
|
6432
|
+
]
|
|
6433
|
+
},
|
|
6434
|
+
{
|
|
6435
|
+
"name": "email",
|
|
6436
|
+
"label": "Email",
|
|
6437
|
+
"type": "TEXT_INPUT",
|
|
6438
|
+
"subType": "EMAIL",
|
|
6439
|
+
"required": true
|
|
6440
|
+
},
|
|
6441
|
+
{
|
|
6442
|
+
"name": "phone",
|
|
6443
|
+
"label": "Phone",
|
|
6444
|
+
"type": "TEXT_INPUT",
|
|
6445
|
+
"subType": "PHONE",
|
|
6446
|
+
"required": true
|
|
6447
|
+
}
|
|
6448
|
+
]
|
|
6449
|
+
}
|
|
6450
|
+
},
|
|
6451
|
+
{
|
|
6452
|
+
"type": "GROUP",
|
|
6453
|
+
"subType": "SECTION",
|
|
6454
|
+
"sectionConfig": {
|
|
6455
|
+
"label": "Experience",
|
|
6456
|
+
"allowMulti": true,
|
|
6457
|
+
"name": "experienceList",
|
|
6458
|
+
"children": [
|
|
6459
|
+
{
|
|
6460
|
+
"name": "company",
|
|
6461
|
+
"label": "Company Name",
|
|
6462
|
+
"type": "TEXT_INPUT",
|
|
6463
|
+
"subType": "SHORT",
|
|
6464
|
+
"required": true
|
|
6465
|
+
},
|
|
6466
|
+
{
|
|
6467
|
+
"name": "position",
|
|
6468
|
+
"label": "Position",
|
|
6469
|
+
"type": "TEXT_INPUT",
|
|
6470
|
+
"subType": "SHORT",
|
|
6471
|
+
"required": true
|
|
6472
|
+
},
|
|
6473
|
+
{
|
|
6474
|
+
"type": "ROW",
|
|
6475
|
+
"subType": "HORIZONTAL",
|
|
6476
|
+
"children": [
|
|
6477
|
+
{
|
|
6478
|
+
"name": "startDate",
|
|
6479
|
+
"label": "Start Date",
|
|
6480
|
+
"type": "DATE",
|
|
6481
|
+
"subType": "SINGLE",
|
|
6482
|
+
"required": true
|
|
6483
|
+
},
|
|
6484
|
+
{
|
|
6485
|
+
"name": "endDate",
|
|
6486
|
+
"label": "End Date",
|
|
6487
|
+
"type": "DATE",
|
|
6488
|
+
"subType": "SINGLE"
|
|
6489
|
+
}
|
|
6490
|
+
]
|
|
6491
|
+
},
|
|
6492
|
+
{
|
|
6493
|
+
"name": "responsibilities",
|
|
6494
|
+
"label": "Key Responsibilities",
|
|
6495
|
+
"type": "TEXT_INPUT",
|
|
6496
|
+
"subType": "LONG"
|
|
6497
|
+
}
|
|
6498
|
+
]
|
|
6499
|
+
}
|
|
6500
|
+
},
|
|
6501
|
+
{
|
|
6502
|
+
"type": "GROUP",
|
|
6503
|
+
"subType": "SECTION",
|
|
6504
|
+
"sectionConfig": {
|
|
6505
|
+
"label": "Skills & Qualifications",
|
|
6506
|
+
"children": [
|
|
6507
|
+
{
|
|
6508
|
+
"name": "skills",
|
|
6509
|
+
"label": "Technical Skills",
|
|
6510
|
+
"type": "CHIP",
|
|
6511
|
+
"subType": "MULTIPLE",
|
|
6512
|
+
"optionConfig": {
|
|
6513
|
+
"optionList": [
|
|
6514
|
+
{ "label": "JavaScript", "code": "JS" },
|
|
6515
|
+
{ "label": "TypeScript", "code": "TS" },
|
|
6516
|
+
{ "label": "Angular", "code": "ANGULAR" },
|
|
6517
|
+
{ "label": "React", "code": "REACT" },
|
|
6518
|
+
{ "label": "Node.js", "code": "NODE" },
|
|
6519
|
+
{ "label": "Python", "code": "PYTHON" }
|
|
6520
|
+
]
|
|
6521
|
+
}
|
|
6522
|
+
},
|
|
6523
|
+
{
|
|
6524
|
+
"name": "yearsOfExperience",
|
|
6525
|
+
"label": "Years of Experience",
|
|
6526
|
+
"type": "NUMBER_INPUT",
|
|
6527
|
+
"subType": "INTEGER",
|
|
6528
|
+
"required": true,
|
|
6529
|
+
"numberConfig": {
|
|
6530
|
+
"min": 0,
|
|
6531
|
+
"max": 50
|
|
6532
|
+
}
|
|
6533
|
+
},
|
|
6534
|
+
{
|
|
6535
|
+
"name": "availableToStart",
|
|
6536
|
+
"label": "Available to Start",
|
|
6537
|
+
"type": "DATE",
|
|
6538
|
+
"subType": "SINGLE",
|
|
6539
|
+
"required": true,
|
|
6540
|
+
"dateConfig": {
|
|
6541
|
+
"allowFuture": true
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
]
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
],
|
|
6548
|
+
"showStep": true,
|
|
6549
|
+
"isHorizontal": true
|
|
6550
|
+
}
|
|
6551
|
+
}`,
|
|
6552
|
+
// Donor Form based on UI
|
|
6553
|
+
donorForm: `{
|
|
6554
|
+
"entityType": "DONOR",
|
|
6555
|
+
"label": "Donor Form",
|
|
6556
|
+
"formType": "SECTION",
|
|
6557
|
+
"sectionConfig": {
|
|
6558
|
+
"children": [
|
|
6559
|
+
{
|
|
6560
|
+
"type": "GROUP",
|
|
6561
|
+
"subType": "SECTION",
|
|
6562
|
+
"sectionConfig": {
|
|
6563
|
+
"label": "Donor Details",
|
|
6564
|
+
"name": "donorDetails",
|
|
6565
|
+
"children": [
|
|
6566
|
+
{
|
|
6567
|
+
"name": "legalName",
|
|
6568
|
+
"label": "Donor Legal Name",
|
|
6569
|
+
"type": "TEXT_INPUT",
|
|
6570
|
+
"subType": "SHORT",
|
|
6571
|
+
"required": true,
|
|
6572
|
+
"placeholder": "Enter the name"
|
|
6573
|
+
},
|
|
6574
|
+
{
|
|
6575
|
+
"type": "ROW",
|
|
6576
|
+
"subType": "HORIZONTAL",
|
|
6577
|
+
"children": [
|
|
6578
|
+
{
|
|
6579
|
+
"name": "shortCode",
|
|
6580
|
+
"label": "Donor Short Code",
|
|
6581
|
+
"type": "TEXT_INPUT",
|
|
6582
|
+
"subType": "SHORT",
|
|
6583
|
+
"colSpan": 3,
|
|
6584
|
+
"disabled": true,
|
|
6585
|
+
"defaultValue": "DLP-24-L1",
|
|
6586
|
+
"hint": "System-generated, unique internal code"
|
|
6587
|
+
},
|
|
6588
|
+
{
|
|
6589
|
+
"name": "donorType",
|
|
6590
|
+
"label": "Donor Type",
|
|
6591
|
+
"type": "DROPDOWN",
|
|
6592
|
+
"subType": "SINGLE",
|
|
6593
|
+
"colSpan": 3,
|
|
6594
|
+
"required": true,
|
|
6595
|
+
"placeholder": "Select",
|
|
6596
|
+
"optionConfig": {
|
|
6597
|
+
"optionList": [
|
|
6598
|
+
{ "label": "Individual", "code": "INDIVIDUAL" },
|
|
6599
|
+
{ "label": "Corporate", "code": "CORPORATE" },
|
|
6600
|
+
{ "label": "Trust", "code": "TRUST" }
|
|
6601
|
+
]
|
|
6602
|
+
}
|
|
6603
|
+
},
|
|
6604
|
+
{
|
|
6605
|
+
"name": "regNumber",
|
|
6606
|
+
"label": "Registration / Legal Entity Number",
|
|
6607
|
+
"type": "TEXT_INPUT",
|
|
6608
|
+
"subType": "SHORT",
|
|
6609
|
+
"colSpan": 3,
|
|
6610
|
+
"placeholder": "Type",
|
|
6611
|
+
"hint": "CIN / Trust Reg. No / Govt ID (if applicable)"
|
|
6612
|
+
},
|
|
6613
|
+
{
|
|
6614
|
+
"name": "country",
|
|
6615
|
+
"label": "Country of Incorporation",
|
|
6616
|
+
"type": "DROPDOWN",
|
|
6617
|
+
"subType": "SINGLE",
|
|
6618
|
+
"colSpan": 3,
|
|
6619
|
+
"required": true,
|
|
6620
|
+
"placeholder": "Select",
|
|
6621
|
+
"optionConfig": {
|
|
6622
|
+
"optionList": [
|
|
6623
|
+
{ "label": "India", "code": "IN" },
|
|
6624
|
+
{ "label": "USA", "code": "US" },
|
|
6625
|
+
{ "label": "UK", "code": "UK" }
|
|
6626
|
+
]
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
]
|
|
6630
|
+
},
|
|
6631
|
+
{
|
|
6632
|
+
"type": "ROW",
|
|
6633
|
+
"subType": "HORIZONTAL",
|
|
6634
|
+
"children": [
|
|
6635
|
+
{
|
|
6636
|
+
"name": "profilePicture",
|
|
6637
|
+
"label": "Profile Picture",
|
|
6638
|
+
"type": "FILE_UPLOAD",
|
|
6639
|
+
"subType": "SINGLE",
|
|
6640
|
+
"colSpan": 3,
|
|
6641
|
+
"attachmentConfig": {
|
|
6642
|
+
"multiple": false,
|
|
6643
|
+
"maxSizeMB": 2,
|
|
6644
|
+
"accept": "image/*",
|
|
6645
|
+
"acceptLabel": "JPG, PNG, SVG (max 2 MB)"
|
|
6646
|
+
}
|
|
6647
|
+
},
|
|
6648
|
+
{
|
|
6649
|
+
"name": "website",
|
|
6650
|
+
"label": "Organization Website (Optional)",
|
|
6651
|
+
"type": "TEXT_INPUT",
|
|
6652
|
+
"subType": "SHORT",
|
|
6653
|
+
"colSpan": 9,
|
|
6654
|
+
"placeholder": "Add your website link or LinkedIn profile",
|
|
6655
|
+
"hint": "Add your website link to automatically fetch your logo, powered by Clearbit."
|
|
6656
|
+
}
|
|
6657
|
+
]
|
|
6658
|
+
}
|
|
6659
|
+
]
|
|
6660
|
+
}
|
|
6661
|
+
},
|
|
6662
|
+
{
|
|
6663
|
+
"type": "GROUP",
|
|
6664
|
+
"subType": "SECTION",
|
|
6665
|
+
"sectionConfig": {
|
|
6666
|
+
"label": "Primary Contact",
|
|
6667
|
+
"allowMulti": true,
|
|
6668
|
+
"name": "contacts",
|
|
6669
|
+
"children": [
|
|
6670
|
+
{
|
|
6671
|
+
"type": "ROW",
|
|
6672
|
+
"subType": "HORIZONTAL",
|
|
6673
|
+
"children": [
|
|
6674
|
+
{
|
|
6675
|
+
"name": "contactName",
|
|
6676
|
+
"label": "Full Name",
|
|
6677
|
+
"type": "TEXT_INPUT",
|
|
6678
|
+
"subType": "SHORT",
|
|
6679
|
+
"colSpan": 6,
|
|
6680
|
+
"required": true,
|
|
6681
|
+
"placeholder": "Enter the name"
|
|
6682
|
+
},
|
|
6683
|
+
{
|
|
6684
|
+
"name": "designation",
|
|
6685
|
+
"label": "Designation/Title",
|
|
6686
|
+
"type": "DROPDOWN",
|
|
6687
|
+
"subType": "SINGLE",
|
|
6688
|
+
"colSpan": 6,
|
|
6689
|
+
"required": true,
|
|
6690
|
+
"placeholder": "Select",
|
|
6691
|
+
"optionConfig": {
|
|
6692
|
+
"optionList": [
|
|
6693
|
+
{ "label": "CEO", "code": "CEO" },
|
|
6694
|
+
{ "label": "Manager", "code": "MANAGER" },
|
|
6695
|
+
{ "label": "Other", "code": "OTHER" }
|
|
6696
|
+
]
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
]
|
|
6700
|
+
},
|
|
6701
|
+
{
|
|
6702
|
+
"type": "ROW",
|
|
6703
|
+
"subType": "HORIZONTAL",
|
|
6704
|
+
"children": [
|
|
6705
|
+
{
|
|
6706
|
+
"name": "email",
|
|
6707
|
+
"label": "Email Address",
|
|
6708
|
+
"type": "TEXT_INPUT",
|
|
6709
|
+
"subType": "EMAIL",
|
|
6710
|
+
"colSpan": 4,
|
|
6711
|
+
"required": true,
|
|
6712
|
+
"placeholder": "Enter your email address"
|
|
6713
|
+
},
|
|
6714
|
+
{
|
|
6715
|
+
"name": "phone",
|
|
6716
|
+
"label": "Phone Number",
|
|
6717
|
+
"type": "TEXT_INPUT",
|
|
6718
|
+
"subType": "PHONE",
|
|
6719
|
+
"colSpan": 4,
|
|
6720
|
+
"required": true,
|
|
6721
|
+
"placeholder": "+91 7007713990"
|
|
6722
|
+
},
|
|
6723
|
+
{
|
|
6724
|
+
"name": "communicationPreference",
|
|
6725
|
+
"label": "Communication Preference",
|
|
6726
|
+
"type": "RADIO",
|
|
6727
|
+
"subType": "SINGLE",
|
|
6728
|
+
"colSpan": 4,
|
|
6729
|
+
"required": true,
|
|
6730
|
+
"optionConfig": {
|
|
6731
|
+
"optionList": [
|
|
6732
|
+
{ "label": "Email", "code": "EMAIL" },
|
|
6733
|
+
{ "label": "Phone Number", "code": "PHONE" }
|
|
6734
|
+
]
|
|
6735
|
+
}
|
|
6736
|
+
}
|
|
6737
|
+
]
|
|
6738
|
+
}
|
|
6739
|
+
]
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
]
|
|
6743
|
+
}
|
|
6744
|
+
}`,
|
|
6745
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
6746
|
+
// User Registration Form — Basic Details / Primary Address / User Role
|
|
6747
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
6748
|
+
userBasicDetailsForm: `{
|
|
6749
|
+
"entityType": "USER_REGISTRATION",
|
|
6750
|
+
"label": "User Registration",
|
|
6751
|
+
"description": "Fill in your details to create a new account",
|
|
6752
|
+
"formType": "SECTION",
|
|
6753
|
+
"sectionConfig": {
|
|
6754
|
+
"children": [
|
|
6755
|
+
|
|
6756
|
+
{
|
|
6757
|
+
"type": "GROUP",
|
|
6758
|
+
"subType": "SECTION",
|
|
6759
|
+
"sectionConfig": {
|
|
6760
|
+
"label": "Basic Details",
|
|
6761
|
+
"name": "basicDetails",
|
|
6762
|
+
"children": [
|
|
6763
|
+
|
|
6764
|
+
{
|
|
6765
|
+
"type": "ROW",
|
|
6766
|
+
"subType": "HORIZONTAL",
|
|
6767
|
+
"children": [
|
|
6768
|
+
{
|
|
6769
|
+
"name": "firstName",
|
|
6770
|
+
"label": "First Name",
|
|
6771
|
+
"type": "TEXT_INPUT",
|
|
6772
|
+
"subType": "SHORT",
|
|
6773
|
+
"required": true,
|
|
6774
|
+
"placeholder": "Enter first name"
|
|
6775
|
+
},
|
|
6776
|
+
{
|
|
6777
|
+
"name": "lastName",
|
|
6778
|
+
"label": "Last Name",
|
|
6779
|
+
"type": "TEXT_INPUT",
|
|
6780
|
+
"subType": "SHORT",
|
|
6781
|
+
"required": true,
|
|
6782
|
+
"placeholder": "Enter last name"
|
|
6783
|
+
}
|
|
6784
|
+
]
|
|
6785
|
+
},
|
|
6786
|
+
|
|
6787
|
+
{
|
|
6788
|
+
"type": "ROW",
|
|
6789
|
+
"subType": "HORIZONTAL",
|
|
6790
|
+
"children": [
|
|
6791
|
+
{
|
|
6792
|
+
"name": "contactNo",
|
|
6793
|
+
"label": "Contact No",
|
|
6794
|
+
"type": "TEXT_INPUT",
|
|
6795
|
+
"subType": "PHONE",
|
|
6796
|
+
"required": true,
|
|
6797
|
+
"placeholder": "+91 9999999999",
|
|
6798
|
+
"hint": "Enter a valid 10-digit mobile number"
|
|
6799
|
+
},
|
|
6800
|
+
{
|
|
6801
|
+
"name": "emailId",
|
|
6802
|
+
"label": "Email ID",
|
|
6803
|
+
"type": "TEXT_INPUT",
|
|
6804
|
+
"subType": "EMAIL",
|
|
6805
|
+
"required": true,
|
|
6806
|
+
"placeholder": "example@domain.com"
|
|
6807
|
+
}
|
|
6808
|
+
]
|
|
6809
|
+
},
|
|
6810
|
+
|
|
6811
|
+
{
|
|
6812
|
+
"type": "ROW",
|
|
6813
|
+
"subType": "HORIZONTAL",
|
|
6814
|
+
"children": [
|
|
6815
|
+
{
|
|
6816
|
+
"name": "loginId",
|
|
6817
|
+
"label": "Login / User ID",
|
|
6818
|
+
"type": "TEXT_INPUT",
|
|
6819
|
+
"subType": "SHORT",
|
|
6820
|
+
"required": true,
|
|
6821
|
+
"placeholder": "Choose a unique login ID",
|
|
6822
|
+
"hint": "Alphanumeric, no spaces",
|
|
6823
|
+
"textConfig": {
|
|
6824
|
+
"length": { "min": 4, "max": 30 },
|
|
6825
|
+
"pattern": "^[a-zA-Z0-9_]+$",
|
|
6826
|
+
"patternMessage": "Only letters, numbers and underscores are allowed"
|
|
6827
|
+
}
|
|
6828
|
+
},
|
|
6829
|
+
{
|
|
6830
|
+
"name": "password",
|
|
6831
|
+
"label": "Password",
|
|
6832
|
+
"type": "TEXT_INPUT",
|
|
6833
|
+
"subType": "PASSWORD",
|
|
6834
|
+
"required": true,
|
|
6835
|
+
"placeholder": "Min. 8 characters",
|
|
6836
|
+
"hint": "Use at least 8 characters with letters and numbers",
|
|
6837
|
+
"textConfig": {
|
|
6838
|
+
"length": { "min": 8, "max": 64 }
|
|
6839
|
+
}
|
|
6840
|
+
},
|
|
6841
|
+
{
|
|
6842
|
+
"name": "confirmPassword",
|
|
6843
|
+
"label": "Confirm Password",
|
|
6844
|
+
"type": "TEXT_INPUT",
|
|
6845
|
+
"subType": "PASSWORD",
|
|
6846
|
+
"required": true,
|
|
6847
|
+
"placeholder": "Re-enter your password",
|
|
6848
|
+
"textConfig": {
|
|
6849
|
+
"matchField": "password"
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
]
|
|
6853
|
+
}
|
|
6854
|
+
|
|
6855
|
+
]
|
|
6856
|
+
}
|
|
6857
|
+
},
|
|
6858
|
+
|
|
6859
|
+
{
|
|
6860
|
+
"type": "GROUP",
|
|
6861
|
+
"subType": "SECTION",
|
|
6862
|
+
"sectionConfig": {
|
|
6863
|
+
"label": "Primary Address",
|
|
6864
|
+
"name": "primaryAddress",
|
|
6865
|
+
"children": [
|
|
6866
|
+
|
|
6867
|
+
{
|
|
6868
|
+
"type": "ROW",
|
|
6869
|
+
"subType": "HORIZONTAL",
|
|
6870
|
+
"children": [
|
|
6871
|
+
{
|
|
6872
|
+
"name": "addressLine1",
|
|
6873
|
+
"label": "Address Line 1",
|
|
6874
|
+
"type": "TEXT_INPUT",
|
|
6875
|
+
"subType": "SHORT",
|
|
6876
|
+
"required": true,
|
|
6877
|
+
"placeholder": "House / Flat No., Building, Street"
|
|
6878
|
+
},
|
|
6879
|
+
{
|
|
6880
|
+
"name": "addressLine2",
|
|
6881
|
+
"label": "Address Line 2",
|
|
6882
|
+
"type": "TEXT_INPUT",
|
|
6883
|
+
"subType": "SHORT",
|
|
6884
|
+
"placeholder": "Area / Locality / Colony (optional)"
|
|
6885
|
+
}
|
|
6886
|
+
]
|
|
6887
|
+
},
|
|
6888
|
+
|
|
6889
|
+
{
|
|
6890
|
+
"type": "ROW",
|
|
6891
|
+
"subType": "HORIZONTAL",
|
|
6892
|
+
"children": [
|
|
6893
|
+
{
|
|
6894
|
+
"name": "country",
|
|
6895
|
+
"label": "Country",
|
|
6896
|
+
"type": "DROPDOWN",
|
|
6897
|
+
"subType": "SINGLE",
|
|
6898
|
+
"required": true,
|
|
6899
|
+
"placeholder": "Select Country",
|
|
6900
|
+
"optionConfig": {
|
|
6901
|
+
"optionList": [
|
|
6902
|
+
{ "label": "India", "code": "REGION_COUNTRY.INDIA" },
|
|
6903
|
+
{ "label": "USA", "code": "REGION_COUNTRY.USA" }
|
|
6904
|
+
]
|
|
6905
|
+
}
|
|
6906
|
+
},
|
|
6907
|
+
{
|
|
6908
|
+
"name": "state",
|
|
6909
|
+
"label": "State",
|
|
6910
|
+
"type": "DROPDOWN",
|
|
6911
|
+
"subType": "SINGLE",
|
|
6912
|
+
"required": true,
|
|
6913
|
+
"placeholder": "Select State",
|
|
6914
|
+
"optionConfig": {
|
|
6915
|
+
"apiUrl": "https://dev.platformcommons.org/ctld/api/globalrefdata/v1?refClass=GREF.REGION_STATE&parentRefClass=GREF.REGION_COUNTRY",
|
|
6916
|
+
"labelPath": "label",
|
|
6917
|
+
"valuePath": "dataCode",
|
|
6918
|
+
"dependencies": {
|
|
6919
|
+
"parentRefData": "country"
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
},
|
|
6923
|
+
{
|
|
6924
|
+
"name": "city",
|
|
6925
|
+
"label": "City",
|
|
6926
|
+
"type": "DROPDOWN",
|
|
6927
|
+
"subType": "SINGLE",
|
|
6928
|
+
"required": true,
|
|
6929
|
+
"placeholder": "Select City",
|
|
6930
|
+
"optionConfig": {
|
|
6931
|
+
"apiUrl": "https://dev.platformcommons.org/ctld/api/globalrefdata/v1?refClass=GREF.REGION_DISTRICT&parentRefClass=GREF.REGION_STATE",
|
|
6932
|
+
"labelPath": "label",
|
|
6933
|
+
"valuePath": "dataCode",
|
|
6934
|
+
"dependencies": {
|
|
6935
|
+
"parentRefData": "state"
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6939
|
+
]
|
|
6940
|
+
},
|
|
6941
|
+
|
|
6942
|
+
{
|
|
6943
|
+
"type": "ROW",
|
|
6944
|
+
"subType": "HORIZONTAL",
|
|
6945
|
+
"children": [
|
|
6946
|
+
{
|
|
6947
|
+
"name": "block",
|
|
6948
|
+
"label": "Block / Taluka",
|
|
6949
|
+
"type": "DROPDOWN",
|
|
6950
|
+
"subType": "SINGLE",
|
|
6951
|
+
"placeholder": "Select Block",
|
|
6952
|
+
"optionConfig": {
|
|
6953
|
+
"apiUrl": "https://dev.platformcommons.org/ctld/api/globalrefdata/v1?refClass=GREF.REGION_BLOCK&parentRefClass=GREF.REGION_DISTRICT",
|
|
6954
|
+
"labelPath": "label",
|
|
6955
|
+
"valuePath": "dataCode",
|
|
6956
|
+
"dependencies": {
|
|
6957
|
+
"parentRefData": "city"
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
},
|
|
6961
|
+
{
|
|
6962
|
+
"name": "pinCode",
|
|
6963
|
+
"label": "PIN Code",
|
|
6964
|
+
"type": "TEXT_INPUT",
|
|
6965
|
+
"subType": "SHORT",
|
|
6966
|
+
"required": true,
|
|
6967
|
+
"placeholder": "6-digit PIN code",
|
|
6968
|
+
"textConfig": {
|
|
6969
|
+
"length": { "min": 6, "max": 6 },
|
|
6970
|
+
"pattern": "^[0-9]{6}$",
|
|
6971
|
+
"patternMessage": "Enter a valid 6-digit PIN code"
|
|
6972
|
+
}
|
|
6973
|
+
}
|
|
6974
|
+
]
|
|
6975
|
+
}
|
|
6976
|
+
|
|
6977
|
+
]
|
|
6978
|
+
}
|
|
6979
|
+
},
|
|
6980
|
+
|
|
6981
|
+
{
|
|
6982
|
+
"type": "GROUP",
|
|
6983
|
+
"subType": "SECTION",
|
|
6984
|
+
"sectionConfig": {
|
|
6985
|
+
"label": "User Role",
|
|
6986
|
+
"name": "userRole",
|
|
6987
|
+
"type": "ROW",
|
|
6988
|
+
"subType": "HORIZONTAL",
|
|
6989
|
+
"children": [
|
|
6990
|
+
{
|
|
6991
|
+
"name": "role",
|
|
6992
|
+
"label": "User Role",
|
|
6993
|
+
"type": "DROPDOWN",
|
|
6994
|
+
"subType": "SINGLE",
|
|
6995
|
+
"required": true,
|
|
6996
|
+
"placeholder": "Select a role",
|
|
6997
|
+
"optionConfig": {
|
|
6998
|
+
"apiUrl": "https://dev.platformcommons.org/gateway/commons-report-service/api/v1/datasets/name/VMS_GET_TENANT_ROLES/execute?params=TENANT_ID=1823",
|
|
6999
|
+
"labelPath": "roleName",
|
|
7000
|
+
"valuePath": "code"
|
|
7001
|
+
}
|
|
7002
|
+
},
|
|
7003
|
+
{
|
|
7004
|
+
"name": "reportingRole",
|
|
7005
|
+
"label": "Reporting Role",
|
|
7006
|
+
"type": "DROPDOWN",
|
|
7007
|
+
"subType": "SINGLE",
|
|
7008
|
+
"required": true,
|
|
7009
|
+
"placeholder": "Select Reporting Role",
|
|
7010
|
+
"optionConfig": {
|
|
7011
|
+
"apiUrl": "https://dev.platformcommons.org/ctld/api/rolehierarchy/v1?criteria=roleCode.code='{{userRole}}' and isActive=1&includeAttributes=id,parentRoleCode",
|
|
7012
|
+
"dataPath": "payload",
|
|
7013
|
+
"labelPath": "parentRoleCode",
|
|
7014
|
+
"valuePath": "parentRoleCode",
|
|
7015
|
+
"dependencies": {
|
|
7016
|
+
"userRole": "role"
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
},
|
|
7020
|
+
{
|
|
7021
|
+
"name": "reportingManager",
|
|
7022
|
+
"label": "Reporting Manager",
|
|
7023
|
+
"type": "DROPDOWN",
|
|
7024
|
+
"subType": "SINGLE",
|
|
7025
|
+
"required": true,
|
|
7026
|
+
"placeholder": "Select Reporting Manager",
|
|
7027
|
+
"optionConfig": {
|
|
7028
|
+
"apiUrl": "https://dev.platformcommons.org/ctld/api/tenant/user/role/criteria/v1?criteria=role.code='{{reportingRole}}' and isActive=1 and user.isActive=1",
|
|
7029
|
+
"dataPath": "payload",
|
|
7030
|
+
"labelPath": "user.userName",
|
|
7031
|
+
"valuePath": "user.id",
|
|
7032
|
+
"dependencies": {
|
|
7033
|
+
"reportingRole": "reportingRole"
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
]
|
|
7038
|
+
}
|
|
7039
|
+
}
|
|
7040
|
+
|
|
7041
|
+
]
|
|
7042
|
+
}
|
|
7043
|
+
}`,
|
|
7044
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
7045
|
+
// Document Upload Form — demonstrates the FILE_UPLOAD field type
|
|
7046
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
7047
|
+
documentUploadForm: `{
|
|
7048
|
+
"entityType": "DOCUMENT_UPLOAD",
|
|
7049
|
+
"label": "Documents",
|
|
7050
|
+
"description": "Upload the required documents",
|
|
7051
|
+
"formType": "SECTION",
|
|
7052
|
+
"sectionConfig": {
|
|
7053
|
+
"children": [
|
|
7054
|
+
{
|
|
7055
|
+
"type": "GROUP",
|
|
7056
|
+
"subType": "SECTION",
|
|
7057
|
+
"sectionConfig": {
|
|
7058
|
+
"label": "Documents",
|
|
7059
|
+
"name": "documents",
|
|
7060
|
+
"children": [
|
|
7061
|
+
{
|
|
7062
|
+
"name": "profilePicture",
|
|
7063
|
+
"label": "Profile Picture",
|
|
7064
|
+
"type": "FILE_UPLOAD",
|
|
7065
|
+
"subType": "SINGLE",
|
|
7066
|
+
"required": true,
|
|
7067
|
+
"attachmentConfig": {
|
|
7068
|
+
"multiple": false,
|
|
7069
|
+
"maxSizeMB": 5,
|
|
7070
|
+
"accept": ".jpg,.jpeg,.png,.webp,image/*",
|
|
7071
|
+
"acceptLabel": "JPG, PNG, WEBP"
|
|
7072
|
+
}
|
|
7073
|
+
},
|
|
7074
|
+
{
|
|
7075
|
+
"name": "certificates",
|
|
7076
|
+
"label": "Certificates",
|
|
7077
|
+
"type": "FILE_UPLOAD",
|
|
7078
|
+
"subType": "MULTIPLE",
|
|
7079
|
+
"attachmentConfig": {
|
|
7080
|
+
"multiple": true,
|
|
7081
|
+
"maxFiles": 5,
|
|
7082
|
+
"maxSizeMB": 10,
|
|
7083
|
+
"accept": ".pdf,.doc,.docx,.jpg,.jpeg,.png",
|
|
7084
|
+
"acceptLabel": "PDF, DOCX, JPG, PNG"
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
]
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
]
|
|
7091
|
+
}
|
|
7092
|
+
}`,
|
|
7093
|
+
// Demand Definition Form (matching user request image)
|
|
7094
|
+
demandDefinitionForm: `{
|
|
7095
|
+
"entityType": "DEMAND",
|
|
7096
|
+
"label": "Demand Definition",
|
|
7097
|
+
"formType": "SECTION",
|
|
7098
|
+
"sectionConfig": {
|
|
7099
|
+
"children": [
|
|
7100
|
+
{
|
|
7101
|
+
"type": "ROW",
|
|
7102
|
+
"subType": "HORIZONTAL",
|
|
7103
|
+
"children": [
|
|
7104
|
+
{
|
|
7105
|
+
"name": "beneficiaries",
|
|
7106
|
+
"label": "Required Beneficiaries",
|
|
7107
|
+
"type": "NUMBER_INPUT",
|
|
7108
|
+
"subType": "INTEGER",
|
|
7109
|
+
"required": true,
|
|
7110
|
+
"suffix": "Students",
|
|
7111
|
+
"placeholder": "0",
|
|
7112
|
+
"numberConfig": { "min": 0 }
|
|
7113
|
+
},
|
|
7114
|
+
{
|
|
7115
|
+
"name": "deliveryModel",
|
|
7116
|
+
"label": "Preferred Delivery Model",
|
|
7117
|
+
"type": "DROPDOWN",
|
|
7118
|
+
"subType": "SINGLE",
|
|
7119
|
+
"required": true,
|
|
7120
|
+
"placeholder": "Select",
|
|
7121
|
+
"optionConfig": {
|
|
7122
|
+
"optionList": [
|
|
7123
|
+
{ "label": "Onsite Delivery", "code": "ONSITE" },
|
|
7124
|
+
{ "label": "Hybrid Model", "code": "HYBRID" }
|
|
7125
|
+
]
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
]
|
|
7129
|
+
},
|
|
7130
|
+
{
|
|
7131
|
+
"type": "ROW",
|
|
7132
|
+
"subType": "HORIZONTAL",
|
|
7133
|
+
"children": [
|
|
7134
|
+
{
|
|
7135
|
+
"name": "minAge",
|
|
7136
|
+
"label": "Min Target Age",
|
|
7137
|
+
"type": "NUMBER_INPUT",
|
|
7138
|
+
"subType": "INTEGER",
|
|
7139
|
+
"required": true,
|
|
7140
|
+
"placeholder": "0",
|
|
7141
|
+
"numberConfig": { "min": 0, "max": 100 },
|
|
7142
|
+
"onValidate": "!maxAge || minAge <= maxAge",
|
|
7143
|
+
"errorMessage": "Min Age cannot be greater than Max Age"
|
|
7144
|
+
},
|
|
7145
|
+
{
|
|
7146
|
+
"name": "maxAge",
|
|
7147
|
+
"label": "Max Target Age",
|
|
7148
|
+
"type": "NUMBER_INPUT",
|
|
7149
|
+
"subType": "INTEGER",
|
|
7150
|
+
"required": true,
|
|
7151
|
+
"placeholder": "100",
|
|
7152
|
+
"numberConfig": { "min": 0, "max": 100 },
|
|
7153
|
+
"onValidate": "!minAge || maxAge >= minAge",
|
|
7154
|
+
"errorMessage": "Max Age cannot be less than Min Age"
|
|
7155
|
+
}
|
|
7156
|
+
]
|
|
7157
|
+
},
|
|
7158
|
+
{
|
|
7159
|
+
"type": "ROW",
|
|
7160
|
+
"subType": "HORIZONTAL",
|
|
7161
|
+
"children": [
|
|
7162
|
+
{
|
|
7163
|
+
"name": "male",
|
|
7164
|
+
"label": "Male Split (%)",
|
|
7165
|
+
"type": "NUMBER_INPUT",
|
|
7166
|
+
"subType": "INTEGER",
|
|
7167
|
+
"required": true,
|
|
7168
|
+
"suffix": "%",
|
|
7169
|
+
"placeholder": "50",
|
|
7170
|
+
"numberConfig": { "min": 0, "max": 100 },
|
|
7171
|
+
"onValidate": "(male || 0) + (female || 0) === 100",
|
|
7172
|
+
"errorMessage": "Male + Female % must equal 100%"
|
|
7173
|
+
},
|
|
7174
|
+
{
|
|
7175
|
+
"name": "female",
|
|
7176
|
+
"label": "Female Split (%)",
|
|
7177
|
+
"type": "NUMBER_INPUT",
|
|
7178
|
+
"subType": "INTEGER",
|
|
7179
|
+
"required": true,
|
|
7180
|
+
"suffix": "%",
|
|
7181
|
+
"placeholder": "50",
|
|
7182
|
+
"numberConfig": { "min": 0, "max": 100 },
|
|
7183
|
+
"onValidate": "(male || 0) + (female || 0) === 100",
|
|
7184
|
+
"errorMessage": "Male + Female % must equal 100%"
|
|
7185
|
+
}
|
|
7186
|
+
]
|
|
7187
|
+
}
|
|
7188
|
+
]
|
|
7189
|
+
}
|
|
7190
|
+
}`,
|
|
7191
|
+
// Project Info Form (matching user request image)
|
|
7192
|
+
// Project Form (Combined Info + Demand)
|
|
7193
|
+
projectInfoForm: `{
|
|
7194
|
+
"entityType": "PROJECT",
|
|
7195
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.TITLE",
|
|
7196
|
+
"formType": "SECTION",
|
|
7197
|
+
"sectionConfig": {
|
|
7198
|
+
"children": [
|
|
7199
|
+
{
|
|
7200
|
+
"type": "GROUP",
|
|
7201
|
+
"subType": "SECTION",
|
|
7202
|
+
"sectionConfig": {
|
|
7203
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.SECTION.INFO",
|
|
7204
|
+
"children": [
|
|
7205
|
+
{
|
|
7206
|
+
"type": "ROW",
|
|
7207
|
+
"subType": "HORIZONTAL",
|
|
7208
|
+
"children": [
|
|
7209
|
+
{
|
|
7210
|
+
"name": "projectName",
|
|
7211
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.NAME.LABEL",
|
|
7212
|
+
"placeholder": "UI_PLAYGROUND.SMART_FORM.PROJECT.NAME.PH",
|
|
7213
|
+
"type": "TEXT_INPUT",
|
|
7214
|
+
"subType": "SHORT",
|
|
7215
|
+
"required": true,
|
|
7216
|
+
"colSpan": 6
|
|
7217
|
+
},
|
|
7218
|
+
{
|
|
7219
|
+
"name": "programId",
|
|
7220
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.PROG_ID.LABEL",
|
|
7221
|
+
"type": "TEXT_INPUT",
|
|
7222
|
+
"subType": "SHORT",
|
|
7223
|
+
"defaultValue": "DLP-24-L1",
|
|
7224
|
+
"readonly": true,
|
|
7225
|
+
"colSpan": 6
|
|
7226
|
+
}
|
|
7227
|
+
]
|
|
7228
|
+
},
|
|
7229
|
+
{
|
|
7230
|
+
"type": "ROW",
|
|
7231
|
+
"subType": "HORIZONTAL",
|
|
7232
|
+
"children": [
|
|
7233
|
+
{
|
|
7234
|
+
"name": "linkedProgram",
|
|
7235
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.LINKED_PROG.LABEL",
|
|
7236
|
+
"type": "TEXT_INPUT",
|
|
7237
|
+
"subType": "SHORT",
|
|
7238
|
+
"defaultValue": "National Skills Initiative",
|
|
7239
|
+
"readonly": true,
|
|
7240
|
+
"colSpan": 4
|
|
7241
|
+
},
|
|
7242
|
+
{
|
|
7243
|
+
"name": "linkedContract",
|
|
7244
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.LINKED_CONTRACT.LABEL",
|
|
7245
|
+
"type": "TEXT_INPUT",
|
|
7246
|
+
"subType": "SHORT",
|
|
7247
|
+
"defaultValue": "NSI-Gov-Oct23",
|
|
7248
|
+
"readonly": true,
|
|
7249
|
+
"colSpan": 4
|
|
7250
|
+
},
|
|
7251
|
+
{
|
|
7252
|
+
"name": "assignedDistrict",
|
|
7253
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.DISTRICT.LABEL",
|
|
7254
|
+
"type": "TEXT_INPUT",
|
|
7255
|
+
"subType": "SHORT",
|
|
7256
|
+
"defaultValue": "Bangalore Rural",
|
|
7257
|
+
"readonly": true,
|
|
7258
|
+
"colSpan": 4
|
|
7259
|
+
}
|
|
7260
|
+
]
|
|
7261
|
+
}
|
|
7262
|
+
]
|
|
7263
|
+
}
|
|
7264
|
+
},
|
|
7265
|
+
{
|
|
7266
|
+
"type": "GROUP",
|
|
7267
|
+
"subType": "SECTION",
|
|
7268
|
+
"sectionConfig": {
|
|
7269
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.SECTION.DEMAND",
|
|
7270
|
+
"children": [
|
|
7271
|
+
{
|
|
7272
|
+
"type": "ROW",
|
|
7273
|
+
"subType": "HORIZONTAL",
|
|
7274
|
+
"children": [
|
|
7275
|
+
{
|
|
7276
|
+
"name": "requiredBeneficiaries",
|
|
7277
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.BENEFICIARIES.LABEL",
|
|
7278
|
+
"placeholder": "UI_PLAYGROUND.SMART_FORM.PROJECT.BENEFICIARIES.PH",
|
|
7279
|
+
"type": "NUMBER_INPUT",
|
|
7280
|
+
"subType": "INTEGER",
|
|
7281
|
+
"required": true,
|
|
7282
|
+
"colSpan": 6,
|
|
7283
|
+
"numberConfig": {
|
|
7284
|
+
"min": 1
|
|
7285
|
+
}
|
|
7286
|
+
},
|
|
7287
|
+
{
|
|
7288
|
+
"name": "deliveryModel",
|
|
7289
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.DELIVERY_MODEL.LABEL",
|
|
7290
|
+
"type": "DROPDOWN",
|
|
7291
|
+
"subType": "SINGLE",
|
|
7292
|
+
"required": true,
|
|
7293
|
+
"colSpan": 6,
|
|
7294
|
+
"optionConfig": {
|
|
7295
|
+
"optionList": [
|
|
7296
|
+
{
|
|
7297
|
+
"label": "UI_PLAYGROUND.OPTION.IN_PERSON",
|
|
7298
|
+
"code": "IN_PERSON"
|
|
7299
|
+
},
|
|
7300
|
+
{
|
|
7301
|
+
"label": "UI_PLAYGROUND.OPTION.HYBRID",
|
|
7302
|
+
"code": "HYBRID"
|
|
7303
|
+
},
|
|
7304
|
+
{
|
|
7305
|
+
"label": "UI_PLAYGROUND.OPTION.ONLINE",
|
|
7306
|
+
"code": "ONLINE"
|
|
7307
|
+
}
|
|
7308
|
+
]
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
]
|
|
7312
|
+
},
|
|
7313
|
+
{
|
|
7314
|
+
"type": "ROW",
|
|
7315
|
+
"subType": "HORIZONTAL",
|
|
7316
|
+
"children": [
|
|
7317
|
+
{
|
|
7318
|
+
"name": "minAge",
|
|
7319
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.AGE_MIN.LABEL",
|
|
7320
|
+
"type": "NUMBER_INPUT",
|
|
7321
|
+
"subType": "INTEGER",
|
|
7322
|
+
"required": true,
|
|
7323
|
+
"placeholder": "UI_PLAYGROUND.SMART_FORM.PROJECT.AGE_MIN.LABEL",
|
|
7324
|
+
"numberConfig": {
|
|
7325
|
+
"min": 0,
|
|
7326
|
+
"max": 100
|
|
7327
|
+
},
|
|
7328
|
+
"onValidate": "!maxAge || minAge <= maxAge",
|
|
7329
|
+
"errorMessage": "UI_PLAYGROUND.SMART_FORM.PROJECT.AGE_MIN.ERR"
|
|
7330
|
+
},
|
|
7331
|
+
{
|
|
7332
|
+
"name": "maxAge",
|
|
7333
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.AGE_MAX.LABEL",
|
|
7334
|
+
"type": "NUMBER_INPUT",
|
|
7335
|
+
"subType": "INTEGER",
|
|
7336
|
+
"required": true,
|
|
7337
|
+
"placeholder": "UI_PLAYGROUND.SMART_FORM.PROJECT.AGE_MAX.LABEL",
|
|
7338
|
+
"numberConfig": {
|
|
7339
|
+
"min": 0,
|
|
7340
|
+
"max": 100
|
|
7341
|
+
},
|
|
7342
|
+
"onValidate": "!minAge || maxAge >= minAge",
|
|
7343
|
+
"errorMessage": "UI_PLAYGROUND.SMART_FORM.PROJECT.AGE_MAX.ERR"
|
|
7344
|
+
}
|
|
7345
|
+
]
|
|
7346
|
+
},
|
|
7347
|
+
{
|
|
7348
|
+
"type": "ROW",
|
|
7349
|
+
"subType": "HORIZONTAL",
|
|
7350
|
+
"children": [
|
|
7351
|
+
{
|
|
7352
|
+
"name": "maleSplit",
|
|
7353
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.MALE.LABEL",
|
|
7354
|
+
"type": "NUMBER_INPUT",
|
|
7355
|
+
"subType": "INTEGER",
|
|
7356
|
+
"required": true,
|
|
7357
|
+
"suffix": "%",
|
|
7358
|
+
"placeholder": "UI_PLAYGROUND.SMART_FORM.PROJECT.MALE.LABEL",
|
|
7359
|
+
"numberConfig": {
|
|
7360
|
+
"min": 0,
|
|
7361
|
+
"max": 100
|
|
7362
|
+
},
|
|
7363
|
+
"onValidate": "(maleSplit || 0) + (femaleSplit || 0) === 100",
|
|
7364
|
+
"errorMessage": "UI_PLAYGROUND.SMART_FORM.PROJECT.GENDER_SPLIT.ERR"
|
|
7365
|
+
},
|
|
7366
|
+
{
|
|
7367
|
+
"name": "femaleSplit",
|
|
7368
|
+
"label": "UI_PLAYGROUND.SMART_FORM.PROJECT.FEMALE.LABEL",
|
|
7369
|
+
"type": "NUMBER_INPUT",
|
|
7370
|
+
"subType": "INTEGER",
|
|
7371
|
+
"required": true,
|
|
7372
|
+
"suffix": "%",
|
|
7373
|
+
"placeholder": "UI_PLAYGROUND.SMART_FORM.PROJECT.FEMALE.LABEL",
|
|
7374
|
+
"numberConfig": {
|
|
7375
|
+
"min": 0,
|
|
7376
|
+
"max": 100
|
|
7377
|
+
},
|
|
7378
|
+
"onValidate": "(maleSplit || 0) + (femaleSplit || 0) === 100",
|
|
7379
|
+
"errorMessage": "UI_PLAYGROUND.SMART_FORM.PROJECT.GENDER_SPLIT.ERR"
|
|
7380
|
+
}
|
|
7381
|
+
]
|
|
7382
|
+
}
|
|
7383
|
+
]
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
7386
|
+
]
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
`,
|
|
5880
7390
|
};
|
|
5881
7391
|
|
|
5882
7392
|
var smartForm_examples = /*#__PURE__*/Object.freeze({
|
|
@@ -5892,5 +7402,5 @@ var smartForm_examples = /*#__PURE__*/Object.freeze({
|
|
|
5892
7402
|
* Generated bundle index. Do not edit.
|
|
5893
7403
|
*/
|
|
5894
7404
|
|
|
5895
|
-
export { AlertComponent, AlertModule, ButtonComponent, ButtonModule, CheckboxComponent, ConfigurableFormComponent, configurableForm_examples as ConfigurableFormExamples, ConfigurableFormModule, ConfirmationModalComponent, ConfirmationModalModule, DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE_SIZE_OPTIONS, DatepickerComponent, DropdownComponent, ExpressionService, FilterComponent, FilterModule, FilterSidebarComponent, FilterSidebarModule, FormComponentsModule, InputComponent, MaterialModule, NAV_ORIENTATION_DEFAULT, NAV_VARIANT_DEFAULT, NavComponent, NavModule, PAGINATION_THEME_DARK, PAGINATION_THEME_DEFAULT, PaginationComponent, PaginationModule, RadioComponent, SearchComponent, SharedUiModule, SmartFormComponent, SmartFormController, smartForm_examples as SmartFormExamples, SmartFormModule, SmartTableComponent, SmartTableModule, SummaryCardComponent, SummaryCardModule, ToggleComponent, ValidationUtils, clearLocalStorage, clearSessionStorage, getLocalStorageItem, getSessionStorageItem, removeLocalStorageItem, removeSessionStorageItem, setLocalStorageItem, setSessionStorageItem };
|
|
7405
|
+
export { AlertComponent, AlertModule, ButtonComponent, ButtonModule, CheckboxComponent, ConfigurableFormComponent, configurableForm_examples as ConfigurableFormExamples, ConfigurableFormModule, ConfirmationModalComponent, ConfirmationModalModule, DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE_SIZE_OPTIONS, DatepickerComponent, DropdownComponent, ExpressionService, FilterComponent, FilterModule, FilterSidebarComponent, FilterSidebarModule, FormComponentsModule, InputComponent, MaterialModule, NAV_ORIENTATION_DEFAULT, NAV_VARIANT_DEFAULT, NavComponent, NavModule, PAGINATION_THEME_DARK, PAGINATION_THEME_DEFAULT, PaginationComponent, PaginationModule, RadioComponent, SearchComponent, SharedUiModule, SmartFormComponent, SmartFormController, smartForm_examples as SmartFormExamples, SmartFormModule, SmartTableComponent, SmartTableModule, StringUtils, SummaryCardComponent, SummaryCardModule, ToggleComponent, ValidationUtils, clearLocalStorage, clearSessionStorage, getLocalStorageItem, getSessionStorageItem, removeLocalStorageItem, removeSessionStorageItem, setLocalStorageItem, setSessionStorageItem, translateConfig };
|
|
5896
7406
|
//# sourceMappingURL=commons-shared-web-ui.mjs.map
|