diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 76381a6..ea4c859 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -14,6 +14,7 @@ import { TrocaNumero } from './pages/troca-numero/troca-numero'; import { Dashboard } from './pages/dashboard/dashboard'; import { Notificacoes } from './pages/notificacoes/notificacoes'; import { NovoUsuario } from './pages/novo-usuario/novo-usuario'; +import { ChipsControleRecebidos } from './pages/chips-controle-recebidos/chips-controle-recebidos'; export const routes: Routes = [ { path: '', component: Home }, @@ -28,6 +29,7 @@ export const routes: Routes = [ { path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard] }, { path: 'notificacoes', component: Notificacoes, canActivate: [authGuard] }, { path: 'novo-usuario', component: NovoUsuario, canActivate: [authGuard] }, + { path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard] }, // ✅ rota correta { path: 'dashboard', component: Dashboard, canActivate: [authGuard] }, diff --git a/src/app/app.ts b/src/app/app.ts index 021813a..3cea806 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -35,6 +35,7 @@ export class AppComponent { '/trocanumero', '/dashboard', // ✅ ADICIONADO: esconde footer na página de dashboard '/notificacoes', + '/chips-controle-recebidos', ]; constructor( diff --git a/src/app/components/custom-select/custom-select.html b/src/app/components/custom-select/custom-select.html new file mode 100644 index 0000000..9619462 --- /dev/null +++ b/src/app/components/custom-select/custom-select.html @@ -0,0 +1,29 @@ +
+ + +
+ + +
+ Nenhuma opção +
+
+
diff --git a/src/app/components/custom-select/custom-select.scss b/src/app/components/custom-select/custom-select.scss new file mode 100644 index 0000000..b6fed55 --- /dev/null +++ b/src/app/components/custom-select/custom-select.scss @@ -0,0 +1,124 @@ +:host { + display: block; + width: 100%; +} + +.app-select { + position: relative; + width: 100%; +} + +.app-select-trigger { + width: 100%; + height: 42px; + border-radius: 10px; + border: 1.5px solid rgba(15, 23, 42, 0.12); + padding: 0 36px 0 12px; + background: #fff; + color: #0f172a; + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +:host(.form-control) .app-select-trigger, +:host(.form-select) .app-select-trigger { + border-radius: 8px; + border: 1px solid rgba(17, 18, 20, 0.15); + font-size: 0.9rem; +} + +:host(.select-glass) .app-select-trigger { + background: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(17, 18, 20, 0.15); + border-radius: 12px; + font-weight: 800; +} + +:host(.select-glass) .app-select-trigger:hover { + background: #fff; + border-color: rgba(17, 18, 20, 0.7); + box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1); +} + +.app-select.sm .app-select-trigger { + height: 36px; + font-size: 13px; + padding-right: 32px; +} + +.app-select-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + + +.app-select-trigger i { + color: #64748b; + font-size: 12px; +} + +.app-select.open .app-select-trigger { + border-color: #e33dcf; + box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); +} + +.app-select.disabled .app-select-trigger { + background-color: #f1f5f9; + color: #94a3b8; + cursor: not-allowed; +} + +.app-select-panel { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + max-height: 260px; + overflow-y: auto; + background: #fff; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.12); + box-shadow: 0 14px 28px rgba(15, 23, 42, 0.12); + z-index: 1200; + padding: 6px; +} + +.app-select-option { + width: 100%; + border: none; + background: transparent; + text-align: left; + padding: 8px 10px; + border-radius: 8px; + font-size: 13px; + color: #0f172a; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; +} + +.app-select-option:hover { + background: rgba(227, 61, 207, 0.08); +} + +.app-select-option.selected { + background: rgba(227, 61, 207, 0.12); + color: #b71fb0; + font-weight: 600; +} + +.app-select-empty { + padding: 12px 10px; + font-size: 12px; + color: #94a3b8; +} diff --git a/src/app/components/custom-select/custom-select.ts b/src/app/components/custom-select/custom-select.ts new file mode 100644 index 0000000..6bd1df3 --- /dev/null +++ b/src/app/components/custom-select/custom-select.ts @@ -0,0 +1,118 @@ +import { Component, ElementRef, HostListener, Input, forwardRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'app-select', + standalone: true, + imports: [CommonModule], + templateUrl: './custom-select.html', + styleUrls: ['./custom-select.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CustomSelectComponent), + multi: true, + }, + ], +}) +export class CustomSelectComponent implements ControlValueAccessor { + @Input() options: any[] = []; + @Input() placeholder = 'Selecione uma opção'; + @Input() labelKey = 'label'; + @Input() valueKey = 'value'; + @Input() size: 'sm' | 'md' = 'md'; + @Input() disabled = false; + + isOpen = false; + value: any = null; + + private onChange: (value: any) => void = () => {}; + private onTouched: () => void = () => {}; + + constructor(private host: ElementRef) {} + + writeValue(value: any): void { + this.value = value; + } + + registerOnChange(fn: (value: any) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) this.isOpen = false; + } + + get displayLabel(): string { + const selected = this.findOption(this.value); + if (selected !== undefined) return this.getOptionLabel(selected); + if (this.value === null || this.value === undefined || this.value === '') return this.placeholder; + return String(this.value); + } + + get hasValue(): boolean { + return !(this.value === null || this.value === undefined || this.value === ''); + } + + toggle(): void { + if (this.disabled) return; + this.isOpen = !this.isOpen; + } + + close(): void { + this.isOpen = false; + } + + selectOption(option: any): void { + if (this.disabled) return; + const value = this.getOptionValue(option); + this.value = value; + this.onChange(value); + this.onTouched(); + this.close(); + } + + isSelected(option: any): boolean { + return this.getOptionValue(option) === this.value; + } + + trackByValue = (_: number, option: any) => this.getOptionValue(option); + + private getOptionValue(option: any): any { + if (option && typeof option === 'object') { + return option[this.valueKey]; + } + return option; + } + + getOptionLabel(option: any): string { + if (option && typeof option === 'object') { + const v = option[this.labelKey]; + return v === undefined || v === null ? '' : String(v); + } + return option === undefined || option === null ? '' : String(option); + } + + private findOption(value: any): any { + return (this.options || []).find((o) => this.getOptionValue(o) === value); + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent): void { + if (!this.isOpen) return; + const target = event.target as Node | null; + if (target && this.host.nativeElement.contains(target)) return; + this.close(); + } + + @HostListener('document:keydown.escape') + onEsc(): void { + if (this.isOpen) this.close(); + } +} diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index 63a49f3..273f743 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -64,8 +64,13 @@ (click)="markNotificationRead(n)" >
-
- +
+
@@ -106,12 +111,15 @@
+ +
-
-
+ + +
- - - - - diff --git a/src/app/components/header/header.scss b/src/app/components/header/header.scss index 3e31990..8d62ea0 100644 --- a/src/app/components/header/header.scss +++ b/src/app/components/header/header.scss @@ -1,578 +1,457 @@ -/* Variáveis de apoio */ +@use 'sass:color'; + +/* Variáveis */ $primary: #1c38c9; +$primary-hover: #152ca0; $danger: #ef4444; $warning: #f59e0b; +$success: #10b981; $text-main: #111827; $text-muted: #6b7280; -$bg-light: #f3f4f6; -$border-color: rgba(0,0,0,0.06); +$bg-light: #f9fafb; +$border-color: #e5e7eb; +/* Utils */ +* { box-sizing: border-box; } +.custom-scroll::-webkit-scrollbar { width: 6px; height: 6px; } +.custom-scroll::-webkit-scrollbar-track { background: transparent; } +.custom-scroll::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 10px; } +.custom-scroll::-webkit-scrollbar-thumb:hover { background: #9ca3af; } + +/* HEADER PRINCIPAL */ .app-header { - position: fixed; - top: 0; left: 0; width: 100%; - z-index: 1000; - padding: 14px 0; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border-bottom: 1px solid rgba(0,0,0,0.05); - transition: all 0.3s ease; - - &.scrolled { - padding: 10px 0; - background: rgba(255, 255, 255, 0.95); - box-shadow: 0 4px 20px rgba(0,0,0,0.03); - } + position: fixed; top: 0; left: 0; width: 100%; z-index: 1000; + padding: 14px 0; background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid rgba(0,0,0,0.05); transition: all 0.3s ease; + &.scrolled { padding: 10px 0; box-shadow: 0 4px 20px rgba(0,0,0,0.03); } } -.header-inner { - display: flex; align-items: center; justify-content: space-between; -} - -.logged-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - width: 100%; -} - -/* --- LOGO & MENU --- */ +.header-inner { display: flex; align-items: center; justify-content: space-between; gap: 24px; } +.logged-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%; } .left-logged { display: flex; align-items: center; gap: 16px; } .btn-icon { - background: transparent; - border: none; - width: 40px; height: 40px; - border-radius: 50%; - display: grid; place-items: center; - cursor: pointer; - transition: background 0.2s; - color: $text-main; - + background: transparent; border: none; width: 40px; height: 40px; border-radius: 50%; + display: grid; place-items: center; cursor: pointer; transition: background 0.2s; color: $text-main; &:hover { background: rgba(0,0,0,0.04); } i { font-size: 20px; } } .logo-area { - display: flex; align-items: center; gap: 10px; - text-decoration: none; color: #111827; - + display: flex; align-items: center; gap: 10px; text-decoration: none; color: #111827; .logo-icon { - width: 36px; height: 36px; - background: conic-gradient(from 210deg, #2f6bff, #7c3aed, #ec4899, #f59e0b, #22c55e, #2f6bff); - color: #fff; - border-radius: 50%; - display: grid; place-items: center; - font-size: 18px; - box-shadow: 0 6px 14px rgba(47, 107, 255, 0.2); + width: 36px; height: 36px; background: conic-gradient(from 210deg, #2f6bff, #7c3aed, #ec4899, #f59e0b, #22c55e, #2f6bff); + color: #fff; border-radius: 50%; display: grid; place-items: center; font-size: 18px; box-shadow: 0 6px 14px rgba(47, 107, 255, 0.2); } - .logo-text { font-size: 19px; font-weight: 700; letter-spacing: -0.5px; - .highlight { - background: linear-gradient(90deg, #2f6bff 0%, #7c3aed 55%, #ec4899 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - } + .highlight { background: linear-gradient(90deg, #2f6bff 0%, #7c3aed 55%, #ec4899 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } } } -.logged-actions { - display: flex; - align-items: center; - gap: 12px; - margin-left: auto; +.nav-links { display: flex; align-items: center; justify-content: center; gap: 22px; flex: 1; } +.nav-links .nav-link { + display: inline-flex; align-items: center; gap: 6px; color: $text-main; text-decoration: none; font-weight: 600; font-size: 14px; transition: color 0.2s; + &:hover { color: $primary; } +} +.header-actions { display: flex; align-items: center; } +.btn-login-header { + display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 99px; + border: 1px solid rgba(28, 56, 201, 0.18); background: #fff; color: $primary; font-weight: 700; font-size: 13px; text-decoration: none; transition: all 0.2s; + &:hover { transform: translateY(-1px); background: rgba(28, 56, 201, 0.04); box-shadow: 0 4px 12px rgba(28, 56, 201, 0.15); } } -/* --- NOTIFICAÇÕES (Dropdown) --- */ -.notifications-menu { position: relative; } +@media (max-width: 900px) { .nav-links { display: none; } } +.logged-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; } +@media (min-width: 1200px) { + .header-inner.container { + max-width: none; + width: 100%; + padding-left: 28px; + padding-right: 28px; + } +} + +/* DROPDOWNS */ +.notifications-menu, .options-menu { position: relative; } +.notifications-dropdown, .options-dropdown { + position: absolute; top: calc(100% + 12px); right: -10px; width: 340px; background: #fff; border-radius: 16px; + box-shadow: 0 10px 40px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.05); z-index: 1200; transform-origin: top right; + animation: slideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1); overflow: hidden; +} +.options-dropdown { width: 220px; right: 0; padding: 6px; } +@keyframes slideDown { from { opacity: 0; transform: translateY(-10px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } + +.user-trigger { + display: flex; align-items: center; gap: 8px; padding: 4px; background: #fff; border: 1px solid $border-color; border-radius: 99px; cursor: pointer; transition: all 0.2s; + &:hover, &[aria-expanded="true"] { border-color: $primary; box-shadow: 0 0 0 2px rgba(28, 56, 201, 0.1); } + .user-avatar { width: 32px; height: 32px; background: $bg-light; border-radius: 50%; display: grid; place-items: center; color: $text-muted; } + .chevron { font-size: 10px; color: $text-muted; margin: 0 6px 0 2px; } +} +.options-item { + width: 100%; text-align: left; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; color: $text-main; display: flex; align-items: center; gap: 10px; cursor: pointer; + &:hover { background: $bg-light; } + &.danger { color: $danger; &:hover { background: rgba($danger, 0.05); } } +} +.divider { height: 1px; background: $border-color; margin: 4px 0; } + +/* NOTIFICAÇÕES */ .btn-bell { - position: relative; - - &.has-unread { - color: $primary; - background: rgba(28, 56, 201, 0.06); - } - + &.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-red 2s infinite; + 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; } } +@keyframes pulse { 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba($danger, 0.7); } 70% { transform: scale(1); box-shadow: 0 0 0 6px rgba($danger, 0); } 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba($danger, 0); } } + .notifications-head { padding: 16px; border-bottom: 1px solid $border-color; display: flex; justify-content: space-between; align-items: center; .head-title { font-weight: 700; font-size: 14px; display: flex; align-items: center; gap: 6px; } .see-all { font-size: 12px; color: $primary; text-decoration: none; font-weight: 600; } } +.notifications-body { max-height: 360px; overflow-y: auto; } +.notifications-empty { padding: 32px; text-align: center; color: $text-muted; .empty-icon { font-size: 24px; margin-bottom: 8px; } } -@keyframes pulse-red { - 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba($danger, 0.7); } - 70% { transform: scale(1); box-shadow: 0 0 0 6px rgba($danger, 0); } - 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba($danger, 0); } +.notification-item { + display: flex; gap: 12px; padding: 12px 16px; border-bottom: 1px solid $border-color; cursor: pointer; + &:hover { background: $bg-light; } + &.unread { background: rgba(28, 56, 201, 0.03); .notif-title { font-weight: 700; } .status-dot { width: 6px; height: 6px; background: $primary; border-radius: 50%; } } + .icon-circle { + width: 36px; height: 36px; border-radius: 8px; display: grid; place-items: center; background: #f3f4f6; color: $text-muted; font-size: 16px; + &.danger { background-color: #fee2e2; color: #dc2626; } + &.warn { background-color: #fef3c7; color: #d97706; } + } + .notif-content { flex: 1; } + .notif-header { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 2px; } + .notif-date { font-size: 11px; color: $text-muted; } + .notif-desc { margin: 0; font-size: 12px; color: $text-muted; line-height: 1.3; } } -.notifications-dropdown { - position: absolute; - top: calc(100% + 12px); right: -10px; - width: 340px; - background: #fff; - border-radius: 16px; - box-shadow: 0 20px 60px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.04); - z-index: 1200; - transform-origin: top right; - animation: slideDown 0.2s ease-out; +/* MODAIS GERAIS */ +.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); backdrop-filter: blur(2px); z-index: 1400; animation: fadeIn 0.2s; } +.modal-card { + position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); + width: min(500px, calc(100vw - 32px)); background: #fff; border-radius: 16px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); z-index: 1450; display: flex; flex-direction: column; animation: scaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes scaleIn { from { opacity: 0; transform: translate(-50%, -48%) scale(0.96); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } +.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid $border-color; h3 { margin: 0; font-size: 16px; font-weight: 600; color: $text-main; } } +.close-x { width: 32px; height: 32px; border-radius: 6px; &:hover { background: #f3f4f6; } } +.modal-body { padding: 20px; } +.modal-actions { padding: 16px 20px; display: flex; justify-content: flex-end; gap: 10px; background: $bg-light; border-radius: 0 0 16px 16px; } + +.form-field { + display: grid; gap: 6px; margin-bottom: 16px; + label { font-size: 13px; font-weight: 500; color: $text-main; } + input, select { width: 100%; height: 40px; border-radius: 8px; border: 1px solid #d1d5db; padding: 0 12px; font-size: 14px; transition: all 0.2s; &:focus { outline: none; border-color: $primary; box-shadow: 0 0 0 3px rgba(28, 56, 201, 0.1); } } +} +.field-error { font-size: 12px; color: $danger; margin-top: 2px; } +.form-alert { + padding: 12px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; + &.error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; } + &.success { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; } + ul { margin: 4px 0 0; padding-left: 16px; } +} +.btn-primary, .btn-secondary, .btn-ghost { height: 38px; padding: 0 16px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: none; display: inline-flex; align-items: center; justify-content: center; } +.btn-primary { background: $primary; color: #fff; &:hover:not(:disabled) { background: $primary-hover; } &:disabled { opacity: 0.7; cursor: not-allowed; } } +.btn-secondary { background: #fff; border: 1px solid $border-color; color: $text-main; &:hover { background: $bg-light; } } +.btn-ghost { background: transparent; color: $text-muted; &:hover { background: rgba(0,0,0,0.05); color: $text-main; } } + +/* ========================================================================== + MODAL EDITAR USUÁRIO - LAYOUT FINAL + ========================================================================== */ +.modal-card.manage-users-modal { + width: min(1200px, 95vw); + height: min(650px, 90vh); +} + +.manage-body { + padding: 0; + display: grid; + grid-template-columns: 50% 50%; + height: 100%; overflow: hidden; } -@keyframes slideDown { - from { opacity: 0; transform: translateY(-10px) scale(0.98); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.notifications-head { - padding: 16px; - border-bottom: 1px solid $border-color; - display: flex; align-items: center; justify-content: space-between; +/* Lado Esquerdo */ +.manage-left { + display: flex; flex-direction: column; + border-right: 1px solid $border-color; background: #fff; - - .head-title { - font-weight: 700; font-size: 15px; color: $text-main; - display: flex; align-items: center; gap: 8px; - } - - .badge-count { - background: $danger; color: #fff; - font-size: 10px; padding: 2px 6px; - border-radius: 99px; font-weight: 800; - } - - .see-all { - font-size: 12px; font-weight: 600; color: $primary; - text-decoration: none; - &:hover { text-decoration: underline; } + min-width: 0; +} +.manage-search { + padding: 12px 16px; border-bottom: 1px solid $border-color; + .search-input-wrapper { + position: relative; + i { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: $text-muted; } + input { width: 100%; height: 36px; padding-left: 36px; padding-right: 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: $bg-light; font-size: 13px; &:focus { background: #fff; border-color: $primary; outline: none; } } } } -.notifications-body { - max-height: 380px; - overflow-y: auto; +.manage-table-wrap { + flex: 1; overflow-y: auto; position: relative; overflow-x: hidden; } -/* Scrollbar Bonito */ -.custom-scroll::-webkit-scrollbar { width: 5px; } -.custom-scroll::-webkit-scrollbar-track { background: transparent; } -.custom-scroll::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 10px; } -.custom-scroll::-webkit-scrollbar-thumb:hover { background: #d1d5db; } - -.notifications-empty { - padding: 40px 20px; - text-align: center; - color: $text-muted; +.manage-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; - .empty-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.5; } - p { margin: 0; font-size: 13px; font-weight: 500; } -} - -/* Item da Notificação */ -.notification-item { - display: flex; gap: 12px; - padding: 12px 16px; - border-bottom: 1px solid $border-color; - cursor: pointer; - transition: background 0.15s; - position: relative; - - &:hover { background: $bg-light; } - &:last-child { border-bottom: none; } - - /* Estilo Não Lido */ - &.unread { - background: rgba(28, 56, 201, 0.02); - &:hover { background: rgba(28, 56, 201, 0.05); } - - .notif-title { color: $text-main; font-weight: 700; } - .status-dot { - width: 8px; height: 8px; - background: $primary; - border-radius: 50%; - display: block; + thead { + position: sticky; top: 0; background: #fff; z-index: 10; + th { + font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; + color: $text-muted; font-weight: 600; padding: 10px 16px; + border-bottom: 1px solid $border-color; background: #f9fafb; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + /* Classes de alinhamento no TH */ + &.text-center { text-align: center; } + &.text-end { text-align: right; } + } + } + tbody tr { + border-bottom: 1px solid $border-color; transition: background 0.15s; cursor: pointer; + &:hover { background: #f9fafb; } + &.selected { background: rgba(28, 56, 201, 0.04); border-left: 3px solid $primary; } + td { + padding: 12px 16px; vertical-align: middle; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + /* Classes de alinhamento no TD */ + &.text-center { text-align: center; overflow: visible; } + &.text-end { text-align: right; overflow: visible; } } } } -.icon-circle { - width: 36px; height: 36px; - border-radius: 10px; - display: grid; place-items: center; - background: #f3f4f6; color: $text-muted; - font-size: 16px; - - &.danger { background: rgba($danger, 0.1); color: $danger; } - &.warn { background: rgba($warning, 0.1); color: darken($warning, 10%); } -} - -.notif-content { flex: 1; min-width: 0; } - -.notif-header { - display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; -} - -.notif-title { font-size: 13px; color: $text-main; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px; } -.notif-date { font-size: 11px; color: $text-muted; } - -.notif-desc { - margin: 0; font-size: 12px; color: $text-muted; - line-height: 1.4; -} - -.notif-meta { - margin-top: 4px; font-size: 11px; color: rgba(0,0,0,0.4); - display: flex; align-items: center; gap: 4px; -} - -.notif-status { - display: flex; align-items: center; justify-content: center; - padding-left: 4px; -} - -/* --- USER OPTIONS (Dropdown) --- */ -.user-trigger { - display: flex; align-items: center; gap: 8px; - padding: 4px 8px 4px 4px; - background: #fff; - border: 1px solid $border-color; - border-radius: 99px; - cursor: pointer; - transition: all 0.2s; - - &:hover { border-color: rgba(0,0,0,0.2); box-shadow: 0 2px 8px rgba(0,0,0,0.05); } - - .user-avatar { - width: 32px; height: 32px; - background: $bg-light; - border-radius: 50%; - display: grid; place-items: center; - color: $text-muted; +.user-cell { + display: flex; align-items: center; gap: 10px; max-width: 100%; + .avatar-mini { + width: 32px; height: 32px; min-width: 32px; + background: $primary; color: #fff; border-radius: 50%; + display: grid; place-items: center; font-weight: 600; font-size: 12px; + } + .user-info { + display: flex; flex-direction: column; min-width: 0; + .u-name { font-size: 13px; font-weight: 500; color: $text-main; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .u-email { font-size: 11px; color: $text-muted; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } - .chevron { font-size: 10px; color: $text-muted; margin-right: 4px; } } -.options-menu { - position: relative; /* Essencial: Torna este o ponto de referência */ - display: flex; - align-items: center; -} +.badge-role { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; background: #eef2ff; color: $primary; border: 1px solid rgba(28, 56, 201, 0.1); } +.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: $success; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); &.off { background: $text-muted; box-shadow: none; opacity: 0.5; } } -.options-dropdown { - position: absolute; - top: 100%; /* Cola no final do botão */ - right: 0; /* Alinha à direita do botão */ - margin-top: 10px; /* Dá o espaçamento visual */ +.actions-group { + display: flex; gap: 4px; + /* Centraliza os botões quando a célula é text-center */ + justify-content: center; - width: 200px; /* Sugestão: um pouco mais largo para caber bem os textos */ - background: #fff; - border-radius: 12px; - box-shadow: 0 10px 30px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.04); - padding: 6px; - z-index: 1200; + .btn-action { + width: 28px; height: 28px; border-radius: 6px; border: none; background: transparent; color: $text-muted; display: inline-flex; align-items: center; justify-content: center; transition: all 0.2s; cursor: pointer; + &:hover { background: #e5e7eb; color: $text-main; } + &.edit:hover { color: $primary; background: rgba(28, 56, 201, 0.1); } + &.delete:hover { color: $danger; background: rgba(239, 68, 68, 0.1); } + } +} - /* Animação suave (Opcional) */ - transform-origin: top right; - animation: slideDown 0.2s ease-out; +.empty-state-list { padding: 40px 20px; text-align: center; color: $text-muted; i { font-size: 24px; opacity: 0.5; display: block; margin-bottom: 8px; } p { font-size: 13px; margin: 0; } } +.loading-state { padding: 40px; text-align: center; } +.list-footer { + padding: 10px 16px; border-top: 1px solid $border-color; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: $text-muted; background: #fff; + .pagination { display: flex; gap: 4px; } + .icon-only { width: 28px; height: 28px; padding: 0; } +} - .options-item { - width: 100%; text-align: left; - padding: 8px 12px; - background: transparent; border: none; - border-radius: 8px; - font-size: 13px; font-weight: 500; color: $text-main; - display: flex; align-items: center; gap: 10px; - cursor: pointer; +/* Lado Direito */ +.manage-right-wrapper { background: #fff; display: flex; flex-direction: column; height: 100%; overflow-y: auto; } +.manage-right { padding: 32px 40px; flex: 1; display: flex; flex-direction: column; } +.edit-header-info { + display: flex; align-items: center; gap: 16px; margin-bottom: 24px; + .avatar-large { width: 56px; height: 56px; background: linear-gradient(135deg, $primary, #4f46e5); color: #fff; font-size: 20px; font-weight: 600; border-radius: 50%; display: grid; place-items: center; box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2); } + .info-text h4 { margin: 0; font-size: 18px; color: $text-main; } + .info-text span { font-size: 13px; color: $text-muted; } +} + +/* AQUI: Placeholder CORRIGIDO */ +.manage-right.placeholder { + align-items: center; justify-content: center; text-align: center; height: 100%; padding: 32px; + background: #fff; /* Fundo branco explícito */ + cursor: default; /* Remove qualquer cursor de loading herdado */ + + .placeholder-content { + max-width: 320px; + margin: 0 auto; + animation: fadeIn 0.3s ease; + + .placeholder-icon { + font-size: 64px; + color: $text-muted; + opacity: 0.2; /* Estilo marca d'água */ + margin-bottom: 16px; + } - &:hover { background: $bg-light; } - &.danger { color: $danger; &:hover { background: rgba($danger, 0.05); } } + h3 { + font-size: 16px; + font-weight: 600; + color: $text-main; + margin-bottom: 8px; + } + + p { + font-size: 14px; + color: $text-muted; + line-height: 1.5; + font-weight: 400; + margin: 0; + } + } +} + +.refined-form { display: flex; flex-direction: column; gap: 16px; .form-row { display: flex; gap: 16px; &.two-col { display: grid; grid-template-columns: 1fr 1fr; } &.align-end { align-items: end; } } } +.toggle-wrapper { display: flex; align-items: center; gap: 12px; height: 40px; .toggle-status { font-size: 13px; color: $text-muted; font-weight: 500; &.active { color: $success; } } } +.switch { + position: relative; display: inline-block; width: 44px; height: 24px; + input { opacity: 0; width: 0; height: 0; } + .slider { position: absolute; cursor: pointer; inset: 0; background-color: #e5e7eb; transition: .4s; &.round { border-radius: 24px; } &.round:before { border-radius: 50%; } } + .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } + input:checked + .slider { background-color: $success; } + 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; } + +@media (max-width: 900px) { + .manage-body { grid-template-columns: 1fr; overflow-y: auto; } + .manage-left { height: 40%; border-right: none; border-bottom: 1px solid $border-color; } + .manage-right-wrapper { height: 60%; } + .manage-right { padding: 20px; } + .form-row.two-col { grid-template-columns: 1fr; } + .manage-table-wrap { overflow-x: auto; } +} + +@media (max-width: 1200px) { + .modal-card.manage-users-modal { + width: min(980px, 92vw); + height: min(600px, 86vh); + } + .modal-card { + width: min(460px, 92vw); + } +} + +@media (max-width: 900px) { + .modal-card.manage-users-modal { + width: min(720px, 92vw); + height: min(560px, 86vh); + } + .modal-card { + width: min(420px, 92vw); + } +} + +@media (max-width: 640px) { + .modal-card.manage-users-modal { + width: min(520px, 94vw); + height: min(520px, 84vh); + } + .modal-card { + width: min(360px, 94vw); + } +} + +/* ========================================================================== + AJUSTES PARA NOTEBOOKS / TELAS MENORES (SOLICITADO) + ========================================================================== */ +/* Adicionado max-width: 1440px e max-height: 800px para pegar notebooks padrão */ +@media (max-width: 1440px), (max-height: 800px) { + /* Modal Genérico (Novo Usuário) - Compacto */ + .modal-card { + /* Limita a altura para não sair da tela e habilita scroll interno */ + max-height: 95vh; } - .divider { height: 1px; background: $border-color; margin: 4px 0; } -} - -/* --- MODAL NOVO USUÁRIO --- */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(15, 23, 42, 0.35); - z-index: 1400; -} - -.modal-card { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: min(720px, calc(100vw - 32px)); - background: #fff; - border-radius: 18px; - border: 1px solid rgba(15, 23, 42, 0.08); - box-shadow: 0 24px 60px rgba(15, 23, 42, 0.25); - z-index: 1450; - display: flex; - flex-direction: column; -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18px 20px; - border-bottom: 1px solid $border-color; - - h3 { - margin: 0; - font-size: 18px; - font-weight: 700; - color: $text-main; - } -} - -.modal-body { - padding: 18px 20px 10px; -} - -.form-alert { - border-radius: 10px; - padding: 10px 12px; - font-size: 13px; - margin-bottom: 12px; - line-height: 1.4; - ul { - margin: 6px 0 0; - padding-left: 18px; - } - &.error { - background: rgba($danger, 0.08); - color: darken($danger, 5%); - border: 1px solid rgba($danger, 0.25); - } - &.success { - background: rgba(#22c55e, 0.1); - color: #15803d; - border: 1px solid rgba(#22c55e, 0.25); - } -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 12px; - padding: 0 20px 18px; -} - -.close-x { - width: 34px; - height: 34px; -} - -.modal-card .user-form { - display: grid; - gap: 14px; -} - -.modal-card .form-field { - display: grid; - gap: 6px; - - label { - font-size: 13px; - font-weight: 600; - color: $text-main; + .modal-header { + padding: 12px 16px; + h3 { font-size: 15px; } } - input, - select { - height: 42px; - border-radius: 10px; - border: 1.5px solid #d7dbe6; - padding: 0 12px; - font-size: 14px; - color: $text-main; - background: #fff; - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05); - transition: border-color 0.2s ease, box-shadow 0.2s ease; - } - - input:focus, - select:focus { - outline: none; - border-color: #2f6bff; - box-shadow: 0 0 0 3px rgba(47, 107, 255, 0.15); - } - - &.has-error input, - &.has-error select { - border-color: $danger; - box-shadow: 0 0 0 3px rgba($danger, 0.12); - } -} - -.field-error { - font-size: 11px; - color: $danger; -} - -.modal-card .btn-primary, -.modal-card .btn-secondary { - height: 40px; - min-width: 110px; - border-radius: 10px; - border: none; - font-weight: 600; - font-size: 14px; - cursor: pointer; - transition: transform 0.15s ease, box-shadow 0.15s ease; -} - -.modal-card .btn-primary { - background: #2f6bff; - color: #fff; - box-shadow: 0 10px 20px rgba(47, 107, 255, 0.2); -} - -.modal-card .btn-primary:disabled { - opacity: 0.6; - cursor: not-allowed; - box-shadow: none; -} - -.modal-card .btn-secondary { - background: #e2e8f0; - color: $text-main; -} - -.modal-card .btn-primary:hover, -.modal-card .btn-secondary:hover { - transform: translateY(-1px); -} - -@media (max-width: 768px) { - .modal-card { - width: min(520px, calc(100vw - 24px)); + .modal-body { + padding: 16px; + /* Essencial para que o conteúdo não estoure o modal quando encolhido */ + overflow-y: auto; } .modal-actions { - flex-direction: column; - align-items: stretch; + padding: 12px 16px; } - .modal-card .btn-primary, - .modal-card .btn-secondary { - width: 100%; + /* Compactar formulários */ + .form-field { + margin-bottom: 12px; + label { font-size: 12px; } + input, select { + height: 36px; + font-size: 13px; + } + } + + .form-alert { + padding: 10px; + margin-bottom: 12px; + font-size: 12px; + } + + /* Modal de Gestão (Editar Usuário) - MUITO COMPACTO (Solicitado) */ + .modal-card.manage-users-modal { + /* Reduzido consideravelmente */ + height: min(500px, 80vh); + width: min(900px, 92vw); + } + + .manage-right { + padding: 24px; + } + + .edit-header-info { + margin-bottom: 16px; + .avatar-large { + width: 48px; + height: 48px; + font-size: 16px; + } + .info-text h4 { + font-size: 16px; + } + } + + .refined-form { + gap: 12px; } } -/* --- MENU LATERAL --- */ -.menu-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.25); - z-index: 1050; -} +/* SIDE MENU */ +.menu-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1050; } .side-menu { - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 280px; - background: #fff; - box-shadow: 8px 0 24px rgba(0, 0, 0, 0.12); - transform: translateX(-100%); - transition: transform 0.25s ease; - z-index: 1100; - display: flex; - flex-direction: column; + position: fixed; top: 0; left: 0; height: 100vh; width: 260px; background: #fff; box-shadow: 4px 0 20px rgba(0,0,0,0.1); transform: translateX(-100%); transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); z-index: 1100; display: flex; flex-direction: column; + &.open { transform: translateX(0); } } - -.side-menu.open { - transform: translateX(0); -} - -.side-menu-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18px 16px; - border-bottom: 1px solid $border-color; -} - -.side-logo { - display: inline-flex; - align-items: center; - gap: 10px; - text-decoration: none; - color: #111827; -} - -.side-logo-icon { - width: 34px; - height: 34px; - border-radius: 50%; - background: conic-gradient(from 210deg, #2f6bff, #7c3aed, #ec4899, #f59e0b, #22c55e, #2f6bff); - color: #fff; - display: grid; - place-items: center; - font-size: 16px; - box-shadow: 0 6px 14px rgba(47, 107, 255, 0.2); -} - -.side-logo-text { - font-size: 16px; - font-weight: 700; - letter-spacing: -0.4px; - .highlight { - background: linear-gradient(90deg, #2f6bff 0%, #7c3aed 55%, #ec4899 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - } -} - +.side-menu-header { padding: 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid $border-color; } .close-btn { background: transparent; border: none; - width: 36px; - height: 36px; - border-radius: 50%; + box-shadow: none; + width: 32px; + height: 32px; + border-radius: 0; display: grid; place-items: center; cursor: pointer; color: $text-main; - transition: background 0.2s; - &:hover { background: rgba(0, 0, 0, 0.06); } + transition: color 0.2s ease; + &:hover { color: $primary; } } - -.side-menu-body { - padding: 12px; - display: flex; - flex-direction: column; - gap: 6px; -} - +.side-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; color: $text-main; font-weight: 700; .side-logo-icon { width: 32px; height: 32px; background: $primary; color: #fff; border-radius: 50%; display: grid; place-items: center; } } +.side-menu-body { padding: 16px; display: flex; flex-direction: column; gap: 4px; } .side-item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - border-radius: 10px; - text-decoration: none; - color: $text-main; - font-weight: 600; - font-size: 14px; - transition: background 0.2s; - + padding: 10px 12px; border-radius: 8px; color: $text-main; text-decoration: none; font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 10px; &:hover { background: $bg-light; } - &.active { background: rgba(28, 56, 201, 0.1); color: $primary; } + &.active { background: rgba(28, 56, 201, 0.08); color: $primary; } } diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index 41c9980..7825a8e 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -6,13 +6,14 @@ import { filter } from 'rxjs/operators'; import { AuthService } from '../../services/auth.service'; import { NotificationsService, NotificationDto } from '../../services/notifications.service'; import { UsersService, CreateUserPayload, ApiFieldError } from '../../services/users.service'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors, FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; +import { CustomSelectComponent } from '../custom-select/custom-select'; @Component({ selector: 'app-header', standalone: true, - imports: [RouterLink, CommonModule, ReactiveFormsModule], + imports: [RouterLink, CommonModule, ReactiveFormsModule, FormsModule, CustomSelectComponent], templateUrl: './header.html', styleUrls: ['./header.scss'], }) @@ -23,6 +24,7 @@ export class Header { optionsOpen = false; notificationsOpen = false; createUserOpen = false; + manageUsersOpen = false; isLoggedHeader = false; isHome = false; isAdmin = false; @@ -37,6 +39,25 @@ export class Header { createUserErrors: ApiFieldError[] = []; createUserForbidden = false; createUserSuccess = ''; + readonly permissionOptions = [ + { value: 'admin', label: 'Administrador' }, + { value: 'gestor', label: 'Gestor' }, + ]; + + manageUsersLoading = false; + manageUsersErrors: ApiFieldError[] = []; + manageUsersSuccess = ''; + manageUsers: any[] = []; + manageSearch = ''; + managePage = 1; + managePageSize = 10; + manageTotal = 0; + + editUserForm: FormGroup; + editUserSubmitting = false; + editUserErrors: ApiFieldError[] = []; + editUserSuccess = ''; + editUserTarget: any | null = null; private readonly loggedPrefixes = [ '/geral', @@ -45,9 +66,10 @@ export class Header { '/dadosusuarios', '/vigencia', '/trocanumero', - '/dashboard', // ✅ ADICIONADO + '/dashboard', '/notificacoes', '/novo-usuario', + '/chips-controle-recebidos', ]; constructor( @@ -69,11 +91,18 @@ export class Header { { validators: this.passwordsMatchValidator } ); - // ✅ resolve no carregamento inicial + this.editUserForm = this.fb.group({ + nome: [''], + email: [''], + senha: [''], + confirmarSenha: [''], + permissao: [''], + ativo: [true], + }); + this.syncHeaderState(this.router.url); this.syncPermissions(); - // ✅ resolve em toda navegação this.router.events .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) .subscribe((event) => { @@ -139,6 +168,19 @@ export class Header { this.resetCreateUserState(); } + openManageUsersModal() { + if (!this.isAdmin) return; + this.manageUsersOpen = true; + this.closeOptions(); + this.resetManageUsersState(); + this.fetchManageUsers(1); + } + + closeManageUsersModal() { + this.manageUsersOpen = false; + this.resetManageUsersState(); + } + toggleNotifications() { this.notificationsOpen = !this.notificationsOpen; if (this.notificationsOpen) { @@ -192,6 +234,7 @@ export class Header { this.closeOptions(); this.closeNotifications(); this.closeCreateUserModal(); + this.closeManageUsersModal(); } acknowledgeNotification(notification: NotificationDto) { @@ -268,6 +311,7 @@ export class Header { } this.createUserSubmitting = true; + this.setCreateFormDisabled(true); this.createUserErrors = []; this.createUserForbidden = false; this.createUserSuccess = ''; @@ -276,11 +320,13 @@ export class Header { this.usersService.create(payload).subscribe({ next: (created) => { this.createUserSubmitting = false; + this.setCreateFormDisabled(false); this.createUserSuccess = `Usuario ${created.nome} criado com sucesso.`; this.createUserForm.reset({ permissao: '' }); }, error: (err: HttpErrorResponse) => { this.createUserSubmitting = false; + this.setCreateFormDisabled(false); if (err.status === 401 || err.status === 403) { this.createUserForbidden = true; return; @@ -298,6 +344,173 @@ export class Header { }); } + fetchManageUsers(goToPage?: number) { + if (goToPage) this.managePage = goToPage; + this.manageUsersLoading = true; + this.manageUsersErrors = []; + this.manageUsersSuccess = ''; + + this.usersService + .list({ + search: this.manageSearch?.trim() || undefined, + page: this.managePage, + pageSize: this.managePageSize, + }) + .subscribe({ + next: (res) => { + this.manageUsers = res.items || []; + this.manageTotal = res.total || 0; + this.manageUsersLoading = false; + }, + error: () => { + this.manageUsers = []; + this.manageTotal = 0; + this.manageUsersLoading = false; + }, + }); + } + + onManageSearch() { + this.managePage = 1; + this.fetchManageUsers(); + } + + clearManageSearch() { + this.manageSearch = ''; + this.managePage = 1; + this.fetchManageUsers(); + } + + manageGoToPage(p: number) { + this.managePage = p; + this.fetchManageUsers(); + } + + get manageTotalPages(): number { + return Math.max(1, Math.ceil((this.manageTotal || 0) / (this.managePageSize || 10))); + } + + get managePageNumbers(): number[] { + const total = this.manageTotalPages; + const current = this.managePage; + const max = 5; + let start = Math.max(1, current - 2); + let end = Math.min(total, start + (max - 1)); + start = Math.max(1, end - (max - 1)); + const pages: number[] = []; + for (let i = start; i <= end; i++) pages.push(i); + return pages; + } + + openEditUser(user: any) { + this.editUserTarget = null; + this.editUserErrors = []; + this.editUserSuccess = ''; + this.editUserSubmitting = false; + this.setEditFormDisabled(false); + this.editUserForm.reset({ nome: '', email: '', senha: '', confirmarSenha: '', permissao: '', ativo: true }); + + this.usersService.getById(user.id).subscribe({ + next: (full) => { + this.editUserTarget = full; + this.editUserForm.reset({ + nome: full.nome ?? '', + email: full.email ?? '', + senha: '', + confirmarSenha: '', + permissao: full.permissao ?? '', + ativo: full.ativo ?? true, + }); + }, + error: () => { + this.editUserErrors = [{ message: 'Erro ao carregar usuario.' }]; + }, + }); + } + + cancelEditUser() { + this.editUserTarget = null; + this.editUserErrors = []; + this.editUserSuccess = ''; + this.editUserSubmitting = false; + this.setEditFormDisabled(false); + this.editUserForm.reset({ nome: '', email: '', senha: '', confirmarSenha: '', permissao: '', ativo: true }); + } + + submitEditUser() { + if (this.editUserSubmitting || !this.editUserTarget) return; + + const payload: any = {}; + const nome = (this.editUserForm.get('nome')?.value || '').toString().trim(); + const email = (this.editUserForm.get('email')?.value || '').toString().trim(); + const permissao = (this.editUserForm.get('permissao')?.value || '').toString().trim(); + const ativo = !!this.editUserForm.get('ativo')?.value; + + if (nome && nome !== (this.editUserTarget.nome || '').trim()) payload.nome = nome; + if (email && email !== (this.editUserTarget.email || '').trim()) payload.email = email; + if (permissao && permissao !== (this.editUserTarget.permissao || '').trim()) payload.permissao = permissao; + if ((this.editUserTarget.ativo ?? true) !== ativo) payload.ativo = ativo; + + const senha = (this.editUserForm.get('senha')?.value || '').toString(); + const confirmar = (this.editUserForm.get('confirmarSenha')?.value || '').toString(); + if (senha || confirmar) { + if (!senha || !confirmar) { + this.editUserErrors = [{ message: 'Para alterar a senha, preencha senha e confirmacao.' }]; + return; + } + if (senha.length < 6) { + this.editUserErrors = [{ message: 'Senha deve ter no minimo 6 caracteres.' }]; + return; + } + if (senha !== confirmar) { + this.editUserErrors = [{ message: 'As senhas nao conferem.' }]; + return; + } + payload.senha = senha; + payload.confirmarSenha = confirmar; + } + + if (Object.keys(payload).length === 0) { + this.editUserErrors = [{ message: 'Nenhuma alteracao detectada.' }]; + return; + } + + this.editUserSubmitting = true; + this.setEditFormDisabled(true); + this.editUserErrors = []; + this.editUserSuccess = ''; + + this.usersService.update(this.editUserTarget.id, payload).subscribe({ + next: (updated) => { + this.editUserSubmitting = false; + this.setEditFormDisabled(false); + this.editUserSuccess = `Usuario ${updated.nome} atualizado.`; + this.editUserTarget = updated; + this.fetchManageUsers(this.managePage); + }, + error: (err: HttpErrorResponse) => { + this.editUserSubmitting = false; + this.setEditFormDisabled(false); + const apiErrors = err?.error?.errors; + if (Array.isArray(apiErrors)) { + this.editUserErrors = apiErrors.map((e: any) => ({ + field: e?.field, + message: e?.message || 'Erro ao atualizar usuario.', + })); + } else { + this.editUserErrors = [{ message: err?.error?.message || 'Erro ao atualizar usuario.' }]; + } + }, + }); + } + + confirmDeleteUser(user: any) { + if (!confirm(`Excluir usuario ${user.nome}?`)) return; + this.usersService.update(user.id, { ativo: false }).subscribe({ + next: () => this.fetchManageUsers(this.managePage), + }); + } + hasFieldError(field: string): boolean { return this.getFieldErrors(field).length > 0; } @@ -318,13 +531,35 @@ export class Header { this.createUserForbidden = false; this.createUserSuccess = ''; this.createUserSubmitting = false; + this.setCreateFormDisabled(false); this.createUserForm.reset({ permissao: '' }); } + private resetManageUsersState() { + this.manageUsersErrors = []; + this.manageUsersSuccess = ''; + this.manageUsersLoading = false; + this.manageUsers = []; + this.manageSearch = ''; + this.managePage = 1; + this.manageTotal = 0; + this.cancelEditUser(); + } + private normalizeField(field?: string | null): string { return (field || '').trim().toLowerCase(); } + private setCreateFormDisabled(disabled: boolean) { + if (disabled) this.createUserForm.disable({ emitEvent: false }); + else this.createUserForm.enable({ emitEvent: false }); + } + + private setEditFormDisabled(disabled: boolean) { + if (disabled) this.editUserForm.disable({ emitEvent: false }); + else this.editUserForm.enable({ emitEvent: false }); + } + private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null { const senha = group.get('senha')?.value; const confirmar = group.get('confirmarSenha')?.value; @@ -332,5 +567,3 @@ export class Header { return senha === confirmar ? null : { passwordsMismatch: true }; } } - - diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html new file mode 100644 index 0000000..092ce92 --- /dev/null +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html @@ -0,0 +1,485 @@ +
+
+
+ LineGestao + +
+
{{ toastMessage }}
+
+
+ +
+ + + + + +
+
+ + +
+
+
+ Gestão de Chips +
+ +
+
Chips Virgens e Recebidos
+ Importação e acompanhamento +
+ +
+
+ +
+ + +
+ +
+
+ +
+ +
+ + + +
+
+ +
+
+ + + + + + + + +
+ +
+ Itens por pág: +
+ + + +
+
+
+
+ + +
+ + +
+
+ +
+ +
+ Nenhum registro encontrado. +
+ +
+
+
+
+
{{ g.observacao }}
+
+ {{ g.total }} Registros +
+
+ +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + +
ITEMNÚMERO DO CHIPOBSERVAÇÕESAÇÕES
{{ r.item }}{{ display(r.numeroDoChip) }}{{ display(r.observacoes) }} +
+ +
+
+
+
+ +
+
+ +
+
+ Mostrando {{ activePageStart }} a {{ activePageEnd }} de {{ activeTotal }} grupos +
+ +
+
+
+ + + +
+
+ +
+ +
+ Nenhum registro encontrado. +
+ +
+
+
+
+
{{ g.conteudo }}
+
+ {{ g.total }} Registros +
+
+ +
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
ANONOTA FISCALDATA DA NFQTD.CONTEÚDO DA NFDATA DO RECEBIMENTOAÇÕES
{{ display(r.ano) }}{{ display(r.notaFiscal) }}{{ formatDate(r.dataDaNf) }}{{ display(r.quantidade) }}{{ display(r.conteudoDaNf) }}{{ formatDate(r.dataDoRecebimento) }} +
+ +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ANONOTA FISCALCHIPSERIALNÚMERO DA LINHAVALOR UNIT.VALOR DA NFAÇÕES
{{ display(r.ano) }}{{ display(r.notaFiscal) }}{{ display(r.chip) }}{{ display(r.serial) }}{{ display(r.numeroDaLinha) }}{{ formatMoney(r.valorUnit) }}{{ formatMoney(r.valorDaNf) }} +
+ +
+
+
+
+
+
+
+
+ +
+
+ Mostrando {{ activePageStart }} a {{ activePageEnd }} de {{ activeTotal }} grupos +
+ +
+
+
+
+ + +
+
+
+ + + + + + + + + + + diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss new file mode 100644 index 0000000..b2ee8cf --- /dev/null +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss @@ -0,0 +1,519 @@ +/* ========================================================== */ +/* VARIÁVEIS E BASE */ +/* ========================================================== */ +:host { + --brand: #E33DCF; + --blue: #030FAA; + --text: #111214; + --muted: rgba(17, 18, 20, 0.65); + + --success-bg: rgba(25, 135, 84, 0.1); + --success-text: #198754; + --warn-bg: rgba(255, 193, 7, 0.15); + --warn-text: #b58100; + + --radius-xl: 22px; + --radius-lg: 16px; + --shadow-card: 0 22px 46px rgba(17, 18, 20, 0.10); + --glass-bg: rgba(255, 255, 255, 0.82); + --glass-border: 1px solid rgba(227, 61, 207, 0.16); + + display: block; + font-family: 'Inter', sans-serif; + color: var(--text); + box-sizing: border-box; +} + +/* fallback: garante que o footer global não apareça nesta rota */ +:host ::ng-deep app-footer { + display: none !important; +} + +/* ========================================================== */ +/* LAYOUT PRINCIPAL (travado para não aparecer footer global) */ +/* ========================================================== */ +.chips-page { + min-height: 100vh; /* ✅ igual Mureg: ocupa 100% da tela */ + overflow-y: auto; + padding: 0 12px; + display: flex; + align-items: flex-start; + justify-content: center; + position: relative; + background: + radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%), + radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%); + + &::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: rgba(255, 255, 255, 0.25); + } +} + +.page-blob { + position: fixed; + pointer-events: none; + border-radius: 999px; + filter: blur(34px); + opacity: 0.55; + z-index: 0; + background: radial-gradient(circle at 30% 30%, rgba(227,61,207,0.55), rgba(227,61,207,0.06)); + animation: floaty 10s ease-in-out infinite; + + &.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; } + &.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; } + &.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; } + &.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: .45; } +} + +@keyframes floaty { + 0% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(18px, 10px) scale(1.03); } + 100% { transform: translate(0, 0) scale(1); } +} + +.container-chips { + width: 100%; + max-width: 1240px; + position: relative; + z-index: 1; + margin-top: 40px; + margin-bottom: 24px; /* ✅ remove aquele "200px" que ajudava o footer global a aparecer */ + display: flex; + min-height: 0; +} + +/* ========================================================== */ +/* CARD PRINCIPAL */ +/* ========================================================== */ +.chips-card { + width: 100%; + border-radius: var(--radius-xl); + overflow-y: auto; + background: var(--glass-bg); + border: var(--glass-border); + backdrop-filter: blur(12px); + box-shadow: var(--shadow-card); + position: relative; + + display: flex; + flex-direction: column; + + height: auto; /* ✅ ocupa a altura útil (igual sensação do Mureg) */ + min-height: 70vh; + + &::before { + content: ''; + position: absolute; + inset: 1px; + border-radius: calc(var(--radius-xl) - 1px); + pointer-events: none; + border: 1px solid rgba(255, 255, 255, 0.65); + opacity: 0.75; + } +} + +.chips-header { + padding: 16px 24px; + border-bottom: 1px solid rgba(17, 18, 20, 0.06); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.06), rgba(255, 255, 255, 0.2)); + flex-shrink: 0; +} + +.header-row-top { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 12px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + text-align: center; + + .title-badge { justify-self: center; margin-bottom: 8px; } + } +} + +.title-badge { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(227, 61, 207, 0.22); + backdrop-filter: blur(10px); + color: var(--text); + font-size: 13px; + font-weight: 800; + + i { color: var(--brand); } +} + +.header-title { + justify-self: center; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.title { + font-size: 24px; + font-weight: 950; + letter-spacing: -0.3px; + color: var(--text); + margin-top: 10px; + margin-bottom: 0; +} + +.subtitle { color: rgba(17, 18, 20, 0.65); font-weight: 700; } + +/* ========================================================== */ +/* TABS E FILTROS */ +/* ========================================================== */ +.tab-row { display: flex; gap: 8px; justify-content: center; margin-top: 16px; } + +.tab-btn { + border: 1px solid rgba(17, 18, 20, 0.1); + background: rgba(255, 255, 255, 0.5); + color: var(--muted); + padding: 8px 16px; + border-radius: 12px; + font-weight: 800; + font-size: 0.85rem; + display: inline-flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; + + &:hover { color: var(--text); background: #fff; } + + &.active { + color: var(--brand); + border-color: rgba(227, 61, 207, 0.35); + background: #fff; + box-shadow: 0 6px 16px rgba(227, 61, 207, 0.15); + } +} + +.controls { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + margin-top: 16px; +} + +/* Pesquisa */ +.search-group { + max-width: 300px; + border-radius: 12px; + overflow-y: auto; + 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); + } + + .input-group-text { background: transparent; border: none; color: var(--muted); padding-left: 14px; } + .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; color: var(--text); box-shadow: none; &:focus { outline: none; } } + .btn-clear { background: transparent; border: none; color: var(--muted); padding: 0 12px; cursor: pointer; &:hover { color: #dc3545; } } +} + +/* Filtros */ +.filters-row { display: flex; gap: 16px; align-items: center; margin-top: 12px; justify-content: center; } +.filter-tabs { display: flex; gap: 4px; padding: 4px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.08); border-radius: 12px; } +.filter-tab { + border: none; background: transparent; padding: 6px 12px; border-radius: 8px; font-size: 0.8rem; font-weight: 800; color: var(--muted); transition: all 0.2s; + &.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); } +} + +/* Select */ +.select-wrapper { position: relative; display: inline-block; min-width: 90px; } +.select-glass { + appearance: none; + background: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(17, 18, 20, 0.15); + border-radius: 12px; + color: var(--blue); + font-weight: 800; + font-size: 0.9rem; + padding: 8px 36px 8px 14px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); + cursor: pointer; + transition: all 0.2s ease; + width: 100%; + + &:hover { background: #fff; border-color: var(--blue); } +} + +/* ========================================================== */ +/* BODY (scroll interno igual Mureg) */ +/* ========================================================== */ +.chips-body { + padding: 0; + background: transparent; + flex: 1; + min-height: 70vh; + overflow: visible; + display: flex; + flex-direction: column; +} + +.content-scroll { + padding: 16px; + overflow: visible; + height: auto; + flex: 1; + min-height: 0; +} + +/* Lists / Groups */ +.group-list { display: flex; flex-direction: column; gap: 12px; } + +.group-card { + background: #fff; + border-radius: 16px; + border: 1px solid rgba(17, 18, 20, 0.08); + overflow: hidden; + transition: all 0.3s ease; + + &:hover { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.1); } + &.expanded { border-color: var(--brand); box-shadow: 0 8px 24px rgba(227, 61, 207, 0.12); } +} + +.group-header { + padding: 16px 24px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + background: linear-gradient(180deg, #fff, #fdfdfd); + + &:hover .group-toggle-icon { color: var(--brand); } +} + +.group-info { display: flex; flex-direction: column; gap: 6px; } +.group-title { margin: 0; font-weight: 800; color: var(--text); font-size: 1rem; } +.group-badges { display: flex; gap: 8px; } + +.badge-pill { + font-size: 0.7rem; + padding: 4px 10px; + border-radius: 999px; + font-weight: 800; + text-transform: uppercase; + background: rgba(3, 15, 170, 0.1); + color: var(--blue); +} + +.group-toggle-icon { font-size: 1.2rem; color: var(--muted); transition: transform 0.3s ease; } +.group-card.expanded .group-toggle-icon { transform: rotate(180deg); color: var(--brand); } + +.group-body-content { + border-top: 1px solid rgba(17, 18, 20, 0.06); + background: #fbfbfc; + animation: slideDown 0.3s cubic-bezier(0.16, 1, 0.3, 1); + padding: 0; +} + +@keyframes slideDown { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.empty-group { + background: rgba(255,255,255,0.7); + border: 1px dashed rgba(17,18,20,0.12); + border-radius: 16px; + padding: 18px; + text-align: center; + font-weight: 800; + color: var(--muted); +} + +/* Table */ +.table-wrap { overflow-x: auto; overflow-y: visible; height: auto; min-height: 0; } +.inner-table-wrap { max-height: none; } + +.table-section { padding: 6px 10px 12px; } +.table-section + .table-section { border-top: 1px dashed rgba(17, 18, 20, 0.12); margin-top: 8px; } +.section-label { + font-size: 0.7rem; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + padding: 8px 6px; +} + +.table-modern { + width: 100%; + border-collapse: separate; + border-spacing: 0; + + thead th { + position: sticky; + top: 0; + z-index: 10; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + border-bottom: 2px solid rgba(227, 61, 207, 0.15); + padding: 12px; + color: rgba(17, 18, 20, 0.7); + font-size: 0.8rem; + font-weight: 950; + letter-spacing: 0.05em; + text-transform: uppercase; + white-space: nowrap; + cursor: pointer; + text-align: center !important; + + &:hover { color: var(--brand); } + } + + tbody tr { + transition: background-color 0.2s; + border-bottom: 1px solid rgba(17, 18, 20, 0.05); + + &:hover { background-color: rgba(227, 61, 207, 0.05); } + + td { border-bottom: 1px solid rgba(17, 18, 20, 0.04); } + } + + td { + padding: 12px; + vertical-align: middle; + white-space: nowrap; + font-size: 0.875rem; + color: var(--text); + text-align: center !important; + } +} + +.sort-caret { font-size: 0.75rem; color: rgba(17, 18, 20, 0.35); &.active { color: var(--brand); } } +.th-content { display: inline-flex; align-items: center; gap: 6px; justify-content: center; } + +.text-brand { color: var(--brand) !important; } +.font-monospace { font-family: 'JetBrains Mono', monospace; letter-spacing: -0.5px; } +.td-clip { max-width: 260px; overflow-y: auto; text-overflow: ellipsis; } +.row-clickable { cursor: pointer; } + +/* Paginação interna */ +.table-pagination { + padding: 12px 8px 16px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} +.page-info { + font-weight: 800; + color: var(--muted); +} + +/* Ações na tabela (estilo Mureg) */ +.action-group { display: flex; justify-content: center; gap: 6px; } +.action-group .btn-icon { + width: 32px; + height: 32px; + border: none; + background: transparent; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: rgba(17,18,20,0.5); + transition: all 0.2s; + cursor: pointer; + &:hover { background: rgba(17,18,20,0.05); color: var(--text); transform: translateY(-1px); } + &.info:hover { color: var(--brand); background: rgba(227, 61, 207, 0.12); } +} + +/* ========================================================== */ +/* FOOTER interno (igual Mureg) */ +/* ========================================================== */ +.chips-footer { + padding: 14px 24px; + border-top: 1px solid rgba(17, 18, 20, 0.06); + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + flex-shrink: 0; + + @media (max-width: 768px) { + justify-content: center; + text-align: center; + } +} + +.pagination-modern .page-link { + color: var(--blue); + font-weight: 900; + border-radius: 10px; + border: 1px solid rgba(17,18,20,0.1); + background: rgba(255,255,255,0.6); + margin: 0 2px; + + &:hover { transform: translateY(-1px); border-color: var(--brand); color: var(--brand); } +} +.pagination-modern .page-item.active .page-link { + background-color: var(--blue); + border-color: var(--blue); + color: #fff; +} + +/* ========================================================== */ +/* MODAIS (mantidos) */ +/* ========================================================== */ +.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } +.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } +.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow-y: auto; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; } +.modal-card.modal-xl-custom { width: min(980px, 92vw); max-height: 82vh; } +.modal-card.modal-lg { width: min(720px, 92vw); max-height: 80vh; } +@keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } + +.modal-header { + padding: 16px 24px; border-bottom: 1px solid rgba(0,0,0,0.06); background: #fff; + display: flex; justify-content: space-between; align-items: center; + + .modal-title { font-size: 1.1rem; font-weight: 800; color: var(--text); display: flex; align-items: center; gap: 12px; } + .icon-bg { width: 32px; height: 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 16px; + &.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); } + } + .btn-icon { color: var(--muted); background: transparent; font-size: 1.2rem; border:none; cursor: pointer; &:hover { color: var(--brand); } } +} + +.modal-body { padding: 20px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } } + +.details-dashboard { display: grid; grid-template-columns: 1fr; gap: 20px; } +div.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.02); overflow-y: auto; height: auto; display: flex; flex-direction: column; } +div.box-header { 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; } +div.box-body { padding: 16px; } + +.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 0; } +.info-item { + display: flex; flex-direction: column; align-items: center; text-align: center; + padding: 6px 8px; background: rgba(245, 245, 247, 0.5); border-radius: 10px; border: 1px solid rgba(0,0,0,0.03); + &.span-2 { grid-column: span 2; } + .lbl { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 800; color: var(--muted); margin-bottom: 2px; } + .val { font-size: 0.85rem; font-weight: 700; color: var(--text); word-break: break-word; line-height: 1.2; } +} + diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts new file mode 100644 index 0000000..769dee2 --- /dev/null +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts @@ -0,0 +1,440 @@ +import { Component, Inject, PLATFORM_ID, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { ChipsControleService, ChipVirgemListDto, ControleRecebidoListDto, SortDir } from '../../services/chips-controle.service'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; + +// Interface para o Agrupamento +interface ChipGroup { + observacao: string; + total: number; + items: ChipVirgemListDto[]; +} + +interface ControleGroup { + conteudo: string; + total: number; + items: ControleRecebidoListDto[]; +} + +type ChipsSortKey = 'item' | 'numeroDoChip' | 'observacoes'; +type ControleSortKey = + | 'ano' | 'item' | 'notaFiscal' | 'chip' | 'serial' | 'conteudoDaNf' | 'numeroDaLinha' + | 'valorUnit' | 'valorDaNf' | 'dataDaNf' | 'dataDoRecebimento' | 'quantidade' | 'isResumo'; + +@Component({ + selector: 'app-chips-controle-recebidos', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './chips-controle-recebidos.html', + styleUrls: ['./chips-controle-recebidos.scss'] +}) +export class ChipsControleRecebidos implements OnInit, OnDestroy { + activeTab: 'chips' | 'controle' = 'chips'; + + // --- Chips --- + chipsRows: ChipVirgemListDto[] = []; + chipsGroups: ChipGroup[] = []; + pagedChipsGroups: ChipGroup[] = []; + expandedGroupObservacao: string | null = null; + + chipsLoading = false; + chipsSearch = ''; + chipsPage = 1; + chipsPageSize = 10; + chipsTotal = 0; + chipsSortBy: ChipsSortKey = 'item'; + chipsSortDir: SortDir = 'asc'; + private chipsSearchTimer: any = null; + + // --- Controle --- + controleRows: ControleRecebidoListDto[] = []; + controleGroups: ControleGroup[] = []; + pagedControleGroups: ControleGroup[] = []; + expandedControleConteudo: string | null = null; + controleLoading = false; + controleSearch = ''; + controlePage = 1; + controlePageSize = 10; + controleTotal = 0; + controleSortBy: ControleSortKey = 'ano'; + controleSortDir: SortDir = 'desc'; + controleAno: number | '' = ''; + controleResumo: '' | 'true' | 'false' = ''; + private controleSearchTimer: any = null; + + // --- Opções --- + pageSizeOptions = [10, 20, 50, 100]; + anoOptions = [ + { label: 'Todos', value: '' }, + { label: '2022', value: 2022 }, + { label: '2023', value: 2023 }, + { label: '2024', value: 2024 }, + { label: '2025', value: 2025 } + ]; + + toastOpen = false; + toastMessage = ''; + toastType: 'success' | 'danger' = 'success'; + private toastTimer: any = null; + + chipDetailOpen = false; + chipDetailLoading = false; + chipDetailData: ChipVirgemListDto | null = null; + + controleDetailOpen = false; + controleDetailLoading = false; + controleDetailData: ControleRecebidoListDto | null = null; + + constructor( + @Inject(PLATFORM_ID) private platformId: object, + private service: ChipsControleService, + private http: HttpClient + ) {} + + ngOnInit(): void { + if (!isPlatformBrowser(this.platformId)) return; + this.fetchChips(); + this.fetchControle(); + } + + ngOnDestroy(): void { + if (this.chipsSearchTimer) clearTimeout(this.chipsSearchTimer); + if (this.controleSearchTimer) clearTimeout(this.controleSearchTimer); + if (this.toastTimer) clearTimeout(this.toastTimer); + } + + setTab(tab: 'chips' | 'controle') { + this.activeTab = tab; + + if (tab === 'chips') { + this.expandedGroupObservacao = null; + this.applyChipsPagination(); + this.closeControleDetail(); + } else { + this.expandedControleConteudo = null; + this.applyControlePagination(); + this.closeChipDetail(); + } + } + + // ===================== + // Chips Virgens + // ===================== + fetchChips() { + this.chipsLoading = true; + + this.service.getChipsVirgens({ + search: this.chipsSearch, + page: 1, + pageSize: 5000, + sortBy: this.chipsSortBy, + sortDir: this.chipsSortDir + }).subscribe({ + next: (res) => { + const items = (res as any)?.items ?? []; + this.chipsRows = items.map((x: any, idx: number) => this.normalizeChip(x, idx)); + this.buildChipsGroups(); + this.chipsTotal = this.chipsGroups.length; + this.applyChipsPagination(); + this.chipsLoading = false; + }, + error: () => { + this.chipsLoading = false; + this.showToast('Erro ao carregar Chips Virgens.', 'danger'); + } + }); + } + + private buildChipsGroups() { + const groupsMap = new Map(); + + this.chipsRows.forEach(row => { + const key = row.observacoes && row.observacoes.trim() !== '' + ? row.observacoes.trim() + : '(Sem Observações)'; + + if (!groupsMap.has(key)) groupsMap.set(key, []); + groupsMap.get(key)?.push(row); + }); + + this.chipsGroups = []; + groupsMap.forEach((items, key) => { + this.chipsGroups.push({ observacao: key, total: items.length, items }); + }); + + this.chipsGroups.sort((a, b) => a.observacao.localeCompare(b.observacao)); + this.expandedGroupObservacao = null; + } + + private applyChipsPagination() { + const start = (this.chipsPage - 1) * this.chipsPageSize; + const end = start + this.chipsPageSize; + this.pagedChipsGroups = this.chipsGroups.slice(start, end); + + if (this.expandedGroupObservacao && !this.pagedChipsGroups.some(g => g.observacao === this.expandedGroupObservacao)) { + this.expandedGroupObservacao = null; + } + } + + toggleGroup(obs: string) { + this.expandedGroupObservacao = this.expandedGroupObservacao === obs ? null : obs; + } + + openChipDetail(row: ChipVirgemListDto) { + if (!row?.id) return; + this.chipDetailOpen = true; + this.chipDetailLoading = true; + this.chipDetailData = null; + + this.service.getChipVirgemById(row.id).subscribe({ + next: (data) => { + this.chipDetailData = data ?? row; + this.chipDetailLoading = false; + }, + error: () => { + this.chipDetailLoading = false; + this.chipDetailData = row; + } + }); + } + + closeChipDetail() { + this.chipDetailOpen = false; + this.chipDetailLoading = false; + this.chipDetailData = null; + } + + onChipsSearch() { + if (this.chipsSearchTimer) clearTimeout(this.chipsSearchTimer); + this.chipsSearchTimer = setTimeout(() => { + this.chipsPage = 1; + this.fetchChips(); + }, 300); + } + + clearChipsSearch() { + this.chipsSearch = ''; + this.chipsPage = 1; + this.fetchChips(); + } + + onChipsPageSizeChange() { + this.chipsPage = 1; + this.applyChipsPagination(); + } + + // ===================== + // Controle Recebidos + // ===================== + fetchControle() { + this.controleLoading = true; + + this.service.getControleRecebidos({ + search: this.controleSearch, + page: 1, + pageSize: 5000, + sortBy: this.controleSortBy, + sortDir: this.controleSortDir, + ano: this.controleAno, + isResumo: this.controleResumo + }).subscribe({ + next: (res) => { + const items = (res as any)?.items ?? []; + this.controleRows = items.map((x: any, idx: number) => this.normalizeControle(x, idx)); + this.buildControleGroups(); + this.controleTotal = this.controleGroups.length; + this.applyControlePagination(); + this.controleLoading = false; + }, + error: () => { + this.controleLoading = false; + this.showToast('Erro ao carregar Controle.', 'danger'); + } + }); + } + + onControleSearch() { + if (this.controleSearchTimer) clearTimeout(this.controleSearchTimer); + this.controleSearchTimer = setTimeout(() => { + this.controlePage = 1; + this.fetchControle(); + }, 300); + } + + clearControleSearch() { + this.controleSearch = ''; + this.controlePage = 1; + this.fetchControle(); + } + + setControleSort(key: ControleSortKey) { + if (this.controleSortBy === key) { + this.controleSortDir = this.controleSortDir === 'asc' ? 'desc' : 'asc'; + } else { + this.controleSortBy = key; + this.controleSortDir = 'asc'; + } + this.controlePage = 1; + this.fetchControle(); + } + + onControlePageSizeChange() { + this.controlePage = 1; + this.applyControlePagination(); + } + + onControleAnoChange() { + this.controlePage = 1; + this.fetchControle(); + } + + setControleResumo(val: '' | 'true' | 'false') { + this.controleResumo = val; + this.controlePage = 1; + this.fetchControle(); + } + + private buildControleGroups() { + const groupsMap = new Map(); + + this.controleRows.forEach(row => { + const key = row.conteudoDaNf && row.conteudoDaNf.trim() !== '' + ? row.conteudoDaNf.trim() + : '(Sem Conteúdo)'; + + if (!groupsMap.has(key)) groupsMap.set(key, []); + groupsMap.get(key)?.push(row); + }); + + this.controleGroups = []; + groupsMap.forEach((items, key) => { + this.controleGroups.push({ conteudo: key, total: items.length, items }); + }); + + this.controleGroups.sort((a, b) => a.conteudo.localeCompare(b.conteudo)); + this.expandedControleConteudo = null; + } + + private applyControlePagination() { + const start = (this.controlePage - 1) * this.controlePageSize; + const end = start + this.controlePageSize; + this.pagedControleGroups = this.controleGroups.slice(start, end); + + if (this.expandedControleConteudo && !this.pagedControleGroups.some(g => g.conteudo === this.expandedControleConteudo)) { + this.expandedControleConteudo = null; + } + } + + toggleControleGroup(conteudo: string) { + this.expandedControleConteudo = this.expandedControleConteudo === conteudo ? null : conteudo; + } + + openControleDetail(row: ControleRecebidoListDto) { + if (!row?.id) return; + this.controleDetailOpen = true; + this.controleDetailLoading = true; + this.controleDetailData = null; + + this.service.getControleRecebidoById(row.id).subscribe({ + next: (data) => { + this.controleDetailData = data ?? row; + this.controleDetailLoading = false; + }, + error: () => { + this.controleDetailLoading = false; + this.controleDetailData = row; + } + }); + } + + closeControleDetail() { + this.controleDetailOpen = false; + this.controleDetailLoading = false; + this.controleDetailData = null; + } + + // ===================== + // Paginação e Helpers + // ===================== + get activePage() { return this.activeTab === 'chips' ? this.chipsPage : this.controlePage; } + get activeTotal() { return this.activeTab === 'chips' ? this.chipsTotal : this.controleTotal; } + get activePageSize() { return this.activeTab === 'chips' ? this.chipsPageSize : this.controlePageSize; } + get activeTotalPages() { return Math.max(1, Math.ceil((this.activeTotal || 0) / (this.activePageSize || 10))); } + + get activePageStart() { return this.activeTotal === 0 ? 0 : (this.activePage - 1) * this.activePageSize + 1; } + get activePageEnd() { return this.activeTotal === 0 ? 0 : Math.min(this.activePage * this.activePageSize, this.activeTotal); } + + get activeLoading() { return this.activeTab === 'chips' ? this.chipsLoading : this.controleLoading; } // ✅ novo + + get activePageNumbers() { + const total = this.activeTotalPages; + const current = this.activePage; + const max = 5; + let start = Math.max(1, current - 2); + let end = Math.min(total, start + (max - 1)); + start = Math.max(1, end - (max - 1)); + const pages = []; + for (let i = start; i <= end; i++) pages.push(i); + return pages; + } + + goToPage(p: number) { + const target = Math.max(1, Math.min(this.activeTotalPages, p)); + + if (this.activeTab === 'chips') { + this.chipsPage = target; + this.applyChipsPagination(); + } else { + this.controlePage = target; + this.applyControlePagination(); + } + } + + normalizeChip(x: any, idx: number): ChipVirgemListDto { + return { + id: String(x.id || idx), + item: Number(x.item || 0), + numeroDoChip: x.numeroDoChip || x.NumeroDoChip, + observacoes: x.observacoes || x.Observacoes + }; + } + + normalizeControle(x: any, idx: number): ControleRecebidoListDto { + return { + id: String(x.id || idx), + ano: x.ano, + item: x.item, + notaFiscal: x.notaFiscal, + chip: x.chip, + serial: x.serial, + conteudoDaNf: x.conteudoDaNf, + numeroDaLinha: x.numeroDaLinha, + valorUnit: x.valorUnit, + valorDaNf: x.valorDaNf, + dataDaNf: x.dataDaNf, + dataDoRecebimento: x.dataDoRecebimento, + quantidade: x.quantidade, + isResumo: x.isResumo + }; + } + + display(val: any) { return val ? String(val) : '-'; } + formatMoney(val: any) { + if (!val) return '-'; + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(val); + } + formatDate(val: any) { if (!val) return '-'; return new Date(val).toLocaleDateString('pt-BR'); } + isResumo(r: any) { return !!r.isResumo; } + getResumoItems(items: ControleRecebidoListDto[]) { return (items || []).filter(r => this.isResumo(r)); } + getDetalheItems(items: ControleRecebidoListDto[]) { return (items || []).filter(r => !this.isResumo(r)); } + trackById(idx: number, item: any) { return item.id; } + + showToast(msg: string, type: 'success' | 'danger') { + this.toastMessage = msg; + this.toastType = type; + this.toastOpen = true; + setTimeout(() => this.toastOpen = false, 3000); + } +} diff --git a/src/app/pages/dados-usuarios/dados-usuarios.html b/src/app/pages/dados-usuarios/dados-usuarios.html index cc9f1b7..703d9f5 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.html +++ b/src/app/pages/dados-usuarios/dados-usuarios.html @@ -76,12 +76,8 @@
Itens por pág:
- - + +
@@ -206,4 +202,5 @@
- \ No newline at end of file + + diff --git a/src/app/pages/dados-usuarios/dados-usuarios.ts b/src/app/pages/dados-usuarios/dados-usuarios.ts index c236b2c..554f286 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.ts +++ b/src/app/pages/dados-usuarios/dados-usuarios.ts @@ -1,7 +1,8 @@ import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { HttpClientModule, HttpErrorResponse } from '@angular/common/http'; +import { HttpErrorResponse } from '@angular/common/http'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { DadosUsuariosService, @@ -16,10 +17,9 @@ type ViewMode = 'lines' | 'groups'; @Component({ selector: 'app-dados-usuarios', standalone: true, - imports: [CommonModule, FormsModule, HttpClientModule], + imports: [CommonModule, FormsModule, CustomSelectComponent], templateUrl: './dados-usuarios.html', - styleUrls: ['./dados-usuarios.scss'], - providers: [DadosUsuariosService] + styleUrls: ['./dados-usuarios.scss'] }) export class DadosUsuarios implements OnInit { @@ -34,6 +34,7 @@ export class DadosUsuarios implements OnInit { // Paginação page = 1; pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; total = 0; // Ordenação @@ -226,4 +227,4 @@ export class DadosUsuarios implements OnInit { } hideToast() { this.toastOpen = false; } -} \ No newline at end of file +} diff --git a/src/app/pages/faturamento/faturamento.html b/src/app/pages/faturamento/faturamento.html index 025f995..f529fd0 100644 --- a/src/app/pages/faturamento/faturamento.html +++ b/src/app/pages/faturamento/faturamento.html @@ -116,7 +116,7 @@
- Total Clientes + Clientes Faturados {{ kpiTotalClientes || 0 }} @@ -124,7 +124,7 @@
- Total Linhas + Linhas Faturadas {{ kpiTotalLinhas || 0 }} @@ -183,13 +183,8 @@
- - + +
@@ -452,3 +447,5 @@ + + diff --git a/src/app/pages/faturamento/faturamento.ts b/src/app/pages/faturamento/faturamento.ts index b8ec83e..78d82bc 100644 --- a/src/app/pages/faturamento/faturamento.ts +++ b/src/app/pages/faturamento/faturamento.ts @@ -12,6 +12,7 @@ import { import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { BillingService, @@ -33,7 +34,7 @@ interface BillingClientGroup { @Component({ standalone: true, - imports: [CommonModule, FormsModule, HttpClientModule], + imports: [CommonModule, FormsModule, HttpClientModule, CustomSelectComponent], templateUrl: './faturamento.html', styleUrls: ['./faturamento.scss'] }) @@ -68,6 +69,7 @@ export class Faturamento implements AfterViewInit, OnDestroy { // pagina por CLIENTES (grupos) page = 1; pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; total = 0; // total de grupos // agrupamento @@ -489,15 +491,35 @@ export class Faturamento implements AfterViewInit, OnDestroy { let totalVivo = 0; let totalLine = 0; let totalLucro = 0; + const clientTotals = new Map(); for (const r of arr) { const c = (r.cliente ?? '').trim(); if (c) unique.add(c); totalLinhas += Number(r.qtdLinhas ?? 0) || 0; - totalVivo += Number(r.valorContratoVivo ?? 0) || 0; - totalLine += Number(r.valorContratoLine ?? 0) || 0; - totalLucro += Number((r as any).lucro ?? 0) || 0; + + const key = this.normalizeText(c); + if (!key) continue; + + const vivo = Number(r.valorContratoVivo ?? 0) || 0; + const line = Number(r.valorContratoLine ?? 0) || 0; + const lucro = Number((r as any).lucro ?? 0) || 0; + + const existing = clientTotals.get(key); + if (!existing) { + clientTotals.set(key, { vivo, line, lucro }); + } else { + if (!existing.vivo && vivo) existing.vivo = vivo; + if (!existing.line && line) existing.line = line; + if (!existing.lucro && lucro) existing.lucro = lucro; + } + } + + for (const vals of clientTotals.values()) { + totalVivo += vals.vivo; + totalLine += vals.line; + totalLucro += vals.lucro; } this.kpiTotalClientes = unique.size; diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index c2b9d32..251e62d 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -182,13 +182,7 @@
- - +
@@ -498,10 +492,7 @@
- +
@@ -560,10 +551,7 @@
- +
@@ -578,10 +566,7 @@
- +
@@ -897,7 +882,7 @@
-
+
@@ -910,7 +895,7 @@ Contrato & Plano
-
+
@@ -921,11 +906,11 @@ Status & Logística
-
+
-
+
@@ -978,3 +963,4 @@
+ diff --git a/src/app/pages/geral/geral.scss b/src/app/pages/geral/geral.scss index 70e0bd2..05a0880 100644 --- a/src/app/pages/geral/geral.scss +++ b/src/app/pages/geral/geral.scss @@ -207,6 +207,7 @@ .btn-icon { color: var(--muted); background: transparent; font-size: 1.2rem; &:hover { color: var(--brand); } } } .modal-body { padding: 24px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } } +.modal-body .box-body { overflow: visible; } .modal-xl-custom { width: min(1100px, 95vw); max-height: 85vh; } /* === MODAL DE EDITAR E SEÇÕES (Accordion) === */ @@ -231,8 +232,8 @@ .details-2col { grid-template-columns: 1fr 1fr; @media (max-width: 900px) { grid-template-columns: 1fr; } } /* Caixas de Detalhes e Accordions simples */ -details.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.02); overflow: hidden; height: fit-content; &:not([open]) { padding-bottom: 0; } } -div.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.02); overflow: hidden; height: 100%; display: flex; flex-direction: column; } +details.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.02); overflow: visible; height: fit-content; &:not([open]) { padding-bottom: 0; } } +div.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.02); overflow: visible; height: 100%; display: flex; flex-direction: column; } summary.box-header { 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; cursor: pointer; list-style: none; user-select: none; i:not(.transition-icon) { color: var(--brand); margin-right: 8px; } &::-webkit-details-marker { display: none; } .transition-icon { margin-left: auto; transition: transform 0.3s ease; font-size: 1rem; color: var(--muted); } } details[open] summary .transition-icon { transform: rotate(180deg); color: var(--brand); } @@ -259,4 +260,4 @@ div.box-body { padding: 16px; &.compact { padding: 12px 16px; } &.compact-paddin /* === FORMULÁRIOS (GERAL) === */ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; @media (max-width: 600px) { grid-template-columns: 1fr; } } .form-field { display: flex; flex-direction: column; gap: 6px; label { font-size: 0.75rem; font-weight: 900; letter-spacing: 0.04em; text-transform: uppercase; color: rgba(17,18,20,0.65); } &.span-2 { grid-column: span 2; } } -.form-control, .form-select { border-radius: 8px; border: 1px solid rgba(17,18,20,0.15); background-color: #fff; font-size: 0.9rem; font-weight: 500; color: var(--text); transition: border-color 0.2s, box-shadow 0.2s; &:hover { border-color: rgba(17, 18, 20, 0.7); } &:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); outline: none; } &:disabled, &[readonly] { background-color: rgba(17, 18, 20, 0.04); border-color: rgba(17, 18, 20, 0.2); color: var(--muted); } } \ No newline at end of file +.form-control, .form-select { border-radius: 8px; border: 1px solid rgba(17,18,20,0.15); background-color: #fff; font-size: 0.9rem; font-weight: 500; color: var(--text); transition: border-color 0.2s, box-shadow 0.2s; &:hover { border-color: rgba(17, 18, 20, 0.7); } &:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); outline: none; } &:disabled, &[readonly] { background-color: rgba(17, 18, 20, 0.04); border-color: rgba(17, 18, 20, 0.2); color: var(--muted); } } diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 9eefd14..4b919bc 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -13,10 +13,10 @@ import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, - HttpClientModule, HttpParams, HttpErrorResponse } from '@angular/common/http'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; type SortDir = 'asc' | 'desc'; type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP'; @@ -53,6 +53,7 @@ interface ApiLineList { interface ApiLineDetail { id: string; item: number; + qtdLinhas?: number | null; conta?: string | null; linha?: string | null; chip?: string | null; @@ -99,7 +100,7 @@ interface ClientGroupDto { @Component({ standalone: true, - imports: [CommonModule, FormsModule, HttpClientModule], + imports: [CommonModule, FormsModule, CustomSelectComponent], templateUrl: './geral.html', styleUrls: ['./geral.scss'] }) @@ -145,6 +146,7 @@ export class Geral implements AfterViewInit, OnDestroy { sortDir: SortDir = 'asc'; page = 1; pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; total = 0; detailOpen = false; @@ -200,6 +202,22 @@ export class Geral implements AfterViewInit, OnDestroy { 'TIM' ]; + get contaOptionsForEdit(): string[] { + return this.mergeOption(this.editModel?.conta, this.contaOptions); + } + + get planOptionsForEdit(): string[] { + return this.mergeOption(this.editModel?.planoContrato, this.planOptions); + } + + get statusOptionsForEdit(): string[] { + return this.mergeOption(this.editModel?.status, this.statusOptions); + } + + get skilOptionsForEdit(): string[] { + return this.mergeOption(this.editModel?.skil, this.skilOptions); + } + createModel: any = { cliente: '', docType: 'PF', @@ -1132,6 +1150,7 @@ export class Geral implements AfterViewInit, OnDestroy { lucro: this.toNullableNumber(this.createModel.lucro) }; + this.http.post(this.apiBase, payload).subscribe({ next: async () => { this.createSaving = false; @@ -1270,4 +1289,10 @@ export class Geral implements AfterViewInit, OnDestroy { const n = parseFloat(v.toString().replace(',', '.')); return Number.isNaN(n) ? null : n; } + + private mergeOption(current: any, list: string[]): string[] { + const v = (current ?? '').toString().trim(); + if (!v) return list; + return list.includes(v) ? list : [v, ...list]; + } } diff --git a/src/app/pages/home/home.ts b/src/app/pages/home/home.ts index d3b10b0..3c8cab9 100644 --- a/src/app/pages/home/home.ts +++ b/src/app/pages/home/home.ts @@ -1,13 +1,11 @@ import { Component, AfterViewInit, Inject, PLATFORM_ID } from '@angular/core'; import { CommonModule, isPlatformBrowser } from '@angular/common'; -import { FeatureCardComponent } from '../../components/feature-card/feature-card'; -import { CtaButtonComponent } from '../../components/cta-button/cta-button'; import { Router } from '@angular/router'; @Component({ selector: 'app-home', standalone: true, - imports: [CommonModule, FeatureCardComponent, CtaButtonComponent], + imports: [CommonModule], templateUrl: './home.html', styleUrls: ['./home.scss'], }) diff --git a/src/app/pages/mureg/mureg.html b/src/app/pages/mureg/mureg.html index f688235..58c0764 100644 --- a/src/app/pages/mureg/mureg.html +++ b/src/app/pages/mureg/mureg.html @@ -88,18 +88,7 @@
- - +
@@ -270,14 +259,7 @@
- + Carregando clientes... @@ -287,19 +269,7 @@
- + Carregando linhas... @@ -394,14 +364,7 @@
- + Carregando clientes... @@ -411,19 +374,7 @@
- + Carregando linhas... @@ -579,3 +530,4 @@
+ diff --git a/src/app/pages/mureg/mureg.ts b/src/app/pages/mureg/mureg.ts index fa633b6..ce66b51 100644 --- a/src/app/pages/mureg/mureg.ts +++ b/src/app/pages/mureg/mureg.ts @@ -9,8 +9,9 @@ import { } from '@angular/core'; import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { LinesService } from '../../services/lines.service'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente'; @@ -50,6 +51,7 @@ interface LineOptionDto { usuario: string | null; cliente?: string | null; skil?: string | null; + label?: string; } interface MuregDetailDto { @@ -73,7 +75,7 @@ interface MuregDetailDto { @Component({ standalone: true, - imports: [CommonModule, FormsModule, HttpClientModule], + imports: [CommonModule, FormsModule, CustomSelectComponent], templateUrl: './mureg.html', styleUrls: ['./mureg.scss'] }) @@ -109,6 +111,7 @@ export class Mureg implements AfterViewInit { private searchTimer: any = null; page = 1; pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; total = 0; // ====== OPTIONS (GERAL) ====== @@ -411,7 +414,8 @@ export class Mureg implements AfterViewInit { chip: x.chip ?? null, usuario: x.usuario ?? null, cliente: x.cliente ?? null, - skil: x.skil ?? null + skil: x.skil ?? null, + label: `${x.item ?? ''} • ${x.linha ?? '-'} • ${x.usuario ?? 'SEM USUÁRIO'}` })) .filter(x => !!String(x.linha ?? '').trim()); diff --git a/src/app/pages/notificacoes/notificacoes.scss b/src/app/pages/notificacoes/notificacoes.scss index 4ef58c1..1bd47b3 100644 --- a/src/app/pages/notificacoes/notificacoes.scss +++ b/src/app/pages/notificacoes/notificacoes.scss @@ -1,3 +1,5 @@ +@use 'sass:color'; + /* Variáveis */ $bg-page: #f9fafb; $white: #ffffff; @@ -153,7 +155,7 @@ $border: #e5e7eb; font-weight: 800; letter-spacing: 0.5px; &.danger { background: rgba($danger, 0.1); color: $danger; } - &.warn { background: rgba($warning, 0.1); color: darken($warning, 10%); } + &.warn { background: rgba($warning, 0.1); color: color.adjust($warning, $lightness: -10%); } } .item-time { font-size: 12px; color: $text-secondary; font-weight: 500; } @@ -183,4 +185,4 @@ $border: #e5e7eb; /* Mobile optimization: show button usually only on hover desktop, always mobile */ @media(min-width: 768px) { opacity: 0.6; } -} \ No newline at end of file +} diff --git a/src/app/pages/novo-usuario/novo-usuario.html b/src/app/pages/novo-usuario/novo-usuario.html index f1c91a4..52c9939 100644 --- a/src/app/pages/novo-usuario/novo-usuario.html +++ b/src/app/pages/novo-usuario/novo-usuario.html @@ -1,48 +1,176 @@
-
-
-

Novo Usuário LineGestão

-

Preencha os dados para criar um novo usuário.

+
+
+
+

Novo Usuário LineGestão

+

Preencha os dados para criar um novo usuário.

+
+ +
+ Confira os campos: +
    +
  • {{ err.message }}
  • +
+
+
{{ createSuccess }}
+ +
+
+ + + Nome obrigatório. +
+ +
+ + + Email inválido. +
+ +
+ + + Senha inválida. +
+ +
+ + + As senhas não conferem. +
+ +
+ + + Selecione uma permissão. +
+ +
+ + +
+
-
-
- - +
+
+
+

Usuários

+

Gerencie permissões e status.

+
+
+ + + +
-
- - +
+
Carregando...
+ + + + + + + + + + + + + + + + + + + +
NomeEmailPermissãoStatusAções
{{ u.nome }}{{ u.email }}{{ u.permissao }} + + {{ u.ativo === false ? 'Inativo' : 'Ativo' }} + + + +
+
+ Nenhum usuario encontrado. +
-
- - + - -
- - -
- -
- - -
- -
- - -
- +
+ + + + + diff --git a/src/app/pages/novo-usuario/novo-usuario.scss b/src/app/pages/novo-usuario/novo-usuario.scss index 286da34..bd77823 100644 --- a/src/app/pages/novo-usuario/novo-usuario.scss +++ b/src/app/pages/novo-usuario/novo-usuario.scss @@ -12,6 +12,13 @@ place-items: center; } +.grid-shell { + width: 100%; + display: grid; + gap: 24px; + grid-template-columns: minmax(0, 1fr); +} + .form-card { width: min(720px, 100%); background: #ffffff; @@ -74,6 +81,174 @@ } } +.form-field.inline { + grid-template-columns: 1fr; +} + +.form-alert { + border-radius: 12px; + padding: 12px 14px; + font-size: 13px; + margin-bottom: 12px; +} + +.form-alert.error { + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.2); + color: #b91c1c; +} + +.form-alert.success { + background: rgba(16, 185, 129, 0.08); + border: 1px solid rgba(16, 185, 129, 0.2); + color: #047857; +} + +.field-error { + color: #b91c1c; + font-size: 12px; +} + +.list-card { + width: min(900px, 100%); + background: #ffffff; + border-radius: 18px; + border: 1px solid rgba(15, 23, 42, 0.08); + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12); + padding: 22px 22px 18px; +} + +.list-header { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: center; + justify-content: space-between; + + h2 { + margin: 0; + font-size: 18px; + color: #0f172a; + } + + p { + margin: 4px 0 0; + font-size: 12px; + color: #64748b; + } +} + +.list-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; + + input { + height: 38px; + border-radius: 10px; + border: 1.5px solid #d7dbe6; + padding: 0 12px; + font-size: 13px; + min-width: 220px; + } +} + +.list-body { + margin-top: 16px; +} + +.users-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th, + td { + padding: 10px 8px; + border-bottom: 1px solid #edf0f6; + text-align: left; + } + + th { + font-weight: 600; + color: #475569; + } +} + +.status-pill { + display: inline-flex; + padding: 4px 10px; + border-radius: 999px; + background: rgba(16, 185, 129, 0.12); + color: #047857; + font-weight: 600; + font-size: 12px; +} + +.status-pill.off { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; +} + +.list-footer { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + margin-top: 12px; + font-size: 12px; + color: #64748b; +} + +.pagination { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.btn-ghost { + height: 34px; + border-radius: 10px; + border: 1px solid #d7dbe6; + background: #fff; + padding: 0 10px; + font-size: 12px; + cursor: pointer; +} + +.btn-ghost.active { + background: #2f6bff; + border-color: #2f6bff; + color: #ffffff; +} + +.btn-link { + border: none; + background: transparent; + color: #2f6bff; + cursor: pointer; + font-weight: 600; +} + +.loading, +.empty { + padding: 18px 0; + text-align: center; + color: #64748b; +} + +.cap { + text-transform: capitalize; +} + +.toggle { + display: inline-flex; + align-items: center; + gap: 8px; +} + .form-actions { display: flex; justify-content: flex-end; @@ -104,16 +279,75 @@ color: #0f172a; } +.btn-secondary, +.btn-ghost { + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + .btn-primary:hover, -.btn-secondary:hover { +.btn-secondary:hover, +.btn-ghost:hover { transform: translateY(-1px); } +/* Modal */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.4); + z-index: 999; +} + +.modal-card { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: min(520px, 92vw); + background: #ffffff; + border-radius: 16px; + box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2); + z-index: 1000; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 18px 10px; + border-bottom: 1px solid #edf0f6; +} + +.modal-body { + padding: 16px 18px; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 0 18px 16px; +} + +.btn-icon { + background: transparent; + border: none; + cursor: pointer; +} + @media (max-width: 768px) { + .grid-shell { + gap: 18px; + } + .form-card { padding: 22px 20px 20px; } + .list-card { + padding: 20px 18px 16px; + } + .form-actions { flex-direction: column; align-items: stretch; @@ -124,3 +358,15 @@ width: 100%; } } + +@media (min-width: 980px) { + .grid-shell { + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); + align-items: start; + } + + .form-card, + .list-card { + width: 100%; + } +} diff --git a/src/app/pages/novo-usuario/novo-usuario.ts b/src/app/pages/novo-usuario/novo-usuario.ts index 761c481..3f6065c 100644 --- a/src/app/pages/novo-usuario/novo-usuario.ts +++ b/src/app/pages/novo-usuario/novo-usuario.ts @@ -1,11 +1,309 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms'; +import { UsersService, CreateUserPayload, UpdateUserPayload, UserDto, ApiFieldError } from '../../services/users.service'; +import { FormsModule } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; @Component({ selector: 'app-novo-usuario', standalone: true, - imports: [CommonModule], + imports: [CommonModule, ReactiveFormsModule, FormsModule, CustomSelectComponent], templateUrl: './novo-usuario.html', styleUrls: ['./novo-usuario.scss'], }) -export class NovoUsuario {} +export class NovoUsuario implements OnInit { + createForm: FormGroup; + editForm: FormGroup; + + permissionOptions = [ + { value: 'admin', label: 'Administrador' }, + { value: 'gestor', label: 'Gestor' }, + ]; + + + createSubmitting = false; + editSubmitting = false; + + createErrors: ApiFieldError[] = []; + editErrors: ApiFieldError[] = []; + + createSuccess = ''; + editSuccess = ''; + + users: UserDto[] = []; + loading = false; + + search = ''; + page = 1; + pageSize = 10; + total = 0; + + editOpen = false; + private editBase: UserDto | null = null; + + constructor(private usersService: UsersService, private fb: FormBuilder) { + this.createForm = this.fb.group( + { + nome: ['', [Validators.required, Validators.minLength(2)]], + email: ['', [Validators.required, Validators.email]], + senha: ['', [Validators.required, Validators.minLength(6)]], + confirmarSenha: ['', [Validators.required, Validators.minLength(6)]], + permissao: ['', [Validators.required]], + }, + { validators: this.passwordsMatchValidator } + ); + + this.editForm = this.fb.group( + { + nome: [''], + email: [''], + senha: [''], + confirmarSenha: [''], + permissao: [''], + ativo: [true], + } + ); + } + + ngOnInit(): void { + this.fetchUsers(1); + } + + get totalPages(): number { + return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10))); + } + + get pageNumbers(): number[] { + const total = this.totalPages; + const current = this.page; + const max = 5; + let start = Math.max(1, current - 2); + let end = Math.min(total, start + (max - 1)); + start = Math.max(1, end - (max - 1)); + const pages: number[] = []; + for (let i = start; i <= end; i++) pages.push(i); + return pages; + } + + fetchUsers(goToPage?: number) { + if (goToPage) this.page = goToPage; + this.loading = true; + this.usersService.list({ + search: this.search?.trim() || undefined, + page: this.page, + pageSize: this.pageSize, + }).subscribe({ + next: (res) => { + this.users = res.items || []; + this.total = res.total || 0; + this.loading = false; + }, + error: () => { + this.users = []; + this.total = 0; + this.loading = false; + } + }); + } + + onSearch() { + this.page = 1; + this.fetchUsers(); + } + + clearSearch() { + this.search = ''; + this.page = 1; + this.fetchUsers(); + } + + onPageSizeChange() { + this.page = 1; + this.fetchUsers(); + } + + goToPage(p: number) { + this.page = p; + this.fetchUsers(); + } + + submitCreate() { + if (this.createSubmitting) return; + if (this.createForm.invalid) { + this.createForm.markAllAsTouched(); + return; + } + + this.createSubmitting = true; + this.setCreateFormDisabled(true); + this.createErrors = []; + this.createSuccess = ''; + + const payload = this.createForm.value as CreateUserPayload; + this.usersService.create(payload).subscribe({ + next: (created) => { + this.createSubmitting = false; + this.setCreateFormDisabled(false); + this.createSuccess = `Usuario ${created.nome} criado com sucesso.`; + this.createForm.reset({ permissao: '' }); + this.fetchUsers(1); + }, + error: (err: HttpErrorResponse) => { + this.createSubmitting = false; + this.setCreateFormDisabled(false); + const apiErrors = err?.error?.errors; + if (Array.isArray(apiErrors)) { + this.createErrors = apiErrors.map((e: any) => ({ + field: e?.field, + message: e?.message || 'Erro ao criar usuario.', + })); + } else { + this.createErrors = [{ message: err?.error?.message || 'Erro ao criar usuario.' }]; + } + }, + }); + } + + openEdit(user: UserDto) { + this.editOpen = true; + this.editErrors = []; + this.editSuccess = ''; + this.editSubmitting = false; + this.setEditFormDisabled(false); + this.editBase = null; + this.editForm.reset({ nome: '', email: '', senha: '', confirmarSenha: '', permissao: '', ativo: true }); + + this.usersService.getById(user.id).subscribe({ + next: (full) => { + this.editBase = full; + this.editForm.reset({ + nome: full.nome ?? '', + email: full.email ?? '', + senha: '', + confirmarSenha: '', + permissao: full.permissao ?? '', + ativo: full.ativo ?? true, + }); + }, + error: () => { + this.editErrors = [{ message: 'Erro ao carregar usuario.' }]; + }, + }); + } + + closeEdit() { + this.editOpen = false; + this.editErrors = []; + this.editSuccess = ''; + this.editSubmitting = false; + this.editBase = null; + this.setEditFormDisabled(false); + } + + submitEdit() { + if (this.editSubmitting || !this.editBase) return; + this.editErrors = []; + this.editSuccess = ''; + + const payload: UpdateUserPayload = {}; + const nome = (this.editForm.get('nome')?.value || '').toString().trim(); + const email = (this.editForm.get('email')?.value || '').toString().trim(); + const permissao = (this.editForm.get('permissao')?.value || '').toString().trim(); + const ativo = !!this.editForm.get('ativo')?.value; + + if (nome && nome !== (this.editBase.nome || '').trim()) payload.nome = nome; + if (email && email !== (this.editBase.email || '').trim()) payload.email = email; + if (permissao && permissao !== (this.editBase.permissao || '').trim()) payload.permissao = permissao as any; + if ((this.editBase.ativo ?? true) !== ativo) payload.ativo = ativo; + + const senha = (this.editForm.get('senha')?.value || '').toString(); + const confirmar = (this.editForm.get('confirmarSenha')?.value || '').toString(); + if (senha || confirmar) { + if (!senha || !confirmar) { + this.editErrors = [{ message: 'Para alterar a senha, preencha senha e confirmaçao.' }]; + return; + } + if (senha.length < 6) { + this.editErrors = [{ message: 'Senha deve ter no minimo 6 caracteres.' }]; + return; + } + if (senha !== confirmar) { + this.editErrors = [{ message: 'As senhas nao conferem.' }]; + return; + } + payload.senha = senha; + payload.confirmarSenha = confirmar; + } + + if (Object.keys(payload).length === 0) { + this.editErrors = [{ message: 'Nenhuma alteraçao detectada.' }]; + return; + } + + this.editSubmitting = true; + this.setEditFormDisabled(true); + this.usersService.update(this.editBase.id, payload).subscribe({ + next: (updated) => { + this.editSubmitting = false; + this.setEditFormDisabled(false); + this.editSuccess = `Usuario ${updated.nome} atualizado.`; + this.fetchUsers(); + }, + error: (err: HttpErrorResponse) => { + this.editSubmitting = false; + this.setEditFormDisabled(false); + const apiErrors = err?.error?.errors; + if (Array.isArray(apiErrors)) { + this.editErrors = apiErrors.map((e: any) => ({ + field: e?.field, + message: e?.message || 'Erro ao atualizar usuario.', + })); + } else { + this.editErrors = [{ message: err?.error?.message || 'Erro ao atualizar usuario.' }]; + } + } + }); + } + + hasCreateFieldError(field: string): boolean { + return this.getFieldErrors(this.createErrors, field).length > 0; + } + + hasEditFieldError(field: string): boolean { + return this.getFieldErrors(this.editErrors, field).length > 0; + } + + getFieldErrors(source: ApiFieldError[], field: string): string[] { + const key = this.normalizeField(field); + return source + .filter((e) => this.normalizeField(e.field) === key) + .map((e) => e.message || 'Erro'); + } + + get createPasswordMismatch(): boolean { + return !!this.createForm.errors?.['passwordsMismatch']; + } + + private normalizeField(field?: string | null): string { + return (field || '').trim().toLowerCase(); + } + + private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null { + const senha = group.get('senha')?.value; + const confirmar = group.get('confirmarSenha')?.value; + if (!senha || !confirmar) return null; + return senha === confirmar ? null : { passwordsMismatch: true }; + } + + private setCreateFormDisabled(disabled: boolean) { + if (disabled) this.createForm.disable({ emitEvent: false }); + else this.createForm.enable({ emitEvent: false }); + } + + private setEditFormDisabled(disabled: boolean) { + if (disabled) this.editForm.disable({ emitEvent: false }); + else this.editForm.enable({ emitEvent: false }); + } +} diff --git a/src/app/pages/troca-numero/troca-numero.html b/src/app/pages/troca-numero/troca-numero.html index 650118c..0430e7a 100644 --- a/src/app/pages/troca-numero/troca-numero.html +++ b/src/app/pages/troca-numero/troca-numero.html @@ -85,13 +85,8 @@
Itens por pág:
- - + +
@@ -320,13 +315,7 @@
- + Carregando clientes... @@ -336,15 +325,7 @@
- + Carregando linhas... @@ -392,3 +373,5 @@
+ + diff --git a/src/app/pages/troca-numero/troca-numero.ts b/src/app/pages/troca-numero/troca-numero.ts index e502e63..5c954c3 100644 --- a/src/app/pages/troca-numero/troca-numero.ts +++ b/src/app/pages/troca-numero/troca-numero.ts @@ -9,7 +9,8 @@ import { } from '@angular/core'; import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao'; @@ -49,11 +50,12 @@ interface LineOptionDto { cliente: string | null; usuario: string | null; skil: string | null; + label?: string; } @Component({ standalone: true, - imports: [CommonModule, FormsModule, HttpClientModule], + imports: [CommonModule, FormsModule, CustomSelectComponent], templateUrl: './troca-numero.html', styleUrls: ['./troca-numero.scss'] }) @@ -92,6 +94,7 @@ export class TrocaNumero implements AfterViewInit { private searchTimer: any = null; page = 1; pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; total = 0; // ====== EDIT MODAL ====== @@ -357,7 +360,10 @@ export class TrocaNumero implements AfterViewInit { this.http.get(`${this.linesApiBase}/by-client`, { params }).subscribe({ next: (res) => { - this.linesFromClient = (res ?? []); + this.linesFromClient = (res ?? []).map((x) => ({ + ...x, + label: `${x.item ?? ''} • ${x.linha ?? '-'} • ${x.usuario ?? 'SEM USUÁRIO'}` + })); this.loadingLines = false; this.cdr.detectChanges(); }, diff --git a/src/app/pages/vigencia/vigencia.html b/src/app/pages/vigencia/vigencia.html index 193ead7..357cd5b 100644 --- a/src/app/pages/vigencia/vigencia.html +++ b/src/app/pages/vigencia/vigencia.html @@ -39,7 +39,7 @@ Total Vencidos {{ kpiTotalVencidos }}
-
+
Valor Total {{ kpiValorTotal | currency:'BRL' }}
@@ -56,12 +56,7 @@
Itens por pág: - +
@@ -216,4 +211,5 @@ - \ No newline at end of file + + diff --git a/src/app/pages/vigencia/vigencia.scss b/src/app/pages/vigencia/vigencia.scss index 76d67aa..1a9a7f4 100644 --- a/src/app/pages/vigencia/vigencia.scss +++ b/src/app/pages/vigencia/vigencia.scss @@ -96,6 +96,14 @@ .val { font-size: 1.25rem; font-weight: 950; color: var(--text); } .text-brand { color: var(--brand) !important; } } + + .kpi.kpi-stack { + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + text-align: center; + } } /* Controls */ @@ -175,4 +183,4 @@ .lg-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } .lg-modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } .lg-modal-card { background: #ffffff; border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); width: 600px; overflow: hidden; animation: popUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } -@keyframes popUp { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } \ No newline at end of file +@keyframes popUp { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } diff --git a/src/app/pages/vigencia/vigencia.ts b/src/app/pages/vigencia/vigencia.ts index e02f24c..d5dd528 100644 --- a/src/app/pages/vigencia/vigencia.ts +++ b/src/app/pages/vigencia/vigencia.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult } from '../../services/vigencia.service'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; type SortDir = 'asc' | 'desc'; type ToastType = 'success' | 'danger'; @@ -11,7 +12,7 @@ type ViewMode = 'lines' | 'groups'; @Component({ selector: 'app-vigencia', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, CustomSelectComponent], templateUrl: './vigencia.html', styleUrls: ['./vigencia.scss'], }) @@ -27,6 +28,7 @@ export class VigenciaComponent implements OnInit { // Paginação page = 1; pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; total = 0; // Ordenação @@ -214,4 +216,4 @@ export class VigenciaComponent implements OnInit { this.toastTimer = setTimeout(() => this.toastOpen = false, 3000); } hideToast() { this.toastOpen = false; } -} \ No newline at end of file +} diff --git a/src/app/services/chips-controle.service.ts b/src/app/services/chips-controle.service.ts new file mode 100644 index 0000000..87374b7 --- /dev/null +++ b/src/app/services/chips-controle.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +export type SortDir = 'asc' | 'desc'; + +export interface PagedResult { + page: number; + pageSize: number; + total: number; + items: T[]; +} + +export interface ChipVirgemListDto { + id: string; + item: number; + numeroDoChip: string | null; + observacoes: string | null; +} + +export interface ControleRecebidoListDto { + id: string; + ano: number | null; + item: number | null; + notaFiscal: string | null; + chip: string | null; + serial: string | null; + conteudoDaNf: string | null; + numeroDaLinha: string | null; + valorUnit: number | null; + valorDaNf: number | null; + dataDaNf: string | null; + dataDoRecebimento: string | null; + quantidade: number | null; + isResumo: boolean | null; +} + +@Injectable({ providedIn: 'root' }) +export class ChipsControleService { + private readonly baseApi: string; + + constructor(private http: HttpClient) { + const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, ''); + this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + } + + getChipsVirgens(opts: { + search?: string; + page?: number; + pageSize?: number; + sortBy?: string; + sortDir?: SortDir; + }): Observable> { + let params = new HttpParams(); + if (opts.search && opts.search.trim()) params = params.set('search', opts.search.trim()); + + params = params.set('page', String(opts.page ?? 1)); + params = params.set('pageSize', String(opts.pageSize ?? 20)); + params = params.set('sortBy', (opts.sortBy ?? 'item').trim()); + params = params.set('sortDir', opts.sortDir ?? 'asc'); + + return this.http.get>(`${this.baseApi}/chips-virgens`, { params }); + } + + getChipVirgemById(id: string): Observable { + return this.http.get(`${this.baseApi}/chips-virgens/${id}`); + } + + getControleRecebidos(opts: { + ano?: number | string | null; + isResumo?: boolean | string | null; + search?: string; + page?: number; + pageSize?: number; + sortBy?: string; + sortDir?: SortDir; + }): Observable> { + let params = new HttpParams(); + const ano = opts.ano ?? ''; + const resumo = opts.isResumo ?? ''; + + if (String(ano).trim()) params = params.set('ano', String(ano).trim()); + if (String(resumo).trim()) params = params.set('isResumo', String(resumo).trim()); + if (opts.search && opts.search.trim()) params = params.set('search', opts.search.trim()); + + params = params.set('page', String(opts.page ?? 1)); + params = params.set('pageSize', String(opts.pageSize ?? 20)); + params = params.set('sortBy', (opts.sortBy ?? 'ano').trim()); + params = params.set('sortDir', opts.sortDir ?? 'asc'); + + return this.http.get>(`${this.baseApi}/controle-recebidos`, { params }); + } + + getControleRecebidoById(id: string): Observable { + return this.http.get(`${this.baseApi}/controle-recebidos/${id}`); + } +} diff --git a/src/app/services/users.service.ts b/src/app/services/users.service.ts index 6fe8d0f..0bd3433 100644 --- a/src/app/services/users.service.ts +++ b/src/app/services/users.service.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; -export type UserPermission = 'admin' | 'gestor' | 'operador' | 'leitura'; +export type UserPermission = 'admin' | 'gestor'; export type UserDto = { id: string; @@ -40,8 +40,12 @@ export type UsersListParams = { }; export type UpdateUserPayload = { - permissao: UserPermission; - ativo: boolean; + nome?: string; + email?: string; + senha?: string; + confirmarSenha?: string; + permissao?: UserPermission; + ativo?: boolean; }; export type PagedResult = { @@ -73,6 +77,10 @@ export class UsersService { return this.http.get>(`${this.baseApi}/users`, { params: httpParams }); } + getById(id: string): Observable { + return this.http.get(`${this.baseApi}/users/${id}`); + } + update(id: string, payload: UpdateUserPayload): Observable { return this.http.patch(`${this.baseApi}/users/${id}`, payload); } diff --git a/src/styles.scss b/src/styles.scss index 570eb99..8cc0a47 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -23,6 +23,51 @@ body { -webkit-font-smoothing: antialiased; /* Garante scroll da página em todo o app */ overflow-y: auto !important; +/* Global select styling to match LineGestao UI */ +select, +select.form-select, +select.form-control { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 42px; + border-radius: 10px; + border: 1.5px solid rgba(15, 23, 42, 0.12); + padding: 0 36px 0 12px; + font-size: 14px; + font-weight: 500; + color: var(--text-main); + background-color: #fff; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 20 20'%3E%3Cpath fill='%2364748B' d='M5.25 7.5 10 12.25 14.75 7.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 14px 14px; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +select:focus, +select.form-select:focus, +select.form-control:focus { + outline: none; + border-color: var(--brand-primary); + box-shadow: 0 0 0 3px var(--brand-soft); +} + +select:disabled, +select.form-select:disabled, +select.form-control:disabled { + background-color: #f1f5f9; + color: var(--text-muted); + cursor: not-allowed; +} + +select.form-select-sm, +select.form-control-sm { + height: 36px; + font-size: 13px; + padding-right: 32px; + background-position: right 10px center; +} } /* Utilitário de animação suave */ @@ -234,3 +279,4 @@ app-header .modal-card .btn-secondary:hover { width: 100%; } } +