Responsividade Mobile

This commit is contained in:
Eduardo 2026-02-24 11:05:51 -03:00
parent 1f277b8c8a
commit 96d1b28c19
22 changed files with 2821 additions and 65 deletions

View File

@ -340,7 +340,20 @@
<button type="button" class="btn-action edit" title="Editar" (click)="openEditUser(u); $event.stopPropagation()">
<i class="bi bi-pencil-fill"></i>
</button>
<button type="button" class="btn-action delete" title="Desativar/Excluir" (click)="confirmDeleteUser(u); $event.stopPropagation()">
<button
type="button"
class="btn-action"
[class.edit]="u.ativo === false"
[class.delete]="u.ativo !== false"
[title]="u.ativo === false ? 'Reativar conta' : 'Inativar conta'"
(click)="confirmToggleUserStatus(u); $event.stopPropagation()">
<i class="bi" [class.bi-person-check-fill]="u.ativo === false" [class.bi-person-x-fill]="u.ativo !== false"></i>
</button>
<button
type="button"
class="btn-action delete"
[title]="u.ativo !== false ? 'Inative a conta antes de excluir permanentemente' : 'Excluir permanentemente'"
(click)="confirmPermanentDeleteUser(u); $event.stopPropagation()">
<i class="bi bi-trash-fill"></i>
</button>
</div>
@ -430,6 +443,14 @@
</form>
<div class="manage-actions-footer">
<button
type="button"
class="btn-secondary btn-delete-permanent-left"
(click)="confirmPermanentDeleteUser(target)"
[disabled]="editUserSubmitting"
[title]="target.ativo !== false ? 'Inative a conta antes de excluir permanentemente' : 'Excluir permanentemente'">
Excluir Permanentemente
</button>
<button type="button" class="btn-ghost" (click)="cancelEditUser()" [disabled]="editUserSubmitting">Cancelar</button>
<button type="submit" form="editUserHeaderForm" class="btn-primary" [disabled]="editUserSubmitting || !editUserTarget">
<span *ngIf="!editUserSubmitting">Salvar Alterações</span>

View File

@ -99,6 +99,8 @@ $border-color: #e5e7eb;
/* NOTIFICAÇÕES */
.btn-bell {
position: relative;
&.has-unread { color: $primary; background: rgba(28, 56, 201, 0.06); }
.badge-pulse {
position: absolute; top: 10px; right: 10px; width: 8px; height: 8px; background: $danger; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 0 rgba($danger, 0.7); animation: pulse 2s infinite;
@ -505,6 +507,21 @@ $border-color: #e5e7eb;
input:checked + .slider:before { transform: translateX(20px); }
}
.manage-actions-footer { margin-top: 32px; padding-top: 20px; border-top: 1px solid $border-color; display: flex; justify-content: flex-end; gap: 12px; }
.manage-actions-footer .btn-delete-permanent-left { margin-right: auto; }
.manage-actions-footer .btn-delete-permanent-left {
background: #ef4444;
color: #fff;
border: 1px solid #dc2626;
}
.manage-actions-footer .btn-delete-permanent-left:hover:not(:disabled) {
background: #dc2626;
border-color: #b91c1c;
}
.manage-actions-footer .btn-delete-permanent-left:disabled {
background: #fecaca;
border-color: #fecaca;
color: #7f1d1d;
}
@media (max-width: 900px) {
.manage-body { grid-template-columns: 1fr; overflow-y: auto; }
@ -644,3 +661,381 @@ $border-color: #e5e7eb;
&:hover { background: $bg-light; }
&.active { background: rgba(28, 56, 201, 0.08); color: $primary; }
}
/* ==========================================================================
RESPONSIVIDADE MOBILE/TABLET (HEADER, NOTIFICAÇÕES E MODAIS DO TOPO)
========================================================================== */
@media (max-width: 900px) {
.app-header {
padding: 10px 0;
}
.header-inner {
gap: 12px;
}
.logged-header {
gap: 10px;
}
.left-logged {
gap: 10px;
min-width: 0;
}
.logo-area {
min-width: 0;
.logo-text {
font-size: 16px;
white-space: nowrap;
}
}
.logged-actions {
gap: 8px;
}
.notifications-dropdown {
right: 0;
width: min(360px, calc(100vw - 24px));
max-width: calc(100vw - 24px);
}
.options-dropdown {
right: 0;
width: min(260px, calc(100vw - 24px));
}
.notifications-head {
align-items: flex-start;
.head-title {
min-width: 0;
flex-wrap: wrap;
}
.head-actions {
margin-left: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
}
.notifications-body {
max-height: min(52vh, 420px);
}
}
@media (max-width: 640px) {
.app-header {
padding: 8px 0;
}
.app-header.scrolled {
/* Mobile: não muda a altura do header ao rolar (evita "vão" no topo ao voltar) */
padding: 8px 0;
}
.header-inner {
gap: 8px;
}
.logged-header {
gap: 8px;
}
.btn-icon {
width: 36px;
height: 36px;
i {
font-size: 18px;
}
}
.btn-bell .badge-pulse {
top: 8px;
right: 8px;
}
.left-logged {
gap: 8px;
flex: 1 1 auto;
}
.logo-area {
gap: 8px;
min-width: 0;
.logo-icon {
width: 32px;
height: 32px;
font-size: 16px;
}
.logo-text {
font-size: 14px;
line-height: 1;
}
}
.logged-actions {
gap: 6px;
}
.user-trigger {
padding: 2px;
.user-avatar {
width: 30px;
height: 30px;
}
}
.notifications-dropdown {
position: fixed;
top: calc(var(--app-header-offset, 76px) + 8px);
left: 8px;
right: 8px;
width: auto;
max-width: none;
border-radius: 16px;
transform-origin: top center;
z-index: 1250;
}
.options-dropdown {
position: fixed;
top: calc(var(--app-header-offset, 76px) + 8px);
right: 8px;
width: min(260px, calc(100vw - 16px));
z-index: 1250;
}
.notifications-head {
padding: 12px;
flex-wrap: wrap;
gap: 10px;
.head-title {
width: 100%;
justify-content: space-between;
gap: 8px;
font-size: 13px;
}
.head-actions {
width: 100%;
margin-left: 0;
justify-content: space-between;
gap: 6px;
}
.head-btn {
font-size: 10px;
padding: 5px 7px;
gap: 4px;
}
.see-all {
margin-left: auto;
font-size: 11px;
}
}
.notifications-tabs {
padding: 8px 12px;
flex-direction: column;
gap: 8px;
}
.notif-tab {
width: 100%;
padding: 9px 10px;
text-align: center;
}
.notifications-state {
margin: 8px 12px 6px;
padding: 8px 9px;
font-size: 11px;
}
.notifications-body {
max-height: calc(100dvh - 220px);
}
.notifications-empty {
padding: 20px 12px;
}
.notification-item {
gap: 10px;
padding: 12px;
align-items: flex-start;
.notif-content {
min-width: 0;
}
.notif-title-line {
white-space: normal;
flex-wrap: wrap;
row-gap: 2px;
}
.notif-client {
max-width: 100%;
}
.notif-desc {
flex-wrap: wrap;
align-items: baseline;
}
.notif-meta-line {
flex-wrap: wrap;
row-gap: 2px;
}
.notif-restore-btn {
margin-left: 0;
margin-top: 6px;
align-self: flex-start;
}
}
.modal-card {
width: min(100vw - 12px, 420px);
max-height: calc(100dvh - 12px);
border-radius: 14px;
}
.modal-card.manage-users-modal {
width: calc(100vw - 12px);
height: min(calc(100dvh - 12px), 680px);
max-height: calc(100dvh - 12px);
border-radius: 14px;
}
.manage-body {
height: 100%;
}
.manage-left {
height: auto;
max-height: 38vh;
}
.manage-right-wrapper {
height: auto;
min-height: 0;
}
.manage-right {
padding: 14px;
}
.edit-header-info {
gap: 10px;
margin-bottom: 14px;
.avatar-large {
width: 42px;
height: 42px;
font-size: 14px;
}
.info-text h4 {
font-size: 14px;
}
.info-text span {
font-size: 12px;
}
}
.manage-actions-footer {
margin-top: 18px;
padding-top: 14px;
gap: 8px;
.btn-delete-permanent-left {
margin-right: 0;
}
}
.list-footer {
flex-wrap: wrap;
gap: 8px;
.pagination {
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
}
.side-menu {
width: min(88vw, 320px);
}
}
@media (max-width: 420px) {
.logo-area .logo-text {
display: none;
}
.logged-actions {
gap: 4px;
}
.user-trigger {
.chevron {
display: none;
}
}
.notifications-dropdown {
left: 6px;
right: 6px;
top: calc(var(--app-header-offset, 76px) + 6px);
}
.options-dropdown {
right: 6px;
top: calc(var(--app-header-offset, 76px) + 6px);
}
.notifications-head {
.head-actions {
justify-content: stretch;
}
.head-btn {
flex: 1 1 100%;
justify-content: center;
}
.see-all {
width: 100%;
text-align: center;
margin-left: 0;
padding-top: 2px;
}
}
.notification-item {
.icon-circle {
width: 32px;
height: 32px;
font-size: 14px;
}
.notif-meta-line {
gap: 4px;
font-size: 11px;
}
}
}

View File

@ -10,6 +10,7 @@ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractContro
import { HttpErrorResponse } from '@angular/common/http';
import { CustomSelectComponent } from '../custom-select/custom-select';
import { Subscription } from 'rxjs';
import { confirmActionModal, confirmDeletionWithTyping, showDeletionWarning } from '../../utils/destructive-confirmation';
@Component({
selector: 'app-header',
@ -20,6 +21,8 @@ import { Subscription } from 'rxjs';
})
export class Header implements AfterViewInit, OnDestroy {
isScrolled = false;
private headerResizeObserver?: ResizeObserver;
private headerTransitionEndCleanup?: (() => void) | null = null;
menuOpen = false;
optionsOpen = false;
@ -56,6 +59,7 @@ export class Header implements AfterViewInit, OnDestroy {
manageUsersLoading = false;
manageUsersErrors: ApiFieldError[] = [];
manageUsersSuccess = '';
private manageUsersFeedbackTimer: ReturnType<typeof setTimeout> | null = null;
manageUsers: any[] = [];
manageSearch = '';
managePage = 1;
@ -90,6 +94,7 @@ export class Header implements AfterViewInit, OnDestroy {
private notificationsService: NotificationsService,
private usersService: UsersService,
private fb: FormBuilder,
private hostElement: ElementRef<HTMLElement>,
@Inject(PLATFORM_ID) private platformId: object
) {
this.createUserForm = this.fb.group(
@ -432,6 +437,13 @@ export class Header implements AfterViewInit, OnDestroy {
onWindowScroll() {
if (!isPlatformBrowser(this.platformId)) return;
this.isScrolled = window.scrollY > 10;
this.scheduleHeaderOffsetSync();
}
@HostListener('window:resize', [])
onWindowResize() {
if (!isPlatformBrowser(this.platformId)) return;
this.scheduleHeaderOffsetSync();
}
@HostListener('document:click', [])
@ -464,6 +476,7 @@ export class Header implements AfterViewInit, OnDestroy {
}
ngAfterViewInit() {
this.setupHeaderOffsetTracking();
if (this.pendingToastCheck) {
this.pendingToastCheck = false;
void this.maybeShowVigenciaToast();
@ -528,6 +541,14 @@ export class Header implements AfterViewInit, OnDestroy {
}
ngOnDestroy(): void {
this.headerResizeObserver?.disconnect();
this.headerResizeObserver = undefined;
this.headerTransitionEndCleanup?.();
this.headerTransitionEndCleanup = null;
if (this.manageUsersFeedbackTimer) {
clearTimeout(this.manageUsersFeedbackTimer);
this.manageUsersFeedbackTimer = null;
}
this.subs.unsubscribe();
}
@ -745,13 +766,25 @@ export class Header implements AfterViewInit, OnDestroy {
this.editUserErrors = [];
this.editUserSuccess = '';
const currentTarget = { ...this.editUserTarget };
this.usersService.update(this.editUserTarget.id, payload).subscribe({
next: (updated) => {
next: () => {
const merged = this.mergeUserUpdate(currentTarget, payload);
this.editUserSubmitting = false;
this.setEditFormDisabled(false);
this.editUserSuccess = `Usuario ${updated.nome} atualizado.`;
this.editUserTarget = updated;
this.fetchManageUsers(this.managePage);
this.editUserSuccess = `Usuario ${merged.nome} atualizado com sucesso.`;
this.editUserTarget = merged;
this.editUserForm.patchValue({
nome: merged.nome ?? '',
email: merged.email ?? '',
permissao: merged.permissao ?? '',
ativo: merged.ativo ?? true,
senha: '',
confirmarSenha: '',
});
this.upsertManageUser(merged);
this.showManageUsersFeedback(`Usuario ${merged.nome} atualizado com sucesso.`, 'success');
},
error: (err: HttpErrorResponse) => {
this.editUserSubmitting = false;
@ -765,14 +798,81 @@ export class Header implements AfterViewInit, OnDestroy {
} else {
this.editUserErrors = [{ message: err?.error?.message || 'Erro ao atualizar usuario.' }];
}
this.showManageUsersFeedback(this.editUserErrors[0]?.message || 'Erro ao atualizar usuario.', 'error');
},
});
}
confirmDeleteUser(user: any) {
if (!confirm(`Excluir usuario ${user.nome}?`)) return;
this.usersService.update(user.id, { ativo: false }).subscribe({
next: () => this.fetchManageUsers(this.managePage),
async confirmToggleUserStatus(user: any) {
const nextActive = user.ativo === false;
const actionLabel = nextActive ? 'reativar' : 'inativar';
const confirmed = await confirmActionModal({
title: nextActive ? 'Reativar Usuário' : 'Inativar Usuário',
message: nextActive
? `Deseja reativar o usuário ${user.nome}? Ele voltará a ter acesso ao sistema.`
: `Deseja inativar o usuário ${user.nome}? A conta ficará sem acesso até ser reativada.`,
confirmLabel: nextActive ? 'Reativar' : 'Inativar',
tone: nextActive ? 'neutral' : 'warning',
});
if (!confirmed) return;
this.usersService.update(user.id, { ativo: nextActive }).subscribe({
next: () => {
const updated = this.mergeUserUpdate(user, { ativo: nextActive });
this.upsertManageUser(updated);
if (this.editUserTarget?.id === user.id) {
this.editUserTarget = { ...this.editUserTarget, ativo: nextActive };
this.editUserForm.patchValue({ ativo: nextActive, senha: '', confirmarSenha: '' });
this.editUserErrors = [];
this.editUserSuccess = `Usuario ${user.nome} ${nextActive ? 'reativado' : 'inativado'} com sucesso.`;
}
},
error: (err: HttpErrorResponse) => {
const message = err?.error?.message || `Erro ao ${actionLabel} usuario.`;
if (this.editUserTarget?.id === user.id) {
this.editUserSuccess = '';
this.editUserErrors = [{ message }];
}
},
});
}
async confirmPermanentDeleteUser(user: any) {
if (user?.ativo !== false) {
const message = 'Inative a conta antes de excluir permanentemente.';
if (this.editUserTarget?.id === user?.id) {
this.editUserSuccess = '';
this.editUserErrors = [{ message }];
} else {
await showDeletionWarning(message, 'Exclusão não permitida');
}
return;
}
const confirmed = await confirmDeletionWithTyping(`o usuário ${user.nome}`);
if (!confirmed) return;
this.usersService.delete(user.id).subscribe({
next: () => {
this.removeManageUser(user.id);
if (this.editUserTarget?.id === user.id) {
this.cancelEditUser();
}
},
error: (err: HttpErrorResponse) => {
const apiErrors = err?.error?.errors;
const message = Array.isArray(apiErrors)
? (apiErrors[0]?.message || 'Erro ao excluir usuario.')
: (err?.error?.message || 'Erro ao excluir usuario.');
if (this.editUserTarget?.id === user.id) {
this.editUserSuccess = '';
this.editUserErrors = [{ message }];
return;
}
void showDeletionWarning(message, 'Falha na exclusão');
},
});
}
@ -801,6 +901,10 @@ export class Header implements AfterViewInit, OnDestroy {
}
private resetManageUsersState() {
if (this.manageUsersFeedbackTimer) {
clearTimeout(this.manageUsersFeedbackTimer);
this.manageUsersFeedbackTimer = null;
}
this.manageUsersErrors = [];
this.manageUsersSuccess = '';
this.manageUsersLoading = false;
@ -825,10 +929,107 @@ export class Header implements AfterViewInit, OnDestroy {
else this.editUserForm.enable({ emitEvent: false });
}
private upsertManageUser(user: any) {
this.manageUsers = this.manageUsers.map((item) =>
item.id === user.id ? { ...item, ...user } : item
);
}
private removeManageUser(userId: string) {
const prevLength = this.manageUsers.length;
this.manageUsers = this.manageUsers.filter((item) => item.id !== userId);
if (this.manageUsers.length !== prevLength && this.manageTotal > 0) {
this.manageTotal = Math.max(0, this.manageTotal - 1);
}
if (!this.manageUsers.length && this.managePage > 1) {
this.fetchManageUsers(this.managePage - 1);
}
}
private mergeUserUpdate(current: any, payload: any) {
return {
...current,
nome: payload.nome ?? current.nome,
email: payload.email ? String(payload.email).trim().toLowerCase() : current.email,
permissao: payload.permissao ?? current.permissao,
ativo: payload.ativo ?? current.ativo,
};
}
private showManageUsersFeedback(message: string, type: 'success' | 'error') {
if (this.manageUsersFeedbackTimer) {
clearTimeout(this.manageUsersFeedbackTimer);
this.manageUsersFeedbackTimer = null;
}
if (type === 'success') {
this.manageUsersErrors = [];
this.manageUsersSuccess = message;
} else {
this.manageUsersSuccess = '';
this.manageUsersErrors = [{ message }];
}
this.manageUsersFeedbackTimer = setTimeout(() => {
this.manageUsersSuccess = '';
this.manageUsersErrors = [];
this.manageUsersFeedbackTimer = null;
}, 3000);
}
private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {
const senha = group.get('senha')?.value;
const confirmar = group.get('confirmarSenha')?.value;
if (!senha || !confirmar) return null;
return senha === confirmar ? null : { passwordsMismatch: true };
}
private setupHeaderOffsetTracking() {
if (!isPlatformBrowser(this.platformId)) return;
this.syncHeaderOffsetCssVar();
const headerEl = this.getHeaderElement();
if (!headerEl || typeof ResizeObserver === 'undefined') return;
this.headerResizeObserver?.disconnect();
this.headerResizeObserver = new ResizeObserver(() => {
this.syncHeaderOffsetCssVar();
});
this.headerResizeObserver.observe(headerEl);
// Garante sincronismo do offset após a animação de "scrolled" do header.
this.headerTransitionEndCleanup?.();
const onTransitionEnd = (ev: TransitionEvent) => {
if (ev.target !== headerEl) return;
if (ev.propertyName && ev.propertyName !== 'padding-top' && ev.propertyName !== 'padding-bottom') {
return;
}
this.syncHeaderOffsetCssVar();
};
headerEl.addEventListener('transitionend', onTransitionEnd);
this.headerTransitionEndCleanup = () => headerEl.removeEventListener('transitionend', onTransitionEnd);
}
private scheduleHeaderOffsetSync() {
if (!isPlatformBrowser(this.platformId)) return;
requestAnimationFrame(() => this.syncHeaderOffsetCssVar());
}
private syncHeaderOffsetCssVar() {
if (!isPlatformBrowser(this.platformId)) return;
const headerEl = this.getHeaderElement();
if (!headerEl) return;
const height = Math.ceil(headerEl.getBoundingClientRect().height || headerEl.offsetHeight || 0);
if (height > 0) {
document.documentElement.style.setProperty('--app-header-offset', `${height}px`);
}
}
private getHeaderElement(): HTMLElement | null {
return this.hostElement.nativeElement.querySelector('.app-header');
}
}

View File

@ -258,6 +258,82 @@
&.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); }
}
@media (min-width: 701px) {
.filters-row .filter-item .select-wrapper,
.filters-row .filter-item app-select.select-glass {
width: clamp(112px, 8vw, 148px);
}
}
@media (max-width: 768px) {
.tab-row {
margin-top: 12px;
gap: 6px;
align-items: stretch;
}
.tab-btn {
flex: 1 1 0;
min-width: 0;
justify-content: center;
padding: 8px 10px;
gap: 6px;
line-height: 1.08;
text-align: center;
white-space: normal;
text-wrap: balance;
}
.tab-btn i {
flex: 0 0 auto;
font-size: 0.95rem;
}
}
@media (max-width: 700px) {
.filters-row {
gap: 10px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.filters-row .filter-item {
width: auto;
min-width: 0;
display: flex;
justify-content: center;
}
.filters-row .filter-item .select-wrapper,
.filters-row .filter-item app-select.select-glass {
width: clamp(112px, 34vw, 148px);
max-width: calc(100vw - 32px);
}
.filters-row .filter-tabs {
width: fit-content;
max-width: calc(100vw - 32px);
justify-content: center;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
padding: 4px;
margin-inline: auto;
align-self: center;
}
.filters-row .filter-tabs .filter-tab {
flex: 0 0 auto;
white-space: nowrap;
padding: 5px 8px;
font-size: 0.76rem;
border-radius: 7px;
}
}
/* Select */
.select-wrapper { position: relative; display: inline-block; min-width: 90px; }
.select-glass {
@ -629,11 +705,60 @@ summary.box-header {
cursor: pointer;
user-select: none;
list-style: none;
padding: 10px 16px;
font-size: 0.8rem;
font-weight: 800;
text-transform: uppercase;
color: var(--muted);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
background: #fdfdfd;
display: flex;
align-items: center;
border-radius: 14px 14px 0 0;
transition: background 0.2s ease, color 0.2s ease;
span {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
line-height: 1.2;
white-space: normal;
overflow-wrap: anywhere;
}
i:not(.transition-icon) { color: var(--brand); margin-right: 6px; }
&::-webkit-details-marker { display: none; }
}
.modal-card.create-modal summary.box-header {
padding: 11px 14px;
border-bottom-color: rgba(227, 61, 207, 0.08);
background: linear-gradient(135deg, rgba(227, 61, 207, 0.10), rgba(3, 15, 170, 0.06));
span {
font-size: 0.76rem;
line-height: 1.2;
color: rgba(17, 18, 20, 0.84);
letter-spacing: 0.04em;
font-weight: 900;
}
i:not(.transition-icon) {
width: 22px;
height: 22px;
margin-right: 0;
border-radius: 7px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(227, 61, 207, 0.12);
color: var(--brand);
flex-shrink: 0;
}
}
.transition-icon { color: var(--muted); transition: transform 0.25s ease, color 0.25s ease; }
details[open] .transition-icon { transform: rotate(180deg); color: var(--brand); }

View File

@ -5,6 +5,7 @@ import { HttpClient } from '@angular/common/http';
import { ChipsControleService, ChipVirgemListDto, ControleRecebidoListDto, SortDir, UpdateChipVirgemRequest, UpdateControleRecebidoRequest, CreateChipVirgemRequest, CreateControleRecebidoRequest } from '../../services/chips-controle.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { AuthService } from '../../services/auth.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
// Interface para o Agrupamento
interface ChipGroup {
@ -328,8 +329,9 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
this.chipDeleteTarget = null;
}
confirmChipDelete() {
async confirmChipDelete() {
if (!this.chipDeleteTarget) return;
if (!(await confirmDeletionWithTyping('este chip virgem'))) return;
const id = this.chipDeleteTarget.id;
this.service.removeChipVirgem(id).subscribe({
next: () => {
@ -667,8 +669,9 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
this.controleDeleteTarget = null;
}
confirmControleDelete() {
async confirmControleDelete() {
if (!this.controleDeleteTarget) return;
if (!(await confirmDeletionWithTyping('este registro de controle recebido'))) return;
const id = this.controleDeleteTarget.id;
this.service.removeControleRecebido(id).subscribe({
next: () => {

View File

@ -70,11 +70,11 @@
</div>
<div class="controls mt-3 mb-2" data-animate>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm" [class.btn-brand]="tipoFilter === 'PF'" [class.btn-outline-secondary]="tipoFilter !== 'PF'" (click)="setTipoFilter('PF')">
<div class="filter-tabs tipo-filter-tabs">
<button type="button" class="filter-tab" [class.active]="tipoFilter === 'PF'" (click)="setTipoFilter('PF')">
Pessoa Física
</button>
<button type="button" class="btn btn-sm" [class.btn-brand]="tipoFilter === 'PJ'" [class.btn-outline-secondary]="tipoFilter !== 'PJ'" (click)="setTipoFilter('PJ')">
<button type="button" class="filter-tab" [class.active]="tipoFilter === 'PJ'" (click)="setTipoFilter('PJ')">
Pessoa Jurídica
</button>
</div>
@ -270,13 +270,18 @@
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
</summary>
<div class="box-body">
<div class="form-grid">
<div class="form-grid user-modal-grid">
<div class="form-field span-2"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cliente" /></div>
<div class="form-field"><label>Tipo</label>
<select class="form-control form-control-sm" [(ngModel)]="createModel.tipoPessoa" (ngModelChange)="onCreateTipoChange()">
<option value="PF">Pessoa Física</option>
<option value="PJ">Pessoa Jurídica</option>
</select>
<div class="form-field field-tipo"><label>Tipo</label>
<app-select
class="form-select"
size="sm"
[options]="tipoPessoaOptions"
labelKey="label"
valueKey="value"
[(ngModel)]="createModel.tipoPessoa"
(ngModelChange)="onCreateTipoChange()">
</app-select>
</div>
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'">
<label>Nome</label>
@ -286,15 +291,19 @@
<label>Razão Social</label>
<input class="form-control form-control-sm" [(ngModel)]="createModel.razaoSocial" />
</div>
<div class="form-field"><label>Linha</label><input class="form-control form-control-sm" [(ngModel)]="createModel.linha" /></div>
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="createModel.item" /></div>
<div class="form-field" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'"><label>CPF</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cpf" /></div>
<div class="form-field" *ngIf="(createModel.tipoPessoa || 'PF') === 'PJ'"><label>CNPJ</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cnpj" /></div>
<div class="form-field"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="createModel.rg" /></div>
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="createModel.linha" /></div>
<div class="form-field field-item field-auto">
<label>Item (Automático)</label>
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
<small class="field-hint">Gerado automaticamente pelo sistema</small>
</div>
<div class="form-field field-cpf-cnpj" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'"><label>CPF</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cpf" /></div>
<div class="form-field field-cpf-cnpj" *ngIf="(createModel.tipoPessoa || 'PF') === 'PJ'"><label>CNPJ</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cnpj" /></div>
<div class="form-field field-rg"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="createModel.rg" /></div>
<div class="form-field span-2"><label>E-mail</label><input class="form-control form-control-sm" [(ngModel)]="createModel.email" /></div>
<div class="form-field span-2"><label>Endereço</label><input class="form-control form-control-sm" [(ngModel)]="createModel.endereco" /></div>
<div class="form-field"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="createModel.celular" /></div>
<div class="form-field"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="createModel.telefoneFixo" /></div>
<div class="form-field field-celular"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="createModel.celular" /></div>
<div class="form-field field-telefone"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="createModel.telefoneFixo" /></div>
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'"><label>Data de Nascimento</label><input class="form-control form-control-sm" type="date" [(ngModel)]="createDateNascimento" /></div>
</div>
</div>
@ -327,13 +336,18 @@
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
</summary>
<div class="box-body">
<div class="form-grid">
<div class="form-grid user-modal-grid">
<div class="form-field span-2"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" /></div>
<div class="form-field"><label>Tipo</label>
<select class="form-control form-control-sm" [(ngModel)]="editModel.tipoPessoa" (ngModelChange)="onEditTipoChange()">
<option value="PF">Pessoa Física</option>
<option value="PJ">Pessoa Jurídica</option>
</select>
<div class="form-field field-tipo"><label>Tipo</label>
<app-select
class="form-select"
size="sm"
[options]="tipoPessoaOptions"
labelKey="label"
valueKey="value"
[(ngModel)]="editModel.tipoPessoa"
(ngModelChange)="onEditTipoChange()">
</app-select>
</div>
<div class="form-field span-2" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
<label>Nome</label>
@ -343,17 +357,21 @@
<label>Razão Social</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.razaoSocial" />
</div>
<div class="form-field"><label>Linha</label><input class="form-control form-control-sm" [(ngModel)]="editModel.linha" /></div>
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.item" /></div>
<div class="form-field" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="editModel.linha" /></div>
<div class="form-field field-item field-auto">
<label>Item (Automático)</label>
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" readonly title="Gerado automaticamente pelo sistema" />
<small class="field-hint">Gerado automaticamente pelo sistema</small>
</div>
<div class="form-field field-cpf-cnpj" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
<label>CPF</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.cpf" />
</div>
<div class="form-field" *ngIf="(editModel.tipoPessoa || 'PF') === 'PJ'">
<div class="form-field field-cpf-cnpj" *ngIf="(editModel.tipoPessoa || 'PF') === 'PJ'">
<label>CNPJ</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.cnpj" />
</div>
<div class="form-field"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="editModel.rg" /></div>
<div class="form-field field-rg"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="editModel.rg" /></div>
</div>
</div>
</details>
@ -364,10 +382,10 @@
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
</summary>
<div class="box-body">
<div class="form-grid">
<div class="form-grid user-modal-grid contact-modal-grid">
<div class="form-field span-2"><label>E-mail</label><input class="form-control form-control-sm" type="email" [(ngModel)]="editModel.email" /></div>
<div class="form-field"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="editModel.celular" /></div>
<div class="form-field"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="editModel.telefoneFixo" /></div>
<div class="form-field field-celular"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="editModel.celular" /></div>
<div class="form-field field-telefone"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="editModel.telefoneFixo" /></div>
<div class="form-field span-2"><label>Endereço</label><input class="form-control form-control-sm" [(ngModel)]="editModel.endereco" /></div>
</div>
</div>

View File

@ -192,6 +192,74 @@
/* Controls */
.controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
.filter-tabs {
display: flex;
gap: 4px;
padding: 4px;
background: rgba(255, 255, 255, 0.62);
border: 1px solid rgba(17, 18, 20, 0.08);
border-radius: 12px;
backdrop-filter: blur(8px);
box-shadow: 0 2px 8px rgba(17, 18, 20, 0.04);
}
.filter-tab {
border: 1px solid transparent;
background: transparent;
padding: 8px 14px;
border-radius: 8px;
font-size: 0.84rem;
font-weight: 800;
color: rgba(17, 18, 20, 0.62);
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
white-space: nowrap;
min-height: 34px;
&:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.72);
border-color: rgba(17, 18, 20, 0.08);
transform: translateY(-1px);
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.14);
}
&.active {
background: #fff;
color: var(--brand);
border-color: rgba(227, 61, 207, 0.16);
box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15);
}
}
.tipo-filter-tabs {
.filter-tab {
min-width: 122px;
}
}
@media (max-width: 700px) {
.tipo-filter-tabs {
width: 100%;
justify-content: stretch;
}
.tipo-filter-tabs .filter-tab {
flex: 1 1 0;
min-width: 0;
padding: 8px 10px;
font-size: 0.8rem;
}
}
.search-group {
max-width: 270px; border-radius: 12px; overflow: hidden; display: flex; align-items: stretch; background: #fff; border: 1px solid rgba(17, 18, 20, 0.15); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); transition: all 0.2s ease;
&:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); transform: translateY(-1px); }
@ -326,6 +394,122 @@
.modal-body { padding: 16px; }
.modal-card.create-modal .modal-footer { flex-direction: column-reverse; }
.modal-card.create-modal .modal-footer .btn { width: 100%; min-width: 0; }
.modal-card.create-modal summary.box-header {
padding: 10px 12px;
span {
font-size: 0.72rem;
line-height: 1.2;
gap: 7px;
}
i:not(.transition-icon) {
width: 20px;
height: 20px;
border-radius: 6px;
font-size: 0.8rem;
}
}
.form-field.field-line {
order: initial;
grid-column: span 2;
}
.form-field.field-line .form-control {
min-height: 42px;
font-size: 0.95rem;
font-weight: 700;
letter-spacing: 0.02em;
font-variant-numeric: tabular-nums;
}
.form-field.field-item {
align-items: flex-start;
}
.form-field.field-item .form-control {
width: 100%;
max-width: none;
min-height: 38px;
text-align: left;
font-size: 0.82rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.user-modal-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
gap: 10px;
}
.user-modal-grid .span-2,
.user-modal-grid .field-tipo,
.user-modal-grid .field-line,
.user-modal-grid .field-rg {
grid-column: span 2 !important;
}
.user-modal-grid .field-item,
.user-modal-grid .field-cpf-cnpj,
.user-modal-grid .field-celular,
.user-modal-grid .field-telefone {
grid-column: span 1 !important;
min-width: 0;
}
.user-modal-grid .field-item .field-hint {
display: none;
}
.user-modal-grid .field-tipo .form-control {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
min-height: 42px;
border-radius: 12px;
border-color: rgba(3, 15, 170, 0.18);
background:
linear-gradient(180deg, rgba(255,255,255,0.98), rgba(247,248,251,0.98));
box-shadow: 0 2px 8px rgba(3, 15, 170, 0.06);
padding-right: 34px;
font-weight: 800;
color: var(--blue);
background-image:
linear-gradient(45deg, transparent 50%, rgba(17,18,20,0.55) 50%),
linear-gradient(135deg, rgba(17,18,20,0.55) 50%, transparent 50%);
background-position:
calc(100% - 16px) calc(50% - 2px),
calc(100% - 11px) calc(50% - 2px);
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.user-modal-grid .field-tipo .form-control:focus {
border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(227,61,207,0.14);
}
.user-modal-grid .field-cpf-cnpj .form-control,
.user-modal-grid .field-item .form-control {
min-height: 40px;
font-size: 0.84rem;
font-variant-numeric: tabular-nums;
}
.user-modal-grid .field-rg .form-control {
min-height: 40px;
font-size: 0.88rem;
max-width: 100%;
}
.contact-modal-grid .field-celular .form-control,
.contact-modal-grid .field-telefone .form-control {
width: 100%;
min-width: 0;
min-height: 40px;
}
}
/* FORM & DETAILS */
@ -353,6 +537,8 @@ summary.box-header {
cursor: pointer;
user-select: none;
list-style: none;
border-radius: 14px 14px 0 0;
transition: background 0.2s ease, color 0.2s ease;
i:not(.transition-icon) {
color: var(--brand);
@ -362,6 +548,41 @@ summary.box-header {
&::-webkit-details-marker { display: none; }
}
.modal-card.create-modal summary.box-header {
padding: 11px 14px;
border-bottom-color: rgba(227, 61, 207, 0.08);
background: linear-gradient(135deg, rgba(227, 61, 207, 0.10), rgba(3, 15, 170, 0.06));
span {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
font-size: 0.76rem;
line-height: 1.2;
color: rgba(17, 18, 20, 0.84);
text-transform: uppercase;
letter-spacing: 0.04em;
font-weight: 900;
white-space: normal;
overflow-wrap: anywhere;
}
i:not(.transition-icon) {
width: 22px;
height: 22px;
margin-right: 0;
border-radius: 7px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(227, 61, 207, 0.12);
color: var(--brand);
flex-shrink: 0;
}
}
.transition-icon { transition: transform 0.25s ease, color 0.25s ease; color: var(--muted); }
details[open] .transition-icon { transform: rotate(180deg); color: var(--brand); }
@ -391,6 +612,24 @@ details[open] .transition-icon { transform: rotate(180deg); color: var(--brand);
&.span-2 { grid-column: span 2; }
}
.field-hint {
display: block;
font-size: 0.66rem;
line-height: 1.2;
color: rgba(17, 18, 20, 0.52);
font-weight: 700;
}
.form-field.field-auto .form-control {
background: rgba(245, 245, 247, 0.9);
border-color: rgba(17, 18, 20, 0.1);
color: rgba(17, 18, 20, 0.72);
}
.form-field.field-auto .form-control[readonly] {
cursor: default;
}
.details-dashboard .form-field > div {
border: 1px solid rgba(17, 18, 20, 0.08);
border-radius: 12px;

View File

@ -15,6 +15,7 @@ import {
} from '../../services/dados-usuarios.service';
import { AuthService } from '../../services/auth.service';
import { LinesService, MobileLineDetail } from '../../services/lines.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
type ViewMode = 'lines' | 'groups';
@ -26,6 +27,11 @@ interface LineOptionDto {
label?: string;
}
interface SimpleOption {
label: string;
value: string;
}
@Component({
selector: 'app-dados-usuarios',
standalone: true,
@ -90,6 +96,10 @@ export class DadosUsuarios implements OnInit {
createDateNascimento = '';
clientsFromGeral: string[] = [];
lineOptionsCreate: LineOptionDto[] = [];
readonly tipoPessoaOptions: SimpleOption[] = [
{ label: 'Pessoa Física', value: 'PF' },
{ label: 'Pessoa Jurídica', value: 'PJ' },
];
createClientsLoading = false;
createLinesLoading = false;
@ -532,8 +542,9 @@ export class DadosUsuarios implements OnInit {
this.deleteTarget = null;
}
confirmDelete() {
async confirmDelete() {
if (!this.deleteTarget) return;
if (!(await confirmDeletionWithTyping('este registro de dados do usuário'))) return;
const id = this.deleteTarget.id;
this.service.remove(id).subscribe({
next: () => {

View File

@ -24,6 +24,7 @@ import {
BillingUpdateRequest
} from '../../services/billing';
import { AuthService } from '../../services/auth.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
interface BillingClientGroup {
cliente: string;
@ -695,8 +696,9 @@ export class Faturamento implements AfterViewInit, OnDestroy {
this.cdr.detectChanges();
}
confirmDelete() {
async confirmDelete() {
if (!this.deleteTarget) return;
if (!(await confirmDeletionWithTyping('este registro de faturamento'))) return;
const id = this.deleteTarget.id;
this.billing.remove(id).subscribe({
next: () => {

View File

@ -302,7 +302,7 @@
<div class="group-body" *ngIf="expandedGroup === group.cliente">
<div class="d-flex justify-content-between align-items-center px-4 py-2 border-bottom bg-white">
<small class="text-muted fw-bold">Gerenciar Grupo</small>
<button class="btn btn-sm btn-primary" (click)="onAddLineToGroup(group.cliente)">
<button class="btn btn-sm btn-add-line-group" (click)="onAddLineToGroup(group.cliente)">
<i class="bi bi-plus-lg me-1"></i> Adicionar Linha
</button>
</div>

View File

@ -287,6 +287,55 @@
.group-toggle-icon { font-size: 1.2rem; color: var(--muted); transition: transform 0.3s ease; }
.client-group-card.expanded .group-toggle-icon { transform: rotate(180deg); color: var(--brand); }
.group-body { border-top: 1px solid rgba(17,18,20,0.06); background: #fbfbfc; animation: slideDown 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
.btn-add-line-group {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
border: 1px solid transparent;
border-radius: 12px;
padding: 0.42rem 0.85rem;
line-height: 1;
white-space: nowrap;
font-weight: 800;
letter-spacing: 0.01em;
color: #fff;
background:
linear-gradient(135deg, rgba(227, 61, 207, 0.95), rgba(3, 15, 170, 0.95));
background-clip: padding-box;
box-shadow:
0 10px 22px rgba(3, 15, 170, 0.16),
inset 0 1px 0 rgba(255,255,255,0.2);
transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
i {
font-weight: 900;
}
&:hover {
color: #fff;
transform: translateY(-1px);
filter: saturate(1.05) brightness(1.02);
box-shadow:
0 14px 26px rgba(227, 61, 207, 0.18),
0 8px 18px rgba(3, 15, 170, 0.14);
}
&:focus-visible {
outline: none;
box-shadow:
0 0 0 3px rgba(255, 255, 255, 0.95),
0 0 0 6px rgba(227, 61, 207, 0.28),
0 12px 24px rgba(3, 15, 170, 0.16);
}
&:active {
transform: translateY(0);
box-shadow:
0 8px 16px rgba(3, 15, 170, 0.14),
inset 0 1px 0 rgba(255,255,255,0.16);
}
}
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
/* Inner Table Destravada */

View File

@ -4,6 +4,7 @@ import {
ViewChild,
Inject,
PLATFORM_ID,
OnInit,
AfterViewInit,
ChangeDetectorRef,
OnDestroy,
@ -22,6 +23,7 @@ import { PlanAutoFillService } from '../../services/plan-autofill.service';
import { AuthService } from '../../services/auth.service';
import { firstValueFrom, Subscription, filter } from 'rxjs';
import { environment } from '../../../environments/environment';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
type SortDir = 'asc' | 'desc';
type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP';
@ -127,7 +129,7 @@ interface AccountCompanyOption {
templateUrl: './geral.html',
styleUrls: ['./geral.scss']
})
export class Geral implements AfterViewInit, OnDestroy {
export class Geral implements OnInit, AfterViewInit, OnDestroy {
toastMessage = '';
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
@ -413,10 +415,13 @@ export class Geral implements AfterViewInit, OnDestroy {
this.navigationSub?.unsubscribe();
}
ngOnInit(): void {
if (!isPlatformBrowser(this.platformId)) return;
this.isAdmin = this.authService.hasRole('admin');
}
async ngAfterViewInit() {
if (!isPlatformBrowser(this.platformId)) return;
this.isAdmin = this.authService.hasRole('admin');
this.initAnimations();
setTimeout(() => {
@ -1646,7 +1651,7 @@ export class Geral implements AfterViewInit, OnDestroy {
return;
}
if (!confirm(`Remover linha ${r.linha}?`)) return;
if (!(await confirmDeletionWithTyping(`a linha ${r.linha}`))) return;
this.loading = true;

View File

@ -674,6 +674,396 @@
@media (max-width: 700px) {
.filters-grid { grid-template-columns: 1fr; }
.search-group { width: 100%; max-width: 100%; }
.entity-cell { flex-direction: column; align-items: flex-start; }
.expand-btn { align-self: flex-end; }
.entity-cell {
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 6px;
}
.entity-label { flex: 1 1 auto; min-width: 0; }
.expand-btn { align-self: center; flex-shrink: 0; }
}
@media (max-width: 768px) {
.historico-page {
padding: 0 8px;
}
.container-geral-responsive {
width: calc(100vw - 16px) !important;
margin-top: 18px;
margin-bottom: 28px;
}
.page-blob {
opacity: 0.28;
filter: blur(40px);
&.blob-1,
&.blob-2,
&.blob-3,
&.blob-4 {
width: 240px;
height: 240px;
}
}
.geral-card {
min-height: auto;
border-radius: 16px;
}
.geral-header {
padding: 12px;
}
.title {
font-size: 22px;
margin-top: 0;
line-height: 1.15;
}
.subtitle {
font-size: 12px;
text-align: center;
line-height: 1.35;
}
.header-actions {
width: 100%;
justify-self: stretch;
display: flex;
}
.header-actions .btn {
width: 100%;
justify-content: center;
}
.filters-card {
padding: 12px;
gap: 12px;
border-radius: 14px;
}
.filters-head {
align-items: stretch;
}
.filters-title {
width: 100%;
justify-content: center;
text-align: center;
}
.filters-actions {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.filters-actions .btn-primary,
.filters-actions .btn-ghost {
width: 100%;
justify-content: center;
min-width: 0;
padding: 0 10px;
}
.filters-grid {
grid-template-columns: 1fr !important;
gap: 10px;
}
.filter-search {
grid-column: span 1;
}
.filter-field {
min-width: 0;
}
.filter-field input {
width: 100%;
min-width: 0;
font-size: 13px;
}
.filter-field app-select,
.filter-field .select-glass {
width: 100%;
min-width: 0;
display: block;
}
.search-group {
max-width: 100%;
min-width: 0;
min-height: 40px;
align-items: center;
}
.search-group .form-control {
min-width: 0;
height: 40px;
padding: 0;
font-size: 16px;
line-height: 1.2;
}
.search-group .input-group-text,
.search-group .btn-clear {
height: 40px;
min-height: 40px;
padding-top: 0;
padding-bottom: 0;
display: inline-flex;
align-items: center;
line-height: 1;
}
.search-group .input-group-text {
padding-left: 10px;
padding-right: 6px;
}
.search-group .btn-clear {
padding-left: 8px;
padding-right: 10px;
}
.geral-body {
min-width: 0;
}
.table-wrap {
width: 100%;
max-width: 100%;
overflow-x: auto;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
}
.table-modern {
min-width: 860px !important;
}
.table-modern thead th,
.table-modern td {
padding: 10px 8px;
font-size: 0.78rem;
}
.table-modern th:nth-child(2),
.table-modern td:nth-child(2) {
min-width: 190px;
}
.table-modern th:nth-child(5),
.table-modern td:nth-child(5) {
min-width: 210px;
}
.td-clip {
max-width: 180px;
}
.entity-cell {
justify-content: flex-start;
gap: 6px;
}
.entity-label {
min-width: 0;
flex: 1 1 auto;
}
.entity-id {
margin-top: 2px;
line-height: 1.2;
}
.details-row td {
padding: 0 8px 12px;
}
.details-panel {
padding: 10px;
gap: 10px;
border-radius: 14px;
max-width: 640px;
margin: 0 auto;
}
.section-title {
font-size: 0.82rem;
flex-wrap: wrap;
line-height: 1.25;
}
.change-item {
padding: 8px 10px;
}
.change-head {
align-items: flex-start;
flex-wrap: wrap;
}
.change-field {
min-width: 0;
overflow-wrap: break-word;
}
.change-values {
flex-direction: row;
align-items: center;
gap: 6px;
font-size: 0.75rem;
line-height: 1.2;
}
.change-values i {
transform: none;
font-size: 0.7rem;
flex-shrink: 0;
}
.change-values .old,
.change-values .new {
min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.tech-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.tech-value {
font-size: 13px;
}
.alert.alert-danger {
margin: 12px !important;
display: grid;
gap: 8px;
}
.alert.alert-danger .btn {
margin-left: 0 !important;
width: 100%;
}
.geral-footer {
padding: 12px;
gap: 10px;
}
.footer-meta {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.footer-meta > .small {
text-align: center;
}
.page-size {
width: 100%;
justify-content: space-between;
}
.select-wrapper {
min-width: 110px;
}
.geral-footer nav {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.pagination-modern {
flex-wrap: nowrap;
width: max-content;
min-width: 100%;
justify-content: center;
padding-bottom: 2px;
}
.pagination-modern .page-link {
white-space: nowrap;
font-size: 12px;
padding: 0.35rem 0.6rem;
}
}
@media (max-width: 480px) {
.title-badge {
font-size: 12px;
padding: 6px 10px;
gap: 8px;
}
.title {
font-size: 20px;
}
.filters-actions {
grid-template-columns: 1fr;
}
.btn-primary,
.btn-ghost {
width: 100%;
justify-content: center;
}
.table-modern {
min-width: 760px !important;
}
.td-clip {
max-width: 140px;
}
.table-modern th:nth-child(2),
.table-modern td:nth-child(2) {
min-width: 170px;
}
.table-modern th:nth-child(5),
.table-modern td:nth-child(5) {
min-width: 180px;
}
.details-panel {
max-width: 520px;
padding: 8px;
gap: 8px;
}
.change-item {
padding: 7px 8px;
}
.change-values {
font-size: 0.72rem;
gap: 5px;
}
.entity-cell {
gap: 6px;
}
.expand-btn {
width: 30px;
height: 30px;
border-radius: 8px;
}
}

View File

@ -13,6 +13,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { LinesService } from '../../services/lines.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { environment } from '../../../environments/environment';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente';
@ -698,8 +699,9 @@ export class Mureg implements AfterViewInit {
this.deleteSaving = false;
}
confirmDelete() {
async confirmDelete() {
if (!this.deleteTarget?.id) return;
if (!(await confirmDeletionWithTyping('esta Mureg'))) return;
this.deleteSaving = true;
const targetId = this.deleteTarget.id;

View File

@ -33,6 +33,7 @@ import {
ParcelamentoCreateModalComponent,
ParcelamentoCreateModel,
} from './components/parcelamento-create-modal/parcelamento-create-modal';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
type MonthOption = { value: number; label: string };
type ParcelamentoStatus = 'ativos' | 'futuros' | 'finalizados';
@ -452,8 +453,9 @@ export class Parcelamentos implements OnInit, OnDestroy {
this.deleteTarget = null;
}
confirmDelete(): void {
async confirmDelete(): Promise<void> {
if (!this.deleteTarget || this.deleteLoading) return;
if (!(await confirmDeletionWithTyping('este parcelamento'))) return;
const id = this.getItemId(this.deleteTarget);
if (!id) return;
this.deleteLoading = true;

View File

@ -69,6 +69,16 @@
transform: translateY(0);
}
@media (max-width: 768px) {
.wrap {
padding-top: 12px;
}
:host(.animate-ready) [data-animate] {
transform: translateY(8px);
}
}
/* Header */
.page-head {
display: flex;
@ -253,6 +263,27 @@
}
}
@media (max-width: 768px) {
.tab-bar {
width: 100%;
max-width: 100%;
min-width: 0;
flex-wrap: nowrap;
justify-content: flex-start;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: thin;
padding-bottom: 6px;
}
.tab-btn {
flex: 0 0 auto;
white-space: nowrap;
}
}
/* Hero Section */
.section-hero {
background: white;

View File

@ -51,8 +51,12 @@
<i class="bi" [class.bi-search]="!loading" [class.bi-hourglass-split]="loading"></i>
</span>
<input
type="search"
class="form-control"
placeholder="Pesquisar..."
inputmode="search"
enterkeyhint="search"
autocomplete="off"
[(ngModel)]="search"
(ngModelChange)="onSearchChange()">
<button
@ -98,7 +102,7 @@
<div class="group-body" *ngIf="expandedGroup === g.cliente">
<div class="d-flex justify-content-between align-items-center px-4 py-2 border-bottom bg-white">
<div class="group-body-top d-flex justify-content-between align-items-center px-4 py-2 border-bottom bg-white">
<small class="text-muted fw-bold">Linhas do Cliente</small>
<span class="chip-muted">Total: {{ g.total | currency:'BRL' }}</span>
</div>
@ -304,7 +308,7 @@
<div class="box-body">
<div class="form-grid">
<div class="form-field"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cliente" /></div>
<div class="form-field"><label>Linha</label><input class="form-control form-control-sm" [(ngModel)]="createModel.linha" /></div>
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="createModel.linha" /></div>
<div class="form-field"><label>Conta</label><input class="form-control form-control-sm" [(ngModel)]="createModel.conta" /></div>
<div class="form-field"><label>Usuário</label><input class="form-control form-control-sm" [(ngModel)]="createModel.usuario" /></div>
<div class="form-field span-2">
@ -324,7 +328,11 @@
(ngModelChange)="onCreatePlanChange()"
/>
</div>
<div class="form-field"><label>Item (opcional)</label><input class="form-control form-control-sm" type="number" [(ngModel)]="createModel.item" /></div>
<div class="form-field field-item field-auto">
<label>Item (Automático)</label>
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
<small class="field-hint">Gerado automaticamente pelo sistema</small>
</div>
</div>
</div>
</details>
@ -375,7 +383,7 @@
<div class="box-body">
<div class="form-grid">
<div class="form-field"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" /></div>
<div class="form-field"><label>Linha</label><input class="form-control form-control-sm" [(ngModel)]="editModel.linha" /></div>
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="editModel.linha" /></div>
<div class="form-field"><label>Conta</label><input class="form-control form-control-sm" [(ngModel)]="editModel.conta" /></div>
<div class="form-field"><label>Usuário</label><input class="form-control form-control-sm" [(ngModel)]="editModel.usuario" /></div>
<div class="form-field span-2"><label>Plano</label><input class="form-control form-control-sm" [(ngModel)]="editModel.planoContrato" (ngModelChange)="onEditPlanChange()" /></div>

View File

@ -626,10 +626,525 @@ details[open] .transition-icon { transform: rotate(180deg); color: var(--brand);
.lg-modal-card { border-radius: 16px; }
.lg-modal-card .modal-header { padding: 12px 16px; }
.lg-modal-card .modal-body { padding: 16px !important; }
.lg-modal-card.create-modal .modal-footer { flex-direction: column-reverse; }
.lg-modal-card.create-modal .modal-footer .btn { width: 100%; min-width: 0; }
.lg-modal-card.modal-xl .modal-footer {
flex-direction: column-reverse;
}
.lg-modal-card.modal-xl .modal-footer .btn {
width: 100%;
min-width: 0;
}
.lg-modal-card.modal-xl .modal-footer .btn.btn-sm {
min-height: 0;
padding: 0.28rem 0.55rem;
font-size: 0.8rem;
line-height: 1.15;
border-radius: 10px;
}
.lg-modal-card.create-modal.modal-xl .modal-footer .btn.btn-sm {
min-height: 0 !important;
height: auto !important;
padding: 0.25rem 0.5rem !important;
font-size: 0.875rem !important;
line-height: 1.5 !important;
}
.lg-modal-card.create-modal.modal-xl .modal-footer .btn {
// Reset global mobile flex-basis (180px) so stacked buttons don't grow vertically.
flex: 0 0 auto !important;
}
.form-grid,
.info-grid { grid-template-columns: 1fr; }
.info-item.span-2,
.form-field.span-2 { grid-column: span 1; }
.form-field.field-line {
order: -1;
}
.form-field.field-line .form-control {
min-height: 42px;
font-size: 0.95rem;
font-weight: 700;
letter-spacing: 0.02em;
font-variant-numeric: tabular-nums;
}
.form-field.field-item {
align-items: flex-start;
}
.form-field.field-item .form-control {
max-width: 152px;
min-height: 38px;
text-align: left;
font-size: 0.82rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.form-field.field-item .field-hint {
max-width: 220px;
}
}
@media (max-width: 768px) {
.vigencia-page {
padding: 0 8px;
}
.vigencia-page .page-blob {
opacity: 0.28;
filter: blur(40px);
}
.vigencia-page .blob-1,
.vigencia-page .blob-2,
.vigencia-page .blob-3 {
width: 240px;
height: 240px;
}
.container-geral-responsive {
width: calc(100vw - 16px);
margin-top: 18px;
margin-bottom: 28px;
}
.geral-card {
min-height: auto;
border-radius: 16px;
}
.geral-header {
padding: 12px;
}
.header-row-top {
display: grid;
grid-template-columns: 1fr;
justify-items: center;
align-items: center;
text-align: center;
gap: 12px;
}
.title-badge {
justify-self: center;
max-width: 100%;
white-space: normal;
text-align: center;
gap: 8px;
padding: 6px 10px;
}
.header-title {
width: 100%;
max-width: 420px;
display: grid;
gap: 4px;
}
.title {
font-size: 1.22rem;
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0;
white-space: normal;
overflow-wrap: anywhere;
text-wrap: balance;
}
.subtitle {
display: block;
font-size: 0.76rem;
line-height: 1.3;
white-space: normal;
overflow-wrap: anywhere;
text-wrap: balance;
max-width: 32ch;
margin: 0 auto;
}
.header-actions {
width: 100%;
justify-content: stretch;
}
.header-actions .btn {
width: 100%;
min-width: 0;
}
.mureg-kpis {
margin-top: 12px !important;
gap: 8px;
}
.mureg-kpis .kpi {
min-height: 52px;
padding: 8px 10px;
gap: 8px;
}
.mureg-kpis .kpi .lbl {
font-size: 0.62rem;
line-height: 1.15;
white-space: normal;
}
.mureg-kpis .kpi .val {
font-size: 0.95rem;
line-height: 1;
flex-shrink: 0;
}
.controls {
margin-top: 12px !important;
margin-bottom: 0 !important;
display: grid !important;
grid-template-columns: 1fr;
align-items: stretch !important;
justify-content: stretch !important;
gap: 10px !important;
}
.controls > * {
width: 100%;
min-width: 0;
}
.search-group {
width: 100%;
max-width: 100%;
min-width: 0;
min-height: 42px;
align-items: center;
border-radius: 14px;
}
.search-group .input-group-text,
.search-group .btn-clear {
height: 42px;
min-height: 42px;
padding-top: 0;
padding-bottom: 0;
display: inline-flex;
align-items: center;
line-height: 1;
flex-shrink: 0;
}
.search-group .input-group-text {
padding-left: 10px;
padding-right: 6px;
}
.search-group .form-control {
min-width: 0;
height: 42px;
padding: 0;
font-size: 16px;
line-height: 1.2;
}
.search-group .btn-clear {
padding-left: 8px;
padding-right: 10px;
}
.page-size {
width: 100%;
justify-content: space-between;
gap: 8px !important;
padding: 8px 10px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(17, 18, 20, 0.08);
}
.page-size > span {
min-width: 0;
line-height: 1.15;
white-space: normal;
}
.page-size app-select {
flex-shrink: 0;
}
.geral-body {
min-width: 0;
}
.groups-container {
padding: 12px;
min-width: 0;
}
.client-group-card {
border-radius: 14px;
}
.group-header {
padding: 12px 14px;
gap: 10px;
align-items: flex-start;
}
.group-info {
min-width: 0;
flex: 1 1 auto;
}
.group-title {
font-size: 0.95rem;
line-height: 1.25;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
text-wrap: balance;
}
.group-badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px !important;
}
.badge-pill {
font-size: 0.62rem;
line-height: 1.1;
padding: 4px 8px;
letter-spacing: 0.03em;
white-space: normal;
}
.group-toggle-icon {
width: 30px;
height: 30px;
border-radius: 9px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(3, 15, 170, 0.06);
color: var(--blue);
flex-shrink: 0;
margin-top: 2px;
}
.group-body-top {
padding: 10px 12px !important;
gap: 8px;
flex-wrap: wrap;
align-items: flex-start !important;
}
.group-body-top small,
.group-body-top .chip-muted {
white-space: normal;
line-height: 1.2;
}
.chip-muted {
font-size: 0.7rem;
padding: 4px 8px;
}
.inner-table-wrap {
max-height: 420px;
-webkit-overflow-scrolling: touch;
}
.table-modern {
min-width: 860px;
}
.table-modern thead th {
padding: 10px 8px;
font-size: 0.72rem;
white-space: nowrap;
}
.table-modern td {
padding: 9px 8px;
font-size: 0.78rem;
}
.table-modern td.small {
font-size: 0.74rem !important;
line-height: 1.2;
}
.td-clip {
max-width: 160px;
}
.actions-col {
min-width: 120px;
}
.action-group {
gap: 4px;
}
.btn-icon {
width: 30px;
height: 30px;
border-radius: 8px;
}
.geral-footer {
padding: 12px;
display: grid;
grid-template-columns: 1fr;
gap: 10px;
align-items: stretch;
}
.geral-footer > .small {
text-align: center;
line-height: 1.25;
white-space: normal;
overflow-wrap: anywhere;
}
.geral-footer nav {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.pagination-modern {
flex-wrap: nowrap;
width: max-content;
min-width: 100%;
justify-content: center;
}
.pagination-modern .page-link {
white-space: nowrap;
font-size: 12px;
padding: 0.35rem 0.6rem;
}
}
@media (max-width: 480px) {
.title-badge {
font-size: 12px;
padding: 6px 9px;
}
.title {
font-size: 1.08rem;
line-height: 1.12;
}
.subtitle {
font-size: 0.72rem;
max-width: 28ch;
}
.mureg-kpis .kpi {
padding: 8px 9px;
min-height: 50px;
}
.controls {
gap: 8px !important;
}
.page-size {
padding: 8px;
}
.groups-container {
padding: 10px;
}
.group-header {
padding: 11px 12px;
gap: 8px;
}
.group-title {
font-size: 0.88rem;
}
.badge-pill {
font-size: 0.58rem;
padding: 4px 7px;
}
.group-body-top {
padding: 8px 10px !important;
}
.chip-muted {
font-size: 0.67rem;
}
.inner-table-wrap {
max-height: 380px;
}
.table-modern {
min-width: 760px;
}
.table-modern thead th {
padding: 9px 7px;
font-size: 0.68rem;
}
.table-modern td {
padding: 8px 7px;
font-size: 0.74rem;
}
.table-modern td.small {
font-size: 0.7rem !important;
}
.td-clip {
max-width: 120px;
}
.actions-col {
min-width: 106px;
}
.action-group {
gap: 3px;
}
.btn-icon {
width: 28px;
height: 28px;
border-radius: 7px;
}
.geral-footer {
padding: 10px;
gap: 8px;
}
}
.field-hint {
display: block;
font-size: 0.66rem;
line-height: 1.2;
color: rgba(17, 18, 20, 0.52);
font-weight: 700;
}
.form-field.field-auto .form-control {
background: rgba(245, 245, 247, 0.9);
border-color: rgba(17, 18, 20, 0.1);
color: rgba(17, 18, 20, 0.72);
}
.form-field.field-auto .form-control[readonly] {
cursor: default;
}

View File

@ -7,6 +7,7 @@ import { CustomSelectComponent } from '../../components/custom-select/custom-sel
import { AuthService } from '../../services/auth.service';
import { LinesService, MobileLineDetail } from '../../services/lines.service';
import { PlanAutoFillService } from '../../services/plan-autofill.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
type SortDir = 'asc' | 'desc';
type ToastType = 'success' | 'danger';
@ -516,8 +517,9 @@ export class VigenciaComponent implements OnInit, OnDestroy {
this.deleteTarget = null;
}
confirmDelete() {
async confirmDelete() {
if (!this.deleteTarget) return;
if (!(await confirmDeletionWithTyping('este registro de vigência'))) return;
const id = this.deleteTarget.id;
this.vigenciaService.remove(id).subscribe({
next: () => {

View File

@ -81,7 +81,11 @@ export class UsersService {
return this.http.get<UserDto>(`${this.baseApi}/users/${id}`);
}
update(id: string, payload: UpdateUserPayload): Observable<UserDto> {
return this.http.patch<UserDto>(`${this.baseApi}/users/${id}`, payload);
update(id: string, payload: UpdateUserPayload): Observable<void> {
return this.http.patch<void>(`${this.baseApi}/users/${id}`, payload);
}
delete(id: string): Observable<void> {
return this.http.delete<void>(`${this.baseApi}/users/${id}`);
}
}

View File

@ -0,0 +1,333 @@
type BaseDialogOptions = {
title: string;
message: string;
confirmLabel: string;
cancelLabel?: string;
tone?: 'danger' | 'warning' | 'neutral';
inputLabel?: string;
inputPlaceholder?: string;
requiredText?: string;
requiredHint?: string;
};
let stylesInjected = false;
let activeOverlay: HTMLElement | null = null;
export async function confirmDeletionWithTyping(
targetLabel?: string,
confirmWord = 'EXCLUIR'
): Promise<boolean> {
const normalizedWord = confirmWord.trim().toUpperCase();
const label = (targetLabel || 'este registro').trim();
return openDialog({
title: 'Confirmar Exclusão',
message: `Tem certeza que deseja excluir ${label}? Esta ação não poderá ser desfeita.`,
confirmLabel: 'Excluir',
cancelLabel: 'Cancelar',
tone: 'danger',
inputLabel: `Digite ${normalizedWord} para confirmar`,
inputPlaceholder: normalizedWord,
requiredText: normalizedWord,
requiredHint: `A exclusão só será confirmada se você digitar ${normalizedWord}.`,
});
}
export async function showDeletionWarning(message: string, title = 'Atenção'): Promise<void> {
await openDialog({
title,
message,
confirmLabel: 'Entendi',
tone: 'warning',
});
}
export async function confirmActionModal(options: {
title: string;
message: string;
confirmLabel: string;
cancelLabel?: string;
tone?: 'danger' | 'warning' | 'neutral';
}): Promise<boolean> {
return openDialog({
title: options.title,
message: options.message,
confirmLabel: options.confirmLabel,
cancelLabel: options.cancelLabel ?? 'Cancelar',
tone: options.tone ?? 'neutral',
});
}
function openDialog(options: BaseDialogOptions): Promise<boolean> {
if (typeof document === 'undefined') {
return Promise.resolve(false);
}
if (activeOverlay) {
activeOverlay.remove();
activeOverlay = null;
}
ensureStyles();
return new Promise<boolean>((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'lgx-confirm-overlay';
const card = document.createElement('div');
card.className = `lgx-confirm-card tone-${options.tone || 'neutral'}`;
card.setAttribute('role', 'dialog');
card.setAttribute('aria-modal', 'true');
card.setAttribute('aria-label', options.title);
const icon = getToneIcon(options.tone || 'neutral');
const hasInput = !!options.requiredText;
card.innerHTML = `
<div class="lgx-confirm-head">
<div class="lgx-confirm-icon">${icon}</div>
<div>
<h3>${escapeHtml(options.title)}</h3>
<p>${escapeHtml(options.message)}</p>
</div>
</div>
${
hasInput
? `
<div class="lgx-confirm-field">
<label>${escapeHtml(options.inputLabel || '')}</label>
<input type="text" autocomplete="off" placeholder="${escapeHtml(options.inputPlaceholder || '')}" />
<small>${escapeHtml(options.requiredHint || '')}</small>
</div>
`
: ''
}
<div class="lgx-confirm-actions">
${
options.cancelLabel
? `<button type="button" class="lgx-btn ghost js-cancel">${escapeHtml(options.cancelLabel)}</button>`
: ''
}
<button type="button" class="lgx-btn ${options.tone === 'danger' ? 'danger' : 'primary'} js-confirm" ${
hasInput ? 'disabled' : ''
}>${escapeHtml(options.confirmLabel)}</button>
</div>
`;
overlay.appendChild(card);
document.body.appendChild(overlay);
activeOverlay = overlay;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const input = hasInput ? (card.querySelector('input') as HTMLInputElement | null) : null;
const confirmBtn = card.querySelector('.js-confirm') as HTMLButtonElement | null;
const cancelBtn = card.querySelector('.js-cancel') as HTMLButtonElement | null;
const requiredText = (options.requiredText || '').trim().toUpperCase();
const cleanup = (result: boolean) => {
document.removeEventListener('keydown', onKeyDown);
overlay.remove();
if (activeOverlay === overlay) activeOverlay = null;
document.body.style.overflow = prevOverflow;
resolve(result);
};
const canConfirm = () => {
if (!hasInput) return true;
return (input?.value || '').trim().toUpperCase() === requiredText;
};
const updateConfirmState = () => {
if (!confirmBtn) return;
confirmBtn.disabled = !canConfirm();
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault();
cleanup(false);
}
if (event.key === 'Enter') {
if (document.activeElement === cancelBtn) return;
if (canConfirm()) {
event.preventDefault();
cleanup(true);
}
}
};
overlay.addEventListener('click', (event) => {
if (event.target === overlay) cleanup(false);
});
cancelBtn?.addEventListener('click', () => cleanup(false));
confirmBtn?.addEventListener('click', () => cleanup(canConfirm()));
input?.addEventListener('input', updateConfirmState);
input?.addEventListener('paste', () => setTimeout(updateConfirmState));
document.addEventListener('keydown', onKeyDown);
const focusTarget = input ?? cancelBtn ?? confirmBtn;
setTimeout(() => focusTarget?.focus(), 0);
});
}
function ensureStyles() {
if (stylesInjected || typeof document === 'undefined') return;
const style = document.createElement('style');
style.id = 'lgx-confirm-modal-styles';
style.textContent = `
.lgx-confirm-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(2px);
display: grid;
place-items: center;
z-index: 5000;
padding: 16px;
animation: lgxFadeIn .12s ease-out;
}
.lgx-confirm-card {
width: min(460px, calc(100vw - 32px));
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 16px;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.18);
padding: 18px;
animation: lgxScaleIn .14s ease-out;
font-family: inherit;
color: #111827;
}
.lgx-confirm-head {
display: grid;
grid-template-columns: 40px 1fr;
gap: 12px;
align-items: start;
}
.lgx-confirm-icon {
width: 40px;
height: 40px;
border-radius: 999px;
display: grid;
place-items: center;
font-size: 18px;
font-weight: 700;
background: #f3f4f6;
color: #374151;
}
.lgx-confirm-card h3 {
margin: 2px 0 6px;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.lgx-confirm-card p {
margin: 0;
font-size: 13px;
color: #6b7280;
line-height: 1.4;
}
.lgx-confirm-field {
margin-top: 14px;
display: grid;
gap: 6px;
}
.lgx-confirm-field label {
font-size: 12px;
font-weight: 600;
color: #111827;
}
.lgx-confirm-field input {
height: 40px;
border: 1px solid #d1d5db;
border-radius: 10px;
padding: 0 12px;
font-size: 14px;
outline: none;
transition: border-color .15s ease, box-shadow .15s ease;
}
.lgx-confirm-field input:focus {
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12);
}
.lgx-confirm-field small {
font-size: 11px;
color: #6b7280;
}
.lgx-confirm-actions {
margin-top: 16px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.lgx-btn {
height: 38px;
border-radius: 10px;
border: 1px solid transparent;
padding: 0 14px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.lgx-btn.ghost {
background: #fff;
color: #374151;
border-color: #d1d5db;
}
.lgx-btn.ghost:hover { background: #f9fafb; }
.lgx-btn.primary {
background: #1d4ed8;
color: #fff;
border-color: #1d4ed8;
}
.lgx-btn.primary:hover:not(:disabled) { background: #1e40af; border-color: #1e40af; }
.lgx-btn.danger {
background: #dc2626;
color: #fff;
border-color: #dc2626;
}
.lgx-btn.danger:hover:not(:disabled) { background: #b91c1c; border-color: #b91c1c; }
.lgx-btn:disabled {
opacity: .6;
cursor: not-allowed;
}
.lgx-confirm-card.tone-danger .lgx-confirm-icon {
background: #fee2e2;
color: #b91c1c;
}
.lgx-confirm-card.tone-warning .lgx-confirm-icon {
background: #fef3c7;
color: #b45309;
}
.lgx-confirm-card.tone-warning .lgx-btn.primary {
background: #d97706;
border-color: #d97706;
}
.lgx-confirm-card.tone-warning .lgx-btn.primary:hover:not(:disabled) {
background: #b45309;
border-color: #b45309;
}
@keyframes lgxFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes lgxScaleIn { from { opacity: 0; transform: translateY(6px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
`;
document.head.appendChild(style);
stylesInjected = true;
}
function getToneIcon(tone: BaseDialogOptions['tone']): string {
if (tone === 'danger') return '!';
if (tone === 'warning') return '!';
return 'i';
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

View File

@ -90,20 +90,35 @@ select.form-control-sm {
/* Empurra o conteúdo pra baixo do header fixo */
.app-main.has-header {
position: relative;
padding-top: 76px; /* evita vão visual entre o header fixo e o conteúdo */
padding-top: var(--app-header-offset, 76px); /* sincroniza com a altura real do header */
background: transparent;
}
@media (max-width: 600px) {
.app-main.has-header {
padding-top: 88px;
padding-top: var(--app-header-offset, 88px);
}
}
html,
body {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
img,
svg,
canvas,
video {
max-width: 100%;
height: auto;
}
/* Ajuste para monitores grandes: elimina o "vão" visual entre header e corpo. */
@media (min-width: 1400px) {
.app-main.has-header {
padding-top: 76px;
padding-top: var(--app-header-offset, 76px);
}
}
@ -116,7 +131,9 @@ select.form-control-sm {
.container-fat,
.container-mureg,
.container-troca,
.container-geral-responsive {
.container-geral-responsive,
.container-dashboard,
.container-chips {
max-width: 1100px !important; /* Largura controlada */
width: 96% !important; /* Margem segura em telas menores */
margin-left: auto !important;
@ -321,3 +338,386 @@ app-header .modal-card .btn-secondary:hover {
box-shadow: none !important;
}
}
/* Títulos de cliente no agrupamento: não herdar o "td-clip" das tabelas */
.client-group-card .group-header {
gap: 12px;
}
.client-group-card .group-header .group-info {
flex: 1 1 auto;
min-width: 0;
}
.client-group-card .group-header .group-info > h6,
.client-group-card .group-header .group-info > h6.td-clip {
min-width: 0;
max-width: none !important;
overflow: visible !important;
text-overflow: clip !important;
white-space: normal !important;
word-break: normal !important;
overflow-wrap: break-word;
line-height: 1.25;
}
.client-group-card .group-header .group-toggle-icon {
flex: 0 0 auto;
}
/* ========================================================== */
/* Ajustes globais de responsividade (mobile-first) */
/* ========================================================== */
@media (min-width: 1600px) {
.container-geral,
.container-fat,
.container-mureg,
.container-troca,
.container-geral-responsive,
.container-dashboard,
.container-chips {
max-width: 1240px !important;
}
}
@media (min-width: 2200px) {
.container-geral,
.container-fat,
.container-mureg,
.container-troca,
.container-geral-responsive,
.container-dashboard,
.container-chips {
max-width: 1360px !important;
}
}
@media (max-width: 1024px) {
.container-geral,
.container-fat,
.container-mureg,
.container-troca,
.container-geral-responsive,
.container-dashboard,
.container-chips {
width: min(100%, calc(100vw - 20px)) !important;
}
.header-actions,
.filters-actions {
flex-wrap: wrap;
gap: 8px !important;
}
.controls,
.filters-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: stretch;
}
.search-group,
.input-group.search-group {
min-width: 0;
flex: 1 1 280px;
}
.page-size {
margin-left: 0 !important;
flex: 0 1 auto;
}
}
@media (max-width: 768px) {
.app-main.has-header {
padding-top: var(--app-header-offset, 84px);
}
.geral-card,
.fat-card,
.mureg-card,
.troca-card,
.vigencia-page .geral-card {
min-height: auto !important;
margin-bottom: 20px !important;
}
.header-row-top {
gap: 12px !important;
}
.header-actions {
width: 100%;
justify-content: stretch !important;
}
.header-actions .btn,
.header-actions button.btn {
flex: 1 1 220px;
}
.filters-head,
.filters-meta {
align-items: stretch !important;
}
.filters-actions {
width: 100%;
justify-content: stretch !important;
}
.filters-actions .btn-primary,
.filters-actions .btn-ghost,
.filters-actions .btn,
.filters-actions button {
flex: 1 1 160px;
justify-content: center;
}
.filters-grid {
grid-template-columns: 1fr !important;
gap: 10px !important;
}
.filter-tabs {
width: 100%;
display: flex;
flex-wrap: nowrap;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.filter-tabs .filter-tab {
flex: 0 0 auto;
white-space: nowrap;
}
.client-filter-wrap,
.additional-filter-wrap,
.search-group,
.input-group.search-group,
.search-box,
.page-size {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
}
.chips-container {
max-width: 100% !important;
}
.btn-client-filter {
width: 100%;
min-height: 40px;
justify-content: space-between;
gap: 8px;
}
.client-dropdown,
.additional-dropdown {
width: min(100%, calc(100vw - 24px)) !important;
max-width: calc(100vw - 24px) !important;
right: auto !important;
left: 0 !important;
}
.controls .page-size {
justify-content: space-between !important;
}
.table-wrap,
.table-wrap-tall,
.inner-table-wrap,
.parcelamentos-table-wrap {
width: 100%;
max-width: 100%;
overflow-x: auto !important;
overflow-y: visible !important;
-webkit-overflow-scrolling: touch;
}
.table-wrap table.table-modern,
.inner-table-wrap table.table-modern,
.table-wrap .table-modern,
.parcelamentos-table-wrap .table-modern,
table.table-modern {
width: max-content !important;
min-width: max(100%, 920px) !important;
}
.table-modern thead th,
.table-modern td {
padding: 10px 8px !important;
}
.table-modern thead th {
font-size: 0.7rem !important;
}
.table-modern td {
font-size: 0.8rem !important;
}
.td-clip {
max-width: 180px !important;
}
.actions-col {
min-width: 120px !important;
}
.modal-custom,
.lg-modal,
.macrophony-modal,
.grouped-modal {
padding: 8px !important;
align-items: center !important;
justify-content: center !important;
}
.modal-card,
.lg-modal-card,
.macrophony-card,
.grouped-card {
width: min(100%, calc(100vw - 16px)) !important;
max-width: calc(100vw - 16px) !important;
max-height: min(92dvh, calc(100vh - 16px)) !important;
border-radius: 14px !important;
min-height: 0;
}
.modal-header,
.lg-modal-card .modal-header,
.detail-head {
padding: 12px 14px !important;
gap: 10px;
flex-wrap: wrap;
}
.modal-title,
.lg-modal-card .modal-title {
min-width: 0;
flex: 1 1 auto;
}
.modal-header h3,
.detail-head h4 {
font-size: 15px !important;
line-height: 1.3;
}
.modal-body,
.modern-body,
.lg-modal-card .modal-body,
.macrophony-modal-body,
.grouped-modal-body {
padding: 14px !important;
overflow-y: auto;
}
.modal-footer,
.modal-actions,
.lg-modal-card .modal-footer,
.manage-actions-footer {
display: flex !important;
flex-wrap: wrap !important;
gap: 8px !important;
justify-content: stretch !important;
align-items: stretch !important;
}
.modal-footer .btn,
.modal-footer button,
.modal-actions .btn,
.modal-actions button,
.lg-modal-card .modal-footer .btn,
.manage-actions-footer .btn,
.manage-actions-footer button {
flex: 1 1 180px;
min-width: 0 !important;
justify-content: center;
}
.manage-actions-footer .btn-delete-permanent-left {
margin-right: 0 !important;
}
.form-grid,
.edit-grid,
.info-grid,
.details-2col,
.finance-dashboard {
grid-template-columns: 1fr !important;
}
.details-dashboard {
grid-template-columns: 1fr !important;
gap: 12px !important;
}
.macrophony-summary,
.grouped-summary-bar {
padding: 12px 14px !important;
gap: 12px !important;
}
.toast-container {
left: 8px !important;
right: 8px !important;
width: auto !important;
padding: 8px !important;
}
.toast-container .toast {
width: 100% !important;
}
}
@media (max-width: 480px) {
.container-geral,
.container-fat,
.container-mureg,
.container-troca,
.container-geral-responsive,
.container-dashboard,
.container-chips {
width: calc(100vw - 14px) !important;
}
.title-badge,
.header-title .title {
word-break: break-word;
}
.header-actions .btn,
.header-actions button.btn,
.filters-actions .btn-primary,
.filters-actions .btn-ghost,
.filters-actions .btn,
.filters-actions button,
.modal-footer .btn,
.modal-footer button,
.modal-actions .btn,
.modal-actions button {
width: 100%;
flex-basis: 100%;
}
.table-wrap table.table-modern,
.inner-table-wrap table.table-modern,
.table-wrap .table-modern,
.parcelamentos-table-wrap .table-modern,
table.table-modern {
min-width: max(100%, 820px) !important;
}
.td-clip {
max-width: 140px !important;
}
}