kanban dashboard
This commit is contained in:
+11
-4
@@ -102,6 +102,7 @@ import (
|
|||||||
"tm/internal/customer"
|
"tm/internal/customer"
|
||||||
"tm/internal/feedback"
|
"tm/internal/feedback"
|
||||||
"tm/internal/inquiry"
|
"tm/internal/inquiry"
|
||||||
|
"tm/internal/kanban"
|
||||||
"tm/internal/notification"
|
"tm/internal/notification"
|
||||||
"tm/internal/scraper"
|
"tm/internal/scraper"
|
||||||
"tm/internal/tender"
|
"tm/internal/tender"
|
||||||
@@ -167,13 +168,17 @@ func main() {
|
|||||||
inquiryRepository := inquiry.NewRepository(mongoManager, logger)
|
inquiryRepository := inquiry.NewRepository(mongoManager, logger)
|
||||||
contactRepository := contact.NewContactRepository(mongoManager, logger)
|
contactRepository := contact.NewContactRepository(mongoManager, logger)
|
||||||
cmsRepository := cms.NewRepository(mongoManager, logger)
|
cmsRepository := cms.NewRepository(mongoManager, logger)
|
||||||
|
boardRepository := kanban.NewBoardRepository(mongoManager, logger)
|
||||||
|
columnRepository := kanban.NewColumnRepository(mongoManager, logger)
|
||||||
|
cardRepository := kanban.NewCardRepository(mongoManager, logger)
|
||||||
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
||||||
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "contact", "cms"},
|
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "contact", "cms", "kanban_board", "kanban_column", "kanban_card"},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize validation services
|
// Initialize validation services
|
||||||
userValidator := user.NewValidationService()
|
userValidator := user.NewValidationService()
|
||||||
customerValidator := customer.NewValidationService()
|
customerValidator := customer.NewValidationService()
|
||||||
|
_ = kanban.NewValidationService() // Register hexcolor validator
|
||||||
|
|
||||||
// Initialize services with repositories
|
// Initialize services with repositories
|
||||||
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK)
|
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK)
|
||||||
@@ -189,8 +194,9 @@ func main() {
|
|||||||
contactService := contact.NewService(contactRepository, logger, notificationSDK)
|
contactService := contact.NewService(contactRepository, logger, notificationSDK)
|
||||||
cmsService := cms.NewService(cmsRepository, logger)
|
cmsService := cms.NewService(cmsRepository, logger)
|
||||||
scraperService := scraper.NewService(conf.Scraper.BaseURL, conf.Scraper.Timeout, logger)
|
scraperService := scraper.NewService(conf.Scraper.BaseURL, conf.Scraper.Timeout, logger)
|
||||||
|
kanbanService := kanban.NewService(boardRepository, columnRepository, cardRepository, logger)
|
||||||
logger.Info("Services initialized successfully", map[string]interface{}{
|
logger.Info("Services initialized successfully", map[string]interface{}{
|
||||||
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper"},
|
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper", "kanban"},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize handlers with services
|
// Initialize handlers with services
|
||||||
@@ -207,15 +213,16 @@ func main() {
|
|||||||
contactHandler := contact.NewHandler(contactService, hcaptchaVerifier)
|
contactHandler := contact.NewHandler(contactService, hcaptchaVerifier)
|
||||||
cmsHandler := cms.NewHandler(cmsService)
|
cmsHandler := cms.NewHandler(cmsService)
|
||||||
scraperHandler := scraper.NewHandler(scraperService)
|
scraperHandler := scraper.NewHandler(scraperService)
|
||||||
|
kanbanHandler := kanban.NewHandler(kanbanService)
|
||||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||||
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper"},
|
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper", "kanban"},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
e := bootstrap.InitHTTPServer(conf, logger)
|
e := bootstrap.InitHTTPServer(conf, logger)
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, scraperHandler, xssPolicy)
|
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, scraperHandler, kanbanHandler, xssPolicy)
|
||||||
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, xssPolicy)
|
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, xssPolicy)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"tm/internal/customer"
|
"tm/internal/customer"
|
||||||
"tm/internal/feedback"
|
"tm/internal/feedback"
|
||||||
"tm/internal/inquiry"
|
"tm/internal/inquiry"
|
||||||
|
"tm/internal/kanban"
|
||||||
"tm/internal/notification"
|
"tm/internal/notification"
|
||||||
"tm/internal/scraper"
|
"tm/internal/scraper"
|
||||||
"tm/internal/tender"
|
"tm/internal/tender"
|
||||||
@@ -19,7 +20,7 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, scraperHandler *scraper.Handler, xssPolicy *security.XSSPolicy) {
|
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, scraperHandler *scraper.Handler, kanbanHandler *kanban.Handler, xssPolicy *security.XSSPolicy) {
|
||||||
adminV1 := e.Group("/admin/v1")
|
adminV1 := e.Group("/admin/v1")
|
||||||
|
|
||||||
// Add CSP middleware to admin routes (stricter policy)
|
// Add CSP middleware to admin routes (stricter policy)
|
||||||
@@ -183,6 +184,13 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
|
|||||||
scraperGP.Use(userHandler.AuthMiddleware())
|
scraperGP.Use(userHandler.AuthMiddleware())
|
||||||
scraperGP.POST("/download", scraperHandler.ScrapeDocuments)
|
scraperGP.POST("/download", scraperHandler.ScrapeDocuments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kanban Routes
|
||||||
|
kanbanGP := adminV1.Group("/kanban")
|
||||||
|
{
|
||||||
|
kanbanGP.Use(userHandler.AuthMiddleware())
|
||||||
|
kanbanHandler.RegisterRoutes(kanbanGP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, xssPolicy *security.XSSPolicy) {
|
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, xssPolicy *security.XSSPolicy) {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package kanban
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"tm/pkg/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Board represents a kanban board
|
||||||
|
type Board struct {
|
||||||
|
mongo.Model `bson:",inline"`
|
||||||
|
Name string `bson:"name" json:"name"`
|
||||||
|
Description string `bson:"description" json:"description"`
|
||||||
|
Columns []Column `bson:"columns" json:"columns"`
|
||||||
|
IsDefault bool `bson:"is_default" json:"is_default"` // Whether this is the default board
|
||||||
|
UserID string `bson:"user_id" json:"user_id"` // User who owns this board
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column represents a column on a kanban board
|
||||||
|
type Column struct {
|
||||||
|
mongo.Model `bson:",inline"`
|
||||||
|
BoardID string `bson:"board_id" json:"board_id"`
|
||||||
|
Name string `bson:"name" json:"name"`
|
||||||
|
Order int `bson:"order" json:"order"` // Display order in the board
|
||||||
|
Color string `bson:"color" json:"color"` // Hex color code for the column
|
||||||
|
Limit *int `bson:"limit,omitempty" json:"limit,omitempty"` // WIP limit (optional)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card represents a card on a kanban board (references a tender)
|
||||||
|
type Card struct {
|
||||||
|
mongo.Model `bson:",inline"`
|
||||||
|
ColumnID string `bson:"column_id" json:"column_id"`
|
||||||
|
TenderID string `bson:"tender_id" json:"tender_id"` // Reference to the tender
|
||||||
|
Title string `bson:"title" json:"title"` // Cached title from tender
|
||||||
|
Order int `bson:"order" json:"order"` // Position within the column
|
||||||
|
Priority CardPriority `bson:"priority" json:"priority"`
|
||||||
|
Labels []string `bson:"labels,omitempty" json:"labels,omitempty"`
|
||||||
|
DueDate *int64 `bson:"due_date,omitempty" json:"due_date,omitempty"` // Unix timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardPriority represents the priority level of a card
|
||||||
|
type CardPriority string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CardPriorityLow CardPriority = "low"
|
||||||
|
CardPriorityMedium CardPriority = "medium"
|
||||||
|
CardPriorityHigh CardPriority = "high"
|
||||||
|
CardPriorityUrgent CardPriority = "urgent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetID returns the board ID
|
||||||
|
func (b *Board) GetID() string {
|
||||||
|
return b.ID.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the column ID
|
||||||
|
func (c *Column) GetID() string {
|
||||||
|
return c.ID.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the card ID
|
||||||
|
func (c *Card) GetID() string {
|
||||||
|
return c.ID.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOverLimit returns true if the column has a WIP limit and is over it
|
||||||
|
func (c *Column) IsOverLimit(cardCount int) bool {
|
||||||
|
if c.Limit == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return cardCount > *c.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOrder updates the column's display order
|
||||||
|
func (c *Column) UpdateOrder(order int) {
|
||||||
|
c.Order = order
|
||||||
|
c.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOrder updates the card's position within the column
|
||||||
|
func (c *Card) UpdateOrder(order int) {
|
||||||
|
c.Order = order
|
||||||
|
c.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveToColumn moves the card to a different column and resets order
|
||||||
|
func (c *Card) MoveToColumn(columnID string, order int) {
|
||||||
|
c.ColumnID = columnID
|
||||||
|
c.Order = order
|
||||||
|
c.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePriority updates the card's priority
|
||||||
|
func (c *Card) UpdatePriority(priority CardPriority) {
|
||||||
|
c.Priority = priority
|
||||||
|
c.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLabel adds a label to the card
|
||||||
|
func (c *Card) AddLabel(label string) {
|
||||||
|
if c.Labels == nil {
|
||||||
|
c.Labels = []string{}
|
||||||
|
}
|
||||||
|
// Check if label already exists
|
||||||
|
for _, l := range c.Labels {
|
||||||
|
if l == label {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Labels = append(c.Labels, label)
|
||||||
|
c.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveLabel removes a label from the card
|
||||||
|
func (c *Card) RemoveLabel(label string) {
|
||||||
|
if c.Labels == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, l := range c.Labels {
|
||||||
|
if l == label {
|
||||||
|
c.Labels = append(c.Labels[:i], c.Labels[i+1:]...)
|
||||||
|
c.UpdatedAt = time.Now().Unix()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package kanban
|
||||||
|
|
||||||
|
import "tm/pkg/response"
|
||||||
|
|
||||||
|
// CreateBoardRequest represents a request to create a new kanban board
|
||||||
|
type CreateBoardRequest struct {
|
||||||
|
Name string `json:"name" valid:"required~Board name is required,stringlength(1|100)~Board name must be between 1 and 100 characters"`
|
||||||
|
Description string `json:"description" valid:"stringlength(0|500)~Description must not exceed 500 characters"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
UserID string `json:"-"` // Set from context/auth middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBoardRequest represents a request to update an existing board
|
||||||
|
type UpdateBoardRequest struct {
|
||||||
|
ID string `json:"id" valid:"required~Board ID is required"`
|
||||||
|
Name *string `json:"name,omitempty" valid:"stringlength(1|100)~Board name must be between 1 and 100 characters"`
|
||||||
|
Description *string `json:"description,omitempty" valid:"stringlength(0|500)~Description must not exceed 500 characters"`
|
||||||
|
IsDefault *bool `json:"is_default,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateColumnRequest represents a request to create a new column
|
||||||
|
type CreateColumnRequest struct {
|
||||||
|
BoardID string `json:"board_id" valid:"required~Board ID is required"`
|
||||||
|
Name string `json:"name" valid:"required~Column name is required,stringlength(1|50)~Column name must be between 1 and 50 characters"`
|
||||||
|
Color string `json:"color" valid:"hexcolor~Color must be a valid hex color"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Limit *int `json:"limit,omitempty" valid:"range(1|100)~WIP limit must be between 1 and 100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateColumnRequest represents a request to update an existing column
|
||||||
|
type UpdateColumnRequest struct {
|
||||||
|
ID string `json:"id" valid:"required~Column ID is required"`
|
||||||
|
Name *string `json:"name,omitempty" valid:"stringlength(1|50)~Column name must be between 1 and 50 characters"`
|
||||||
|
Color *string `json:"color,omitempty" valid:"hexcolor~Color must be a valid hex color"`
|
||||||
|
Order *int `json:"order,omitempty"`
|
||||||
|
Limit *int `json:"limit,omitempty" valid:"range(1|100)~WIP limit must be between 1 and 100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCardRequest represents a request to create a new card
|
||||||
|
type CreateCardRequest struct {
|
||||||
|
ColumnID string `json:"column_id" valid:"required~Column ID is required"`
|
||||||
|
TenderID string `json:"tender_id" valid:"required~Tender ID is required"`
|
||||||
|
Title string `json:"title" valid:"required~Title is required,stringlength(1|200)~Title must be between 1 and 200 characters"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Priority CardPriority `json:"priority"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
DueDate *int64 `json:"due_date,omitempty"` // Unix timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCardRequest represents a request to update an existing card
|
||||||
|
type UpdateCardRequest struct {
|
||||||
|
ID string `json:"id" valid:"required~Card ID is required"`
|
||||||
|
Title *string `json:"title,omitempty" valid:"stringlength(1|200)~Title must be between 1 and 200 characters"`
|
||||||
|
Priority *CardPriority `json:"priority,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
DueDate *int64 `json:"due_date,omitempty"` // Unix timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveCardRequest represents a request to move a card to a different column
|
||||||
|
type MoveCardRequest struct {
|
||||||
|
CardID string `json:"card_id" valid:"required~Card ID is required"`
|
||||||
|
ColumnID string `json:"column_id" valid:"required~Column ID is required"`
|
||||||
|
NewOrder int `json:"new_order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderCardsRequest represents a request to reorder cards within a column
|
||||||
|
type ReorderCardsRequest struct {
|
||||||
|
ColumnID string `json:"column_id" valid:"required~Column ID is required"`
|
||||||
|
CardOrders map[string]int `json:"card_orders" valid:"required~Card orders are required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderColumnsRequest represents a request to reorder columns within a board
|
||||||
|
type ReorderColumnsRequest struct {
|
||||||
|
BoardID string `json:"board_id" valid:"required~Board ID is required"`
|
||||||
|
ColumnOrders map[string]int `json:"column_orders" valid:"required~Column orders are required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoardResponse represents a board in API responses
|
||||||
|
type BoardResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Columns []ColumnResponse `json:"columns"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnResponse represents a column in API responses
|
||||||
|
type ColumnResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
BoardID string `json:"board_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Limit *int `json:"limit,omitempty"`
|
||||||
|
Cards []CardResponse `json:"cards"`
|
||||||
|
CardCount int `json:"card_count"`
|
||||||
|
IsOverLimit bool `json:"is_over_limit"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardResponse represents a card in API responses
|
||||||
|
type CardResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ColumnID string `json:"column_id"`
|
||||||
|
TenderID string `json:"tender_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Priority CardPriority `json:"priority"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
DueDate *int64 `json:"due_date,omitempty"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoardsListResponse represents the response for listing boards
|
||||||
|
type BoardsListResponse struct {
|
||||||
|
Boards []BoardResponse `json:"boards"`
|
||||||
|
Meta *response.Meta `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoardDetailResponse represents detailed board response with full column and card data
|
||||||
|
type BoardDetailResponse struct {
|
||||||
|
Board BoardResponse `json:"board"`
|
||||||
|
Meta *response.Meta `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBoardResponse converts a Board entity to BoardResponse
|
||||||
|
func (b *Board) ToResponse() *BoardResponse {
|
||||||
|
return &BoardResponse{
|
||||||
|
ID: b.ID.Hex(),
|
||||||
|
Name: b.Name,
|
||||||
|
Description: b.Description,
|
||||||
|
Columns: make([]ColumnResponse, 0), // Will be populated by service
|
||||||
|
IsDefault: b.IsDefault,
|
||||||
|
UserID: b.UserID,
|
||||||
|
CreatedAt: b.CreatedAt,
|
||||||
|
UpdatedAt: b.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToColumnResponse converts a Column entity to ColumnResponse
|
||||||
|
func (c *Column) ToResponse() *ColumnResponse {
|
||||||
|
return &ColumnResponse{
|
||||||
|
ID: c.ID.Hex(),
|
||||||
|
BoardID: c.BoardID,
|
||||||
|
Name: c.Name,
|
||||||
|
Order: c.Order,
|
||||||
|
Color: c.Color,
|
||||||
|
Limit: c.Limit,
|
||||||
|
Cards: make([]CardResponse, 0), // Will be populated by service
|
||||||
|
CardCount: 0, // Will be populated by service
|
||||||
|
IsOverLimit: false, // Will be calculated by service
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
UpdatedAt: c.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToCardResponse converts a Card entity to CardResponse
|
||||||
|
func (c *Card) ToResponse() *CardResponse {
|
||||||
|
return &CardResponse{
|
||||||
|
ID: c.ID.Hex(),
|
||||||
|
ColumnID: c.ColumnID,
|
||||||
|
TenderID: c.TenderID,
|
||||||
|
Title: c.Title,
|
||||||
|
Order: c.Order,
|
||||||
|
Priority: c.Priority,
|
||||||
|
Labels: c.Labels,
|
||||||
|
DueDate: c.DueDate,
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
UpdatedAt: c.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
package kanban
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"tm/pkg/response"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler handles kanban HTTP requests
|
||||||
|
type Handler struct {
|
||||||
|
service Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a new kanban handler
|
||||||
|
func NewHandler(service Service) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes registers kanban routes
|
||||||
|
func (h *Handler) RegisterRoutes(router *echo.Group) {
|
||||||
|
kanban := router.Group("/kanban")
|
||||||
|
{
|
||||||
|
// Board routes
|
||||||
|
kanban.POST("/boards", h.CreateBoard)
|
||||||
|
kanban.GET("/boards", h.GetBoardsByUser)
|
||||||
|
kanban.GET("/boards/default", h.GetDefaultBoard)
|
||||||
|
kanban.GET("/boards/:id", h.GetBoardByID)
|
||||||
|
kanban.PUT("/boards/:id", h.UpdateBoard)
|
||||||
|
kanban.DELETE("/boards/:id", h.DeleteBoard)
|
||||||
|
|
||||||
|
// Column routes
|
||||||
|
kanban.POST("/columns", h.CreateColumn)
|
||||||
|
kanban.PUT("/columns/:id", h.UpdateColumn)
|
||||||
|
kanban.DELETE("/columns/:id", h.DeleteColumn)
|
||||||
|
kanban.PUT("/columns/reorder", h.ReorderColumns)
|
||||||
|
|
||||||
|
// Card routes
|
||||||
|
kanban.POST("/cards", h.CreateCard)
|
||||||
|
kanban.GET("/cards/:id", h.GetCardByID)
|
||||||
|
kanban.PUT("/cards/:id", h.UpdateCard)
|
||||||
|
kanban.DELETE("/cards/:id", h.DeleteCard)
|
||||||
|
kanban.PUT("/cards/move", h.MoveCard)
|
||||||
|
kanban.PUT("/cards/reorder", h.ReorderCards)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBoard creates a new kanban board
|
||||||
|
// @Summary Create a new kanban board
|
||||||
|
// @Description Create a new kanban board with the provided information
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param board body CreateBoardRequest true "Board information"
|
||||||
|
// @Success 201 {object} response.Response{data=BoardResponse}
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/boards [post]
|
||||||
|
func (h *Handler) CreateBoard(c echo.Context) error {
|
||||||
|
var req CreateBoardRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from context (assuming auth middleware sets this)
|
||||||
|
userID, exists := c.Get("user_id").(string)
|
||||||
|
if !exists {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
req.UserID = userID
|
||||||
|
|
||||||
|
board, err := h.service.CreateBoard(c.Request().Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to create board")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Created(c, board, "Board created successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoardByID retrieves a board by ID
|
||||||
|
// @Summary Get a kanban board by ID
|
||||||
|
// @Description Retrieve a kanban board with all its columns and cards
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Board ID"
|
||||||
|
// @Success 200 {object} response.Response{data=BoardDetailResponse}
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/boards/{id} [get]
|
||||||
|
func (h *Handler) GetBoardByID(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
board, err := h.service.GetBoardByID(c.Request().Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to get board")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, board, "Board retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoardsByUser retrieves all boards for the authenticated user
|
||||||
|
// @Summary Get all kanban boards for user
|
||||||
|
// @Description Retrieve all kanban boards belonging to the authenticated user
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.Response{data=BoardsListResponse}
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/boards [get]
|
||||||
|
func (h *Handler) GetBoardsByUser(c echo.Context) error {
|
||||||
|
userID, exists := c.Get("user_id").(string)
|
||||||
|
if !exists {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
boards, err := h.service.GetBoardsByUserID(c.Request().Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to get boards")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, boards, "Boards retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultBoard retrieves the default board for the authenticated user
|
||||||
|
// @Summary Get default kanban board
|
||||||
|
// @Description Retrieve the default kanban board for the authenticated user
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.Response{data=BoardDetailResponse}
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/boards/default [get]
|
||||||
|
func (h *Handler) GetDefaultBoard(c echo.Context) error {
|
||||||
|
userID, exists := c.Get("user_id").(string)
|
||||||
|
if !exists {
|
||||||
|
return response.Unauthorized(c, "User not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
board, err := h.service.GetDefaultBoard(c.Request().Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to get default board")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, board, "Default board retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBoard updates an existing board
|
||||||
|
// @Summary Update a kanban board
|
||||||
|
// @Description Update an existing kanban board with the provided information
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Board ID"
|
||||||
|
// @Param board body UpdateBoardRequest true "Board update information"
|
||||||
|
// @Success 200 {object} response.Response{data=BoardResponse}
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/boards/{id} [put]
|
||||||
|
func (h *Handler) UpdateBoard(c echo.Context) error {
|
||||||
|
var req UpdateBoardRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ID = c.Param("id")
|
||||||
|
|
||||||
|
board, err := h.service.UpdateBoard(c.Request().Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to update board")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, board, "Board updated successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBoard deletes a board
|
||||||
|
// @Summary Delete a kanban board
|
||||||
|
// @Description Delete a kanban board and all its columns and cards
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Board ID"
|
||||||
|
// @Success 204 {object} response.Response
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/boards/{id} [delete]
|
||||||
|
func (h *Handler) DeleteBoard(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := h.service.DeleteBoard(c.Request().Context(), id); err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to delete board")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateColumn creates a new column
|
||||||
|
// @Summary Create a new column
|
||||||
|
// @Description Create a new column in a kanban board
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param column body CreateColumnRequest true "Column information"
|
||||||
|
// @Success 201 {object} response.Response{data=ColumnResponse}
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/columns [post]
|
||||||
|
func (h *Handler) CreateColumn(c echo.Context) error {
|
||||||
|
var req CreateColumnRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
column, err := h.service.CreateColumn(c.Request().Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to create column")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Created(c, column, "Column created successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateColumn updates an existing column
|
||||||
|
// @Summary Update a column
|
||||||
|
// @Description Update an existing column in a kanban board
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Column ID"
|
||||||
|
// @Param column body UpdateColumnRequest true "Column update information"
|
||||||
|
// @Success 200 {object} response.Response{data=ColumnResponse}
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/columns/{id} [put]
|
||||||
|
func (h *Handler) UpdateColumn(c echo.Context) error {
|
||||||
|
var req UpdateColumnRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ID = c.Param("id")
|
||||||
|
|
||||||
|
column, err := h.service.UpdateColumn(c.Request().Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to update column")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, column, "Column updated successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteColumn deletes a column
|
||||||
|
// @Summary Delete a column
|
||||||
|
// @Description Delete a column and move its cards to the next column
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Column ID"
|
||||||
|
// @Success 204 {object} response.Response
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/columns/{id} [delete]
|
||||||
|
func (h *Handler) DeleteColumn(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := h.service.DeleteColumn(c.Request().Context(), id); err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to delete column")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderColumns reorders columns within a board
|
||||||
|
// @Summary Reorder columns
|
||||||
|
// @Description Reorder columns within a kanban board
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param reorder body ReorderColumnsRequest true "Column reorder information"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/columns/reorder [put]
|
||||||
|
func (h *Handler) ReorderColumns(c echo.Context) error {
|
||||||
|
var req ReorderColumnsRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.ReorderColumns(c.Request().Context(), req); err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to reorder columns")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, map[string]string{"message": "Columns reordered successfully"}, "Columns reordered successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCard creates a new card
|
||||||
|
// @Summary Create a new card
|
||||||
|
// @Description Create a new card in a kanban column
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param card body CreateCardRequest true "Card information"
|
||||||
|
// @Success 201 {object} response.Response{data=CardResponse}
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/cards [post]
|
||||||
|
func (h *Handler) CreateCard(c echo.Context) error {
|
||||||
|
var req CreateCardRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default priority if not provided
|
||||||
|
if req.Priority == "" {
|
||||||
|
req.Priority = CardPriorityMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse order from query param if provided
|
||||||
|
if orderStr := c.QueryParam("order"); orderStr != "" {
|
||||||
|
if order, err := strconv.Atoi(orderStr); err == nil {
|
||||||
|
req.Order = order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse due_date from query param if provided
|
||||||
|
if dueDateStr := c.QueryParam("due_date"); dueDateStr != "" {
|
||||||
|
if dueDate, err := strconv.ParseInt(dueDateStr, 10, 64); err == nil {
|
||||||
|
req.DueDate = &dueDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
card, err := h.service.CreateCard(c.Request().Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to create card")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Created(c, card, "Card created successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCardByID retrieves a card by ID
|
||||||
|
// @Summary Get a card by ID
|
||||||
|
// @Description Retrieve a kanban card by its ID
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Card ID"
|
||||||
|
// @Success 200 {object} response.Response{data=CardResponse}
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/cards/{id} [get]
|
||||||
|
func (h *Handler) GetCardByID(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
card, err := h.service.GetCardByID(c.Request().Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to get card")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, card, "Card retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCard updates an existing card
|
||||||
|
// @Summary Update a card
|
||||||
|
// @Description Update an existing kanban card
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Card ID"
|
||||||
|
// @Param card body UpdateCardRequest true "Card update information"
|
||||||
|
// @Success 200 {object} response.Response{data=CardResponse}
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/cards/{id} [put]
|
||||||
|
func (h *Handler) UpdateCard(c echo.Context) error {
|
||||||
|
var req UpdateCardRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ID = c.Param("id")
|
||||||
|
|
||||||
|
// Parse due_date from query param if provided
|
||||||
|
if dueDateStr := c.QueryParam("due_date"); dueDateStr != "" {
|
||||||
|
if dueDate, err := strconv.ParseInt(dueDateStr, 10, 64); err == nil {
|
||||||
|
req.DueDate = &dueDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
card, err := h.service.UpdateCard(c.Request().Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to update card")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, card, "Card updated successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCard deletes a card
|
||||||
|
// @Summary Delete a card
|
||||||
|
// @Description Delete a kanban card
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Card ID"
|
||||||
|
// @Success 204 {object} response.Response
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/cards/{id} [delete]
|
||||||
|
func (h *Handler) DeleteCard(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := h.service.DeleteCard(c.Request().Context(), id); err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to delete card")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveCard moves a card to a different column
|
||||||
|
// @Summary Move a card
|
||||||
|
// @Description Move a kanban card to a different column
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param move body MoveCardRequest true "Card move information"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 404 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/cards/move [put]
|
||||||
|
func (h *Handler) MoveCard(c echo.Context) error {
|
||||||
|
var req MoveCardRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.MoveCard(c.Request().Context(), req); err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to move card")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, map[string]string{"message": "Card moved successfully"}, "Card moved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderCards reorders cards within a column
|
||||||
|
// @Summary Reorder cards
|
||||||
|
// @Description Reorder cards within a kanban column
|
||||||
|
// @Tags kanban
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param reorder body ReorderCardsRequest true "Card reorder information"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /kanban/cards/reorder [put]
|
||||||
|
func (h *Handler) ReorderCards(c echo.Context) error {
|
||||||
|
var req ReorderCardsRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.ReorderCards(c.Request().Context(), req); err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to reorder cards")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, map[string]string{"message": "Cards reordered successfully"}, "Cards reordered successfully")
|
||||||
|
}
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
package kanban
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
orm "tm/pkg/mongo"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoardRepository interface defines board data access methods
|
||||||
|
type BoardRepository interface {
|
||||||
|
// Board CRUD operations
|
||||||
|
CreateBoard(ctx context.Context, board *Board) error
|
||||||
|
GetBoardByID(ctx context.Context, id string) (*Board, error)
|
||||||
|
GetBoardsByUserID(ctx context.Context, userID string) ([]Board, error)
|
||||||
|
GetDefaultBoard(ctx context.Context, userID string) (*Board, error)
|
||||||
|
UpdateBoard(ctx context.Context, board *Board) error
|
||||||
|
DeleteBoard(ctx context.Context, id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnRepository interface defines column data access methods
|
||||||
|
type ColumnRepository interface {
|
||||||
|
// Column CRUD operations
|
||||||
|
CreateColumn(ctx context.Context, column *Column) error
|
||||||
|
GetColumnByID(ctx context.Context, id string) (*Column, error)
|
||||||
|
GetColumnsByBoardID(ctx context.Context, boardID string) ([]Column, error)
|
||||||
|
UpdateColumn(ctx context.Context, column *Column) error
|
||||||
|
DeleteColumn(ctx context.Context, id string) error
|
||||||
|
UpdateColumnOrder(ctx context.Context, boardID string, columnOrders map[string]int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardRepository interface defines card data access methods
|
||||||
|
type CardRepository interface {
|
||||||
|
// Card CRUD operations
|
||||||
|
CreateCard(ctx context.Context, card *Card) error
|
||||||
|
GetCardByID(ctx context.Context, id string) (*Card, error)
|
||||||
|
GetCardsByColumnID(ctx context.Context, columnID string) ([]Card, error)
|
||||||
|
GetCardsByTenderID(ctx context.Context, tenderID string) ([]Card, error)
|
||||||
|
UpdateCard(ctx context.Context, card *Card) error
|
||||||
|
DeleteCard(ctx context.Context, id string) error
|
||||||
|
MoveCard(ctx context.Context, cardID, targetColumnID string, newOrder int) error
|
||||||
|
UpdateCardOrder(ctx context.Context, columnID string, cardOrders map[string]int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func boardCollectionName() string {
|
||||||
|
return "kanban_boards"
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnCollectionName() string {
|
||||||
|
return "kanban_columns"
|
||||||
|
}
|
||||||
|
|
||||||
|
func cardCollectionName() string {
|
||||||
|
return "kanban_cards"
|
||||||
|
}
|
||||||
|
|
||||||
|
// boardRepository implements BoardRepository interface using MongoDB ORM
|
||||||
|
type boardRepository struct {
|
||||||
|
ormRepo orm.Repository[Board]
|
||||||
|
mongoManager *orm.ConnectionManager
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// columnRepository implements ColumnRepository interface using MongoDB ORM
|
||||||
|
type columnRepository struct {
|
||||||
|
ormRepo orm.Repository[Column]
|
||||||
|
mongoManager *orm.ConnectionManager
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// cardRepository implements CardRepository interface using MongoDB ORM
|
||||||
|
type cardRepository struct {
|
||||||
|
ormRepo orm.Repository[Card]
|
||||||
|
mongoManager *orm.ConnectionManager
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBoardRepository creates a new board repository
|
||||||
|
func NewBoardRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) BoardRepository {
|
||||||
|
// Create indexes for boards collection
|
||||||
|
boardIndexes := []orm.Index{
|
||||||
|
*orm.NewIndex("user_id_idx", bson.D{{Key: "user_id", Value: 1}}),
|
||||||
|
*orm.NewIndex("is_default_idx", bson.D{{Key: "is_default", Value: 1}}),
|
||||||
|
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||||
|
*orm.NewIndex("user_default_idx", bson.D{
|
||||||
|
{Key: "user_id", Value: 1},
|
||||||
|
{Key: "is_default", Value: 1},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
if err := mongoManager.CreateIndexes(boardCollectionName(), boardIndexes); err != nil {
|
||||||
|
logger.Warn("Failed to create board indexes", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
boardOrmRepo := orm.NewRepository[Board](mongoManager.GetCollection(boardCollectionName()), logger)
|
||||||
|
|
||||||
|
return &boardRepository{
|
||||||
|
ormRepo: boardOrmRepo,
|
||||||
|
mongoManager: mongoManager,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColumnRepository creates a new column repository
|
||||||
|
func NewColumnRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) ColumnRepository {
|
||||||
|
// Create indexes for columns collection
|
||||||
|
columnIndexes := []orm.Index{
|
||||||
|
*orm.NewIndex("board_id_idx", bson.D{{Key: "board_id", Value: 1}}),
|
||||||
|
*orm.NewIndex("order_idx", bson.D{{Key: "order", Value: 1}}),
|
||||||
|
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||||
|
*orm.NewIndex("board_order_idx", bson.D{
|
||||||
|
{Key: "board_id", Value: 1},
|
||||||
|
{Key: "order", Value: 1},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
if err := mongoManager.CreateIndexes(columnCollectionName(), columnIndexes); err != nil {
|
||||||
|
logger.Warn("Failed to create column indexes", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
columnOrmRepo := orm.NewRepository[Column](mongoManager.GetCollection(columnCollectionName()), logger)
|
||||||
|
|
||||||
|
return &columnRepository{
|
||||||
|
ormRepo: columnOrmRepo,
|
||||||
|
mongoManager: mongoManager,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCardRepository creates a new card repository
|
||||||
|
func NewCardRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) CardRepository {
|
||||||
|
// Create indexes for cards collection
|
||||||
|
cardIndexes := []orm.Index{
|
||||||
|
*orm.NewIndex("column_id_idx", bson.D{{Key: "column_id", Value: 1}}),
|
||||||
|
*orm.NewIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}),
|
||||||
|
*orm.NewIndex("order_idx", bson.D{{Key: "order", Value: 1}}),
|
||||||
|
*orm.NewIndex("priority_idx", bson.D{{Key: "priority", Value: 1}}),
|
||||||
|
*orm.NewIndex("due_date_idx", bson.D{{Key: "due_date", Value: 1}}),
|
||||||
|
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||||
|
*orm.NewIndex("column_order_idx", bson.D{
|
||||||
|
{Key: "column_id", Value: 1},
|
||||||
|
{Key: "order", Value: 1},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
if err := mongoManager.CreateIndexes(cardCollectionName(), cardIndexes); err != nil {
|
||||||
|
logger.Warn("Failed to create card indexes", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cardOrmRepo := orm.NewRepository[Card](mongoManager.GetCollection(cardCollectionName()), logger)
|
||||||
|
|
||||||
|
return &cardRepository{
|
||||||
|
ormRepo: cardOrmRepo,
|
||||||
|
mongoManager: mongoManager,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoardRepository implementation
|
||||||
|
|
||||||
|
func (r *boardRepository) CreateBoard(ctx context.Context, board *Board) error {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
board.SetCreatedAt(now)
|
||||||
|
|
||||||
|
err := r.ormRepo.Create(ctx, board)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to create board", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"name": board.Name,
|
||||||
|
"user_id": board.UserID,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Board created successfully", map[string]interface{}{
|
||||||
|
"board_id": board.ID,
|
||||||
|
"name": board.Name,
|
||||||
|
"user_id": board.UserID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *boardRepository) GetBoardByID(ctx context.Context, id string) (*Board, error) {
|
||||||
|
board, err := r.ormRepo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get board by ID", map[string]interface{}{
|
||||||
|
"board_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return board, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *boardRepository) GetBoardsByUserID(ctx context.Context, userID string) ([]Board, error) {
|
||||||
|
filter := bson.M{"user_id": userID}
|
||||||
|
pagination := orm.Pagination{
|
||||||
|
SortField: "created_at",
|
||||||
|
SortOrder: -1, // Newest first
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get boards by user ID", map[string]interface{}{
|
||||||
|
"user_id": userID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *boardRepository) GetDefaultBoard(ctx context.Context, userID string) (*Board, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"is_default": true,
|
||||||
|
}
|
||||||
|
pagination := orm.Pagination{Limit: 1}
|
||||||
|
|
||||||
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get default board", map[string]interface{}{
|
||||||
|
"user_id": userID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Items) == 0 {
|
||||||
|
return nil, orm.ErrDocumentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result.Items[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *boardRepository) UpdateBoard(ctx context.Context, board *Board) error {
|
||||||
|
board.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
err := r.ormRepo.Update(ctx, board)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to update board", map[string]interface{}{
|
||||||
|
"board_id": board.ID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Board updated successfully", map[string]interface{}{
|
||||||
|
"board_id": board.ID,
|
||||||
|
"name": board.Name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *boardRepository) DeleteBoard(ctx context.Context, id string) error {
|
||||||
|
err := r.ormRepo.Delete(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to delete board", map[string]interface{}{
|
||||||
|
"board_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Board deleted successfully", map[string]interface{}{
|
||||||
|
"board_id": id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnRepository implementation
|
||||||
|
|
||||||
|
func (r *columnRepository) CreateColumn(ctx context.Context, column *Column) error {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
column.SetCreatedAt(now)
|
||||||
|
|
||||||
|
err := r.ormRepo.Create(ctx, column)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to create column", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"name": column.Name,
|
||||||
|
"board_id": column.BoardID,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Column created successfully", map[string]interface{}{
|
||||||
|
"column_id": column.ID,
|
||||||
|
"name": column.Name,
|
||||||
|
"board_id": column.BoardID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *columnRepository) GetColumnByID(ctx context.Context, id string) (*Column, error) {
|
||||||
|
column, err := r.ormRepo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get column by ID", map[string]interface{}{
|
||||||
|
"column_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return column, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *columnRepository) GetColumnsByBoardID(ctx context.Context, boardID string) ([]Column, error) {
|
||||||
|
filter := bson.M{"board_id": boardID}
|
||||||
|
pagination := orm.Pagination{
|
||||||
|
SortField: "order",
|
||||||
|
SortOrder: 1, // Ascending order
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get columns by board ID", map[string]interface{}{
|
||||||
|
"board_id": boardID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *columnRepository) UpdateColumn(ctx context.Context, column *Column) error {
|
||||||
|
column.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
err := r.ormRepo.Update(ctx, column)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to update column", map[string]interface{}{
|
||||||
|
"column_id": column.ID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Column updated successfully", map[string]interface{}{
|
||||||
|
"column_id": column.ID,
|
||||||
|
"name": column.Name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *columnRepository) DeleteColumn(ctx context.Context, id string) error {
|
||||||
|
err := r.ormRepo.Delete(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to delete column", map[string]interface{}{
|
||||||
|
"column_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Column deleted successfully", map[string]interface{}{
|
||||||
|
"column_id": id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *columnRepository) UpdateColumnOrder(ctx context.Context, boardID string, columnOrders map[string]int) error {
|
||||||
|
// Update each column's order
|
||||||
|
for columnID, order := range columnOrders {
|
||||||
|
column, err := r.ormRepo.FindByID(ctx, columnID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to find column for order update", map[string]interface{}{
|
||||||
|
"column_id": columnID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
column.Order = order
|
||||||
|
column.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
err = r.ormRepo.Update(ctx, column)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to update column order", map[string]interface{}{
|
||||||
|
"column_id": columnID,
|
||||||
|
"order": order,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Column orders updated successfully", map[string]interface{}{
|
||||||
|
"board_id": boardID,
|
||||||
|
"count": len(columnOrders),
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardRepository implementation
|
||||||
|
|
||||||
|
func (r *cardRepository) CreateCard(ctx context.Context, card *Card) error {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
card.SetCreatedAt(now)
|
||||||
|
|
||||||
|
err := r.ormRepo.Create(ctx, card)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to create card", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"title": card.Title,
|
||||||
|
"tender_id": card.TenderID,
|
||||||
|
"column_id": card.ColumnID,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Card created successfully", map[string]interface{}{
|
||||||
|
"card_id": card.ID,
|
||||||
|
"title": card.Title,
|
||||||
|
"tender_id": card.TenderID,
|
||||||
|
"column_id": card.ColumnID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cardRepository) GetCardByID(ctx context.Context, id string) (*Card, error) {
|
||||||
|
card, err := r.ormRepo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get card by ID", map[string]interface{}{
|
||||||
|
"card_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return card, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cardRepository) GetCardsByColumnID(ctx context.Context, columnID string) ([]Card, error) {
|
||||||
|
filter := bson.M{"column_id": columnID}
|
||||||
|
pagination := orm.Pagination{
|
||||||
|
SortField: "order",
|
||||||
|
SortOrder: 1, // Ascending order
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get cards by column ID", map[string]interface{}{
|
||||||
|
"column_id": columnID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cardRepository) GetCardsByTenderID(ctx context.Context, tenderID string) ([]Card, error) {
|
||||||
|
filter := bson.M{"tender_id": tenderID}
|
||||||
|
pagination := orm.Pagination{
|
||||||
|
SortField: "created_at",
|
||||||
|
SortOrder: -1, // Newest first
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get cards by tender ID", map[string]interface{}{
|
||||||
|
"tender_id": tenderID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cardRepository) UpdateCard(ctx context.Context, card *Card) error {
|
||||||
|
card.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
err := r.ormRepo.Update(ctx, card)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to update card", map[string]interface{}{
|
||||||
|
"card_id": card.ID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Card updated successfully", map[string]interface{}{
|
||||||
|
"card_id": card.ID,
|
||||||
|
"title": card.Title,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cardRepository) DeleteCard(ctx context.Context, id string) error {
|
||||||
|
err := r.ormRepo.Delete(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to delete card", map[string]interface{}{
|
||||||
|
"card_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Card deleted successfully", map[string]interface{}{
|
||||||
|
"card_id": id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cardRepository) MoveCard(ctx context.Context, cardID, targetColumnID string, newOrder int) error {
|
||||||
|
card, err := r.ormRepo.FindByID(ctx, cardID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to find card for move", map[string]interface{}{
|
||||||
|
"card_id": cardID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldColumnID := card.ColumnID
|
||||||
|
oldOrder := card.Order
|
||||||
|
|
||||||
|
// Update card position
|
||||||
|
card.MoveToColumn(targetColumnID, newOrder)
|
||||||
|
|
||||||
|
err = r.ormRepo.Update(ctx, card)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to move card", map[string]interface{}{
|
||||||
|
"card_id": cardID,
|
||||||
|
"target_column_id": targetColumnID,
|
||||||
|
"new_order": newOrder,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Card moved successfully", map[string]interface{}{
|
||||||
|
"card_id": cardID,
|
||||||
|
"old_column_id": oldColumnID,
|
||||||
|
"target_column_id": targetColumnID,
|
||||||
|
"old_order": oldOrder,
|
||||||
|
"new_order": newOrder,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cardRepository) UpdateCardOrder(ctx context.Context, columnID string, cardOrders map[string]int) error {
|
||||||
|
// Update each card's order
|
||||||
|
for cardID, order := range cardOrders {
|
||||||
|
// Find the card
|
||||||
|
card, err := r.ormRepo.FindByID(ctx, cardID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to find card for order update", map[string]interface{}{
|
||||||
|
"card_id": cardID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the order
|
||||||
|
card.Order = order
|
||||||
|
card.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
// Save the card
|
||||||
|
err = r.ormRepo.Update(ctx, card)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to update card order", map[string]interface{}{
|
||||||
|
"card_id": cardID,
|
||||||
|
"order": order,
|
||||||
|
"column_id": columnID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Card orders updated successfully", map[string]interface{}{
|
||||||
|
"column_id": columnID,
|
||||||
|
"count": len(cardOrders),
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package kanban
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationService defines validation methods for kanban operations
|
||||||
|
type ValidationService interface {
|
||||||
|
IsValidHexColor(hexColor string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// validationService implements the ValidationService interface
|
||||||
|
type validationService struct {
|
||||||
|
hexColorRegex *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidationService creates a new validation service
|
||||||
|
func NewValidationService() ValidationService {
|
||||||
|
// Compile the hex color regex pattern (supports #RGB, #RRGGBB, #RRGGBBAA)
|
||||||
|
hexColorRegex := regexp.MustCompile(`^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$`)
|
||||||
|
|
||||||
|
// Register custom validator with govalidator
|
||||||
|
govalidator.CustomTypeTagMap.Set("hexcolor", govalidator.CustomTypeValidator(func(i interface{}, o interface{}) bool {
|
||||||
|
hexColor, ok := i.(string)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hexColorRegex.MatchString(hexColor)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return &validationService{
|
||||||
|
hexColorRegex: hexColorRegex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidHexColor checks if a string is a valid hex color code
|
||||||
|
func (v *validationService) IsValidHexColor(hexColor string) bool {
|
||||||
|
return v.hexColorRegex.MatchString(hexColor)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user