Docker aplicado
This commit is contained in:
parent
49cdaefddf
commit
99807d78f7
|
|
@ -0,0 +1,10 @@
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.vscode
|
||||||
|
.angular
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
out-tsc
|
||||||
|
coverage
|
||||||
|
*.log
|
||||||
|
README.md
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=4000
|
||||||
|
ENV API_BASE_URL=http://backend:8080
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
|
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
CMD ["node", "dist/line-gestao-frontend/server/server.mjs"]
|
||||||
|
|
@ -44,13 +44,13 @@
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "500kB",
|
"maximumWarning": "2MB",
|
||||||
"maximumError": "1MB"
|
"maximumError": "3MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "4kB",
|
"maximumWarning": "20kB",
|
||||||
"maximumError": "8kB"
|
"maximumError": "40kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
|
|
@ -23,14 +23,14 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "^20.3.0",
|
"@angular/common": "20.3.16",
|
||||||
"@angular/compiler": "^20.3.0",
|
"@angular/compiler": "20.3.16",
|
||||||
"@angular/core": "^20.3.0",
|
"@angular/core": "20.3.16",
|
||||||
"@angular/forms": "^20.3.0",
|
"@angular/forms": "20.3.16",
|
||||||
"@angular/platform-browser": "^20.3.0",
|
"@angular/platform-browser": "20.3.16",
|
||||||
"@angular/platform-server": "^20.3.0",
|
"@angular/platform-server": "20.3.16",
|
||||||
"@angular/router": "^20.3.0",
|
"@angular/router": "20.3.16",
|
||||||
"@angular/ssr": "^20.3.10",
|
"@angular/ssr": "20.3.16",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
|
@ -41,9 +41,9 @@
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.3.10",
|
"@angular/build": "20.3.16",
|
||||||
"@angular/cli": "^20.3.10",
|
"@angular/cli": "20.3.16",
|
||||||
"@angular/compiler-cli": "^20.3.0",
|
"@angular/compiler-cli": "20.3.16",
|
||||||
"@types/bootstrap": "^5.2.10",
|
"@types/bootstrap": "^5.2.10",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
|
|
@ -55,5 +55,8 @@
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"qs": "^6.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@ import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||||
export const serverRoutes: ServerRoute[] = [
|
export const serverRoutes: ServerRoute[] = [
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
renderMode: RenderMode.Prerender
|
renderMode: RenderMode.Server
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import { VigenciaComponent } from './pages/vigencia/vigencia';
|
||||||
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
||||||
import { Dashboard } from './pages/dashboard/dashboard';
|
import { Dashboard } from './pages/dashboard/dashboard';
|
||||||
import { Notificacoes } from './pages/notificacoes/notificacoes';
|
import { Notificacoes } from './pages/notificacoes/notificacoes';
|
||||||
import { NovoUsuario } from './pages/novo-usuario/novo-usuario';
|
|
||||||
import { ChipsControleRecebidos } from './pages/chips-controle-recebidos/chips-controle-recebidos';
|
import { ChipsControleRecebidos } from './pages/chips-controle-recebidos/chips-controle-recebidos';
|
||||||
import { Resumo } from './pages/resumo/resumo';
|
import { Resumo } from './pages/resumo/resumo';
|
||||||
import { Parcelamentos } from './pages/parcelamentos/parcelamentos';
|
import { Parcelamentos } from './pages/parcelamentos/parcelamentos';
|
||||||
|
|
@ -33,7 +32,6 @@ export const routes: Routes = [
|
||||||
{ path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' },
|
{ path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' },
|
||||||
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' },
|
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' },
|
||||||
{ path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' },
|
{ path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' },
|
||||||
{ path: 'novo-usuario', component: NovoUsuario, canActivate: [authGuard], title: 'Novo Usuário' },
|
|
||||||
{ path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard], title: 'Chips Controle Recebidos' },
|
{ path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard], title: 'Chips Controle Recebidos' },
|
||||||
{ path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' },
|
{ path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' },
|
||||||
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard], title: 'Parcelamentos' },
|
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard], title: 'Parcelamentos' },
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
import { App } from './app';
|
import { App } from './app';
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [App],
|
imports: [App],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -14,10 +22,10 @@ describe('App', () => {
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render title', () => {
|
it('should render app layout', () => {
|
||||||
const fixture = TestBed.createComponent(App);
|
const fixture = TestBed.createComponent(App);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, line-gestao-frontend');
|
expect(compiled.querySelector('main.app-main')).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
|
|
||||||
|
|
@ -8,7 +11,12 @@ describe('Header', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [Header]
|
imports: [Header],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ export class Header {
|
||||||
'/trocanumero',
|
'/trocanumero',
|
||||||
'/dashboard',
|
'/dashboard',
|
||||||
'/notificacoes',
|
'/notificacoes',
|
||||||
'/novo-usuario',
|
|
||||||
'/chips-controle-recebidos',
|
'/chips-controle-recebidos',
|
||||||
'/resumo',
|
'/resumo',
|
||||||
'/parcelamentos',
|
'/parcelamentos',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
import { DadosUsuarios } from './dados-usuarios';
|
import { DadosUsuarios } from './dados-usuarios';
|
||||||
|
|
||||||
describe('DadosUsuarios', () => {
|
describe('DadosUsuarios', () => {
|
||||||
|
|
@ -8,6 +11,11 @@ describe('DadosUsuarios', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [DadosUsuarios], // standalone component
|
imports: [DadosUsuarios], // standalone component
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DadosUsuarios);
|
fixture = TestBed.createComponent(DadosUsuarios);
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private resumoService: ResumoService,
|
private resumoService: ResumoService,
|
||||||
@Inject(PLATFORM_ID) private platformId: object
|
@Inject(PLATFORM_ID) private platformId: object
|
||||||
) {
|
) {
|
||||||
const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, '');
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { Geral } from './geral';
|
import { Geral } from './geral';
|
||||||
|
|
||||||
|
|
@ -8,7 +11,12 @@ describe('Geral', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [Geral]
|
imports: [Geral],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { CustomSelectComponent } from '../../components/custom-select/custom-sel
|
||||||
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
type SortDir = 'asc' | 'desc';
|
type SortDir = 'asc' | 'desc';
|
||||||
type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP';
|
type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP';
|
||||||
|
|
@ -146,7 +147,11 @@ export class Geral implements AfterViewInit, OnDestroy {
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly apiBase = 'https://localhost:7205/api/lines';
|
private readonly apiBase = (() => {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
return `${apiBase}/lines`;
|
||||||
|
})();
|
||||||
loading = false;
|
loading = false;
|
||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { LinesService } from '../../services/lines.service';
|
import { LinesService } from '../../services/lines.service';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente';
|
type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente';
|
||||||
|
|
||||||
|
|
@ -92,7 +93,11 @@ export class Mureg implements AfterViewInit {
|
||||||
private linesService: LinesService
|
private linesService: LinesService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly apiBase = 'https://localhost:7205/api/mureg';
|
private readonly apiBase = (() => {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
return `${apiBase}/mureg`;
|
||||||
|
})();
|
||||||
|
|
||||||
// ====== DATA ======
|
// ====== DATA ======
|
||||||
clientGroups: ClientGroup[] = [];
|
clientGroups: ClientGroup[] = [];
|
||||||
|
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
<section class="create-user-page">
|
|
||||||
<div class="page-shell">
|
|
||||||
<div class="grid-shell">
|
|
||||||
<div class="form-card">
|
|
||||||
<div class="form-header">
|
|
||||||
<h1>Novo Usuário LineGestão</h1>
|
|
||||||
<p>Preencha os dados para criar um novo usuário.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-alert error" *ngIf="createErrors.length">
|
|
||||||
<strong>Confira os campos:</strong>
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let err of createErrors">{{ err.message }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="form-alert success" *ngIf="createSuccess">{{ createSuccess }}</div>
|
|
||||||
|
|
||||||
<form class="user-form" [formGroup]="createForm" (ngSubmit)="submitCreate()">
|
|
||||||
<div class="form-field" [class.has-error]="hasCreateFieldError('nome') || (createForm.get('nome')?.touched && createForm.get('nome')?.invalid)">
|
|
||||||
<label for="nome">Nome</label>
|
|
||||||
<input id="nome" type="text" placeholder="Nome completo" formControlName="nome" />
|
|
||||||
<small class="field-error" *ngIf="createForm.get('nome')?.touched && createForm.get('nome')?.invalid">Nome obrigatório.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field" [class.has-error]="hasCreateFieldError('email') || (createForm.get('email')?.touched && createForm.get('email')?.invalid)">
|
|
||||||
<label for="email">Email</label>
|
|
||||||
<input id="email" type="email" placeholder="nome@empresa.com" formControlName="email" />
|
|
||||||
<small class="field-error" *ngIf="createForm.get('email')?.touched && createForm.get('email')?.invalid">Email inválido.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field" [class.has-error]="hasCreateFieldError('senha') || (createForm.get('senha')?.touched && createForm.get('senha')?.invalid)">
|
|
||||||
<label for="senha">Senha</label>
|
|
||||||
<input id="senha" type="password" placeholder="Defina uma senha segura" formControlName="senha" />
|
|
||||||
<small class="field-error" *ngIf="createForm.get('senha')?.touched && createForm.get('senha')?.invalid">Senha inválida.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field" [class.has-error]="hasCreateFieldError('confirmarSenha') || (createForm.get('confirmarSenha')?.touched && createForm.get('confirmarSenha')?.invalid) || (createPasswordMismatch && createForm.get('confirmarSenha')?.touched)">
|
|
||||||
<label for="confirmarSenha">Confirmar Senha</label>
|
|
||||||
<input id="confirmarSenha" type="password" placeholder="Repita a senha" formControlName="confirmarSenha" />
|
|
||||||
<small class="field-error" *ngIf="createPasswordMismatch && createForm.get('confirmarSenha')?.touched">As senhas não conferem.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field" [class.has-error]="hasCreateFieldError('permissao') || (createForm.get('permissao')?.touched && createForm.get('permissao')?.invalid)">
|
|
||||||
<label for="permissoes">Permissões</label>
|
|
||||||
<app-select formControlName="permissao" [options]="permissionOptions" labelKey="label" valueKey="value" placeholder="Selecione o nivel"></app-select>
|
|
||||||
<small class="field-error" *ngIf="createForm.get('permissao')?.touched && createForm.get('permissao')?.invalid">Selecione uma permissão.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="button" class="btn-secondary" (click)="createForm.reset({ permissao: '' })" [disabled]="createSubmitting">Cancelar</button>
|
|
||||||
<button type="submit" class="btn-primary" [disabled]="createSubmitting || createForm.invalid">
|
|
||||||
<span *ngIf="!createSubmitting">Salvar</span>
|
|
||||||
<span *ngIf="createSubmitting">Salvando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="list-card">
|
|
||||||
<div class="list-header">
|
|
||||||
<div>
|
|
||||||
<h2>Usuários</h2>
|
|
||||||
<p>Gerencie permissões e status.</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-actions">
|
|
||||||
<input type="text" placeholder="Pesquisar..." [(ngModel)]="search" (keyup.enter)="onSearch()" />
|
|
||||||
<button type="button" class="btn-secondary" (click)="onSearch()">Buscar</button>
|
|
||||||
<button type="button" class="btn-ghost" (click)="clearSearch()">Limpar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="list-body">
|
|
||||||
<div class="loading" *ngIf="loading">Carregando...</div>
|
|
||||||
<table *ngIf="!loading && users.length" class="users-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Nome</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Permissão</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Ações</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let u of users">
|
|
||||||
<td>{{ u.nome }}</td>
|
|
||||||
<td>{{ u.email }}</td>
|
|
||||||
<td class="cap">{{ u.permissao }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="status-pill" [class.off]="u.ativo === false">
|
|
||||||
{{ u.ativo === false ? 'Inativo' : 'Ativo' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" class="btn-link" (click)="openEdit(u)">Editar</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="empty" *ngIf="!loading && !users.length">
|
|
||||||
Nenhum usuario encontrado.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="list-footer">
|
|
||||||
<div class="page-info">
|
|
||||||
Página {{ page }} de {{ totalPages }}
|
|
||||||
</div>
|
|
||||||
<div class="pagination">
|
|
||||||
<button type="button" class="btn-ghost" (click)="goToPage(page - 1)" [disabled]="page <= 1">Anterior</button>
|
|
||||||
<button type="button" class="btn-ghost" (click)="goToPage(p)" *ngFor="let p of pageNumbers" [class.active]="p === page">{{ p }}</button>
|
|
||||||
<button type="button" class="btn-ghost" (click)="goToPage(page + 1)" [disabled]="page >= totalPages">Próxima</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="modal-overlay" *ngIf="editOpen" (click)="closeEdit()"></div>
|
|
||||||
<div class="modal-card" *ngIf="editOpen" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Editar Usuário</h3>
|
|
||||||
<button type="button" class="btn-icon close-x" (click)="closeEdit()" aria-label="Fechar">
|
|
||||||
<i class="bi bi-x-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-alert error" *ngIf="editErrors.length">
|
|
||||||
<strong>Confira os campos:</strong>
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let err of editErrors">{{ err.message }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="form-alert success" *ngIf="editSuccess">{{ editSuccess }}</div>
|
|
||||||
|
|
||||||
<form class="user-form" id="editUserForm" [formGroup]="editForm" (ngSubmit)="submitEdit()">
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="editNome">Nome</label>
|
|
||||||
<input id="editNome" type="text" placeholder="Nome completo" formControlName="nome" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="editEmail">Email</label>
|
|
||||||
<input id="editEmail" type="email" placeholder="nome@empresa.com" formControlName="email" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="editSenha">Nova senha (opcional)</label>
|
|
||||||
<input id="editSenha" type="password" placeholder="Nova senha" formControlName="senha" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="editConfirmarSenha">Confirmar senha</label>
|
|
||||||
<input id="editConfirmarSenha" type="password" placeholder="Confirme a nova senha" formControlName="confirmarSenha" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="editPermissao">Permissões</label>
|
|
||||||
<app-select formControlName="permissao" [options]="permissionOptions" labelKey="label" valueKey="value" placeholder="Selecione o nivel"></app-select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field inline">
|
|
||||||
<label for="editAtivo">Status</label>
|
|
||||||
<div class="toggle">
|
|
||||||
<input id="editAtivo" type="checkbox" formControlName="ativo" />
|
|
||||||
<span>Ativo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" class="btn-secondary" (click)="closeEdit()" [disabled]="editSubmitting">Cancelar</button>
|
|
||||||
<button type="submit" form="editUserForm" class="btn-primary" [disabled]="editSubmitting">
|
|
||||||
<span *ngIf="!editSubmitting">Salvar</span>
|
|
||||||
<span *ngIf="editSubmitting">Salvando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
/* Página Criar Novo Usuário */
|
|
||||||
.create-user-page {
|
|
||||||
min-height: calc(100vh - 69.2px);
|
|
||||||
padding: 32px 16px 80px;
|
|
||||||
background: radial-gradient(circle at 10% 15%, #e8f0ff 0%, #f6f7fb 45%, #ffffff 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-shell {
|
|
||||||
max-width: 980px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-shell {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
gap: 24px;
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-card {
|
|
||||||
width: min(720px, 100%);
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
|
|
||||||
padding: 28px 28px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-header {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
margin: 0 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-form {
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select {
|
|
||||||
height: 42px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1.5px solid #d7dbe6;
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #0f172a;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus,
|
|
||||||
select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b5bff;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 91, 255, 0.15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field.inline {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-alert {
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-alert.error {
|
|
||||||
background: rgba(239, 68, 68, 0.08);
|
|
||||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-alert.success {
|
|
||||||
background: rgba(16, 185, 129, 0.08);
|
|
||||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
||||||
color: #047857;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-error {
|
|
||||||
color: #b91c1c;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-card {
|
|
||||||
width: min(900px, 100%);
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
|
|
||||||
padding: 22px 22px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-header {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
input {
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1.5px solid #d7dbe6;
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-body {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.users-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
padding: 10px 8px;
|
|
||||||
border-bottom: 1px solid #edf0f6;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(16, 185, 129, 0.12);
|
|
||||||
color: #047857;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pill.off {
|
|
||||||
background: rgba(239, 68, 68, 0.12);
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-footer {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid #d7dbe6;
|
|
||||||
background: #fff;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost.active {
|
|
||||||
background: #2f6bff;
|
|
||||||
border-color: #2f6bff;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-link {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: #2f6bff;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading,
|
|
||||||
.empty {
|
|
||||||
padding: 18px 0;
|
|
||||||
text-align: center;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cap {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary,
|
|
||||||
.btn-secondary {
|
|
||||||
height: 40px;
|
|
||||||
min-width: 110px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: none;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: #2f6bff;
|
|
||||||
color: #ffffff;
|
|
||||||
box-shadow: 0 10px 20px rgba(47, 107, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary,
|
|
||||||
.btn-ghost {
|
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover,
|
|
||||||
.btn-secondary:hover,
|
|
||||||
.btn-ghost:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(15, 23, 42, 0.4);
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-card {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: min(520px, 92vw);
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 18px 10px;
|
|
||||||
border-bottom: 1px solid #edf0f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 16px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0 18px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.grid-shell {
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-card {
|
|
||||||
padding: 22px 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-card {
|
|
||||||
padding: 20px 18px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary,
|
|
||||||
.btn-secondary {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 980px) {
|
|
||||||
.grid-shell {
|
|
||||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-card,
|
|
||||||
.list-card {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
|
|
||||||
import { UsersService, CreateUserPayload, UpdateUserPayload, UserDto, ApiFieldError } from '../../services/users.service';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-novo-usuario',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, ReactiveFormsModule, FormsModule, CustomSelectComponent],
|
|
||||||
templateUrl: './novo-usuario.html',
|
|
||||||
styleUrls: ['./novo-usuario.scss'],
|
|
||||||
})
|
|
||||||
export class NovoUsuario implements OnInit {
|
|
||||||
createForm: FormGroup;
|
|
||||||
editForm: FormGroup;
|
|
||||||
|
|
||||||
permissionOptions = [
|
|
||||||
{ value: 'admin', label: 'Administrador' },
|
|
||||||
{ value: 'gestor', label: 'Gestor' },
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
createSubmitting = false;
|
|
||||||
editSubmitting = false;
|
|
||||||
|
|
||||||
createErrors: ApiFieldError[] = [];
|
|
||||||
editErrors: ApiFieldError[] = [];
|
|
||||||
|
|
||||||
createSuccess = '';
|
|
||||||
editSuccess = '';
|
|
||||||
|
|
||||||
users: UserDto[] = [];
|
|
||||||
loading = false;
|
|
||||||
|
|
||||||
search = '';
|
|
||||||
page = 1;
|
|
||||||
pageSize = 10;
|
|
||||||
total = 0;
|
|
||||||
|
|
||||||
editOpen = false;
|
|
||||||
private editBase: UserDto | null = null;
|
|
||||||
|
|
||||||
constructor(private usersService: UsersService, private fb: FormBuilder) {
|
|
||||||
this.createForm = this.fb.group(
|
|
||||||
{
|
|
||||||
nome: ['', [Validators.required, Validators.minLength(2)]],
|
|
||||||
email: ['', [Validators.required, Validators.email]],
|
|
||||||
senha: ['', [Validators.required, Validators.minLength(6)]],
|
|
||||||
confirmarSenha: ['', [Validators.required, Validators.minLength(6)]],
|
|
||||||
permissao: ['', [Validators.required]],
|
|
||||||
},
|
|
||||||
{ validators: this.passwordsMatchValidator }
|
|
||||||
);
|
|
||||||
|
|
||||||
this.editForm = this.fb.group(
|
|
||||||
{
|
|
||||||
nome: [''],
|
|
||||||
email: [''],
|
|
||||||
senha: [''],
|
|
||||||
confirmarSenha: [''],
|
|
||||||
permissao: [''],
|
|
||||||
ativo: [true],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.fetchUsers(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalPages(): number {
|
|
||||||
return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10)));
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
|
||||||
const total = this.totalPages;
|
|
||||||
const current = this.page;
|
|
||||||
const max = 5;
|
|
||||||
let start = Math.max(1, current - 2);
|
|
||||||
let end = Math.min(total, start + (max - 1));
|
|
||||||
start = Math.max(1, end - (max - 1));
|
|
||||||
const pages: number[] = [];
|
|
||||||
for (let i = start; i <= end; i++) pages.push(i);
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchUsers(goToPage?: number) {
|
|
||||||
if (goToPage) this.page = goToPage;
|
|
||||||
this.loading = true;
|
|
||||||
this.usersService.list({
|
|
||||||
search: this.search?.trim() || undefined,
|
|
||||||
page: this.page,
|
|
||||||
pageSize: this.pageSize,
|
|
||||||
}).subscribe({
|
|
||||||
next: (res) => {
|
|
||||||
this.users = res.items || [];
|
|
||||||
this.total = res.total || 0;
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.users = [];
|
|
||||||
this.total = 0;
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch() {
|
|
||||||
this.page = 1;
|
|
||||||
this.fetchUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSearch() {
|
|
||||||
this.search = '';
|
|
||||||
this.page = 1;
|
|
||||||
this.fetchUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageSizeChange() {
|
|
||||||
this.page = 1;
|
|
||||||
this.fetchUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
goToPage(p: number) {
|
|
||||||
this.page = p;
|
|
||||||
this.fetchUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
submitCreate() {
|
|
||||||
if (this.createSubmitting) return;
|
|
||||||
if (this.createForm.invalid) {
|
|
||||||
this.createForm.markAllAsTouched();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createSubmitting = true;
|
|
||||||
this.setCreateFormDisabled(true);
|
|
||||||
this.createErrors = [];
|
|
||||||
this.createSuccess = '';
|
|
||||||
|
|
||||||
const payload = this.createForm.value as CreateUserPayload;
|
|
||||||
this.usersService.create(payload).subscribe({
|
|
||||||
next: (created) => {
|
|
||||||
this.createSubmitting = false;
|
|
||||||
this.setCreateFormDisabled(false);
|
|
||||||
this.createSuccess = `Usuario ${created.nome} criado com sucesso.`;
|
|
||||||
this.createForm.reset({ permissao: '' });
|
|
||||||
this.fetchUsers(1);
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this.createSubmitting = false;
|
|
||||||
this.setCreateFormDisabled(false);
|
|
||||||
const apiErrors = err?.error?.errors;
|
|
||||||
if (Array.isArray(apiErrors)) {
|
|
||||||
this.createErrors = apiErrors.map((e: any) => ({
|
|
||||||
field: e?.field,
|
|
||||||
message: e?.message || 'Erro ao criar usuario.',
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
this.createErrors = [{ message: err?.error?.message || 'Erro ao criar usuario.' }];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
openEdit(user: UserDto) {
|
|
||||||
this.editOpen = true;
|
|
||||||
this.editErrors = [];
|
|
||||||
this.editSuccess = '';
|
|
||||||
this.editSubmitting = false;
|
|
||||||
this.setEditFormDisabled(false);
|
|
||||||
this.editBase = null;
|
|
||||||
this.editForm.reset({ nome: '', email: '', senha: '', confirmarSenha: '', permissao: '', ativo: true });
|
|
||||||
|
|
||||||
this.usersService.getById(user.id).subscribe({
|
|
||||||
next: (full) => {
|
|
||||||
this.editBase = full;
|
|
||||||
this.editForm.reset({
|
|
||||||
nome: full.nome ?? '',
|
|
||||||
email: full.email ?? '',
|
|
||||||
senha: '',
|
|
||||||
confirmarSenha: '',
|
|
||||||
permissao: full.permissao ?? '',
|
|
||||||
ativo: full.ativo ?? true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.editErrors = [{ message: 'Erro ao carregar usuario.' }];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
closeEdit() {
|
|
||||||
this.editOpen = false;
|
|
||||||
this.editErrors = [];
|
|
||||||
this.editSuccess = '';
|
|
||||||
this.editSubmitting = false;
|
|
||||||
this.editBase = null;
|
|
||||||
this.setEditFormDisabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
submitEdit() {
|
|
||||||
if (this.editSubmitting || !this.editBase) return;
|
|
||||||
this.editErrors = [];
|
|
||||||
this.editSuccess = '';
|
|
||||||
|
|
||||||
const payload: UpdateUserPayload = {};
|
|
||||||
const nome = (this.editForm.get('nome')?.value || '').toString().trim();
|
|
||||||
const email = (this.editForm.get('email')?.value || '').toString().trim();
|
|
||||||
const permissao = (this.editForm.get('permissao')?.value || '').toString().trim();
|
|
||||||
const ativo = !!this.editForm.get('ativo')?.value;
|
|
||||||
|
|
||||||
if (nome && nome !== (this.editBase.nome || '').trim()) payload.nome = nome;
|
|
||||||
if (email && email !== (this.editBase.email || '').trim()) payload.email = email;
|
|
||||||
if (permissao && permissao !== (this.editBase.permissao || '').trim()) payload.permissao = permissao as any;
|
|
||||||
if ((this.editBase.ativo ?? true) !== ativo) payload.ativo = ativo;
|
|
||||||
|
|
||||||
const senha = (this.editForm.get('senha')?.value || '').toString();
|
|
||||||
const confirmar = (this.editForm.get('confirmarSenha')?.value || '').toString();
|
|
||||||
if (senha || confirmar) {
|
|
||||||
if (!senha || !confirmar) {
|
|
||||||
this.editErrors = [{ message: 'Para alterar a senha, preencha senha e confirmaçao.' }];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (senha.length < 6) {
|
|
||||||
this.editErrors = [{ message: 'Senha deve ter no minimo 6 caracteres.' }];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (senha !== confirmar) {
|
|
||||||
this.editErrors = [{ message: 'As senhas nao conferem.' }];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload.senha = senha;
|
|
||||||
payload.confirmarSenha = confirmar;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(payload).length === 0) {
|
|
||||||
this.editErrors = [{ message: 'Nenhuma alteraçao detectada.' }];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editSubmitting = true;
|
|
||||||
this.setEditFormDisabled(true);
|
|
||||||
this.usersService.update(this.editBase.id, payload).subscribe({
|
|
||||||
next: (updated) => {
|
|
||||||
this.editSubmitting = false;
|
|
||||||
this.setEditFormDisabled(false);
|
|
||||||
this.editSuccess = `Usuario ${updated.nome} atualizado.`;
|
|
||||||
this.fetchUsers();
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this.editSubmitting = false;
|
|
||||||
this.setEditFormDisabled(false);
|
|
||||||
const apiErrors = err?.error?.errors;
|
|
||||||
if (Array.isArray(apiErrors)) {
|
|
||||||
this.editErrors = apiErrors.map((e: any) => ({
|
|
||||||
field: e?.field,
|
|
||||||
message: e?.message || 'Erro ao atualizar usuario.',
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
this.editErrors = [{ message: err?.error?.message || 'Erro ao atualizar usuario.' }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hasCreateFieldError(field: string): boolean {
|
|
||||||
return this.getFieldErrors(this.createErrors, field).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEditFieldError(field: string): boolean {
|
|
||||||
return this.getFieldErrors(this.editErrors, field).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldErrors(source: ApiFieldError[], field: string): string[] {
|
|
||||||
const key = this.normalizeField(field);
|
|
||||||
return source
|
|
||||||
.filter((e) => this.normalizeField(e.field) === key)
|
|
||||||
.map((e) => e.message || 'Erro');
|
|
||||||
}
|
|
||||||
|
|
||||||
get createPasswordMismatch(): boolean {
|
|
||||||
return !!this.createForm.errors?.['passwordsMismatch'];
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeField(field?: string | null): string {
|
|
||||||
return (field || '').trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {
|
|
||||||
const senha = group.get('senha')?.value;
|
|
||||||
const confirmar = group.get('confirmarSenha')?.value;
|
|
||||||
if (!senha || !confirmar) return null;
|
|
||||||
return senha === confirmar ? null : { passwordsMismatch: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCreateFormDisabled(disabled: boolean) {
|
|
||||||
if (disabled) this.createForm.disable({ emitEvent: false });
|
|
||||||
else this.createForm.enable({ emitEvent: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
private setEditFormDisabled(disabled: boolean) {
|
|
||||||
if (disabled) this.editForm.disable({ emitEvent: false });
|
|
||||||
else this.editForm.enable({ emitEvent: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { Register } from './register';
|
import { Register } from './register';
|
||||||
|
|
||||||
|
|
@ -8,7 +11,12 @@ describe('Register', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [Register]
|
imports: [Register],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao';
|
type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao';
|
||||||
|
|
||||||
|
|
@ -71,10 +72,18 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
private cdr: ChangeDetectorRef
|
private cdr: ChangeDetectorRef
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly apiBase = 'https://localhost:7205/api/trocanumero';
|
private readonly apiBase = (() => {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
return `${apiBase}/trocanumero`;
|
||||||
|
})();
|
||||||
|
|
||||||
/** ✅ base do GERAL (para buscar clientes/linhas no modal) */
|
/** ✅ base do GERAL (para buscar clientes/linhas no modal) */
|
||||||
private readonly linesApiBase = 'https://localhost:7205/api/lines';
|
private readonly linesApiBase = (() => {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
return `${apiBase}/lines`;
|
||||||
|
})();
|
||||||
|
|
||||||
// ====== DATA ======
|
// ====== DATA ======
|
||||||
groups: GroupItem[] = [];
|
groups: GroupItem[] = [];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
export type SortDir = 'asc' | 'desc';
|
export type SortDir = 'asc' | 'desc';
|
||||||
export type TipoCliente = 'PF' | 'PJ';
|
export type TipoCliente = 'PF' | 'PJ';
|
||||||
|
|
@ -71,9 +72,13 @@ export interface ApiPagedResult<T> {
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class BillingService {
|
export class BillingService {
|
||||||
private readonly baseUrl = 'https://localhost:7205/api/billing';
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
this.baseUrl = `${apiBase}/billing`;
|
||||||
|
}
|
||||||
|
|
||||||
getPaged(q: BillingQuery): Observable<ApiPagedResult<BillingItem>> {
|
getPaged(q: BillingQuery): Observable<ApiPagedResult<BillingItem>> {
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export class ChipsControleService {
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, '');
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export class DadosUsuariosService {
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, '');
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
export interface PagedResult<T> {
|
export interface PagedResult<T> {
|
||||||
page: number;
|
page: number;
|
||||||
|
|
@ -58,10 +59,13 @@ export interface LineOption {
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LinesService {
|
export class LinesService {
|
||||||
// ✅ Mesma base do Swagger (evita redirect no preflight/CORS)
|
private readonly baseUrl: string;
|
||||||
private baseUrl = 'https://localhost:7205/api/lines';
|
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
this.baseUrl = `${apiBase}/lines`;
|
||||||
|
}
|
||||||
|
|
||||||
getLines(page: number, pageSize: number, search: string): Observable<PagedResult<MobileLineList>> {
|
getLines(page: number, pageSize: number, search: string): Observable<PagedResult<MobileLineList>> {
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ export class ResumoService {
|
||||||
private readonly apiBase: string;
|
private readonly apiBase: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, '');
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
this.apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
this.apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiUrl: 'https://localhost:7205'
|
apiUrl: ''
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,60 @@ import {
|
||||||
} from '@angular/ssr/node';
|
} from '@angular/ssr/node';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
const browserDistFolder = join(import.meta.dirname, '../browser');
|
const browserDistFolder = join(import.meta.dirname, '../browser');
|
||||||
|
const apiBaseUrl = (process.env['API_BASE_URL'] || 'http://backend:8080').replace(/\/+$/, '');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const angularApp = new AngularNodeAppEngine();
|
const angularApp = new AngularNodeAppEngine();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example Express Rest API endpoints can be defined here.
|
* Proxy API calls from the browser-facing frontend server to the backend API.
|
||||||
* Uncomment and define endpoints as necessary.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* ```ts
|
|
||||||
* app.get('/api/{*splat}', (req, res) => {
|
|
||||||
* // Handle API request
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
app.use(['/api', '/auth'], async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const targetUrl = new URL(req.originalUrl, `${apiBaseUrl}/`);
|
||||||
|
const headers = new Headers();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (value === undefined) continue;
|
||||||
|
const lower = key.toLowerCase();
|
||||||
|
if (lower === 'host' || lower === 'content-length') continue;
|
||||||
|
|
||||||
|
headers.set(key, Array.isArray(value) ? value.join(',') : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestInit: RequestInit & { duplex?: 'half' } = {
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
redirect: 'manual',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||||
|
requestInit.body = req as unknown as BodyInit;
|
||||||
|
requestInit.duplex = 'half';
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(targetUrl, requestInit);
|
||||||
|
|
||||||
|
res.status(upstream.status);
|
||||||
|
upstream.headers.forEach((value, key) => {
|
||||||
|
const lower = key.toLowerCase();
|
||||||
|
if (lower === 'connection' || lower === 'transfer-encoding') return;
|
||||||
|
res.setHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!upstream.body) {
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Readable.fromWeb(upstream.body as any).pipe(res);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serve static files from /browser
|
* Serve static files from /browser
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue