diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..20b58359c --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Backend +backend/src/*/bin/ +backend/src/*/obj/ +backend/tests/*/bin/ +backend/tests/*/obj/ +*.db +*.db-shm +*.db-wal + +# Frontend +frontend/*/node_modules/ +frontend/*/dist/ +frontend/*/.env.local +frontend/*/.env.*.local + +# IDEs +.vs/ +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index c7adc0808..c7f94f158 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,273 @@ # Taskdeck -Personal Kanban and to-do system for developers โ€“ keyboard-friendly, offline-first, and extensible. + +**Taskdeck** is a personal Kanban and to-do manager designed for developers, featuring a keyboard-friendly interface, offline-first architecture, and clean design principles. + +## ๐ŸŽฏ Features + +- **Kanban Boards**: Visual management with boards โ†’ columns โ†’ cards +- **WIP Limits**: Enforce work-in-progress limits per column to maintain focus +- **Labels & Due Dates**: Organize cards with color-coded labels and track deadlines +- **Blocked Cards**: Mark cards as blocked with reasons to track impediments +- **Clean Architecture**: Backend built with Domain-Driven Design principles +- **Modern Stack**: Vue 3 + TypeScript frontend, .NET 8 + EF Core backend +- **Offline-First**: Local SQLite database, no cloud dependency required + +## ๐Ÿ“‹ Tech Stack + +### Backend +- **.NET 8** - Modern C# runtime +- **ASP.NET Core** - Web API framework +- **Entity Framework Core** - ORM with SQLite +- **Clean Architecture** - Domain, Application, Infrastructure, API layers +- **xUnit + FluentAssertions** - Testing framework + +### Frontend +- **Vue 3** - Progressive JavaScript framework +- **Vite** - Fast build tool +- **TypeScript** - Type-safe JavaScript +- **Pinia** - State management +- **Vue Router** - Client-side routing +- **TailwindCSS** - Utility-first CSS framework +- **Axios** - HTTP client + +## ๐Ÿš€ Getting Started + +### Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [Node.js 20+](https://nodejs.org/) and npm + +### Backend Setup + +1. Navigate to the backend directory: +```bash +cd backend +``` + +2. Restore dependencies: +```bash +dotnet restore +``` + +3. Create the database and run migrations: +```bash +dotnet ef database update -p src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj -s src/Taskdeck.Api/Taskdeck.Api.csproj +``` + +4. Run the API: +```bash +dotnet run --project src/Taskdeck.Api/Taskdeck.Api.csproj +``` + +The API will be available at `http://localhost:5000` (or the port specified in your configuration). + +### Frontend Setup + +1. Navigate to the frontend directory: +```bash +cd frontend/taskdeck-web +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Start the development server: +```bash +npm run dev +``` + +The frontend will be available at `http://localhost:5173`. + +## ๐Ÿงช Running Tests + +### Backend Tests + +Run domain and application tests: +```bash +cd backend +dotnet test +``` + +Run tests with coverage: +```bash +dotnet test /p:CollectCoverage=true +``` + +## ๐Ÿ“ Architecture + +Taskdeck follows **Clean Architecture** principles with clear separation of concerns: + +``` +backend/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ Taskdeck.Domain/ # Domain entities and business rules +โ”‚ โ”‚ โ”œโ”€โ”€ Entities/ # Board, Column, Card, Label +โ”‚ โ”‚ โ”œโ”€โ”€ Common/ # Base entity, Result pattern +โ”‚ โ”‚ โ””โ”€โ”€ Exceptions/ # Domain exceptions +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ Taskdeck.Application/ # Use cases and business logic +โ”‚ โ”‚ โ”œโ”€โ”€ Services/ # BoardService, ColumnService, etc. +โ”‚ โ”‚ โ”œโ”€โ”€ DTOs/ # Data transfer objects +โ”‚ โ”‚ โ””โ”€โ”€ Interfaces/ # Repository interfaces +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ Taskdeck.Infrastructure/ # Data access and external concerns +โ”‚ โ”‚ โ”œโ”€โ”€ Persistence/ # EF Core DbContext +โ”‚ โ”‚ โ””โ”€โ”€ Repositories/ # Repository implementations +โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€ Taskdeck.Api/ # REST API layer +โ”‚ โ””โ”€โ”€ Controllers/ # API endpoints +โ”‚ +โ””โ”€โ”€ tests/ + โ”œโ”€โ”€ Taskdeck.Domain.Tests/ + โ””โ”€โ”€ Taskdeck.Application.Tests/ +``` + +``` +frontend/ +โ””โ”€โ”€ taskdeck-web/ + โ””โ”€โ”€ src/ + โ”œโ”€โ”€ api/ # HTTP client and API calls + โ”œโ”€โ”€ components/ # Vue components + โ”œโ”€โ”€ router/ # Vue Router configuration + โ”œโ”€โ”€ store/ # Pinia state management + โ”œโ”€โ”€ types/ # TypeScript type definitions + โ””โ”€โ”€ views/ # Page-level components +``` + +## ๐ŸŽจ Domain Model + +### Core Entities + +**Board** +- Name, description +- Contains columns and cards +- Archive functionality + +**Column** +- Name, position +- Optional WIP limit +- Belongs to a board + +**Card** +- Title, description +- Due date (optional) +- Position within column +- Blocked status with reason +- Multiple labels + +**Label** +- Name, color (hex) +- Board-scoped +- Many-to-many with cards + +### Business Rules + +1. **WIP Limit Enforcement**: Cards cannot be moved to a column that has reached its WIP limit +2. **Position Management**: Cards and columns maintain ordered positions +3. **Validation**: All entities enforce validation rules (e.g., non-empty names, valid hex colors) +4. **Board Integrity**: Cards must belong to exactly one board and one column + +## ๐Ÿ”Œ API Endpoints + +### Boards +- `GET /api/boards` - List all boards +- `GET /api/boards/{id}` - Get board with columns +- `POST /api/boards` - Create a new board +- `PUT /api/boards/{id}` - Update board +- `DELETE /api/boards/{id}` - Archive board + +### Columns +- `GET /api/boards/{boardId}/columns` - List columns for a board +- `POST /api/boards/{boardId}/columns` - Create a column +- `PATCH /api/boards/{boardId}/columns/{columnId}` - Update column +- `DELETE /api/boards/{boardId}/columns/{columnId}` - Delete column + +### Cards +- `GET /api/boards/{boardId}/cards` - List/search cards +- `POST /api/boards/{boardId}/cards` - Create a card +- `PATCH /api/boards/{boardId}/cards/{cardId}` - Update card +- `POST /api/boards/{boardId}/cards/{cardId}/move` - Move card +- `DELETE /api/boards/{boardId}/cards/{cardId}` - Delete card + +### Labels +- `GET /api/boards/{boardId}/labels` - List labels for a board +- `POST /api/boards/{boardId}/labels` - Create a label +- `PATCH /api/boards/{boardId}/labels/{labelId}` - Update label +- `DELETE /api/boards/{boardId}/labels/{labelId}` - Delete label + +API documentation is available via Swagger at `http://localhost:5000/swagger` when running in development mode. + +## ๐Ÿ—‚๏ธ Database + +Taskdeck uses **SQLite** for local, file-based storage. The database file (`taskdeck.db`) is created in the API project directory on first run. + +### Running Migrations + +Create a new migration after model changes: +```bash +dotnet ef migrations add MigrationName -p src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj -s src/Taskdeck.Api/Taskdeck.Api.csproj +``` + +Apply migrations: +```bash +dotnet ef database update -p src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj -s src/Taskdeck.Api/Taskdeck.Api.csproj +``` + +## ๐Ÿ› ๏ธ Development + +### Code Style + +- **Backend**: Follow standard C# conventions, use `PascalCase` for public members, `camelCase` for private fields +- **Frontend**: Use TypeScript strict mode, follow Vue 3 Composition API patterns + +### Key Design Patterns + +- **Repository Pattern**: Abstracts data access +- **Unit of Work**: Manages transactions +- **Result Pattern**: Type-safe error handling +- **Service Layer**: Encapsulates business logic +- **DTO Pattern**: Separates API contracts from domain models + +## ๐Ÿ“ˆ Roadmap + +### Phase 1 (Complete) +- โœ… Core domain model +- โœ… CRUD operations for all entities +- โœ… WIP limit enforcement +- โœ… Basic Vue 3 frontend +- โœ… API integration + +### Phase 2 (Planned) +- [ ] Drag-and-drop for cards and columns +- [ ] Card modal for detailed editing +- [ ] Time tracking per card +- [ ] Keyboard shortcuts +- [ ] Search and filtering UI + +### Phase 3 (Future) +- [ ] CLI client +- [ ] Recurring tasks +- [ ] Analytics dashboard +- [ ] Dark mode +- [ ] Multi-user support (optional) +- [ ] Sync to remote server (optional) + +## ๐Ÿค Contributing + +This is primarily a personal learning project, but feedback and suggestions are welcome! + +## ๐Ÿ“„ License + +MIT License - feel free to use this project as a reference or starting point for your own Kanban tool. + +## ๐Ÿ™ Acknowledgments + +- Inspired by Trello, Jira, and other Kanban tools +- Built following Clean Architecture principles by Robert C. Martin +- Uses modern best practices for .NET and Vue.js development + +--- + +**Happy task tracking!** ๐ŸŽฏ diff --git a/backend/Taskdeck.sln b/backend/Taskdeck.sln new file mode 100644 index 000000000..7f1410f1f --- /dev/null +++ b/backend/Taskdeck.sln @@ -0,0 +1,61 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9B8F9CAD-2E3F-5G4B-AC6D-7E8F9GA1C2D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Domain", "src\Taskdeck.Domain\Taskdeck.Domain.csproj", "{A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Application", "src\Taskdeck.Application\Taskdeck.Application.csproj", "{B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Infrastructure", "src\Taskdeck.Infrastructure\Taskdeck.Infrastructure.csproj", "{C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Api", "src\Taskdeck.Api\Taskdeck.Api.csproj", "{D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Domain.Tests", "tests\Taskdeck.Domain.Tests\Taskdeck.Domain.Tests.csproj", "{E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskdeck.Application.Tests", "tests\Taskdeck.Application.Tests\Taskdeck.Application.Tests.csproj", "{F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B}.Release|Any CPU.Build.0 = Release|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {B2C3D4E5-F6A7-5B6C-9D0E-1F2A3B4C5D6E} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {C3D4E5F6-A7B8-6C7D-0E1F-2A3B4C5D6E7F} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {D4E5F6A7-B8C9-7D8E-1F2A-3B4C5D6E7F8A} = {8A7E8B9C-1D2E-4F3A-9B5C-6D7E8F9A0B1C} + {E5F6A7B8-C9D0-8E9F-2A3B-4C5D6E7F8A9B} = {9B8F9CAD-2E3F-5G4B-AC6D-7E8F9GA1C2D} + {F6A7B8C9-D0E1-9FA0-3B4C-5D6E7F8A9B0C} = {9B8F9CAD-2E3F-5G4B-AC6D-7E8F9GA1C2D} + EndGlobalSection +EndGlobal diff --git a/backend/src/Taskdeck.Api/Controllers/BoardsController.cs b/backend/src/Taskdeck.Api/Controllers/BoardsController.cs new file mode 100644 index 000000000..6b830b5da --- /dev/null +++ b/backend/src/Taskdeck.Api/Controllers/BoardsController.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Mvc; +using Taskdeck.Application.DTOs; +using Taskdeck.Application.Services; + +namespace Taskdeck.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BoardsController : ControllerBase +{ + private readonly BoardService _boardService; + + public BoardsController(BoardService boardService) + { + _boardService = boardService; + } + + [HttpGet] + public async Task GetBoards([FromQuery] string? search, [FromQuery] bool includeArchived = false) + { + var result = await _boardService.ListBoardsAsync(search, includeArchived); + return result.IsSuccess ? Ok(result.Value) : Problem(result.ErrorMessage, statusCode: 500); + } + + [HttpGet("{id}")] + public async Task GetBoard(Guid id) + { + var result = await _boardService.GetBoardDetailAsync(id); + + if (!result.IsSuccess) + { + return result.ErrorCode == "NotFound" + ? NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }) + : Problem(result.ErrorMessage, statusCode: 500); + } + + return Ok(result.Value); + } + + [HttpPost] + public async Task CreateBoard([FromBody] CreateBoardDto dto) + { + var result = await _boardService.CreateBoardAsync(dto); + + if (!result.IsSuccess) + { + return result.ErrorCode == "ValidationError" + ? BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }) + : Problem(result.ErrorMessage, statusCode: 500); + } + + return CreatedAtAction(nameof(GetBoard), new { id = result.Value.Id }, result.Value); + } + + [HttpPut("{id}")] + public async Task UpdateBoard(Guid id, [FromBody] UpdateBoardDto dto) + { + var result = await _boardService.UpdateBoardAsync(id, dto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return Ok(result.Value); + } + + [HttpDelete("{id}")] + public async Task DeleteBoard(Guid id) + { + var result = await _boardService.DeleteBoardAsync(id); + + if (!result.IsSuccess) + { + return result.ErrorCode == "NotFound" + ? NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }) + : Problem(result.ErrorMessage, statusCode: 500); + } + + return NoContent(); + } +} diff --git a/backend/src/Taskdeck.Api/Controllers/CardsController.cs b/backend/src/Taskdeck.Api/Controllers/CardsController.cs new file mode 100644 index 000000000..ff57a94b2 --- /dev/null +++ b/backend/src/Taskdeck.Api/Controllers/CardsController.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Mvc; +using Taskdeck.Application.DTOs; +using Taskdeck.Application.Services; + +namespace Taskdeck.Api.Controllers; + +[ApiController] +[Route("api/boards/{boardId}/cards")] +public class CardsController : ControllerBase +{ + private readonly CardService _cardService; + + public CardsController(CardService cardService) + { + _cardService = cardService; + } + + [HttpGet] + public async Task GetCards( + Guid boardId, + [FromQuery] string? search, + [FromQuery] Guid? labelId, + [FromQuery] Guid? columnId) + { + var result = await _cardService.SearchCardsAsync(boardId, search, labelId, columnId); + return result.IsSuccess ? Ok(result.Value) : Problem(result.ErrorMessage, statusCode: 500); + } + + [HttpPost] + public async Task CreateCard(Guid boardId, [FromBody] CreateCardDto dto) + { + var createDto = dto with { BoardId = boardId }; + var result = await _cardService.CreateCardAsync(createDto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "WipLimitExceeded" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return CreatedAtAction(nameof(GetCards), new { boardId }, result.Value); + } + + [HttpPatch("{cardId}")] + public async Task UpdateCard(Guid boardId, Guid cardId, [FromBody] UpdateCardDto dto) + { + var result = await _cardService.UpdateCardAsync(cardId, dto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return Ok(result.Value); + } + + [HttpPost("{cardId}/move")] + public async Task MoveCard(Guid boardId, Guid cardId, [FromBody] MoveCardDto dto) + { + var result = await _cardService.MoveCardAsync(cardId, dto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "WipLimitExceeded" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return Ok(result.Value); + } + + [HttpDelete("{cardId}")] + public async Task DeleteCard(Guid boardId, Guid cardId) + { + var result = await _cardService.DeleteCardAsync(cardId); + + if (!result.IsSuccess) + { + return result.ErrorCode == "NotFound" + ? NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }) + : Problem(result.ErrorMessage, statusCode: 500); + } + + return NoContent(); + } +} diff --git a/backend/src/Taskdeck.Api/Controllers/ColumnsController.cs b/backend/src/Taskdeck.Api/Controllers/ColumnsController.cs new file mode 100644 index 000000000..9d4d53f72 --- /dev/null +++ b/backend/src/Taskdeck.Api/Controllers/ColumnsController.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Mvc; +using Taskdeck.Application.DTOs; +using Taskdeck.Application.Services; + +namespace Taskdeck.Api.Controllers; + +[ApiController] +[Route("api/boards/{boardId}/columns")] +public class ColumnsController : ControllerBase +{ + private readonly ColumnService _columnService; + + public ColumnsController(ColumnService columnService) + { + _columnService = columnService; + } + + [HttpGet] + public async Task GetColumns(Guid boardId) + { + var result = await _columnService.GetColumnsByBoardIdAsync(boardId); + return result.IsSuccess ? Ok(result.Value) : Problem(result.ErrorMessage, statusCode: 500); + } + + [HttpPost] + public async Task CreateColumn(Guid boardId, [FromBody] CreateColumnDto dto) + { + // Ensure boardId from route matches DTO + var createDto = dto with { BoardId = boardId }; + var result = await _columnService.CreateColumnAsync(createDto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return CreatedAtAction(nameof(GetColumns), new { boardId }, result.Value); + } + + [HttpPatch("{columnId}")] + public async Task UpdateColumn(Guid boardId, Guid columnId, [FromBody] UpdateColumnDto dto) + { + var result = await _columnService.UpdateColumnAsync(columnId, dto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return Ok(result.Value); + } + + [HttpDelete("{columnId}")] + public async Task DeleteColumn(Guid boardId, Guid columnId) + { + var result = await _columnService.DeleteColumnAsync(columnId); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "Conflict" => Conflict(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return NoContent(); + } +} diff --git a/backend/src/Taskdeck.Api/Controllers/LabelsController.cs b/backend/src/Taskdeck.Api/Controllers/LabelsController.cs new file mode 100644 index 000000000..647c3b8e9 --- /dev/null +++ b/backend/src/Taskdeck.Api/Controllers/LabelsController.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Mvc; +using Taskdeck.Application.DTOs; +using Taskdeck.Application.Services; + +namespace Taskdeck.Api.Controllers; + +[ApiController] +[Route("api/boards/{boardId}/labels")] +public class LabelsController : ControllerBase +{ + private readonly LabelService _labelService; + + public LabelsController(LabelService labelService) + { + _labelService = labelService; + } + + [HttpGet] + public async Task GetLabels(Guid boardId) + { + var result = await _labelService.GetLabelsByBoardIdAsync(boardId); + return result.IsSuccess ? Ok(result.Value) : Problem(result.ErrorMessage, statusCode: 500); + } + + [HttpPost] + public async Task CreateLabel(Guid boardId, [FromBody] CreateLabelDto dto) + { + var createDto = dto with { BoardId = boardId }; + var result = await _labelService.CreateLabelAsync(createDto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return CreatedAtAction(nameof(GetLabels), new { boardId }, result.Value); + } + + [HttpPatch("{labelId}")] + public async Task UpdateLabel(Guid boardId, Guid labelId, [FromBody] UpdateLabelDto dto) + { + var result = await _labelService.UpdateLabelAsync(labelId, dto); + + if (!result.IsSuccess) + { + return result.ErrorCode switch + { + "NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + "ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }), + _ => Problem(result.ErrorMessage, statusCode: 500) + }; + } + + return Ok(result.Value); + } + + [HttpDelete("{labelId}")] + public async Task DeleteLabel(Guid boardId, Guid labelId) + { + var result = await _labelService.DeleteLabelAsync(labelId); + + if (!result.IsSuccess) + { + return result.ErrorCode == "NotFound" + ? NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }) + : Problem(result.ErrorMessage, statusCode: 500); + } + + return NoContent(); + } +} diff --git a/backend/src/Taskdeck.Api/Program.cs b/backend/src/Taskdeck.Api/Program.cs new file mode 100644 index 000000000..5af14bdfc --- /dev/null +++ b/backend/src/Taskdeck.Api/Program.cs @@ -0,0 +1,46 @@ +using Taskdeck.Application.Services; +using Taskdeck.Infrastructure; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Add Infrastructure (DbContext, Repositories) +builder.Services.AddInfrastructure(builder.Configuration); + +// Add Application Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Add CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowFrontend", policy => + { + policy.WithOrigins("http://localhost:5173", "http://localhost:5174") + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors("AllowFrontend"); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/backend/src/Taskdeck.Api/Taskdeck.Api.csproj b/backend/src/Taskdeck.Api/Taskdeck.Api.csproj new file mode 100644 index 000000000..2418d6fbd --- /dev/null +++ b/backend/src/Taskdeck.Api/Taskdeck.Api.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/backend/src/Taskdeck.Api/appsettings.Development.json b/backend/src/Taskdeck.Api/appsettings.Development.json new file mode 100644 index 000000000..34f00ef13 --- /dev/null +++ b/backend/src/Taskdeck.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Information" + } + } +} diff --git a/backend/src/Taskdeck.Api/appsettings.json b/backend/src/Taskdeck.Api/appsettings.json new file mode 100644 index 000000000..b8b155848 --- /dev/null +++ b/backend/src/Taskdeck.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Data Source=taskdeck.db" + } +} diff --git a/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.dgspec.json b/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.dgspec.json new file mode 100644 index 000000000..c74e7c9c6 --- /dev/null +++ b/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.dgspec.json @@ -0,0 +1,295 @@ +{ + "format": 1, + "restore": { + "/home/user/Taskdeck/backend/src/Taskdeck.Api/Taskdeck.Api.csproj": {} + }, + "projects": { + "/home/user/Taskdeck/backend/src/Taskdeck.Api/Taskdeck.Api.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/user/Taskdeck/backend/src/Taskdeck.Api/Taskdeck.Api.csproj", + "projectName": "Taskdeck.Api", + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Api/Taskdeck.Api.csproj", + "packagesPath": "/root/.nuget/packages/", + "outputPath": "/home/user/Taskdeck/backend/src/Taskdeck.Api/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/root/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": { + "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj": { + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj" + }, + "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj": { + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj" + } + } + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + } + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "Microsoft.EntityFrameworkCore.Tools": { + "include": "Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive", + "suppressParent": "All", + "target": "Package", + "version": "[8.0.0, )" + }, + "Swashbuckle.AspNetCore": { + "target": "Package", + "version": "[6.5.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "/opt/dotnet/sdk/8.0.416/PortableRuntimeIdentifierGraph.json" + } + } + }, + "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj", + "projectName": "Taskdeck.Application", + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj", + "packagesPath": "/root/.nuget/packages/", + "outputPath": "/home/user/Taskdeck/backend/src/Taskdeck.Application/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/root/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": { + "/home/user/Taskdeck/backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj": { + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj" + } + } + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + } + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "FluentValidation": { + "target": "Package", + "version": "[11.9.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "/opt/dotnet/sdk/8.0.416/PortableRuntimeIdentifierGraph.json" + } + } + }, + "/home/user/Taskdeck/backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/user/Taskdeck/backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj", + "projectName": "Taskdeck.Domain", + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj", + "packagesPath": "/root/.nuget/packages/", + "outputPath": "/home/user/Taskdeck/backend/src/Taskdeck.Domain/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/root/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + } + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "/opt/dotnet/sdk/8.0.416/PortableRuntimeIdentifierGraph.json" + } + } + }, + "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj", + "projectName": "Taskdeck.Infrastructure", + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj", + "packagesPath": "/root/.nuget/packages/", + "outputPath": "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/root/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": { + "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj": { + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj" + }, + "/home/user/Taskdeck/backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj": { + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Domain/Taskdeck.Domain.csproj" + } + } + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + } + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "Microsoft.EntityFrameworkCore": { + "target": "Package", + "version": "[8.0.0, )" + }, + "Microsoft.EntityFrameworkCore.Design": { + "include": "Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive", + "suppressParent": "All", + "target": "Package", + "version": "[8.0.0, )" + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "target": "Package", + "version": "[8.0.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "/opt/dotnet/sdk/8.0.416/PortableRuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.g.props b/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.g.props new file mode 100644 index 000000000..fd5fa0eb4 --- /dev/null +++ b/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.g.props @@ -0,0 +1,15 @@ +๏ปฟ + + + False + NuGet + $(MSBuildThisFileDirectory)project.assets.json + /root/.nuget/packages/ + /root/.nuget/packages/ + PackageReference + 6.11.1 + + + + + \ No newline at end of file diff --git a/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.g.targets b/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.g.targets new file mode 100644 index 000000000..3dc06ef3c --- /dev/null +++ b/backend/src/Taskdeck.Api/obj/Taskdeck.Api.csproj.nuget.g.targets @@ -0,0 +1,2 @@ +๏ปฟ + \ No newline at end of file diff --git a/backend/src/Taskdeck.Api/obj/project.assets.json b/backend/src/Taskdeck.Api/obj/project.assets.json new file mode 100644 index 000000000..18e71a2f2 --- /dev/null +++ b/backend/src/Taskdeck.Api/obj/project.assets.json @@ -0,0 +1,104 @@ +{ + "version": 3, + "targets": { + "net8.0": {} + }, + "libraries": {}, + "projectFileDependencyGroups": { + "net8.0": [ + "Microsoft.EntityFrameworkCore.Tools >= 8.0.0", + "Swashbuckle.AspNetCore >= 6.5.0" + ] + }, + "packageFolders": { + "/root/.nuget/packages/": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/user/Taskdeck/backend/src/Taskdeck.Api/Taskdeck.Api.csproj", + "projectName": "Taskdeck.Api", + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Api/Taskdeck.Api.csproj", + "packagesPath": "/root/.nuget/packages/", + "outputPath": "/home/user/Taskdeck/backend/src/Taskdeck.Api/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/root/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": { + "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj": { + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Application/Taskdeck.Application.csproj" + }, + "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj": { + "projectPath": "/home/user/Taskdeck/backend/src/Taskdeck.Infrastructure/Taskdeck.Infrastructure.csproj" + } + } + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + } + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "Microsoft.EntityFrameworkCore.Tools": { + "include": "Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive", + "suppressParent": "All", + "target": "Package", + "version": "[8.0.0, )" + }, + "Swashbuckle.AspNetCore": { + "target": "Package", + "version": "[6.5.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "/opt/dotnet/sdk/8.0.416/PortableRuntimeIdentifierGraph.json" + } + } + }, + "logs": [ + { + "code": "NU1301", + "level": "Error", + "message": "Unable to load the service index for source https://api.nuget.org/v3/index.json.", + "libraryId": "Microsoft.EntityFrameworkCore.Tools" + } + ] +} \ No newline at end of file diff --git a/backend/src/Taskdeck.Api/obj/project.nuget.cache b/backend/src/Taskdeck.Api/obj/project.nuget.cache new file mode 100644 index 000000000..7fbf9a170 --- /dev/null +++ b/backend/src/Taskdeck.Api/obj/project.nuget.cache @@ -0,0 +1,15 @@ +{ + "version": 2, + "dgSpecHash": "1MtMU8EgpPI=", + "success": false, + "projectFilePath": "/home/user/Taskdeck/backend/src/Taskdeck.Api/Taskdeck.Api.csproj", + "expectedPackageFiles": [], + "logs": [ + { + "code": "NU1301", + "level": "Error", + "message": "Unable to load the service index for source https://api.nuget.org/v3/index.json.", + "libraryId": "Microsoft.EntityFrameworkCore.Tools" + } + ] +} \ No newline at end of file diff --git a/backend/src/Taskdeck.Application/DTOs/BoardDto.cs b/backend/src/Taskdeck.Application/DTOs/BoardDto.cs new file mode 100644 index 000000000..967be7f87 --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/BoardDto.cs @@ -0,0 +1,31 @@ +namespace Taskdeck.Application.DTOs; + +public record BoardDto( + Guid Id, + string Name, + string? Description, + bool IsArchived, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record BoardDetailDto( + Guid Id, + string Name, + string? Description, + bool IsArchived, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt, + List Columns +); + +public record CreateBoardDto( + string Name, + string? Description +); + +public record UpdateBoardDto( + string? Name, + string? Description, + bool? IsArchived +); diff --git a/backend/src/Taskdeck.Application/DTOs/CardDto.cs b/backend/src/Taskdeck.Application/DTOs/CardDto.cs new file mode 100644 index 000000000..ba48ed69a --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/CardDto.cs @@ -0,0 +1,39 @@ +namespace Taskdeck.Application.DTOs; + +public record CardDto( + Guid Id, + Guid BoardId, + Guid ColumnId, + string Title, + string Description, + DateTimeOffset? DueDate, + bool IsBlocked, + string? BlockReason, + int Position, + List Labels, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record CreateCardDto( + Guid BoardId, + Guid ColumnId, + string Title, + string? Description, + DateTimeOffset? DueDate, + List? LabelIds +); + +public record UpdateCardDto( + string? Title, + string? Description, + DateTimeOffset? DueDate, + bool? IsBlocked, + string? BlockReason, + List? LabelIds +); + +public record MoveCardDto( + Guid TargetColumnId, + int TargetPosition +); diff --git a/backend/src/Taskdeck.Application/DTOs/ColumnDto.cs b/backend/src/Taskdeck.Application/DTOs/ColumnDto.cs new file mode 100644 index 000000000..3627e9a82 --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/ColumnDto.cs @@ -0,0 +1,25 @@ +namespace Taskdeck.Application.DTOs; + +public record ColumnDto( + Guid Id, + Guid BoardId, + string Name, + int Position, + int? WipLimit, + int CardCount, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record CreateColumnDto( + Guid BoardId, + string Name, + int? Position, + int? WipLimit +); + +public record UpdateColumnDto( + string? Name, + int? Position, + int? WipLimit +); diff --git a/backend/src/Taskdeck.Application/DTOs/LabelDto.cs b/backend/src/Taskdeck.Application/DTOs/LabelDto.cs new file mode 100644 index 000000000..9d9ef0ddc --- /dev/null +++ b/backend/src/Taskdeck.Application/DTOs/LabelDto.cs @@ -0,0 +1,21 @@ +namespace Taskdeck.Application.DTOs; + +public record LabelDto( + Guid Id, + Guid BoardId, + string Name, + string ColorHex, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt +); + +public record CreateLabelDto( + Guid BoardId, + string Name, + string ColorHex +); + +public record UpdateLabelDto( + string? Name, + string? ColorHex +); diff --git a/backend/src/Taskdeck.Application/Interfaces/IBoardRepository.cs b/backend/src/Taskdeck.Application/Interfaces/IBoardRepository.cs new file mode 100644 index 000000000..136e76f01 --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/IBoardRepository.cs @@ -0,0 +1,9 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface IBoardRepository : IRepository +{ + Task> SearchAsync(string? searchText, bool includeArchived, CancellationToken cancellationToken = default); + Task GetByIdWithDetailsAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs b/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs new file mode 100644 index 000000000..fdfe75a3b --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs @@ -0,0 +1,11 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface ICardRepository : IRepository +{ + Task> GetByBoardIdAsync(Guid boardId, CancellationToken cancellationToken = default); + Task> GetByColumnIdAsync(Guid columnId, CancellationToken cancellationToken = default); + Task> SearchAsync(Guid boardId, string? searchText, Guid? labelId, Guid? columnId, CancellationToken cancellationToken = default); + Task GetByIdWithLabelsAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Taskdeck.Application/Interfaces/IColumnRepository.cs b/backend/src/Taskdeck.Application/Interfaces/IColumnRepository.cs new file mode 100644 index 000000000..3da06ca67 --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/IColumnRepository.cs @@ -0,0 +1,9 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface IColumnRepository : IRepository +{ + Task> GetByBoardIdAsync(Guid boardId, CancellationToken cancellationToken = default); + Task GetByIdWithCardsAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Taskdeck.Application/Interfaces/ILabelRepository.cs b/backend/src/Taskdeck.Application/Interfaces/ILabelRepository.cs new file mode 100644 index 000000000..e8419434b --- /dev/null +++ b/backend/src/Taskdeck.Application/Interfaces/ILabelRepository.cs @@ -0,0 +1,8 @@ +using Taskdeck.Domain.Entities; + +namespace Taskdeck.Application.Interfaces; + +public interface ILabelRepository : IRepository