Pular para conteúdo

Frontend: Services

Visão Geral

Services do frontend em Ciba.Web/Services/. Encapsulam lógica de comunicação e funcionalidades transversais.

Service Responsabilidade
ApiClient Comunicação HTTP com backend
AuthService Autenticação e sessão
SignalRService Real-time via WebSocket
DialogHelper Abertura de dialogs
ConfirmDeleteHelper Confirmação de exclusão
SnackbarExtensions Feedback de erros
StatusFormatService Formatação de status
WhatsAppFormatter Formatação WhatsApp → HTML
AuthorizationMessageHandler Injeção de JWT nas requests

ApiClient

Client HTTP para comunicação com a API. Única forma de acessar o backend.

Arquivo: Services/ApiClient.cs

Características

  • Retorna Result<T> ou Result (Result pattern do Shared)
  • Usa rotas de ApiRoutes (nunca strings literais)
  • Deserializa erros automaticamente para Error
  • Tratamento de exceções para Errors.Internal

Métodos Auxiliares

private async Task<Result<T>> GetAsync<T>(string url);
private async Task<Result<T>> PostAsync<T>(string url, object? body = null);
private async Task<Result> PostAsync(string url, object? body = null);
private async Task<Result<T>> PutAsync<T>(string url, object? body = null);
private async Task<Result> PutAsync(string url, object? body = null);
private async Task<Result> DeleteAsync(string url);
private async Task<Result<T>> DeleteWithResponseAsync<T>(string url);

Endpoints Disponíveis

Autenticação:

Task<Result<LoginResponse>> LoginAsync(string email, string password)

Agentes:

Task<Result<List<AgentListResponse>>> GetAgentsAsync()
Task<Result<AgentDetailResponse>> GetAgentByIdAsync(Guid id)
Task<Result<CreateAgentResponse>> CreateAgentAsync(CreateAgentRequest request)
Task<Result<UpdateAgentResponse>> UpdateAgentAsync(Guid id, UpdateAgentRequest request)
Task<Result> DeleteAgentAsync(Guid id)
Task<Result<PlaygroundChatResponse>> SendPlaygroundMessageAsync(Guid agentId, List<PlaygroundMessageDto> messages)
Task<Result<AgentAiSettingsResponse>> GetAgentAiSettingsAsync(Guid agentId)
Task<Result<AgentAiSettingsResponse>> UpdateAgentAiSettingsAsync(Guid agentId, UpdateAgentAiSettingsRequest request)

Instâncias:

Task<Result<List<InstanceListResponse>>> GetInstancesAsync()
Task<Result<InstanceDetailResponse>> GetInstanceByIdAsync(Guid id)
Task<Result<CreateInstanceResponse>> CreateInstanceAsync(CreateInstanceRequest request)
Task<Result<UpdateInstanceResponse>> UpdateInstanceAsync(Guid id, UpdateInstanceRequest request)
Task<Result> DeleteInstanceAsync(Guid id)
Task<Result<ConnectResponse>> ConnectInstanceAsync(Guid id)
Task<Result<ConnectionStatusResponse>> GetInstanceStatusAsync(Guid id)
Task<Result> DisconnectInstanceAsync(Guid id)

Conversas:

Task<Result<ConversationListResponse>> GetConversationsByInstanceAsync(Guid instanceId, ...)
Task<Result<ConversationDetailResponse>> GetConversationByIdAsync(Guid conversationId)
Task<Result<MessagePageResponse>> GetConversationMessagesAsync(Guid conversationId, ...)
Task<Result<SendMessageResponse>> SendMessageAsync(Guid conversationId, string content, ...)
Task<Result<ReactToMessageResponse>> ReactToMessageAsync(Guid conversationId, Guid messageId, string emoji)
Task<Result<ResolveConversationResponse>> ResolveConversationAsync(Guid conversationId, string? summary)
Task<Result> MarkConversationAsReadAsync(Guid conversationId)
Task<Result<StartConversationResponse>> StartConversationAsync(Guid instanceId, string contactPhone, ...)

Knowledge Blocks:

Task<Result<List<KnowledgeBlockResponse>>> GetKnowledgeBlocksAsync(Guid agentId)
Task<Result<KnowledgeBlockResponse>> CreateKnowledgeBlockAsync(Guid agentId, CreateKnowledgeBlockRequest)
Task<Result<KnowledgeBlockResponse>> UpdateKnowledgeBlockAsync(Guid agentId, Guid id, UpdateKnowledgeBlockRequest)
Task<Result> DeleteKnowledgeBlockAsync(Guid agentId, Guid id)
Task<Result<List<KnowledgeBlockResponse>>> ReorderKnowledgeBlocksAsync(Guid agentId, List<Guid> orderedIds)

