+ `,
+ styles: []
+})
+export class PaginaNaoEncontradaComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard-routing.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard-routing.module.ts
new file mode 100644
index 00000000..3d0fc158
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard-routing.module.ts
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { AuthGuard } from './../seguranca/auth.guard';
+import { DashboardComponent } from './dashboard/dashboard.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: DashboardComponent,
+ canActivate: [ AuthGuard ],
+ data: { roles: ['ROLE_PESQUISAR_LANCAMENTO'] }
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class DashboardRoutingModule { }
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard.module.ts
new file mode 100644
index 00000000..d6cc4f8d
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard.module.ts
@@ -0,0 +1,24 @@
+import { CommonModule, DecimalPipe } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ChartModule } from 'primeng/chart';
+import { PanelModule } from 'primeng/panel';
+
+import { SharedModule } from './../shared/shared.module';
+import { DashboardRoutingModule } from './dashboard-routing.module';
+import { DashboardComponent } from './dashboard/dashboard.component';
+
+@NgModule({
+ declarations: [DashboardComponent],
+ imports: [
+ CommonModule,
+
+ PanelModule,
+ ChartModule,
+
+ SharedModule,
+ DashboardRoutingModule
+ ],
+ providers: [DecimalPipe]
+})
+export class DashboardModule { }
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard.service.ts b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard.service.ts
new file mode 100644
index 00000000..825e25bd
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard.service.ts
@@ -0,0 +1,41 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { environment } from './../../environments/environment';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DashboardService {
+
+ lancamentosUrl: string;
+
+ constructor(private http: HttpClient) {
+ this.lancamentosUrl = `${environment.apiUrl}/lancamentos`;
+ }
+
+ lancamentosPorCategoria(): Promise
> {
+ return this.http.get(`${this.lancamentosUrl}/estatisticas/por-categoria`)
+ .toPromise()
+ .then((response: any) => response);
+ }
+
+ lancamentosPorDia(): Promise> {
+ return this.http.get(`${this.lancamentosUrl}/estatisticas/por-dia`)
+ .toPromise()
+ .then((response: any) => {
+ const dados = response;
+ this.converterStringsParaDatas(dados);
+
+ return dados;
+ });
+ }
+
+ private converterStringsParaDatas(dados: Array) {
+ for (const dado of dados) {
+ let offset = new Date().getTimezoneOffset() * 60000;
+
+ dado.dia = new Date(dado.dia);
+ dado.dia = new Date(new Date(dado.dia).getTime() + offset);
+ }
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.css b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.html b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.html
new file mode 100644
index 00000000..d6d5b0e6
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.html
@@ -0,0 +1,20 @@
+
+
+
+
Dashboard
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.ts
new file mode 100644
index 00000000..34b8ec70
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/dashboard/dashboard/dashboard.component.ts
@@ -0,0 +1,130 @@
+import { DecimalPipe } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+
+import { DashboardService } from './../dashboard.service';
+
+@Component({
+ selector: 'app-dashboard',
+ templateUrl: './dashboard.component.html',
+ styleUrls: ['./dashboard.component.css']
+})
+export class DashboardComponent implements OnInit {
+ pieChartData: any;
+ lineChartData: any;
+
+ optionsLine = {
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: (context: any): any => {
+ let label = context.dataset.label || '';
+ let value = context.raw || 0;
+ let formattedValue = this.decimalPipe.transform(value, '1.2-2', 'pt_BR');
+ return `${label}: ${formattedValue}`;
+ }
+ }
+ }
+ }
+ }
+
+ optionsPie = {
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: (context: any): any => {
+ let label = context.label || '';
+ let value = context.raw || 0;
+ let formattedValue = this.decimalPipe.transform(value, '1.2-2', 'pt_BR');
+ return `${label}: ${formattedValue}`;
+ }
+ }
+ }
+ }
+ }
+
+ constructor(
+ private dashboardService: DashboardService,
+ private decimalPipe: DecimalPipe) { }
+
+ ngOnInit() {
+ this.configurarGraficoPizza();
+ this.configurarGraficoLinha();
+ }
+
+ configurarGraficoPizza() {
+ this.dashboardService.lancamentosPorCategoria()
+ .then(dados => {
+ this.pieChartData = {
+ labels: dados.map(dado => dado.categoria.nome),
+ datasets: [
+ {
+ data: dados.map(dado => dado.total),
+ backgroundColor: ['#FF9900', '#109618', '#990099', '#3B3EAC', '#0099C6',
+ '#DD4477', '#3366CC', '#DC3912']
+ }
+ ]
+ };
+ });
+ }
+
+ configurarGraficoLinha() {
+ this.dashboardService.lancamentosPorDia()
+ .then(dados => {
+ const diasDoMes = this.configurarDiasMes();
+
+ const totaisReceitas = this.totaisPorCadaDiaMes(
+ dados.filter(dado => dado.tipo === 'RECEITA'), diasDoMes);
+
+ const totaisDespesas = this.totaisPorCadaDiaMes(
+ dados.filter(dado => dado.tipo === 'DESPESA'), diasDoMes);
+
+ this.lineChartData = {
+ labels: diasDoMes,
+ datasets: [
+ {
+ label: 'Receitas',
+ data: totaisReceitas,
+ borderColor: '#3366CC'
+ }, {
+ label: 'Despesas',
+ data: totaisDespesas,
+ borderColor: '#D62B00'
+ }
+ ]
+ }
+ });
+ }
+
+ private totaisPorCadaDiaMes(dados: any, diasDoMes: any) {
+ const totais: number[] = [];
+ for (const dia of diasDoMes) {
+ let total = 0;
+
+ for (const dado of dados) {
+ if (dado.dia.getDate() === dia) {
+ total = dado.total;
+
+ break;
+ }
+ }
+
+ totais.push(total);
+ }
+
+ return totais;
+ }
+
+ private configurarDiasMes() {
+ const mesReferencia = new Date();
+ mesReferencia.setMonth(mesReferencia.getMonth() + 1);
+ mesReferencia.setDate(0);
+ const quantidade = mesReferencia.getDate();
+
+ const dias: number[] = [];
+ for (let i = 1; i <= quantidade; i++) {
+ dias.push(i);
+ }
+
+ return dias;
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.css b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.html b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.html
new file mode 100644
index 00000000..b34f7566
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.html
@@ -0,0 +1,90 @@
+
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.ts
new file mode 100644
index 00000000..85f6a459
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento-cadastro/lancamento-cadastro.component.ts
@@ -0,0 +1,208 @@
+import { Component, OnInit } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { Title } from '@angular/platform-browser';
+
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { MessageService } from 'primeng/api';
+
+import { ErrorHandlerService } from 'src/app/core/error-handler.service';
+import { PessoaService } from 'src/app/pessoas/pessoa.service';
+import { LancamentoService } from '../lancamento.service';
+import { CategoriaService } from './../../categorias/categoria.service';
+import { Lancamento } from './../../core/model';
+
+@Component({
+ selector: 'app-lancamento-cadastro',
+ templateUrl: './lancamento-cadastro.component.html',
+ styleUrls: ['./lancamento-cadastro.component.css']
+})
+export class LancamentoCadastroComponent implements OnInit {
+ formulario!: FormGroup;
+
+ categorias: any[] = [];
+ pessoas: any[] = []
+
+ tipos = [
+ { label: 'Receita', value: 'RECEITA' },
+ { label: 'Despesa', value: 'DESPESA' },
+ ];
+
+ uploadEmAndamento = false;
+
+ constructor(
+ private categoriaService: CategoriaService,
+ private pessoaService: PessoaService,
+ private lancamentoService: LancamentoService,
+ private messageService: MessageService,
+ private errorHandler: ErrorHandlerService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private title: Title,
+ private formBuilder: FormBuilder
+ ) { }
+
+ ngOnInit(): void {
+ this.configurarFormulario();
+
+ const codigoLancamento = this.route.snapshot.params['codigo'];
+
+ this.title.setTitle('Novo lançamento')
+
+ if (codigoLancamento && codigoLancamento !== 'novo') {
+ this.carregarLancamento(codigoLancamento)
+ }
+
+ this.carregarCategorias()
+ this.carregarPessoas()
+ }
+
+ antesUploadAnexo() {
+ this.uploadEmAndamento = true;
+ }
+
+ aoTerminarUploadAnexo(event: any) {
+ const anexo = event.originalEvent.body;
+ this.formulario.patchValue({
+ anexo: anexo.nome,
+ urlAnexo: anexo.url.replace('\\\\', 'https://')
+ });
+ this.uploadEmAndamento = false;
+ }
+
+ erroUpload(event: any) {
+ this.messageService.add({ severity: 'error', detail: 'Erro ao tentar enviar anexo!' });
+ this.uploadEmAndamento = false;
+ }
+
+ removerAnexo() {
+ this.formulario.patchValue({
+ anexo: null,
+ urlAnexo: null
+ });
+ }
+
+ get nomeAnexo() {
+ const nome = this.formulario?.get('anexo')?.value;
+ if (nome) {
+ return nome.substring(nome.indexOf('_') + 1, nome.length);
+ }
+
+ return '';
+ }
+
+ get urlUploadAnexo() {
+ return this.lancamentoService.urlUploadAnexo();
+ }
+
+ get uploadHeaders() {
+ return this.lancamentoService.uploadHeaders();
+ }
+
+ configurarFormulario() {
+ this.formulario = this.formBuilder.group({
+ codigo: [],
+ tipo: ['RECEITA', Validators.required],
+ dataVencimento: [null, Validators.required],
+ dataPagamento: [],
+ descricao: [null, [this.validarObrigatoriedade, this.validarTamanhoMinimo(5)]],
+ valor: [null, Validators.required],
+ pessoa: this.formBuilder.group({
+ codigo: [null, Validators.required],
+ nome: []
+ }),
+ categoria: this.formBuilder.group({
+ codigo: [null, Validators.required],
+ nome: []
+ }),
+ observacao: [],
+ anexo: [],
+ urlAnexo: []
+ });
+ }
+
+ validarObrigatoriedade(input: FormControl) {
+ return (input.value ? null : { obrigatoriedade: true });
+ }
+
+ validarTamanhoMinimo(valor: number) {
+ return (input: FormControl) => {
+ return (!input.value || input.value.length >= valor) ? null : { tamanhoMinimo: { tamanho: valor } };
+ };
+ }
+
+ get editando() {
+ return Boolean(this.formulario.get('codigo')?.value)
+ }
+
+ carregarLancamento(codigo: number) {
+ this.lancamentoService.buscarPorCodigo(codigo)
+ .then(lancamento => {
+ this.formulario.patchValue(lancamento)
+
+ if (this.formulario.get('urlAnexo')?.value)
+ this.formulario.patchValue({
+ urlAnexo: this.formulario.get('urlAnexo')?.value.replace('\\\\', 'https://')
+ });
+
+ this.atualizarTituloEdicao()
+ },
+ erro => this.errorHandler.handle(erro));
+ }
+
+ carregarCategorias() {
+ return this.categoriaService.listarTodas()
+ .then(categorias => {
+ this.categorias = categorias
+ .map((c: any) => ({ label: c.nome, value: c.codigo }));
+ })
+ .catch(erro => this.errorHandler.handle(erro));
+ }
+
+ carregarPessoas() {
+ this.pessoaService.listarTodas()
+ .then(pessoas => {
+ this.pessoas = pessoas
+ .map((p: any) => ({ label: p.nome, value: p.codigo }));
+ })
+ .catch(erro => this.errorHandler.handle(erro));
+ }
+
+ salvar() {
+ if (this.editando) {
+ this.atualizarLancamento()
+ } else {
+ this.adicionarLancamento()
+ }
+ }
+
+ adicionarLancamento() {
+ this.lancamentoService.adicionar(this.formulario.value)
+ .then(lancamentoAdicionado => {
+ this.messageService.add({ severity: 'success', detail: 'Lançamento adicionado com sucesso!' });
+
+ this.router.navigate(['/lancamentos', lancamentoAdicionado.codigo])
+ }
+ ).catch(erro => this.errorHandler.handle(erro));
+ }
+
+ atualizarLancamento() {
+ this.lancamentoService.atualizar(this.formulario.value)
+ .then((lancamento: Lancamento) => {
+ this.formulario.patchValue(lancamento)
+ this.messageService.add({ severity: 'success', detail: 'Lançamento alterado com sucesso!' });
+ this.atualizarTituloEdicao()
+ }
+ ).catch(erro => this.errorHandler.handle(erro))
+ }
+
+ novo() {
+ this.formulario.reset();
+ this.formulario.patchValue(new Lancamento())
+ this.router.navigate(['lancamentos/novo']);
+ }
+
+ atualizarTituloEdicao() {
+ this.title.setTitle(`Edição de lançamento: ${this.formulario.get('descricao')?.value}`)
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento.service.ts b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento.service.ts
new file mode 100644
index 00000000..87077853
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamento.service.ts
@@ -0,0 +1,110 @@
+import { DatePipe } from '@angular/common';
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Lancamento } from '../core/model';
+import { environment } from './../../environments/environment';
+
+export class LancamentoFiltro {
+ descricao?: string
+ dataVencimentoInicio?: Date
+ dataVencimentoFim?: Date
+ pagina: number = 0
+ itensPorPagina: number = 5
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LancamentoService {
+
+ lancamentosUrl: string;
+
+ constructor(
+ private http: HttpClient,
+ private datePipe: DatePipe
+ ) {
+ this.lancamentosUrl = `${environment.apiUrl}/lancamentos`
+ }
+
+ urlUploadAnexo(): string {
+ return `${this.lancamentosUrl}/anexo`;
+ }
+
+ uploadHeaders() {
+ return new HttpHeaders()
+ .append('Authorization', 'Bearer ' + localStorage.getItem('token'))
+ }
+
+ pesquisar(filtro: LancamentoFiltro): Promise {
+ let params = new HttpParams()
+ .set('page', filtro.pagina)
+ .set('size', filtro.itensPorPagina);
+
+ if (filtro.descricao) {
+ params = params.set('descricao', filtro.descricao);
+ }
+
+ if (filtro.dataVencimentoInicio) {
+ params = params.set('dataVencimentoDe', this.datePipe.transform(filtro.dataVencimentoInicio, 'yyyy-MM-dd')!);
+ }
+
+ if (filtro.dataVencimentoFim) {
+ params = params.set('dataVencimentoAte', this.datePipe.transform(filtro.dataVencimentoFim, 'yyyy-MM-dd')!);
+ }
+
+ return this.http.get(`${this.lancamentosUrl}?resumo`, { params })
+ .toPromise()
+ .then((response: any) => {
+ const lancamentos = response['content'];
+
+ const resultado = {
+ lancamentos,
+ total: response['totalElements']
+ };
+
+ return resultado;
+ });
+ }
+
+ excluir(codigo: number): Promise {
+ return this.http.delete(`${this.lancamentosUrl}/${codigo}`)
+ .toPromise();
+ }
+
+ adicionar(lancamento: Lancamento): Promise {
+ return this.http.post(this.lancamentosUrl, lancamento).toPromise();
+ }
+
+ atualizar(lancamento: Lancamento): Promise {
+ return this.http.put(`${this.lancamentosUrl}/${lancamento.codigo}`, lancamento)
+ .toPromise()
+ .then((response: any) => {
+ this.converterStringsParaDatas([response]);
+
+ return response;
+ });
+ }
+
+ buscarPorCodigo(codigo: number): Promise {
+ return this.http.get(`${this.lancamentosUrl}/${codigo}`)
+ .toPromise()
+ .then((response: any) => {
+ this.converterStringsParaDatas([response]);
+
+ return response;
+ });
+ }
+
+ private converterStringsParaDatas(lancamentos: Lancamento[]) {
+ for (const lancamento of lancamentos) {
+ let offset = new Date().getTimezoneOffset() * 60000;
+
+ lancamento.dataVencimento = new Date(new Date(lancamento.dataVencimento!).getTime() + offset);
+
+ if (lancamento.dataPagamento) {
+ lancamento.dataPagamento = new Date(new Date(lancamento.dataPagamento).getTime() + offset);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.css b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.html b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.html
new file mode 100644
index 00000000..1e1d87f1
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+ Pessoa |
+ Descrição |
+
+
+
+ |
+
+
+
+
+
+ Pessoa
+ {{ lancamento.pessoa }}
+ |
+
+
+ Descrição
+ {{ lancamento.descricao }}
+ |
+
+
+ Vencimento
+ {{ lancamento.dataVencimento | date:'dd/MM/y' }}
+ |
+
+
+ Pagamento
+ {{ lancamento.dataPagamento | date:'dd/MM/y' }}
+ |
+
+
+ Valor
+
+ {{ lancamento.valor | number:'1.2-2':'pt-BR' }}
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+ Nenhum lançamento encontrado
+ |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.ts
new file mode 100644
index 00000000..47dffb76
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-pesquisa/lancamentos-pesquisa.component.ts
@@ -0,0 +1,80 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+
+import { ConfirmationService, LazyLoadEvent, MessageService } from 'primeng/api';
+import { Table } from 'primeng/table';
+
+import { AuthService } from './../../seguranca/auth.service';
+import { ErrorHandlerService } from 'src/app/core/error-handler.service';
+import { LancamentoFiltro, LancamentoService } from './../lancamento.service';
+
+@Component({
+ selector: 'app-lancamentos-pesquisa',
+ templateUrl: './lancamentos-pesquisa.component.html',
+ styleUrls: ['./lancamentos-pesquisa.component.css']
+})
+export class LancamentosPesquisaComponent implements OnInit {
+
+ filtro = new LancamentoFiltro();
+
+ totalRegistros: number = 0
+
+ lancamentos: any[] = [];
+ @ViewChild('tabela') grid!: Table;
+
+ constructor(
+ private auth: AuthService,
+ private lancamentoService: LancamentoService,
+ private errorHandler: ErrorHandlerService,
+ private messageService: MessageService,
+ private confirmationService: ConfirmationService,
+ private title: Title
+ ) { }
+
+ ngOnInit() {
+ this.title.setTitle('Pesquisa de lançamentos')
+ }
+
+ pesquisar(pagina: number = 0): void {
+ this.filtro.pagina = pagina;
+
+ this.lancamentoService.pesquisar(this.filtro)
+ .then((resultado: any) => {
+ this.lancamentos = resultado.lancamentos;
+ this.totalRegistros = resultado.total;
+ })
+ .catch(erro => this.errorHandler.handle(erro));
+ }
+
+ aoMudarPagina(event: LazyLoadEvent) {
+ const pagina = event!.first! / event!.rows!;
+ this.pesquisar(pagina);
+ }
+
+ confirmarExclusao(lancamento: any): void {
+ this.confirmationService.confirm({
+ message: 'Tem certeza que deseja excluir?',
+ accept: () => {
+ this.excluir(lancamento);
+ }
+ });
+ }
+
+ excluir(lancamento: any) {
+
+ this.lancamentoService.excluir(lancamento.codigo)
+ .then(() => {
+ if (this.grid.first === 0) {
+ this.pesquisar();
+ } else {
+ this.grid.reset();
+ }
+
+ this.messageService.add({ severity: 'success', detail: 'Lançamento excluído com sucesso!' })
+ })
+ }
+
+ naoTemPermissao(permissao: string) {
+ return !this.auth.temPermissao(permissao);
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-routing.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-routing.module.ts
new file mode 100644
index 00000000..5151c848
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos-routing.module.ts
@@ -0,0 +1,35 @@
+import { NgModule } from "@angular/core";
+import { RouterModule, Routes } from '@angular/router';
+
+import { AuthGuard } from './../seguranca/auth.guard';
+import { LancamentoCadastroComponent } from './lancamento-cadastro/lancamento-cadastro.component';
+import { LancamentosPesquisaComponent } from './lancamentos-pesquisa/lancamentos-pesquisa.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: LancamentosPesquisaComponent,
+ canActivate: [AuthGuard],
+ data: { roles: ['ROLE_PESQUISAR_LANCAMENTO'] }
+ },
+ {
+ path: 'novo',
+ component: LancamentoCadastroComponent,
+ canActivate: [AuthGuard],
+ data: { roles: ['ROLE_CADASTRAR_LANCAMENTO'] }
+ },
+ {
+ path: ':codigo',
+ component: LancamentoCadastroComponent,
+ canActivate: [AuthGuard],
+ data: { roles: ['ROLE_CADASTRAR_LANCAMENTO'] }
+ }
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes)
+ ],
+ exports: [RouterModule]
+})
+export class LancamentosRoutingModule { }
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos.module.ts
new file mode 100644
index 00000000..4bd4d183
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/lancamentos/lancamentos.module.ts
@@ -0,0 +1,51 @@
+import { CommonModule } from '@angular/common';
+import { HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { ButtonModule } from 'primeng/button';
+import { CalendarModule } from 'primeng/calendar';
+import { DropdownModule } from 'primeng/dropdown';
+import { FileUploadModule } from 'primeng/fileupload';
+import { InputNumberModule } from 'primeng/inputnumber';
+import { InputTextModule } from 'primeng/inputtext';
+import { InputTextareaModule } from 'primeng/inputtextarea';
+import { ProgressSpinnerModule } from 'primeng/progressspinner';
+import { SelectButtonModule } from 'primeng/selectbutton';
+import { TableModule } from 'primeng/table';
+import { TooltipModule } from 'primeng/tooltip';
+
+import { SharedModule } from '../shared/shared.module';
+import { LancamentoCadastroComponent } from './lancamento-cadastro/lancamento-cadastro.component';
+import { LancamentosPesquisaComponent } from './lancamentos-pesquisa/lancamentos-pesquisa.component';
+import { LancamentosRoutingModule } from './lancamentos-routing.module';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ HttpClientModule,
+
+ InputNumberModule,
+ InputTextModule,
+ ButtonModule,
+ TableModule,
+ TooltipModule,
+ InputTextareaModule,
+ CalendarModule,
+ SelectButtonModule,
+ DropdownModule,
+ FileUploadModule,
+ ProgressSpinnerModule,
+
+ SharedModule,
+ LancamentosRoutingModule
+ ],
+ declarations: [
+ LancamentoCadastroComponent,
+ LancamentosPesquisaComponent
+ ],
+ exports: []
+})
+export class LancamentosModule { }
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.css b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.html b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.html
new file mode 100644
index 00000000..7381d130
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+ Nome |
+ E-mail |
+ Telefone |
+
+
+
+
+
+
+ {{ contato.nome }} |
+ {{ contato.email }} |
+ {{ contato.telefone }} |
+
+
+
+
+ |
+
+
+
+
+
+
+ Nenhum contato cadastrado
+ |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.ts
new file mode 100644
index 00000000..401e3743
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro-contato/pessoa-cadastro-contato.component.ts
@@ -0,0 +1,54 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { NgForm } from '@angular/forms';
+import { Contato } from 'src/app/core/model';
+
+@Component({
+ selector: 'app-pessoa-cadastro-contato',
+ templateUrl: './pessoa-cadastro-contato.component.html',
+ styleUrls: ['./pessoa-cadastro-contato.component.css']
+})
+export class PessoaCadastroContatoComponent implements OnInit {
+
+ @Input() contatos: Array = []
+ exbindoFormularioContato = false;
+ contato?: Contato;
+ contatoIndex?: number;
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+ prepararNovoContato() {
+ this.exbindoFormularioContato = true;
+ this.contato = new Contato();
+ this.contatoIndex = this.contatos.length;
+ }
+
+ prepararEdicaoContato(contato: Contato, index: number) {
+ this.contato = this.clonarContato(contato);
+ this.exbindoFormularioContato = true;
+ this.contatoIndex = index;
+ }
+
+ confirmarContato(frm: NgForm) {
+ this.contatos[this.contatoIndex!] = this.clonarContato(this.contato!);
+
+ this.exbindoFormularioContato = false;
+
+ frm.reset();
+ }
+
+ removerContato(index: number) {
+ this.contatos.splice(index, 1);
+ }
+
+ clonarContato(contato: Contato): Contato {
+ return new Contato(contato.codigo, contato.nome, contato.email, contato.telefone);
+ }
+
+ get editando() {
+ return this.contato && this.contato?.codigo;
+ }
+
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.css b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.html b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.html
new file mode 100644
index 00000000..8e7912b8
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.html
@@ -0,0 +1,88 @@
+
+
+
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.ts
new file mode 100644
index 00000000..fb4258ea
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa-cadastro/pessoa-cadastro.component.ts
@@ -0,0 +1,126 @@
+import { Component, OnInit } from '@angular/core';
+import { NgForm } from '@angular/forms';
+import { Title } from '@angular/platform-browser';
+import { ActivatedRoute, Router } from '@angular/router';
+import { MessageService } from 'primeng/api';
+
+import { ErrorHandlerService } from '../../core/error-handler.service';
+import { Pessoa } from '../../core/model';
+import { PessoaService } from '../pessoa.service';
+
+@Component({
+ selector: 'app-pessoa-cadastro',
+ templateUrl: './pessoa-cadastro.component.html',
+ styleUrls: ['./pessoa-cadastro.component.css']
+})
+export class PessoaCadastroComponent implements OnInit {
+
+ pessoa = new Pessoa();
+ estados: any[] = [];
+ cidades: any[] = [];
+ estadoSelecionado?: number;
+
+ constructor(
+ private pessoaService: PessoaService,
+ private messageService: MessageService,
+ private errorHandler: ErrorHandlerService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private title: Title
+ ) { }
+
+ ngOnInit() {
+ const codigoPessoa = this.route.snapshot.params['codigo'];
+
+ this.title.setTitle('Nova pessoa');
+
+ this.carregarEstados();
+
+ if (codigoPessoa && codigoPessoa !== 'nova') {
+ this.carregarPessoa(codigoPessoa);
+ }
+ }
+
+ carregarEstados() {
+ this.pessoaService.listarEstados().then(lista => {
+ this.estados = lista.map(uf => ({ label: uf.nome, value: uf.codigo }));
+ })
+ .catch(erro => this.errorHandler.handle(erro));
+ }
+
+ get editando() {
+ return Boolean(this.pessoa.codigo)
+ }
+
+ carregarPessoa(codigo: number) {
+ this.pessoaService.buscarPorCodigo(codigo)
+ .then((pessoa: Pessoa) => {
+ this.pessoa = pessoa;
+ this.estadoSelecionado = (this.pessoa.endereco.cidade) ?
+ this.pessoa.endereco.cidade.estado.codigo : undefined;
+
+ if (this.estadoSelecionado) {
+ this.carregarCidades();
+ }
+ this.atualizarTituloEdicao();
+ })
+ .catch((erro: any) => this.errorHandler.handle(erro));
+ }
+
+ carregarCidades() {
+ this.pessoaService.pesquisarCidades(this.estadoSelecionado!)
+ .then(cidades => {
+ this.cidades = cidades.map(c => ({
+ label: c.nome,
+ value: c.codigo
+ }));
+ if (this.estadoSelecionado !== this.pessoa.endereco.cidade.estado.codigo)
+ this.pessoa.endereco.cidade.codigo = undefined;
+ })
+ .catch((erro: any) => this.errorHandler.handle(erro));
+ }
+
+ salvar(form: NgForm) {
+ if (this.editando) {
+ this.atualizarPessoa(form);
+ } else {
+ this.adicionarPessoa(form);
+ }
+ }
+
+ adicionarPessoa(form: NgForm) {
+ this.pessoaService.adicionar(this.pessoa)
+ .then((pessoaAdicionada: Pessoa) => {
+ this.messageService.add({ severity: 'success', detail: 'Pessoa adicionada com sucesso!' });
+
+ this.router.navigate(['pessoas', pessoaAdicionada.codigo]);
+ })
+ .catch(erro => this.errorHandler.handle(erro));
+ }
+
+ atualizarPessoa(form: NgForm) {
+ this.pessoaService.atualizar(this.pessoa)
+ .then(pessoa => {
+ this.pessoa = pessoa;
+
+ this.messageService.add({ severity: 'success', detail: 'Pessoa alterada com sucesso!' });
+ this.atualizarTituloEdicao();
+ })
+ .catch(erro => this.errorHandler.handle(erro));
+ }
+
+ nova(form: NgForm) {
+ form.reset();
+
+ setTimeout(() => {
+ this.pessoa = new Pessoa();
+ }, 1);
+
+ this.router.navigate(['pessoas', 'nova']);
+ }
+
+ atualizarTituloEdicao() {
+ this.title.setTitle(`Edição de pessoa: ${this.pessoa.nome}`);
+ }
+
+}
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa.service.ts b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa.service.ts
new file mode 100644
index 00000000..fcfa81d9
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoa.service.ts
@@ -0,0 +1,89 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Pessoa } from '../core/model';
+import { environment } from './../../environments/environment';
+import { Cidade, Estado } from './../core/model';
+
+export class PessoaFiltro {
+ nome?: string;
+ pagina: number = 0;
+ itensPorPagina: number = 5;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PessoaService {
+ pessoasUrl: string
+ cidadesUrl: string;
+ estadosUrl: string;
+
+ constructor(private http: HttpClient) {
+ this.pessoasUrl = `${environment.apiUrl}/pessoas`;
+ this.estadosUrl = `${environment.apiUrl}/estados`;
+ this.cidadesUrl = `${environment.apiUrl}/cidades`;
+ }
+
+ pesquisar(filtro: PessoaFiltro): Promise {
+ let params = new HttpParams()
+ .set('page', filtro.pagina)
+ .set('size', filtro.itensPorPagina);
+
+ if (filtro.nome) {
+ params = params.set('nome', filtro.nome);
+ }
+
+ return this.http.get(`${this.pessoasUrl}`, { params })
+ .toPromise()
+ .then((response: any) => {
+ const pessoas = response['content'];
+
+ const resultado = {
+ pessoas,
+ total: response['totalElements']
+ };
+
+ return resultado;
+ });
+ }
+
+ listarTodas(): Promise {
+ return this.http.get(this.pessoasUrl)
+ .toPromise()
+ .then((response: any) => response['content']);
+ }
+
+ excluir(codigo: number): Promise {
+ return this.http.delete(`${this.pessoasUrl}/${codigo}`).toPromise();
+ }
+
+ mudarStatus(codigo: number, ativo: boolean): Promise {
+ return this.http.put(`${this.pessoasUrl}/${codigo}/ativo`, ativo)
+ .toPromise();
+ }
+
+ adicionar(pessoa: Pessoa): Promise {
+ return this.http.post(this.pessoasUrl, pessoa)
+ .toPromise();
+ }
+
+ atualizar(pessoa: Pessoa): Promise {
+ return this.http.put(`${this.pessoasUrl}/${pessoa.codigo}`, pessoa).toPromise();
+ }
+
+ buscarPorCodigo(codigo: number): Promise {
+ return this.http.get(`${this.pessoasUrl}/${codigo}`).toPromise();
+ }
+
+ listarEstados(): Promise {
+ return this.http.get(this.estadosUrl).toPromise();
+ }
+
+ pesquisarCidades(estadoId: number): Promise {
+ const params = new HttpParams()
+ .set('estado', estadoId);
+
+ return this.http.get(this.cidadesUrl, { params }).toPromise();
+ }
+}
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.css b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.html b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.html
new file mode 100644
index 00000000..91c18070
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nome |
+ Cidade |
+ Estado |
+ Status |
+
+
+
+
+
+
+
+ Nome
+ {{ pessoa.nome }}
+ |
+
+ Cidade
+ {{ pessoa.endereco.cidade?.nome }}
+ |
+
+ Estado
+ {{ pessoa.endereco.cidade?.estado.nome }}
+ |
+
+ Status
+
+ {{ pessoa.ativo ? 'Ativo' : 'Inativo' }}
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ Nenhuma pessoa encontrada
+ |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.ts
new file mode 100644
index 00000000..2ad97906
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-pesquisa/pessoas-pesquisa.component.ts
@@ -0,0 +1,82 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+
+import { ConfirmationService, LazyLoadEvent, MessageService } from 'primeng/api';
+
+import { ErrorHandlerService } from 'src/app/core/error-handler.service';
+import { PessoaFiltro, PessoaService } from '../pessoa.service';
+
+@Component({
+ selector: 'app-pessoas-pesquisa',
+ templateUrl: './pessoas-pesquisa.component.html',
+ styleUrls: ['./pessoas-pesquisa.component.css']
+})
+export class PessoasPesquisaComponent implements OnInit {
+ totalRegistros = 0;
+ filtro = new PessoaFiltro()
+ pessoas: any[] = [];
+ @ViewChild('tabela') grid!: any;
+
+ constructor(
+ private pessoaService: PessoaService,
+ private messageService: MessageService,
+ private errorHandler: ErrorHandlerService,
+ private confirmationService: ConfirmationService,
+ private title: Title
+ ) { }
+
+ ngOnInit() {
+ this.title.setTitle('Pesquisa de pessoas');
+ }
+
+ pesquisar(pagina: number = 0): void {
+ this.filtro.pagina = pagina;
+
+ this.pessoaService.pesquisar(this.filtro)
+ .then((dados: any) => {
+ this.pessoas = dados.pessoas;
+ this.totalRegistros = dados.total;
+ });
+ }
+
+ aoMudarPagina(event: LazyLoadEvent) {
+ const pagina = event!.first! / event!.rows!;
+ this.pesquisar(pagina);
+ }
+
+ confirmarExclusao(pessoa: any): void {
+ this.confirmationService.confirm({
+ message: 'Tem certeza que deseja excluir?',
+ accept: () => {
+ this.excluir(pessoa);
+ }
+ });
+ }
+
+ excluir(pessoa: any) {
+
+ this.pessoaService.excluir(pessoa.codigo)
+ .then(
+ () => {
+ this.grid.reset();
+
+ this.messageService.add({ severity: 'success', detail: 'Pessoa excluída com sucesso!' })
+ }
+ )
+ .catch((error) => this.errorHandler.handle(error))
+
+ }
+
+ alternarStatus(pessoa: any): void {
+ const novoStatus = !pessoa.ativo;
+
+ this.pessoaService.mudarStatus(pessoa.codigo, novoStatus)
+ .then(() => {
+ const acao = novoStatus ? 'ativada' : 'desativada';
+
+ pessoa.ativo = novoStatus;
+ this.messageService.add({ severity: 'success', detail: `Pessoa ${acao} com sucesso!` });
+ })
+ .catch(erro => this.errorHandler.handle(erro));
+ }
+}
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-routing.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-routing.module.ts
new file mode 100644
index 00000000..a1827b2b
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas-routing.module.ts
@@ -0,0 +1,35 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from "@angular/router";
+
+import { AuthGuard } from './../seguranca/auth.guard';
+import { PessoaCadastroComponent } from "./pessoa-cadastro/pessoa-cadastro.component";
+import { PessoasPesquisaComponent } from "./pessoas-pesquisa/pessoas-pesquisa.component";
+
+const routes: Routes = [
+ {
+ path: '',
+ component: PessoasPesquisaComponent,
+ canActivate: [AuthGuard],
+ data: { roles: ['ROLE_PESQUISAR_PESSOA'] }
+ },
+ {
+ path: 'nova',
+ component: PessoaCadastroComponent,
+ canActivate: [AuthGuard],
+ data: { roles: ['ROLE_CADASTRAR_PESSOA'] }
+ },
+ {
+ path: ':codigo',
+ component: PessoaCadastroComponent,
+ canActivate: [AuthGuard],
+ data: { roles: ['ROLE_CADASTRAR_PESSOA'] }
+ }
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes)
+ ],
+ exports: [RouterModule]
+})
+export class PessoasRoutingModule { }
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas.module.ts
new file mode 100644
index 00000000..819bf1df
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/pessoas/pessoas.module.ts
@@ -0,0 +1,47 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { ButtonModule } from 'primeng/button';
+import { DialogModule } from 'primeng/dialog';
+import { DropdownModule } from 'primeng/dropdown';
+import { InputMaskModule } from 'primeng/inputmask';
+import { InputTextModule } from 'primeng/inputtext';
+import { PanelModule } from 'primeng/panel';
+import { TableModule } from 'primeng/table';
+import { TooltipModule } from 'primeng/tooltip';
+
+import { SharedModule } from '../shared/shared.module';
+import { PessoasRoutingModule } from './pessoas-routing.module';
+
+import { PessoaCadastroContatoComponent } from './pessoa-cadastro-contato/pessoa-cadastro-contato.component';
+import { PessoaCadastroComponent } from './pessoa-cadastro/pessoa-cadastro.component';
+import { PessoasPesquisaComponent } from './pessoas-pesquisa/pessoas-pesquisa.component';
+
+@NgModule({
+ declarations: [
+ PessoaCadastroComponent,
+ PessoasPesquisaComponent,
+ PessoaCadastroContatoComponent
+ ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ RouterModule,
+
+ InputTextModule,
+ ButtonModule,
+ TableModule,
+ TooltipModule,
+ InputMaskModule,
+ PanelModule,
+ DialogModule,
+ DropdownModule,
+
+ SharedModule,
+ PessoasRoutingModule
+ ],
+ exports: []
+})
+export class PessoasModule { }
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.css b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.html b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.html
new file mode 100644
index 00000000..eb435676
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.html
@@ -0,0 +1,24 @@
+
+
+
+
Relatório de lançamentos por pessoa
+
+
+
+
+
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.ts
new file mode 100644
index 00000000..f3ab7adb
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorio-lancamentos/relatorio-lancamentos.component.ts
@@ -0,0 +1,25 @@
+import { Component, OnInit } from '@angular/core';
+import { RelatoriosService } from './../relatorios.service';
+
+@Component({
+ selector: 'app-relatorio-lancamentos',
+ templateUrl: './relatorio-lancamentos.component.html',
+ styleUrls: ['./relatorio-lancamentos.component.css']
+})
+export class RelatorioLancamentosComponent implements OnInit {
+
+ periodoInicio?: Date;
+ periodoFim?: Date;
+
+ constructor(private relatoriosService: RelatoriosService) { }
+
+ ngOnInit() { }
+
+ gerar() {
+ this.relatoriosService.relatorioLancamentosPorPessoa(this.periodoInicio!, this.periodoFim!)
+ .then(relatorio => {
+ const url = window.URL.createObjectURL(relatorio);
+ window.open(url);
+ });
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios-routing.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios-routing.module.ts
new file mode 100644
index 00000000..f2337a57
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios-routing.module.ts
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { AuthGuard } from '../seguranca/auth.guard';
+import { RelatorioLancamentosComponent } from './relatorio-lancamentos/relatorio-lancamentos.component';
+
+const routes: Routes = [
+ {
+ path: 'lancamentos',
+ component: RelatorioLancamentosComponent,
+ canActivate: [AuthGuard],
+ data: { roles: ['ROLE_PESQUISAR_LANCAMENTO'] }
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class RelatoriosRoutingModule { }
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios.module.ts
new file mode 100644
index 00000000..f9673647
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios.module.ts
@@ -0,0 +1,23 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+import { CalendarModule } from 'primeng/calendar';
+
+import { SharedModule } from './../shared/shared.module';
+import { RelatorioLancamentosComponent } from './relatorio-lancamentos/relatorio-lancamentos.component';
+import { RelatoriosRoutingModule } from './relatorios-routing.module';
+
+@NgModule({
+ declarations: [RelatorioLancamentosComponent],
+ imports: [
+ CommonModule,
+ FormsModule,
+
+ CalendarModule,
+
+ SharedModule,
+ RelatoriosRoutingModule
+ ]
+})
+export class RelatoriosModule { }
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios.service.ts b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios.service.ts
new file mode 100644
index 00000000..6826d230
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/relatorios/relatorios.service.ts
@@ -0,0 +1,30 @@
+import { DatePipe } from '@angular/common';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { environment } from './../../environments/environment';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RelatoriosService {
+
+ lancamentosUrl: string;
+
+ constructor(
+ private http: HttpClient,
+ private datePipe: DatePipe
+ ) {
+ this.lancamentosUrl = `${environment.apiUrl}/lancamentos`;
+ }
+
+ relatorioLancamentosPorPessoa(inicio: Date, fim: Date) {
+ const params = new HttpParams()
+ .set('inicio', this.datePipe.transform(inicio, 'yyyy-MM-dd')!)
+ .set('fim', this.datePipe.transform(fim, 'yyyy-MM-dd')!);
+
+ return this.http.get(`${this.lancamentosUrl}/relatorios/por-pessoa`,
+ { params, responseType: 'blob' })
+ .toPromise();
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/auth.guard.ts b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/auth.guard.ts
new file mode 100644
index 00000000..a327a9f7
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/auth.guard.ts
@@ -0,0 +1,46 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
+import { Observable } from 'rxjs';
+import { AuthService } from './auth.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthGuard implements CanActivate {
+
+ constructor(
+ private auth: AuthService,
+ private router: Router
+ ) { }
+
+ canActivate(
+ next: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree {
+
+ if (this.auth.isAccessTokenInvalido()) {
+ console.log('Navegação com access token inválido. Obtendo novo token...');
+
+ return this.auth.obterNovoAccessToken()
+ .then(() => {
+ if (this.auth.isAccessTokenInvalido()) {
+ this.auth.login();
+ return false;
+ }
+
+ return this.podeAcessarRota(next.data.roles);
+ });
+ }
+
+ return this.podeAcessarRota(next.data.roles);
+ }
+
+ podeAcessarRota(roles: string[]): boolean {
+ if (roles && !this.auth.temQualquerPermissao(roles)) {
+ this.router.navigate(['/nao-autorizado']);
+ return false;
+ }
+
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/auth.service.ts b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/auth.service.ts
new file mode 100644
index 00000000..c3eae4e6
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/auth.service.ts
@@ -0,0 +1,177 @@
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { JwtHelperService } from '@auth0/angular-jwt';
+
+import * as CryptoJS from 'crypto-js';
+
+import { environment } from './../../environments/environment';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthService {
+
+ oauthTokenUrl = environment.apiUrl + '/oauth2/token'
+ oauthAuthorizeUrl = environment.apiUrl + '/oauth2/authorize'
+ jwtPayload: any;
+
+ constructor(
+ private http: HttpClient,
+ private jwtHelper: JwtHelperService
+ ) {
+ this.carregarToken();
+ }
+
+ login() {
+ const state = this.gerarStringAleatoria(40);
+ const codeVerifier = this.gerarStringAleatoria(128);
+
+ localStorage.setItem('state', state);
+ localStorage.setItem('codeVerifier', codeVerifier);
+
+ const challengeMethod = 'S256'
+ const codeChallenge = CryptoJS.SHA256(codeVerifier)
+ .toString(CryptoJS.enc.Base64)
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/, "");
+
+ const redirectURI = encodeURIComponent(environment.oauthCallbackUrl);
+
+ const clientId = 'angular'
+ const scope = 'read write'
+ const responseType = 'code'
+
+ const params = [
+ 'response_type=' + responseType,
+ 'client_id=' + clientId,
+ 'scope=' + scope,
+ 'code_challenge=' + codeChallenge,
+ 'code_challenge_method=' + challengeMethod,
+ 'state=' + state,
+ 'redirect_uri=' + redirectURI
+ ]
+
+ window.location.href = this.oauthAuthorizeUrl + '?' + params.join('&');
+ }
+
+ obterNovoAccessTokenComCode(code: string, state: string): Promise {
+ const stateSalvo = localStorage.getItem('state');
+
+ if (stateSalvo !== state) {
+ return Promise.reject(null);
+ }
+
+ const codeVerifier = localStorage.getItem('codeVerifier')!;
+
+ const payload = new HttpParams()
+ .append('grant_type', 'authorization_code')
+ .append('code', code)
+ .append('redirect_uri', environment.oauthCallbackUrl)
+ .append('code_verifier', codeVerifier);
+
+ const headers = new HttpHeaders()
+ .append('Content-Type', 'application/x-www-form-urlencoded')
+ .append('Authorization', 'Basic YW5ndWxhcjpAbmd1bEByMA==');
+
+ return this.http.post(this.oauthTokenUrl, payload, { headers })
+ .toPromise()
+ .then((response: any) => {
+ this.armazenarToken(response['access_token']);
+ this.armazenarRefreshToken(response['refresh_token']);
+ console.log('Novo access token criado!');
+
+ localStorage.removeItem('state');
+ localStorage.removeItem('codeVerifier');
+
+ return Promise.resolve(null);
+ })
+ .catch((response: any) => {
+ console.error('Erro ao gerar o token com o code.', response);
+ return Promise.resolve();
+ });
+
+ }
+
+ obterNovoAccessToken(): Promise {
+ const headers = new HttpHeaders()
+ .append('Content-Type', 'application/x-www-form-urlencoded')
+ .append('Authorization', 'Basic YW5ndWxhcjpAbmd1bEByMA==');
+
+ const payload = new HttpParams()
+ .append('grant_type', 'refresh_token')
+ .append('refresh_token', localStorage.getItem('refreshToken')!)
+
+ return this.http.post(this.oauthTokenUrl, payload,
+ { headers })
+ .toPromise()
+ .then((response: any) => {
+ this.armazenarToken(response['access_token']);
+ this.armazenarRefreshToken(response['refresh_token'])
+ console.log('Novo access token criado!');
+
+ return Promise.resolve();
+ })
+ .catch((response: any) => {
+ console.error('Erro ao renovar token.', response);
+ return Promise.resolve();
+ });
+ }
+
+ isAccessTokenInvalido() {
+ const token = localStorage.getItem('token');
+ return !token || this.jwtHelper.isTokenExpired(token);
+ }
+
+ temPermissao(permissao: string) {
+ return this.jwtPayload && this.jwtPayload.authorities.includes(permissao);
+ }
+
+ temQualquerPermissao(roles: any) {
+ for (const role of roles) {
+ if (this.temPermissao(role)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public armazenarToken(token: string) {
+ this.jwtPayload = this.jwtHelper.decodeToken(token);
+ localStorage.setItem('token', token);
+ }
+
+ public carregarToken() {
+ const token = localStorage.getItem('token');
+
+ if (token) {
+ this.armazenarToken(token);
+ }
+ }
+
+ limparAccessToken() {
+ localStorage.removeItem('token');
+ this.jwtPayload = null;
+ }
+
+ private armazenarRefreshToken(refreshToken: string) {
+ localStorage.setItem('refreshToken', refreshToken);
+ }
+
+ private gerarStringAleatoria(tamanho: number) {
+ let resultado = '';
+ //Chars que são URL safe
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ for (let i = 0; i < tamanho; i++) {
+ resultado += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return resultado;
+ }
+
+ logout() {
+ this.limparAccessToken();
+ localStorage.clear();
+ window.location.href = environment.apiUrl + '/logout?returnTo=' + environment.logoutRedirectToUrl;
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.css b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.html b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.html
new file mode 100644
index 00000000..52727934
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.html
@@ -0,0 +1 @@
+authorized works!
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.ts
new file mode 100644
index 00000000..ba20961a
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/authorized/authorized.component.ts
@@ -0,0 +1,35 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AuthService } from '../auth.service';
+
+@Component({
+ selector: 'app-authorized',
+ templateUrl: './authorized.component.html',
+ styleUrls: ['./authorized.component.css']
+})
+export class AuthorizedComponent implements OnInit {
+
+ constructor(
+ private activatedRoute: ActivatedRoute,
+ private auth: AuthService,
+ private route: Router
+ ) { }
+
+ ngOnInit(): void {
+ this.activatedRoute.queryParams.subscribe((params: any) => {
+ if (params.code) {
+ this.auth.obterNovoAccessTokenComCode(params.code, params.state)
+ .then(() => {
+ this.route.navigate(['/'])
+ })
+ .catch((e: any) => {
+ console.error('Erro no callback')
+ this.route.navigate(['/'])
+ });
+ } else {
+ this.route.navigate(['/']);
+ }
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/money-http-interceptor.ts b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/money-http-interceptor.ts
new file mode 100644
index 00000000..12471091
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/money-http-interceptor.ts
@@ -0,0 +1,38 @@
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { from, Observable } from 'rxjs';
+import { mergeMap } from 'rxjs/operators';
+
+import { AuthService } from './auth.service';
+
+export class NotAuthenticatedError { }
+
+@Injectable()
+export class MoneyHttpInterceptor implements HttpInterceptor {
+
+ constructor(private auth: AuthService) { }
+
+ intercept(req: HttpRequest, next: HttpHandler): Observable> {
+ if (!req.url.includes('/oauth2/token') && this.auth.isAccessTokenInvalido()) {
+ return from(this.auth.obterNovoAccessToken())
+ .pipe(
+ mergeMap(() => {
+ if (this.auth.isAccessTokenInvalido()) {
+ throw new NotAuthenticatedError();
+ }
+
+ req = req.clone({
+ setHeaders: {
+ Authorization: `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ return next.handle(req);
+ })
+ );
+ }
+
+ return next.handle(req);
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/seguranca-routing.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/seguranca-routing.module.ts
new file mode 100644
index 00000000..128d0a67
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/seguranca-routing.module.ts
@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { AuthorizedComponent } from './authorized/authorized.component';
+
+const routes: Routes = [
+ {
+ path: 'authorized',
+ component: AuthorizedComponent
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class SegurancaRoutingModule { }
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/seguranca/seguranca.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/seguranca.module.ts
new file mode 100644
index 00000000..880b67a0
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/seguranca/seguranca.module.ts
@@ -0,0 +1,49 @@
+import { CommonModule } from '@angular/common';
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+import { JwtHelperService, JwtModule } from '@auth0/angular-jwt';
+
+import { ButtonModule } from 'primeng/button';
+import { InputTextModule } from 'primeng/inputtext';
+
+import { environment } from './../../environments/environment';
+import { AuthGuard } from './auth.guard';
+import { AuthorizedComponent } from './authorized/authorized.component';
+import { MoneyHttpInterceptor } from './money-http-interceptor';
+import { SegurancaRoutingModule } from './seguranca-routing.module';
+
+export function tokenGetter(): string {
+ return localStorage.getItem('token')!;
+}
+@NgModule({
+ declarations: [AuthorizedComponent],
+ imports: [
+ CommonModule,
+ FormsModule,
+
+ JwtModule.forRoot({
+ config: {
+ tokenGetter,
+ allowedDomains: environment.tokenAllowedDomains,
+ disallowedRoutes: environment.tokenDisallowedRoutes
+ }
+ }),
+
+ InputTextModule,
+ ButtonModule,
+
+ SegurancaRoutingModule,
+ ],
+ providers: [
+ JwtHelperService,
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: MoneyHttpInterceptor,
+ multi: true
+ },
+ AuthGuard
+ ]
+})
+export class SegurancaModule { }
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/shared/message/message.component.css b/25.11-integrando-logout-no-client-ng14/src/app/shared/message/message.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/shared/message/message.component.html b/25.11-integrando-logout-no-client-ng14/src/app/shared/message/message.component.html
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/shared/message/message.component.ts b/25.11-integrando-logout-no-client-ng14/src/app/shared/message/message.component.ts
new file mode 100644
index 00000000..65f382a6
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/shared/message/message.component.ts
@@ -0,0 +1,30 @@
+import { Component, Input } from '@angular/core';
+import { AbstractControl, FormControl } from '@angular/forms';
+
+@Component({
+ selector: 'app-message',
+ template: `
+
+ {{ text }}
+
+ `,
+ styles: [`
+ .p-message-error {
+ margin: 0;
+ margin-top: 4px;
+ padding: 3px;
+ }
+ `]
+})
+export class MessageComponent {
+
+ @Input() error: string = '';
+ @Input() control?: AbstractControl | FormControl | null;
+ @Input() text: string = '';
+
+ temErro(): boolean {
+ return this.control ? this.control.hasError(this.error) && this.control.dirty : true;
+ }
+
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/app/shared/shared.module.ts b/25.11-integrando-logout-no-client-ng14/src/app/shared/shared.module.ts
new file mode 100644
index 00000000..eeade022
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/app/shared/shared.module.ts
@@ -0,0 +1,16 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { MessageComponent } from './message/message.component';
+
+@NgModule({
+ declarations: [
+ MessageComponent
+ ],
+ imports: [
+ CommonModule
+ ],
+ exports: [
+ MessageComponent
+ ]
+})
+export class SharedModule { }
diff --git a/25.11-integrando-logout-no-client-ng14/src/assets/.gitkeep b/25.11-integrando-logout-no-client-ng14/src/assets/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/25.11-integrando-logout-no-client-ng14/src/assets/i18n/pt.json b/25.11-integrando-logout-no-client-ng14/src/assets/i18n/pt.json
new file mode 100644
index 00000000..2390137f
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/assets/i18n/pt.json
@@ -0,0 +1,11 @@
+{
+ "primeng": {
+ "dayNames": ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"],
+ "dayNamesShort": ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"],
+ "dayNamesMin": ["Do","Se","Te","Qa","Qi","Sx","Sa"],
+ "monthNames": ["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],
+ "monthNamesShort": ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun","Jul", "Ago", "Set", "Out", "Nov", "Dez"],
+ "today": "Hoje",
+ "weekHeader": "Sem"
+ }
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/environments/environment.prod.ts b/25.11-integrando-logout-no-client-ng14/src/environments/environment.prod.ts
new file mode 100644
index 00000000..6f898974
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/environments/environment.prod.ts
@@ -0,0 +1,14 @@
+export const environment = {
+ production: true,
+ apiUrl: 'https://algamoney-api.herokuapp.com',
+ tokenAllowedDomains: [ /algamoney-api.herokuapp.com/ ],
+ tokenDisallowedRoutes: [/\/oauth2\/token/],
+ oauthCallbackUrl: 'https:/algamoney-app.herokuapp.com/authorized',
+ logoutRedirectToUrl: 'https://algamoney-app.herokuapp.com'
+
+ // apiUrl: 'http://localhost:8080',
+ // tokenAllowedDomains: [/localhost:8080/],
+ // tokenDisallowedRoutes: [/\/oauth\/token/],
+ // oauthCallbackUrl: 'https://oidcdebugger.com/debug',
+ // logoutRedirectToUrl: 'http://local-algamoney.com:8000'
+};
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/environments/environment.ts b/25.11-integrando-logout-no-client-ng14/src/environments/environment.ts
new file mode 100644
index 00000000..b47b3007
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/environments/environment.ts
@@ -0,0 +1,8 @@
+export const environment = {
+ production: false,
+ apiUrl: 'http://localhost:8080',
+ tokenAllowedDomains: [/localhost:8080/],
+ tokenDisallowedRoutes: [/\/oauth2\/token/],
+ oauthCallbackUrl: 'http://local-algamoney.com:8000/authorized',
+ logoutRedirectToUrl: 'http://local-algamoney.com:8000'
+};
diff --git a/25.11-integrando-logout-no-client-ng14/src/favicon.ico b/25.11-integrando-logout-no-client-ng14/src/favicon.ico
new file mode 100644
index 00000000..997406ad
Binary files /dev/null and b/25.11-integrando-logout-no-client-ng14/src/favicon.ico differ
diff --git a/25.11-integrando-logout-no-client-ng14/src/index.html b/25.11-integrando-logout-no-client-ng14/src/index.html
new file mode 100644
index 00000000..d09cc905
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ AlgamoneyUi
+
+
+
+
+
+
+
+
diff --git a/25.11-integrando-logout-no-client-ng14/src/locale/messages.json b/25.11-integrando-logout-no-client-ng14/src/locale/messages.json
new file mode 100644
index 00000000..8ab2825b
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/locale/messages.json
@@ -0,0 +1,4 @@
+{
+ "locale": "en-US",
+ "translations": {}
+}
\ No newline at end of file
diff --git a/25.11-integrando-logout-no-client-ng14/src/main.ts b/25.11-integrando-logout-no-client-ng14/src/main.ts
new file mode 100644
index 00000000..c7b673cf
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.error(err));
diff --git a/25.11-integrando-logout-no-client-ng14/src/polyfills.ts b/25.11-integrando-logout-no-client-ng14/src/polyfills.ts
new file mode 100644
index 00000000..4eef33bb
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/polyfills.ts
@@ -0,0 +1,57 @@
+/***************************************************************************************************
+ * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
+ */
+import '@angular/localize/init';
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ * (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/25.11-integrando-logout-no-client-ng14/src/styles.css b/25.11-integrando-logout-no-client-ng14/src/styles.css
new file mode 100644
index 00000000..f5b68b4f
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/styles.css
@@ -0,0 +1,97 @@
+/* You can add global styles to this file, and also import other style files */
+body {
+ margin: 0;
+ font-family: Arial, Helvetica, sans-serif;
+ color: #404C51;
+}
+
+label {
+ font-weight: bold;
+}
+
+@media (min-width: 1200px) {
+ .container {
+ width: 1170px;
+ margin: 0 auto;
+ }
+}
+
+.label {
+ margin: 0.5em;
+}
+
+.col-valor-header {
+ width: 120px;
+ text-align: center !important;
+}
+
+.col-valor-content {
+ text-align: right !important ;
+}
+
+.col-data-header {
+ width: 120px;
+}
+
+.col-data-content {
+ text-align: center !important;
+}
+
+.col-acoes-header {
+ width: 115px;
+}
+
+.col-acoes {
+ width: 115px;
+ text-align: center;
+}
+
+.col-acoes > a {
+ margin-right: 5px;
+}
+
+button.p-button {
+ margin: 0 2px;
+}
+
+@media (min-width: 40em) {
+ .col-valor-header {
+ width: 120px;
+ }
+
+ .col-valor {
+ text-align: right !important;
+ }
+
+ .col-data-header {
+ width: 120px;
+ }
+
+ .col-data {
+ text-align: center !important;
+ }
+
+ .col-acoes-header {
+ width: 120px;
+ }
+
+ .col-acoes {
+ width: 120px;
+ text-align: center !important;
+ }
+
+ }
+
+a {
+ color: #337ab7;
+ text-decoration: none;
+}
+
+a:hover, a:focus {
+ text-decoration: underline;
+}
+
+a.p-button:hover,
+a.p-button:focus {
+ text-decoration: none;
+}
diff --git a/25.11-integrando-logout-no-client-ng14/src/test.ts b/25.11-integrando-logout-no-client-ng14/src/test.ts
new file mode 100644
index 00000000..0bd702b0
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/src/test.ts
@@ -0,0 +1,27 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: {
+ context(path: string, deep?: boolean, filter?: RegExp): {
+ keys(): string[];
+ (id: string): T;
+ };
+};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting(), {
+ teardown: { destroyAfterEach: false }
+}
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/25.11-integrando-logout-no-client-ng14/tsconfig.app.json b/25.11-integrando-logout-no-client-ng14/tsconfig.app.json
new file mode 100644
index 00000000..82d91dc4
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/tsconfig.app.json
@@ -0,0 +1,15 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/25.11-integrando-logout-no-client-ng14/tsconfig.json b/25.11-integrando-logout-no-client-ng14/tsconfig.json
new file mode 100644
index 00000000..6200984d
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/tsconfig.json
@@ -0,0 +1,30 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "es2020",
+ "module": "es2020",
+ "lib": [
+ "es2018",
+ "dom"
+ ]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/25.11-integrando-logout-no-client-ng14/tsconfig.spec.json b/25.11-integrando-logout-no-client-ng14/tsconfig.spec.json
new file mode 100644
index 00000000..092345b0
--- /dev/null
+++ b/25.11-integrando-logout-no-client-ng14/tsconfig.spec.json
@@ -0,0 +1,18 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "files": [
+ "src/test.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}