Aplicação Funcionando

This commit is contained in:
Eduardo 2026-02-09 21:50:03 -03:00
parent 99807d78f7
commit 5f7b4e19e8
5 changed files with 103 additions and 42 deletions

View File

@ -2,11 +2,11 @@ import { Component, EventEmitter, Output, Input } from '@angular/core';
@Component({ @Component({
selector: 'app-cta-button', selector: 'app-cta-button',
standalone: true,
templateUrl: './cta-button.html', templateUrl: './cta-button.html',
styleUrls: ['./cta-button.scss'] styleUrls: ['./cta-button.scss']
}) })
export class CtaButtonComponent { export class CtaButtonComponent {
@Input() label: string = 'COMEÇAR AGORA'; @Input() label: string = 'COMEÇAR AGORA';
@Input() width: string = '250px'; @Input() width: string = '250px';

View File

@ -102,14 +102,14 @@
<input <input
type="text" type="text"
placeholder="Pesquisar..." placeholder="Pesquisar..."
[(ngModel)]="macrophonySearch" [value]="macrophonySearch"
(ngModelChange)="onMacrophonySearch()" /> (input)="onMacrophonySearch($any($event.target).value)" />
</div> </div>
<div class="tools-right"> <div class="tools-right">
<label class="select-label"> <label class="select-label">
Exibir Exibir
<select [(ngModel)]="macrophonyPageSize" (ngModelChange)="onMacrophonyPageSizeChange()"> <select [value]="macrophonyPageSize" (change)="onMacrophonyPageSizeChange($any($event.target).value)">
<option *ngFor="let size of macrophonyPageOptions" [value]="size">{{ size }}</option> <option *ngFor="let size of macrophonyPageOptions" [value]="size">{{ size }}</option>
</select> </select>
</label> </label>
@ -120,7 +120,7 @@
</button> </button>
<button class="btn-icon-text" type="button" (click)="exportMacrophonyCsv()"> <button class="btn-icon-text" type="button" (click)="exportMacrophonyCsv()">
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
<span class="hide-mobile">CSV</span> <span class="hide-mobile">Exportar</span>
</button> </button>
</div> </div>
</div> </div>
@ -428,8 +428,8 @@
<input <input
type="text" type="text"
placeholder="Pesquisar..." placeholder="Pesquisar..."
[(ngModel)]="group.search" [value]="group.search"
(ngModelChange)="onGroupedSearch(group)" /> (input)="onGroupedSearch(group, $any($event.target).value)" />
</div> </div>
<div class="tools-right"> <div class="tools-right">
@ -439,7 +439,7 @@
</button> </button>
<button class="btn-icon-text" type="button" (click)="exportGroupedCsv(group, file)"> <button class="btn-icon-text" type="button" (click)="exportGroupedCsv(group, file)">
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
<span class="hide-mobile">CSV</span> <span class="hide-mobile">Exportar</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -11,7 +11,6 @@ import {
HostBinding HostBinding
} from '@angular/core'; } from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common'; import { isPlatformBrowser, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import type { ChartConfiguration, ChartData, ScriptableContext, TooltipItem } from 'chart.js'; import type { ChartConfiguration, ChartData, ScriptableContext, TooltipItem } from 'chart.js';
@ -70,7 +69,7 @@ type GroupedTableState<T> = { key: string; label: string; table: TableState<T>;
@Component({ @Component({
standalone: true, standalone: true,
imports: [CommonModule, FormsModule], imports: [CommonModule],
templateUrl: './resumo.html', templateUrl: './resumo.html',
styleUrls: ['./resumo.scss'] styleUrls: ['./resumo.scss']
}) })
@ -500,15 +499,32 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
// Métodos obrigatórios para o template funcionar // Métodos obrigatórios para o template funcionar
toggleMacrophonyCompact() { this.macrophonyCompact = !this.macrophonyCompact; } toggleMacrophonyCompact() { this.macrophonyCompact = !this.macrophonyCompact; }
onMacrophonySearch() { this.macrophonyPage = 1; this.updateMacrophonyView(); } onMacrophonySearch(value?: string) {
onMacrophonyPageSizeChange() { this.macrophonyPage = 1; this.updateMacrophonyView(); } if (typeof value === 'string') this.macrophonySearch = value;
this.macrophonyPage = 1;
this.updateMacrophonyView();
}
onMacrophonyPageSizeChange(value?: number | string) {
if (value !== undefined && value !== null) {
const parsed = Number(value);
if (Number.isFinite(parsed) && parsed > 0) {
this.macrophonyPageSize = parsed;
}
}
this.macrophonyPage = 1;
this.updateMacrophonyView();
}
isMacrophonyOpen(key: string) { return this.macrophonyOpen.has(key); } isMacrophonyOpen(key: string) { return this.macrophonyOpen.has(key); }
toggleMacrophonyGroup(key: string) { if (this.macrophonyOpen.has(key)) this.macrophonyOpen.delete(key); else this.macrophonyOpen.add(key); } toggleMacrophonyGroup(key: string) { if (this.macrophonyOpen.has(key)) this.macrophonyOpen.delete(key); else this.macrophonyOpen.add(key); }
openMacrophonyDetail(g: MacrophonyGroup) { this.macrophonyDetailGroup = g; this.macrophonyDetailOpen = true; } openMacrophonyDetail(g: MacrophonyGroup) { this.macrophonyDetailGroup = g; this.macrophonyDetailOpen = true; }
closeMacrophonyDetail() { this.macrophonyDetailOpen = false; this.macrophonyDetailGroup = null; } closeMacrophonyDetail() { this.macrophonyDetailOpen = false; this.macrophonyDetailGroup = null; }
goToMacrophonyPage(p: number) { this.macrophonyPage = p; this.updateMacrophonyView(); } goToMacrophonyPage(p: number) { this.macrophonyPage = p; this.updateMacrophonyView(); }
onGroupedSearch<T>(g: GroupedTableState<T>) { g.page = 1; this.updateGroupView(g); } onGroupedSearch<T>(g: GroupedTableState<T>, value?: string) {
if (typeof value === 'string') g.search = value;
g.page = 1;
this.updateGroupView(g);
}
toggleGroupedCompact<T>(g: GroupedTableState<T>) { g.compact = !g.compact; } toggleGroupedCompact<T>(g: GroupedTableState<T>) { g.compact = !g.compact; }
exportGroupedCsv<T>(g: GroupedTableState<T>, file: string) { this.exportCsv(g.table, file); } exportGroupedCsv<T>(g: GroupedTableState<T>, file: string) { this.exportCsv(g.table, file); }
isGroupedOpen<T>(g: GroupedTableState<T>, key: string) { return g.open.has(key); } isGroupedOpen<T>(g: GroupedTableState<T>, key: string) { return g.open.has(key); }
@ -1037,21 +1053,72 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
if (!isPlatformBrowser(this.platformId)) return; if (!isPlatformBrowser(this.platformId)) return;
const rows = table.data ?? []; const rows = table.data ?? [];
const columns = table.columns ?? []; const columns = table.columns ?? [];
const header = columns.map((c) => c.label); const generatedAt = new Date().toLocaleString('pt-BR');
const body = rows.map((row) => const escapeHtml = (value: string) =>
columns.map((column) => { value
const value = this.formatCell(column, row); .replace(/&/g, '&amp;')
const escaped = String(value).replace(/"/g, '""'); .replace(/</g, '&lt;')
return `"${escaped}"`; .replace(/>/g, '&gt;')
}) .replace(/"/g, '&quot;')
); .replace(/'/g, '&#39;');
const csv = [header.join(';'), ...body.map((line) => line.join(';'))].join('\n'); const headerHtml = columns
const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' }); .map((column) => `<th class="${column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : ''}">${escapeHtml(column.label)}</th>`)
.join('');
const bodyHtml = rows
.map((row, index) => {
const cells = columns
.map((column) => {
const value = this.formatCell(column, row);
const toneClass = column.tone ? this.getToneClass(column.value(row)) : '';
const alignClass = column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : '';
const classes = [alignClass, toneClass].filter(Boolean).join(' ');
return `<td class="${classes}">${escapeHtml(String(value))}</td>`;
})
.join('');
return `<tr class="${index % 2 === 0 ? 'even' : 'odd'}">${cells}</tr>`;
})
.join('');
const html = `<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<style>
body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; color: #0f172a; }
.sheet-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
.sheet-subtitle { font-size: 12px; color: #64748b; margin-bottom: 14px; }
table { border-collapse: collapse; width: 100%; table-layout: auto; }
th, td { border: 1px solid #dbe2ef; padding: 8px 10px; font-size: 12px; }
th { background: #e8eefc; color: #1e3a8a; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; }
tr.even td { background: #ffffff; }
tr.odd td { background: #f8fafc; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-success { color: #047857; font-weight: 700; }
.text-danger { color: #b91c1c; font-weight: 700; }
</style>
</head>
<body>
<div class="sheet-title">${escapeHtml(table.label || 'Resumo')}</div>
<div class="sheet-subtitle">Exportado em ${escapeHtml(generatedAt)} | Total de linhas: ${rows.length}</div>
<table>
<thead>
<tr>${headerHtml}</tr>
</thead>
<tbody>
${bodyHtml}
</tbody>
</table>
</body>
</html>`;
const blob = new Blob([`\uFEFF${html}`], { type: 'application/vnd.ms-excel;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `${filename}.csv`; a.download = `${filename}.xls`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }

View File

@ -9,7 +9,7 @@ import { join } from 'node:path';
import { Readable } from 'node:stream'; 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 apiBaseUrl = (process.env['API_BASE_URL'] || 'http://localhost:5298').replace(/\/+$/, '');
const app = express(); const app = express();
const angularApp = new AngularNodeAppEngine(); const angularApp = new AngularNodeAppEngine();

View File

@ -17,7 +17,13 @@
} }
body { body {
background-color: var(--bg-body); min-height: 100vh;
background:
radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.1), transparent 60%),
radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.06), transparent 60%),
linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
background-attachment: fixed;
background-repeat: no-repeat;
color: var(--text-main); color: var(--text-main);
font-family: var(--font-sans); font-family: var(--font-sans);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -84,31 +90,20 @@ select.form-control-sm {
/* Empurra o conteúdo pra baixo do header fixo */ /* Empurra o conteúdo pra baixo do header fixo */
.app-main.has-header { .app-main.has-header {
position: relative; position: relative;
padding-top: 84px; /* altura segura p/ header (mobile/desktop) */ padding-top: 76px; /* evita vão visual entre o header fixo e o conteúdo */
background: background: transparent;
radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.1), transparent 60%),
radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.06), transparent 60%),
linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.app-main.has-header { .app-main.has-header {
padding-top: 96px; padding-top: 88px;
} }
} }
/* Ajuste para monitores grandes: elimina o "vão" visual entre header e corpo. */ /* Ajuste para monitores grandes: elimina o "vão" visual entre header e corpo. */
@media (min-width: 1400px) { @media (min-width: 1400px) {
.app-main.has-header { .app-main.has-header {
padding-top: 72px; padding-top: 76px;
}
.container-geral,
.container-geral-responsive,
.container-fat,
.container-mureg,
.container-troca {
margin-top: 14px !important;
} }
} }
@ -326,4 +321,3 @@ app-header .modal-card .btn-secondary:hover {
box-shadow: none !important; box-shadow: none !important;
} }
} }