Whitelist/Blacklist:

Task<Result<List<WhitelistEntryResponse>>> GetInstanceWhitelistAsync(Guid instanceId)
Task<Result<WhitelistEntryResponse>> CreateInstanceWhitelistEntryAsync(Guid instanceId, ...)
// ... mesmos métodos para Blacklist

Onboarding:

Task<Result<OnboardingStatusResponse>> GetOnboardingStatusAsync()
Task<Result> CompleteOnboardingAsync()
Task<Result> DismissOnboardingAsync()

Uso

public partial class AgentList
{
    [Inject] private ApiClient ApiClient { get; set; } = null!;

    private async Task Load()
    {
        var result = await ApiClient.GetAgentsAsync();
        if (result.IsSuccess)
        {
            _agents = result.Value;
        }
    }
}

AuthService

Gerenciamento de autenticação e sessão do usuário.

Arquivo: Services/AuthService.cs

Responsabilidades

  • Login e logout
  • Armazenamento de token JWT no LocalStorage
  • Verificação de roles (SuperAdmin, Admin)
  • Seleção de tenant para SuperAdmin

Dados Armazenados (LocalStorage)

Chave Conteudo
authToken JWT token
userName Nome do usuario
userRole Role (SuperAdmin, Admin, User)
selectedTenantId Tenant selecionado (SuperAdmin)
selectedTenantName Nome do tenant selecionado
onboardingChecked Flag de cache do onboarding (limpo no logout)
welcomeCardDismissed Flag de dismiss do welcome card (limpo no logout)

Métodos

// Autenticação
Task<Result> LoginAsync(string email, string password)
Task LogoutAsync()
Task<string?> GetTokenAsync()
Task<string?> GetUserNameAsync()
Task<string?> GetRoleAsync()

// Verificação de roles
Task<bool> IsSuperAdminAsync()
Task<bool> IsAdminAsync()

// Seleção de tenant (SuperAdmin)
Task SetSelectedTenantAsync(Guid tenantId, string tenantName)
Task<Guid?> GetSelectedTenantIdAsync()
Task<string?> GetSelectedTenantNameAsync()
Task<bool> HasTenantSelectedAsync()

// Eventos
event Action? OnTenantChanged

Uso

// No Login
var result = await AuthService.LoginAsync(email, password);
if (result.IsSuccess)
{
    Navigation.NavigateTo("/");
}

// No Logout
await AuthService.LogoutAsync();
Navigation.NavigateTo("/login");

// Verificação de role
if (await AuthService.IsSuperAdminAsync())
{
    // Mostrar menu de tenants
}

AuthorizationMessageHandler

DelegatingHandler que injeta JWT e tenant em todas as requisições HTTP.

Arquivo: Services/AuthorizationMessageHandler.cs

Comportamento

  1. Adiciona Authorization: Bearer {token} em todas as requests
  2. Adiciona X-Tenant-Id se SuperAdmin tem tenant selecionado
  3. Redireciona para /login em caso de 401 Unauthorized
  4. Limpa LocalStorage no logout automático

Registro

// Program.cs
services.AddTransient<AuthorizationMessageHandler>();

services.AddHttpClient<ApiClient>(client =>
{
    client.BaseAddress = new Uri(apiBaseUrl);
})
.AddHttpMessageHandler<AuthorizationMessageHandler>();

SignalRService

Conexão WebSocket para atualizações em tempo real.

Arquivo: Services/SignalRService.cs

Eventos

event Action<Guid, MessageNotification>? OnNewMessage
event Action<Guid, Guid, string, DateTime?>? OnMessageStatusUpdated
event Action<ConversationNotification>? OnConversationUpdated
event Action<ConversationNotification>? OnConversationCreated
event Action<Guid, string?>? OnProfilePictureUpdated
event Action<Guid, ReactionNotification>? OnMessageReaction
event Action<HubConnectionState>? OnConnectionStateChanged

Métodos

Task ConnectAsync()
Task DisconnectAsync()
Task JoinConversationAsync(Guid conversationId)
Task LeaveConversationAsync(Guid conversationId)
Task JoinTenantAsync(Guid tenantId)
Task LeaveTenantAsync(Guid tenantId)

Propriedades

