-
-
-
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nenhum registro encontrado.
+
+
+
+
+
+
+
+
+
+
+
+ | ITEM |
+ NÚMERO DO CHIP |
+ OBSERVAÇÕES |
+ AÇÕES |
+
+
+
+
+ | {{ r.item }} |
+ {{ display(r.numeroDoChip) }} |
+ {{ display(r.observacoes) }} |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nenhum registro encontrado.
+
+
+
+
+
+
+
+
+
+ 0">
+
Resumo
+
+
+
+ | ANO |
+ NOTA FISCAL |
+ DATA DA NF |
+ QTD. |
+ CONTEÚDO DA NF |
+ DATA DO RECEBIMENTO |
+ AÇÕES |
+
+
+
+
+ | {{ display(r.ano) }} |
+ {{ display(r.notaFiscal) }} |
+ {{ formatDate(r.dataDaNf) }} |
+ {{ display(r.quantidade) }} |
+ {{ display(r.conteudoDaNf) }} |
+ {{ formatDate(r.dataDoRecebimento) }} |
+
+
+
+
+ |
+
+
+
+
+
+
+
+ 0">
+
Detalhe
+
+
+
+ | ANO |
+ NOTA FISCAL |
+ CHIP |
+ SERIAL |
+ NÚMERO DA LINHA |
+ VALOR UNIT. |
+ VALOR DA NF |
+ AÇÕES |
+
+
+
+
+ | {{ display(r.ano) }} |
+ {{ display(r.notaFiscal) }} |
+ {{ display(r.chip) }} |
+ {{ display(r.serial) }} |
+ {{ display(r.numeroDaLinha) }} |
+ {{ formatMoney(r.valorUnit) }} |
+ {{ formatMoney(r.valorDaNf) }} |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Carregando detalhes...
+
+
+
+
+
+
+
+
+ Item
+ {{ display(chipDetailData.item) }}
+
+
+ Número do Chip
+ {{ display(chipDetailData.numeroDoChip) }}
+
+
+ Observações
+ {{ display(chipDetailData.observacoes) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Carregando detalhes...
+
+
+
+
+
+
+
+
+ Ano
+ {{ display(controleDetailData.ano) }}
+
+
+ Item
+ {{ display(controleDetailData.item) }}
+
+
+ Nota Fiscal
+ {{ display(controleDetailData.notaFiscal) }}
+
+
+ Chip
+ {{ display(controleDetailData.chip) }}
+
+
+ Serial
+ {{ display(controleDetailData.serial) }}
+
+
+ Conteúdo da NF
+ {{ display(controleDetailData.conteudoDaNf) }}
+
+
+ Número da Linha
+ {{ display(controleDetailData.numeroDaLinha) }}
+
+
+ Quantidade
+ {{ display(controleDetailData.quantidade) }}
+
+
+ Valor Unit
+ {{ formatMoney(controleDetailData.valorUnit) }}
+
+
+ Valor da NF
+ {{ formatMoney(controleDetailData.valorDaNf) }}
+
+
+ Data da NF
+ {{ formatDate(controleDetailData.dataDaNf) }}
+
+
+ Recebimento
+ {{ formatDate(controleDetailData.dataDoRecebimento) }}
+
+
+ Tipo
+ {{ isResumo(controleDetailData) ? "RESUMO" : "DETALHE" }}
+
+
+
+
+
+
+
+
+
+
+
+
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 @@