package run import ( "encoding/json" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" "git.iwork-ai.com/xdc/iqudo-top1/internal/scriptkit" "gorm.io/gorm" ) // NodeView is the public view of the current SOP node. type NodeView struct { NodeKey string `json:"node_key"` Type string `json:"type"` Title string `json:"title"` Content string `json:"content"` Config json.RawMessage `json:"config,omitempty"` Fields []PublicFieldView `json:"fields,omitempty"` Presentation *NodePresentation `json:"presentation,omitempty"` Stage *scriptkit.StageView `json:"stage,omitempty"` } // PublicFieldView mirrors a scenario field for the client. type PublicFieldView struct { Key string `json:"key"` Name string `json:"name"` Type string `json:"type"` Required bool `json:"required"` Options json.RawMessage `json:"options"` Validation json.RawMessage `json:"validation"` } func nodeView(db *gorm.DB, run model.SOPRun, node model.SOPNode) (NodeView, error) { view := NodeView{NodeKey: node.NodeKey, Type: node.Type, Title: node.Title, Content: node.Content, Config: json.RawMessage(node.Config)} switch node.Type { case "start": input, derived, answers := mapsFromRun(run) presentation, err := loadStartPresentation(db, node, run.TenantID, scriptkit.RenderContext{Input: input, Derived: derived, Form: answers}) if err != nil { return view, err } view.Presentation = presentation case "question", "choice", "form": fields, err := loadPublicNodeFields(db, node, run.TenantID) if err != nil { return view, err } view.Fields = fields case "stage": stage, err := buildStageView(db, run, node) if err != nil { return view, err } view.Stage = stage } return view, nil } // mapsFromRun returns input, derived and form maps for legacy views. func mapsFromRun(run model.SOPRun) (map[string]interface{}, map[string]interface{}, map[string]interface{}) { input := map[string]interface{}{} _ = json.Unmarshal(run.Input, &input) derived := map[string]interface{}{} _ = json.Unmarshal(run.Derived, &derived) answers := map[string]interface{}{} _ = json.Unmarshal(run.Answers, &answers) return input, derived, answers } func loadPublicNodeFields(db *gorm.DB, node model.SOPNode, tenantID uint64) ([]PublicFieldView, error) { var config answerNodeConfig if err := json.Unmarshal(node.Config, &config); err != nil { return nil, err } keys := config.FieldKeys if config.FieldKey != "" { keys = []string{config.FieldKey} } if len(keys) == 0 { return nil, nil } var fields []model.ScenarioField if err := db.Table("scenario_fields sf").Select("sf.*").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ? AND sf.field_key IN ?", node.SOPID, tenantID, keys).Find(&fields).Error; err != nil { return nil, err } byKey := make(map[string]model.ScenarioField, len(fields)) for _, field := range fields { byKey[field.FieldKey] = field } views := make([]PublicFieldView, 0, len(keys)) for _, key := range keys { field, ok := byKey[key] if !ok { continue } views = append(views, PublicFieldView{Key: field.FieldKey, Name: field.FieldName, Type: field.FieldType, Required: field.Required || config.Required || containsString(config.RequiredFieldKeys, key), Options: json.RawMessage(field.Options), Validation: json.RawMessage(field.Validation)}) } return views, nil }