HubConnectionState State { get; }
bool IsConnected { get; }

Reconexão Automática

.WithAutomaticReconnect(new[]
{
    TimeSpan.Zero,           // Imediato
    TimeSpan.FromSeconds(2),
    TimeSpan.FromSeconds(5),
    TimeSpan.FromSeconds(10),
    TimeSpan.FromSeconds(30)
})

DTOs

record MessageNotification(
    Guid Id, string Role, string Type, string Content,
    string? MediaUrl, string? MediaMimeType, string? MediaFileName,
    string Status, DateTime CreatedAt);

record ConversationNotification(
    Guid Id, Guid InstanceId, string ContactPhone, string? ContactName,
    string? ContactProfilePictureUrl, string Status, string Mode,
    DateTime LastMessageAt, string? LastMessage, string? LastMessageRole,
    string? LastMessageStatus, int MessageCount, int UnreadCount);

record ReactionNotification(
    Guid MessageId, string Emoji, string SenderPhone,
    bool FromMe, DateTime CreatedAt);

Uso

public partial class ConversationChat : IDisposable
{
    [Inject] private SignalRService SignalR { get; set; } = null!;

    protected override async Task OnInitializedAsync()
    {
        SignalR.OnNewMessage += HandleNewMessage;
        await SignalR.ConnectAsync();
        await SignalR.JoinConversationAsync(ConversationId);
    }

    private void HandleNewMessage(Guid conversationId, MessageNotification message)
    {
        if (conversationId == ConversationId)
        {
            _messages.Add(message);
            InvokeAsync(StateHasChanged);
        }
    }

    public void Dispose()
    {
        SignalR.OnNewMessage -= HandleNewMessage;
    }
}

SnackbarExtensions

Extensions para exibir feedback de erros via Snackbar.

Arquivo: Services/SnackbarExtensions.cs

Métodos

// Retorna true se erro foi exibido
bool ShowIfError(this ISnackbar snackbar, Result result)
bool ShowIfError<T>(this ISnackbar snackbar, Result<T> result)

Uso

var result = await ApiClient.DeleteAgentAsync(id);
if (Snackbar.ShowIfError(result)) return; // Early return se erro

Snackbar.Add("Agente excluído!", Severity.Success);
await Load();

DialogHelper

Padroniza abertura de dialogs de formulário. Sempre usar DialogHelper ao invés de criar DialogOptions manualmente.

Arquivo: Services/DialogHelper.cs

Métodos

// Retorna true se dialog foi submetido (não cancelado)
Task<bool> ShowFormAsync<TDialog>(
    string? title = null,
    DialogParameters<TDialog>? parameters = null,
    MaxWidth maxWidth = MaxWidth.Small,
    bool closeButton = false)

// Retorna dados do dialog (ou null se cancelado)
Task<TResult?> ShowFormAsync<TDialog, TResult>(
    string? title = null,
    DialogParameters<TDialog>? parameters = null,
    MaxWidth maxWidth = MaxWidth.Small,
    bool closeButton = false)

// Retorna DialogResult para controle total
Task<DialogResult?> ShowAsync<TDialog>(
    string? title = null,
    DialogParameters<TDialog>? parameters = null,
    MaxWidth maxWidth = MaxWidth.Small,
    bool closeButton = false,
    bool allowClose = false)  // habilita BackdropClick e EscapeKey

Uso

[Inject] private DialogHelper Dialog { get; set; } = null!;

// Simples - apenas verifica se não cancelou
if (await Dialog.ShowFormAsync<AgentCreateDialog>())
    await LoadAgents();

// Com parâmetros
var parameters = new DialogParameters<KnowledgeBlockDialog>
{
    { x => x.AgentId, AgentId },
    { x => x.Block, null }
};

if (await Dialog.ShowFormAsync<KnowledgeBlockDialog>("Novo Bloco", parameters))
    await LoadBlocks();

// Tamanho diferente
await Dialog.ShowFormAsync<MeuDialog>(maxWidth: MaxWidth.Medium);

// Retorna dados do dialog
var result = await Dialog.ShowFormAsync<StartConversationDialog, StartConversationDialogResult>(
    "Nova Conversa", parameters, closeButton: true);

if (result != null)
    Navigation.NavigateTo($"/conversations/{result.ConversationId}");

// Controle total (ex: para navegação com ID criado)
var result = await Dialog.ShowAsync<InstanceCreateDialog>();

if (result is { Canceled: false, Data: Guid createdId })
    Navigation.NavigateTo($"/instances/{createdId}?connect=true");

