package model import ( "fmt" "sort" "strconv" "strings" ) // TableColumn is one column in the center data table. type TableColumn struct { Key string Title string Width int } var subjectExtraKeys = map[Subject][]string{ SubjectTasks: {"status", "deadline", "responsible"}, SubjectCalendars: {"description"}, SubjectEvents: {"start", "end"}, SubjectContacts: {"primaryEmail", "displayName"}, SubjectPersons: {"primaryEmail", "displayName"}, SubjectCompanies: {"primaryEmail", "displayName"}, SubjectOpportunities: {"stage", "bidValue", "bidCurrency"}, SubjectCases: {"status"}, SubjectCRMTasks: {"status", "deadline"}, SubjectMailInbox: {"from", "date"}, SubjectMailSent: {"to", "date"}, SubjectMailDrafts: {"to", "date"}, SubjectMailTrash: {"from", "date"}, SubjectMailSpam: {"from", "date"}, SubjectUsers: {"email", "displayName"}, } // BuildColumns derives table columns from the list subject and item payloads. func BuildColumns(subject Subject, items []Item) []TableColumn { if subject == SubjectProjects { return buildProjectColumns(items) } cols := []TableColumn{ {Key: "_sel", Title: "✓", Width: 3}, {Key: "id", Title: "ID", Width: 10}, {Key: "title", Title: "Title", Width: 28}, } if hasSubtitle(items) { cols = append(cols, TableColumn{Key: "subtitle", Title: "Subtitle", Width: 22}) } seen := map[string]bool{"_sel": true, "id": true, "title": true, "subtitle": true} for _, key := range subjectExtraKeys[subject] { if seen[key] || !columnHasData(items, key) { continue } seen[key] = true cols = append(cols, TableColumn{Key: key, Title: titleLabel(key), Width: defaultWidth(key)}) } discovered := discoverRawKeys(items, seen, 8) for _, key := range discovered { cols = append(cols, TableColumn{Key: key, Title: titleLabel(key), Width: defaultWidth(key)}) } sizeColumns(cols, items) return cols } func buildProjectColumns(items []Item) []TableColumn { cols := []TableColumn{ {Key: "_sel", Title: "✓", Width: 3}, {Key: "id", Title: "ID", Width: 8}, {Key: "title", Title: "Title", Width: 28}, {Key: "tasks", Title: "Tasks (open/closed)", Width: 18}, {Key: "documents", Title: "Documents", Width: 11}, {Key: "users", Title: "Users", Width: 8}, } sizeColumns(cols, items) return cols } // CellText returns the display string for one table cell. func CellText(it Item, key string) string { switch key { case "_sel": if it.Selected { return "●" } return "○" case "id": return it.ID case "title": return it.Title case "subtitle": return it.Subtitle case "tasks": return formatProjectTasks(it.Raw) case "documents": return intRaw(it.Raw, "documentsCount") case "users": return intRaw(it.Raw, "participantCount") case "kind": return string(it.Kind) default: if it.Raw == nil { return "" } return formatAny(it.Raw[key]) } } func formatProjectTasks(raw map[string]any) string { if raw == nil { return "0/0" } open := intRawVal(raw, "taskCount") total := intRawVal(raw, "taskCountTotal") closed := total - open if closed < 0 { closed = 0 } return fmt.Sprintf("%d/%d", open, closed) } func intRaw(raw map[string]any, key string) string { if raw == nil { return "" } v := intRawVal(raw, key) if v == 0 { if _, ok := raw[key]; !ok { return "" } } return fmt.Sprintf("%d", v) } func intRawVal(raw map[string]any, key string) int { if raw == nil { return 0 } switch v := raw[key].(type) { case int: return v case int64: return int(v) case float64: return int(v) case string: n, _ := strconv.Atoi(v) return n default: if raw[key] == nil { return 0 } n, _ := strconv.Atoi(fmt.Sprint(raw[key])) return n } } func hasSubtitle(items []Item) bool { for _, it := range items { if it.Subtitle != "" { return true } } return false } func columnHasData(items []Item, key string) bool { for _, it := range items { if CellText(it, key) != "" { return true } } return false } func discoverRawKeys(items []Item, seen map[string]bool, limit int) []string { counts := map[string]int{} for _, it := range items { if it.Raw == nil { continue } for k := range it.Raw { if seen[k] || k == "id" || k == "title" { continue } counts[k]++ } } type kv struct { k string n int } var ranked []kv for k, n := range counts { ranked = append(ranked, kv{k, n}) } sort.Slice(ranked, func(i, j int) bool { if ranked[i].n == ranked[j].n { return ranked[i].k < ranked[j].k } return ranked[i].n > ranked[j].n }) out := make([]string, 0, limit) for _, r := range ranked { if len(out) >= limit { break } out = append(out, r.k) } return out } func titleLabel(key string) string { if key == "" { return "" } parts := strings.Split(key, "_") for i, p := range parts { if p == "" { continue } parts[i] = strings.ToUpper(p[:1]) + p[1:] } return strings.Join(parts, " ") } func defaultWidth(key string) int { switch key { case "id", "status", "stage": return 10 case "start", "end", "deadline", "date": return 18 case "from", "to", "email", "primaryEmail": return 22 case "description", "displayName": return 24 default: return 14 } } func sizeColumns(cols []TableColumn, items []Item) { for i := range cols { maxW := runeLen(cols[i].Title) for _, it := range items { n := runeLen(CellText(it, cols[i].Key)) if n > maxW { maxW = n } } maxW += 2 if maxW < 4 { maxW = 4 } if maxW > 36 { maxW = 36 } cols[i].Width = maxW } } func runeLen(s string) int { return len([]rune(s)) } func formatAny(v any) string { if v == nil { return "" } switch x := v.(type) { case string: return x case bool: if x { return "yes" } return "no" case float64: if x == float64(int64(x)) { return fmt.Sprintf("%d", int64(x)) } return fmt.Sprintf("%g", x) default: return fmt.Sprint(v) } }