mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-20 00:01:21 +00:00
feat: add CRM task team assignment
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS idx_crm_tasks_assigned_team;
|
||||
|
||||
ALTER TABLE crm_tasks
|
||||
DROP COLUMN IF EXISTS assigned_team_id;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- A CRM task can be assigned to an individual user (assigned_to) AND/OR a team.
|
||||
-- The two are independent: a task may have a person, a team, both, or neither.
|
||||
-- ON DELETE SET NULL mirrors the soft-clearing behaviour of the user assignment:
|
||||
-- deleting the team unassigns the task rather than deleting it.
|
||||
ALTER TABLE crm_tasks
|
||||
ADD COLUMN assigned_team_id uuid REFERENCES teams (id) ON DELETE SET NULL;
|
||||
|
||||
-- Filtering and counting by team + status is the hot path for the tasks views
|
||||
-- (team filter + status facets share the search/summary predicate).
|
||||
CREATE INDEX idx_crm_tasks_assigned_team ON crm_tasks (assigned_team_id, status);
|
||||
+31
-27
@@ -319,6 +319,7 @@ type CRMTask struct {
|
||||
ContactID *uuid.UUID `json:"contact_id,omitempty"`
|
||||
DealID *uuid.UUID `json:"deal_id,omitempty"`
|
||||
AssignedTo *uuid.UUID `json:"assigned_to,omitempty"`
|
||||
AssignedTeamID *uuid.UUID `json:"assigned_team_id,omitempty"`
|
||||
CreatedBy uuid.UUID `json:"created_by"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
@@ -337,24 +338,26 @@ type CRMTasksResult struct {
|
||||
}
|
||||
|
||||
type CreateCRMTask struct {
|
||||
ContactID *uuid.UUID `json:"contact_id,omitempty"`
|
||||
DealID *uuid.UUID `json:"deal_id,omitempty"`
|
||||
AssignedTo *uuid.UUID `json:"assigned_to,omitempty"`
|
||||
Title string `json:"title" binding:"required,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
ContactID *uuid.UUID `json:"contact_id,omitempty"`
|
||||
DealID *uuid.UUID `json:"deal_id,omitempty"`
|
||||
AssignedTo *uuid.UUID `json:"assigned_to,omitempty"`
|
||||
AssignedTeamID *uuid.UUID `json:"assigned_team_id,omitempty"`
|
||||
Title string `json:"title" binding:"required,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCRMTask struct {
|
||||
AssignedTo *uuid.UUID `json:"assigned_to,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Priority *string `json:"priority,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
AssignedTo *uuid.UUID `json:"assigned_to,omitempty"`
|
||||
AssignedTeamID *uuid.UUID `json:"assigned_team_id,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Priority *string `json:"priority,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// =====================
|
||||
@@ -367,18 +370,19 @@ type UpdateCRMTask struct {
|
||||
// that makes the tasks view correct at scale instead of paging a cursor and
|
||||
// reducing client-side.
|
||||
type SearchTasks struct {
|
||||
Query string `json:"query"` // title ILIKE
|
||||
Statuses []string `json:"statuses"` // pending | in_progress | completed | cancelled (any of)
|
||||
Priorities []string `json:"priorities"` // low | medium | high | urgent (any of)
|
||||
Types []string `json:"types"` // task type NAME is any of
|
||||
AssignedTo []string `json:"assigned_to"` // assignee user id is any of
|
||||
ContactID *string `json:"contact_id"` // linked contact
|
||||
DealID *string `json:"deal_id"` // linked deal
|
||||
DueAfter *time.Time `json:"due_after"` // due_date >=
|
||||
DueBefore *time.Time `json:"due_before"` // due_date <=
|
||||
Overdue bool `json:"overdue"` // due_date < now() AND not completed/cancelled
|
||||
SortBy string `json:"sort_by"` // created_at|due_date|priority|title|updated_at
|
||||
Reverse bool `json:"reverse"` // true = ASC, false = DESC (default)
|
||||
Query string `json:"query"` // title ILIKE
|
||||
Statuses []string `json:"statuses"` // pending | in_progress | completed | cancelled (any of)
|
||||
Priorities []string `json:"priorities"` // low | medium | high | urgent (any of)
|
||||
Types []string `json:"types"` // task type NAME is any of
|
||||
AssignedTo []string `json:"assigned_to"` // assignee user id is any of
|
||||
TeamIDs []uuid.UUID `json:"team_ids"` // task team is any of, OR assignee is a member of any of
|
||||
ContactID *string `json:"contact_id"` // linked contact
|
||||
DealID *string `json:"deal_id"` // linked deal
|
||||
DueAfter *time.Time `json:"due_after"` // due_date >=
|
||||
DueBefore *time.Time `json:"due_before"` // due_date <=
|
||||
Overdue bool `json:"overdue"` // due_date < now() AND not completed/cancelled
|
||||
SortBy string `json:"sort_by"` // created_at|due_date|priority|title|updated_at
|
||||
Reverse bool `json:"reverse"` // true = ASC, false = DESC (default)
|
||||
}
|
||||
|
||||
// TasksSearchResult is the offset-paginated result of POST /crm/tasks/search.
|
||||
|
||||
@@ -1071,18 +1071,18 @@ func (r *crmRepository) CreateCRMTask(ctx context.Context, orgID, userID uuid.UU
|
||||
taskType := data.Type
|
||||
|
||||
query := `
|
||||
INSERT INTO crm_tasks (organization_id, contact_id, deal_id, assigned_to, created_by, title, description, due_date, priority, type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, organization_id, contact_id, deal_id, assigned_to, created_by, title, description,
|
||||
INSERT INTO crm_tasks (organization_id, contact_id, deal_id, assigned_to, assigned_team_id, created_by, title, description, due_date, priority, type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING id, organization_id, contact_id, deal_id, assigned_to, assigned_team_id, created_by, title, description,
|
||||
due_date, priority, type, status, completed_at, created_at, updated_at
|
||||
`
|
||||
var task models.CRMTask
|
||||
err := r.db.QueryRow(ctx, query,
|
||||
orgID, data.ContactID, data.DealID, data.AssignedTo, userID,
|
||||
orgID, data.ContactID, data.DealID, data.AssignedTo, data.AssignedTeamID, userID,
|
||||
data.Title, data.Description, data.DueDate, priority, taskType,
|
||||
).Scan(
|
||||
&task.ID, &task.OrganizationID, &task.ContactID, &task.DealID,
|
||||
&task.AssignedTo, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.AssignedTo, &task.AssignedTeamID, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.DueDate, &task.Priority, &task.Type, &task.Status, &task.CompletedAt,
|
||||
&task.CreatedAt, &task.UpdatedAt,
|
||||
)
|
||||
@@ -1094,7 +1094,7 @@ func (r *crmRepository) CreateCRMTask(ctx context.Context, orgID, userID uuid.UU
|
||||
|
||||
func (r *crmRepository) GetCRMTask(ctx context.Context, orgID, taskID uuid.UUID) (*models.CRMTask, error) {
|
||||
query := `
|
||||
SELECT id, organization_id, contact_id, deal_id, assigned_to, created_by, title, description,
|
||||
SELECT id, organization_id, contact_id, deal_id, assigned_to, assigned_team_id, created_by, title, description,
|
||||
due_date, priority, type, status, completed_at, created_at, updated_at
|
||||
FROM crm_tasks
|
||||
WHERE organization_id = $1 AND id = $2
|
||||
@@ -1102,7 +1102,7 @@ func (r *crmRepository) GetCRMTask(ctx context.Context, orgID, taskID uuid.UUID)
|
||||
var task models.CRMTask
|
||||
err := r.db.QueryRow(ctx, query, orgID, taskID).Scan(
|
||||
&task.ID, &task.OrganizationID, &task.ContactID, &task.DealID,
|
||||
&task.AssignedTo, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.AssignedTo, &task.AssignedTeamID, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.DueDate, &task.Priority, &task.Type, &task.Status, &task.CompletedAt,
|
||||
&task.CreatedAt, &task.UpdatedAt,
|
||||
)
|
||||
@@ -1148,7 +1148,7 @@ func (r *crmRepository) ListCRMTasks(ctx context.Context, orgID uuid.UUID, conta
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT id, organization_id, contact_id, deal_id, assigned_to, created_by, title, description,
|
||||
SELECT id, organization_id, contact_id, deal_id, assigned_to, assigned_team_id, created_by, title, description,
|
||||
due_date, priority, type, status, completed_at, created_at, updated_at
|
||||
FROM crm_tasks
|
||||
WHERE %s
|
||||
@@ -1168,7 +1168,7 @@ func (r *crmRepository) ListCRMTasks(ctx context.Context, orgID uuid.UUID, conta
|
||||
var task models.CRMTask
|
||||
if err := rows.Scan(
|
||||
&task.ID, &task.OrganizationID, &task.ContactID, &task.DealID,
|
||||
&task.AssignedTo, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.AssignedTo, &task.AssignedTeamID, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.DueDate, &task.Priority, &task.Type, &task.Status, &task.CompletedAt,
|
||||
&task.CreatedAt, &task.UpdatedAt,
|
||||
); err != nil {
|
||||
@@ -1226,6 +1226,19 @@ func taskSearchWhere(orgID uuid.UUID, f models.SearchTasks) ([]string, []any) {
|
||||
appendIn("type", f.Types)
|
||||
appendIn("assigned_to", f.AssignedTo)
|
||||
|
||||
// Team filter: a task matches when it is directly assigned to one of the
|
||||
// given teams, OR its individual assignee is a member of one of them. Bound
|
||||
// once as a uuid[] so the same predicate (and the same $N) is reused
|
||||
// identically by the search rows query and the summary aggregate.
|
||||
if len(f.TeamIDs) > 0 {
|
||||
clauses = append(clauses, fmt.Sprintf(
|
||||
"(t.assigned_team_id = ANY($%d) OR t.assigned_to IN (SELECT user_id FROM team_members WHERE team_id = ANY($%d)))",
|
||||
pos, pos,
|
||||
))
|
||||
args = append(args, f.TeamIDs)
|
||||
pos++
|
||||
}
|
||||
|
||||
if f.ContactID != nil {
|
||||
clauses = append(clauses, fmt.Sprintf("t.contact_id = $%d", pos))
|
||||
args = append(args, *f.ContactID)
|
||||
@@ -1299,7 +1312,7 @@ func (r *crmRepository) SearchCRMTasks(ctx context.Context, orgID uuid.UUID, fil
|
||||
limitPos := len(args) + 1
|
||||
offsetPos := len(args) + 2
|
||||
query := fmt.Sprintf(`
|
||||
SELECT t.id, t.organization_id, t.contact_id, t.deal_id, t.assigned_to, t.created_by, t.title, t.description,
|
||||
SELECT t.id, t.organization_id, t.contact_id, t.deal_id, t.assigned_to, t.assigned_team_id, t.created_by, t.title, t.description,
|
||||
t.due_date, t.priority, t.type, t.status, t.completed_at, t.created_at, t.updated_at
|
||||
FROM crm_tasks t
|
||||
WHERE %s
|
||||
@@ -1319,7 +1332,7 @@ func (r *crmRepository) SearchCRMTasks(ctx context.Context, orgID uuid.UUID, fil
|
||||
var task models.CRMTask
|
||||
if err := rows.Scan(
|
||||
&task.ID, &task.OrganizationID, &task.ContactID, &task.DealID,
|
||||
&task.AssignedTo, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.AssignedTo, &task.AssignedTeamID, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.DueDate, &task.Priority, &task.Type, &task.Status, &task.CompletedAt,
|
||||
&task.CreatedAt, &task.UpdatedAt,
|
||||
); err != nil {
|
||||
@@ -1387,6 +1400,11 @@ func (r *crmRepository) UpdateCRMTask(ctx context.Context, orgID, taskID uuid.UU
|
||||
args = append(args, *data.AssignedTo)
|
||||
argPos++
|
||||
}
|
||||
if data.AssignedTeamID != nil {
|
||||
setClauses = append(setClauses, fmt.Sprintf("assigned_team_id = $%d", argPos))
|
||||
args = append(args, *data.AssignedTeamID)
|
||||
argPos++
|
||||
}
|
||||
if data.Title != nil {
|
||||
setClauses = append(setClauses, fmt.Sprintf("title = $%d", argPos))
|
||||
args = append(args, *data.Title)
|
||||
@@ -1431,14 +1449,14 @@ func (r *crmRepository) UpdateCRMTask(ctx context.Context, orgID, taskID uuid.UU
|
||||
query := fmt.Sprintf(`
|
||||
UPDATE crm_tasks SET %s
|
||||
WHERE organization_id = $1 AND id = $2
|
||||
RETURNING id, organization_id, contact_id, deal_id, assigned_to, created_by, title, description,
|
||||
RETURNING id, organization_id, contact_id, deal_id, assigned_to, assigned_team_id, created_by, title, description,
|
||||
due_date, priority, type, status, completed_at, created_at, updated_at
|
||||
`, strings.Join(setClauses, ", "))
|
||||
|
||||
var task models.CRMTask
|
||||
err := r.db.QueryRow(ctx, query, args...).Scan(
|
||||
&task.ID, &task.OrganizationID, &task.ContactID, &task.DealID,
|
||||
&task.AssignedTo, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.AssignedTo, &task.AssignedTeamID, &task.CreatedBy, &task.Title, &task.Description,
|
||||
&task.DueDate, &task.Priority, &task.Type, &task.Status, &task.CompletedAt,
|
||||
&task.CreatedAt, &task.UpdatedAt,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user