Nota: Para ShowMessageBox (confirmações simples), ainda usar IDialogService diretamente.


ConfirmDeleteHelper

Padroniza confirmação de exclusão com feedback.

Arquivo: Services/ConfirmDeleteHelper.cs

Métodos

// Confirma → Executa → Feedback
// Retorna true se excluído com sucesso
Task<bool> ConfirmAndDelete(string itemName, Func<Task<Result>> deleteAction)

Uso

var deleted = await ConfirmDeleteHelper.ConfirmAndDelete(
    _agent.Name,
    () => ApiClient.DeleteAgentAsync(Id));

if (deleted)
{
    Navigation.NavigateTo("/agents");
}

StatusFormatService

Utilitários estáticos para formatação de status e datas.

Arquivo: Services/StatusFormatService.cs

Métodos

// Status de conexão
static Color GetStatusColor(string? status)      // connected → Success
static string GetStatusLabel(string? status)     // connected → "Conectado"

// Status de conversa
static Color GetConversationStatusColor(string? status)
static string GetConversationStatusLabel(string? status)

// Formatação de data
static string FormatDateTime(DateTime dateTime, string format = "dd/MM/yyyy HH:mm")
static string FormatLocalDateTime(DateTime utcDateTime, string format = "dd/MM/yyyy HH:mm")

// Stage da conversa (aguardando cliente/atendimento)
static string GetConversationStage(string status, string? lastMessageRole)
static Color GetConversationStageColor(string status, string? lastMessageRole)

Mapeamento de Status

Conexão: | Status | Label | Cor | |--------|-------|-----| | connected/open | "Conectado" | Success | | connecting | "Conectando" | Warning | | disconnected/close | "Desconectado" | Default |

Conversa: | Status | Label | Cor | |--------|-------|-----| | Active | "Ativa" | Success | | Escalated | "Escalada" | Warning | | Resolved | "Resolvida" | Default |

Stage (Conversa Ativa): | LastMessageRole | Stage | Cor | |-----------------|-------|-----| | User | "Aguardando Atendimento" | Error | | Assistant | "Aguardando Cliente" | Success | | null | "Nova Conversa" | Info |

Uso

<MudChip Color="@StatusFormatService.GetStatusColor(_instance.Status)">
    @StatusFormatService.GetStatusLabel(_instance.Status)
</MudChip>

WhatsAppFormatter

Converte formatação WhatsApp em HTML para exibição no frontend. Classe estática com regex compilados.

Arquivo: Helpers/WhatsAppFormatter.cs

Formatação Suportada

WhatsApp HTML Exemplo
*texto* <b>texto</b> texto
_texto_ <i>texto</i> texto
~texto~ <s>texto</s> ~~texto~~
`texto` <code>texto</code> texto
\n <br> Quebra de linha

Segurança

  • Aplica WebUtility.HtmlEncode antes das substituições regex para prevenir XSS
  • Retorna MarkupString para renderização segura no Blazor

Uso

@((MarkupString)WhatsAppFormatter.FormatAsHtml(msg.Content).ToString())

Registro de Services

// Program.cs
services.AddBlazoredLocalStorage();

services.AddScoped<AuthService>();
services.AddScoped<SignalRService>();
services.AddScoped<DialogHelper>();
services.AddScoped<ConfirmDeleteHelper>();

services.AddTransient<AuthorizationMessageHandler>();

services.AddHttpClient<ApiClient>(client =>
{
    client.BaseAddress = new Uri(apiBaseUrl);
})
.AddHttpMessageHandler<AuthorizationMessageHandler>();

Padrões de Uso

Carregamento de Dados

private bool _loading = true;
private Result<List<AgentListResponse>>? _result;

protected override async Task OnInitializedAsync() => await Load();

private async Task Load()
{
    _loading = true;
    _result = await ApiClient.GetAgentsAsync();
    _loading = false;
}

Ação com Feedback

private async Task Save()
{
    _saving = true;
    var result = await ApiClient.CreateAgentAsync(_request);
    _saving = false;

    if (Snackbar.ShowIfError(result)) return;

    Snackbar.Add("Agente criado!", Severity.Success);
    Navigation.NavigateTo("/agents");
}

Exclusão com Confirmação

private async Task Delete()
{
    var deleted = await ConfirmDeleteHelper.ConfirmAndDelete(
        _item.Name,
        () => ApiClient.DeleteItemAsync(_item.Id));

    if (deleted) await Load();
}