From 85492f0f0e392baa1b76825167a635cb798b16d6 Mon Sep 17 00:00:00 2001 From: Jo Al <1-19-22394@aluno.faminas.edu.br> Date: Mon, 27 Apr 2026 14:42:29 -0300 Subject: [PATCH 1/2] refactor: padronizar controllers com DefaultApiConventions --- TaskFlow.Api/ApiConventions.cs | 3 + TaskFlow.Api/Controllers/TarefasController.cs | 87 +++++++++++-- TaskFlow.Api/Program.cs | 123 ++++++++++-------- TaskFlow.Api/Services/TarefaService.cs | 17 +-- TaskFlow.Api/TaskFlow.Api.csproj | 15 ++- taskflow-app/src/app/core/tarefa.service.ts | 2 +- 6 files changed, 163 insertions(+), 84 deletions(-) create mode 100644 TaskFlow.Api/ApiConventions.cs diff --git a/TaskFlow.Api/ApiConventions.cs b/TaskFlow.Api/ApiConventions.cs new file mode 100644 index 0000000..f8c71d8 --- /dev/null +++ b/TaskFlow.Api/ApiConventions.cs @@ -0,0 +1,3 @@ +using Microsoft.AspNetCore.Mvc; + +[assembly: ApiConventionType(typeof(DefaultApiConventions))] diff --git a/TaskFlow.Api/Controllers/TarefasController.cs b/TaskFlow.Api/Controllers/TarefasController.cs index 5856f56..8730c04 100644 --- a/TaskFlow.Api/Controllers/TarefasController.cs +++ b/TaskFlow.Api/Controllers/TarefasController.cs @@ -5,43 +5,102 @@ namespace TaskFlow.Api.Controllers { + /// + /// Gerencia as operações CRUD de tarefas. + /// + /// + /// Os status codes de resposta seguem as + /// definidas no assembly. O único desvio é o PUT, que retorna + /// 204 No Content em vez do corpo atualizado — o cliente deve + /// recarregar o recurso com um GET subsequente se necessário. + /// [Route("api/[controller]")] [ApiController] [Produces("application/json")] public class TarefasController(TarefaService service) : ControllerBase { + /// + /// Retorna todas as tarefas, com filtro opcional por status. + /// + /// + /// Filtra as tarefas pelo status informado. + /// Valores aceitos: Pendente, EmAndamento, Concluida. + /// Quando omitido, retorna todas as tarefas independentemente do status. + /// + /// Lista de tarefas ordenada por data de criação decrescente. [HttpGet] - public async Task>> ListarAsync( + public async Task>> GetAsync( [FromQuery] StatusTarefa? status = null) { - var tarefas = await service.ListarAsync(status); + var tarefas = await service.GetAsync(status); return Ok(tarefas); } + /// + /// Retorna uma tarefa pelo seu identificador único. + /// + /// Identificador da tarefa. + /// Dados completos da tarefa encontrada. [HttpGet("{id}")] - public async Task> BuscarIdAsync(int id) + public async Task> GetByIdAsync(int id) { - var tarefa = await service.BuscarIdAsync(id); + var tarefa = await service.GetByIdAsync(id); if (tarefa is null) return NotFound(new { Message = "Tarefa não encontrada" }); return Ok(tarefa); } + /// + /// Cria uma nova tarefa. + /// + /// + /// O status inicial é sempre Pendente, independentemente do valor + /// enviado no corpo da requisição. As datas de início e conclusão são + /// gerenciadas internamente pela API conforme o status evolui. + /// + /// Dados da tarefa a ser criada. + /// + /// A tarefa criada, com o cabeçalho Location apontando para + /// o endpoint GET /api/tarefas/{id} do recurso recém-criado. + /// [HttpPost] - public async Task> CriarAsync(TarefaRequest request) + public async Task> PostAsync(TarefaRequest request) { - var tarefa = await service.CriarAsync(request); - return StatusCode(201, tarefa); // 201 - created. + var tarefa = await service.PostAsync(request); + return CreatedAtAction("GetById", new { id = tarefa.Id }, tarefa); } + /// + /// Atualiza os dados de uma tarefa existente. + /// + /// + /// Atualiza título, descrição e status da tarefa. A transição de status + /// gerencia automaticamente as datas de ciclo de vida: + /// + /// + /// Pendente — zera DataInicio e DataConclusao. + /// + /// + /// EmAndamento — define DataInicio (somente na primeira + /// transição para este status; não sobrescreve se já definida). + /// + /// + /// Concluida — define DataConclusao com o momento atual. + /// + /// + /// Espaços no início e fim de Titulo e Descricao são removidos + /// automaticamente antes de persistir. + /// + /// Identificador da tarefa a ser atualizada. + /// Novos dados da tarefa. [HttpPut("{id}")] - public async Task> AtualizarAsync(int id, TarefaRequest request) + public async Task PutAsync(int id, TarefaRequest request) { try { - var tarefa = await service.AtualizarAsync(id, request); - return Ok(tarefa); + await service.PutAsync(id, request); + return NoContent(); } catch (KeyNotFoundException) { @@ -49,12 +108,16 @@ public async Task> AtualizarAsync(int id, TarefaReq } } + /// + /// Remove uma tarefa permanentemente. + /// + /// Identificador da tarefa a ser removida. [HttpDelete("{id}")] - public async Task DeletarAsync(int id) + public async Task DeleteAsync(int id) { try { - await service.DeletarAsync(id); + await service.DeleteAsync(id); return NoContent(); } catch (KeyNotFoundException) diff --git a/TaskFlow.Api/Program.cs b/TaskFlow.Api/Program.cs index d3fe025..aa83fbd 100644 --- a/TaskFlow.Api/Program.cs +++ b/TaskFlow.Api/Program.cs @@ -1,54 +1,69 @@ -using Microsoft.EntityFrameworkCore; -using TaskFlow.Api.Data; -using TaskFlow.Api.Services; - -var builder = WebApplication.CreateBuilder(args); - -// Bugfix para serialização de enums como strings (em vez de números) na API | resolve envio de form data do Angular -builder.Services.AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); - }); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -builder.Services.AddScoped(); - -/* DbContext -A chave "DefaultConnection" é lida do User Secrets (ou de appsettings.json em produção). - Para configurá-la localmente via User Secrets, execute no terminal (dentro de TaskFlow.Api/): - > dotnet user-secrets init - > dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=...;Database=...;User Id=...;Password=...;TrustServerCertificate=true" - Se quiser usar um nome diferente de "DefaultConnection", altere tanto aqui quanto na chave do User Secret. -*/ -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); -builder.Services.AddDbContext(options => - options.UseSqlServer(connectionString)); - -// CORS - Angular -builder.Services.AddCors(options => -{ - options.AddPolicy("Angular", policy => - { - policy.WithOrigins("http://localhost:4200") - .AllowAnyHeader() - .AllowAnyMethod(); - }); -}); - - -var app = builder.Build(); - -app.UseSwagger(); -app.UseSwaggerUI(); - -app.UseCors("Angular"); - -app.UseHttpsRedirection(); - -app.MapControllers(); - -app.Run(); - +using Microsoft.EntityFrameworkCore; +using TaskFlow.Api.Data; +using TaskFlow.Api.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + }); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "TaskFlow API", + Version = "v1", + Description = "API REST para gerenciamento de tarefas com suporte a status e filtros." + }); + + var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath); +}); + +builder.Services.AddScoped(); + +/* DbContext +A chave "DefaultConnection" é lida do User Secrets (ou de appsettings.json em produção). +*/ +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +if (string.IsNullOrWhiteSpace(connectionString)) +{ + throw new InvalidOperationException( + "Connection string 'ConnectionStrings:DefaultConnection' não configurada. Configure o banco de dados via Docker e User Secrets conforme o README."); +} + +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); + +builder.Services.AddCors(options => +{ + options.AddPolicy("Angular", policy => + { + policy.WithOrigins("http://localhost:4200") + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + + +var app = builder.Build(); + +app.UseSwagger(); +app.UseSwaggerUI(options => +{ + options.SwaggerEndpoint("/swagger/v1/swagger.json", "TaskFlow API v1"); + options.DocumentTitle = "TaskFlow API"; +}); + +app.UseCors("Angular"); + +app.UseHttpsRedirection(); + +app.MapControllers(); + +app.Run(); diff --git a/TaskFlow.Api/Services/TarefaService.cs b/TaskFlow.Api/Services/TarefaService.cs index 6b1f879..5f1f0bf 100644 --- a/TaskFlow.Api/Services/TarefaService.cs +++ b/TaskFlow.Api/Services/TarefaService.cs @@ -7,7 +7,7 @@ namespace TaskFlow.Api.Services; public class TarefaService(AppDbContext db) { - public async Task> ListarAsync(StatusTarefa? status = null) + public async Task> GetAsync(StatusTarefa? status = null) { var query = db.Tarefas.AsQueryable(); @@ -20,13 +20,13 @@ public async Task> ListarAsync(StatusTarefa? status = null) return tarefas.Select(t => t.ToResponse()).ToList(); } - public async Task BuscarIdAsync(int id) + public async Task GetByIdAsync(int id) { var tarefa = await db.Tarefas.FindAsync(id); return tarefa?.ToResponse(); } - public async Task CriarAsync(TarefaRequest request) + public async Task PostAsync(TarefaRequest request) { var tarefa = request.ToEntity(); @@ -36,28 +36,25 @@ public async Task CriarAsync(TarefaRequest request) return tarefa.ToResponse(); } - public async Task AtualizarAsync(int id, TarefaRequest request) + public async Task PutAsync(int id, TarefaRequest request) { var tarefa = await db.Tarefas.FindAsync(id); if (tarefa == null) throw new KeyNotFoundException($"Tarefa {id} não encontrada"); - tarefa.Titulo = request.Titulo.Trim(); // Remove espaços acidentais + tarefa.Titulo = request.Titulo.Trim(); tarefa.Descricao = request.Descricao.Trim(); - // Bugfix: Atualiza as datas com base no novo status, somente se o status tiver sido modificado if (tarefa.Status != request.Status) { tarefa.Status = request.Status; - // data de início if (tarefa.Status == StatusTarefa.Pendente) tarefa.DataInicio = null; - else if (tarefa.DataInicio == null) // Somente definir a data de início se ainda não tiver sido definida + else if (tarefa.DataInicio == null) tarefa.DataInicio = DateTime.UtcNow; - // data de conclusão if (tarefa.Status == StatusTarefa.Concluida) tarefa.DataConclusao = DateTime.UtcNow; else @@ -67,7 +64,7 @@ public async Task AtualizarAsync(int id, TarefaRequest request) return tarefa.ToResponse(); } - public async Task DeletarAsync(int id) + public async Task DeleteAsync(int id) { var tarefa = await db.Tarefas.FindAsync(id); if (tarefa == null) diff --git a/TaskFlow.Api/TaskFlow.Api.csproj b/TaskFlow.Api/TaskFlow.Api.csproj index c98b3d9..7f48615 100644 --- a/TaskFlow.Api/TaskFlow.Api.csproj +++ b/TaskFlow.Api/TaskFlow.Api.csproj @@ -5,6 +5,8 @@ enable enable 2a3ab8af-df6d-40fc-8535-c46a7f517ebe + true + 1591 @@ -23,12 +25,11 @@ - - - - - + + + + + - + - diff --git a/taskflow-app/src/app/core/tarefa.service.ts b/taskflow-app/src/app/core/tarefa.service.ts index d232b48..fc88adc 100644 --- a/taskflow-app/src/app/core/tarefa.service.ts +++ b/taskflow-app/src/app/core/tarefa.service.ts @@ -27,7 +27,7 @@ export class TarefaService { } atualizar(id: number, request: TarefaRequest) { - return this.http.put(`${this.api}/${id}`, request); + return this.http.put(`${this.api}/${id}`, request); } excluir(id: number) { From b2d64646b712a484f5074c5e35cd602985bbfdd0 Mon Sep 17 00:00:00 2001 From: Jo Al <1-19-22394@aluno.faminas.edu.br> Date: Mon, 27 Apr 2026 14:42:35 -0300 Subject: [PATCH 2/2] docs: detalhar futuras melhorias do backend no roadmap --- README.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 871f3ad..6042f75 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,7 @@ Aplicação full stack para gerenciamento de tarefas com Angular 17 no frontend - [Executar frontend](#executar-frontend) - [Backend (ASP.NET Core)](#backend-aspnet-core) - [Stack do backend](#stack-do-backend) - - [Subir SQL Server no Docker](#subir-sql-server-no-docker) - - [Configurar connection string (User Secrets)](#configurar-connection-string-user-secrets) - - [Aplicar migrations](#aplicar-migrations) + - [Setup local do Backend](#setup-local-do-backend) - [Executar backend](#executar-backend) - [Execução local com API](#execução-local-com-api) - [Endpoints da API](#endpoints-da-api) @@ -101,10 +99,14 @@ Aplicação local: http://localhost:4200/tarefas - SQL Server - Swagger -### Subir SQL Server no Docker +### Setup local do Backend + +Siga os passos abaixo para configurar o banco de dados e as credenciais localmente. + +1) Subir SQL Server no Docker ```bash -docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=SuaSenhaAqui@123" \ +docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=SuaSenhaAqui@123" \ -p 1433:1433 --name sqlserver \ -d mcr.microsoft.com/mssql/server:2022-latest ``` @@ -115,7 +117,8 @@ Verifique se o container está ativo: docker ps ``` -### Configurar connection string (User Secrets) +2) Configurar connection string (User Secrets) + A aplicação lê a string de conexão de `ConnectionStrings:DefaultConnection`. Para configurá-la localmente com segurança, utilize **User Secrets**. ```bash @@ -127,12 +130,13 @@ dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost, > [!NOTE] > TrustServerCertificate=true é necessário em ambiente local com SQL Server no Docker. -### Aplicar migrations +3) Aplicar migrations + +Caso seja a primeira vez configurando ou tenha modificado os modelos, crie a migração inicial antes de aplicá-la ao banco: ```bash cd TaskFlow.Api -dotnet tool install --global dotnet-ef -dotnet ef migrations add Migration +dotnet ef migrations add InitialCreate dotnet ef database update ``` @@ -197,7 +201,7 @@ Exemplo de payload (POST/PUT): > [!TIP] > Se a lista de tarefas não carregar, confirme se o modo correto está ativo (mock ou api) e se a URL da API está acessível. -- Erro de conexão com SQL Server: valide container, senha do usuário sa e connection string. +- Erro de conexão com SQL Server (Login failed for user 'sa'): Verifique se a senha no comando `docker run` e no `dotnet user-secrets set` são idênticas. Se necessário, remova o container (`docker rm -f sqlserver`) e refaça o passo 1 e 2 do setup. - Erro de CORS: confirme origem http://localhost:4200 no backend. - Erro de certificado local: use o endpoint HTTP em desenvolvimento (http://localhost:5055). @@ -214,6 +218,8 @@ Embora o projeto atenda a todos os requisitos do desafio, algumas evoluções ar ### Backend - **Implementação de Soft Delete:** Substituir a exclusão física (DELETE no banco) por exclusão lógica (adicionando campos como `IsDeleted` ou `DataExclusao`). Isso preserva o histórico de dados e previne perdas acidentais. - **Tratamento Global de Exceções:** Extrair os blocos de `try-catch` das *Controllers* e implementar um *Global Exception Handler* via Middleware. Isso centraliza o tratamento de erros, padroniza as respostas de falha da API (utilizando o padrão `ProblemDetails`) e deixa os *Controllers* muito mais limpos e focados apenas no roteamento. +- **Refatoração para Princípios SOLID (DIP e ISP):** Introduzir interfaces para os serviços e repositórios, eliminando a dependência direta de classes concretas e do `DbContext`. Isso melhora o desacoplamento e facilita a escrita de testes unitários. +- **Abstração da Camada de Dados (Repository Pattern):** Isolar o acesso ao banco de dados em repositórios específicos, permitindo que os serviços foquem apenas nas regras de negócio e simplificando a troca ou evolução do provedor de persistência. ### Expansão e Ecossistema - **Desenvolvimento de Plugin Nativo para o Obsidian:** Evoluir a solução front-end para atuar como um plugin do Obsidian. O objetivo é preencher uma lacuna atual na comunidade, oferecendo um quadro Kanban capaz de persistir, centralizar e sincronizar estados diretamente em uma API externa (cloud), permitindo a integração do fluxo de trabalho com sistemas de terceiros.