Files
tm_back/internal/kanban/service.go
T
2026-01-05 15:36:20 +03:30

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
}