525 lines
14 KiB
Go
525 lines
14 KiB
Go
package kanban
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"tm/pkg/logger"
|
|
)
|
|
|
|
// KanbanService interface defines kanban business logic operations
|
|
type Service interface {
|
|
// Board operations
|
|
CreateBoard(ctx context.Context, req CreateBoardRequest) (*BoardResponse, error)
|
|
GetBoardByID(ctx context.Context, id string) (*BoardDetailResponse, error)
|
|
GetBoardsByUserID(ctx context.Context, userID string) (*BoardsListResponse, error)
|
|
GetDefaultBoard(ctx context.Context, userID string) (*BoardDetailResponse, error)
|
|
UpdateBoard(ctx context.Context, req UpdateBoardRequest) (*BoardResponse, error)
|
|
DeleteBoard(ctx context.Context, id string) error
|
|
|
|
// Column operations
|
|
CreateColumn(ctx context.Context, req CreateColumnRequest) (*ColumnResponse, error)
|
|
UpdateColumn(ctx context.Context, req UpdateColumnRequest) (*ColumnResponse, error)
|
|
DeleteColumn(ctx context.Context, id string) error
|
|
ReorderColumns(ctx context.Context, req ReorderColumnsRequest) error
|
|
|
|
// Card operations
|
|
CreateCard(ctx context.Context, req CreateCardRequest) (*CardResponse, error)
|
|
GetCardByID(ctx context.Context, id string) (*CardResponse, error)
|
|
UpdateCard(ctx context.Context, req UpdateCardRequest) (*CardResponse, error)
|
|
DeleteCard(ctx context.Context, id string) error
|
|
MoveCard(ctx context.Context, req MoveCardRequest) error
|
|
ReorderCards(ctx context.Context, req ReorderCardsRequest) error
|
|
}
|
|
|
|
// kanbanService implements KanbanService interface
|
|
type kanbanService struct {
|
|
boardRepository BoardRepository
|
|
columnRepository ColumnRepository
|
|
cardRepository CardRepository
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewService creates a new kanban service
|
|
func NewService(boardRepo BoardRepository, columnRepo ColumnRepository, cardRepo CardRepository, logger logger.Logger) Service {
|
|
return &kanbanService{
|
|
boardRepository: boardRepo,
|
|
columnRepository: columnRepo,
|
|
cardRepository: cardRepo,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// CreateBoard creates a new kanban board
|
|
func (s *kanbanService) CreateBoard(ctx context.Context, req CreateBoardRequest) (*BoardResponse, error) {
|
|
s.logger.Info("Creating new board", map[string]interface{}{
|
|
"name": req.Name,
|
|
"user_id": req.UserID,
|
|
"is_default": req.IsDefault,
|
|
})
|
|
|
|
// If this is set as default, unset any existing default board for this user
|
|
if req.IsDefault {
|
|
if err := s.unsetDefaultBoard(ctx, req.UserID); err != nil {
|
|
s.logger.Warn("Failed to unset existing default board", map[string]interface{}{
|
|
"user_id": req.UserID,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
board := &Board{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
IsDefault: req.IsDefault,
|
|
UserID: req.UserID,
|
|
Columns: []Column{}, // Start with empty columns
|
|
}
|
|
|
|
if err := s.boardRepository.CreateBoard(ctx, board); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return board.ToResponse(), nil
|
|
}
|
|
|
|
// GetBoardByID retrieves a board with full column and card details
|
|
func (s *kanbanService) GetBoardByID(ctx context.Context, id string) (*BoardDetailResponse, error) {
|
|
s.logger.Info("Retrieving board by ID", map[string]interface{}{
|
|
"board_id": id,
|
|
})
|
|
|
|
board, err := s.boardRepository.GetBoardByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
boardResponse := board.ToResponse()
|
|
|
|
// Load columns with cards
|
|
columns, err := s.columnRepository.GetColumnsByBoardID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, column := range columns {
|
|
columnResponse := column.ToResponse()
|
|
|
|
// Load cards for this column
|
|
cards, err := s.cardRepository.GetCardsByColumnID(ctx, column.ID.Hex())
|
|
if err != nil {
|
|
s.logger.Warn("Failed to load cards for column", map[string]interface{}{
|
|
"column_id": column.ID.Hex(),
|
|
"error": err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
columnResponse.CardCount = len(cards)
|
|
if column.Limit != nil && len(cards) > *column.Limit {
|
|
columnResponse.IsOverLimit = true
|
|
}
|
|
|
|
for _, card := range cards {
|
|
columnResponse.Cards = append(columnResponse.Cards, *card.ToResponse())
|
|
}
|
|
|
|
boardResponse.Columns = append(boardResponse.Columns, *columnResponse)
|
|
}
|
|
|
|
return &BoardDetailResponse{
|
|
Board: *boardResponse,
|
|
}, nil
|
|
}
|
|
|
|
// GetBoardsByUserID retrieves all boards for a user
|
|
func (s *kanbanService) GetBoardsByUserID(ctx context.Context, userID string) (*BoardsListResponse, error) {
|
|
s.logger.Info("Retrieving boards by user ID", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
boards, err := s.boardRepository.GetBoardsByUserID(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var boardResponses []BoardResponse
|
|
for _, board := range boards {
|
|
boardResponses = append(boardResponses, *board.ToResponse())
|
|
}
|
|
|
|
return &BoardsListResponse{
|
|
Boards: boardResponses,
|
|
}, nil
|
|
}
|
|
|
|
// GetDefaultBoard retrieves the default board for a user
|
|
func (s *kanbanService) GetDefaultBoard(ctx context.Context, userID string) (*BoardDetailResponse, error) {
|
|
s.logger.Info("Retrieving default board", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
board, err := s.boardRepository.GetDefaultBoard(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.GetBoardByID(ctx, board.ID.Hex())
|
|
}
|
|
|
|
// UpdateBoard updates an existing board
|
|
func (s *kanbanService) UpdateBoard(ctx context.Context, req UpdateBoardRequest) (*BoardResponse, error) {
|
|
s.logger.Info("Updating board", map[string]interface{}{
|
|
"board_id": req.ID,
|
|
})
|
|
|
|
board, err := s.boardRepository.GetBoardByID(ctx, req.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update fields
|
|
if req.Name != nil {
|
|
board.Name = *req.Name
|
|
}
|
|
if req.Description != nil {
|
|
board.Description = *req.Description
|
|
}
|
|
if req.IsDefault != nil {
|
|
// If setting as default, unset other default boards
|
|
if *req.IsDefault && !board.IsDefault {
|
|
if err := s.unsetDefaultBoard(ctx, board.UserID); err != nil {
|
|
s.logger.Warn("Failed to unset existing default board", map[string]interface{}{
|
|
"user_id": board.UserID,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
board.IsDefault = *req.IsDefault
|
|
}
|
|
|
|
if err := s.boardRepository.UpdateBoard(ctx, board); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return board.ToResponse(), nil
|
|
}
|
|
|
|
// DeleteBoard deletes a board and all its columns and cards
|
|
func (s *kanbanService) DeleteBoard(ctx context.Context, id string) error {
|
|
s.logger.Info("Deleting board", map[string]interface{}{
|
|
"board_id": id,
|
|
})
|
|
|
|
// Get all columns for this board
|
|
columns, err := s.columnRepository.GetColumnsByBoardID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete all cards in each column
|
|
for _, column := range columns {
|
|
cards, err := s.cardRepository.GetCardsByColumnID(ctx, column.ID.Hex())
|
|
if err != nil {
|
|
s.logger.Warn("Failed to get cards for column during board deletion", map[string]interface{}{
|
|
"column_id": column.ID.Hex(),
|
|
"error": err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
for _, card := range cards {
|
|
if err := s.cardRepository.DeleteCard(ctx, card.ID.Hex()); err != nil {
|
|
s.logger.Warn("Failed to delete card during board deletion", map[string]interface{}{
|
|
"card_id": card.ID.Hex(),
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Delete the column
|
|
if err := s.columnRepository.DeleteColumn(ctx, column.ID.Hex()); err != nil {
|
|
s.logger.Warn("Failed to delete column during board deletion", map[string]interface{}{
|
|
"column_id": column.ID.Hex(),
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Finally delete the board
|
|
return s.boardRepository.DeleteBoard(ctx, id)
|
|
}
|
|
|
|
// CreateColumn creates a new column in a board
|
|
func (s *kanbanService) CreateColumn(ctx context.Context, req CreateColumnRequest) (*ColumnResponse, error) {
|
|
s.logger.Info("Creating new column", map[string]interface{}{
|
|
"name": req.Name,
|
|
"board_id": req.BoardID,
|
|
"order": req.Order,
|
|
})
|
|
|
|
column := &Column{
|
|
BoardID: req.BoardID,
|
|
Name: req.Name,
|
|
Color: req.Color,
|
|
Order: req.Order,
|
|
Limit: req.Limit,
|
|
}
|
|
|
|
if err := s.columnRepository.CreateColumn(ctx, column); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return column.ToResponse(), nil
|
|
}
|
|
|
|
// UpdateColumn updates an existing column
|
|
func (s *kanbanService) UpdateColumn(ctx context.Context, req UpdateColumnRequest) (*ColumnResponse, error) {
|
|
s.logger.Info("Updating column", map[string]interface{}{
|
|
"column_id": req.ID,
|
|
})
|
|
|
|
column, err := s.columnRepository.GetColumnByID(ctx, req.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update fields
|
|
if req.Name != nil {
|
|
column.Name = *req.Name
|
|
}
|
|
if req.Color != nil {
|
|
column.Color = *req.Color
|
|
}
|
|
if req.Order != nil {
|
|
column.Order = *req.Order
|
|
}
|
|
if req.Limit != nil {
|
|
column.Limit = req.Limit
|
|
}
|
|
|
|
if err := s.columnRepository.UpdateColumn(ctx, column); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return column.ToResponse(), nil
|
|
}
|
|
|
|
// DeleteColumn deletes a column and moves all its cards to the next column
|
|
func (s *kanbanService) DeleteColumn(ctx context.Context, id string) error {
|
|
s.logger.Info("Deleting column", map[string]interface{}{
|
|
"column_id": id,
|
|
})
|
|
|
|
column, err := s.columnRepository.GetColumnByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get all columns for this board to find the next one
|
|
columns, err := s.columnRepository.GetColumnsByBoardID(ctx, column.BoardID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the next column (one with higher order)
|
|
var nextColumn *Column
|
|
sort.Slice(columns, func(i, j int) bool {
|
|
return columns[i].Order < columns[j].Order
|
|
})
|
|
|
|
for _, col := range columns {
|
|
if col.Order > column.Order {
|
|
nextColumn = &col
|
|
break
|
|
}
|
|
}
|
|
|
|
// Move cards to next column or delete them if no next column exists
|
|
cards, err := s.cardRepository.GetCardsByColumnID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if nextColumn != nil {
|
|
// Move cards to next column
|
|
for _, card := range cards {
|
|
if err := s.cardRepository.MoveCard(ctx, card.ID.Hex(), nextColumn.ID.Hex(), card.Order); err != nil {
|
|
s.logger.Warn("Failed to move card during column deletion", map[string]interface{}{
|
|
"card_id": card.ID.Hex(),
|
|
"target_column_id": nextColumn.ID.Hex(),
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
// Delete cards if no next column
|
|
for _, card := range cards {
|
|
if err := s.cardRepository.DeleteCard(ctx, card.ID.Hex()); err != nil {
|
|
s.logger.Warn("Failed to delete card during column deletion", map[string]interface{}{
|
|
"card_id": card.ID.Hex(),
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete the column
|
|
return s.columnRepository.DeleteColumn(ctx, id)
|
|
}
|
|
|
|
// ReorderColumns reorders columns within a board
|
|
func (s *kanbanService) ReorderColumns(ctx context.Context, req ReorderColumnsRequest) error {
|
|
s.logger.Info("Reordering columns", map[string]interface{}{
|
|
"board_id": req.BoardID,
|
|
"count": len(req.ColumnOrders),
|
|
})
|
|
|
|
return s.columnRepository.UpdateColumnOrder(ctx, req.BoardID, req.ColumnOrders)
|
|
}
|
|
|
|
// CreateCard creates a new card in a column
|
|
func (s *kanbanService) CreateCard(ctx context.Context, req CreateCardRequest) (*CardResponse, error) {
|
|
s.logger.Info("Creating new card", map[string]interface{}{
|
|
"title": req.Title,
|
|
"tender_id": req.TenderID,
|
|
"column_id": req.ColumnID,
|
|
"order": req.Order,
|
|
})
|
|
|
|
// Validate that the column exists
|
|
_, err := s.columnRepository.GetColumnByID(ctx, req.ColumnID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("column not found: %w", err)
|
|
}
|
|
|
|
// Check for duplicate card (same tender in same column)
|
|
existingCards, err := s.cardRepository.GetCardsByTenderID(ctx, req.TenderID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, card := range existingCards {
|
|
if card.ColumnID == req.ColumnID {
|
|
return nil, fmt.Errorf("card for this tender already exists in the column")
|
|
}
|
|
}
|
|
|
|
card := &Card{
|
|
ColumnID: req.ColumnID,
|
|
TenderID: req.TenderID,
|
|
Title: req.Title,
|
|
Order: req.Order,
|
|
Priority: req.Priority,
|
|
Labels: req.Labels,
|
|
DueDate: req.DueDate,
|
|
}
|
|
|
|
if err := s.cardRepository.CreateCard(ctx, card); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return card.ToResponse(), nil
|
|
}
|
|
|
|
// GetCardByID retrieves a card by ID
|
|
func (s *kanbanService) GetCardByID(ctx context.Context, id string) (*CardResponse, error) {
|
|
s.logger.Info("Retrieving card by ID", map[string]interface{}{
|
|
"card_id": id,
|
|
})
|
|
|
|
card, err := s.cardRepository.GetCardByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return card.ToResponse(), nil
|
|
}
|
|
|
|
// UpdateCard updates an existing card
|
|
func (s *kanbanService) UpdateCard(ctx context.Context, req UpdateCardRequest) (*CardResponse, error) {
|
|
s.logger.Info("Updating card", map[string]interface{}{
|
|
"card_id": req.ID,
|
|
})
|
|
|
|
card, err := s.cardRepository.GetCardByID(ctx, req.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update fields
|
|
if req.Title != nil {
|
|
card.Title = *req.Title
|
|
}
|
|
if req.Priority != nil {
|
|
card.Priority = *req.Priority
|
|
}
|
|
if req.Labels != nil {
|
|
card.Labels = req.Labels
|
|
}
|
|
if req.DueDate != nil {
|
|
card.DueDate = req.DueDate
|
|
}
|
|
|
|
if err := s.cardRepository.UpdateCard(ctx, card); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return card.ToResponse(), nil
|
|
}
|
|
|
|
// DeleteCard deletes a card
|
|
func (s *kanbanService) DeleteCard(ctx context.Context, id string) error {
|
|
s.logger.Info("Deleting card", map[string]interface{}{
|
|
"card_id": id,
|
|
})
|
|
|
|
return s.cardRepository.DeleteCard(ctx, id)
|
|
}
|
|
|
|
// MoveCard moves a card to a different column
|
|
func (s *kanbanService) MoveCard(ctx context.Context, req MoveCardRequest) error {
|
|
s.logger.Info("Moving card", map[string]interface{}{
|
|
"card_id": req.CardID,
|
|
"column_id": req.ColumnID,
|
|
"new_order": req.NewOrder,
|
|
})
|
|
|
|
// Validate that the target column exists
|
|
_, err := s.columnRepository.GetColumnByID(ctx, req.ColumnID)
|
|
if err != nil {
|
|
return fmt.Errorf("target column not found: %w", err)
|
|
}
|
|
|
|
return s.cardRepository.MoveCard(ctx, req.CardID, req.ColumnID, req.NewOrder)
|
|
}
|
|
|
|
// ReorderCards reorders cards within a column
|
|
func (s *kanbanService) ReorderCards(ctx context.Context, req ReorderCardsRequest) error {
|
|
s.logger.Info("Reordering cards", map[string]interface{}{
|
|
"column_id": req.ColumnID,
|
|
"count": len(req.CardOrders),
|
|
})
|
|
|
|
return s.cardRepository.UpdateCardOrder(ctx, req.ColumnID, req.CardOrders)
|
|
}
|
|
|
|
// unsetDefaultBoard removes the default flag from all boards for a user
|
|
func (s *kanbanService) unsetDefaultBoard(ctx context.Context, userID string) error {
|
|
boards, err := s.boardRepository.GetBoardsByUserID(ctx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, board := range boards {
|
|
if board.IsDefault {
|
|
board.IsDefault = false
|
|
if err := s.boardRepository.UpdateBoard(ctx, &board); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|