259 lines
11 KiB
HTML
259 lines
11 KiB
HTML
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 10000;">
|
||
<div #successToast class="toast text-bg-danger border-0 shadow" role="alert" aria-live="assertive" aria-atomic="true">
|
||
<div class="toast-header border-bottom-0">
|
||
<strong class="me-auto text-primary">LineGestão</strong>
|
||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
|
||
</div>
|
||
<div class="toast-body bg-white rounded-bottom text-dark">
|
||
{{ toastMessage }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="historico-page">
|
||
<span class="page-blob blob-1" aria-hidden="true"></span>
|
||
<span class="page-blob blob-2" aria-hidden="true"></span>
|
||
<span class="page-blob blob-3" aria-hidden="true"></span>
|
||
<span class="page-blob blob-4" aria-hidden="true"></span>
|
||
|
||
<div class="container-geral-responsive">
|
||
<div class="geral-card">
|
||
<div class="geral-header">
|
||
<div class="header-row-top">
|
||
<div class="title-badge">
|
||
<i class="bi bi-clock-history"></i> Auditoria
|
||
</div>
|
||
|
||
<div class="header-title">
|
||
<h5 class="title mb-0">Histórico</h5>
|
||
<small class="subtitle">Registros de alterações feitas no sistema.</small>
|
||
</div>
|
||
|
||
<div class="header-actions d-flex gap-2 justify-content-end">
|
||
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
||
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filters-card mt-4">
|
||
<div class="filters-head">
|
||
<div class="filters-title">
|
||
<i class="bi bi-funnel"></i>
|
||
<span>Filtros</span>
|
||
</div>
|
||
<div class="filters-actions">
|
||
<button class="btn-primary" type="button" (click)="applyFilters()" [disabled]="loading">
|
||
<i class="bi bi-check2"></i> Aplicar
|
||
</button>
|
||
<button class="btn-ghost" type="button" (click)="clearFilters()" [disabled]="loading">
|
||
<i class="bi bi-x-circle"></i> Limpar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filters-grid">
|
||
<div class="filter-field filter-search">
|
||
<label>Período (De)</label>
|
||
<input type="date" [(ngModel)]="dateFrom" [disabled]="loading" />
|
||
</div>
|
||
<div class="filter-field">
|
||
<label>Período (Até)</label>
|
||
<input type="date" [(ngModel)]="dateTo" [disabled]="loading" />
|
||
</div>
|
||
<div class="filter-field">
|
||
<label>Página</label>
|
||
<app-select
|
||
class="select-glass"
|
||
size="sm"
|
||
[options]="pageOptions"
|
||
labelKey="label"
|
||
valueKey="value"
|
||
placeholder="Todas"
|
||
[(ngModel)]="filterPageName"
|
||
[disabled]="loading">
|
||
</app-select>
|
||
</div>
|
||
<div class="filter-field">
|
||
<label>Ação</label>
|
||
<app-select
|
||
class="select-glass"
|
||
size="sm"
|
||
[options]="actionOptions"
|
||
labelKey="label"
|
||
valueKey="value"
|
||
placeholder="Todas"
|
||
[(ngModel)]="filterAction"
|
||
[disabled]="loading">
|
||
</app-select>
|
||
</div>
|
||
<div class="filter-field filter-user">
|
||
<label>Usuário</label>
|
||
<input type="text" placeholder="Nome ou e-mail do usuário" [(ngModel)]="filterUser" [disabled]="loading" />
|
||
</div>
|
||
<div class="filter-field filter-search">
|
||
<label>Busca geral</label>
|
||
<div class="input-group input-group-sm search-group">
|
||
<span class="input-group-text">
|
||
<i class="bi" [class.bi-search]="!loading" [class.bi-hourglass-split]="loading"></i>
|
||
</span>
|
||
<input
|
||
class="form-control"
|
||
placeholder="Pesquisar..."
|
||
[(ngModel)]="filterSearch"
|
||
(ngModelChange)="onSearchChange()" />
|
||
<button class="btn btn-outline-secondary btn-clear" type="button" (click)="clearSearch()" *ngIf="filterSearch">
|
||
<i class="bi bi-x-lg"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="geral-body">
|
||
<div class="table-wrap">
|
||
<div class="text-center p-5" *ngIf="loading">
|
||
<span class="spinner-border text-brand"></span>
|
||
</div>
|
||
|
||
<div class="alert alert-danger m-4" role="alert" *ngIf="!loading && error">
|
||
{{ errorMsg || 'Erro ao carregar histórico.' }}
|
||
<button class="btn btn-sm btn-outline-danger ms-3" type="button" (click)="refresh()">Tentar novamente</button>
|
||
</div>
|
||
|
||
<div class="empty-group" *ngIf="!loading && !error && logs.length === 0">
|
||
Nenhum log encontrado para os filtros atuais.
|
||
</div>
|
||
|
||
<table class="table table-modern align-middle mb-0" *ngIf="!loading && !error && logs.length > 0">
|
||
<thead>
|
||
<tr>
|
||
<th>Data/Hora</th>
|
||
<th>Usuário</th>
|
||
<th>Página</th>
|
||
<th>Ação</th>
|
||
<th>Item/Entidade</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<ng-container *ngFor="let log of logs; trackBy: trackByLog">
|
||
<tr class="table-row-item" [class.expanded]="expandedLogId === log.id">
|
||
<td class="fw-bold text-muted">{{ formatDateTime(log.occurredAtUtc) }}</td>
|
||
<td>
|
||
<div class="user-cell">
|
||
<span class="user-name">{{ displayUserName(log) }}</span>
|
||
<small class="user-email">{{ log.userEmail || '-' }}</small>
|
||
</div>
|
||
</td>
|
||
<td class="td-clip" [title]="log.page">{{ log.page || '-' }}</td>
|
||
<td>
|
||
<span class="badge-action" [ngClass]="actionClass(log.action)">{{ formatAction(log.action) }}</span>
|
||
</td>
|
||
<td class="entity-col">
|
||
<div class="entity-cell">
|
||
<div class="entity-label td-clip" [title]="displayEntity(log)">
|
||
{{ displayEntity(log) }}
|
||
</div>
|
||
<button
|
||
class="expand-btn"
|
||
type="button"
|
||
(click)="toggleDetails(log, $event)"
|
||
[attr.aria-expanded]="expandedLogId === log.id"
|
||
[attr.aria-label]="expandedLogId === log.id ? 'Fechar detalhes' : 'Abrir detalhes'">
|
||
<i class="bi" [class.bi-chevron-down]="expandedLogId !== log.id" [class.bi-chevron-up]="expandedLogId === log.id"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr class="details-row" *ngIf="expandedLogId === log.id">
|
||
<td colspan="5">
|
||
<div class="details-panel">
|
||
<div class="details-section">
|
||
<div class="section-title">
|
||
<i class="bi bi-pencil-square"></i> Mudanças
|
||
</div>
|
||
<div class="changes-list" *ngIf="log.changes?.length; else noChanges">
|
||
<div class="change-item" *ngFor="let change of log.changes; trackBy: trackByField">
|
||
<div class="change-head">
|
||
<span class="change-field">{{ change.field }}</span>
|
||
<span class="change-type" [ngClass]="changeTypeClass(change.changeType)">
|
||
{{ changeTypeLabel(change.changeType) }}
|
||
</span>
|
||
</div>
|
||
<div class="change-values">
|
||
<span class="old">{{ formatChangeValue(change.oldValue) }}</span>
|
||
<i class="bi bi-arrow-right"></i>
|
||
<span class="new">{{ formatChangeValue(change.newValue) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<ng-template #noChanges>
|
||
<div class="empty-state">Sem mudanças registradas.</div>
|
||
</ng-template>
|
||
</div>
|
||
|
||
<div class="details-section tech">
|
||
<div class="section-title">
|
||
<i class="bi bi-terminal"></i> Detalhes técnicos
|
||
</div>
|
||
<div class="tech-grid">
|
||
<div class="tech-item">
|
||
<span class="tech-label">Método</span>
|
||
<span class="tech-value">{{ log.requestMethod || '-' }}</span>
|
||
</div>
|
||
<div class="tech-item">
|
||
<span class="tech-label">Endpoint</span>
|
||
<span class="tech-value">{{ log.requestPath || '-' }}</span>
|
||
</div>
|
||
<div class="tech-item" *ngIf="log.ipAddress">
|
||
<span class="tech-label">IP</span>
|
||
<span class="tech-value">{{ log.ipAddress }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</ng-container>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="geral-footer">
|
||
<div class="footer-meta">
|
||
<div class="small text-muted fw-bold">Mostrando {{ pageStart }}–{{ pageEnd }} de {{ total }} registros</div>
|
||
<div class="page-size d-flex align-items-center gap-2">
|
||
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
||
<div class="select-wrapper">
|
||
<app-select
|
||
class="select-glass"
|
||
size="sm"
|
||
[options]="pageSizeOptions"
|
||
[(ngModel)]="pageSize"
|
||
(ngModelChange)="onPageSizeChange()"
|
||
[disabled]="loading">
|
||
</app-select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<nav>
|
||
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
||
<li class="page-item" [class.disabled]="page === 1 || loading">
|
||
<button class="page-link" (click)="goToPage(page - 1)">Anterior</button>
|
||
</li>
|
||
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
||
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
||
</li>
|
||
<li class="page-item" [class.disabled]="page === totalPages || loading">
|
||
<button class="page-link" (click)="goToPage(page + 1)">Próxima</button>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|