489 lines
12 KiB
Go
489 lines
12 KiB
Go
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 <dossier-id> <path>")
|
|
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()
|
|
lib.RefDBInit("/tank/inou/data/reference.db")
|
|
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 <dossier-id> [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 <dossier-id> <path> (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()
|
|
lib.RefDBInit("/tank/inou/data/reference.db")
|
|
|
|
// 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
|
|
}
|