fix(oo): assign owner and deadline on task create (#9)
* fix(oo): assign owner and deadline on task create CRM and project task create omitted responsibleId/responsibles, so the portal attached a removed stub profile and CRM create without a deadline failed or looked empty in the UI. Default both to the authenticated user and now+14d; add crm-tasks reassign-self for backfill. * fix(oo): collect all bad CRM tasks before reassign-self apply Offset-0 apply stopped early when the first page was already fixed.
This commit is contained in:
committed by
GitHub
co-authored by
GitHub
parent
e3c706aefe
commit
b9cea62d46
@@ -171,10 +171,15 @@ func CellText(it Item, key string) string {
|
|||||||
if it.Raw == nil {
|
if it.Raw == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if rel := FormatRelativeDeadline(it.Raw["deadline"]); rel != "" {
|
// CRM tasks use camelCase deadLine; project tasks use deadline.
|
||||||
|
rawDL := it.Raw["deadline"]
|
||||||
|
if rawDL == nil || formatAny(rawDL) == "" {
|
||||||
|
rawDL = it.Raw["deadLine"]
|
||||||
|
}
|
||||||
|
if rel := FormatRelativeDeadline(rawDL); rel != "" {
|
||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
return formatAny(it.Raw["deadline"])
|
return formatAny(rawDL)
|
||||||
case "responsible":
|
case "responsible":
|
||||||
if it.Kind == KindTask || it.Kind == KindCRMTask {
|
if it.Kind == KindTask || it.Kind == KindCRMTask {
|
||||||
return TaskResponsibleLabel(it.Raw)
|
return TaskResponsibleLabel(it.Raw)
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ func personName(v any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if name := strRaw(m, "displayName"); name != "" {
|
if name := strRaw(m, "displayName"); name != "" {
|
||||||
|
if strings.EqualFold(name, "Profile has been removed") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
first := strRaw(m, "firstName")
|
first := strRaw(m, "firstName")
|
||||||
|
|||||||
+103
-3
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -17,6 +18,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(crmTasksCmd)
|
rootCmd.AddCommand(crmTasksCmd)
|
||||||
crmTasksCmd.AddCommand(crmTasksListCmd())
|
crmTasksCmd.AddCommand(crmTasksListCmd())
|
||||||
crmTasksCmd.AddCommand(crmTasksCreateCmd())
|
crmTasksCmd.AddCommand(crmTasksCreateCmd())
|
||||||
|
crmTasksCmd.AddCommand(crmTasksReassignSelfCmd())
|
||||||
crmTasksCmd.AddCommand(crmTasksDeleteCmd())
|
crmTasksCmd.AddCommand(crmTasksDeleteCmd())
|
||||||
crmTasksCmd.AddCommand(crmTasksCategoriesCmd())
|
crmTasksCmd.AddCommand(crmTasksCategoriesCmd())
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,7 @@ func crmTasksListCmd() *cobra.Command {
|
|||||||
if outputFormat == "table" {
|
if outputFormat == "table" {
|
||||||
fmt.Printf("total: %d (shown: %d)\n", total, len(list))
|
fmt.Printf("total: %d (shown: %d)\n", total, len(list))
|
||||||
}
|
}
|
||||||
printTable([]string{"id", "title", "deadline", "isClosed", "categoryID"}, list)
|
printTable([]string{"id", "title", "deadLine", "isClosed", "categoryID"}, list)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -53,7 +55,7 @@ func crmTasksCreateCmd() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "create TITLE",
|
Use: "create TITLE",
|
||||||
Aliases: []string{"add"},
|
Aliases: []string{"add"},
|
||||||
Short: "Create a CRM task",
|
Short: "Create a CRM task (assigns self + deadline default now+14d)",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
c, err := newOO(cmd)
|
c, err := newOO(cmd)
|
||||||
@@ -68,7 +70,7 @@ func crmTasksCreateCmd() *cobra.Command {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&deadline, "deadline", "", "deadline (YYYY-MM-DD or ISO8601)")
|
cmd.Flags().StringVar(&deadline, "deadline", "", "deadline (YYYY-MM-DD or ISO8601); default now+14d")
|
||||||
cmd.Flags().IntVar(&categoryID, "category", 0, "category id (see `oo crm-tasks categories`)")
|
cmd.Flags().IntVar(&categoryID, "category", 0, "category id (see `oo crm-tasks categories`)")
|
||||||
cmd.Flags().IntVar(&contactID, "contact", 0, "contact id")
|
cmd.Flags().IntVar(&contactID, "contact", 0, "contact id")
|
||||||
cmd.Flags().StringVar(&entityType, "entity-type", "", "opportunity|case|contact")
|
cmd.Flags().StringVar(&entityType, "entity-type", "", "opportunity|case|contact")
|
||||||
@@ -77,6 +79,104 @@ func crmTasksCreateCmd() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func crmTaskNeedsOwner(t map[string]any) (need bool, name string) {
|
||||||
|
resp, _ := t["responsible"].(map[string]any)
|
||||||
|
rid, _ := resp["id"].(string)
|
||||||
|
name, _ = resp["displayName"].(string)
|
||||||
|
need = rid == "" || strings.EqualFold(name, "Profile has been removed")
|
||||||
|
return need, name
|
||||||
|
}
|
||||||
|
|
||||||
|
func crmTaskCategoryID(t map[string]any) int {
|
||||||
|
cat, ok := t["category"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch v := cat["id"].(type) {
|
||||||
|
case float64:
|
||||||
|
return int(v)
|
||||||
|
case int:
|
||||||
|
return v
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func crmTasksReassignSelfCmd() *cobra.Command {
|
||||||
|
var apply bool
|
||||||
|
var count, max int
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "reassign-self",
|
||||||
|
Short: "Reassign CRM tasks with missing/removed owner to the authenticated user",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
uid, err := c.SelfUserID(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
id, title, deadline, ownerName string
|
||||||
|
categoryID int
|
||||||
|
}
|
||||||
|
var todos []item
|
||||||
|
scanned := 0
|
||||||
|
for start := 0; ; start += count {
|
||||||
|
list, total, err := c.ListCRMTasks(cmd.Context(), count, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, t := range list {
|
||||||
|
scanned++
|
||||||
|
need, name := crmTaskNeedsOwner(t)
|
||||||
|
if !need {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
title, _ := t["title"].(string)
|
||||||
|
dl, _ := t["deadLine"].(string)
|
||||||
|
todos = append(todos, item{
|
||||||
|
id: fmt.Sprint(t["id"]), title: title, deadline: dl,
|
||||||
|
ownerName: name, categoryID: crmTaskCategoryID(t),
|
||||||
|
})
|
||||||
|
if max > 0 && len(todos) >= max {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (max > 0 && len(todos) >= max) || start+count >= total || len(list) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed := 0
|
||||||
|
for _, t := range todos {
|
||||||
|
fmt.Printf(" task %s %q owner=%q → %s\n", t.id, t.title, t.ownerName, uid)
|
||||||
|
if !apply {
|
||||||
|
fixed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := c.UpdateCRMTask(cmd.Context(), t.id, t.title, t.deadline, t.categoryID, uid); err != nil {
|
||||||
|
return fmt.Errorf("update %s: %w", t.id, err)
|
||||||
|
}
|
||||||
|
fixed++
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := "DRY-RUN"
|
||||||
|
if apply {
|
||||||
|
mode = "APPLIED"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: scanned=%d reassigned=%d (self=%s)\n", mode, scanned, fixed, uid)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().BoolVar(&apply, "apply", false, "write changes (default dry-run)")
|
||||||
|
cmd.Flags().IntVar(&count, "count", 100, "page size")
|
||||||
|
cmd.Flags().IntVar(&max, "max", 0, "stop after N candidates (0 = all)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
func crmTasksDeleteCmd() *cobra.Command {
|
func crmTasksDeleteCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "delete TASK_ID [TASK_ID...]",
|
Use: "delete TASK_ID [TASK_ID...]",
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@ func taskCreateCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(&project, "project", "p", "", "project id (default $OO_PROJECT_ID)")
|
cmd.Flags().StringVarP(&project, "project", "p", "", "project id (default $OO_PROJECT_ID)")
|
||||||
cmd.Flags().StringVar(&desc, "description", "", "description")
|
cmd.Flags().StringVar(&desc, "description", "", "description")
|
||||||
cmd.Flags().StringVar(&deadline, "deadline", "", "deadline YYYY-MM-DD")
|
cmd.Flags().StringVar(&deadline, "deadline", "", "deadline YYYY-MM-DD (default now+14d; always assigned to you)")
|
||||||
cmd.Flags().StringVar(&prio, "priority", "normal", "high|normal|low")
|
cmd.Flags().StringVar(&prio, "priority", "normal", "high|normal|low")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListContacts returns a page of CRM contacts and the total count.
|
// ListContacts returns a page of CRM contacts and the total count.
|
||||||
@@ -491,11 +492,25 @@ func (c *Client) ListCRMTasks(ctx context.Context, count, startIndex int) ([]map
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateCRMTask creates a CRM task (reminder) attached to an entity.
|
// CreateCRMTask creates a CRM task (reminder) attached to an entity.
|
||||||
|
// Empty deadline defaults to now+14d (API requires a deadline). Empty
|
||||||
|
// responsibleID falls back to the authenticated user — without this the
|
||||||
|
// portal assigns a stub "Profile has been removed" owner.
|
||||||
func (c *Client) CreateCRMTask(ctx context.Context, title, deadline string, categoryID, contactID int, entityType string, entityID int, description string) (map[string]any, error) {
|
func (c *Client) CreateCRMTask(ctx context.Context, title, deadline string, categoryID, contactID int, entityType string, entityID int, description string) (map[string]any, error) {
|
||||||
|
if deadline == "" {
|
||||||
|
deadline = time.Now().Add(14 * 24 * time.Hour).Format("2006-01-02T15:04:05")
|
||||||
|
}
|
||||||
|
if categoryID == 0 {
|
||||||
|
categoryID = 2 // Opportunity — matches applications sync
|
||||||
|
}
|
||||||
|
uid, err := c.SelfUserID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("CreateCRMTask: resolve self: %w", err)
|
||||||
|
}
|
||||||
fields := url.Values{}
|
fields := url.Values{}
|
||||||
fields.Set("title", title)
|
fields.Set("title", title)
|
||||||
fields.Set("deadline", deadline)
|
fields.Set("deadline", deadline)
|
||||||
fields.Set("categoryId", strconv.Itoa(categoryID))
|
fields.Set("categoryId", strconv.Itoa(categoryID))
|
||||||
|
fields.Set("responsibleId", uid)
|
||||||
if contactID != 0 {
|
if contactID != 0 {
|
||||||
fields.Set("contactId", strconv.Itoa(contactID))
|
fields.Set("contactId", strconv.Itoa(contactID))
|
||||||
}
|
}
|
||||||
@@ -511,6 +526,29 @@ func (c *Client) CreateCRMTask(ctx context.Context, title, deadline string, cate
|
|||||||
return c.postFormObject(ctx, "/api/2.0/crm/task.json", fields)
|
return c.postFormObject(ctx, "/api/2.0/crm/task.json", fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateCRMTask updates title, deadline, category, and responsible on a CRM task.
|
||||||
|
// Empty responsibleID falls back to SelfUserID; empty deadline is left unchanged
|
||||||
|
// by the API only if omitted — callers should pass deadLine when known.
|
||||||
|
func (c *Client) UpdateCRMTask(ctx context.Context, id, title, deadline string, categoryID int, responsibleID string) (map[string]any, error) {
|
||||||
|
if responsibleID == "" {
|
||||||
|
uid, err := c.SelfUserID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UpdateCRMTask: resolve self: %w", err)
|
||||||
|
}
|
||||||
|
responsibleID = uid
|
||||||
|
}
|
||||||
|
fields := url.Values{}
|
||||||
|
fields.Set("title", title)
|
||||||
|
fields.Set("responsibleId", responsibleID)
|
||||||
|
if deadline != "" {
|
||||||
|
fields.Set("deadline", deadline)
|
||||||
|
}
|
||||||
|
if categoryID != 0 {
|
||||||
|
fields.Set("categoryId", strconv.Itoa(categoryID))
|
||||||
|
}
|
||||||
|
return c.putFormObject(ctx, fmt.Sprintf("/api/2.0/crm/task/%s.json", url.PathEscape(id)), fields)
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteCRMTask removes a CRM task by id.
|
// DeleteCRMTask removes a CRM task by id.
|
||||||
func (c *Client) DeleteCRMTask(ctx context.Context, id string) (map[string]any, error) {
|
func (c *Client) DeleteCRMTask(ctx context.Context, id string) (map[string]any, error) {
|
||||||
return c.deleteObject(ctx, fmt.Sprintf("/api/2.0/crm/task/%s.json", url.PathEscape(id)))
|
return c.deleteObject(ctx, fmt.Sprintf("/api/2.0/crm/task/%s.json", url.PathEscape(id)))
|
||||||
|
|||||||
@@ -233,17 +233,26 @@ func (c *Client) GetTaskByID(ctx context.Context, taskID string) (map[string]any
|
|||||||
// AddTask creates a task via the form-encoded endpoint (no milestone/start).
|
// AddTask creates a task via the form-encoded endpoint (no milestone/start).
|
||||||
// Prefer the typed CreateProjectTask for new code; AddTask is kept for
|
// Prefer the typed CreateProjectTask for new code; AddTask is kept for
|
||||||
// parity with the Python reference tooling.
|
// parity with the Python reference tooling.
|
||||||
|
//
|
||||||
|
// Always assigns the authenticated user as responsible (form field
|
||||||
|
// "responsibles"). Empty deadline defaults to today+14 days.
|
||||||
func (c *Client) AddTask(ctx context.Context, projectID, title, description string, priority int, deadline string) (map[string]any, error) {
|
func (c *Client) AddTask(ctx context.Context, projectID, title, description string, priority int, deadline string) (map[string]any, error) {
|
||||||
if projectID == "" {
|
if projectID == "" {
|
||||||
projectID = c.defaults.ProjectID
|
projectID = c.defaults.ProjectID
|
||||||
}
|
}
|
||||||
|
if deadline == "" {
|
||||||
|
deadline = time.Now().Add(14 * 24 * time.Hour).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
uid, err := c.SelfUserID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AddTask: resolve self: %w", err)
|
||||||
|
}
|
||||||
fields := url.Values{}
|
fields := url.Values{}
|
||||||
fields.Set("title", title)
|
fields.Set("title", title)
|
||||||
fields.Set("description", description)
|
fields.Set("description", description)
|
||||||
fields.Set("priority", strconv.Itoa(priority))
|
fields.Set("priority", strconv.Itoa(priority))
|
||||||
if deadline != "" {
|
fields.Set("deadline", deadline)
|
||||||
fields.Set("deadline", deadline)
|
fields.Set("responsibles", uid)
|
||||||
}
|
|
||||||
return c.postFormObject(ctx, fmt.Sprintf("/api/2.0/project/%s/task.json", url.PathEscape(projectID)), fields)
|
return c.postFormObject(ctx, fmt.Sprintf("/api/2.0/project/%s/task.json", url.PathEscape(projectID)), fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user