package api import ( "context" "fmt" "log" "net/url" "sort" "sync" "time" "task-dashboard/internal/models" ) const trelloBaseURL = "https://api.trello.com/1" // TrelloClient handles interactions with the Trello API type TrelloClient struct { BaseClient apiKey string token string } // NewTrelloClient creates a new Trello API client func NewTrelloClient(apiKey, token string) *TrelloClient { return &TrelloClient{ BaseClient: NewBaseClient(trelloBaseURL), apiKey: apiKey, token: token, } } func (c *TrelloClient) authParams() url.Values { params := url.Values{} params.Set("key", c.apiKey) params.Set("token", c.token) return params } // trelloBoardResponse represents a board from the API type trelloBoardResponse struct { ID string `json:"id"` Name string `json:"name"` } // trelloCardResponse represents a card from the API type trelloCardResponse struct { ID string `json:"id"` Name string `json:"name"` IDList string `json:"idList"` Due *string `json:"due"` URL string `json:"url"` Desc string `json:"desc"` IDBoard string `json:"idBoard"` } // trelloListResponse represents a list from the API type trelloListResponse struct { ID string `json:"id"` Name string `json:"name"` } // GetBoards fetches all boards for the authenticated user func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) { params := c.authParams() params.Set("filter", "open") params.Set("fields", "id,name") var apiBoards []trelloBoardResponse if err := c.Get(ctx, "/members/me/boards?"+params.Encode(), nil, &apiBoards); err != nil { return nil, fmt.Errorf("failed to fetch boards: %w", err) } boards := make([]models.Board, 0, len(apiBoards)) for _, apiBoard := range apiBoards { boards = append(boards, models.Board{ ID: apiBoard.ID, Name: apiBoard.Name, Cards: []models.Card{}, }) } return boards, nil } // GetCards fetches all cards for a specific board func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.Card, error) { params := c.authParams() params.Set("filter", "open") params.Set("fields", "id,name,idList,due,url,idBoard") var apiCards []trelloCardResponse path := fmt.Sprintf("/boards/%s/cards?%s", boardID, params.Encode()) if err := c.Get(ctx, path, nil, &apiCards); err != nil { return nil, fmt.Errorf("failed to fetch cards: %w", err) } if len(apiCards) == 0 { log.Printf("Trello GetCards: board %s returned 0 cards (may have only archived cards)", boardID) } else { log.Printf("Trello GetCards: board %s returned %d cards", boardID, len(apiCards)) } // Fetch lists to get list names lists, err := c.getLists(ctx, boardID) listMap := make(map[string]string) if err != nil { log.Printf("Warning: failed to fetch lists for board %s: %v", boardID, err) } else { for _, list := range lists { listMap[list.ID] = list.Name } } cards := make([]models.Card, 0, len(apiCards)) for _, apiCard := range apiCards { card := models.Card{ ID: apiCard.ID, Name: apiCard.Name, ListID: apiCard.IDList, ListName: listMap[apiCard.IDList], URL: apiCard.URL, } if apiCard.Due != nil && *apiCard.Due != "" { if dueDate, err := time.Parse(time.RFC3339, *apiCard.Due); err == nil { card.DueDate = &dueDate } } cards = append(cards, card) } return cards, nil } // getLists fetches lists for a board func (c *TrelloClient) getLists(ctx context.Context, boardID string) ([]models.List, error) { params := c.authParams() params.Set("fields", "id,name") var apiLists []trelloListResponse path := fmt.Sprintf("/boards/%s/lists?%s", boardID, params.Encode()) if err := c.Get(ctx, path, nil, &apiLists); err != nil { return nil, fmt.Errorf("failed to fetch lists: %w", err) } lists := make([]models.List, 0, len(apiLists)) for _, apiList := range apiLists { lists = append(lists, models.List{ ID: apiList.ID, Name: apiList.Name, }) } return lists, nil } // GetLists fetches lists for a specific board func (c *TrelloClient) GetLists(ctx context.Context, boardID string) ([]models.List, error) { return c.getLists(ctx, boardID) } // GetBoardsWithCards fetches all boards and their cards concurrently func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board, error) { boards, err := c.GetBoards(ctx) if err != nil { return nil, err } var wg sync.WaitGroup sem := make(chan struct{}, 5) // Limit to 5 concurrent requests for i := range boards { wg.Add(1) go func(i int) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() cards, err := c.GetCards(ctx, boards[i].ID) if err != nil { log.Printf("Error fetching cards for board %s (%s): %v", boards[i].Name, boards[i].ID, err) } else { for j := range cards { cards[j].BoardName = boards[i].Name } boards[i].Cards = cards } lists, err := c.getLists(ctx, boards[i].ID) if err != nil { log.Printf("Error fetching lists for board %s: %v", boards[i].Name, err) } else { boards[i].Lists = lists } }(i) } wg.Wait() // Sort boards: Non-empty boards first, newest card activity, then alphabetical sort.Slice(boards, func(i, j int) bool { hasCardsI := len(boards[i].Cards) > 0 hasCardsJ := len(boards[j].Cards) > 0 if hasCardsI != hasCardsJ { return hasCardsI } if hasCardsI && hasCardsJ { maxIDI := "" for _, card := range boards[i].Cards { if maxIDI == "" || card.ID > maxIDI { maxIDI = card.ID } } maxIDJ := "" for _, card := range boards[j].Cards { if maxIDJ == "" || card.ID > maxIDJ { maxIDJ = card.ID } } if maxIDI != maxIDJ { return maxIDI > maxIDJ } } return boards[i].Name < boards[j].Name }) return boards, nil } // CreateCard creates a new card in the specified list func (c *TrelloClient) CreateCard(ctx context.Context, listID, name, description string, dueDate *time.Time) (*models.Card, error) { data := c.authParams() data.Set("idList", listID) data.Set("name", name) if description != "" { data.Set("desc", description) } if dueDate != nil { data.Set("due", dueDate.Format(time.RFC3339)) } var apiCard trelloCardResponse if err := c.PostForm(ctx, "/cards", nil, data.Encode(), &apiCard); err != nil { return nil, fmt.Errorf("failed to create card: %w", err) } card := &models.Card{ ID: apiCard.ID, Name: apiCard.Name, ListID: apiCard.IDList, URL: apiCard.URL, } if apiCard.Due != nil && *apiCard.Due != "" { if parsedDate, err := time.Parse(time.RFC3339, *apiCard.Due); err == nil { card.DueDate = &parsedDate } } return card, nil } // UpdateCard updates a card with the specified changes func (c *TrelloClient) UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error { data := c.authParams() for key, value := range updates { data.Set(key, fmt.Sprintf("%v", value)) } path := fmt.Sprintf("/cards/%s", cardID) if err := c.Put(ctx, path, nil, data.Encode(), nil); err != nil { return fmt.Errorf("failed to update card: %w", err) } return nil }