Skip to content

Commit 86b4795

Browse files
cpcloudclaude
andcommitted
feat(ui): format vendor phone numbers for display
Adds Locale field to Vendor model and cellTelephoneNumber kind. Formats phone numbers at row-build time using per-vendor locale with system-default fallback. Wires formatting into the TUI table, extraction preview, and preserves Locale through form round-trips. closes #860 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9ac79cf commit 86b4795

15 files changed

Lines changed: 205 additions & 20 deletions

internal/app/coldefs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ var vendorColumnDefs = []columnDef{
218218
{"Name", columnSpec{Title: "Name", Min: 14, Max: 24, Flex: true}},
219219
{"Contact", columnSpec{Title: "Contact", Min: 10, Max: 20, Flex: true}},
220220
{"Email", columnSpec{Title: "Email", Min: 12, Max: 24, Flex: true}},
221-
{"Phone", columnSpec{Title: "Phone", Min: 12, Max: 16}},
221+
{"Phone", columnSpec{Title: "Phone", Min: 12, Max: 20, Kind: cellTelephoneNumber}},
222222
{"Website", columnSpec{Title: "Website", Min: 12, Max: 28, Flex: true}},
223223
{
224224
"Quotes",

internal/app/extraction_render.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"charm.land/lipgloss/v2"
15+
"github.com/micasa-dev/micasa/internal/config"
1516
"github.com/micasa-dev/micasa/internal/data"
1617
"github.com/micasa-dev/micasa/internal/extract"
1718
"github.com/micasa-dev/micasa/internal/locale"
@@ -707,7 +708,7 @@ func previewColumns(tableName string, cur locale.Currency) []previewColDef {
707708
{data.ColName, s[1], fmtAnyText},
708709
{data.ColContactName, s[2], fmtAnyText},
709710
{data.ColEmail, s[3], fmtAnyText},
710-
{data.ColPhone, s[4], fmtAnyText},
711+
{data.ColPhone, s[4], fmtPhone},
711712
{data.ColWebsite, s[5], fmtAnyText},
712713
}
713714
case tableDocuments:
@@ -873,6 +874,17 @@ func fmtAnyText(v any) string {
873874
}
874875
}
875876

877+
func fmtPhone(v any) string {
878+
if v == nil {
879+
return ""
880+
}
881+
s, ok := v.(string)
882+
if !ok {
883+
return fmtAnyText(v)
884+
}
885+
return locale.FormatPhoneNumber(s, strings.ToUpper(config.DetectCountry()))
886+
}
887+
876888
func fmtAnyFK(v any) string {
877889
s := fmtAnyText(v)
878890
if s != "" && s != "0" {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2026 Phillip Cloud
2+
// Licensed under the Apache License, Version 2.0
3+
4+
package app
5+
6+
import (
7+
"testing"
8+
9+
"github.com/micasa-dev/micasa/internal/data"
10+
"github.com/micasa-dev/micasa/internal/locale"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestPreviewColumnsVendorFormatsPhone(t *testing.T) {
16+
// Not parallel: t.Setenv modifies process-global state.
17+
// LC_ALL has highest precedence in DetectCountry().
18+
t.Setenv("LC_ALL", "en_US.UTF-8")
19+
cur, err := locale.ResolveDefault("")
20+
require.NoError(t, err)
21+
cols := previewColumns(data.TableVendors, cur)
22+
23+
// Find the phone column by key.
24+
var phoneFmt func(any) string
25+
for _, c := range cols {
26+
if c.dataKey == data.ColPhone {
27+
phoneFmt = c.format
28+
break
29+
}
30+
}
31+
require.NotNil(t, phoneFmt, "phone column not found in vendor preview")
32+
33+
// Should format parseable numbers, passthrough garbage.
34+
assert.Equal(t, "(555) 123-4567", phoneFmt("5551234567"))
35+
assert.Equal(t, "not a phone", phoneFmt("not a phone"))
36+
}

internal/app/forms.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ type vendorFormData struct {
126126
Phone string
127127
Website string
128128
Notes string
129+
Locale string
129130
}
130131

131132
// entityRef identifies a polymorphic document parent (kind + ID).
@@ -1102,6 +1103,7 @@ func (m *Model) parseVendorFormData() (data.Vendor, error) {
11021103
Phone: strings.TrimSpace(values.Phone),
11031104
Website: strings.TrimSpace(values.Website),
11041105
Notes: strings.TrimSpace(values.Notes),
1106+
Locale: strings.TrimSpace(values.Locale),
11051107
}, nil
11061108
}
11071109

@@ -1153,6 +1155,7 @@ func vendorFormValues(vendor data.Vendor) *vendorFormData {
11531155
Phone: vendor.Phone,
11541156
Website: vendor.Website,
11551157
Notes: vendor.Notes,
1158+
Locale: vendor.Locale,
11561159
}
11571160
}
11581161

internal/app/handlers.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ package app
55

66
import (
77
"fmt"
8+
"strings"
89
"time"
910

1011
"charm.land/bubbles/v2/table"
12+
"github.com/micasa-dev/micasa/internal/config"
1113
"github.com/micasa-dev/micasa/internal/data"
1214
)
1315

@@ -494,7 +496,8 @@ func (vendorHandler) Load(
494496
quoteCounts := fetchCounts(store.CountQuotesByVendor, ids)
495497
jobCounts := fetchCounts(store.CountServiceLogsByVendor, ids)
496498
docCounts := fetchDocCounts(store, data.DocumentEntityVendor, ids)
497-
rows, meta, cellRows := vendorRows(vendors, quoteCounts, jobCounts, docCounts)
499+
defaultRegion := strings.ToUpper(config.DetectCountry())
500+
rows, meta, cellRows := vendorRows(vendors, quoteCounts, jobCounts, docCounts, defaultRegion)
498501
return rows, meta, cellRows, nil
499502
}
500503

internal/app/mag.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ func magFormat(c cell, includeUnit bool, currencySymbol string) string {
2626
}
2727

2828
// Only transform kinds that carry meaningful numeric data.
29-
// cellText is excluded because it covers phone numbers, serial numbers,
29+
// cellText is excluded because it covers serial numbers,
3030
// model numbers, and other identifiers that happen to look numeric.
3131
switch c.Kind {
3232
case cellMoney, cellDrilldown, cellOps:
3333
// Definitely numeric; continue to parsing below.
3434
case cellText, cellReadonly, cellDate, cellStatus, cellWarranty,
35-
cellUrgency, cellNotes, cellEntity:
35+
cellUrgency, cellNotes, cellEntity, cellTelephoneNumber:
3636
return value
3737
default:
3838
panic(fmt.Sprintf("unhandled cellKind: %d", c.Kind))

internal/app/mag_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ func seedTabMoneyCells(tab *Tab, amounts []string) {
300300
case cellReadonly:
301301
cr[j] = cell{Value: "1", Kind: cellReadonly}
302302
case cellText, cellDate, cellStatus, cellDrilldown, cellWarranty,
303-
cellUrgency, cellNotes, cellEntity, cellOps:
303+
cellUrgency, cellNotes, cellEntity, cellOps, cellTelephoneNumber:
304304
cr[j] = cell{Value: "test", Kind: spec.Kind}
305305
default:
306306
panic(fmt.Sprintf("unhandled cellKind: %d", spec.Kind))
@@ -390,7 +390,7 @@ func seedMoneyCells(m *Model) {
390390
case cellReadonly:
391391
row[i] = cell{Value: "1", Kind: cellReadonly}
392392
case cellText, cellDate, cellStatus, cellDrilldown, cellWarranty,
393-
cellUrgency, cellNotes, cellEntity, cellOps:
393+
cellUrgency, cellNotes, cellEntity, cellOps, cellTelephoneNumber:
394394
row[i] = cell{Value: "test", Kind: spec.Kind}
395395
default:
396396
panic(fmt.Sprintf("unhandled cellKind: %d", spec.Kind))

internal/app/sort.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func compareCells(tab *Tab, col, a, b int) int {
129129
return cmpOrdered(strings.ToLower(va), strings.ToLower(vb))
130130
}
131131
return cmpOrdered(na, nb)
132-
case cellText, cellStatus, cellNotes, cellEntity:
132+
case cellText, cellStatus, cellNotes, cellEntity, cellTelephoneNumber:
133133
return cmpOrdered(strings.ToLower(va), strings.ToLower(vb))
134134
}
135135
panic(fmt.Sprintf("unhandled cellKind: %d", kind))

internal/app/table.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ func cellStyle(kind cellKind) lipgloss.Style {
644644
case cellReadonly:
645645
return appStyles.Readonly()
646646
case cellText, cellDate, cellStatus, cellDrilldown, cellWarranty,
647-
cellUrgency, cellNotes, cellEntity, cellOps:
647+
cellUrgency, cellNotes, cellEntity, cellOps, cellTelephoneNumber:
648648
return defaultStyle
649649
}
650650
panic(fmt.Sprintf("unhandled cellKind: %d", kind))

internal/app/tables.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"fmt"
99
"strconv"
10+
"strings"
1011
"time"
1112

1213
"charm.land/bubbles/v2/table"
@@ -311,8 +312,13 @@ func vendorRows(
311312
quoteCounts map[string]int,
312313
jobCounts map[string]int,
313314
docCounts map[string]int,
315+
defaultRegion string,
314316
) ([]table.Row, []rowMeta, [][]cell) {
315317
return buildRows(vendors, func(v data.Vendor) rowSpec {
318+
region := defaultRegion
319+
if v.Locale != "" {
320+
region = strings.ToUpper(v.Locale)
321+
}
316322
return rowSpec{
317323
ID: v.ID,
318324
Deleted: v.DeletedAt.Valid,
@@ -321,7 +327,7 @@ func vendorRows(
321327
{Value: v.Name, Kind: cellText},
322328
{Value: v.ContactName, Kind: cellText},
323329
{Value: v.Email, Kind: cellText},
324-
{Value: v.Phone, Kind: cellText},
330+
{Value: locale.FormatPhoneNumber(v.Phone, region), Kind: cellTelephoneNumber},
325331
{Value: v.Website, Kind: cellText},
326332
{Value: countStr(quoteCounts, v.ID), Kind: cellDrilldown},
327333
{Value: countStr(jobCounts, v.ID), Kind: cellDrilldown},

0 commit comments

Comments
 (0)