package main import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "inou/lib" ) // MyChart JSON format type MyChartResponse struct { OrderName string `json:"orderName"` Key string `json:"key"` Results []MyChartResult `json:"results"` } type MyChartResult struct { OrderMetadata MyChartOrderMeta `json:"orderMetadata"` ResultComponents []MyChartComponent `json:"resultComponents"` IsAbnormal bool `json:"isAbnormal"` } type MyChartOrderMeta struct { OrderProviderName string `json:"orderProviderName"` AuthorizingProviderName string `json:"authorizingProviderName"` PrioritizedInstantISO string `json:"prioritizedInstantISO"` ResultStatus string `json:"resultStatus"` ResultType string `json:"resultType"` ResultingLab struct { Name string `json:"name"` } `json:"resultingLab"` SpecimensDisplay string `json:"specimensDisplay"` AssociatedDiagnoses []string `json:"associatedDiagnoses"` } type MyChartComponent struct { ComponentInfo struct { Name string `json:"name"` CommonName string `json:"commonName"` Units string `json:"units"` } `json:"componentInfo"` ComponentResultInfo struct { Value string `json:"value"` NumericValue *float64 `json:"numericValue,omitempty"` } `json:"componentResultInfo"` ComponentComments struct { HasContent bool `json:"hasContent"` ContentAsString string `json:"contentAsString"` } `json:"componentComments"` } // Quality Labs JSON format type QualityLabsFile struct { LabOrder struct { OrderName string `json:"order_name"` OrderKey string `json:"order_key"` CollectionDate string `json:"collection_date"` Provider string `json:"provider"` LabName string `json:"lab_name"` Specimen string `json:"specimen"` } `json:"lab_order"` LabTests []struct { Name string `json:"name"` Value string `json:"value"` NumericValue *float64 `json:"numeric_value"` Units *string `json:"units"` } `json:"lab_tests"` } // Intermediate representation type labOrder struct { sourceKey string orderName string timestamp int64 localTime string // original ISO8601 with timezone offset provider string labName string specimen string source string tests []labTest } type labTest struct { sourceKey string name string commonName string value string numericValue *float64 unit string } func main() { if len(os.Args) >= 2 && os.Args[1] == "--patch-time" { if len(os.Args) < 4 { fmt.Println("Usage: import-lab --patch-time ") os.Exit(1) } patchLocalTime(os.Args[2], os.Args[3]) return } if len(os.Args) >= 2 && os.Args[1] == "--populate-refs" { if err := lib.Init(); err != nil { fmt.Println("lib.Init failed:", err) os.Exit(1) } lib.ConfigInit() fmt.Println("Populating reference ranges is no longer needed - use import-caliper instead.") fmt.Println("Done.") return } if len(os.Args) == 2 { // Just normalize, no import dossierID := os.Args[1] if err := lib.Init(); err != nil { fmt.Println("lib.Init failed:", err) os.Exit(1) } lib.ConfigInit() fmt.Println("Normalizing test names...") if err := lib.Normalize(dossierID, lib.CategoryLab); err != nil { fmt.Printf("Normalization failed: %v\n", err) os.Exit(1) } return } if len(os.Args) < 3 { fmt.Println("Usage: import-lab [path]") fmt.Println(" path: directory of JSONs or single JSON file") fmt.Println(" Without path: only normalize existing entries") fmt.Println(" import-lab --patch-time (patch local_time into existing entries)") os.Exit(1) } dossierID := os.Args[1] inputPath := os.Args[2] // Init lib (crypto + DB + LLM keys) if err := lib.Init(); err != nil { fmt.Println("lib.Init failed:", err) os.Exit(1) } lib.ConfigInit() // Load existing lab entries for dedup existing, err := lib.EntryQuery(nil, dossierID, lib.CategoryLab, "", "*") if err != nil { fmt.Printf("Warning: could not load existing entries: %v\n", err) } existingByKey := make(map[string]*lib.Entry, len(existing)) for _, e := range existing { var data struct { SourceKey string `json:"source_key"` } if json.Unmarshal([]byte(e.Data), &data) == nil && data.SourceKey != "" { existingByKey[data.SourceKey] = e } } fmt.Printf("Loaded %d existing lab entries (%d with source_key)\n", len(existing), len(existingByKey)) // Collect input files var files []string info, err := os.Stat(inputPath) if err != nil { fmt.Printf("Cannot access %s: %v\n", inputPath, err) os.Exit(1) } if info.IsDir() { matches, _ := filepath.Glob(filepath.Join(inputPath, "*.json")) files = matches } else { files = []string{inputPath} } fmt.Printf("Processing %d files\n", len(files)) // Parse all files into orders var orders []labOrder var skipFiles int for _, f := range files { order, err := parseFile(f) if err != nil { skipFiles++ continue } if order != nil { orders = append(orders, *order) } } fmt.Printf("Parsed %d lab orders (%d files skipped)\n", len(orders), skipFiles) // Build entries var entries []*lib.Entry var created, updated int now := time.Now().Unix() _ = now for _, order := range orders { // Parent entry parentID := "" if ex, ok := existingByKey[order.sourceKey]; ok { parentID = ex.EntryID updated++ } else { parentID = lib.NewID() created++ } parentData, _ := json.Marshal(map[string]interface{}{ "source_key": order.sourceKey, "source": order.source, "provider": order.provider, "lab_name": order.labName, "specimen": order.specimen, "local_time": order.localTime, }) entries = append(entries, &lib.Entry{ EntryID: parentID, DossierID: dossierID, Category: lib.CategoryLab, Type: "lab_order", Value: order.orderName, Timestamp: order.timestamp, Tags: order.source, Data: string(parentData), }) // Child entries for _, test := range order.tests { childID := "" if ex, ok := existingByKey[test.sourceKey]; ok { childID = ex.EntryID } else { childID = lib.NewID() } // Build summary for portal display summary := test.commonName + ": " + test.value if summary == ": " { summary = test.name + ": " + test.value } if test.unit != "" { summary += " " + test.unit } childData := map[string]interface{}{ "source_key": test.sourceKey, } if test.commonName != "" { childData["common_name"] = test.commonName } if test.numericValue != nil { childData["numeric_value"] = *test.numericValue } if test.unit != "" { childData["unit"] = test.unit } dataJSON, _ := json.Marshal(childData) entries = append(entries, &lib.Entry{ EntryID: childID, DossierID: dossierID, ParentID: parentID, Category: lib.CategoryLab, Type: test.name, Value: test.value, Summary: summary, Timestamp: order.timestamp, Data: string(dataJSON), }) } } // Save fmt.Printf("Saving %d entries...\n", len(entries)) start := time.Now() if err := lib.EntryWrite("", entries...); err != nil { fmt.Printf("EntryWrite failed: %v\n", err) os.Exit(1) } fmt.Printf("Done in %v: %d orders (%d created, %d updated), %d total entries\n", time.Since(start), len(orders), created, updated, len(entries)) // Normalize test names within this dossier fmt.Println("Normalizing test names...") if err := lib.Normalize(dossierID, lib.CategoryLab); err != nil { fmt.Printf("Warning: normalization failed: %v\n", err) } } func parseFile(path string) (*labOrder, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } // Auto-detect format var raw map[string]json.RawMessage if json.Unmarshal(data, &raw) != nil { return nil, fmt.Errorf("invalid JSON") } if _, ok := raw["orderName"]; ok { return parseMyChart(data) } if _, ok := raw["lab_order"]; ok { return parseQualityLabs(data) } return nil, fmt.Errorf("unknown format") } func parseMyChart(data []byte) (*labOrder, error) { var resp MyChartResponse if err := json.Unmarshal(data, &resp); err != nil { return nil, err } if len(resp.Results) == 0 { return nil, nil } r := resp.Results[0] if r.OrderMetadata.ResultType != "LAB" { return nil, nil } if len(r.ResultComponents) == 0 { return nil, nil } provider := r.OrderMetadata.OrderProviderName if provider == "" { provider = r.OrderMetadata.AuthorizingProviderName } ts, _ := time.Parse(time.RFC3339, r.OrderMetadata.PrioritizedInstantISO) order := &labOrder{ sourceKey: resp.Key, orderName: resp.OrderName, timestamp: ts.Unix(), localTime: r.OrderMetadata.PrioritizedInstantISO, provider: provider, labName: r.OrderMetadata.ResultingLab.Name, specimen: r.OrderMetadata.SpecimensDisplay, source: "mychart", } for _, c := range r.ResultComponents { name := c.ComponentInfo.Name if name == "" { continue } order.tests = append(order.tests, labTest{ sourceKey: resp.Key + "|" + name, name: name, commonName: c.ComponentInfo.CommonName, value: c.ComponentResultInfo.Value, numericValue: c.ComponentResultInfo.NumericValue, unit: c.ComponentInfo.Units, }) } return order, nil } func parseQualityLabs(data []byte) (*labOrder, error) { var ql QualityLabsFile if err := json.Unmarshal(data, &ql); err != nil { return nil, err } ts, _ := time.Parse(time.RFC3339, ql.LabOrder.CollectionDate) order := &labOrder{ sourceKey: ql.LabOrder.OrderKey, orderName: ql.LabOrder.OrderName, timestamp: ts.Unix(), localTime: ql.LabOrder.CollectionDate, provider: ql.LabOrder.Provider, labName: ql.LabOrder.LabName, specimen: ql.LabOrder.Specimen, source: "quality_labs", } for _, t := range ql.LabTests { if t.Name == "" { continue } unit := "" if t.Units != nil { unit = *t.Units } // Use name as common_name for quality_labs (no separate common name) cn := strings.ToUpper(t.Name) order.tests = append(order.tests, labTest{ sourceKey: ql.LabOrder.OrderKey + "|" + t.Name, name: t.Name, commonName: cn, value: t.Value, numericValue: t.NumericValue, unit: unit, }) } return order, nil } // patchLocalTime reads source JSONs, matches existing entries by source_key, // and patches local_time into their Data JSON. No new entries created. func patchLocalTime(dossierID, inputPath string) { if err := lib.Init(); err != nil { fmt.Println("lib.Init failed:", err) os.Exit(1) } // Load existing lab entries existing, err := lib.EntryQuery(nil, dossierID, lib.CategoryLab, "", "*") if err != nil { fmt.Printf("Failed to load entries: %v\n", err) os.Exit(1) } byKey := make(map[string]*lib.Entry, len(existing)) for _, e := range existing { var data struct { SourceKey string `json:"source_key"` } if json.Unmarshal([]byte(e.Data), &data) == nil && data.SourceKey != "" { byKey[data.SourceKey] = e } } fmt.Printf("Loaded %d existing entries (%d with source_key)\n", len(existing), len(byKey)) // Collect input files var files []string info, err := os.Stat(inputPath) if err != nil { fmt.Printf("Cannot access %s: %v\n", inputPath, err) os.Exit(1) } if info.IsDir() { matches, _ := filepath.Glob(filepath.Join(inputPath, "*.json")) files = matches } else { files = []string{inputPath} } // Parse and patch var patched int for _, f := range files { order, err := parseFile(f) if err != nil || order == nil { continue } if e, ok := byKey[order.sourceKey]; ok { if patchDataLocalTime(e, order.localTime) { if err := lib.EntryWrite("", e); err == nil { patched++ } } } } fmt.Printf("Patched %d entries with local_time\n", patched) } // patchDataLocalTime adds local_time to an entry's Data JSON func patchDataLocalTime(e *lib.Entry, localTime string) bool { if localTime == "" { return false } var data map[string]interface{} if json.Unmarshal([]byte(e.Data), &data) != nil { return false } if existing, ok := data["local_time"]; ok && existing == localTime { return false } data["local_time"] = localTime b, _ := json.Marshal(data) e.Data = string(b) return true }