feat(crm): complete contact label avatar gaps
This commit is contained in:
@@ -337,6 +337,40 @@ func (s *CompanyService) AddContact(ctx context.Context, companyID, accountID, c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CompanyService) DestroyCustomAttributes(ctx context.Context, companyID, accountID uint, keys []string) (*model.Company, error) {
|
||||
company, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attrs := map[string]any{}
|
||||
if len(company.CustomAttributes) > 0 {
|
||||
_ = json.Unmarshal(company.CustomAttributes, &attrs)
|
||||
}
|
||||
for _, key := range keys {
|
||||
delete(attrs, key)
|
||||
}
|
||||
bytes, _ := json.Marshal(attrs)
|
||||
company.CustomAttributes = datatypes.JSON(bytes)
|
||||
if err := s.companyRepo.Update(ctx, company); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.indexCompany(ctx, company)
|
||||
return company, nil
|
||||
}
|
||||
|
||||
func (s *CompanyService) DeleteAvatar(ctx context.Context, companyID, accountID uint) (*model.Company, error) {
|
||||
company, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
company.FaviconURL = ""
|
||||
if err := s.companyRepo.Update(ctx, company); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.indexCompany(ctx, company)
|
||||
return company, nil
|
||||
}
|
||||
|
||||
func mergeJSON(current datatypes.JSON, incoming datatypes.JSON) datatypes.JSON {
|
||||
if len(incoming) == 0 {
|
||||
return current
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
@@ -62,16 +63,16 @@ func (s *ContactService) DB() *gorm.DB {
|
||||
}
|
||||
|
||||
// ListByAccount retrieves all contacts for an account with optional sort.
|
||||
func (s *ContactService) ListByAccount(ctx context.Context, accountID uint, offset, limit int, sort string) ([]model.Contact, int64, error) {
|
||||
return s.repo.FindByAccount(ctx, accountID, offset, limit, sort)
|
||||
func (s *ContactService) ListByAccount(ctx context.Context, accountID uint, offset, limit int, sort string, labels ...[]string) ([]model.Contact, int64, error) {
|
||||
return s.repo.FindByAccount(ctx, accountID, offset, limit, sort, labels...)
|
||||
}
|
||||
|
||||
// Search searches contacts by name, email, phone, or identifier with optional sort.
|
||||
func (s *ContactService) Search(ctx context.Context, accountID uint, query string, offset, limit int, sort string, searchMode search.SearchMode) ([]model.Contact, int64, error) {
|
||||
func (s *ContactService) Search(ctx context.Context, accountID uint, query string, offset, limit int, sort string, searchMode search.SearchMode, labels ...[]string) ([]model.Contact, int64, error) {
|
||||
if query == "" {
|
||||
return s.repo.FindByAccount(ctx, accountID, offset, limit, sort)
|
||||
return s.repo.FindByAccount(ctx, accountID, offset, limit, sort, labels...)
|
||||
}
|
||||
return s.repo.Search(ctx, accountID, query, offset, limit, sort, searchMode)
|
||||
return s.repo.Search(ctx, accountID, query, offset, limit, sort, searchMode, labels...)
|
||||
}
|
||||
|
||||
// GetByID retrieves a single contact.
|
||||
@@ -474,6 +475,84 @@ func (s *ContactService) DestroyCustomAttributes(ctx context.Context, accountID,
|
||||
return contact, nil
|
||||
}
|
||||
|
||||
func (s *ContactService) DeleteAvatar(ctx context.Context, accountID, contactID uint) (*model.Contact, error) {
|
||||
contact, err := s.repo.FindByAccountAndID(ctx, accountID, contactID)
|
||||
if err != nil {
|
||||
return nil, errors.New("contact not found")
|
||||
}
|
||||
contact.AvatarURL = ""
|
||||
if err := s.repo.Update(ctx, contact); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.indexContact(ctx, contact)
|
||||
return contact, nil
|
||||
}
|
||||
|
||||
func (s *ContactService) GetLabels(ctx context.Context, accountID, contactID uint) ([]string, error) {
|
||||
if _, err := s.repo.FindByAccountAndID(ctx, accountID, contactID); err != nil {
|
||||
return nil, errors.New("contact not found")
|
||||
}
|
||||
var rows []struct{ Name string }
|
||||
err := s.DB().WithContext(ctx).Table("contact_labels").
|
||||
Select("tags.name").
|
||||
Joins("JOIN tags ON tags.id = contact_labels.tag_id").
|
||||
Where("contact_labels.account_id = ? AND contact_labels.contact_id = ? AND tags.deleted_at IS NULL", accountID, contactID).
|
||||
Order("contact_labels.created_at ASC, tags.name ASC").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labels := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
labels = append(labels, row.Name)
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func (s *ContactService) UpdateLabels(ctx context.Context, accountID, contactID uint, labels []string) ([]string, error) {
|
||||
if _, err := s.repo.FindByAccountAndID(ctx, accountID, contactID); err != nil {
|
||||
return nil, errors.New("contact not found")
|
||||
}
|
||||
normalized := normalizeContactServiceLabels(labels)
|
||||
err := s.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("account_id = ? AND contact_id = ?", accountID, contactID).Delete(&model.ContactLabel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, label := range normalized {
|
||||
tag := model.Tag{AccountID: accountID, Name: label}
|
||||
if err := tx.Where("account_id = ? AND name = ?", accountID, label).FirstOrCreate(&tag).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
contactLabel := model.ContactLabel{AccountID: accountID, ContactID: contactID, TagID: tag.ID}
|
||||
if err := tx.Create(&contactLabel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func normalizeContactServiceLabels(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 mergeContactJSON(current datatypes.JSON, incoming datatypes.JSON) datatypes.JSON {
|
||||
if len(incoming) == 0 {
|
||||
return current
|
||||
|
||||
@@ -66,6 +66,8 @@ func setupServiceTestDB(t *testing.T) *gorm.DB {
|
||||
&model.PortalMember{},
|
||||
&model.Company{},
|
||||
&model.CompanyNote{},
|
||||
&model.Tag{},
|
||||
&model.ContactLabel{},
|
||||
); err != nil {
|
||||
t.Fatalf("failed to auto-migrate models: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user