feat(crm): complete contact label avatar gaps
This commit is contained in:
@@ -39,29 +39,42 @@ func (r *ContactRepo) DB() *gorm.DB {
|
||||
|
||||
// FindByAccount retrieves all contacts for an account with optional sort.
|
||||
// sort: default "last_activity_at DESC, id DESC", alternatives: "name ASC", "email ASC", "created_at DESC"
|
||||
func (r *ContactRepo) FindByAccount(ctx context.Context, accountID uint, offset, limit int, sort string) ([]model.Contact, int64, error) {
|
||||
func (r *ContactRepo) FindByAccount(ctx context.Context, accountID uint, offset, limit int, sort string, labels ...[]string) ([]model.Contact, int64, error) {
|
||||
var contacts []model.Contact
|
||||
var total int64
|
||||
|
||||
countDB := r.db.WithContext(ctx).Model(&model.Contact{}).Where("account_id = ?", accountID)
|
||||
labelFilter := firstContactLabelFilter(labels)
|
||||
base := r.db.WithContext(ctx).Model(&model.Contact{}).Where("contacts.account_id = ?", accountID)
|
||||
base = applyContactLabelFilter(base, accountID, labelFilter)
|
||||
|
||||
countDB := base
|
||||
if len(labelFilter) > 0 {
|
||||
countDB = countDB.Distinct("contacts.id")
|
||||
}
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
orderClause := resolveContactSort(sort)
|
||||
err := r.db.WithContext(ctx).Where("account_id = ?", accountID).
|
||||
queryDB := base
|
||||
if len(labelFilter) > 0 {
|
||||
queryDB = queryDB.Distinct("contacts.*")
|
||||
}
|
||||
err := queryDB.
|
||||
Offset(offset).Limit(limit).Order(orderClause).
|
||||
Find(&contacts).Error
|
||||
return contacts, total, err
|
||||
}
|
||||
|
||||
// Search searches contacts by name, email, or phone number with optional sort.
|
||||
func (r *ContactRepo) Search(ctx context.Context, accountID uint, query string, offset, limit int, sort string, searchMode search.SearchMode) ([]model.Contact, int64, error) {
|
||||
func (r *ContactRepo) Search(ctx context.Context, accountID uint, query string, offset, limit int, sort string, searchMode search.SearchMode, labels ...[]string) ([]model.Contact, int64, error) {
|
||||
var contacts []model.Contact
|
||||
var total int64
|
||||
|
||||
condition := r.db.WithContext(ctx).Model(&model.Contact{}).
|
||||
Where("account_id = ?", accountID)
|
||||
Where("contacts.account_id = ?", accountID)
|
||||
labelFilter := firstContactLabelFilter(labels)
|
||||
condition = applyContactLabelFilter(condition, accountID, labelFilter)
|
||||
|
||||
if query != "" {
|
||||
if searchMode == search.SearchModeTrigram {
|
||||
@@ -76,31 +89,73 @@ func (r *ContactRepo) Search(ctx context.Context, accountID uint, query string,
|
||||
}
|
||||
}
|
||||
|
||||
if err := condition.Count(&total).Error; err != nil {
|
||||
countDB := condition
|
||||
if len(labelFilter) > 0 {
|
||||
countDB = countDB.Distinct("contacts.id")
|
||||
}
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
orderClause := resolveContactSort(sort)
|
||||
|
||||
err := condition.Offset(offset).Limit(limit).Order(orderClause).
|
||||
queryDB := condition
|
||||
if len(labelFilter) > 0 {
|
||||
queryDB = queryDB.Distinct("contacts.*")
|
||||
}
|
||||
err := queryDB.Offset(offset).Limit(limit).Order(orderClause).
|
||||
Find(&contacts).Error
|
||||
return contacts, total, err
|
||||
}
|
||||
|
||||
func firstContactLabelFilter(filters [][]string) []string {
|
||||
if len(filters) == 0 {
|
||||
return nil
|
||||
}
|
||||
return normalizeContactLabels(filters[0])
|
||||
}
|
||||
|
||||
func normalizeContactLabels(labels []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
result := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
label = strings.TrimSpace(label)
|
||||
if label == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[label]; ok {
|
||||
continue
|
||||
}
|
||||
seen[label] = struct{}{}
|
||||
result = append(result, label)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func applyContactLabelFilter(q *gorm.DB, accountID uint, labels []string) *gorm.DB {
|
||||
labels = normalizeContactLabels(labels)
|
||||
if len(labels) == 0 {
|
||||
return q
|
||||
}
|
||||
return q.Joins("JOIN contact_labels ON contact_labels.contact_id = contacts.id").
|
||||
Joins("JOIN tags ON tags.id = contact_labels.tag_id").
|
||||
Where("contact_labels.account_id = ? AND tags.account_id = ? AND tags.name IN ?", accountID, accountID, labels)
|
||||
}
|
||||
|
||||
// resolveContactSort maps a sort parameter to a SQL ORDER BY clause.
|
||||
// Supported values: "name", "email", "created_at", "last_activity_at" (default).
|
||||
// Direction defaults to DESC for date fields, ASC for name/email.
|
||||
func resolveContactSort(sort string) string {
|
||||
allowedSorts := map[string]string{
|
||||
"name": "name ASC",
|
||||
"email": "email ASC",
|
||||
"created_at": "created_at DESC",
|
||||
"last_activity_at": "last_activity_at DESC NULLS LAST, id DESC",
|
||||
"name": "contacts.name ASC",
|
||||
"email": "contacts.email ASC",
|
||||
"created_at": "contacts.created_at DESC",
|
||||
"last_activity_at": "contacts.last_activity_at DESC NULLS LAST, contacts.id DESC",
|
||||
}
|
||||
if clause, ok := allowedSorts[sort]; ok {
|
||||
return clause
|
||||
}
|
||||
return "last_activity_at DESC NULLS LAST, id DESC"
|
||||
return "contacts.last_activity_at DESC NULLS LAST, contacts.id DESC"
|
||||
}
|
||||
|
||||
// FindByAccountAndID retrieves a contact scoped to an account.
|
||||
@@ -221,7 +276,8 @@ func (r *ContactRepo) Filter(ctx context.Context, accountID uint, params Contact
|
||||
var contacts []model.Contact
|
||||
var total int64
|
||||
|
||||
q := r.db.WithContext(ctx).Model(&model.Contact{}).Where("account_id = ?", accountID)
|
||||
q := r.db.WithContext(ctx).Model(&model.Contact{}).Where("contacts.account_id = ?", accountID)
|
||||
usesDistinct := false
|
||||
|
||||
// Apply contact type filter
|
||||
if params.ContactType != "" {
|
||||
@@ -236,14 +292,13 @@ func (r *ContactRepo) Filter(ctx context.Context, accountID uint, params Contact
|
||||
// Apply inbox filter via contact_inboxes join
|
||||
if params.InboxID != nil {
|
||||
q = q.Joins("JOIN contact_inboxes ON contact_inboxes.contact_id = contacts.id AND contact_inboxes.inbox_id = ?", *params.InboxID)
|
||||
usesDistinct = true
|
||||
}
|
||||
|
||||
// Apply labels filter (contacts with conversations having matching labels)
|
||||
// Apply labels filter (Chatwoot: contact label_list tagged_with any: true)
|
||||
if params.Labels != "" {
|
||||
labelList := strings.Split(params.Labels, ",")
|
||||
for _, label := range labelList {
|
||||
q = q.Joins("JOIN conversations ON conversations.contact_id = contacts.id AND conversations.labels LIKE ?", "%"+label+"%")
|
||||
}
|
||||
q = applyContactLabelFilter(q, accountID, strings.Split(params.Labels, ","))
|
||||
usesDistinct = true
|
||||
}
|
||||
|
||||
// Apply status filter (active = has recent activity, inactive = no recent activity)
|
||||
@@ -260,12 +315,20 @@ func (r *ContactRepo) Filter(ctx context.Context, accountID uint, params Contact
|
||||
}
|
||||
|
||||
// Count total matching records
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
countDB := q
|
||||
if usesDistinct {
|
||||
countDB = countDB.Distinct("contacts.id")
|
||||
}
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
orderClause := resolveContactSort(params.Sort)
|
||||
err := q.Offset(offset).Limit(limit).Order(orderClause).
|
||||
queryDB := q
|
||||
if usesDistinct {
|
||||
queryDB = queryDB.Distinct("contacts.*")
|
||||
}
|
||||
err := queryDB.Offset(offset).Limit(limit).Order(orderClause).
|
||||
Find(&contacts).Error
|
||||
return contacts, total, err
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ func defaultTestModels() []interface{} {
|
||||
&csat.CsatSurveyResponse{},
|
||||
&model.Tag{},
|
||||
&model.ConversationLabel{},
|
||||
&model.ContactLabel{},
|
||||
&model.PlatformApp{},
|
||||
&model.Permissible{},
|
||||
&model.AccessToken{},
|
||||
|
||||
Reference in New Issue
Block a user