feat(crm): complete contact label avatar gaps

This commit is contained in:
2026-06-05 02:53:24 +08:00
parent 5cf735076d
commit 7a033e28a0
16 changed files with 512 additions and 31 deletions
+34
View File
@@ -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
+84 -5
View File
@@ -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
+2
View File
@@ -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)
}