feat: upstream generic workspace tooling (board-sync, crm audit, catalog names)
Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go 1.25) (pull_request) Successful in 1m16s
Tests / Test (Go stable) (pull_request) Successful in 1m36s

Promote the generic, reusable parts of the private oo-workspace into the
public library/CLI, so oo-workspace can shrink to business glue.

- board.go: Board types + (c *Client) SyncBoard — upsert project
  milestones/tasks by exact title from a YAML board (dry-run when
  apply=false). CLI: oo projects board-sync.
- crm_audit.go: OpportunityAudit + (c *Client) AuditOpportunities —
  file/task/member counts with a generic class (ok|dup|empty|junk-title).
  CLI: oo crm audit [--out].
- catalog/names.go: CleanPersonNames / GuessNameFromEmail /
  FormatProjectTitle (ported from oo-workspace).
- catalog: adopt the newer apply/match logic — name repair (never encode
  company in lastName), company grouping key, contact-info type
  normalization, preserve an already-applied oo_id. Keeps the local
  Address/OOProjects fields and the config-driven classifier.

Business-only oo-workspace commands (crm clean/migrate, folders, search)
deliberately stay private.
This commit is contained in:
2026-09-22 23:13:40 +01:00
parent 8848ee00f1
commit 82da45dc4d
13 changed files with 777 additions and 24 deletions
+14 -15
View File
@@ -115,20 +115,13 @@ func applyCompany(ctx context.Context, client *onlyoffice.Client, e *Entry) (boo
}
func applyPerson(ctx context.Context, client *onlyoffice.Client, e *Entry) (bool, error) {
first := strings.TrimSpace(e.First)
last := strings.TrimSpace(e.Last)
if first == "" && last == "" {
first, last = SplitDisplayName(e.Name)
}
if first == "" {
first = strings.TrimSpace(e.Name)
}
org := strings.TrimSpace(e.Org)
first, last := CleanPersonNames(e.First, e.Last, e.Name, org, e.Emails)
e.First, e.Last = first, last
if first == "" {
return false, fmt.Errorf("person missing name")
}
if last == "" {
last = "-"
}
e.Name = strings.TrimSpace(first + " " + strings.Trim(last, "-"))
var p map[string]any
var err error
@@ -144,21 +137,27 @@ func applyPerson(ctx context.Context, client *onlyoffice.Client, e *Entry) (bool
}
created := false
companyID := 0
if e.Org != "" {
if co, ferr := client.FindCompany(ctx, e.Org); ferr == nil && co != nil {
if org != "" {
if co, ferr := client.FindCompany(ctx, org); ferr == nil && co != nil {
companyID, _ = strconv.Atoi(contactIDString(co))
}
}
if p == nil {
about := ""
if e.Org != "" {
about = "org: " + e.Org
if org != "" {
about = "org: " + org
}
p, err = client.CreatePerson(ctx, first, last, companyID, "", about)
if err != nil {
return false, err
}
created = true
} else {
// Repair names + ensure company link (never encode company in lastName).
id := contactIDString(p)
if _, err := client.UpdatePerson(ctx, id, first, last, companyID, "", ""); err != nil {
return false, fmt.Errorf("update person %s: %w", id, err)
}
}
id := contactIDString(p)
e.OOID = id