@ramonbsales/noah-angular 1.0.0
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.
- package/README.md +84 -0
- package/fesm2022/ramonbsales-noah-angular.mjs +2188 -0
- package/fesm2022/ramonbsales-noah-angular.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/components/breadcrumb/breadcrumb.component.d.ts +9 -0
- package/lib/components/button/button.component.d.ts +16 -0
- package/lib/components/checkbox/checkbox.component.d.ts +24 -0
- package/lib/components/dropdown/dropdown.component.d.ts +37 -0
- package/lib/components/input/input.component.d.ts +37 -0
- package/lib/components/table/table.component.d.ts +40 -0
- package/lib/components/toggle/toggle.component.d.ts +15 -0
- package/lib/interceptors/auth.interceptor.d.ts +15 -0
- package/lib/interceptors/index.d.ts +1 -0
- package/lib/pages/login/login.component.d.ts +82 -0
- package/lib/services/index.d.ts +3 -0
- package/lib/services/local-storage/auth-storage.service.d.ts +65 -0
- package/lib/services/local-storage/local-storage.service.d.ts +82 -0
- package/lib/services/login/login.service.d.ts +96 -0
- package/lib/shared-components.component.d.ts +5 -0
- package/lib/shared-components.service.d.ts +6 -0
- package/lib/sidebar/sidebar.component.d.ts +83 -0
- package/lib/types/auth.types.d.ts +104 -0
- package/lib/types/sidebar.types.d.ts +52 -0
- package/package.json +44 -0
- package/public-api.d.ts +16 -0
|
@@ -0,0 +1,2188 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, Component, EventEmitter, forwardRef, Output, Input, inject, HostListener, InjectionToken, Optional, Inject } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import * as i3 from '@angular/forms';
|
|
6
|
+
import { ReactiveFormsModule, NG_VALUE_ACCESSOR, NgControl, FormBuilder, Validators } from '@angular/forms';
|
|
7
|
+
import * as i1$1 from '@angular/router';
|
|
8
|
+
import { RouterModule, Router } from '@angular/router';
|
|
9
|
+
import { Subject, BehaviorSubject, Observable, from, throwError } from 'rxjs';
|
|
10
|
+
import { map, takeUntil, switchMap, catchError } from 'rxjs/operators';
|
|
11
|
+
|
|
12
|
+
class SharedComponentsService {
|
|
13
|
+
constructor() { }
|
|
14
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
15
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsService, providedIn: 'root' });
|
|
16
|
+
}
|
|
17
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsService, decorators: [{
|
|
18
|
+
type: Injectable,
|
|
19
|
+
args: [{
|
|
20
|
+
providedIn: 'root'
|
|
21
|
+
}]
|
|
22
|
+
}], ctorParameters: () => [] });
|
|
23
|
+
|
|
24
|
+
class SharedComponentsComponent {
|
|
25
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
26
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: SharedComponentsComponent, isStandalone: true, selector: "lib-shared-components", ngImport: i0, template: `
|
|
27
|
+
<p>
|
|
28
|
+
shared-components works!
|
|
29
|
+
</p>
|
|
30
|
+
`, isInline: true, styles: [""] });
|
|
31
|
+
}
|
|
32
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsComponent, decorators: [{
|
|
33
|
+
type: Component,
|
|
34
|
+
args: [{ selector: 'lib-shared-components', imports: [], template: `
|
|
35
|
+
<p>
|
|
36
|
+
shared-components works!
|
|
37
|
+
</p>
|
|
38
|
+
` }]
|
|
39
|
+
}] });
|
|
40
|
+
|
|
41
|
+
class DropdownComponent {
|
|
42
|
+
label = 'Nome do campo';
|
|
43
|
+
placeholder = 'Selecione uma opção';
|
|
44
|
+
options = [];
|
|
45
|
+
disabled = false;
|
|
46
|
+
required = false;
|
|
47
|
+
errorMessage = 'Campo obrigatório';
|
|
48
|
+
selectedOption = null;
|
|
49
|
+
hideLabel = false;
|
|
50
|
+
valueChange = new EventEmitter();
|
|
51
|
+
internalValue = null;
|
|
52
|
+
isOpen = false;
|
|
53
|
+
// ControlValueAccessor implementation
|
|
54
|
+
onChange = (value) => { };
|
|
55
|
+
onTouched = () => { };
|
|
56
|
+
constructor() { }
|
|
57
|
+
ngOnInit() {
|
|
58
|
+
// Se selectedOption foi passado como Input, use-o como valor inicial
|
|
59
|
+
if (this.selectedOption !== null && this.selectedOption !== undefined) {
|
|
60
|
+
this.internalValue = this.selectedOption;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
ngOnChanges(changes) {
|
|
64
|
+
// Detecta mudanças no selectedOption Input
|
|
65
|
+
if (changes['selectedOption'] && !changes['selectedOption'].firstChange) {
|
|
66
|
+
const newValue = changes['selectedOption'].currentValue;
|
|
67
|
+
if (newValue !== this.internalValue) {
|
|
68
|
+
this.internalValue = newValue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
selectOption(option) {
|
|
73
|
+
this.internalValue = option.value;
|
|
74
|
+
this.isOpen = false;
|
|
75
|
+
this.onChange(option.value);
|
|
76
|
+
this.onTouched();
|
|
77
|
+
this.valueChange.emit(option.value);
|
|
78
|
+
}
|
|
79
|
+
toggleDropdown() {
|
|
80
|
+
if (!this.disabled) {
|
|
81
|
+
this.isOpen = !this.isOpen;
|
|
82
|
+
if (this.isOpen) {
|
|
83
|
+
this.onTouched();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
getSelectedLabel() {
|
|
88
|
+
if (this.internalValue !== null && this.internalValue !== undefined) {
|
|
89
|
+
const option = this.options.find(opt => opt.value === this.internalValue);
|
|
90
|
+
return option ? option.label : '';
|
|
91
|
+
}
|
|
92
|
+
return this.placeholder;
|
|
93
|
+
}
|
|
94
|
+
hasValue() {
|
|
95
|
+
return this.internalValue !== null && this.internalValue !== undefined;
|
|
96
|
+
}
|
|
97
|
+
// ControlValueAccessor methods
|
|
98
|
+
writeValue(value) {
|
|
99
|
+
this.internalValue = value;
|
|
100
|
+
this.valueChange.emit(value);
|
|
101
|
+
}
|
|
102
|
+
registerOnChange(fn) {
|
|
103
|
+
this.onChange = fn;
|
|
104
|
+
}
|
|
105
|
+
registerOnTouched(fn) {
|
|
106
|
+
this.onTouched = fn;
|
|
107
|
+
}
|
|
108
|
+
setDisabledState(isDisabled) {
|
|
109
|
+
this.disabled = isDisabled;
|
|
110
|
+
}
|
|
111
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
112
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: DropdownComponent, isStandalone: true, selector: "lib-dropdown", inputs: { label: "label", placeholder: "placeholder", options: "options", disabled: "disabled", required: "required", errorMessage: "errorMessage", selectedOption: "selectedOption", hideLabel: "hideLabel" }, outputs: { valueChange: "valueChange" }, providers: [
|
|
113
|
+
{
|
|
114
|
+
provide: NG_VALUE_ACCESSOR,
|
|
115
|
+
useExisting: forwardRef(() => DropdownComponent),
|
|
116
|
+
multi: true
|
|
117
|
+
}
|
|
118
|
+
], usesOnChanges: true, ngImport: i0, template: "<label *ngIf=\"!hideLabel\" for=\"\">{{label}}</label>\n<div class=\"dropdown\" tabindex=\"0\" (blur)=\"isOpen = false\">\n <button class=\"btn btn-light w-100 text-start d-flex justify-content-between align-items-center\" type=\"button\"\n (click)=\"toggleDropdown()\" [attr.aria-expanded]=\"isOpen\" [disabled]=\"disabled\">\n <span class=\"select-option-text\" *ngIf=\"hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"placeholder-text\" *ngIf=\"!hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"material-icons\">\n unfold_more\n </span>\n </button>\n <ul class=\"dropdown-menu w-100 show\" *ngIf=\"isOpen\">\n <li *ngFor=\"let option of options\">\n <a class=\"dropdown-item\" (click)=\"selectOption(option)\">{{ option.label }}</a>\n </li>\n </ul>\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.dropdown{font-family:DM Sans,sans-serif}.btn{font-family:DM Sans,sans-serif;font-weight:500;font-size:.875rem;border:1px solid #dee2e6;background-color:#fff}.btn .select-option-text{color:#343a40}.btn .placeholder-text{color:#6c757d}.btn .material-icons{font-size:.875rem}.dropdown-item{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
119
|
+
}
|
|
120
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DropdownComponent, decorators: [{
|
|
121
|
+
type: Component,
|
|
122
|
+
args: [{ selector: 'lib-dropdown', imports: [CommonModule, ReactiveFormsModule], providers: [
|
|
123
|
+
{
|
|
124
|
+
provide: NG_VALUE_ACCESSOR,
|
|
125
|
+
useExisting: forwardRef(() => DropdownComponent),
|
|
126
|
+
multi: true
|
|
127
|
+
}
|
|
128
|
+
], template: "<label *ngIf=\"!hideLabel\" for=\"\">{{label}}</label>\n<div class=\"dropdown\" tabindex=\"0\" (blur)=\"isOpen = false\">\n <button class=\"btn btn-light w-100 text-start d-flex justify-content-between align-items-center\" type=\"button\"\n (click)=\"toggleDropdown()\" [attr.aria-expanded]=\"isOpen\" [disabled]=\"disabled\">\n <span class=\"select-option-text\" *ngIf=\"hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"placeholder-text\" *ngIf=\"!hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"material-icons\">\n unfold_more\n </span>\n </button>\n <ul class=\"dropdown-menu w-100 show\" *ngIf=\"isOpen\">\n <li *ngFor=\"let option of options\">\n <a class=\"dropdown-item\" (click)=\"selectOption(option)\">{{ option.label }}</a>\n </li>\n </ul>\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.dropdown{font-family:DM Sans,sans-serif}.btn{font-family:DM Sans,sans-serif;font-weight:500;font-size:.875rem;border:1px solid #dee2e6;background-color:#fff}.btn .select-option-text{color:#343a40}.btn .placeholder-text{color:#6c757d}.btn .material-icons{font-size:.875rem}.dropdown-item{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;cursor:pointer}\n"] }]
|
|
129
|
+
}], ctorParameters: () => [], propDecorators: { label: [{
|
|
130
|
+
type: Input
|
|
131
|
+
}], placeholder: [{
|
|
132
|
+
type: Input
|
|
133
|
+
}], options: [{
|
|
134
|
+
type: Input
|
|
135
|
+
}], disabled: [{
|
|
136
|
+
type: Input
|
|
137
|
+
}], required: [{
|
|
138
|
+
type: Input
|
|
139
|
+
}], errorMessage: [{
|
|
140
|
+
type: Input
|
|
141
|
+
}], selectedOption: [{
|
|
142
|
+
type: Input
|
|
143
|
+
}], hideLabel: [{
|
|
144
|
+
type: Input
|
|
145
|
+
}], valueChange: [{
|
|
146
|
+
type: Output
|
|
147
|
+
}] } });
|
|
148
|
+
|
|
149
|
+
class InputComponent {
|
|
150
|
+
injector;
|
|
151
|
+
label = 'Nome do campo';
|
|
152
|
+
placeholder = 'Digite aqui...';
|
|
153
|
+
type = 'text';
|
|
154
|
+
customErrorMessages = {};
|
|
155
|
+
// Detects if the field is required based on FormControl validator
|
|
156
|
+
get isRequired() {
|
|
157
|
+
if (this.control?.control?.validator) {
|
|
158
|
+
const validator = this.control.control.validator({});
|
|
159
|
+
return !!(validator && validator['required']);
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
// Detects if the field is disabled
|
|
164
|
+
get isDisabled() {
|
|
165
|
+
return (this.control?.control?.disabled ?? false);
|
|
166
|
+
}
|
|
167
|
+
value = '';
|
|
168
|
+
isFocused = false;
|
|
169
|
+
control;
|
|
170
|
+
// ControlValueAccessor implementation
|
|
171
|
+
onChange = (value) => { };
|
|
172
|
+
onTouched = () => { };
|
|
173
|
+
// Mapeamento padrão de mensagens de erro
|
|
174
|
+
defaultErrorMessages = {
|
|
175
|
+
required: 'Campo obrigatório',
|
|
176
|
+
email: 'Email inválido',
|
|
177
|
+
minlength: 'Muito curto',
|
|
178
|
+
maxlength: 'Muito longo',
|
|
179
|
+
min: 'Valor muito baixo',
|
|
180
|
+
max: 'Valor muito alto',
|
|
181
|
+
pattern: 'Formato inválido'
|
|
182
|
+
};
|
|
183
|
+
constructor(injector) {
|
|
184
|
+
this.injector = injector;
|
|
185
|
+
}
|
|
186
|
+
ngOnInit() {
|
|
187
|
+
// Injeta o NgControl para acessar os erros do FormControl
|
|
188
|
+
this.control = this.injector.get(NgControl, null) || undefined;
|
|
189
|
+
}
|
|
190
|
+
onFocus() {
|
|
191
|
+
this.isFocused = true;
|
|
192
|
+
this.onTouched();
|
|
193
|
+
}
|
|
194
|
+
onBlur() {
|
|
195
|
+
this.isFocused = false;
|
|
196
|
+
}
|
|
197
|
+
onInput(event) {
|
|
198
|
+
const target = event.target;
|
|
199
|
+
let newValue = target.value;
|
|
200
|
+
// Conversão de tipos
|
|
201
|
+
if (this.type === 'number') {
|
|
202
|
+
newValue = newValue === '' ? null : Number(newValue);
|
|
203
|
+
}
|
|
204
|
+
this.value = newValue;
|
|
205
|
+
this.onChange(newValue);
|
|
206
|
+
}
|
|
207
|
+
// Verifica se deve mostrar erro
|
|
208
|
+
get showError() {
|
|
209
|
+
return !!(this.control?.invalid && (this.control?.dirty || this.control?.touched));
|
|
210
|
+
}
|
|
211
|
+
get showRequiredAsterisk() {
|
|
212
|
+
return !!this.isRequired && !!this.control?.invalid;
|
|
213
|
+
}
|
|
214
|
+
// Obtém a mensagem de erro do FormControl
|
|
215
|
+
get errorMessage() {
|
|
216
|
+
if (!this.control?.errors) {
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
const errors = this.control.errors;
|
|
220
|
+
// Primeiro verifica se há mensagem customizada
|
|
221
|
+
for (const errorKey in errors) {
|
|
222
|
+
if (this.customErrorMessages[errorKey]) {
|
|
223
|
+
const errorValue = errors[errorKey];
|
|
224
|
+
return this.interpolateErrorMessage(this.customErrorMessages[errorKey], errorValue);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Depois verifica mensagens padrão
|
|
228
|
+
for (const errorKey in errors) {
|
|
229
|
+
if (this.defaultErrorMessages[errorKey]) {
|
|
230
|
+
const errorValue = errors[errorKey];
|
|
231
|
+
return this.interpolateErrorMessage(this.defaultErrorMessages[errorKey], errorValue);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Retorna erro genérico se não encontrar mensagem específica
|
|
235
|
+
return 'Campo inválido';
|
|
236
|
+
}
|
|
237
|
+
// Interpola valores dinâmicos na mensagem de erro
|
|
238
|
+
interpolateErrorMessage(message, errorValue) {
|
|
239
|
+
if (typeof errorValue === 'object') {
|
|
240
|
+
// Para erros como minlength, maxlength, min, max
|
|
241
|
+
if (errorValue.requiredLength) {
|
|
242
|
+
message = message.replace('{{requiredLength}}', errorValue.requiredLength);
|
|
243
|
+
message = message.replace('{{actualLength}}', errorValue.actualLength);
|
|
244
|
+
}
|
|
245
|
+
if (errorValue.min !== undefined) {
|
|
246
|
+
message = message.replace('{{min}}', errorValue.min);
|
|
247
|
+
message = message.replace('{{actual}}', errorValue.actual);
|
|
248
|
+
}
|
|
249
|
+
if (errorValue.max !== undefined) {
|
|
250
|
+
message = message.replace('{{max}}', errorValue.max);
|
|
251
|
+
message = message.replace('{{actual}}', errorValue.actual);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return message;
|
|
255
|
+
}
|
|
256
|
+
validateField() {
|
|
257
|
+
// Removido - agora usa a validação do FormControl
|
|
258
|
+
}
|
|
259
|
+
hasValue() {
|
|
260
|
+
return this.value !== null && this.value !== undefined && this.value !== '';
|
|
261
|
+
}
|
|
262
|
+
// ControlValueAccessor methods
|
|
263
|
+
writeValue(value) {
|
|
264
|
+
this.value = value;
|
|
265
|
+
}
|
|
266
|
+
registerOnChange(fn) {
|
|
267
|
+
this.onChange = fn;
|
|
268
|
+
}
|
|
269
|
+
registerOnTouched(fn) {
|
|
270
|
+
this.onTouched = fn;
|
|
271
|
+
}
|
|
272
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: InputComponent, deps: [{ token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component });
|
|
273
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: InputComponent, isStandalone: true, selector: "lib-input", inputs: { label: "label", placeholder: "placeholder", type: "type", customErrorMessages: "customErrorMessages" }, providers: [
|
|
274
|
+
{
|
|
275
|
+
provide: NG_VALUE_ACCESSOR,
|
|
276
|
+
useExisting: forwardRef(() => InputComponent),
|
|
277
|
+
multi: true
|
|
278
|
+
}
|
|
279
|
+
], ngImport: i0, template: "<div class=\"form-group\">\n\n <label>{{ label }}\n <span class=\"required-asterisk\" *ngIf=\"showRequiredAsterisk\">*</span>\n </label>\n\n <div class=\"input-container\" [class.focused]=\"isFocused\" [class.has-value]=\"hasValue()\" [class.disabled]=\"isDisabled\"\n [class.error]=\"showError\">\n\n <input [type]=\"type\" [placeholder]=\"placeholder\" [value]=\"value || ''\" [disabled]=\"isDisabled\"\n [required]=\"isRequired\" (input)=\"onInput($event)\" (focus)=\"onFocus()\" (blur)=\"onBlur()\"\n [ngClass]=\"{'error': showError}\" />\n </div>\n\n <small class=\"form-text error\" *ngIf=\"showError\">\n {{ errorMessage }}\n </small>\n\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.required-asterisk{color:#dc3545;margin-left:.25rem}.input-container{font-family:DM Sans,sans-serif}input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid #dee2e6;border-radius:.25rem;display:block;width:100%;padding:.375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-clip:padding-box;outline:none}input::placeholder{color:#6c757d}input:focus{border-color:#b2b9be;box-shadow:none}input:focus.error{border-color:#dc3545}small.error{color:#dc3545}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
280
|
+
}
|
|
281
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: InputComponent, decorators: [{
|
|
282
|
+
type: Component,
|
|
283
|
+
args: [{ selector: 'lib-input', imports: [CommonModule, ReactiveFormsModule], providers: [
|
|
284
|
+
{
|
|
285
|
+
provide: NG_VALUE_ACCESSOR,
|
|
286
|
+
useExisting: forwardRef(() => InputComponent),
|
|
287
|
+
multi: true
|
|
288
|
+
}
|
|
289
|
+
], template: "<div class=\"form-group\">\n\n <label>{{ label }}\n <span class=\"required-asterisk\" *ngIf=\"showRequiredAsterisk\">*</span>\n </label>\n\n <div class=\"input-container\" [class.focused]=\"isFocused\" [class.has-value]=\"hasValue()\" [class.disabled]=\"isDisabled\"\n [class.error]=\"showError\">\n\n <input [type]=\"type\" [placeholder]=\"placeholder\" [value]=\"value || ''\" [disabled]=\"isDisabled\"\n [required]=\"isRequired\" (input)=\"onInput($event)\" (focus)=\"onFocus()\" (blur)=\"onBlur()\"\n [ngClass]=\"{'error': showError}\" />\n </div>\n\n <small class=\"form-text error\" *ngIf=\"showError\">\n {{ errorMessage }}\n </small>\n\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.required-asterisk{color:#dc3545;margin-left:.25rem}.input-container{font-family:DM Sans,sans-serif}input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid #dee2e6;border-radius:.25rem;display:block;width:100%;padding:.375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-clip:padding-box;outline:none}input::placeholder{color:#6c757d}input:focus{border-color:#b2b9be;box-shadow:none}input:focus.error{border-color:#dc3545}small.error{color:#dc3545}\n"] }]
|
|
290
|
+
}], ctorParameters: () => [{ type: i0.Injector }], propDecorators: { label: [{
|
|
291
|
+
type: Input
|
|
292
|
+
}], placeholder: [{
|
|
293
|
+
type: Input
|
|
294
|
+
}], type: [{
|
|
295
|
+
type: Input
|
|
296
|
+
}], customErrorMessages: [{
|
|
297
|
+
type: Input
|
|
298
|
+
}] } });
|
|
299
|
+
|
|
300
|
+
class ButtonComponent {
|
|
301
|
+
title = 'Salvar informação';
|
|
302
|
+
backgroundColor = '#007bff';
|
|
303
|
+
color = '#fff';
|
|
304
|
+
borderColor = '#007bff';
|
|
305
|
+
minWidth = '8rem';
|
|
306
|
+
disabled = false;
|
|
307
|
+
loading = false;
|
|
308
|
+
loadingText = 'Carregando...';
|
|
309
|
+
onClickEvent = new EventEmitter();
|
|
310
|
+
onClick() {
|
|
311
|
+
if (!this.loading && !this.disabled) {
|
|
312
|
+
this.onClickEvent.emit();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
316
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: ButtonComponent, isStandalone: true, selector: "lib-button", inputs: { title: "title", backgroundColor: "backgroundColor", color: "color", borderColor: "borderColor", minWidth: "minWidth", disabled: "disabled", loading: "loading", loadingText: "loadingText" }, outputs: { onClickEvent: "onClickEvent" }, ngImport: i0, template: "<button type=\"button\" class=\"btn\" [style.background-color]=\"backgroundColor\" [style.color]=\"color\"\n [style.border-color]=\"borderColor\" [style.min-width]=\"minWidth\" [disabled]=\"disabled || loading\"\n (click)=\"onClick()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-2\" role=\"status\" aria-hidden=\"true\"></span>\n <span *ngIf=\"!loading\">{{title}}</span>\n <span *ngIf=\"loading\">{{loadingText}}</span>\n</button>", styles: ["span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
317
|
+
}
|
|
318
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ButtonComponent, decorators: [{
|
|
319
|
+
type: Component,
|
|
320
|
+
args: [{ selector: 'lib-button', imports: [CommonModule], template: "<button type=\"button\" class=\"btn\" [style.background-color]=\"backgroundColor\" [style.color]=\"color\"\n [style.border-color]=\"borderColor\" [style.min-width]=\"minWidth\" [disabled]=\"disabled || loading\"\n (click)=\"onClick()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-2\" role=\"status\" aria-hidden=\"true\"></span>\n <span *ngIf=\"!loading\">{{title}}</span>\n <span *ngIf=\"loading\">{{loadingText}}</span>\n</button>", styles: ["span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"] }]
|
|
321
|
+
}], propDecorators: { title: [{
|
|
322
|
+
type: Input
|
|
323
|
+
}], backgroundColor: [{
|
|
324
|
+
type: Input
|
|
325
|
+
}], color: [{
|
|
326
|
+
type: Input
|
|
327
|
+
}], borderColor: [{
|
|
328
|
+
type: Input
|
|
329
|
+
}], minWidth: [{
|
|
330
|
+
type: Input
|
|
331
|
+
}], disabled: [{
|
|
332
|
+
type: Input
|
|
333
|
+
}], loading: [{
|
|
334
|
+
type: Input
|
|
335
|
+
}], loadingText: [{
|
|
336
|
+
type: Input
|
|
337
|
+
}], onClickEvent: [{
|
|
338
|
+
type: Output
|
|
339
|
+
}] } });
|
|
340
|
+
|
|
341
|
+
class CheckboxComponent {
|
|
342
|
+
/** Array de opções: [{ label: string, value: any }] */
|
|
343
|
+
options = [];
|
|
344
|
+
/** Se true, permite múltipla seleção */
|
|
345
|
+
multiple = false;
|
|
346
|
+
/** FormControl do Reactive Forms */
|
|
347
|
+
control;
|
|
348
|
+
/** Emite valor selecionado ao pai (opcional) */
|
|
349
|
+
selectedChange = new EventEmitter();
|
|
350
|
+
get disabled() {
|
|
351
|
+
return !!this.control?.disabled;
|
|
352
|
+
}
|
|
353
|
+
get selected() {
|
|
354
|
+
return this.control?.value;
|
|
355
|
+
}
|
|
356
|
+
isChecked(value) {
|
|
357
|
+
if (this.multiple) {
|
|
358
|
+
return Array.isArray(this.selected) && this.selected.includes(value);
|
|
359
|
+
}
|
|
360
|
+
return this.selected === value;
|
|
361
|
+
}
|
|
362
|
+
onOptionChange(event, value) {
|
|
363
|
+
if (this.disabled)
|
|
364
|
+
return;
|
|
365
|
+
if (this.multiple) {
|
|
366
|
+
const checked = event.target.checked;
|
|
367
|
+
let newSelected = Array.isArray(this.selected) ? [...this.selected] : [];
|
|
368
|
+
if (checked) {
|
|
369
|
+
if (!newSelected.includes(value))
|
|
370
|
+
newSelected.push(value);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
newSelected = newSelected.filter(v => v !== value);
|
|
374
|
+
}
|
|
375
|
+
this.control?.setValue(newSelected);
|
|
376
|
+
this.control?.markAsDirty();
|
|
377
|
+
this.selectedChange.emit(newSelected);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
this.control?.setValue(value);
|
|
381
|
+
this.control?.markAsDirty();
|
|
382
|
+
this.selectedChange.emit(value);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
get showError() {
|
|
386
|
+
return !!this.control && this.control.invalid && (this.control.dirty || this.control.touched);
|
|
387
|
+
}
|
|
388
|
+
get errorMessage() {
|
|
389
|
+
if (!this.control || !this.control.errors)
|
|
390
|
+
return null;
|
|
391
|
+
if (this.control.errors['required'])
|
|
392
|
+
return 'Seleção obrigatória.';
|
|
393
|
+
// Adicione outras mensagens customizadas conforme necessário
|
|
394
|
+
return 'Valor inválido.';
|
|
395
|
+
}
|
|
396
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: CheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
397
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: CheckboxComponent, isStandalone: true, selector: "lib-checkbox", inputs: { options: "options", multiple: "multiple", control: "control" }, outputs: { selectedChange: "selectedChange" }, ngImport: i0, template: "<div *ngFor=\"let option of options; let i = index\"\n style=\"display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;\">\n\n <input class=\"form-check-input\" [type]=\"multiple ? 'checkbox' : 'radio'\"\n [name]=\"multiple ? 'checkbox-group' : 'radio-group'\" [value]=\"option.value\" [checked]=\"isChecked(option.value)\"\n [disabled]=\"disabled\" (change)=\"onOptionChange($event, option.value)\" />\n <span>{{ option.label }}</span>\n</div>\n<div *ngIf=\"showError\" class=\"text-danger small mt-1\">\n {{ errorMessage }}\n</div>", styles: ["input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
398
|
+
}
|
|
399
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: CheckboxComponent, decorators: [{
|
|
400
|
+
type: Component,
|
|
401
|
+
args: [{ selector: 'lib-checkbox', imports: [CommonModule, ReactiveFormsModule], template: "<div *ngFor=\"let option of options; let i = index\"\n style=\"display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;\">\n\n <input class=\"form-check-input\" [type]=\"multiple ? 'checkbox' : 'radio'\"\n [name]=\"multiple ? 'checkbox-group' : 'radio-group'\" [value]=\"option.value\" [checked]=\"isChecked(option.value)\"\n [disabled]=\"disabled\" (change)=\"onOptionChange($event, option.value)\" />\n <span>{{ option.label }}</span>\n</div>\n<div *ngIf=\"showError\" class=\"text-danger small mt-1\">\n {{ errorMessage }}\n</div>", styles: ["input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"] }]
|
|
402
|
+
}], propDecorators: { options: [{
|
|
403
|
+
type: Input
|
|
404
|
+
}], multiple: [{
|
|
405
|
+
type: Input
|
|
406
|
+
}], control: [{
|
|
407
|
+
type: Input
|
|
408
|
+
}], selectedChange: [{
|
|
409
|
+
type: Output
|
|
410
|
+
}] } });
|
|
411
|
+
|
|
412
|
+
class ToggleComponent {
|
|
413
|
+
title = '';
|
|
414
|
+
enableTitle = false;
|
|
415
|
+
value = false;
|
|
416
|
+
onChange = (value) => { };
|
|
417
|
+
onTouched = () => { };
|
|
418
|
+
writeValue(value) {
|
|
419
|
+
this.value = value;
|
|
420
|
+
}
|
|
421
|
+
registerOnChange(fn) {
|
|
422
|
+
this.onChange = fn;
|
|
423
|
+
}
|
|
424
|
+
registerOnTouched(fn) {
|
|
425
|
+
this.onTouched = fn;
|
|
426
|
+
}
|
|
427
|
+
toggle() {
|
|
428
|
+
this.value = !this.value;
|
|
429
|
+
this.onChange(this.value);
|
|
430
|
+
this.onTouched();
|
|
431
|
+
}
|
|
432
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
433
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: ToggleComponent, isStandalone: true, selector: "lib-toggle", inputs: { title: "title", enableTitle: "enableTitle" }, providers: [
|
|
434
|
+
{
|
|
435
|
+
provide: NG_VALUE_ACCESSOR,
|
|
436
|
+
useExisting: forwardRef(() => ToggleComponent),
|
|
437
|
+
multi: true
|
|
438
|
+
}
|
|
439
|
+
], ngImport: i0, template: "<div class=\"form-check form-switch centered-toggle\">\n <div>\n <input class=\"form-check-input\" type=\"checkbox\" [checked]=\"value\" (change)=\"toggle()\" />\n </div>\n <span *ngIf=\"enableTitle\">{{ title }}</span>\n</div>", styles: [".centered-toggle{display:flex;align-items:anchor-center}input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
440
|
+
}
|
|
441
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ToggleComponent, decorators: [{
|
|
442
|
+
type: Component,
|
|
443
|
+
args: [{ selector: 'lib-toggle', imports: [CommonModule], providers: [
|
|
444
|
+
{
|
|
445
|
+
provide: NG_VALUE_ACCESSOR,
|
|
446
|
+
useExisting: forwardRef(() => ToggleComponent),
|
|
447
|
+
multi: true
|
|
448
|
+
}
|
|
449
|
+
], template: "<div class=\"form-check form-switch centered-toggle\">\n <div>\n <input class=\"form-check-input\" type=\"checkbox\" [checked]=\"value\" (change)=\"toggle()\" />\n </div>\n <span *ngIf=\"enableTitle\">{{ title }}</span>\n</div>", styles: [".centered-toggle{display:flex;align-items:anchor-center}input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"] }]
|
|
450
|
+
}], propDecorators: { title: [{
|
|
451
|
+
type: Input
|
|
452
|
+
}], enableTitle: [{
|
|
453
|
+
type: Input
|
|
454
|
+
}] } });
|
|
455
|
+
|
|
456
|
+
class BreadcrumbComponent {
|
|
457
|
+
items = [];
|
|
458
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: BreadcrumbComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
459
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: BreadcrumbComponent, isStandalone: true, selector: "lib-breadcrumb", inputs: { items: "items" }, ngImport: i0, template: "<nav class=\"breadcrumb\">\n <ol class=\"breadcrumb-list\">\n <li class=\"breadcrumb-item\" *ngFor=\"let item of items; let last = last\">\n <a *ngIf=\"!last\" [routerLink]=\"item.url\">{{ item.label }}</a>\n <span *ngIf=\"last\">{{ item.label }}</span>\n </li>\n </ol>\n</nav>", styles: [".breadcrumb{font-family:DM Sans,sans-serif;background:none;padding:.5rem 0;margin-bottom:1rem}.breadcrumb-list{display:flex;flex-wrap:wrap;list-style:none;padding:0;margin:0}.breadcrumb-item{display:flex;align-items:center;font-size:.875rem;color:#495057;font-weight:500}.breadcrumb-item a{color:#414141;text-decoration:none;padding:.25rem .5rem;border-radius:4px;transition:background .2s,color .2s}.breadcrumb-item a:hover{background:#dee2e6;color:#343a40;text-decoration:underline}.breadcrumb-separator{margin:0 .25rem;color:#b2b9be;font-size:1rem}.breadcrumb-item:last-child{color:#6c757d;font-weight:400}\n"], dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
460
|
+
}
|
|
461
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: BreadcrumbComponent, decorators: [{
|
|
462
|
+
type: Component,
|
|
463
|
+
args: [{ selector: 'lib-breadcrumb', imports: [RouterModule, CommonModule], template: "<nav class=\"breadcrumb\">\n <ol class=\"breadcrumb-list\">\n <li class=\"breadcrumb-item\" *ngFor=\"let item of items; let last = last\">\n <a *ngIf=\"!last\" [routerLink]=\"item.url\">{{ item.label }}</a>\n <span *ngIf=\"last\">{{ item.label }}</span>\n </li>\n </ol>\n</nav>", styles: [".breadcrumb{font-family:DM Sans,sans-serif;background:none;padding:.5rem 0;margin-bottom:1rem}.breadcrumb-list{display:flex;flex-wrap:wrap;list-style:none;padding:0;margin:0}.breadcrumb-item{display:flex;align-items:center;font-size:.875rem;color:#495057;font-weight:500}.breadcrumb-item a{color:#414141;text-decoration:none;padding:.25rem .5rem;border-radius:4px;transition:background .2s,color .2s}.breadcrumb-item a:hover{background:#dee2e6;color:#343a40;text-decoration:underline}.breadcrumb-separator{margin:0 .25rem;color:#b2b9be;font-size:1rem}.breadcrumb-item:last-child{color:#6c757d;font-weight:400}\n"] }]
|
|
464
|
+
}], propDecorators: { items: [{
|
|
465
|
+
type: Input
|
|
466
|
+
}] } });
|
|
467
|
+
|
|
468
|
+
class TableComponent {
|
|
469
|
+
columns = [];
|
|
470
|
+
data = [];
|
|
471
|
+
columnTypes = {};
|
|
472
|
+
page = 1;
|
|
473
|
+
pageSize = 10;
|
|
474
|
+
totalItems = 0;
|
|
475
|
+
pageSizeOptions = [
|
|
476
|
+
{ value: 5, label: '5' },
|
|
477
|
+
{ value: 10, label: '10' },
|
|
478
|
+
{ value: 20, label: '20' },
|
|
479
|
+
{ value: 50, label: '50' }
|
|
480
|
+
];
|
|
481
|
+
pageChange = new EventEmitter();
|
|
482
|
+
pageSizeChange = new EventEmitter();
|
|
483
|
+
get hasActions() {
|
|
484
|
+
return this.columns.some(c => !!c.actions && c.actions.length > 0);
|
|
485
|
+
}
|
|
486
|
+
get actionsList() {
|
|
487
|
+
const col = this.columns.find((c) => !!c.actions && c.actions.length > 0);
|
|
488
|
+
return col?.actions || [];
|
|
489
|
+
}
|
|
490
|
+
onPageSizeChange(newSize) {
|
|
491
|
+
this.pageSizeChange.emit(newSize);
|
|
492
|
+
}
|
|
493
|
+
// Paginação
|
|
494
|
+
get totalPages() {
|
|
495
|
+
return Math.max(1, Math.ceil(this.totalItems / this.pageSize));
|
|
496
|
+
}
|
|
497
|
+
get currentPage() {
|
|
498
|
+
return this.page;
|
|
499
|
+
}
|
|
500
|
+
goToPage(page) {
|
|
501
|
+
if (page < 1 || page > this.totalPages || page === this.currentPage)
|
|
502
|
+
return;
|
|
503
|
+
this.pageChange.emit(page);
|
|
504
|
+
}
|
|
505
|
+
getPages() {
|
|
506
|
+
const pages = [];
|
|
507
|
+
const total = this.totalPages;
|
|
508
|
+
const current = this.currentPage;
|
|
509
|
+
if (total <= 8) {
|
|
510
|
+
for (let i = 1; i <= total; i++)
|
|
511
|
+
pages.push(i);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
if (current <= 4) {
|
|
515
|
+
for (let i = 1; i <= 5; i++)
|
|
516
|
+
pages.push(i);
|
|
517
|
+
pages.push('...');
|
|
518
|
+
pages.push(total - 1, total);
|
|
519
|
+
}
|
|
520
|
+
else if (current >= total - 3) {
|
|
521
|
+
pages.push(1, 2);
|
|
522
|
+
pages.push('...');
|
|
523
|
+
for (let i = total - 4; i <= total; i++)
|
|
524
|
+
pages.push(i);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
pages.push(1, 2);
|
|
528
|
+
pages.push('...');
|
|
529
|
+
for (let i = current - 1; i <= current + 1; i++)
|
|
530
|
+
pages.push(i);
|
|
531
|
+
pages.push('...');
|
|
532
|
+
pages.push(total - 1, total);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return pages;
|
|
536
|
+
}
|
|
537
|
+
toNumber(val) {
|
|
538
|
+
return typeof val === 'number' ? val : parseInt(val, 10);
|
|
539
|
+
}
|
|
540
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
541
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: TableComponent, isStandalone: true, selector: "lib-table", inputs: { columns: "columns", data: "data", columnTypes: "columnTypes", page: "page", pageSize: "pageSize", totalItems: "totalItems", pageSizeOptions: "pageSizeOptions" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<table class=\"table\">\n <thead class=\"table-light\">\n <tr>\n <ng-container *ngFor=\"let col of columns\">\n <th [style.width]=\"col.width || null\">\n {{ col.title }}\n </th>\n </ng-container>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <ng-container *ngFor=\"let col of columns\">\n <td class=\"align-middle\">\n <ng-container *ngIf=\"!col.actions\" [ngSwitch]=\"columnTypes[col.prop]\">\n <ng-container *ngSwitchCase=\"'date'\">{{ row[col.prop] | date:'dd/MM/yyyy' }}</ng-container>\n <ng-container *ngSwitchCase=\"'time'\">{{ row[col.prop] | date:'HH:mm' }}</ng-container>\n <ng-container *ngSwitchCase=\"'tag'\">\n <span class=\"badge rounded-pill\" [ngClass]=\"row[col.prop + 'Class'] || 'text-bg-dark'\">{{\n row[col.prop] }}</span>\n </ng-container>\n <ng-container *ngSwitchDefault>{{ row[col.prop] }}</ng-container>\n </ng-container>\n <ng-container *ngIf=\"col.actions\">\n <div class=\"dropdown\">\n <button class=\"btn btn-light btn-sm\" type=\"button\" data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\">\n <i class=\"material-icons align-middle\">more_horiz</i>\n </button>\n <ul class=\"dropdown-menu\">\n <li *ngFor=\"let action of col.actions\">\n <a class=\"dropdown-item\" href=\"#\"\n (click)=\"action.callback(row); $event.preventDefault()\">\n <i *ngIf=\"action.icon\" class=\"material-icons align-middle me-2\">{{ action.icon\n }}</i>\n {{ action.title }}\n </a>\n </li>\n </ul>\n </div>\n </ng-container>\n </td>\n </ng-container>\n </tr>\n </tbody>\n</table>\n\n<!-- Paginator -->\n<nav aria-label=\"Tabela pagina\u00E7\u00E3o\" class=\"d-flex justify-content-between mt-3\">\n <lib-dropdown [options]=\"pageSizeOptions\" placeholder=\"Tamanho da p\u00E1gina\" [selectedOption]=\"pageSize\"\n [hideLabel]=\"true\" (valueChange)=\"onPageSizeChange($event)\"></lib-dropdown>\n\n <ul *ngIf=\"totalPages > 1\" class=\"pagination pagination-sm\">\n <li class=\"page-item\" [class.disabled]=\"currentPage === 1\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1\">\n <i class=\"material-icons align-middle\">chevron_left</i>\n </button>\n </li>\n <ng-container *ngFor=\"let page of getPages()\">\n <li class=\"page-item\" *ngIf=\"page !== '...'\" [class.active]=\"page === currentPage\">\n <button class=\"page-link\" (click)=\"goToPage(toNumber(page))\" [disabled]=\"page === currentPage\">{{ page\n }}</button>\n </li>\n <li class=\"page-item disabled\" *ngIf=\"page === '...'\"><span class=\"page-link\">...</span></li>\n </ng-container>\n <li class=\"page-item\" [class.disabled]=\"currentPage === totalPages\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages\">\n <i class=\"material-icons align-middle\">chevron_right</i>\n </button>\n </li>\n </ul>\n</nav>", styles: ["thead{border-top:1px solid #c6c7c8}thead th{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem}tbody th,tbody td,tbody .dropdown-menu{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}.pagination i{font-size:20px}.pagination .page-item .page-link{color:#414141;background-color:#fff}.pagination .page-item.active .page-link{color:#fff;background-color:#414141;border-color:#414141}\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.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "pipe", type: i1.DatePipe, name: "date" }, { kind: "component", type: DropdownComponent, selector: "lib-dropdown", inputs: ["label", "placeholder", "options", "disabled", "required", "errorMessage", "selectedOption", "hideLabel"], outputs: ["valueChange"] }] });
|
|
542
|
+
}
|
|
543
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: TableComponent, decorators: [{
|
|
544
|
+
type: Component,
|
|
545
|
+
args: [{ selector: 'lib-table', imports: [CommonModule, DropdownComponent], template: "<table class=\"table\">\n <thead class=\"table-light\">\n <tr>\n <ng-container *ngFor=\"let col of columns\">\n <th [style.width]=\"col.width || null\">\n {{ col.title }}\n </th>\n </ng-container>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <ng-container *ngFor=\"let col of columns\">\n <td class=\"align-middle\">\n <ng-container *ngIf=\"!col.actions\" [ngSwitch]=\"columnTypes[col.prop]\">\n <ng-container *ngSwitchCase=\"'date'\">{{ row[col.prop] | date:'dd/MM/yyyy' }}</ng-container>\n <ng-container *ngSwitchCase=\"'time'\">{{ row[col.prop] | date:'HH:mm' }}</ng-container>\n <ng-container *ngSwitchCase=\"'tag'\">\n <span class=\"badge rounded-pill\" [ngClass]=\"row[col.prop + 'Class'] || 'text-bg-dark'\">{{\n row[col.prop] }}</span>\n </ng-container>\n <ng-container *ngSwitchDefault>{{ row[col.prop] }}</ng-container>\n </ng-container>\n <ng-container *ngIf=\"col.actions\">\n <div class=\"dropdown\">\n <button class=\"btn btn-light btn-sm\" type=\"button\" data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\">\n <i class=\"material-icons align-middle\">more_horiz</i>\n </button>\n <ul class=\"dropdown-menu\">\n <li *ngFor=\"let action of col.actions\">\n <a class=\"dropdown-item\" href=\"#\"\n (click)=\"action.callback(row); $event.preventDefault()\">\n <i *ngIf=\"action.icon\" class=\"material-icons align-middle me-2\">{{ action.icon\n }}</i>\n {{ action.title }}\n </a>\n </li>\n </ul>\n </div>\n </ng-container>\n </td>\n </ng-container>\n </tr>\n </tbody>\n</table>\n\n<!-- Paginator -->\n<nav aria-label=\"Tabela pagina\u00E7\u00E3o\" class=\"d-flex justify-content-between mt-3\">\n <lib-dropdown [options]=\"pageSizeOptions\" placeholder=\"Tamanho da p\u00E1gina\" [selectedOption]=\"pageSize\"\n [hideLabel]=\"true\" (valueChange)=\"onPageSizeChange($event)\"></lib-dropdown>\n\n <ul *ngIf=\"totalPages > 1\" class=\"pagination pagination-sm\">\n <li class=\"page-item\" [class.disabled]=\"currentPage === 1\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1\">\n <i class=\"material-icons align-middle\">chevron_left</i>\n </button>\n </li>\n <ng-container *ngFor=\"let page of getPages()\">\n <li class=\"page-item\" *ngIf=\"page !== '...'\" [class.active]=\"page === currentPage\">\n <button class=\"page-link\" (click)=\"goToPage(toNumber(page))\" [disabled]=\"page === currentPage\">{{ page\n }}</button>\n </li>\n <li class=\"page-item disabled\" *ngIf=\"page === '...'\"><span class=\"page-link\">...</span></li>\n </ng-container>\n <li class=\"page-item\" [class.disabled]=\"currentPage === totalPages\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages\">\n <i class=\"material-icons align-middle\">chevron_right</i>\n </button>\n </li>\n </ul>\n</nav>", styles: ["thead{border-top:1px solid #c6c7c8}thead th{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem}tbody th,tbody td,tbody .dropdown-menu{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}.pagination i{font-size:20px}.pagination .page-item .page-link{color:#414141;background-color:#fff}.pagination .page-item.active .page-link{color:#fff;background-color:#414141;border-color:#414141}\n"] }]
|
|
546
|
+
}], propDecorators: { columns: [{
|
|
547
|
+
type: Input
|
|
548
|
+
}], data: [{
|
|
549
|
+
type: Input
|
|
550
|
+
}], columnTypes: [{
|
|
551
|
+
type: Input
|
|
552
|
+
}], page: [{
|
|
553
|
+
type: Input
|
|
554
|
+
}], pageSize: [{
|
|
555
|
+
type: Input
|
|
556
|
+
}], totalItems: [{
|
|
557
|
+
type: Input
|
|
558
|
+
}], pageSizeOptions: [{
|
|
559
|
+
type: Input
|
|
560
|
+
}], pageChange: [{
|
|
561
|
+
type: Output
|
|
562
|
+
}], pageSizeChange: [{
|
|
563
|
+
type: Output
|
|
564
|
+
}] } });
|
|
565
|
+
|
|
566
|
+
class SidebarComponent {
|
|
567
|
+
destroy$ = new Subject();
|
|
568
|
+
router = inject(Router);
|
|
569
|
+
// Configuração e dados
|
|
570
|
+
config = {};
|
|
571
|
+
menuItems = [];
|
|
572
|
+
isCollapsed = false;
|
|
573
|
+
// Eventos
|
|
574
|
+
itemClick = new EventEmitter();
|
|
575
|
+
toggle = new EventEmitter();
|
|
576
|
+
overlayToggle = new EventEmitter();
|
|
577
|
+
logoClick = new EventEmitter();
|
|
578
|
+
// Estado interno
|
|
579
|
+
state$ = new BehaviorSubject({
|
|
580
|
+
isCollapsed: false,
|
|
581
|
+
isMobile: false,
|
|
582
|
+
isOverlayOpen: false,
|
|
583
|
+
activeItemId: undefined
|
|
584
|
+
});
|
|
585
|
+
// Configuração padrão
|
|
586
|
+
defaultConfig = {
|
|
587
|
+
width: '280px',
|
|
588
|
+
collapsedWidth: '64px',
|
|
589
|
+
position: 'left',
|
|
590
|
+
collapsible: true,
|
|
591
|
+
autoCollapse: true,
|
|
592
|
+
overlay: true,
|
|
593
|
+
logo: {
|
|
594
|
+
src: '',
|
|
595
|
+
alt: 'Logo',
|
|
596
|
+
route: '/'
|
|
597
|
+
},
|
|
598
|
+
theme: 'light',
|
|
599
|
+
customCssClass: ''
|
|
600
|
+
};
|
|
601
|
+
// Getters para o estado
|
|
602
|
+
get currentState() {
|
|
603
|
+
return this.state$.value;
|
|
604
|
+
}
|
|
605
|
+
get finalConfig() {
|
|
606
|
+
return { ...this.defaultConfig, ...this.config };
|
|
607
|
+
}
|
|
608
|
+
get sidebarWidth() {
|
|
609
|
+
return this.currentState.isCollapsed ? this.finalConfig.collapsedWidth : this.finalConfig.width;
|
|
610
|
+
}
|
|
611
|
+
ngOnInit() {
|
|
612
|
+
this.initializeState();
|
|
613
|
+
this.detectActiveRoute();
|
|
614
|
+
}
|
|
615
|
+
ngOnDestroy() {
|
|
616
|
+
this.destroy$.next();
|
|
617
|
+
this.destroy$.complete();
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Detecta mudanças de tela
|
|
621
|
+
*/
|
|
622
|
+
onResize(event) {
|
|
623
|
+
this.checkMobileState();
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Inicializa o estado do sidebar
|
|
627
|
+
*/
|
|
628
|
+
initializeState() {
|
|
629
|
+
this.checkMobileState();
|
|
630
|
+
// Configura estado inicial baseado no input
|
|
631
|
+
this.updateState({
|
|
632
|
+
isCollapsed: this.isCollapsed
|
|
633
|
+
});
|
|
634
|
+
// Auto-collapse em mobile se configurado
|
|
635
|
+
if (this.finalConfig.autoCollapse && this.currentState.isMobile) {
|
|
636
|
+
this.updateState({ isCollapsed: true });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Verifica se está em modo mobile
|
|
641
|
+
*/
|
|
642
|
+
checkMobileState() {
|
|
643
|
+
const isMobile = window.innerWidth < 768; // Breakpoint md
|
|
644
|
+
this.updateState({ isMobile });
|
|
645
|
+
if (isMobile && this.finalConfig.autoCollapse) {
|
|
646
|
+
this.updateState({ isCollapsed: true });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Detecta rota ativa
|
|
651
|
+
*/
|
|
652
|
+
detectActiveRoute() {
|
|
653
|
+
const currentUrl = this.router.url;
|
|
654
|
+
const activeItem = this.findActiveItem(this.menuItems, currentUrl);
|
|
655
|
+
if (activeItem) {
|
|
656
|
+
this.updateState({ activeItemId: activeItem.id });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Encontra item ativo baseado na URL
|
|
661
|
+
*/
|
|
662
|
+
findActiveItem(items, url) {
|
|
663
|
+
for (const item of items) {
|
|
664
|
+
if (item.route && url.startsWith(item.route)) {
|
|
665
|
+
return item;
|
|
666
|
+
}
|
|
667
|
+
if (item.children) {
|
|
668
|
+
const childItem = this.findActiveItem(item.children, url);
|
|
669
|
+
if (childItem)
|
|
670
|
+
return childItem;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Atualiza o estado
|
|
677
|
+
*/
|
|
678
|
+
updateState(newState) {
|
|
679
|
+
const current = this.currentState;
|
|
680
|
+
this.state$.next({ ...current, ...newState });
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Toggle do sidebar
|
|
684
|
+
*/
|
|
685
|
+
toggleSidebar() {
|
|
686
|
+
const newCollapsedState = !this.currentState.isCollapsed;
|
|
687
|
+
this.updateState({ isCollapsed: newCollapsedState });
|
|
688
|
+
this.toggle.emit(!newCollapsedState);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Toggle do overlay (mobile)
|
|
692
|
+
*/
|
|
693
|
+
toggleOverlay() {
|
|
694
|
+
const newOverlayState = !this.currentState.isOverlayOpen;
|
|
695
|
+
this.updateState({ isOverlayOpen: newOverlayState });
|
|
696
|
+
this.overlayToggle.emit(newOverlayState);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Clique no item do menu
|
|
700
|
+
*/
|
|
701
|
+
onItemClick(item, event) {
|
|
702
|
+
if (item.disabled) {
|
|
703
|
+
event.preventDefault();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
// Se tem filhos, toggle expansão
|
|
707
|
+
if (item.children && item.children.length > 0) {
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
this.toggleItemExpansion(item);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
// Ação personalizada
|
|
713
|
+
if (item.action) {
|
|
714
|
+
event.preventDefault();
|
|
715
|
+
item.action();
|
|
716
|
+
}
|
|
717
|
+
// Navegação
|
|
718
|
+
if (item.route) {
|
|
719
|
+
this.updateState({ activeItemId: item.id });
|
|
720
|
+
this.router.navigate([item.route]);
|
|
721
|
+
// Fecha overlay em mobile após navegação
|
|
722
|
+
if (this.currentState.isMobile && this.currentState.isOverlayOpen) {
|
|
723
|
+
this.updateState({ isOverlayOpen: false });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Emite evento
|
|
727
|
+
this.itemClick.emit(item);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Toggle expansão de item com filhos
|
|
731
|
+
*/
|
|
732
|
+
toggleItemExpansion(item) {
|
|
733
|
+
item.isExpanded = !item.isExpanded;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Clique no logo
|
|
737
|
+
*/
|
|
738
|
+
onLogoClick() {
|
|
739
|
+
if (this.finalConfig.logo.route) {
|
|
740
|
+
this.router.navigate([this.finalConfig.logo.route]);
|
|
741
|
+
}
|
|
742
|
+
this.logoClick.emit();
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Fecha overlay ao clicar fora (mobile)
|
|
746
|
+
*/
|
|
747
|
+
closeOverlay() {
|
|
748
|
+
if (this.currentState.isMobile && this.currentState.isOverlayOpen) {
|
|
749
|
+
this.updateState({ isOverlayOpen: false });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Verifica se item está ativo
|
|
754
|
+
*/
|
|
755
|
+
isItemActive(item) {
|
|
756
|
+
return this.currentState.activeItemId === item.id;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Verifica se item tem filhos ativos
|
|
760
|
+
*/
|
|
761
|
+
hasActiveChild(item) {
|
|
762
|
+
if (!item.children)
|
|
763
|
+
return false;
|
|
764
|
+
return item.children.some(child => this.isItemActive(child) || this.hasActiveChild(child));
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* TrackBy function para performance
|
|
768
|
+
*/
|
|
769
|
+
trackByItemId(index, item) {
|
|
770
|
+
return item.id;
|
|
771
|
+
}
|
|
772
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
773
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: SidebarComponent, isStandalone: true, selector: "lib-sidebar", inputs: { config: "config", menuItems: "menuItems", isCollapsed: "isCollapsed" }, outputs: { itemClick: "itemClick", toggle: "toggle", overlayToggle: "overlayToggle", logoClick: "logoClick" }, host: { listeners: { "window:resize": "onResize($event)" } }, ngImport: i0, template: "<!-- Overlay para mobile -->\n<div *ngIf=\"currentState.isMobile && currentState.isOverlayOpen\" class=\"sidebar-overlay\" (click)=\"closeOverlay()\">\n</div>\n\n<!-- Sidebar principal -->\n<aside class=\"sidebar\" [class.collapsed]=\"currentState.isCollapsed\" [class.mobile]=\"currentState.isMobile\"\n [class.overlay-open]=\"currentState.isOverlayOpen\" [class.theme-light]=\"finalConfig.theme === 'light'\"\n [class.theme-dark]=\"finalConfig.theme === 'dark'\" [class.position-left]=\"finalConfig.position === 'left'\"\n [class.position-right]=\"finalConfig.position === 'right'\" [ngClass]=\"finalConfig.customCssClass\"\n [style.width]=\"sidebarWidth\">\n\n <!-- Header com logo -->\n <div class=\"sidebar-header\" *ngIf=\"finalConfig.logo.src\">\n <button class=\"logo-container\" (click)=\"onLogoClick()\" [attr.aria-label]=\"finalConfig.logo.alt\">\n <img [src]=\"finalConfig.logo.src\" [alt]=\"finalConfig.logo.alt\" class=\"logo-image\">\n\n <span class=\"logo-text\" *ngIf=\"!currentState.isCollapsed\">\n {{ finalConfig.logo.alt }}\n </span>\n </button>\n\n <!-- Bot\u00E3o de toggle (se habilitado) -->\n <button *ngIf=\"finalConfig.collapsible && !currentState.isMobile\" class=\"toggle-btn\" (click)=\"toggleSidebar()\"\n [attr.aria-label]=\"currentState.isCollapsed ? 'Expandir menu' : 'Recolher menu'\">\n <i class=\"toggle-icon fa\" [class.fa-angle-left]=\"!currentState.isCollapsed\"\n [class.fa-angle-right]=\"currentState.isCollapsed\">\n </i>\n </button>\n </div>\n\n <!-- Navega\u00E7\u00E3o principal -->\n <nav class=\"sidebar-nav\">\n <ul class=\"nav-list\">\n <ng-container *ngFor=\"let item of menuItems; trackBy: trackByItemId\">\n\n <!-- Divisor -->\n <li *ngIf=\"item.divider\" class=\"nav-divider\" role=\"separator\"></li>\n\n <!-- Item do menu -->\n <li class=\"nav-item\" [class.active]=\"isItemActive(item)\"\n [class.has-children]=\"item.children && item.children.length > 0\" [class.expanded]=\"item.isExpanded\"\n [class.disabled]=\"item.disabled\" [class.has-active-child]=\"hasActiveChild(item)\">\n\n <a class=\"nav-link\" [routerLink]=\"item.route\" [class.no-route]=\"!item.route\"\n (click)=\"onItemClick(item, $event)\" [attr.aria-label]=\"item.label\"\n [attr.aria-expanded]=\"item.children ? item.isExpanded : null\" [attr.tabindex]=\"item.disabled ? -1 : 0\">\n\n <!-- \u00CDcone -->\n <i class=\"nav-icon\" [ngClass]=\"item.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label -->\n <span class=\"nav-label\" *ngIf=\"!currentState.isCollapsed\">\n {{ item.label }}\n </span>\n\n <!-- Badge -->\n <span class=\"nav-badge\" *ngIf=\"item.badge && !currentState.isCollapsed\">\n {{ item.badge }}\n </span>\n\n <!-- Seta para submenu -->\n <i class=\"nav-arrow fa\" *ngIf=\"item.children && item.children.length > 0 && !currentState.isCollapsed\"\n [class.fa-chevron-down]=\"item.isExpanded\" [class.fa-chevron-right]=\"!item.isExpanded\"\n [attr.aria-hidden]=\"true\">\n </i>\n </a>\n\n <!-- Submenu -->\n <ul class=\"nav-submenu\"\n *ngIf=\"item.children && item.children.length > 0 && item.isExpanded && !currentState.isCollapsed\">\n <li class=\"nav-subitem\" *ngFor=\"let subItem of item.children; trackBy: trackByItemId\"\n [class.active]=\"isItemActive(subItem)\" [class.disabled]=\"subItem.disabled\">\n\n <a class=\"nav-sublink\" [routerLink]=\"subItem.route\" [class.no-route]=\"!subItem.route\"\n (click)=\"onItemClick(subItem, $event)\" [attr.aria-label]=\"subItem.label\"\n [attr.tabindex]=\"subItem.disabled ? -1 : 0\">\n\n <!-- \u00CDcone do subitem -->\n <i class=\"nav-subicon\" [ngClass]=\"subItem.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label do subitem -->\n <span class=\"nav-sublabel\">\n {{ subItem.label }}\n </span>\n\n <!-- Badge do subitem -->\n <span class=\"nav-subbadge\" *ngIf=\"subItem.badge\">\n {{ subItem.badge }}\n </span>\n </a>\n </li>\n </ul>\n </li>\n </ng-container>\n </ul>\n </nav>\n\n <!-- Footer opcional -->\n <div class=\"sidebar-footer\" *ngIf=\"!currentState.isCollapsed\">\n <ng-content select=\"[slot=footer]\"></ng-content>\n </div>\n</aside>\n\n<!-- Bot\u00E3o mobile para abrir sidebar -->\n<button *ngIf=\"currentState.isMobile && !currentState.isOverlayOpen\" class=\"mobile-toggle-btn\" (click)=\"toggleOverlay()\"\n [attr.aria-label]=\"'Abrir menu'\">\n <i class=\"fa fa-bars\" [attr.aria-hidden]=\"true\"></i>\n</button>", styles: [":host{--sidebar-bg: #ffffff;--sidebar-text: #343a40;--sidebar-text-secondary: #495057;--sidebar-border: #dee2e6;--sidebar-hover: #f8f9fa;--sidebar-active: #007bff;--sidebar-active-bg: rgba(0, 123, 255, .1);--sidebar-transition: .3s cubic-bezier(.4, 0, .2, 1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .08)}:host.theme-dark{--sidebar-bg: #343a40;--sidebar-text: #ffffff;--sidebar-text-secondary: #b2b9be;--sidebar-border: #495057;--sidebar-hover: rgba(255, 255, 255, .1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .25)}.sidebar-overlay{position:fixed;inset:0;background:#00000080;z-index:998;backdrop-filter:blur(2px)}.sidebar{position:fixed;top:0;bottom:0;left:0;width:280px;background:var(--sidebar-bg);border-right:1px solid var(--sidebar-border);box-shadow:var(--sidebar-shadow);transition:var(--sidebar-transition);z-index:999;display:flex;flex-direction:column;overflow:visible}.sidebar.position-right{left:auto;right:0;border-left:1px solid var(--sidebar-border);border-right:none}.sidebar.collapsed{width:64px}.sidebar.collapsed .sidebar-header .logo-text,.sidebar.collapsed .nav-label,.sidebar.collapsed .nav-badge,.sidebar.collapsed .nav-arrow,.sidebar.collapsed .nav-submenu{opacity:0;pointer-events:none}.sidebar.collapsed .nav-link{justify-content:center;padding-left:.5rem;padding-right:.5rem;gap:0}.sidebar.collapsed .nav-icon{margin:0 auto}.sidebar.collapsed .sidebar-header{padding:1rem;justify-content:center!important}.sidebar.collapsed .logo-container{width:100%;display:flex;justify-content:center;align-items:center;gap:0;padding:0;margin:0}.sidebar.collapsed .logo-image{margin:0 auto;display:block;width:32px;height:32px}.sidebar.collapsed .toggle-btn{right:-18px;width:36px;height:36px}.sidebar.mobile{transform:translate(-100%)}.sidebar.mobile.position-right{transform:translate(100%)}.sidebar.mobile.overlay-open{transform:translate(0)}.sidebar-header{padding:1.5rem;border-bottom:1px solid var(--sidebar-border);display:flex;align-items:center;justify-content:space-between;min-height:80px;position:relative}.logo-container{display:flex;align-items:center;gap:1rem;background:none;border:none;padding:0;cursor:pointer;color:inherit;text-decoration:none;transition:var(--sidebar-transition)}.logo-container:hover{opacity:.8}.logo-image{width:32px;height:32px;object-fit:contain;flex-shrink:0}.logo-text{font-family:DM Sans,sans-serif;font-size:1.125rem;font-weight:600;color:var(--sidebar-text);transition:var(--sidebar-transition);white-space:nowrap}.toggle-btn{background:none;border:none;width:32px;height:32px;border-radius:.5rem;display:flex;align-items:center;background:var(--sidebar-bg);border:1px solid rgba(0,0,0,.06);box-shadow:0 6px 14px #0614231f;backdrop-filter:blur(4px);transition:transform .18s var(--sidebar-transition),box-shadow .18s var(--sidebar-transition);position:absolute;top:50%;transform:translateY(-50%);right:-16px;z-index:1000}.toggle-btn:hover{transform:translateY(-50%) translateY(-2px);box-shadow:0 10px 20px #06142329}.toggle-btn:hover{background:var(--sidebar-hover);color:var(--sidebar-text)}.toggle-btn .toggle-icon{font-size:1rem}.sidebar-nav{flex:1;overflow-y:auto;overflow-x:hidden;padding:1rem 0}.sidebar-nav::-webkit-scrollbar{width:4px}.sidebar-nav::-webkit-scrollbar-track{background:transparent}.sidebar-nav::-webkit-scrollbar-thumb{background:var(--sidebar-border);border-radius:2px}.sidebar-nav::-webkit-scrollbar-thumb:hover{background:var(--sidebar-text-secondary)}.nav-list{list-style:none;margin:0;padding:0}.nav-divider{height:1px;background:var(--sidebar-border);margin:1rem 0}.nav-item{position:relative;margin:0 1rem .25rem}.nav-item.active>.nav-link{background:var(--sidebar-active-bg);color:var(--sidebar-active)}.nav-item.active>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.disabled{opacity:.5;pointer-events:none}.nav-link{display:flex;align-items:center;gap:1rem;padding:1rem;border-radius:.5rem;color:var(--sidebar-text);text-decoration:none;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:500;line-height:1.5;transition:var(--sidebar-transition);cursor:pointer;position:relative}.nav-link:hover:not(.active){background:var(--sidebar-hover)}.nav-link.no-route{cursor:pointer}.nav-icon{font-size:1.125rem;color:var(--sidebar-text-secondary);flex-shrink:0;width:20px;text-align:center;transition:var(--sidebar-transition)}.nav-label{flex:1;white-space:nowrap;transition:var(--sidebar-transition)}.nav-badge{background:var(--sidebar-active);color:#fff;font-size:.75rem;font-weight:600;padding:2px 6px;border-radius:10px;min-width:18px;text-align:center;transition:var(--sidebar-transition)}.nav-arrow{font-size:.875rem;color:var(--sidebar-text-secondary);transition:var(--sidebar-transition);margin-left:auto}.nav-submenu{list-style:none;margin:.25rem 0 0;padding:0;border-left:2px solid var(--sidebar-border);margin-left:calc(20px + 1rem);transition:var(--sidebar-transition)}.nav-subitem{margin:0 0 .25rem}.nav-subitem.active>.nav-sublink{color:var(--sidebar-active);background:var(--sidebar-active-bg)}.nav-subitem.active>.nav-sublink .nav-subicon{color:var(--sidebar-active)}.nav-subitem.disabled{opacity:.5;pointer-events:none}.nav-subicon{font-size:.875rem;width:16px;text-align:center;flex-shrink:0;transition:var(--sidebar-transition)}.nav-sublabel{flex:1;white-space:nowrap}.nav-subbadge{background:var(--sidebar-active);color:#fff;font-size:10px;font-weight:600;padding:1px 4px;border-radius:8px;min-width:14px;text-align:center}.sidebar-footer{padding:1rem;border-top:1px solid var(--sidebar-border);margin-top:auto}.mobile-toggle-btn{position:fixed;top:1.5rem;left:1.5rem;width:48px;height:48px;background:var(--sidebar-active);color:#fff;border:none;border-radius:.75rem;display:flex;align-items:center;justify-content:center;font-size:1.125rem;box-shadow:var(--sidebar-shadow);z-index:997;cursor:pointer;transition:var(--sidebar-transition)}.mobile-toggle-btn:hover{transform:scale(1.05)}.mobile-toggle-btn:active{transform:scale(.95)}@media (max-width: 768px){.sidebar{width:280px!important}.sidebar.position-right{transform:translate(100%)}.sidebar.position-right.overlay-open{transform:translate(0)}}@keyframes slideInLeft{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.sidebar.mobile.overlay-open{animation:slideInLeft .3s ease-out}.sidebar.mobile.overlay-open.position-right{animation:slideInRight .3s ease-out}.nav-link:focus,.nav-sublink:focus,.toggle-btn:focus,.logo-container:focus,.mobile-toggle-btn:focus{outline:2px solid var(--sidebar-active);outline-offset:2px}.collapsed .nav-submenu{display:none!important}.has-children.collapsed .nav-arrow{display:none}\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: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] });
|
|
774
|
+
}
|
|
775
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SidebarComponent, decorators: [{
|
|
776
|
+
type: Component,
|
|
777
|
+
args: [{ selector: 'lib-sidebar', imports: [CommonModule, RouterModule], template: "<!-- Overlay para mobile -->\n<div *ngIf=\"currentState.isMobile && currentState.isOverlayOpen\" class=\"sidebar-overlay\" (click)=\"closeOverlay()\">\n</div>\n\n<!-- Sidebar principal -->\n<aside class=\"sidebar\" [class.collapsed]=\"currentState.isCollapsed\" [class.mobile]=\"currentState.isMobile\"\n [class.overlay-open]=\"currentState.isOverlayOpen\" [class.theme-light]=\"finalConfig.theme === 'light'\"\n [class.theme-dark]=\"finalConfig.theme === 'dark'\" [class.position-left]=\"finalConfig.position === 'left'\"\n [class.position-right]=\"finalConfig.position === 'right'\" [ngClass]=\"finalConfig.customCssClass\"\n [style.width]=\"sidebarWidth\">\n\n <!-- Header com logo -->\n <div class=\"sidebar-header\" *ngIf=\"finalConfig.logo.src\">\n <button class=\"logo-container\" (click)=\"onLogoClick()\" [attr.aria-label]=\"finalConfig.logo.alt\">\n <img [src]=\"finalConfig.logo.src\" [alt]=\"finalConfig.logo.alt\" class=\"logo-image\">\n\n <span class=\"logo-text\" *ngIf=\"!currentState.isCollapsed\">\n {{ finalConfig.logo.alt }}\n </span>\n </button>\n\n <!-- Bot\u00E3o de toggle (se habilitado) -->\n <button *ngIf=\"finalConfig.collapsible && !currentState.isMobile\" class=\"toggle-btn\" (click)=\"toggleSidebar()\"\n [attr.aria-label]=\"currentState.isCollapsed ? 'Expandir menu' : 'Recolher menu'\">\n <i class=\"toggle-icon fa\" [class.fa-angle-left]=\"!currentState.isCollapsed\"\n [class.fa-angle-right]=\"currentState.isCollapsed\">\n </i>\n </button>\n </div>\n\n <!-- Navega\u00E7\u00E3o principal -->\n <nav class=\"sidebar-nav\">\n <ul class=\"nav-list\">\n <ng-container *ngFor=\"let item of menuItems; trackBy: trackByItemId\">\n\n <!-- Divisor -->\n <li *ngIf=\"item.divider\" class=\"nav-divider\" role=\"separator\"></li>\n\n <!-- Item do menu -->\n <li class=\"nav-item\" [class.active]=\"isItemActive(item)\"\n [class.has-children]=\"item.children && item.children.length > 0\" [class.expanded]=\"item.isExpanded\"\n [class.disabled]=\"item.disabled\" [class.has-active-child]=\"hasActiveChild(item)\">\n\n <a class=\"nav-link\" [routerLink]=\"item.route\" [class.no-route]=\"!item.route\"\n (click)=\"onItemClick(item, $event)\" [attr.aria-label]=\"item.label\"\n [attr.aria-expanded]=\"item.children ? item.isExpanded : null\" [attr.tabindex]=\"item.disabled ? -1 : 0\">\n\n <!-- \u00CDcone -->\n <i class=\"nav-icon\" [ngClass]=\"item.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label -->\n <span class=\"nav-label\" *ngIf=\"!currentState.isCollapsed\">\n {{ item.label }}\n </span>\n\n <!-- Badge -->\n <span class=\"nav-badge\" *ngIf=\"item.badge && !currentState.isCollapsed\">\n {{ item.badge }}\n </span>\n\n <!-- Seta para submenu -->\n <i class=\"nav-arrow fa\" *ngIf=\"item.children && item.children.length > 0 && !currentState.isCollapsed\"\n [class.fa-chevron-down]=\"item.isExpanded\" [class.fa-chevron-right]=\"!item.isExpanded\"\n [attr.aria-hidden]=\"true\">\n </i>\n </a>\n\n <!-- Submenu -->\n <ul class=\"nav-submenu\"\n *ngIf=\"item.children && item.children.length > 0 && item.isExpanded && !currentState.isCollapsed\">\n <li class=\"nav-subitem\" *ngFor=\"let subItem of item.children; trackBy: trackByItemId\"\n [class.active]=\"isItemActive(subItem)\" [class.disabled]=\"subItem.disabled\">\n\n <a class=\"nav-sublink\" [routerLink]=\"subItem.route\" [class.no-route]=\"!subItem.route\"\n (click)=\"onItemClick(subItem, $event)\" [attr.aria-label]=\"subItem.label\"\n [attr.tabindex]=\"subItem.disabled ? -1 : 0\">\n\n <!-- \u00CDcone do subitem -->\n <i class=\"nav-subicon\" [ngClass]=\"subItem.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label do subitem -->\n <span class=\"nav-sublabel\">\n {{ subItem.label }}\n </span>\n\n <!-- Badge do subitem -->\n <span class=\"nav-subbadge\" *ngIf=\"subItem.badge\">\n {{ subItem.badge }}\n </span>\n </a>\n </li>\n </ul>\n </li>\n </ng-container>\n </ul>\n </nav>\n\n <!-- Footer opcional -->\n <div class=\"sidebar-footer\" *ngIf=\"!currentState.isCollapsed\">\n <ng-content select=\"[slot=footer]\"></ng-content>\n </div>\n</aside>\n\n<!-- Bot\u00E3o mobile para abrir sidebar -->\n<button *ngIf=\"currentState.isMobile && !currentState.isOverlayOpen\" class=\"mobile-toggle-btn\" (click)=\"toggleOverlay()\"\n [attr.aria-label]=\"'Abrir menu'\">\n <i class=\"fa fa-bars\" [attr.aria-hidden]=\"true\"></i>\n</button>", styles: [":host{--sidebar-bg: #ffffff;--sidebar-text: #343a40;--sidebar-text-secondary: #495057;--sidebar-border: #dee2e6;--sidebar-hover: #f8f9fa;--sidebar-active: #007bff;--sidebar-active-bg: rgba(0, 123, 255, .1);--sidebar-transition: .3s cubic-bezier(.4, 0, .2, 1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .08)}:host.theme-dark{--sidebar-bg: #343a40;--sidebar-text: #ffffff;--sidebar-text-secondary: #b2b9be;--sidebar-border: #495057;--sidebar-hover: rgba(255, 255, 255, .1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .25)}.sidebar-overlay{position:fixed;inset:0;background:#00000080;z-index:998;backdrop-filter:blur(2px)}.sidebar{position:fixed;top:0;bottom:0;left:0;width:280px;background:var(--sidebar-bg);border-right:1px solid var(--sidebar-border);box-shadow:var(--sidebar-shadow);transition:var(--sidebar-transition);z-index:999;display:flex;flex-direction:column;overflow:visible}.sidebar.position-right{left:auto;right:0;border-left:1px solid var(--sidebar-border);border-right:none}.sidebar.collapsed{width:64px}.sidebar.collapsed .sidebar-header .logo-text,.sidebar.collapsed .nav-label,.sidebar.collapsed .nav-badge,.sidebar.collapsed .nav-arrow,.sidebar.collapsed .nav-submenu{opacity:0;pointer-events:none}.sidebar.collapsed .nav-link{justify-content:center;padding-left:.5rem;padding-right:.5rem;gap:0}.sidebar.collapsed .nav-icon{margin:0 auto}.sidebar.collapsed .sidebar-header{padding:1rem;justify-content:center!important}.sidebar.collapsed .logo-container{width:100%;display:flex;justify-content:center;align-items:center;gap:0;padding:0;margin:0}.sidebar.collapsed .logo-image{margin:0 auto;display:block;width:32px;height:32px}.sidebar.collapsed .toggle-btn{right:-18px;width:36px;height:36px}.sidebar.mobile{transform:translate(-100%)}.sidebar.mobile.position-right{transform:translate(100%)}.sidebar.mobile.overlay-open{transform:translate(0)}.sidebar-header{padding:1.5rem;border-bottom:1px solid var(--sidebar-border);display:flex;align-items:center;justify-content:space-between;min-height:80px;position:relative}.logo-container{display:flex;align-items:center;gap:1rem;background:none;border:none;padding:0;cursor:pointer;color:inherit;text-decoration:none;transition:var(--sidebar-transition)}.logo-container:hover{opacity:.8}.logo-image{width:32px;height:32px;object-fit:contain;flex-shrink:0}.logo-text{font-family:DM Sans,sans-serif;font-size:1.125rem;font-weight:600;color:var(--sidebar-text);transition:var(--sidebar-transition);white-space:nowrap}.toggle-btn{background:none;border:none;width:32px;height:32px;border-radius:.5rem;display:flex;align-items:center;background:var(--sidebar-bg);border:1px solid rgba(0,0,0,.06);box-shadow:0 6px 14px #0614231f;backdrop-filter:blur(4px);transition:transform .18s var(--sidebar-transition),box-shadow .18s var(--sidebar-transition);position:absolute;top:50%;transform:translateY(-50%);right:-16px;z-index:1000}.toggle-btn:hover{transform:translateY(-50%) translateY(-2px);box-shadow:0 10px 20px #06142329}.toggle-btn:hover{background:var(--sidebar-hover);color:var(--sidebar-text)}.toggle-btn .toggle-icon{font-size:1rem}.sidebar-nav{flex:1;overflow-y:auto;overflow-x:hidden;padding:1rem 0}.sidebar-nav::-webkit-scrollbar{width:4px}.sidebar-nav::-webkit-scrollbar-track{background:transparent}.sidebar-nav::-webkit-scrollbar-thumb{background:var(--sidebar-border);border-radius:2px}.sidebar-nav::-webkit-scrollbar-thumb:hover{background:var(--sidebar-text-secondary)}.nav-list{list-style:none;margin:0;padding:0}.nav-divider{height:1px;background:var(--sidebar-border);margin:1rem 0}.nav-item{position:relative;margin:0 1rem .25rem}.nav-item.active>.nav-link{background:var(--sidebar-active-bg);color:var(--sidebar-active)}.nav-item.active>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.disabled{opacity:.5;pointer-events:none}.nav-link{display:flex;align-items:center;gap:1rem;padding:1rem;border-radius:.5rem;color:var(--sidebar-text);text-decoration:none;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:500;line-height:1.5;transition:var(--sidebar-transition);cursor:pointer;position:relative}.nav-link:hover:not(.active){background:var(--sidebar-hover)}.nav-link.no-route{cursor:pointer}.nav-icon{font-size:1.125rem;color:var(--sidebar-text-secondary);flex-shrink:0;width:20px;text-align:center;transition:var(--sidebar-transition)}.nav-label{flex:1;white-space:nowrap;transition:var(--sidebar-transition)}.nav-badge{background:var(--sidebar-active);color:#fff;font-size:.75rem;font-weight:600;padding:2px 6px;border-radius:10px;min-width:18px;text-align:center;transition:var(--sidebar-transition)}.nav-arrow{font-size:.875rem;color:var(--sidebar-text-secondary);transition:var(--sidebar-transition);margin-left:auto}.nav-submenu{list-style:none;margin:.25rem 0 0;padding:0;border-left:2px solid var(--sidebar-border);margin-left:calc(20px + 1rem);transition:var(--sidebar-transition)}.nav-subitem{margin:0 0 .25rem}.nav-subitem.active>.nav-sublink{color:var(--sidebar-active);background:var(--sidebar-active-bg)}.nav-subitem.active>.nav-sublink .nav-subicon{color:var(--sidebar-active)}.nav-subitem.disabled{opacity:.5;pointer-events:none}.nav-subicon{font-size:.875rem;width:16px;text-align:center;flex-shrink:0;transition:var(--sidebar-transition)}.nav-sublabel{flex:1;white-space:nowrap}.nav-subbadge{background:var(--sidebar-active);color:#fff;font-size:10px;font-weight:600;padding:1px 4px;border-radius:8px;min-width:14px;text-align:center}.sidebar-footer{padding:1rem;border-top:1px solid var(--sidebar-border);margin-top:auto}.mobile-toggle-btn{position:fixed;top:1.5rem;left:1.5rem;width:48px;height:48px;background:var(--sidebar-active);color:#fff;border:none;border-radius:.75rem;display:flex;align-items:center;justify-content:center;font-size:1.125rem;box-shadow:var(--sidebar-shadow);z-index:997;cursor:pointer;transition:var(--sidebar-transition)}.mobile-toggle-btn:hover{transform:scale(1.05)}.mobile-toggle-btn:active{transform:scale(.95)}@media (max-width: 768px){.sidebar{width:280px!important}.sidebar.position-right{transform:translate(100%)}.sidebar.position-right.overlay-open{transform:translate(0)}}@keyframes slideInLeft{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.sidebar.mobile.overlay-open{animation:slideInLeft .3s ease-out}.sidebar.mobile.overlay-open.position-right{animation:slideInRight .3s ease-out}.nav-link:focus,.nav-sublink:focus,.toggle-btn:focus,.logo-container:focus,.mobile-toggle-btn:focus{outline:2px solid var(--sidebar-active);outline-offset:2px}.collapsed .nav-submenu{display:none!important}.has-children.collapsed .nav-arrow{display:none}\n"] }]
|
|
778
|
+
}], propDecorators: { config: [{
|
|
779
|
+
type: Input
|
|
780
|
+
}], menuItems: [{
|
|
781
|
+
type: Input
|
|
782
|
+
}], isCollapsed: [{
|
|
783
|
+
type: Input
|
|
784
|
+
}], itemClick: [{
|
|
785
|
+
type: Output
|
|
786
|
+
}], toggle: [{
|
|
787
|
+
type: Output
|
|
788
|
+
}], overlayToggle: [{
|
|
789
|
+
type: Output
|
|
790
|
+
}], logoClick: [{
|
|
791
|
+
type: Output
|
|
792
|
+
}], onResize: [{
|
|
793
|
+
type: HostListener,
|
|
794
|
+
args: ['window:resize', ['$event']]
|
|
795
|
+
}] } });
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Tipos e interfaces para autenticação
|
|
799
|
+
*/
|
|
800
|
+
/**
|
|
801
|
+
* Estados do processo de login
|
|
802
|
+
*/
|
|
803
|
+
var LoginStatus;
|
|
804
|
+
(function (LoginStatus) {
|
|
805
|
+
LoginStatus["IDLE"] = "IDLE";
|
|
806
|
+
LoginStatus["LOADING"] = "LOADING";
|
|
807
|
+
LoginStatus["SUCCESS"] = "SUCCESS";
|
|
808
|
+
LoginStatus["ERROR"] = "ERROR";
|
|
809
|
+
})(LoginStatus || (LoginStatus = {}));
|
|
810
|
+
|
|
811
|
+
class LocalStorageService {
|
|
812
|
+
storageAvailable = new BehaviorSubject(this.isStorageAvailable());
|
|
813
|
+
// Observable para monitorar disponibilidade do storage
|
|
814
|
+
get storageAvailable$() {
|
|
815
|
+
return this.storageAvailable.asObservable();
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Salva item no storage com validação básica
|
|
819
|
+
*/
|
|
820
|
+
async setItem(key, value, storageType = 'localStorage', options) {
|
|
821
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
822
|
+
console.warn(`${storageType} não está disponível`);
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
if (!this.validateKey(key)) {
|
|
826
|
+
console.error('Chave inválida fornecida');
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const storage = this.getStorage(storageType);
|
|
831
|
+
let serializedValue = this.safeJsonStringify(value);
|
|
832
|
+
if (!serializedValue) {
|
|
833
|
+
console.error('Falha na serialização segura dos dados');
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
// Aplicar criptografia REAL se solicitada
|
|
837
|
+
if (options?.encrypt) {
|
|
838
|
+
serializedValue = await this.encryptData(serializedValue);
|
|
839
|
+
if (!serializedValue) {
|
|
840
|
+
console.error('Falha na criptografia dos dados');
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Validação de tamanho (localStorage tem limite ~5MB)
|
|
845
|
+
if (serializedValue.length > 5000000) {
|
|
846
|
+
console.error('Dados muito grandes para o storage');
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
storage.setItem(this.sanitizeKey(key), serializedValue);
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
console.error(`Erro ao salvar no ${storageType}:`, error);
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Recupera item do storage com tipo seguro
|
|
859
|
+
*/
|
|
860
|
+
async getItem(key, storageType = 'localStorage', options) {
|
|
861
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
const storage = this.getStorage(storageType);
|
|
866
|
+
let item = storage.getItem(this.sanitizeKey(key));
|
|
867
|
+
if (!item)
|
|
868
|
+
return null;
|
|
869
|
+
// Aplicar descriptografia REAL se necessário
|
|
870
|
+
if (options?.encrypt && item) {
|
|
871
|
+
item = await this.decryptData(item);
|
|
872
|
+
if (!item) {
|
|
873
|
+
console.error('Falha ao descriptografar item');
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return item ? this.safeJsonParse(item) : null;
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
console.error(`Erro ao recuperar do ${storageType}:`, error);
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Remove item específico
|
|
886
|
+
*/
|
|
887
|
+
removeItem(key, storageType = 'localStorage') {
|
|
888
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
const storage = this.getStorage(storageType);
|
|
893
|
+
storage.removeItem(this.sanitizeKey(key));
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
console.error(`Erro ao remover do ${storageType}:`, error);
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Limpa todo o storage
|
|
903
|
+
*/
|
|
904
|
+
clear(storageType = 'localStorage') {
|
|
905
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const storage = this.getStorage(storageType);
|
|
910
|
+
storage.clear();
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
console.error(`Erro ao limpar ${storageType}:`, error);
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Verifica se item existe
|
|
920
|
+
*/
|
|
921
|
+
hasItem(key, storageType = 'localStorage') {
|
|
922
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
const storage = this.getStorage(storageType);
|
|
926
|
+
return storage.getItem(this.sanitizeKey(key)) !== null;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Salva item com expiração
|
|
930
|
+
*/
|
|
931
|
+
async setItemWithExpiry(key, value, expiryInMinutes, storageType = 'localStorage') {
|
|
932
|
+
const now = new Date();
|
|
933
|
+
const item = {
|
|
934
|
+
value: value,
|
|
935
|
+
expiry: now.getTime() + (expiryInMinutes * 60 * 1000)
|
|
936
|
+
};
|
|
937
|
+
return await this.setItem(key, item, storageType);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Recupera item com verificação de expiração
|
|
941
|
+
*/
|
|
942
|
+
async getItemWithExpiry(key, storageType = 'localStorage') {
|
|
943
|
+
const itemStr = await this.getItem(key, storageType);
|
|
944
|
+
if (!itemStr) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
const now = new Date();
|
|
948
|
+
if (now.getTime() > itemStr.expiry) {
|
|
949
|
+
this.removeItem(key, storageType);
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
return itemStr.value;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Obtém todas as chaves do storage
|
|
956
|
+
*/
|
|
957
|
+
getAllKeys(storageType = 'localStorage') {
|
|
958
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
959
|
+
return [];
|
|
960
|
+
}
|
|
961
|
+
const storage = this.getStorage(storageType);
|
|
962
|
+
const keys = [];
|
|
963
|
+
for (let i = 0; i < storage.length; i++) {
|
|
964
|
+
const key = storage.key(i);
|
|
965
|
+
if (key)
|
|
966
|
+
keys.push(key);
|
|
967
|
+
}
|
|
968
|
+
return keys;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Obtém tamanho ocupado do storage em bytes (aproximado)
|
|
972
|
+
*/
|
|
973
|
+
getStorageSize(storageType = 'localStorage') {
|
|
974
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
const storage = this.getStorage(storageType);
|
|
978
|
+
let total = 0;
|
|
979
|
+
for (let key in storage) {
|
|
980
|
+
if (storage.hasOwnProperty(key)) {
|
|
981
|
+
total += storage[key].length + key.length;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return total;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Monitor de eventos de storage
|
|
988
|
+
*/
|
|
989
|
+
monitorStorageEvents() {
|
|
990
|
+
return new Observable(observer => {
|
|
991
|
+
const handler = (event) => {
|
|
992
|
+
// Validação básica de segurança
|
|
993
|
+
if (this.validateStorageEvent(event)) {
|
|
994
|
+
observer.next(event);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
window.addEventListener('storage', handler);
|
|
998
|
+
return () => {
|
|
999
|
+
window.removeEventListener('storage', handler);
|
|
1000
|
+
};
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Limpa itens expirados do storage
|
|
1005
|
+
*/
|
|
1006
|
+
async cleanExpiredItems(storageType = 'localStorage') {
|
|
1007
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
1008
|
+
return 0;
|
|
1009
|
+
}
|
|
1010
|
+
const keys = this.getAllKeys(storageType);
|
|
1011
|
+
let cleanedCount = 0;
|
|
1012
|
+
for (const key of keys) {
|
|
1013
|
+
try {
|
|
1014
|
+
const item = await this.getItem(key, storageType);
|
|
1015
|
+
if (item && item.expiry) {
|
|
1016
|
+
const now = new Date().getTime();
|
|
1017
|
+
if (now > item.expiry) {
|
|
1018
|
+
this.removeItem(key, storageType);
|
|
1019
|
+
cleanedCount++;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
// Ignora erros de parsing - pode não ser um item com expiração
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return cleanedCount;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Backup de todos os dados do storage
|
|
1031
|
+
*/
|
|
1032
|
+
backup(storageType = 'localStorage') {
|
|
1033
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
1034
|
+
return {};
|
|
1035
|
+
}
|
|
1036
|
+
const storage = this.getStorage(storageType);
|
|
1037
|
+
const backup = {};
|
|
1038
|
+
for (let i = 0; i < storage.length; i++) {
|
|
1039
|
+
const key = storage.key(i);
|
|
1040
|
+
if (key) {
|
|
1041
|
+
backup[key] = storage.getItem(key);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return backup;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Restaura backup para o storage
|
|
1048
|
+
*/
|
|
1049
|
+
restore(backup, storageType = 'localStorage') {
|
|
1050
|
+
if (!this.isStorageAvailable(storageType)) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
// Validação rigorosa do backup
|
|
1054
|
+
if (!backup || typeof backup !== 'object' || Array.isArray(backup)) {
|
|
1055
|
+
console.error('Backup inválido fornecido');
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
const storage = this.getStorage(storageType);
|
|
1060
|
+
const keys = Object.keys(backup);
|
|
1061
|
+
// Limita número de itens para evitar DoS
|
|
1062
|
+
if (keys.length > 100) {
|
|
1063
|
+
console.error('Backup muito grande - máximo 100 itens');
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
for (const key of keys) {
|
|
1067
|
+
// Validação rigorosa de cada chave
|
|
1068
|
+
if (!this.validateKey(key)) {
|
|
1069
|
+
console.warn(`Chave inválida ignorada no backup: ${key}`);
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const value = backup[key];
|
|
1073
|
+
// Validação do valor
|
|
1074
|
+
if (typeof value !== 'string') {
|
|
1075
|
+
console.warn(`Valor inválido ignorado para chave: ${key}`);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
// Validação de tamanho
|
|
1079
|
+
if (value.length > 50000) { // 50KB por item
|
|
1080
|
+
console.warn(`Valor muito grande ignorado para chave: ${key}`);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
storage.setItem(key, value);
|
|
1084
|
+
}
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
catch (error) {
|
|
1088
|
+
console.error('Erro ao restaurar backup:', error);
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* MÉTODOS PRIVADOS SIMPLIFICADOS
|
|
1094
|
+
*/
|
|
1095
|
+
isStorageAvailable(storageType = 'localStorage') {
|
|
1096
|
+
try {
|
|
1097
|
+
const storage = this.getStorage(storageType);
|
|
1098
|
+
const test = '__storage_test__';
|
|
1099
|
+
storage.setItem(test, test);
|
|
1100
|
+
storage.removeItem(test);
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
getStorage(storageType) {
|
|
1108
|
+
return storageType === 'localStorage' ? localStorage : sessionStorage;
|
|
1109
|
+
}
|
|
1110
|
+
sanitizeKey(key) {
|
|
1111
|
+
if (!key || typeof key !== 'string') {
|
|
1112
|
+
return '';
|
|
1113
|
+
}
|
|
1114
|
+
return key
|
|
1115
|
+
.replace(/[^a-zA-Z0-9_.-]/g, '') // Remove caracteres especiais
|
|
1116
|
+
.substring(0, 50); // Limita tamanho
|
|
1117
|
+
}
|
|
1118
|
+
validateKey(key) {
|
|
1119
|
+
if (!key || typeof key !== 'string') {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
// Bloqueia chaves perigosas para prototype pollution
|
|
1123
|
+
const dangerousKeys = ['__proto__', 'constructor', 'prototype', 'toString', 'valueOf'];
|
|
1124
|
+
if (dangerousKeys.includes(key)) {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
// Validação de tamanho e caracteres
|
|
1128
|
+
if (key.length > 30 || key.length === 0) {
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
// Apenas alfanumérico e underscore
|
|
1132
|
+
if (!/^[a-zA-Z0-9_]+$/.test(key)) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
safeJsonStringify(obj) {
|
|
1138
|
+
try {
|
|
1139
|
+
// Verifica se não é função ou undefined
|
|
1140
|
+
if (typeof obj === 'function' || obj === undefined) {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
// Limita profundidade para evitar referências circulares
|
|
1144
|
+
const seen = new WeakSet();
|
|
1145
|
+
const replacer = (key, value) => {
|
|
1146
|
+
// Bloqueia chaves perigosas
|
|
1147
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
1148
|
+
return undefined;
|
|
1149
|
+
}
|
|
1150
|
+
// Evita referências circulares
|
|
1151
|
+
if (typeof value === 'object' && value !== null) {
|
|
1152
|
+
if (seen.has(value)) {
|
|
1153
|
+
return '[Circular Reference]';
|
|
1154
|
+
}
|
|
1155
|
+
seen.add(value);
|
|
1156
|
+
}
|
|
1157
|
+
return value;
|
|
1158
|
+
};
|
|
1159
|
+
return JSON.stringify(obj, replacer);
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
console.error('Erro na serialização segura:', error);
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
safeJsonParse(text) {
|
|
1167
|
+
try {
|
|
1168
|
+
return JSON.parse(text, (key, value) => {
|
|
1169
|
+
// Bloqueia prototype pollution
|
|
1170
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
return value;
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
console.error('Erro no parsing seguro:', error);
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async encryptData(text) {
|
|
1182
|
+
try {
|
|
1183
|
+
// Gera chave simples baseada no contexto da aplicação
|
|
1184
|
+
const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(window.location.hostname + '_secure_key'), 'PBKDF2', false, ['deriveKey']);
|
|
1185
|
+
// Gera salt fixo baseado no hostname (para poder descriptografar)
|
|
1186
|
+
const salt = new TextEncoder().encode(window.location.hostname.padEnd(16, '0').substring(0, 16));
|
|
1187
|
+
const key = await crypto.subtle.deriveKey({
|
|
1188
|
+
name: 'PBKDF2',
|
|
1189
|
+
salt: salt,
|
|
1190
|
+
iterations: 10000,
|
|
1191
|
+
hash: 'SHA-256'
|
|
1192
|
+
}, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
|
|
1193
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1194
|
+
const encoded = new TextEncoder().encode(text);
|
|
1195
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, encoded);
|
|
1196
|
+
// Combina IV + dados criptografados em base64
|
|
1197
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
1198
|
+
combined.set(iv);
|
|
1199
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
1200
|
+
return btoa(String.fromCharCode(...combined));
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
console.error('Erro na criptografia:', error);
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async decryptData(encryptedText) {
|
|
1208
|
+
try {
|
|
1209
|
+
// Mesmo processo para gerar a chave
|
|
1210
|
+
const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(window.location.hostname + '_secure_key'), 'PBKDF2', false, ['deriveKey']);
|
|
1211
|
+
const salt = new TextEncoder().encode(window.location.hostname.padEnd(16, '0').substring(0, 16));
|
|
1212
|
+
const key = await crypto.subtle.deriveKey({
|
|
1213
|
+
name: 'PBKDF2',
|
|
1214
|
+
salt: salt,
|
|
1215
|
+
iterations: 10000,
|
|
1216
|
+
hash: 'SHA-256'
|
|
1217
|
+
}, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
|
|
1218
|
+
// Decodifica base64 e separa IV dos dados
|
|
1219
|
+
const combined = new Uint8Array(atob(encryptedText).split('').map(c => c.charCodeAt(0)));
|
|
1220
|
+
const iv = combined.slice(0, 12);
|
|
1221
|
+
const encrypted = combined.slice(12);
|
|
1222
|
+
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, encrypted);
|
|
1223
|
+
return new TextDecoder().decode(decrypted);
|
|
1224
|
+
}
|
|
1225
|
+
catch (error) {
|
|
1226
|
+
console.error('Erro na descriptografia:', error);
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
validateStorageEvent(event) {
|
|
1231
|
+
// Validação rigorosa de origem
|
|
1232
|
+
if (!event.url || event.url.trim() === '') {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
try {
|
|
1236
|
+
const eventOrigin = new URL(event.url).origin;
|
|
1237
|
+
const currentOrigin = window.location.origin;
|
|
1238
|
+
if (eventOrigin !== currentOrigin) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
// Validação rigorosa de chave
|
|
1246
|
+
if (event.key && !this.validateKey(event.key)) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
// Verificação adicional de integridade temporal
|
|
1250
|
+
const now = Date.now();
|
|
1251
|
+
if (!this.lastEventTime) {
|
|
1252
|
+
this.lastEventTime = now;
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
// Rate limiting básico
|
|
1256
|
+
if (now - this.lastEventTime < 50) { // Máximo 20 eventos/segundo
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
this.lastEventTime = now;
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
lastEventTime = 0;
|
|
1263
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LocalStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1264
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LocalStorageService, providedIn: 'root' });
|
|
1265
|
+
}
|
|
1266
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LocalStorageService, decorators: [{
|
|
1267
|
+
type: Injectable,
|
|
1268
|
+
args: [{
|
|
1269
|
+
providedIn: 'root'
|
|
1270
|
+
}]
|
|
1271
|
+
}] });
|
|
1272
|
+
|
|
1273
|
+
class AuthStorageService {
|
|
1274
|
+
storage;
|
|
1275
|
+
TOKEN_KEY = 'access_token';
|
|
1276
|
+
USER_DATA_KEY = 'user_data';
|
|
1277
|
+
// Observable para monitorar estado de autenticação
|
|
1278
|
+
isAuthenticated$ = new BehaviorSubject(false);
|
|
1279
|
+
get authState$() {
|
|
1280
|
+
return this.isAuthenticated$.asObservable();
|
|
1281
|
+
}
|
|
1282
|
+
constructor(storage) {
|
|
1283
|
+
this.storage = storage;
|
|
1284
|
+
// Inicializa o estado de autenticação de forma assíncrona
|
|
1285
|
+
this.initAuthState();
|
|
1286
|
+
}
|
|
1287
|
+
async initAuthState() {
|
|
1288
|
+
const hasToken = await this.hasToken();
|
|
1289
|
+
this.isAuthenticated$.next(hasToken);
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Salva Access Token COM CRIPTOGRAFIA (backend fará todas as validações)
|
|
1293
|
+
*/
|
|
1294
|
+
async setToken(token, expiryInMinutes = 60) {
|
|
1295
|
+
if (!token || token.trim().length === 0) {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
// 🔐 SEMPRE criptografar tokens para máxima segurança
|
|
1299
|
+
// Criamos estrutura com expiração manualmente para usar criptografia
|
|
1300
|
+
const now = new Date();
|
|
1301
|
+
const item = {
|
|
1302
|
+
value: token,
|
|
1303
|
+
expiry: now.getTime() + (expiryInMinutes * 60 * 1000)
|
|
1304
|
+
};
|
|
1305
|
+
const success = await this.storage.setItem(this.TOKEN_KEY, item, 'sessionStorage', { encrypt: true });
|
|
1306
|
+
if (success) {
|
|
1307
|
+
this.isAuthenticated$.next(true);
|
|
1308
|
+
}
|
|
1309
|
+
return success;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Recupera Access Token COM DESCRIPTOGRAFIA e validação de expiração
|
|
1313
|
+
*/
|
|
1314
|
+
async getToken() {
|
|
1315
|
+
try {
|
|
1316
|
+
// 🔓 SEMPRE descriptografar tokens
|
|
1317
|
+
const item = await this.storage.getItem(this.TOKEN_KEY, 'sessionStorage', { encrypt: true });
|
|
1318
|
+
if (!item || !item.value) {
|
|
1319
|
+
this.isAuthenticated$.next(false);
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
// Verificar expiração
|
|
1323
|
+
const now = Date.now();
|
|
1324
|
+
if (item.expiry && now > item.expiry) {
|
|
1325
|
+
// Token expirado - remover
|
|
1326
|
+
this.storage.removeItem(this.TOKEN_KEY, 'sessionStorage');
|
|
1327
|
+
this.isAuthenticated$.next(false);
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
return item.value;
|
|
1331
|
+
}
|
|
1332
|
+
catch (error) {
|
|
1333
|
+
console.warn('Erro ao recuperar token criptografado:', error);
|
|
1334
|
+
this.isAuthenticated$.next(false);
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
// REMOVIDO: Refresh Token methods - ficam no servidor
|
|
1339
|
+
// O backend gerencia refresh automaticamente via interceptors
|
|
1340
|
+
/**
|
|
1341
|
+
* Salva dados do usuário COM CRIPTOGRAFIA e validação básica
|
|
1342
|
+
*/
|
|
1343
|
+
async setUserData(userData) {
|
|
1344
|
+
if (!userData || typeof userData !== 'object') {
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
// Validação básica de tamanho
|
|
1348
|
+
try {
|
|
1349
|
+
const serialized = JSON.stringify(userData);
|
|
1350
|
+
if (serialized.length > 100000) { // 100KB limite
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
catch {
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
// 🔐 SEMPRE criptografar dados do usuário também
|
|
1358
|
+
return await this.storage.setItem(this.USER_DATA_KEY, userData, 'sessionStorage', { encrypt: true });
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Recupera dados do usuário COM DESCRIPTOGRAFIA
|
|
1362
|
+
*/
|
|
1363
|
+
async getUserData() {
|
|
1364
|
+
try {
|
|
1365
|
+
// 🔓 SEMPRE descriptografar dados do usuário
|
|
1366
|
+
return await this.storage.getItem(this.USER_DATA_KEY, 'sessionStorage', { encrypt: true });
|
|
1367
|
+
}
|
|
1368
|
+
catch (error) {
|
|
1369
|
+
console.warn('Erro ao recuperar dados do usuário criptografados:', error);
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Verifica apenas se possui token (backend validará se é válido)
|
|
1375
|
+
*/
|
|
1376
|
+
async hasToken() {
|
|
1377
|
+
const token = await this.getToken();
|
|
1378
|
+
return !!token && token.length > 0;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Verifica se está "aparentemente" autenticado (token existe)
|
|
1382
|
+
*/
|
|
1383
|
+
async isAuthenticated() {
|
|
1384
|
+
return await this.hasToken();
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Limpa APENAS dados do cliente (não afeta refresh token no servidor)
|
|
1388
|
+
*/
|
|
1389
|
+
clearAuthData() {
|
|
1390
|
+
this.storage.removeItem(this.TOKEN_KEY, 'sessionStorage');
|
|
1391
|
+
this.storage.removeItem(this.USER_DATA_KEY, 'sessionStorage');
|
|
1392
|
+
this.isAuthenticated$.next(false);
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Decodifica payload do JWT (SEM validação - apenas para UI)
|
|
1396
|
+
* IMPORTANTE: Use apenas para exibição, nunca para lógica de segurança
|
|
1397
|
+
*
|
|
1398
|
+
* ⚠️ ATENÇÃO: Agora requer token como parâmetro ou uso assíncrono
|
|
1399
|
+
*/
|
|
1400
|
+
decodeTokenPayload(token) {
|
|
1401
|
+
try {
|
|
1402
|
+
// Se não foi passado token, não podemos usar getTokenSync (descontinuado)
|
|
1403
|
+
if (!token) {
|
|
1404
|
+
console.warn('⚠️ decodeTokenPayload(): Passe o token como parâmetro ou use ' +
|
|
1405
|
+
'await getToken() primeiro, pois tokens são criptografados.');
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
const jwt = token;
|
|
1409
|
+
if (!jwt || typeof jwt !== 'string')
|
|
1410
|
+
return null;
|
|
1411
|
+
// Validação básica do formato JWT
|
|
1412
|
+
const parts = jwt.split('.');
|
|
1413
|
+
if (parts.length !== 3)
|
|
1414
|
+
return null;
|
|
1415
|
+
// Validação base64url
|
|
1416
|
+
const base64UrlPattern = /^[A-Za-z0-9_-]+$/;
|
|
1417
|
+
if (!base64UrlPattern.test(parts[1]))
|
|
1418
|
+
return null;
|
|
1419
|
+
// Validação de tamanho razoável
|
|
1420
|
+
if (parts[1].length > 10000)
|
|
1421
|
+
return null;
|
|
1422
|
+
const payload = parts[1];
|
|
1423
|
+
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
1424
|
+
// Parse seguro com proteção contra prototype pollution
|
|
1425
|
+
return JSON.parse(decoded, (key, value) => {
|
|
1426
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
1427
|
+
return undefined;
|
|
1428
|
+
}
|
|
1429
|
+
return value;
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
console.warn('Erro ao decodificar token para UI:', error);
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Versão síncrona DESATUALIZADA (tokens agora são criptografados)
|
|
1439
|
+
* ⚠️ AVISO: Esta versão não funciona mais com tokens criptografados
|
|
1440
|
+
* Use getToken() async para acessar tokens de forma segura
|
|
1441
|
+
*/
|
|
1442
|
+
getTokenSync() {
|
|
1443
|
+
console.warn('⚠️ getTokenSync() DESCONTINUADO: Tokens agora são criptografados. ' +
|
|
1444
|
+
'Use getToken() async para descriptografia segura.');
|
|
1445
|
+
// Não é possível descriptografar sincronamente
|
|
1446
|
+
// A criptografia AES-GCM requer operações assíncronas
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Verifica se token está próximo de expirar (para UI)
|
|
1451
|
+
* ⚠️ ATENÇÃO: Agora é assíncrono devido à criptografia
|
|
1452
|
+
*/
|
|
1453
|
+
async isTokenExpiringSoon(minutesThreshold = 5) {
|
|
1454
|
+
try {
|
|
1455
|
+
const token = await this.getToken();
|
|
1456
|
+
if (!token)
|
|
1457
|
+
return true;
|
|
1458
|
+
const tokenData = this.decodeTokenPayload(token);
|
|
1459
|
+
if (!tokenData || !tokenData.exp)
|
|
1460
|
+
return true;
|
|
1461
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1462
|
+
const timeUntilExpiry = tokenData.exp - now;
|
|
1463
|
+
return timeUntilExpiry < (minutesThreshold * 60);
|
|
1464
|
+
}
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
console.warn('Erro ao verificar expiração do token:', error);
|
|
1467
|
+
return true; // Em caso de erro, assumir que está expirando
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Método utilitário: Obtém e decodifica token em uma operação
|
|
1472
|
+
* Ideal para componentes que precisam dos dados do token para UI
|
|
1473
|
+
*/
|
|
1474
|
+
async getDecodedTokenData() {
|
|
1475
|
+
try {
|
|
1476
|
+
const token = await this.getToken();
|
|
1477
|
+
if (!token)
|
|
1478
|
+
return null;
|
|
1479
|
+
return this.decodeTokenPayload(token);
|
|
1480
|
+
}
|
|
1481
|
+
catch (error) {
|
|
1482
|
+
console.warn('Erro ao obter dados decodificados do token:', error);
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthStorageService, deps: [{ token: LocalStorageService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1487
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthStorageService, providedIn: 'root' });
|
|
1488
|
+
}
|
|
1489
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthStorageService, decorators: [{
|
|
1490
|
+
type: Injectable,
|
|
1491
|
+
args: [{
|
|
1492
|
+
providedIn: 'root'
|
|
1493
|
+
}]
|
|
1494
|
+
}], ctorParameters: () => [{ type: LocalStorageService }] });
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Token de injeção para o AuthProvider
|
|
1498
|
+
*/
|
|
1499
|
+
const AUTH_PROVIDER = new InjectionToken('AuthProvider');
|
|
1500
|
+
class LoginService {
|
|
1501
|
+
authStorage;
|
|
1502
|
+
authProvider;
|
|
1503
|
+
DEFAULT_OPTIONS = {
|
|
1504
|
+
// Configurações visuais
|
|
1505
|
+
title: 'Login',
|
|
1506
|
+
subtitle: 'Entre na sua conta',
|
|
1507
|
+
logoUrl: '',
|
|
1508
|
+
backgroundUrl: '',
|
|
1509
|
+
footerText: '',
|
|
1510
|
+
// Funcionalidades
|
|
1511
|
+
redirectUrl: '/dashboard',
|
|
1512
|
+
showForgotPassword: true,
|
|
1513
|
+
showRegisterLink: true,
|
|
1514
|
+
passwordMinLength: 6,
|
|
1515
|
+
// Links personalizados
|
|
1516
|
+
links: {
|
|
1517
|
+
forgotPassword: '/forgot-password',
|
|
1518
|
+
register: '/register',
|
|
1519
|
+
termsOfService: '/terms',
|
|
1520
|
+
privacyPolicy: '/privacy'
|
|
1521
|
+
},
|
|
1522
|
+
// Personalização visual
|
|
1523
|
+
theme: 'light',
|
|
1524
|
+
primaryColor: '#3b82f6'
|
|
1525
|
+
};
|
|
1526
|
+
// Estado reativo do login
|
|
1527
|
+
loginState$ = new BehaviorSubject({
|
|
1528
|
+
status: LoginStatus.IDLE,
|
|
1529
|
+
user: null,
|
|
1530
|
+
isAuthenticated: false,
|
|
1531
|
+
errors: {},
|
|
1532
|
+
lastAttempt: null
|
|
1533
|
+
});
|
|
1534
|
+
// Configurações atuais
|
|
1535
|
+
currentOptions = { ...this.DEFAULT_OPTIONS };
|
|
1536
|
+
constructor(authStorage, authProvider) {
|
|
1537
|
+
this.authStorage = authStorage;
|
|
1538
|
+
this.authProvider = authProvider;
|
|
1539
|
+
this.initializeService();
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Observable do estado completo do login
|
|
1543
|
+
*/
|
|
1544
|
+
get state$() {
|
|
1545
|
+
return this.loginState$.asObservable();
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Observable apenas do status de loading
|
|
1549
|
+
*/
|
|
1550
|
+
get isLoading$() {
|
|
1551
|
+
return this.loginState$.pipe(map(state => state.status === LoginStatus.LOADING));
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Observable apenas do status de autenticação
|
|
1555
|
+
*/
|
|
1556
|
+
get isAuthenticated$() {
|
|
1557
|
+
return this.loginState$.pipe(map(state => state.isAuthenticated));
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Observable apenas dos erros
|
|
1561
|
+
*/
|
|
1562
|
+
get errors$() {
|
|
1563
|
+
return this.loginState$.pipe(map(state => state.errors));
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Observable do usuário atual
|
|
1567
|
+
*/
|
|
1568
|
+
get currentUser$() {
|
|
1569
|
+
return this.loginState$.pipe(map(state => state.user));
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Configura opções do serviço de login
|
|
1573
|
+
*/
|
|
1574
|
+
configure(options) {
|
|
1575
|
+
this.currentOptions = { ...this.currentOptions, ...options };
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Realiza login com as credenciais fornecidas
|
|
1579
|
+
*/
|
|
1580
|
+
async login(credentials) {
|
|
1581
|
+
// Valida credenciais básicas
|
|
1582
|
+
const validationErrors = this.validateCredentials(credentials);
|
|
1583
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
1584
|
+
this.updateState({
|
|
1585
|
+
status: LoginStatus.ERROR,
|
|
1586
|
+
errors: validationErrors
|
|
1587
|
+
});
|
|
1588
|
+
return { success: false, message: 'Dados inválidos', errors: Object.values(validationErrors).flat() };
|
|
1589
|
+
}
|
|
1590
|
+
// Inicia processo de login
|
|
1591
|
+
this.updateState({
|
|
1592
|
+
status: LoginStatus.LOADING,
|
|
1593
|
+
errors: {}
|
|
1594
|
+
});
|
|
1595
|
+
try {
|
|
1596
|
+
let response;
|
|
1597
|
+
// if (this.authProvider) {
|
|
1598
|
+
// // Usa provider customizado da aplicação
|
|
1599
|
+
// response = await this.authProvider.login(credentials);
|
|
1600
|
+
// } else {
|
|
1601
|
+
// // Simula resposta (para desenvolvimento/demo)
|
|
1602
|
+
// response = await this.simulateLogin(credentials);
|
|
1603
|
+
// }
|
|
1604
|
+
response = await this.simulateLogin(credentials);
|
|
1605
|
+
if (response.success && response.accessToken && response.user) {
|
|
1606
|
+
// Login bem-sucedido
|
|
1607
|
+
await this.handleSuccessfulLogin(response);
|
|
1608
|
+
this.updateState({
|
|
1609
|
+
status: LoginStatus.SUCCESS,
|
|
1610
|
+
user: response.user,
|
|
1611
|
+
isAuthenticated: true,
|
|
1612
|
+
errors: {}
|
|
1613
|
+
});
|
|
1614
|
+
// Login bem-sucedido
|
|
1615
|
+
return response;
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
// Login falhou
|
|
1619
|
+
await this.handleFailedLogin();
|
|
1620
|
+
this.updateState({
|
|
1621
|
+
status: LoginStatus.ERROR,
|
|
1622
|
+
errors: { general: [response.message || 'Credenciais inválidas'] }
|
|
1623
|
+
});
|
|
1624
|
+
return response;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
console.error('Erro durante login:', error);
|
|
1629
|
+
await this.handleFailedLogin();
|
|
1630
|
+
const errorMessage = error instanceof Error ? error.message : 'Erro interno do servidor';
|
|
1631
|
+
this.updateState({
|
|
1632
|
+
status: LoginStatus.ERROR,
|
|
1633
|
+
errors: { general: [errorMessage] }
|
|
1634
|
+
});
|
|
1635
|
+
return { success: false, message: errorMessage };
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Realiza logout
|
|
1640
|
+
*/
|
|
1641
|
+
async logout() {
|
|
1642
|
+
this.updateState({ status: LoginStatus.LOADING });
|
|
1643
|
+
try {
|
|
1644
|
+
let response = { success: true };
|
|
1645
|
+
if (this.authProvider?.logout) {
|
|
1646
|
+
// Notifica o backend
|
|
1647
|
+
response = await this.authProvider.logout();
|
|
1648
|
+
}
|
|
1649
|
+
// Limpa dados locais independente da resposta do backend
|
|
1650
|
+
this.authStorage.clearAuthData();
|
|
1651
|
+
this.updateState({
|
|
1652
|
+
status: LoginStatus.IDLE,
|
|
1653
|
+
user: null,
|
|
1654
|
+
isAuthenticated: false,
|
|
1655
|
+
errors: {},
|
|
1656
|
+
lastAttempt: null
|
|
1657
|
+
});
|
|
1658
|
+
return response;
|
|
1659
|
+
}
|
|
1660
|
+
catch (error) {
|
|
1661
|
+
console.error('Erro durante logout:', error);
|
|
1662
|
+
// Mesmo com erro, limpa dados locais
|
|
1663
|
+
this.authStorage.clearAuthData();
|
|
1664
|
+
this.updateState({
|
|
1665
|
+
status: LoginStatus.IDLE,
|
|
1666
|
+
user: null,
|
|
1667
|
+
isAuthenticated: false,
|
|
1668
|
+
errors: {}
|
|
1669
|
+
});
|
|
1670
|
+
return { success: true }; // Sempre considera logout como sucesso localmente
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Verifica se o usuário atual está autenticado
|
|
1675
|
+
*/
|
|
1676
|
+
async checkAuthenticationStatus() {
|
|
1677
|
+
try {
|
|
1678
|
+
const hasToken = await this.authStorage.hasToken();
|
|
1679
|
+
if (!hasToken) {
|
|
1680
|
+
this.updateState({
|
|
1681
|
+
status: LoginStatus.IDLE,
|
|
1682
|
+
isAuthenticated: false,
|
|
1683
|
+
user: null
|
|
1684
|
+
});
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
// Carrega dados do usuário se autenticado
|
|
1688
|
+
const userData = await this.authStorage.getUserData();
|
|
1689
|
+
if (userData) {
|
|
1690
|
+
this.updateState({
|
|
1691
|
+
status: LoginStatus.SUCCESS,
|
|
1692
|
+
isAuthenticated: true,
|
|
1693
|
+
user: userData
|
|
1694
|
+
});
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
catch (error) {
|
|
1700
|
+
console.error('Erro ao verificar autenticação:', error);
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Obtém informações do usuário atual
|
|
1706
|
+
*/
|
|
1707
|
+
async getCurrentUser() {
|
|
1708
|
+
return await this.authStorage.getUserData();
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Verifica se token está expirando em breve
|
|
1712
|
+
* ⚠️ ATENÇÃO: Agora é assíncrono devido à criptografia
|
|
1713
|
+
*/
|
|
1714
|
+
async isTokenExpiringSoon(minutesThreshold = 5) {
|
|
1715
|
+
return await this.authStorage.isTokenExpiringSoon(minutesThreshold);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Limpa erros do estado atual
|
|
1719
|
+
*/
|
|
1720
|
+
clearErrors() {
|
|
1721
|
+
this.updateState({ errors: {} });
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Reseta estado do login
|
|
1725
|
+
*/
|
|
1726
|
+
async resetLoginState() {
|
|
1727
|
+
this.updateState({
|
|
1728
|
+
status: LoginStatus.IDLE,
|
|
1729
|
+
errors: {}
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
// ========== MÉTODOS PRIVADOS ==========
|
|
1733
|
+
/**
|
|
1734
|
+
* Inicializa o serviço
|
|
1735
|
+
*/
|
|
1736
|
+
async initializeService() {
|
|
1737
|
+
// Inicialização simples sem controle de tentativas
|
|
1738
|
+
// Verifica autenticação atual
|
|
1739
|
+
await this.checkAuthenticationStatus();
|
|
1740
|
+
// Monitora mudanças de autenticação
|
|
1741
|
+
this.authStorage.authState$.subscribe(isAuthenticated => {
|
|
1742
|
+
if (!isAuthenticated && this.loginState$.value.isAuthenticated) {
|
|
1743
|
+
// Usuário perdeu autenticação
|
|
1744
|
+
this.updateState({
|
|
1745
|
+
status: LoginStatus.IDLE,
|
|
1746
|
+
user: null,
|
|
1747
|
+
isAuthenticated: false
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Atualiza estado reativo
|
|
1754
|
+
*/
|
|
1755
|
+
updateState(updates) {
|
|
1756
|
+
const currentState = this.loginState$.value;
|
|
1757
|
+
this.loginState$.next({
|
|
1758
|
+
...currentState,
|
|
1759
|
+
...updates,
|
|
1760
|
+
lastAttempt: updates.status === LoginStatus.LOADING ? new Date() : currentState.lastAttempt
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Valida credenciais do formulário
|
|
1765
|
+
*/
|
|
1766
|
+
validateCredentials(credentials) {
|
|
1767
|
+
const errors = {};
|
|
1768
|
+
// Validação de email
|
|
1769
|
+
if (!credentials.email) {
|
|
1770
|
+
errors.email = ['Email é obrigatório'];
|
|
1771
|
+
}
|
|
1772
|
+
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) {
|
|
1773
|
+
errors.email = ['Email deve ter um formato válido'];
|
|
1774
|
+
}
|
|
1775
|
+
// Validação de senha
|
|
1776
|
+
if (!credentials.password) {
|
|
1777
|
+
errors.password = ['Senha é obrigatória'];
|
|
1778
|
+
}
|
|
1779
|
+
else if (credentials.password.length < 3) {
|
|
1780
|
+
errors.password = ['Senha deve ter pelo menos 3 caracteres'];
|
|
1781
|
+
}
|
|
1782
|
+
return errors;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Trata login bem-sucedido
|
|
1786
|
+
*/
|
|
1787
|
+
async handleSuccessfulLogin(response) {
|
|
1788
|
+
if (!response.accessToken || !response.user) {
|
|
1789
|
+
throw new Error('Resposta de login inválida');
|
|
1790
|
+
}
|
|
1791
|
+
// Salva token
|
|
1792
|
+
const expiryMinutes = response.expiresIn || 60; // Default 1 hora
|
|
1793
|
+
await this.authStorage.setToken(response.accessToken, expiryMinutes);
|
|
1794
|
+
// Salva dados do usuário
|
|
1795
|
+
await this.authStorage.setUserData(response.user);
|
|
1796
|
+
// Credenciais salvas com sucesso
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Trata falha de login
|
|
1800
|
+
*/
|
|
1801
|
+
async handleFailedLogin() {
|
|
1802
|
+
// Registra falha de login
|
|
1803
|
+
console.warn('Tentativa de login falhou');
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Simula resposta de login para desenvolvimento/demo
|
|
1807
|
+
*/
|
|
1808
|
+
async simulateLogin(credentials) {
|
|
1809
|
+
// Simula delay da rede
|
|
1810
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1811
|
+
// Credenciais de demo
|
|
1812
|
+
const demoUsers = [
|
|
1813
|
+
{
|
|
1814
|
+
email: 'admin@demo.com',
|
|
1815
|
+
password: 'admin',
|
|
1816
|
+
user: {
|
|
1817
|
+
id: '1',
|
|
1818
|
+
nome: 'Administrador',
|
|
1819
|
+
email: 'admin@demo.com',
|
|
1820
|
+
perfil: 'ADMIN',
|
|
1821
|
+
permissoes: ['TODAS'],
|
|
1822
|
+
avatar: 'https://via.placeholder.com/100'
|
|
1823
|
+
}
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
email: 'professor@demo.com',
|
|
1827
|
+
password: 'professor',
|
|
1828
|
+
user: {
|
|
1829
|
+
id: '2',
|
|
1830
|
+
nome: 'Professor Demo',
|
|
1831
|
+
email: 'professor@demo.com',
|
|
1832
|
+
perfil: 'PROFESSOR',
|
|
1833
|
+
permissoes: ['CRIAR_PROVA', 'VER_RESULTADOS'],
|
|
1834
|
+
avatar: 'https://via.placeholder.com/100'
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
email: 'aluno@demo.com',
|
|
1839
|
+
password: 'aluno',
|
|
1840
|
+
user: {
|
|
1841
|
+
id: '3',
|
|
1842
|
+
nome: 'Aluno Demo',
|
|
1843
|
+
email: 'aluno@demo.com',
|
|
1844
|
+
perfil: 'ALUNO',
|
|
1845
|
+
permissoes: ['RESPONDER_PROVA'],
|
|
1846
|
+
avatar: 'https://via.placeholder.com/100'
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
];
|
|
1850
|
+
const validUser = demoUsers.find(user => user.email === credentials.email && user.password === credentials.password);
|
|
1851
|
+
if (validUser) {
|
|
1852
|
+
// Gera token fake
|
|
1853
|
+
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
|
1854
|
+
const payload = btoa(JSON.stringify({
|
|
1855
|
+
sub: validUser.user.id,
|
|
1856
|
+
email: validUser.user.email,
|
|
1857
|
+
name: validUser.user.nome,
|
|
1858
|
+
roles: validUser.user.permissoes,
|
|
1859
|
+
iat: Math.floor(Date.now() / 1000),
|
|
1860
|
+
exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hora
|
|
1861
|
+
}));
|
|
1862
|
+
const fakeToken = `${header}.${payload}.fake-signature`;
|
|
1863
|
+
return {
|
|
1864
|
+
success: true,
|
|
1865
|
+
accessToken: fakeToken,
|
|
1866
|
+
user: validUser.user,
|
|
1867
|
+
expiresIn: 60,
|
|
1868
|
+
message: 'Login realizado com sucesso'
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
return {
|
|
1872
|
+
success: false,
|
|
1873
|
+
message: 'Email ou senha incorretos'
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginService, deps: [{ token: AuthStorageService }, { token: AUTH_PROVIDER, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1877
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginService, providedIn: 'root' });
|
|
1878
|
+
}
|
|
1879
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginService, decorators: [{
|
|
1880
|
+
type: Injectable,
|
|
1881
|
+
args: [{
|
|
1882
|
+
providedIn: 'root'
|
|
1883
|
+
}]
|
|
1884
|
+
}], ctorParameters: () => [{ type: AuthStorageService }, { type: undefined, decorators: [{
|
|
1885
|
+
type: Optional
|
|
1886
|
+
}, {
|
|
1887
|
+
type: Inject,
|
|
1888
|
+
args: [AUTH_PROVIDER]
|
|
1889
|
+
}] }] });
|
|
1890
|
+
|
|
1891
|
+
class LoginComponent {
|
|
1892
|
+
loginService;
|
|
1893
|
+
destroy$ = new Subject();
|
|
1894
|
+
fb = inject(FormBuilder);
|
|
1895
|
+
// Configurações do componente
|
|
1896
|
+
options = {};
|
|
1897
|
+
showHeader = true;
|
|
1898
|
+
showFooter = true;
|
|
1899
|
+
logoUrl;
|
|
1900
|
+
backgroundImageUrl;
|
|
1901
|
+
customCssClass;
|
|
1902
|
+
// Eventos
|
|
1903
|
+
loginSuccess = new EventEmitter();
|
|
1904
|
+
loginError = new EventEmitter();
|
|
1905
|
+
formChange = new EventEmitter();
|
|
1906
|
+
// Estado do componente
|
|
1907
|
+
loginForm;
|
|
1908
|
+
loginState$;
|
|
1909
|
+
isLoading$;
|
|
1910
|
+
errors$;
|
|
1911
|
+
// Estados locais
|
|
1912
|
+
showPassword = false;
|
|
1913
|
+
LoginStatus = LoginStatus; // Para usar no template
|
|
1914
|
+
constructor(loginService) {
|
|
1915
|
+
this.loginService = loginService;
|
|
1916
|
+
this.createForm();
|
|
1917
|
+
this.initializeObservables();
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Inicializa os observables
|
|
1921
|
+
*/
|
|
1922
|
+
initializeObservables() {
|
|
1923
|
+
this.loginState$ = this.loginService.state$;
|
|
1924
|
+
this.isLoading$ = this.loginService.isLoading$;
|
|
1925
|
+
this.errors$ = this.loginService.errors$;
|
|
1926
|
+
}
|
|
1927
|
+
ngOnInit() {
|
|
1928
|
+
// Configura opções do serviço
|
|
1929
|
+
if (this.options) {
|
|
1930
|
+
this.loginService.configure(this.options);
|
|
1931
|
+
}
|
|
1932
|
+
// Monitora mudanças do estado
|
|
1933
|
+
this.loginState$
|
|
1934
|
+
.pipe(takeUntil(this.destroy$))
|
|
1935
|
+
.subscribe(state => this.handleStateChange(state));
|
|
1936
|
+
// Monitora mudanças do formulário
|
|
1937
|
+
this.loginForm.valueChanges
|
|
1938
|
+
.pipe(takeUntil(this.destroy$))
|
|
1939
|
+
.subscribe(value => {
|
|
1940
|
+
this.formChange.emit(value);
|
|
1941
|
+
});
|
|
1942
|
+
// Limpa erros quando usuário começa a digitar
|
|
1943
|
+
this.loginForm.get('email')?.valueChanges
|
|
1944
|
+
.pipe(takeUntil(this.destroy$))
|
|
1945
|
+
.subscribe(() => this.loginService.clearErrors());
|
|
1946
|
+
this.loginForm.get('password')?.valueChanges
|
|
1947
|
+
.pipe(takeUntil(this.destroy$))
|
|
1948
|
+
.subscribe(() => this.loginService.clearErrors());
|
|
1949
|
+
}
|
|
1950
|
+
ngOnDestroy() {
|
|
1951
|
+
this.destroy$.next();
|
|
1952
|
+
this.destroy$.complete();
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Cria o formulário reativo
|
|
1956
|
+
*/
|
|
1957
|
+
createForm() {
|
|
1958
|
+
this.loginForm = this.fb.group({
|
|
1959
|
+
email: ['', [
|
|
1960
|
+
Validators.required,
|
|
1961
|
+
Validators.email,
|
|
1962
|
+
Validators.maxLength(255)
|
|
1963
|
+
]],
|
|
1964
|
+
password: ['', [
|
|
1965
|
+
Validators.required,
|
|
1966
|
+
Validators.minLength(3),
|
|
1967
|
+
Validators.maxLength(100)
|
|
1968
|
+
]],
|
|
1969
|
+
rememberMe: [false]
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Manipula mudanças de estado do login
|
|
1974
|
+
*/
|
|
1975
|
+
handleStateChange(state) {
|
|
1976
|
+
if (state.status === LoginStatus.SUCCESS && state.user) {
|
|
1977
|
+
this.loginSuccess.emit({
|
|
1978
|
+
success: true,
|
|
1979
|
+
user: state.user,
|
|
1980
|
+
accessToken: '', // Não expor token no evento
|
|
1981
|
+
message: 'Login realizado com sucesso'
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
if (state.status === LoginStatus.ERROR && state.errors.general) {
|
|
1985
|
+
this.loginError.emit(state.errors.general[0]);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Submete o formulário de login
|
|
1990
|
+
*/
|
|
1991
|
+
async onSubmit() {
|
|
1992
|
+
if (this.loginForm.invalid) {
|
|
1993
|
+
this.markFormGroupTouched();
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const credentials = this.loginForm.value;
|
|
1997
|
+
try {
|
|
1998
|
+
const response = await this.loginService.login(credentials);
|
|
1999
|
+
if (!response.success) {
|
|
2000
|
+
// Erro já tratado pelo serviço via observable
|
|
2001
|
+
console.warn('Login falhou:', response.message);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
catch (error) {
|
|
2005
|
+
console.error('Erro no componente de login:', error);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Alterna visibilidade da senha
|
|
2010
|
+
*/
|
|
2011
|
+
togglePasswordVisibility() {
|
|
2012
|
+
this.showPassword = !this.showPassword;
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Marca todos os campos como tocados para mostrar erros
|
|
2016
|
+
*/
|
|
2017
|
+
markFormGroupTouched() {
|
|
2018
|
+
Object.keys(this.loginForm.controls).forEach(key => {
|
|
2019
|
+
const control = this.loginForm.get(key);
|
|
2020
|
+
control?.markAsTouched();
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Verifica se campo tem erro específico
|
|
2025
|
+
*/
|
|
2026
|
+
hasFieldError(fieldName, errorType) {
|
|
2027
|
+
const field = this.loginForm.get(fieldName);
|
|
2028
|
+
return !!(field?.hasError(errorType) && (field?.dirty || field?.touched));
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Obtém mensagem de erro do campo
|
|
2032
|
+
*/
|
|
2033
|
+
getFieldErrorMessage(fieldName) {
|
|
2034
|
+
const field = this.loginForm.get(fieldName);
|
|
2035
|
+
if (!field || !field.errors || (!field.dirty && !field.touched)) {
|
|
2036
|
+
return '';
|
|
2037
|
+
}
|
|
2038
|
+
const errors = field.errors;
|
|
2039
|
+
if (errors['required']) {
|
|
2040
|
+
return `${this.getFieldLabel(fieldName)} é obrigatório`;
|
|
2041
|
+
}
|
|
2042
|
+
if (errors['email']) {
|
|
2043
|
+
return 'Email deve ter um formato válido';
|
|
2044
|
+
}
|
|
2045
|
+
if (errors['minlength']) {
|
|
2046
|
+
return `${this.getFieldLabel(fieldName)} deve ter pelo menos ${errors['minlength'].requiredLength} caracteres`;
|
|
2047
|
+
}
|
|
2048
|
+
if (errors['maxlength']) {
|
|
2049
|
+
return `${this.getFieldLabel(fieldName)} deve ter no máximo ${errors['maxlength'].requiredLength} caracteres`;
|
|
2050
|
+
}
|
|
2051
|
+
return 'Campo inválido';
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Obtém label do campo
|
|
2055
|
+
*/
|
|
2056
|
+
getFieldLabel(fieldName) {
|
|
2057
|
+
const labels = {
|
|
2058
|
+
email: 'Email',
|
|
2059
|
+
password: 'Senha'
|
|
2060
|
+
};
|
|
2061
|
+
return labels[fieldName] || fieldName;
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Reseta o formulário
|
|
2065
|
+
*/
|
|
2066
|
+
resetForm() {
|
|
2067
|
+
this.loginForm.reset();
|
|
2068
|
+
this.showPassword = false;
|
|
2069
|
+
this.loginService.clearErrors();
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Tenta fazer login com credenciais de demonstração
|
|
2073
|
+
*/
|
|
2074
|
+
async loginWithDemo(userType) {
|
|
2075
|
+
const demoCredentials = {
|
|
2076
|
+
admin: {
|
|
2077
|
+
email: 'admin@demo.com',
|
|
2078
|
+
password: 'admin'
|
|
2079
|
+
},
|
|
2080
|
+
professor: {
|
|
2081
|
+
email: 'professor@demo.com',
|
|
2082
|
+
password: 'professor'
|
|
2083
|
+
},
|
|
2084
|
+
aluno: {
|
|
2085
|
+
email: 'aluno@demo.com',
|
|
2086
|
+
password: 'aluno'
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
const credentials = demoCredentials[userType];
|
|
2090
|
+
if (credentials) {
|
|
2091
|
+
// Preenche formulário
|
|
2092
|
+
this.loginForm.patchValue(credentials);
|
|
2093
|
+
// Executa login
|
|
2094
|
+
await this.onSubmit();
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Manipula esqueci minha senha
|
|
2099
|
+
*/
|
|
2100
|
+
onForgotPassword() {
|
|
2101
|
+
// Implementação específica da aplicação
|
|
2102
|
+
console.log('Forgot password clicked');
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Manipula criação de conta
|
|
2106
|
+
*/
|
|
2107
|
+
onCreateAccount() {
|
|
2108
|
+
// Implementação específica da aplicação
|
|
2109
|
+
console.log('Create account clicked');
|
|
2110
|
+
}
|
|
2111
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginComponent, deps: [{ token: LoginService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2112
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: LoginComponent, isStandalone: true, selector: "lib-login", inputs: { options: "options", showHeader: "showHeader", showFooter: "showFooter", logoUrl: "logoUrl", backgroundImageUrl: "backgroundImageUrl", customCssClass: "customCssClass" }, outputs: { loginSuccess: "loginSuccess", loginError: "loginError", formChange: "formChange" }, ngImport: i0, template: "<!-- Container principal do login -->\n<div class=\"login-container\" [ngClass]=\"customCssClass\"\n [style.background-image]=\"options.backgroundUrl ? 'url(' + options.backgroundUrl + ')' : null\">\n\n <!-- Card de login -->\n <div class=\"login-card\">\n\n <!-- Header -->\n <div class=\"login-header\">\n <img *ngIf=\"options.logoUrl\" [src]=\"options.logoUrl\" alt=\"Logo\" class=\"login-logo\">\n\n <h1 class=\"login-title\">{{ options.title || 'Entrar' }}</h1>\n <p class=\"login-subtitle\">{{ options.subtitle || 'Acesse sua conta' }}</p>\n </div>\n\n <!-- Indicador de loading -->\n <div *ngIf=\"isLoading$ | async\" class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n <span class=\"loading-text\">Entrando...</span>\n </div>\n\n <!-- Estado de bloqueio removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Formul\u00E1rio de login -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"onSubmit()\" class=\"login-form\"\n [class.disabled]=\"(isLoading$ | async)\">\n\n <!-- Campo Email -->\n <div class=\"form-group\">\n <label for=\"email\" class=\"form-label\">\n Email <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"email\" type=\"email\" formControlName=\"email\" class=\"form-input\"\n [class.error]=\"hasFieldError('email', 'required') || hasFieldError('email', 'email')\"\n placeholder=\"Digite seu email\" autocomplete=\"email\" autocapitalize=\"off\">\n\n <i class=\"input-icon icon-email\"></i>\n </div>\n\n <!-- Erro do campo email -->\n <div *ngIf=\"getFieldErrorMessage('email')\" class=\"field-error\">\n {{ getFieldErrorMessage('email') }}\n </div>\n </div>\n\n <!-- Campo Senha -->\n <div class=\"form-group\">\n <label for=\"password\" class=\"form-label\">\n Senha <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"password\" [type]=\"showPassword ? 'text' : 'password'\" formControlName=\"password\"\n class=\"form-input\"\n [class.error]=\"hasFieldError('password', 'required') || hasFieldError('password', 'minlength')\"\n placeholder=\"Digite sua senha\" autocomplete=\"current-password\">\n\n <button type=\"button\" class=\"password-toggle\" (click)=\"togglePasswordVisibility()\"\n [attr.aria-label]=\"showPassword ? 'Ocultar senha' : 'Mostrar senha'\">\n <i [class]=\"showPassword ? 'icon-eye-off' : 'icon-eye'\"></i>\n </button>\n </div>\n\n <!-- Erro do campo senha -->\n <div *ngIf=\"getFieldErrorMessage('password')\" class=\"field-error\">\n {{ getFieldErrorMessage('password') }}\n </div>\n </div>\n\n <!-- Checkbox removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Erros gerais -->\n <div *ngIf=\"(errors$ | async)?.general\" class=\"alert alert-error\">\n <i class=\"icon-error\"></i>\n <div>\n <div *ngFor=\"let error of (errors$ | async)?.general\">\n {{ error }}\n </div>\n </div>\n </div>\n\n <!-- Bot\u00E3o de submit -->\n <button type=\"submit\" class=\"login-button\" [disabled]=\"loginForm.invalid || (isLoading$ | async)\">\n\n <span *ngIf=\"!(isLoading$ | async)\">Entrar</span>\n <span *ngIf=\"isLoading$ | async\" class=\"loading-content\">\n <i class=\"icon-spinner spin\"></i>\n Entrando...\n </span>\n </button>\n\n <!-- Contador de tentativas removido conforme solicita\u00E7\u00E3o -->\n </form>\n\n <!-- Links adicionais -->\n <div class=\"login-links\">\n <a href=\"#\" (click)=\"onForgotPassword(); $event.preventDefault()\" class=\"link\">\n Esqueci minha senha\n </a>\n\n <span class=\"separator\">\u2022</span>\n\n <a href=\"#\" (click)=\"onCreateAccount(); $event.preventDefault()\" class=\"link\">\n Criar conta\n </a>\n </div>\n\n <!-- Se\u00E7\u00E3o de demonstra\u00E7\u00E3o removida conforme solicita\u00E7\u00E3o -->\n\n <!-- Footer -->\n <div *ngIf=\"options.footerText\" class=\"login-footer\">\n <p class=\"footer-text\">\n {{ options.footerText }}\n </p>\n </div>\n </div>\n</div>", styles: ["@charset \"UTF-8\";:host{--login-primary-color: #007bff;--login-primary-hover: #0056b3;--login-secondary-color: #6c757d;--login-background: #f8f9fa;--login-card-background: #ffffff;--login-border-color: #dee2e6;--login-text-color: #343a40;--login-error-color: #dc3545;--login-success-color: #28a745;--login-warning-color: #ffc107;--login-shadow: 0 10px 25px rgba(0, 0, 0, .1);--login-border-radius: .75rem;--login-transition: all .3s ease}.login-container{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1rem;background:var(--login-background);background-size:cover;background-position:center;background-repeat:no-repeat;position:relative;font-family:DM Sans,sans-serif}.login-container:before{content:\"\";position:absolute;inset:0;background:#0000004d;z-index:1;opacity:0;transition:opacity .3s ease}.login-container[style*=background-image]:before{opacity:1}.login-card{background:var(--login-card-background);border-radius:var(--login-border-radius);box-shadow:var(--login-shadow);padding:2rem;width:100%;max-width:420px;position:relative;border:1px solid var(--login-border-color);z-index:2}@media (max-width: 480px){.login-card{padding:1.5rem;margin:.5rem}}.login-header{text-align:center;margin-bottom:2rem}.login-header .login-logo{max-width:80px;height:auto;margin-bottom:1rem}.login-header .login-title{font-family:DM Sans,sans-serif;font-size:1.75rem;font-weight:700;color:var(--login-text-color);margin:0 0 .5rem;line-height:1.2}.login-header .login-subtitle{font-size:.875rem;color:var(--login-secondary-color);margin:0;font-weight:400}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:var(--login-border-radius);z-index:10}.loading-overlay .loading-spinner{width:40px;height:40px;border:3px solid var(--login-border-color);border-top:3px solid var(--login-primary-color);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:1rem}.loading-overlay .loading-text{color:var(--login-secondary-color);font-size:.9rem;font-weight:500}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-form.disabled{pointer-events:none;opacity:.7}.form-group{margin-bottom:1.5rem}.form-label{display:block;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;color:var(--login-text-color);margin-bottom:.5rem;line-height:2.5}.form-label .required{color:var(--login-error-color);margin-left:.25rem}.input-wrapper{position:relative;display:flex;align-items:center}.form-input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid var(--login-border-color);border-radius:.25rem;display:block;width:100%;padding:.375rem 2.75rem .375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;background-clip:padding-box;color:var(--login-text-color);transition:var(--login-transition);outline:none}.form-input::placeholder{color:#6c757d}.form-input:focus{border-color:#b2b9be;box-shadow:none}.form-input:focus.error{border-color:var(--login-error-color)}.form-input.error{border-color:var(--login-error-color)}.form-input:disabled{background:#f8f9fa;color:#6c757d;cursor:not-allowed}.input-icon{position:absolute;right:1rem;color:var(--login-secondary-color);font-size:1.1rem;pointer-events:none}.password-toggle{position:absolute;right:.75rem;background:none;border:none;padding:.25rem;cursor:pointer;color:var(--login-secondary-color);transition:var(--login-transition);border-radius:4px}.password-toggle:hover{color:var(--login-primary-color);background:#3b82f61a}.password-toggle:focus{outline:none;box-shadow:0 0 0 2px #3b82f64d}.field-error{margin-top:.5rem;font-size:.75rem;color:var(--login-error-color);font-family:DM Sans,sans-serif}.field-error:before{content:\"\\26a0 \";font-size:.75rem}.alert{padding:1rem;border-radius:8px;margin-bottom:1rem;display:flex;align-items:flex-start;gap:.75rem;font-size:.9rem}.alert.alert-error{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}.alert.alert-error i{color:var(--login-error-color);font-size:1.1rem;margin-top:.1rem}.alert.alert-warning{background:#fffbeb;border:1px solid #fed7aa;color:#92400e}.alert.alert-warning i{color:var(--login-warning-color);font-size:1.1rem;margin-top:.1rem}.lockout-message{margin-bottom:1.5rem}.lockout-message strong{display:block;margin-bottom:.5rem}.lockout-message p{margin:.25rem 0}.login-button{width:100%;padding:.375rem .75rem;background:var(--login-primary-color);color:#fff;border:none;border-radius:.25rem;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;cursor:pointer;transition:var(--login-transition);position:relative;display:flex;align-items:center;justify-content:center;gap:.5rem;line-height:1.5}.login-button:hover:not(:disabled){background:var(--login-primary-hover)}.login-button:focus{outline:none;box-shadow:0 0 0 .2rem #007bff40}.login-button:disabled{background:#6c757d;cursor:not-allowed}.login-button .loading-content{display:flex;align-items:center;gap:.5rem}.login-button .icon-spinner{animation:spin 1s linear infinite}.login-links{margin-top:1.5rem;text-align:center;display:flex;align-items:center;justify-content:center;gap:.75rem;flex-wrap:wrap}.login-links .link{color:var(--login-primary-color);text-decoration:none;font-size:.875rem;font-weight:500;transition:var(--login-transition)}.login-links .link:hover{color:var(--login-primary-hover);text-decoration:underline}.login-links .link:focus{outline:2px solid var(--login-primary-color);outline-offset:2px;border-radius:4px}.login-links .separator{color:var(--login-secondary-color);font-size:.875rem}.login-footer{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--login-border-color);text-align:center}.login-footer .footer-text{font-family:DM Sans,sans-serif;font-size:.75rem;color:var(--login-secondary-color);margin:0;font-weight:400}@media (max-width: 640px){.login-container{padding:.5rem}.login-card{padding:1.5rem}.login-header .login-title{font-size:1.5rem}.demo-buttons{grid-template-columns:1fr}.login-links{flex-direction:column;gap:.5rem}.login-links .separator{display:none}}@media (max-width: 480px){.form-input{padding:.75rem;font-size:.9rem}.login-button{padding:.8rem;font-size:.9rem}}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.login-card{animation:fadeIn .5s ease-out}@media (prefers-color-scheme: dark){:host{--login-background: #0f172a;--login-card-background: #1e293b;--login-border-color: #334155;--login-text-color: #f1f5f9;--login-secondary-color: #94a3b8}}@media (prefers-contrast: high){:host{--login-border-color: #000000;--login-text-color: #000000;--login-background: #ffffff}.form-input:focus{border-width:3px}}@media (prefers-reduced-motion: reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@media print{.login-container{min-height:auto;background:#fff}.demo-section,.loading-overlay{display:none}}\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: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.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: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }] });
|
|
2113
|
+
}
|
|
2114
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginComponent, decorators: [{
|
|
2115
|
+
type: Component,
|
|
2116
|
+
args: [{ selector: 'lib-login', standalone: true, imports: [CommonModule, ReactiveFormsModule], template: "<!-- Container principal do login -->\n<div class=\"login-container\" [ngClass]=\"customCssClass\"\n [style.background-image]=\"options.backgroundUrl ? 'url(' + options.backgroundUrl + ')' : null\">\n\n <!-- Card de login -->\n <div class=\"login-card\">\n\n <!-- Header -->\n <div class=\"login-header\">\n <img *ngIf=\"options.logoUrl\" [src]=\"options.logoUrl\" alt=\"Logo\" class=\"login-logo\">\n\n <h1 class=\"login-title\">{{ options.title || 'Entrar' }}</h1>\n <p class=\"login-subtitle\">{{ options.subtitle || 'Acesse sua conta' }}</p>\n </div>\n\n <!-- Indicador de loading -->\n <div *ngIf=\"isLoading$ | async\" class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n <span class=\"loading-text\">Entrando...</span>\n </div>\n\n <!-- Estado de bloqueio removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Formul\u00E1rio de login -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"onSubmit()\" class=\"login-form\"\n [class.disabled]=\"(isLoading$ | async)\">\n\n <!-- Campo Email -->\n <div class=\"form-group\">\n <label for=\"email\" class=\"form-label\">\n Email <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"email\" type=\"email\" formControlName=\"email\" class=\"form-input\"\n [class.error]=\"hasFieldError('email', 'required') || hasFieldError('email', 'email')\"\n placeholder=\"Digite seu email\" autocomplete=\"email\" autocapitalize=\"off\">\n\n <i class=\"input-icon icon-email\"></i>\n </div>\n\n <!-- Erro do campo email -->\n <div *ngIf=\"getFieldErrorMessage('email')\" class=\"field-error\">\n {{ getFieldErrorMessage('email') }}\n </div>\n </div>\n\n <!-- Campo Senha -->\n <div class=\"form-group\">\n <label for=\"password\" class=\"form-label\">\n Senha <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"password\" [type]=\"showPassword ? 'text' : 'password'\" formControlName=\"password\"\n class=\"form-input\"\n [class.error]=\"hasFieldError('password', 'required') || hasFieldError('password', 'minlength')\"\n placeholder=\"Digite sua senha\" autocomplete=\"current-password\">\n\n <button type=\"button\" class=\"password-toggle\" (click)=\"togglePasswordVisibility()\"\n [attr.aria-label]=\"showPassword ? 'Ocultar senha' : 'Mostrar senha'\">\n <i [class]=\"showPassword ? 'icon-eye-off' : 'icon-eye'\"></i>\n </button>\n </div>\n\n <!-- Erro do campo senha -->\n <div *ngIf=\"getFieldErrorMessage('password')\" class=\"field-error\">\n {{ getFieldErrorMessage('password') }}\n </div>\n </div>\n\n <!-- Checkbox removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Erros gerais -->\n <div *ngIf=\"(errors$ | async)?.general\" class=\"alert alert-error\">\n <i class=\"icon-error\"></i>\n <div>\n <div *ngFor=\"let error of (errors$ | async)?.general\">\n {{ error }}\n </div>\n </div>\n </div>\n\n <!-- Bot\u00E3o de submit -->\n <button type=\"submit\" class=\"login-button\" [disabled]=\"loginForm.invalid || (isLoading$ | async)\">\n\n <span *ngIf=\"!(isLoading$ | async)\">Entrar</span>\n <span *ngIf=\"isLoading$ | async\" class=\"loading-content\">\n <i class=\"icon-spinner spin\"></i>\n Entrando...\n </span>\n </button>\n\n <!-- Contador de tentativas removido conforme solicita\u00E7\u00E3o -->\n </form>\n\n <!-- Links adicionais -->\n <div class=\"login-links\">\n <a href=\"#\" (click)=\"onForgotPassword(); $event.preventDefault()\" class=\"link\">\n Esqueci minha senha\n </a>\n\n <span class=\"separator\">\u2022</span>\n\n <a href=\"#\" (click)=\"onCreateAccount(); $event.preventDefault()\" class=\"link\">\n Criar conta\n </a>\n </div>\n\n <!-- Se\u00E7\u00E3o de demonstra\u00E7\u00E3o removida conforme solicita\u00E7\u00E3o -->\n\n <!-- Footer -->\n <div *ngIf=\"options.footerText\" class=\"login-footer\">\n <p class=\"footer-text\">\n {{ options.footerText }}\n </p>\n </div>\n </div>\n</div>", styles: ["@charset \"UTF-8\";:host{--login-primary-color: #007bff;--login-primary-hover: #0056b3;--login-secondary-color: #6c757d;--login-background: #f8f9fa;--login-card-background: #ffffff;--login-border-color: #dee2e6;--login-text-color: #343a40;--login-error-color: #dc3545;--login-success-color: #28a745;--login-warning-color: #ffc107;--login-shadow: 0 10px 25px rgba(0, 0, 0, .1);--login-border-radius: .75rem;--login-transition: all .3s ease}.login-container{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1rem;background:var(--login-background);background-size:cover;background-position:center;background-repeat:no-repeat;position:relative;font-family:DM Sans,sans-serif}.login-container:before{content:\"\";position:absolute;inset:0;background:#0000004d;z-index:1;opacity:0;transition:opacity .3s ease}.login-container[style*=background-image]:before{opacity:1}.login-card{background:var(--login-card-background);border-radius:var(--login-border-radius);box-shadow:var(--login-shadow);padding:2rem;width:100%;max-width:420px;position:relative;border:1px solid var(--login-border-color);z-index:2}@media (max-width: 480px){.login-card{padding:1.5rem;margin:.5rem}}.login-header{text-align:center;margin-bottom:2rem}.login-header .login-logo{max-width:80px;height:auto;margin-bottom:1rem}.login-header .login-title{font-family:DM Sans,sans-serif;font-size:1.75rem;font-weight:700;color:var(--login-text-color);margin:0 0 .5rem;line-height:1.2}.login-header .login-subtitle{font-size:.875rem;color:var(--login-secondary-color);margin:0;font-weight:400}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:var(--login-border-radius);z-index:10}.loading-overlay .loading-spinner{width:40px;height:40px;border:3px solid var(--login-border-color);border-top:3px solid var(--login-primary-color);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:1rem}.loading-overlay .loading-text{color:var(--login-secondary-color);font-size:.9rem;font-weight:500}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-form.disabled{pointer-events:none;opacity:.7}.form-group{margin-bottom:1.5rem}.form-label{display:block;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;color:var(--login-text-color);margin-bottom:.5rem;line-height:2.5}.form-label .required{color:var(--login-error-color);margin-left:.25rem}.input-wrapper{position:relative;display:flex;align-items:center}.form-input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid var(--login-border-color);border-radius:.25rem;display:block;width:100%;padding:.375rem 2.75rem .375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;background-clip:padding-box;color:var(--login-text-color);transition:var(--login-transition);outline:none}.form-input::placeholder{color:#6c757d}.form-input:focus{border-color:#b2b9be;box-shadow:none}.form-input:focus.error{border-color:var(--login-error-color)}.form-input.error{border-color:var(--login-error-color)}.form-input:disabled{background:#f8f9fa;color:#6c757d;cursor:not-allowed}.input-icon{position:absolute;right:1rem;color:var(--login-secondary-color);font-size:1.1rem;pointer-events:none}.password-toggle{position:absolute;right:.75rem;background:none;border:none;padding:.25rem;cursor:pointer;color:var(--login-secondary-color);transition:var(--login-transition);border-radius:4px}.password-toggle:hover{color:var(--login-primary-color);background:#3b82f61a}.password-toggle:focus{outline:none;box-shadow:0 0 0 2px #3b82f64d}.field-error{margin-top:.5rem;font-size:.75rem;color:var(--login-error-color);font-family:DM Sans,sans-serif}.field-error:before{content:\"\\26a0 \";font-size:.75rem}.alert{padding:1rem;border-radius:8px;margin-bottom:1rem;display:flex;align-items:flex-start;gap:.75rem;font-size:.9rem}.alert.alert-error{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}.alert.alert-error i{color:var(--login-error-color);font-size:1.1rem;margin-top:.1rem}.alert.alert-warning{background:#fffbeb;border:1px solid #fed7aa;color:#92400e}.alert.alert-warning i{color:var(--login-warning-color);font-size:1.1rem;margin-top:.1rem}.lockout-message{margin-bottom:1.5rem}.lockout-message strong{display:block;margin-bottom:.5rem}.lockout-message p{margin:.25rem 0}.login-button{width:100%;padding:.375rem .75rem;background:var(--login-primary-color);color:#fff;border:none;border-radius:.25rem;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;cursor:pointer;transition:var(--login-transition);position:relative;display:flex;align-items:center;justify-content:center;gap:.5rem;line-height:1.5}.login-button:hover:not(:disabled){background:var(--login-primary-hover)}.login-button:focus{outline:none;box-shadow:0 0 0 .2rem #007bff40}.login-button:disabled{background:#6c757d;cursor:not-allowed}.login-button .loading-content{display:flex;align-items:center;gap:.5rem}.login-button .icon-spinner{animation:spin 1s linear infinite}.login-links{margin-top:1.5rem;text-align:center;display:flex;align-items:center;justify-content:center;gap:.75rem;flex-wrap:wrap}.login-links .link{color:var(--login-primary-color);text-decoration:none;font-size:.875rem;font-weight:500;transition:var(--login-transition)}.login-links .link:hover{color:var(--login-primary-hover);text-decoration:underline}.login-links .link:focus{outline:2px solid var(--login-primary-color);outline-offset:2px;border-radius:4px}.login-links .separator{color:var(--login-secondary-color);font-size:.875rem}.login-footer{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--login-border-color);text-align:center}.login-footer .footer-text{font-family:DM Sans,sans-serif;font-size:.75rem;color:var(--login-secondary-color);margin:0;font-weight:400}@media (max-width: 640px){.login-container{padding:.5rem}.login-card{padding:1.5rem}.login-header .login-title{font-size:1.5rem}.demo-buttons{grid-template-columns:1fr}.login-links{flex-direction:column;gap:.5rem}.login-links .separator{display:none}}@media (max-width: 480px){.form-input{padding:.75rem;font-size:.9rem}.login-button{padding:.8rem;font-size:.9rem}}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.login-card{animation:fadeIn .5s ease-out}@media (prefers-color-scheme: dark){:host{--login-background: #0f172a;--login-card-background: #1e293b;--login-border-color: #334155;--login-text-color: #f1f5f9;--login-secondary-color: #94a3b8}}@media (prefers-contrast: high){:host{--login-border-color: #000000;--login-text-color: #000000;--login-background: #ffffff}.form-input:focus{border-width:3px}}@media (prefers-reduced-motion: reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@media print{.login-container{min-height:auto;background:#fff}.demo-section,.loading-overlay{display:none}}\n"] }]
|
|
2117
|
+
}], ctorParameters: () => [{ type: LoginService }], propDecorators: { options: [{
|
|
2118
|
+
type: Input
|
|
2119
|
+
}], showHeader: [{
|
|
2120
|
+
type: Input
|
|
2121
|
+
}], showFooter: [{
|
|
2122
|
+
type: Input
|
|
2123
|
+
}], logoUrl: [{
|
|
2124
|
+
type: Input
|
|
2125
|
+
}], backgroundImageUrl: [{
|
|
2126
|
+
type: Input
|
|
2127
|
+
}], customCssClass: [{
|
|
2128
|
+
type: Input
|
|
2129
|
+
}], loginSuccess: [{
|
|
2130
|
+
type: Output
|
|
2131
|
+
}], loginError: [{
|
|
2132
|
+
type: Output
|
|
2133
|
+
}], formChange: [{
|
|
2134
|
+
type: Output
|
|
2135
|
+
}] } });
|
|
2136
|
+
|
|
2137
|
+
/**
|
|
2138
|
+
* Interceptor HTTP para autenticação automática
|
|
2139
|
+
* Adiciona o Access Token nas requisições e trata erros 401
|
|
2140
|
+
*/
|
|
2141
|
+
class AuthInterceptor {
|
|
2142
|
+
authStorage;
|
|
2143
|
+
constructor(authStorage) {
|
|
2144
|
+
this.authStorage = authStorage;
|
|
2145
|
+
}
|
|
2146
|
+
intercept(req, next) {
|
|
2147
|
+
// Converte para Observable para trabalhar com async
|
|
2148
|
+
return from(this.authStorage.getToken()).pipe(switchMap(token => {
|
|
2149
|
+
// Clona a requisição e adiciona o token se disponível
|
|
2150
|
+
let authReq = req;
|
|
2151
|
+
if (token) {
|
|
2152
|
+
authReq = req.clone({
|
|
2153
|
+
setHeaders: {
|
|
2154
|
+
Authorization: `Bearer ${token}`
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
// Processa a requisição
|
|
2159
|
+
return next.handle(authReq).pipe(catchError((error) => {
|
|
2160
|
+
// Se receber 401, limpa os dados de autenticação
|
|
2161
|
+
// O backend já fez o refresh automaticamente, se falhou é porque não tem mais sessão válida
|
|
2162
|
+
if (error.status === 401) {
|
|
2163
|
+
this.authStorage.clearAuthData();
|
|
2164
|
+
}
|
|
2165
|
+
// Re-propaga o erro para ser tratado pelos componentes
|
|
2166
|
+
return throwError(() => error);
|
|
2167
|
+
}));
|
|
2168
|
+
}));
|
|
2169
|
+
}
|
|
2170
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthInterceptor, deps: [{ token: AuthStorageService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2171
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthInterceptor });
|
|
2172
|
+
}
|
|
2173
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthInterceptor, decorators: [{
|
|
2174
|
+
type: Injectable
|
|
2175
|
+
}], ctorParameters: () => [{ type: AuthStorageService }] });
|
|
2176
|
+
|
|
2177
|
+
// Export dos interceptors para uso público
|
|
2178
|
+
|
|
2179
|
+
/*
|
|
2180
|
+
* Public API Surface of shared-components
|
|
2181
|
+
*/
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Generated bundle index. Do not edit.
|
|
2185
|
+
*/
|
|
2186
|
+
|
|
2187
|
+
export { AUTH_PROVIDER, AuthInterceptor, AuthStorageService, BreadcrumbComponent, ButtonComponent, CheckboxComponent, DropdownComponent, InputComponent, LocalStorageService, LoginComponent, LoginService, LoginStatus, SharedComponentsComponent, SharedComponentsService, SidebarComponent, TableComponent, ToggleComponent };
|
|
2188
|
+
//# sourceMappingURL=ramonbsales-noah-angular.mjs.map
|