feat: enrich /api/v1/dashboard with family profile display fields

APIDossierEntry now includes initials, color, age, dob, sex, is_self —
computed server-side using the same avatar color palette and age logic
as the web portal dashboard. Self entry now includes name (was empty).
Relation field now uses proper name from RBAC relation int.

Native mobile family switcher UI can display rich profile cards without
any additional API calls.
This commit is contained in:
James 2026-03-23 12:32:57 -04:00
parent f58a4f804e
commit 13e991aa1c
1 changed files with 58 additions and 2 deletions

View File

@ -8,6 +8,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"inou/lib"
)
@ -185,6 +186,12 @@ type APIDossierEntry struct {
Name string `json:"name"`
Relation string `json:"relation"`
CanAdd bool `json:"can_add"`
Initials string `json:"initials"`
Color string `json:"color"`
Age string `json:"age"` // e.g. "8y", "42y", "" if unknown
DOB string `json:"dob"` // YYYY-MM-DD or ""
Sex int `json:"sex"` // 0=unknown, 1=male, 2=female
IsSelf bool `json:"is_self"`
}
type APIDashboardResponse struct {
@ -206,9 +213,15 @@ func handleAPIDashboard(w http.ResponseWriter, r *http.Request) {
// Add self first
dossiers = append(dossiers, APIDossierEntry{
GUID: formatHexID(d.DossierID),
Name: "", // Empty means "self"
Name: d.Name,
Relation: "self",
CanAdd: true,
Initials: apiInitials(d.Name),
Color: apiColor(d.DossierID),
Age: apiAge(d.DateOfBirth),
DOB: apiDOB(d.DateOfBirth),
Sex: d.Sex,
IsSelf: true,
})
// Add others
@ -223,8 +236,13 @@ func handleAPIDashboard(w http.ResponseWriter, r *http.Request) {
dossiers = append(dossiers, APIDossierEntry{
GUID: formatHexID(a.DossierID),
Name: target.Name,
Relation: "other", // Relation removed from RBAC
Relation: relationName(a.Relation),
CanAdd: (a.Ops & lib.PermWrite) != 0,
Initials: apiInitials(target.Name),
Color: apiColor(a.DossierID),
Age: apiAge(target.DateOfBirth),
DOB: apiDOB(target.DateOfBirth),
Sex: target.Sex,
})
}
@ -306,3 +324,41 @@ func relationName(rel int) string {
default: return "other"
}
}
func apiInitials(name string) string {
parts := strings.Fields(name)
if len(parts) == 0 { return "?" }
first := []rune(parts[0])
if len(parts) == 1 {
if len(first) == 0 { return "?" }
return strings.ToUpper(string(first[:1]))
}
last := []rune(parts[len(parts)-1])
if len(first) == 0 || len(last) == 0 { return "?" }
return strings.ToUpper(string(first[:1]) + string(last[:1]))
}
var apiAvatarColors = []string{
"#C47A3D", "#5AAD8A", "#7E8FC2", "#C26E6E",
"#9A82B8", "#C2963D", "#6BA0B8", "#B87898",
"#7DAD6B", "#8B7D6B",
}
func apiColor(id string) string {
if len(id) < 2 { return apiAvatarColors[0] }
b, _ := strconv.ParseUint(id[len(id)-2:], 16, 8)
return apiAvatarColors[b%uint64(len(apiAvatarColors))]
}
func apiAge(dob string) string {
if len(dob) < 10 { return "" }
t, err := time.Parse("2006-01-02", dob[:10])
if err != nil { return "" }
years := int(time.Since(t).Hours() / 8766)
return fmt.Sprintf("%dy", years)
}
func apiDOB(dob string) string {
if len(dob) < 10 { return "" }
return dob[:10]
}