From e88762c6da71a968c26d3ec529af0191eb7fe176 Mon Sep 17 00:00:00 2001 From: Leon Date: Thu, 26 Feb 2026 17:17:45 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20tela=20e=20fluxo=20de=20cria=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20usu=C3=A1rio=20do=20cliente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app.routes.ts | 8 + src/app/app.ts | 1 + src/app/components/header/header.html | 24 +- src/app/components/header/header.ts | 22 +- src/app/guards/admin.guard.ts | 4 +- src/app/guards/system-admin.guard.ts | 27 + .../chips-controle-recebidos.ts | 2 +- .../pages/dados-usuarios/dados-usuarios.ts | 2 +- src/app/pages/dashboard/dashboard.html | 501 ++++++++++-------- src/app/pages/dashboard/dashboard.ts | 121 ++++- src/app/pages/faturamento/faturamento.ts | 2 +- src/app/pages/geral/geral.html | 63 ++- src/app/pages/geral/geral.ts | 47 +- src/app/pages/parcelamentos/parcelamentos.ts | 2 +- .../system-provision-user.html | 146 +++++ .../system-provision-user.scss | 298 +++++++++++ .../system-provision-user.ts | 255 +++++++++ src/app/pages/vigencia/vigencia.ts | 2 +- src/app/services/system-admin.service.ts | 63 +++ src/app/services/users.service.ts | 2 +- 20 files changed, 1282 insertions(+), 310 deletions(-) create mode 100644 src/app/guards/system-admin.guard.ts create mode 100644 src/app/pages/system-provision-user/system-provision-user.html create mode 100644 src/app/pages/system-provision-user/system-provision-user.scss create mode 100644 src/app/pages/system-provision-user/system-provision-user.ts create mode 100644 src/app/services/system-admin.service.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 53c721b..ff379d1 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -9,6 +9,7 @@ import { Faturamento } from './pages/faturamento/faturamento'; import { authGuard } from './guards/auth.guard'; import { adminGuard } from './guards/admin.guard'; +import { systemAdminGuard } from './guards/system-admin.guard'; import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios'; import { VigenciaComponent } from './pages/vigencia/vigencia'; import { TrocaNumero } from './pages/troca-numero/troca-numero'; @@ -19,6 +20,7 @@ import { Resumo } from './pages/resumo/resumo'; import { Parcelamentos } from './pages/parcelamentos/parcelamentos'; import { Historico } from './pages/historico/historico'; import { Perfil } from './pages/perfil/perfil'; +import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user'; export const routes: Routes = [ { path: '', component: Home }, @@ -37,6 +39,12 @@ export const routes: Routes = [ { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, adminGuard], title: 'Parcelamentos' }, { path: 'historico', component: Historico, canActivate: [authGuard, adminGuard], title: 'Histórico' }, { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, + { + path: 'system/fornecer-usuario', + component: SystemProvisionUserPage, + canActivate: [authGuard, systemAdminGuard], + title: 'Fornecer Usuário', + }, // ✅ rota correta { path: 'dashboard', component: Dashboard, canActivate: [authGuard], title: 'Dashboard' }, diff --git a/src/app/app.ts b/src/app/app.ts index 7bafa32..11b8f61 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -41,6 +41,7 @@ export class AppComponent { '/parcelamentos', '/historico', '/perfil', + '/system', ]; constructor( diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index fa1e907..24a3875 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -191,6 +191,9 @@ +
- - - + + + + + -
+
-
+
@@ -320,7 +323,7 @@ LINHA USUÁRIO STATUS - VENCIMENTO + VENCIMENTO AÇÕES @@ -333,13 +336,15 @@ {{ statusLabel(r.status) }} - {{ r.contrato }} + {{ r.contrato }}
- - - + + + + +
@@ -365,7 +370,7 @@ - + @@ -416,7 +421,7 @@
- +
VENCIMENTO @@ -431,13 +436,13 @@ - + Carregando... - + Nenhum registro encontrado. @@ -450,13 +455,15 @@ {{ statusLabel(r.status) }} {{ r.skil }} - {{ r.contrato }} + {{ r.contrato }}
- - - + + + + +
@@ -1578,7 +1585,7 @@
-
+
Gestão @@ -1632,7 +1639,7 @@
-
+
Contrato & Status diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 12adc05..eabf0de 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -205,6 +205,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { })(); loading = false; isAdmin = false; + isGestor = false; + isClientRestricted = false; rows: LineRow[] = []; clientGroups: ClientGroupDto[] = []; @@ -544,7 +546,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { ngOnInit(): void { if (!isPlatformBrowser(this.platformId)) return; - this.isAdmin = this.authService.hasRole('admin'); + this.isAdmin = this.authService.hasRole('sysadmin'); + this.isGestor = this.authService.hasRole('gestor'); + this.isClientRestricted = !(this.isAdmin || this.isGestor); + + if (this.isClientRestricted) { + this.filterSkil = 'ALL'; + this.additionalMode = 'ALL'; + this.selectedAdditionalServices = []; + this.selectedClients = []; + } } async ngAfterViewInit() { @@ -553,7 +564,9 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { setTimeout(() => { this.refreshData(); - this.loadClients(); + if (!this.isClientRestricted) { + this.loadClients(); + } this.loadPlanRules(); this.loadAccountCompanies(); @@ -574,7 +587,9 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (!url.includes('/geral')) return; this.searchResolvedClient = null; - this.loadClients(); + if (!this.isClientRestricted) { + this.loadClients(); + } this.refreshData(); }); } @@ -915,6 +930,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } setFilter(type: 'ALL' | 'PF' | 'PJ' | 'RESERVA') { + if (this.isClientRestricted && type !== 'ALL') return; + const isSameFilter = this.filterSkil === type; this.expandedGroup = null; @@ -927,13 +944,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.clientSearchTerm = ''; this.searchResolvedClient = null; - this.loadClients(); + if (!this.isClientRestricted) { + this.loadClients(); + } this.page = 1; this.refreshData(); } setAdditionalMode(mode: AdditionalMode) { + if (this.isClientRestricted) return; if (this.additionalMode === mode) return; this.additionalMode = mode; @@ -947,6 +967,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } toggleAdditionalService(key: AdditionalServiceKey) { + if (this.isClientRestricted) return; const idx = this.selectedAdditionalServices.indexOf(key); if (idx >= 0) this.selectedAdditionalServices.splice(idx, 1); else this.selectedAdditionalServices.push(key); @@ -965,6 +986,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } clearAdditionalFilters() { + if (this.isClientRestricted) return; this.additionalMode = 'ALL'; this.selectedAdditionalServices = []; this.expandedGroup = null; @@ -1427,11 +1449,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } toggleClientMenu() { + if (this.isClientRestricted) return; if (!this.showClientMenu) this.showAdditionalMenu = false; this.showClientMenu = !this.showClientMenu; } toggleAdditionalMenu() { + if (this.isClientRestricted) return; if (!this.showAdditionalMenu) this.showClientMenu = false; this.showAdditionalMenu = !this.showAdditionalMenu; } @@ -1450,6 +1474,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } selectClient(client: string | null) { + if (this.isClientRestricted) return; if (client === null) { this.selectedClients = []; } else { @@ -1465,6 +1490,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } removeClient(client: string, event: Event) { + if (this.isClientRestricted) return; event.stopPropagation(); const idx = this.selectedClients.indexOf(client); if (idx >= 0) { @@ -1477,6 +1503,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } clearClientSelection(event?: Event) { + if (this.isClientRestricted) return; if (event) event.stopPropagation(); this.selectedClients = []; this.clientSearchTerm = ''; @@ -1776,7 +1803,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { async onRemover(r: LineRow, fromGroup = false) { if (!this.isAdmin) { - await this.showToast('Apenas administradores podem remover linhas.'); + await this.showToast('Apenas sysadmin pode remover linhas.'); return; } @@ -1808,6 +1835,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onCadastrarLinha() { + if (this.isClientRestricted) { + await this.showToast('Você não tem permissão para cadastrar novos clientes.'); + return; + } + this.createMode = 'NEW_CLIENT'; this.resetCreateModel(); this.createOpen = true; @@ -1815,6 +1847,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onAddLineToGroup(clientName: string) { + if (this.isClientRestricted) { + await this.showToast('Você não tem permissão para adicionar linhas.'); + return; + } + this.createMode = 'NEW_LINE_IN_GROUP'; this.resetCreateModel(); diff --git a/src/app/pages/parcelamentos/parcelamentos.ts b/src/app/pages/parcelamentos/parcelamentos.ts index 278e75b..c879fe2 100644 --- a/src/app/pages/parcelamentos/parcelamentos.ts +++ b/src/app/pages/parcelamentos/parcelamentos.ts @@ -158,7 +158,7 @@ export class Parcelamentos implements OnInit, OnDestroy { } private syncPermissions(): void { - this.isAdmin = this.authService.hasRole('admin'); + this.isAdmin = this.authService.hasRole('sysadmin'); } get totalPages(): number { diff --git a/src/app/pages/system-provision-user/system-provision-user.html b/src/app/pages/system-provision-user/system-provision-user.html new file mode 100644 index 0000000..983d7d6 --- /dev/null +++ b/src/app/pages/system-provision-user/system-provision-user.html @@ -0,0 +1,146 @@ +
+ + + + +
+
+
+
+ SYSTEM ADMIN +
+

Fornecer Usuário para Cliente

+

Selecione um tenant-cliente e crie credenciais de acesso sem misturar tenants.

+
+ +
+
+ {{ tenantsError }} +
+ +
+ {{ successMessage }} +
+ + UserId: {{ createdUser.userId }} | TenantId: + {{ createdUser.tenantId }} + +
+
+ +
+ Falha ao criar usuário: +
    +
  • {{ err }}
  • +
+
+ +
+
+
+ +
+ + +
+ Origem: {{ sourceType }} (apenas tenants ativos). + + Selecione um tenant-cliente. + +
+ +
+ + +
+ +
+ + + Email é obrigatório. + Email inválido. +
+ +
+ + + Senha é obrigatória. + Mínimo de 6 caracteres. +
+ +
+ + + + Confirmação é obrigatória. + + As senhas não conferem. +
+ +
+ +
+ +
+ + Selecione ao menos uma role. + +
+
+ +
+ +
+
+
+
+
+
diff --git a/src/app/pages/system-provision-user/system-provision-user.scss b/src/app/pages/system-provision-user/system-provision-user.scss new file mode 100644 index 0000000..07de5f7 --- /dev/null +++ b/src/app/pages/system-provision-user/system-provision-user.scss @@ -0,0 +1,298 @@ +:host { + --brand: #e33dcf; + --brand-dark: #8b2d7f; + --text: #111214; + --muted: rgba(17, 18, 20, 0.66); + --danger: #dc3545; + --success: #198754; + --border: rgba(17, 18, 20, 0.12); + --card-bg: rgba(255, 255, 255, 0.84); + --surface: #ffffff; + display: block; +} + +.system-provision-page { + min-height: 100vh; + padding: 24px 12px 110px; + position: relative; + background: + radial-gradient(780px 360px at 10% 0%, rgba(227, 61, 207, 0.16), transparent 60%), + radial-gradient(900px 380px at 90% 16%, rgba(3, 15, 170, 0.12), transparent 62%), + linear-gradient(180deg, #ffffff 0%, #f4f6fb 100%); +} + +.page-blob { + position: fixed; + pointer-events: none; + border-radius: 999px; + filter: blur(36px); + opacity: 0.5; + z-index: 0; + background: radial-gradient(circle at 40% 40%, rgba(227, 61, 207, 0.4), rgba(227, 61, 207, 0.08)); + + &.blob-1 { + width: 420px; + height: 420px; + top: -150px; + left: -160px; + } + + &.blob-2 { + width: 560px; + height: 560px; + top: -230px; + right: -260px; + } + + &.blob-3 { + width: 360px; + height: 360px; + bottom: -160px; + left: 30%; + } +} + +.container-shell { + width: 100%; + max-width: 1080px; + margin: 0 auto; + position: relative; + z-index: 1; +} + +.card-shell { + background: var(--card-bg); + border: 1px solid rgba(227, 61, 207, 0.2); + backdrop-filter: blur(10px); + border-radius: 24px; + box-shadow: 0 24px 50px rgba(17, 18, 20, 0.12); + overflow: hidden; +} + +.card-header { + padding: 24px 28px 18px; + border-bottom: 1px solid rgba(17, 18, 20, 0.08); + + h1 { + margin: 10px 0 6px; + font-size: clamp(1.4rem, 2.2vw, 2rem); + font-weight: 900; + letter-spacing: -0.02em; + color: var(--text); + } + + p { + margin: 0; + color: var(--muted); + font-size: 0.95rem; + font-weight: 600; + } +} + +.title-badge { + display: inline-flex; + align-items: center; + gap: 10px; + border-radius: 999px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.82); + border: 1px solid rgba(227, 61, 207, 0.24); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.05em; + color: var(--brand-dark); + + i { + color: var(--brand); + } +} + +.card-body { + padding: 20px 28px 28px; +} + +.alert-box { + border-radius: 12px; + padding: 10px 12px; + font-size: 0.9rem; + font-weight: 600; + margin-bottom: 12px; + + ul { + margin: 8px 0 0 18px; + } + + &.error { + color: var(--danger); + background: rgba(220, 53, 69, 0.1); + border: 1px solid rgba(220, 53, 69, 0.22); + } + + &.success { + color: var(--success); + background: rgba(25, 135, 84, 0.1); + border: 1px solid rgba(25, 135, 84, 0.24); + } +} + +.provision-form { + display: grid; + gap: 18px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.form-field { + display: grid; + gap: 6px; + + &.span-2 { + grid-column: span 2; + } + + label { + margin: 0; + font-size: 0.8rem; + font-weight: 800; + color: var(--text); + text-transform: uppercase; + letter-spacing: 0.03em; + } +} + +.select-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; +} + +.form-control { + width: 100%; + height: 42px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + padding: 0 12px; + font-size: 0.92rem; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + + &:focus { + outline: none; + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.14); + } +} + +.field-help { + font-size: 0.78rem; + color: var(--muted); + font-weight: 600; +} + +.field-error { + font-size: 0.78rem; + color: var(--danger); + font-weight: 700; +} + +.roles-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.role-item { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 10px; + border: 1px solid rgba(17, 18, 20, 0.1); + border-radius: 12px; + padding: 10px; + background: rgba(255, 255, 255, 0.86); + cursor: pointer; + + input { + margin-top: 4px; + } +} + +.role-content { + display: grid; + gap: 2px; + + strong { + font-size: 0.88rem; + color: var(--text); + } + + span { + font-size: 0.78rem; + color: var(--muted); + } +} + +.form-actions { + display: flex; + justify-content: flex-end; +} + +.btn { + border: 1px solid transparent; + border-radius: 10px; + padding: 0 14px; + height: 40px; + font-size: 0.88rem; + font-weight: 700; + cursor: pointer; +} + +.btn-primary { + background: linear-gradient(120deg, #e33dcf, #b131a0); + color: #fff; + + &:disabled { + opacity: 0.65; + cursor: not-allowed; + } +} + +.btn-ghost { + border-color: rgba(17, 18, 20, 0.16); + background: #fff; + color: var(--text); +} + +@media (max-width: 992px) { + .card-header, + .card-body { + padding-left: 18px; + padding-right: 18px; + } + + .roles-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .system-provision-page { + padding-top: 16px; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .form-field.span-2 { + grid-column: span 1; + } + + .select-row { + grid-template-columns: 1fr; + } +} diff --git a/src/app/pages/system-provision-user/system-provision-user.ts b/src/app/pages/system-provision-user/system-provision-user.ts new file mode 100644 index 0000000..90ea561 --- /dev/null +++ b/src/app/pages/system-provision-user/system-provision-user.ts @@ -0,0 +1,255 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { + AbstractControl, + FormBuilder, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; + +import { + SystemAdminService, + SystemTenantDto, + CreateSystemTenantUserResponse, +} from '../../services/system-admin.service'; + +type RoleOption = { + value: string; + label: string; + description: string; +}; + +@Component({ + selector: 'app-system-provision-user', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './system-provision-user.html', + styleUrls: ['./system-provision-user.scss'], +}) +export class SystemProvisionUserPage implements OnInit { + readonly roleOptions: RoleOption[] = [ + { value: 'sysadmin', label: 'SysAdmin', description: 'Acesso administrativo global do sistema (apenas SystemTenant).' }, + { value: 'gestor', label: 'Gestor', description: 'Acesso global de gestão, sem permissões administrativas.' }, + { value: 'cliente', label: 'Cliente', description: 'Acesso restrito ao tenant do cliente.' }, + ]; + + readonly sourceType = 'MobileLines.Cliente'; + + provisionForm: FormGroup; + tenants: SystemTenantDto[] = []; + tenantsLoading = false; + tenantsError = ''; + + submitting = false; + submitErrors: string[] = []; + successMessage = ''; + createdUser: CreateSystemTenantUserResponse | null = null; + + constructor( + private fb: FormBuilder, + private systemAdminService: SystemAdminService + ) { + this.provisionForm = this.fb.group( + { + tenantId: ['', [Validators.required]], + name: [''], + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + confirmPassword: ['', [Validators.required, Validators.minLength(6)]], + roles: this.fb.control(['cliente'], [Validators.required]), + }, + { validators: this.passwordsMatchValidator } + ); + } + + ngOnInit(): void { + this.loadTenants(); + } + + loadTenants(): void { + if (this.tenantsLoading) return; + + this.tenantsLoading = true; + this.tenantsError = ''; + + this.systemAdminService + .listTenants({ source: this.sourceType, active: true }) + .subscribe({ + next: (tenants) => { + this.tenants = (tenants || []).slice().sort((a, b) => + (a.nomeOficial || '').localeCompare(b.nomeOficial || '', 'pt-BR', { sensitivity: 'base' }) + ); + this.tenantsLoading = false; + }, + error: (err: HttpErrorResponse) => { + this.tenantsLoading = false; + this.tenantsError = this.extractErrorMessage( + err, + 'Não foi possível carregar os clientes. Verifique se a conta possui role sysadmin.' + ); + }, + }); + } + + isRoleSelected(role: string): boolean { + const selected = this.selectedRoles; + return selected.includes(role); + } + + toggleRole(role: string, checked: boolean): void { + const current = this.selectedRoles; + const next = checked + ? Array.from(new Set([...current, role])) + : current.filter((value) => value !== role); + + this.rolesControl.setValue(next); + this.rolesControl.markAsDirty(); + this.rolesControl.markAsTouched(); + } + + onSubmit(): void { + if (this.submitting) return; + + this.successMessage = ''; + this.submitErrors = []; + this.createdUser = null; + + if (this.provisionForm.invalid || this.selectedRoles.length === 0) { + this.provisionForm.markAllAsTouched(); + if (this.selectedRoles.length === 0) { + this.submitErrors = ['Selecione ao menos uma role para o usuário.']; + } + return; + } + + const tenantId = String(this.provisionForm.get('tenantId')?.value ?? '').trim(); + const email = String(this.provisionForm.get('email')?.value ?? '').trim().toLowerCase(); + const nameRaw = String(this.provisionForm.get('name')?.value ?? '').trim(); + const password = String(this.provisionForm.get('password')?.value ?? ''); + + this.submitting = true; + this.setFormDisabled(true); + + this.systemAdminService + .createTenantUser(tenantId, { + name: nameRaw, + email, + password, + roles: this.selectedRoles, + }) + .subscribe({ + next: (created) => { + this.submitting = false; + this.setFormDisabled(false); + + this.createdUser = created; + const tenant = this.findTenantById(created.tenantId) ?? this.findTenantById(tenantId); + const tenantName = tenant?.nomeOficial || 'cliente selecionado'; + this.successMessage = `Usuário ${created.email} criado com sucesso para ${tenantName}.`; + + this.provisionForm.patchValue({ + name: '', + email: '', + password: '', + confirmPassword: '', + roles: ['cliente'], + }); + this.provisionForm.markAsPristine(); + this.provisionForm.markAsUntouched(); + }, + error: (err: HttpErrorResponse) => { + this.submitting = false; + this.setFormDisabled(false); + this.submitErrors = this.extractErrors(err); + }, + }); + } + + trackByTenantId(_: number, tenant: SystemTenantDto): string { + return tenant.tenantId; + } + + trackByRoleValue(_: number, role: RoleOption): string { + return role.value; + } + + hasFieldError(field: string, error?: string): boolean { + const control = this.provisionForm.get(field); + if (!control) return false; + if (error) return !!(control.touched && control.hasError(error)); + return !!(control.touched && control.invalid); + } + + get passwordMismatch(): boolean { + const confirmTouched = this.provisionForm.get('confirmPassword')?.touched; + return !!(confirmTouched && this.provisionForm.errors?.['passwordMismatch']); + } + + get selectedRoles(): string[] { + const roles = this.rolesControl.value; + return Array.isArray(roles) ? roles : []; + } + + get rolesControl(): AbstractControl { + return this.provisionForm.get('roles') as AbstractControl; + } + + private findTenantById(tenantId: string): SystemTenantDto | undefined { + return this.tenants.find((tenant) => tenant.tenantId === tenantId); + } + + private setFormDisabled(disabled: boolean): void { + if (disabled) { + this.provisionForm.disable({ emitEvent: false }); + return; + } + + this.provisionForm.enable({ emitEvent: false }); + } + + private extractErrors(err: HttpErrorResponse): string[] { + const apiError = err?.error; + + if (Array.isArray(apiError)) { + const list = apiError.map((entry) => String(entry ?? '').trim()).filter(Boolean); + if (list.length) return list; + } + + if (Array.isArray(apiError?.errors)) { + const list = apiError.errors + .map((entry: unknown) => String((entry as { message?: string })?.message ?? entry ?? '').trim()) + .filter(Boolean); + if (list.length) return list; + } + + if (typeof apiError?.message === 'string' && apiError.message.trim()) { + return [apiError.message.trim()]; + } + + if (typeof apiError === 'string' && apiError.trim()) { + return [apiError.trim()]; + } + + if (err.status === 403) { + return ['Acesso negado. Este recurso é exclusivo para sysadmin.']; + } + + return ['Não foi possível criar o usuário para o cliente selecionado.']; + } + + private extractErrorMessage(err: HttpErrorResponse, fallback: string): string { + const messages = this.extractErrors(err); + if (messages.length) return messages[0]; + return fallback; + } + + private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null { + const password = group.get('password')?.value; + const confirm = group.get('confirmPassword')?.value; + if (!password || !confirm) return null; + return password === confirm ? null : { passwordMismatch: true }; + } +} diff --git a/src/app/pages/vigencia/vigencia.ts b/src/app/pages/vigencia/vigencia.ts index 0160e1e..ba9e1bb 100644 --- a/src/app/pages/vigencia/vigencia.ts +++ b/src/app/pages/vigencia/vigencia.ts @@ -113,7 +113,7 @@ export class VigenciaComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - this.isAdmin = this.authService.hasRole('admin'); + this.isAdmin = this.authService.hasRole('sysadmin'); this.loadClients(); this.loadPlanRules(); this.fetch(1); diff --git a/src/app/services/system-admin.service.ts b/src/app/services/system-admin.service.ts new file mode 100644 index 0000000..044183d --- /dev/null +++ b/src/app/services/system-admin.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { environment } from '../../environments/environment'; + +export type SystemTenantDto = { + tenantId: string; + nomeOficial: string; +}; + +export type ListSystemTenantsParams = { + source?: string; + active?: boolean; +}; + +export type CreateSystemTenantUserPayload = { + name: string; + email: string; + password: string; + roles: string[]; +}; + +export type CreateSystemTenantUserResponse = { + userId: string; + tenantId: string; + email: string; + roles: string[]; +}; + +@Injectable({ providedIn: 'root' }) +export class SystemAdminService { + private readonly baseApi: string; + + constructor(private http: HttpClient) { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + } + + listTenants(params?: ListSystemTenantsParams): Observable { + let httpParams = new HttpParams(); + if (params?.source) { + httpParams = httpParams.set('source', params.source); + } + if (typeof params?.active === 'boolean') { + httpParams = httpParams.set('active', String(params.active)); + } + + return this.http.get(`${this.baseApi}/system/tenants`, { + params: httpParams, + }); + } + + createTenantUser( + tenantId: string, + payload: CreateSystemTenantUserPayload + ): Observable { + return this.http.post( + `${this.baseApi}/system/tenants/${tenantId}/users`, + payload + ); + } +} diff --git a/src/app/services/users.service.ts b/src/app/services/users.service.ts index f6141e1..115f3ce 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'; +export type UserPermission = 'sysadmin' | 'gestor' | 'cliente'; export type UserDto = { id: string;