using System; using System.ComponentModel.Design; using System.Data.Common; using System.Globalization; using System.Numerics; using Npgsql; using Spectre.Console; class Program { static DateTime _endTime; static bool _stop = false; static bool _isAuthenticated = false; static readonly string _connectionString = "Server=192.168.10.248;Port=5432;Database=pipefy_move_cards;User Id=postgres;Password=gds21;"; static readonly DateTime dateToday = DateTime.Today; static DateTime date = dateToday; static DateTime firstDayOfMonth = new(date.Year, date.Month, 1); static DateTime lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddSeconds(-1); static string firstDayOfMonthStr = firstDayOfMonth.ToString("yyyy-MM-dd HH\\:mm\\:sszz", new System.Globalization.CultureInfo("pt-BR")); static string lastDayOfMonthStr = lastDayOfMonth.ToString("yyyy-MM-dd HH\\:mm\\:sszz", new System.Globalization.CultureInfo("pt-BR")); #if DEBUG static readonly string _windowsID = "gestao1.3"; #else static readonly string _windowsID = Environment.UserName; #endif static readonly List<(string actionId, string actionName)> _actionIds = // Tuplas de IDs e nomes das fases que queremos monitorar [ new ("318642783-318906957", "Agendou reunião 1"), new ("322784034-332649256", "Agendou reunião 2"), new ("332659066-332653733", "Agendou reunião 3"), new ("332659083-339300210", "Agendou reunião 4"), new ("339300218-339300231", "Agendou reunião 5"), new ("318906957-322784034", "Realizou reunião 1"), new ("332649256-332659066", "Realizou reunião 2"), new ("332653733-332659083", "Realizou reunião 3"), new ("339300210-339300218", "Realizou reunião 4"), new ("339300231-339300232", "Realizou reunião 5"), new ("318906957-318642783", "Reunião cancelada"), new ("332649256-322784034", "Reunião cancelada"), new ("332653733-332659066", "Reunião cancelada"), new ("339300210-332659083", "Reunião cancelada"), new ("339300231-339300218", "Reunião cancelada"), new ("325103205-325103208", "Agendou reunião 1"), new ("325103212-332710299", "Agendou reunião 2"), new ("332710321-332710322", "Agendou reunião 3"), new ("332710362-339304168", "Agendou reunião 4"), new ("339304193-339304215", "Agendou reunião 5"), new ("325103208-325103212", "Realizou reunião 1"), new ("332710299-332710321", "Realizou reunião 2"), new ("332710322-332710362", "Realizou reunião 3"), new ("339304168-339304193", "Realizou reunião 4"), new ("339304215-339304218", "Realizou reunião 5"), new ("325103208-325103205", "Reunião cancelada"), new ("332710299-325103212", "Reunião cancelada"), new ("332710322-332710321", "Reunião cancelada"), new ("339304168-332710362", "Reunião cancelada"), new ("339304215-339304193", "Reunião cancelada") ]; // Agora guardamos uma lista de tuplas (UserID, Nome) static readonly List<(BigInteger UserId, string Nome)> _pipeUsers = []; static void Main() { AnsiConsole.Clear(); // 1) Autenticação (pode retornar múltiplos usuários) Autenticacao(); if (!_isAuthenticated) { AnsiConsole.MarkupLine("[bold red]Nenhum usuário encontrado ou erro na autenticação. Saindo...[/]"); Thread.Sleep(3000); return; } // 2) Loop principal: atualizar dados e aguardar entrada while (!_stop) { AtualizarDados(); AguardarEntrada(); } AnsiConsole.MarkupLine("[bold yellow]Aplicação encerrada.[/]"); } static void SetDateEval(string monthString) { if (System.Text.RegularExpressions.Regex.IsMatch(monthString, @"^\d{4}$")) { if (!DateTime.TryParseExact(monthString[2..] + "/" + monthString[..2], "MM/yy", CultureInfo.InvariantCulture, DateTimeStyles.None, out date)) { date = dateToday; } } else { date = dateToday; } firstDayOfMonth = new DateTime(date.Year, date.Month, 1); lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddSeconds(-1); firstDayOfMonthStr = firstDayOfMonth.ToString("yyyy-MM-dd HH\\:mm\\:sszz", new System.Globalization.CultureInfo("pt-BR")); lastDayOfMonthStr = lastDayOfMonth.ToString("yyyy-MM-dd HH\\:mm\\:sszz", new System.Globalization.CultureInfo("pt-BR")); } static void AtualizarDados() { var records = new List(); var tableContLigacoes = CriarTabelaLigacoes(); var tableContReunioes = CriarTabelaReunioes(); // Para cada usuário, rodar uma query COUNT(*) e adicionar linha na tabela foreach (var (userId, nome) in _pipeUsers) { var userRecords = ObterRegistrosUsuario(userId, nome); var (count, countA, countR, countC) = ContarRegistros(userRecords); var format = ObterFormatoCor(count, userRecords.Select(x => x.MovedAt.Date).Distinct().Count()); tableContLigacoes.AddRow(nome, userId.ToString(), $"{format} {count} [/]"); tableContReunioes.AddRow(nome, userId.ToString(), $"[yellow] {countA:#;-#;} [/]", $"[green] {countR:#;-#;} [/]", $"[red] {countC:#;-#;} [/]"); records.AddRange(userRecords); } var tableReunioes = CriarTabelaDetalhesReunioes(); foreach (var record in records.OrderBy(x => x.User).ThenBy(x => x.Acao).ThenBy(x => x.MovedAt.Date)) { if (record.Acao is not null) { tableReunioes.AddRow( record.MovedAt.ToString("d"), record.Acao ?? "Ação desconhecida", record.Title ?? "", $"[link=https://app.pipefy.com/open-cards/{record.CardID}]{record.CardID}[/]", record.User ?? "" ); } } AnsiConsole.Clear(); AnsiConsole.MarkupLine("\n[gray]Pressione [green]ENTER[/] para atualizar agora, [green]'/'[/] para visualizar outro mês, ou qualquer outra tecla para sair.[/]"); ExibirCabecalho(); ExibirPainel(tableContLigacoes, "[blue bold] Ligações [/]"); ExibirPainel(tableContReunioes, "[blue bold] Reuniões Agendadas e Realizadas [/]"); ExibirPainel(tableReunioes, "[blue bold] Reuniões no Mês [/]"); AnsiConsole.MarkupLine("\n[gray]Pressione [green]ENTER[/] para atualizar agora, [green]'/'[/] para visualizar outro mês, ou qualquer outra tecla para sair.[/]"); } static void ExibirCabecalho() { // Painel de cabeçalho que mostra quantos usuários foram carregados var status = new Align(new Panel($"[bold]WindowsID[/]: {_windowsID} | [bold]Usuários encontrados[/]: {_pipeUsers.Count}"), HorizontalAlignment.Center, VerticalAlignment.Top); var header = new Panel(status) .Header($"[yellow] Resumo Diário por Usuário - {date:Y} [/]", Justify.Center) .Expand(); AnsiConsole.Write(new Align(header, HorizontalAlignment.Center, VerticalAlignment.Top)); } static Table CriarTabelaLigacoes() { var table = new Table().Border(TableBorder.Rounded).Alignment(Justify.Center); table.AddColumn(new TableColumn("[green]Nome[/]").Centered()); table.AddColumn(new TableColumn("[green]UserID[/]").Centered()); table.AddColumn(new TableColumn("[green]Registros " + (date != dateToday ? "no mês" : "Hoje") + "[/]").Centered()); return table; } static Table CriarTabelaReunioes() { var table = new Table().Border(TableBorder.Rounded).Alignment(Justify.Center); table.AddColumn(new TableColumn("[white]Nome[/]").Centered()); table.AddColumn(new TableColumn("[white]UserID[/]").Centered()); table.AddColumn(new TableColumn("[yellow]Agendadas[/]").Centered()); table.AddColumn(new TableColumn("[green]Realizadas[/]").Centered()); table.AddColumn(new TableColumn("[red]Canceladas[/]").Centered()); return table; } static Table CriarTabelaDetalhesReunioes() { var table = new Table().Border(TableBorder.Rounded).Alignment(Justify.Center); table.AddColumn(new TableColumn("[green]Data[/]").Centered()); table.AddColumn(new TableColumn("[green]Status[/]").Centered()); table.AddColumn(new TableColumn("[green]Empresa[/]").Centered()); table.AddColumn(new TableColumn("[green]Card ID\n(Ctrl + click)[/]").Centered()); table.AddColumn(new TableColumn("[green]Prospectante[/]").Centered()); return table; } static void ExibirPainel(Table table, string titulo) { var panel = new Panel(new Align(table, HorizontalAlignment.Center, VerticalAlignment.Top)) .Header(titulo, Justify.Center) .Border(BoxBorder.Double) .Expand(); AnsiConsole.Write(new Align(panel, HorizontalAlignment.Center, VerticalAlignment.Top)); } static List ObterRegistrosUsuario(BigInteger userId, string nome) { var userRecords = new List(); AnsiConsole.Status() .Spinner(Spinner.Known.Line) .Start($"[blue]Obtendo registros de [yellow]{nome}[/]...[/]", ctx => { string query = @$" SELECT * FROM public.""ActionsHistory"" WHERE ""UserID"" = @pipeUser AND ""MovedAt"" >= '{firstDayOfMonthStr}' AND ""MovedAt"" <= '{lastDayOfMonthStr}'"; try { using var conn = new NpgsqlConnection(_connectionString); conn.Open(); using var cmd = new NpgsqlCommand(query, conn); cmd.Parameters.AddWithValue("@pipeUser", userId); using var reader = cmd.ExecuteReader(); while (reader.Read()) { var temp = new Record { Id = ToIntOrNull(reader, "Id"), Action = ToStringOrNull(reader, "Action"), User = _pipeUsers.Find(u => u.UserId == ToIntOrNull(reader, "UserID")).Nome, FieldID = ToStringOrNull(reader, "FieldID"), From = ToIntOrNull(reader, "From"), To = ToIntOrNull(reader, "To"), CardID = ToIntOrNull(reader, "CardID"), PipeSUID = ToStringOrNull(reader, "PipeSUID"), MovedAt = (DateTime)(reader["MovedAt"] ?? DateTime.MinValue), Title = ToStringOrNull(reader, "Title") ?? "-", Acao = "Desconhecida" }; temp.AcaoID = temp.From + "-" + temp.To; temp.Acao = _actionIds.Find(p => p.actionId == temp.AcaoID).actionName; userRecords.Add(temp); } } catch (Exception ex) { AnsiConsole.MarkupLine($"\n[red]Erro ao consultar {nome}:[/] {ex.Message}"); } }); return userRecords; } static (int count, int countA, int countR, int countC) ContarRegistros(List userRecords) { DateTime dateCheckStart = date; DateTime dateCheckEnd = date.AddDays(1).AddSeconds(-1); if (dateCheckStart != dateToday) { dateCheckStart = new DateTime(date.Year, date.Month, 1); dateCheckEnd = dateCheckStart.AddMonths(1).AddSeconds(-1); } int count = userRecords.Count(x => x.MovedAt > dateCheckStart && x.MovedAt <= dateCheckEnd && x.FieldID is not null); int countR = userRecords.Count(x => (x.Acao ?? "").StartsWith("Realizou")); int countA = userRecords.Count(x => (x.Acao ?? "").StartsWith("Agendou")); int countC = userRecords.Count(x => (x.Acao ?? "").EndsWith("cancelada")); return (count, countA, countR, countC); } static string ObterFormatoCor(int count, int days) { days = days == 0 ? 1 : days; // Evita divisão por zero string? format = count switch { int n when n < 20 && date == dateToday => "[red]", int n when n >= 20 && n < 40 && date == dateToday => "[yellow]", int n when n >= 40 && n < 60 && date == dateToday => "[blue]", int n when n >= 60 && date == dateToday => "[white on green bold]", int n when n / days < 20 && date != dateToday => "[red]Média em " + days + " dias: " + n / days + " | Total: ", int n when n / days >= 20 && n / days < 40 && date != dateToday => "[yellow]Média em " + days + " dias: " + n / days + " | Total: ", int n when n / days >= 40 && n / days < 60 && date != dateToday => "[blue]Média em " + days + " dias: " + n / days + " | Total: ", int n when n / days >= 60 && date != dateToday => "[white on green bold]Média em " + days + " dias: " + n / days + " | Total: ", _ => "[white]", }; return format; } static void AguardarEntrada() { // Tempo de espera em segundos (ex.: 10 minutos = 600s) int intervalSeconds = 600; _endTime = DateTime.Now.AddSeconds(intervalSeconds); // Escreve uma linha em branco para “reservar” o lugar do contador AnsiConsole.WriteLine(""); // Começa o loop que vai atualizar a mesma linha a cada segundo while (!_stop) { var remaining = _endTime - DateTime.Now; if (remaining.TotalMilliseconds <= 0) { // Quando chegar a zero, exibe a mensagem de atualização automática AnsiConsole.MarkupLine("\r[red]Atualização automática agora![/]"); break; } // Monta o texto de “Próxima atualização em MM:SS” AnsiConsole.Markup($"\r[blue]Próxima atualização em {remaining.Minutes:00}:{remaining.Seconds:00}[/]"); // Se o usuário apertar qualquer tecla… if (Console.KeyAvailable) { var key = Console.ReadKey(true); // …e não for ENTER, sinalizamos para parar o loop e sair if (key.Key != ConsoleKey.Enter && key.KeyChar != '/') _stop = true; else if (key.KeyChar == '/') { AnsiConsole.MarkupLine("\n[gray]Modo de seleção de mês ativado. Digite o mês no formato [green]AAMM[/] ou [green]ENTER[/] para voltar ao mês atual.[/]"); Console.Write("Mês (AAMM): "); var input = Console.ReadLine(); SetDateEval(input ?? ""); } break; } } // Garante que, ao sair do loop, o cursor fique embaixo do texto Console.WriteLine(""); } static void Autenticacao() { AnsiConsole.Status() .Spinner(Spinner.Known.Dots) .Start("[green]Autenticando usuário...[/]", ctx => { const string query = @" SELECT ""UserID"", ""Nome"" FROM public.""usuarios"" WHERE ""windowsID"" = @WindowsID"; try { using var conn = new NpgsqlConnection(_connectionString); conn.Open(); using var cmd = new NpgsqlCommand(query, conn); cmd.Parameters.AddWithValue("@WindowsID", _windowsID); using var reader = cmd.ExecuteReader(); while (reader.Read()) { var userId = BigInteger.Parse(reader["UserID"].ToString()!); var nome = reader["Nome"].ToString()!; _pipeUsers.Add((userId, nome)); } if (_pipeUsers.Count > 0) { _isAuthenticated = true; } else { AnsiConsole.MarkupLine("[bold red]Nenhum usuário encontrado para este WindowsID.[/]"); _isAuthenticated = false; } } catch (Exception ex) { AnsiConsole.MarkupLine($"\n[red]Erro na autenticação:[/] {ex.Message}"); _isAuthenticated = false; } }); } public class Record { public int? Id { get; set; } public string? Action { get; set; } public string? User { get; set; } public string? FieldID { get; set; } public int? From { get; set; } public int? To { get; set; } public int? CardID { get; set; } public string? PipeSUID { get; set; } public DateTime MovedAt { get; set; } public required string Title { get; set; } public string? AcaoID { get; set; } public required string Acao { get; set; } } #region Helpers static int? ToIntOrNull(DbDataReader r, string col) { return r.IsDBNull(r.GetOrdinal(col)) ? null : Convert.ToInt32(r[col]); ; } static string? ToStringOrNull(DbDataReader r, string col) { return r.IsDBNull(r.GetOrdinal(col)) ? null : r.GetString(r.GetOrdinal(col)); } #endregion }