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.
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) {