valtech-components 4.0.13 → 4.0.14

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.
@@ -2,5 +2,5 @@
2
2
  * Current version of valtech-components.
3
3
  * This is automatically updated during the publish process.
4
4
  */
5
- export const VERSION = '4.0.13';
6
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvdmVyc2lvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsUUFBUSxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDdXJyZW50IHZlcnNpb24gb2YgdmFsdGVjaC1jb21wb25lbnRzLlxuICogVGhpcyBpcyBhdXRvbWF0aWNhbGx5IHVwZGF0ZWQgZHVyaW5nIHRoZSBwdWJsaXNoIHByb2Nlc3MuXG4gKi9cbmV4cG9ydCBjb25zdCBWRVJTSU9OID0gJzQuMC4xMyc7XG4iXX0=
5
+ export const VERSION = '4.0.14';
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvdmVyc2lvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsUUFBUSxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDdXJyZW50IHZlcnNpb24gb2YgdmFsdGVjaC1jb21wb25lbnRzLlxuICogVGhpcyBpcyBhdXRvbWF0aWNhbGx5IHVwZGF0ZWQgZHVyaW5nIHRoZSBwdWJsaXNoIHByb2Nlc3MuXG4gKi9cbmV4cG9ydCBjb25zdCBWRVJTSU9OID0gJzQuMC4xNCc7XG4iXX0=
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.13';
59
+ const VERSION = '4.0.14';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -50078,14 +50078,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50078
50078
  const MEMBER_IMPORT_MODAL_I18N = {
50079
50079
  es: {
50080
50080
  title: 'Importar miembros',
50081
- hint: 'Pega un CSV con columnas: email, nombre, rol (una fila por persona).',
50081
+ subtitle: 'Carga un archivo CSV o pega el contenido directamente. Cada fila debe tener email, nombre y rol. Los cambios se aplican al instante.',
50082
50082
  csvPlaceholder: 'email,nombre,rol\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
50083
+ importMethodLabel: '¿Cómo deseas importar?',
50084
+ importMethodFile: 'Por archivo',
50085
+ importMethodFileHint: 'Sube un archivo .csv desde tu equipo',
50086
+ importMethodPaste: 'Pegar contenido',
50087
+ importMethodPasteHint: 'Escribe o pega el CSV directamente',
50083
50088
  uploadFile: 'Subir archivo CSV',
50084
- orPaste: 'o pega el CSV abajo',
50085
50089
  fileLoaded: 'Archivo cargado: {name}',
50086
50090
  fileReadError: 'No se pudo leer el archivo.',
50087
50091
  rolesAvailable: 'Roles disponibles:',
50088
- onConflictLabel: 'Si el email ya existe',
50092
+ onConflictLabel: '¿Qué hacer si el email ya existe?',
50089
50093
  conflictAssign: 'Asignar el rol',
50090
50094
  conflictSkip: 'Omitir',
50091
50095
  conflictError: 'Marcar error',
@@ -50093,7 +50097,7 @@ const MEMBER_IMPORT_MODAL_I18N = {
50093
50097
  submit: 'Importar',
50094
50098
  submitting: 'Importando...',
50095
50099
  parseError: 'No se pudo interpretar el CSV. Revisa el formato.',
50096
- noRows: 'Pega al menos una fila válida (email y rol).',
50100
+ noRows: 'Agrega al menos una fila válida (email y rol).',
50097
50101
  summary: 'Resumen',
50098
50102
  created: 'creados',
50099
50103
  assigned: 'asignados',
@@ -50110,14 +50114,18 @@ const MEMBER_IMPORT_MODAL_I18N = {
50110
50114
  },
50111
50115
  en: {
50112
50116
  title: 'Import members',
50113
- hint: 'Paste a CSV with columns: email, name, role (one row per person).',
50117
+ subtitle: 'Upload a CSV file or paste the content directly. Each row must include email, name and role. Changes are applied immediately.',
50114
50118
  csvPlaceholder: 'email,name,role\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
50119
+ importMethodLabel: 'How do you want to import?',
50120
+ importMethodFile: 'From file',
50121
+ importMethodFileHint: 'Upload a .csv file from your device',
50122
+ importMethodPaste: 'Paste content',
50123
+ importMethodPasteHint: 'Type or paste the CSV directly',
50115
50124
  uploadFile: 'Upload CSV file',
50116
- orPaste: 'or paste the CSV below',
50117
50125
  fileLoaded: 'File loaded: {name}',
50118
50126
  fileReadError: "Couldn't read the file.",
50119
50127
  rolesAvailable: 'Available roles:',
50120
- onConflictLabel: 'If the email already exists',
50128
+ onConflictLabel: 'What to do if the email already exists?',
50121
50129
  conflictAssign: 'Assign the role',
50122
50130
  conflictSkip: 'Skip',
50123
50131
  conflictError: 'Mark as error',
@@ -50125,7 +50133,7 @@ const MEMBER_IMPORT_MODAL_I18N = {
50125
50133
  submit: 'Import',
50126
50134
  submitting: 'Importing...',
50127
50135
  parseError: "Couldn't parse the CSV. Check the format.",
50128
- noRows: 'Paste at least one valid row (email and role).',
50136
+ noRows: 'Add at least one valid row (email and role).',
50129
50137
  summary: 'Summary',
50130
50138
  created: 'created',
50131
50139
  assigned: 'assigned',
@@ -50143,7 +50151,7 @@ const MEMBER_IMPORT_MODAL_I18N = {
50143
50151
  };
50144
50152
 
50145
50153
  const DEFAULT_NAMESPACE$8 = 'Settings.ImportModal';
50146
- addIcons({ cloudUploadOutline });
50154
+ addIcons({ cloudUploadOutline, documentTextOutline });
50147
50155
  /**
50148
50156
  * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
50149
50157
  *
@@ -50164,13 +50172,13 @@ class MemberImportModalComponent {
50164
50172
  /** Namespace i18n. */
50165
50173
  this.i18nNamespace = DEFAULT_NAMESPACE$8;
50166
50174
  this.csv = '';
50167
- this.sendActivation = true;
50175
+ this.importMode = signal('file');
50168
50176
  this.onConflictControl = new FormControl('assignRole');
50169
- this.conflictSelectProps = computed(() => ({
50177
+ this.sendActivationControl = new FormControl(true);
50178
+ this.conflictRadioProps = computed(() => ({
50170
50179
  control: this.onConflictControl,
50171
50180
  label: '',
50172
- placeholder: this.t('onConflictLabel'),
50173
- type: InputType.SELECT,
50181
+ type: InputType.RADIO,
50174
50182
  state: ComponentStates.ENABLED,
50175
50183
  options: [
50176
50184
  { id: 'assignRole', name: this.t('conflictAssign'), order: 0 },
@@ -50178,6 +50186,13 @@ class MemberImportModalComponent {
50178
50186
  { id: 'error', name: this.t('conflictError'), order: 2 },
50179
50187
  ],
50180
50188
  }));
50189
+ this.sendActivationProps = computed(() => ({
50190
+ control: this.sendActivationControl,
50191
+ label: this.t('sendActivation'),
50192
+ type: InputType.CHECK,
50193
+ state: ComponentStates.ENABLED,
50194
+ labelPlacement: 'end',
50195
+ }));
50181
50196
  this.submitting = signal(false);
50182
50197
  this.errorMsg = signal('');
50183
50198
  this.results = signal(null);
@@ -50248,7 +50263,7 @@ class MemberImportModalComponent {
50248
50263
  .importMembers(this.orgId, {
50249
50264
  users,
50250
50265
  onConflict: (this.onConflictControl.value ?? 'assignRole'),
50251
- sendActivation: this.sendActivation,
50266
+ sendActivation: this.sendActivationControl.value ?? true,
50252
50267
  })
50253
50268
  .subscribe({
50254
50269
  next: res => {
@@ -50277,6 +50292,7 @@ class MemberImportModalComponent {
50277
50292
  this.fileName.set('');
50278
50293
  this.errorMsg.set('');
50279
50294
  this.results.set(null);
50295
+ this.importMode.set('file');
50280
50296
  }
50281
50297
  dismiss() {
50282
50298
  this._modalRef?.dismiss(null, 'cancel');
@@ -50298,10 +50314,9 @@ class MemberImportModalComponent {
50298
50314
 
50299
50315
  <ion-content class="ion-padding">
50300
50316
  <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
50317
+ <val-title [props]="{ content: t('subtitle'), size: 'large', color: '', bold: false }" />
50301
50318
 
50302
50319
  @if (!results()) {
50303
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
50304
-
50305
50320
  @if (rolesHint()) {
50306
50321
  <val-text
50307
50322
  class="import-roles-hint"
@@ -50309,40 +50324,79 @@ class MemberImportModalComponent {
50309
50324
  />
50310
50325
  }
50311
50326
 
50312
- <!-- Subida de archivo: label nativo envuelve el input (label trick, igual
50313
- que val-attachment-uploader) → gesto confiable en iOS/PWA. -->
50314
- <div class="import-upload">
50315
- <label class="import-upload__btn">
50316
- <ion-icon name="cloud-upload-outline" />
50317
- <span>{{ t('uploadFile') }}</span>
50318
- <input type="file" accept=".csv,text/csv,text/plain" (change)="onFileSelected($event)" />
50319
- </label>
50320
- <span class="import-upload__hint">{{ t('orPaste') }}</span>
50327
+ <!-- Selector de método de importación -->
50328
+ <div class="import-section">
50329
+ <val-title [props]="{ content: t('importMethodLabel'), size: 'small', color: 'dark', bold: true }" />
50330
+ <div class="import-method-cards">
50331
+ <button
50332
+ type="button"
50333
+ class="import-method-card"
50334
+ [class.import-method-card--active]="importMode() === 'file'"
50335
+ (click)="importMode.set('file')"
50336
+ >
50337
+ <ion-icon name="cloud-upload-outline" class="import-method-card__icon" />
50338
+ <span class="import-method-card__title">{{ t('importMethodFile') }}</span>
50339
+ <span class="import-method-card__hint">{{ t('importMethodFileHint') }}</span>
50340
+ </button>
50341
+ <button
50342
+ type="button"
50343
+ class="import-method-card"
50344
+ [class.import-method-card--active]="importMode() === 'paste'"
50345
+ (click)="importMode.set('paste')"
50346
+ >
50347
+ <ion-icon name="document-text-outline" class="import-method-card__icon" />
50348
+ <span class="import-method-card__title">{{ t('importMethodPaste') }}</span>
50349
+ <span class="import-method-card__hint">{{ t('importMethodPasteHint') }}</span>
50350
+ </button>
50351
+ </div>
50321
50352
  </div>
50322
50353
 
50323
- @if (fileName()) {
50324
- <p class="import-file-loaded">
50325
- {{ t('fileLoaded').replace('{name}', fileName()) }}
50326
- </p>
50354
+ <!-- Subida de archivo (label trick — gesto confiable en iOS/PWA) -->
50355
+ @if (importMode() === 'file') {
50356
+ <div class="import-section">
50357
+ <label class="import-upload__btn">
50358
+ <ion-icon name="cloud-upload-outline" />
50359
+ <span>{{ t('uploadFile') }}</span>
50360
+ <input type="file" accept=".csv,text/csv,text/plain" (change)="onFileSelected($event)" />
50361
+ </label>
50362
+ @if (fileName()) {
50363
+ <p class="import-file-loaded">
50364
+ {{ t('fileLoaded').replace('{name}', fileName()) }}
50365
+ </p>
50366
+ }
50367
+ </div>
50327
50368
  }
50328
50369
 
50329
- <ion-textarea
50330
- class="import-csv"
50331
- [autoGrow]="true"
50332
- [rows]="6"
50333
- [placeholder]="t('csvPlaceholder')"
50334
- [(ngModel)]="csv"
50335
- />
50370
+ <!-- Pegar CSV -->
50371
+ @if (importMode() === 'paste') {
50372
+ <div class="import-section">
50373
+ <ion-textarea
50374
+ class="import-csv"
50375
+ [autoGrow]="true"
50376
+ [rows]="6"
50377
+ [placeholder]="t('csvPlaceholder')"
50378
+ [(ngModel)]="csv"
50379
+ />
50380
+ </div>
50381
+ }
50382
+
50383
+ <!-- Si hubo carga de archivo, mostrar textarea de solo lectura para revisar -->
50384
+ @if (importMode() === 'file' && csv) {
50385
+ <div class="import-section">
50386
+ <ion-textarea class="import-csv" [autoGrow]="true" [rows]="4" [readonly]="true" [value]="csv" />
50387
+ </div>
50388
+ }
50336
50389
 
50337
- <!-- Selector de conflicto: val-title + val-select-input (mismo patrón que invite modal) -->
50338
- <div class="conflict-select-wrap">
50339
- <val-title [props]="{ content: t('onConflictLabel'), size: 'small', color: 'dark', bold: false }" />
50340
- <val-select-input [props]="conflictSelectProps()" />
50390
+ <!-- Conflicto: radio buttons -->
50391
+ <div class="import-section">
50392
+ <val-title [props]="{ content: t('onConflictLabel'), size: 'small', color: 'dark', bold: true }" />
50393
+ <val-radio-input [props]="conflictRadioProps()" />
50341
50394
  </div>
50342
50395
 
50343
- <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
50344
- {{ t('sendActivation') }}
50345
- </ion-checkbox>
50396
+ <!-- Activación: val-check-input -->
50397
+ <div class="import-section">
50398
+ <val-check-input [props]="sendActivationProps()" />
50399
+ </div>
50346
50400
 
50347
50401
  @if (errorMsg()) {
50348
50402
  <p class="import-error">{{ errorMsg() }}</p>
@@ -50379,7 +50433,7 @@ class MemberImportModalComponent {
50379
50433
  </div>
50380
50434
  }
50381
50435
  </ion-content>
50382
- `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:16px}.import-roles-hint{display:block;margin-bottom:16px}.import-upload{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:16px}.import-upload__btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border:1px solid var(--ion-color-medium, #92949c);border-radius:10px;background:var(--ion-color-light);color:var(--ion-color-dark);font-size:.875rem;font-weight:600;cursor:pointer;transition:border-color .15s ease}.import-upload__btn:hover{border-color:var(--ion-color-primary, #7026df)}.import-upload__btn ion-icon{font-size:1.25rem}.import-upload__btn input[type=file]{display:none}.import-upload__hint{font-size:.8125rem;color:var(--ion-color-medium)}.import-file-loaded{margin:0 0 16px;font-size:.8125rem;color:var(--ion-color-medium-shade, #6b6e76)}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:20px;font-family:monospace;font-size:.8125rem}.conflict-select-wrap{margin-bottom:20px}.conflict-select-wrap val-title{display:block;margin-bottom:6px}.import-activation{display:block;margin-bottom:20px;font-size:.875rem}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: SearchSelectorComponent, selector: "val-select-input", inputs: ["preset", "props"] }] }); }
50436
+ `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:8px}.import-roles-hint{display:block;margin-bottom:24px}.import-section{margin-bottom:28px}.import-section val-title{margin-bottom:10px}.import-method-cards{display:grid;grid-template-columns:1fr 1fr;gap:12px}.import-method-card{display:flex;flex-direction:column;align-items:flex-start;gap:4px;padding:14px 16px;border:1.5px solid var(--ion-color-light-shade, #d7d8da);border-radius:12px;background:var(--ion-color-light);cursor:pointer;text-align:left;transition:border-color .15s ease,background .15s ease}.import-method-card:hover{border-color:var(--ion-color-medium, #92949c)}.import-method-card--active{border-color:var(--ion-color-primary, #7026df);background:color-mix(in srgb,var(--ion-color-primary, #7026df) 6%,var(--ion-color-light))}.import-method-card__icon{font-size:1.375rem;color:var(--ion-color-medium-shade, #6b6e76);margin-bottom:4px}.import-method-card--active .import-method-card__icon{color:var(--ion-color-primary, #7026df)}.import-method-card__title{font-size:.875rem;font-weight:600;color:var(--ion-color-dark)}.import-method-card__hint{font-size:.75rem;color:var(--ion-color-medium, #92949c);line-height:1.3}.import-upload__btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border:1px solid var(--ion-color-medium, #92949c);border-radius:10px;background:var(--ion-color-light);color:var(--ion-color-dark);font-size:.875rem;font-weight:600;cursor:pointer;transition:border-color .15s ease}.import-upload__btn:hover{border-color:var(--ion-color-primary, #7026df)}.import-upload__btn ion-icon{font-size:1.25rem}.import-upload__btn input[type=file]{display:none}.import-file-loaded{margin:10px 0 0;font-size:.8125rem;color:var(--ion-color-medium-shade, #6b6e76)}.import-csv{--background: var(--ion-color-light);border-radius:10px;font-family:monospace;font-size:.8125rem}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:.875rem}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:.75rem;color:var(--ion-color-danger, #c0392b)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: RadioInputComponent, selector: "val-radio-input", inputs: ["props"] }, { kind: "component", type: CheckInputComponent, selector: "val-check-input", inputs: ["preset", "props"] }] }); }
50383
50437
  }
50384
50438
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, decorators: [{
50385
50439
  type: Component,
@@ -50392,13 +50446,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50392
50446
  IonButton,
50393
50447
  IonContent,
50394
50448
  IonTextarea,
50395
- IonCheckbox,
50396
50449
  IonSpinner,
50397
50450
  IonIcon,
50398
50451
  DisplayComponent,
50399
50452
  TextComponent,
50400
50453
  TitleComponent,
50401
- SearchSelectorComponent,
50454
+ RadioInputComponent,
50455
+ CheckInputComponent,
50402
50456
  ], template: `
50403
50457
  <ion-header>
50404
50458
  <ion-toolbar>
@@ -50412,10 +50466,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50412
50466
 
50413
50467
  <ion-content class="ion-padding">
50414
50468
  <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
50469
+ <val-title [props]="{ content: t('subtitle'), size: 'large', color: '', bold: false }" />
50415
50470
 
50416
50471
  @if (!results()) {
50417
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
50418
-
50419
50472
  @if (rolesHint()) {
50420
50473
  <val-text
50421
50474
  class="import-roles-hint"
@@ -50423,40 +50476,79 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50423
50476
  />
50424
50477
  }
50425
50478
 
50426
- <!-- Subida de archivo: label nativo envuelve el input (label trick, igual
50427
- que val-attachment-uploader) → gesto confiable en iOS/PWA. -->
50428
- <div class="import-upload">
50429
- <label class="import-upload__btn">
50430
- <ion-icon name="cloud-upload-outline" />
50431
- <span>{{ t('uploadFile') }}</span>
50432
- <input type="file" accept=".csv,text/csv,text/plain" (change)="onFileSelected($event)" />
50433
- </label>
50434
- <span class="import-upload__hint">{{ t('orPaste') }}</span>
50479
+ <!-- Selector de método de importación -->
50480
+ <div class="import-section">
50481
+ <val-title [props]="{ content: t('importMethodLabel'), size: 'small', color: 'dark', bold: true }" />
50482
+ <div class="import-method-cards">
50483
+ <button
50484
+ type="button"
50485
+ class="import-method-card"
50486
+ [class.import-method-card--active]="importMode() === 'file'"
50487
+ (click)="importMode.set('file')"
50488
+ >
50489
+ <ion-icon name="cloud-upload-outline" class="import-method-card__icon" />
50490
+ <span class="import-method-card__title">{{ t('importMethodFile') }}</span>
50491
+ <span class="import-method-card__hint">{{ t('importMethodFileHint') }}</span>
50492
+ </button>
50493
+ <button
50494
+ type="button"
50495
+ class="import-method-card"
50496
+ [class.import-method-card--active]="importMode() === 'paste'"
50497
+ (click)="importMode.set('paste')"
50498
+ >
50499
+ <ion-icon name="document-text-outline" class="import-method-card__icon" />
50500
+ <span class="import-method-card__title">{{ t('importMethodPaste') }}</span>
50501
+ <span class="import-method-card__hint">{{ t('importMethodPasteHint') }}</span>
50502
+ </button>
50503
+ </div>
50435
50504
  </div>
50436
50505
 
50437
- @if (fileName()) {
50438
- <p class="import-file-loaded">
50439
- {{ t('fileLoaded').replace('{name}', fileName()) }}
50440
- </p>
50506
+ <!-- Subida de archivo (label trick — gesto confiable en iOS/PWA) -->
50507
+ @if (importMode() === 'file') {
50508
+ <div class="import-section">
50509
+ <label class="import-upload__btn">
50510
+ <ion-icon name="cloud-upload-outline" />
50511
+ <span>{{ t('uploadFile') }}</span>
50512
+ <input type="file" accept=".csv,text/csv,text/plain" (change)="onFileSelected($event)" />
50513
+ </label>
50514
+ @if (fileName()) {
50515
+ <p class="import-file-loaded">
50516
+ {{ t('fileLoaded').replace('{name}', fileName()) }}
50517
+ </p>
50518
+ }
50519
+ </div>
50441
50520
  }
50442
50521
 
50443
- <ion-textarea
50444
- class="import-csv"
50445
- [autoGrow]="true"
50446
- [rows]="6"
50447
- [placeholder]="t('csvPlaceholder')"
50448
- [(ngModel)]="csv"
50449
- />
50522
+ <!-- Pegar CSV -->
50523
+ @if (importMode() === 'paste') {
50524
+ <div class="import-section">
50525
+ <ion-textarea
50526
+ class="import-csv"
50527
+ [autoGrow]="true"
50528
+ [rows]="6"
50529
+ [placeholder]="t('csvPlaceholder')"
50530
+ [(ngModel)]="csv"
50531
+ />
50532
+ </div>
50533
+ }
50534
+
50535
+ <!-- Si hubo carga de archivo, mostrar textarea de solo lectura para revisar -->
50536
+ @if (importMode() === 'file' && csv) {
50537
+ <div class="import-section">
50538
+ <ion-textarea class="import-csv" [autoGrow]="true" [rows]="4" [readonly]="true" [value]="csv" />
50539
+ </div>
50540
+ }
50450
50541
 
50451
- <!-- Selector de conflicto: val-title + val-select-input (mismo patrón que invite modal) -->
50452
- <div class="conflict-select-wrap">
50453
- <val-title [props]="{ content: t('onConflictLabel'), size: 'small', color: 'dark', bold: false }" />
50454
- <val-select-input [props]="conflictSelectProps()" />
50542
+ <!-- Conflicto: radio buttons -->
50543
+ <div class="import-section">
50544
+ <val-title [props]="{ content: t('onConflictLabel'), size: 'small', color: 'dark', bold: true }" />
50545
+ <val-radio-input [props]="conflictRadioProps()" />
50455
50546
  </div>
50456
50547
 
50457
- <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
50458
- {{ t('sendActivation') }}
50459
- </ion-checkbox>
50548
+ <!-- Activación: val-check-input -->
50549
+ <div class="import-section">
50550
+ <val-check-input [props]="sendActivationProps()" />
50551
+ </div>
50460
50552
 
50461
50553
  @if (errorMsg()) {
50462
50554
  <p class="import-error">{{ errorMsg() }}</p>
@@ -50493,7 +50585,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50493
50585
  </div>
50494
50586
  }
50495
50587
  </ion-content>
50496
- `, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:16px}.import-roles-hint{display:block;margin-bottom:16px}.import-upload{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:16px}.import-upload__btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border:1px solid var(--ion-color-medium, #92949c);border-radius:10px;background:var(--ion-color-light);color:var(--ion-color-dark);font-size:.875rem;font-weight:600;cursor:pointer;transition:border-color .15s ease}.import-upload__btn:hover{border-color:var(--ion-color-primary, #7026df)}.import-upload__btn ion-icon{font-size:1.25rem}.import-upload__btn input[type=file]{display:none}.import-upload__hint{font-size:.8125rem;color:var(--ion-color-medium)}.import-file-loaded{margin:0 0 16px;font-size:.8125rem;color:var(--ion-color-medium-shade, #6b6e76)}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:20px;font-family:monospace;font-size:.8125rem}.conflict-select-wrap{margin-bottom:20px}.conflict-select-wrap val-title{display:block;margin-bottom:6px}.import-activation{display:block;margin-bottom:20px;font-size:.875rem}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"] }]
50588
+ `, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:8px}.import-roles-hint{display:block;margin-bottom:24px}.import-section{margin-bottom:28px}.import-section val-title{margin-bottom:10px}.import-method-cards{display:grid;grid-template-columns:1fr 1fr;gap:12px}.import-method-card{display:flex;flex-direction:column;align-items:flex-start;gap:4px;padding:14px 16px;border:1.5px solid var(--ion-color-light-shade, #d7d8da);border-radius:12px;background:var(--ion-color-light);cursor:pointer;text-align:left;transition:border-color .15s ease,background .15s ease}.import-method-card:hover{border-color:var(--ion-color-medium, #92949c)}.import-method-card--active{border-color:var(--ion-color-primary, #7026df);background:color-mix(in srgb,var(--ion-color-primary, #7026df) 6%,var(--ion-color-light))}.import-method-card__icon{font-size:1.375rem;color:var(--ion-color-medium-shade, #6b6e76);margin-bottom:4px}.import-method-card--active .import-method-card__icon{color:var(--ion-color-primary, #7026df)}.import-method-card__title{font-size:.875rem;font-weight:600;color:var(--ion-color-dark)}.import-method-card__hint{font-size:.75rem;color:var(--ion-color-medium, #92949c);line-height:1.3}.import-upload__btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border:1px solid var(--ion-color-medium, #92949c);border-radius:10px;background:var(--ion-color-light);color:var(--ion-color-dark);font-size:.875rem;font-weight:600;cursor:pointer;transition:border-color .15s ease}.import-upload__btn:hover{border-color:var(--ion-color-primary, #7026df)}.import-upload__btn ion-icon{font-size:1.25rem}.import-upload__btn input[type=file]{display:none}.import-file-loaded{margin:10px 0 0;font-size:.8125rem;color:var(--ion-color-medium-shade, #6b6e76)}.import-csv{--background: var(--ion-color-light);border-radius:10px;font-family:monospace;font-size:.8125rem}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:.875rem}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:.75rem;color:var(--ion-color-danger, #c0392b)}\n"] }]
50497
50589
  }], ctorParameters: () => [], propDecorators: { orgId: [{
50498
50590
  type: Input
50499
50591
  }], availableRoles: [{