<cfcomponent displayname="Mobile Progress Notes API"
    hint="JSON API for progress notes visit form (securityKey + Schedule_ID). Mirrors progress_notesnew.cfm section visibility and field types."
    output="false">

    <cfheader name="Access-Control-Allow-Origin" value="*">
    <cfheader name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS">
    <cfheader name="Access-Control-Allow-Headers" value="Content-Type, Authorization, Accept, X-Requested-With">
    <cfheader name="Access-Control-Max-Age" value="86400">
    <cfif structKeyExists(cgi, "REQUEST_METHOD") AND compareNoCase(cgi.REQUEST_METHOD, "OPTIONS") EQ 0>
        <cfheader statuscode="204" statustext="No Content">
        <cfabort>
    </cfif>

    <cfset this.securityKeyEncryptKey = "5A2zPUZJ90AbHOje9DpM+g==" />

    <cfif NOT isDefined("Application.DataSrc")>
        <cfset Application.DataSrc = "hhapowerpath" />
    </cfif>

    <!--- Cast numeric IDs so serializeJSON does not emit 123.0 --->
    <cffunction name="toJsonInt" access="private" returntype="any" output="false">
        <cfargument name="value" required="true" />
        <cftry>
            <cfreturn javacast("int", int(val(arguments.value))) />
            <cfcatch type="any">
                <cfreturn int(val(arguments.value)) />
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="safeTrim" access="private" returntype="string" output="false">
        <cfargument name="value" required="false" default="" />
        <cftry>
            <cfif isNull(arguments.value)>
                <cfreturn "" />
            </cfif>
            <cfreturn trim("" & arguments.value) />
            <cfcatch type="any">
                <cfreturn "" />
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="queryCol" access="private" returntype="any" output="false">
        <cfargument name="q" type="query" required="true" />
        <cfargument name="colName" type="string" required="true" />
        <cfargument name="rowIndex" type="numeric" required="true" />
        <cfargument name="defaultValue" required="false" default="" />
        <cfif listFindNoCase(arguments.q.columnList, arguments.colName)>
            <cfreturn arguments.q[arguments.colName][arguments.rowIndex] />
        </cfif>
        <cfreturn arguments.defaultValue />
    </cffunction>

    <cffunction name="parseSecurityKeyEmpId" access="private" returntype="numeric" output="false">
        <cfargument name="securityKey" type="string" required="true" />
        <cfset var plain = "" />
        <cfset var empPart = "" />
        <cftry>
            <cfif NOT len(trim(arguments.securityKey))>
                <cfreturn 0 />
            </cfif>
            <cfset plain = decrypt(trim(arguments.securityKey), this.securityKeyEncryptKey, "AES", "Base64") />
            <cfset empPart = listFirst(plain, "|") />
            <cfif isNumeric(empPart) AND val(empPart) GT 0>
                <cfreturn val(empPart) />
            </cfif>
            <cfreturn 0 />
            <cfcatch type="any">
                <cfreturn 0 />
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="authorizeSecurityKeyForAgency" access="private" returntype="struct" output="false">
        <cfargument name="securityKey" type="string" required="true" />
        <cfset var result = { ok = false, statusCode = 401, message = "Unauthorized", agencySchema = "", locId = 0 } />
        <cfset var empId = parseSecurityKeyEmpId(arguments.securityKey) />
        <cfset var employeeQuery = "" />
        <cfset var schemaCheckQuery = "" />

        <cfif empId EQ 0>
            <cfset result.message = "Invalid or missing security key" />
            <cfreturn result />
        </cfif>

        <cfquery name="employeeQuery" datasource="#Application.DataSrc#">
            SELECT Emp_ID, Loc_ID
            FROM hhapowerpath.pEmployee
            WHERE Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#empId#">
              AND Emp_Type = 'Employee'
              AND Status = 0
            LIMIT 1
        </cfquery>
        <cfif employeeQuery.recordcount EQ 0>
            <cfset result.message = "Employee not found or inactive" />
            <cfreturn result />
        </cfif>
        <cfif NOT isNumeric(employeeQuery.Loc_ID)>
            <cfset result.message = "Invalid agency mapping for user" />
            <cfset result.statusCode = 400 />
            <cfreturn result />
        </cfif>

        <cfset result.agencySchema = "agency_" & val(employeeQuery.Loc_ID) />
        <cfset result.locId = val(employeeQuery.Loc_ID) />

        <cfquery name="schemaCheckQuery" datasource="#Application.DataSrc#">
            SELECT SCHEMA_NAME
            FROM information_schema.schemata
            WHERE SCHEMA_NAME = <cfqueryparam cfsqltype="cf_sql_varchar" value="#result.agencySchema#">
        </cfquery>
        <cfif schemaCheckQuery.recordcount EQ 0>
            <cfset result.message = "Agency database not found" />
            <cfset result.statusCode = 400 />
            <cfreturn result />
        </cfif>

        <cfset result.ok = true />
        <cfset result.message = "" />
        <cfreturn result />
    </cffunction>

    <cffunction name="getScheduleForEmployee" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfset var result = { ok = false, message = "", schedule = {} } />
        <cfset var scheduleQuery = "" />

        <cfif arguments.scheduleId LTE 0>
            <cfset result.message = "Schedule_ID is required" />
            <cfreturn result />
        </cfif>

        <cfquery name="scheduleQuery" datasource="#Application.DataSrc#">
            SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Visit_Date, Skill, Status
            FROM #arguments.agencySchema#.pSchedules
            WHERE Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">
              AND Status = 0
            LIMIT 1
        </cfquery>
        <cfif scheduleQuery.recordcount EQ 0>
            <cfset result.message = "Schedule not found or inactive" />
            <cfreturn result />
        </cfif>
        <cfif val(scheduleQuery.Emp_ID) NEQ val(arguments.empId)>
            <cfset result.message = "Schedule is not assigned to this employee" />
            <cfreturn result />
        </cfif>

        <cfset result.ok = true />
        <cfset result.schedule = {
            Schedule_ID = toJsonInt(scheduleQuery.Schedule_ID),
            Patient_ID = toJsonInt(scheduleQuery.Patient_ID),
            Assmt_ID = toJsonInt(scheduleQuery.Assmt_ID),
            Emp_ID = toJsonInt(scheduleQuery.Emp_ID),
            Visit_Type = safeTrim(scheduleQuery.Visit_Type),
            Visit_Date = isDate(scheduleQuery.Visit_Date) ? dateFormat(scheduleQuery.Visit_Date, "yyyy-mm-dd") : safeTrim(scheduleQuery.Visit_Date),
            Skill = safeTrim(scheduleQuery.Skill)
        } />
        <cfreturn result />
    </cffunction>

    <cffunction name="parseProgressNotesRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="scheduleIdArg" type="string" required="false" default="" />
        <cfset var out = { securityKey = trim(arguments.securityKeyArg), scheduleId = val(trim(arguments.scheduleIdArg)) } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />

        <cfif NOT len(out.securityKey) AND structKeyExists(form, "securityKey")>
            <cfset out.securityKey = trim(form.securityKey) />
        </cfif>
        <cfif out.scheduleId LTE 0 AND structKeyExists(form, "Schedule_ID")>
            <cfset out.scheduleId = val(trim(form.Schedule_ID)) />
        </cfif>
        <cfif out.scheduleId LTE 0 AND structKeyExists(form, "schedule_id")>
            <cfset out.scheduleId = val(trim(form.schedule_id)) />
        </cfif>

        <cfif NOT len(out.securityKey) OR out.scheduleId LTE 0>
            <cftry>
                <cfset httpData = getHttpRequestData() />
                <cfif structKeyExists(httpData, "content") AND isBinary(httpData.content)>
                    <cfset bodyText = toString(httpData.content) />
                <cfelseif structKeyExists(httpData, "content")>
                    <cfset bodyText = "" & httpData.content />
                </cfif>
                <cfif len(trim(bodyText)) AND left(trim(bodyText), 1) EQ "{">
                    <cfset bodyJson = deserializeJSON(bodyText) />
                    <cfif NOT len(out.securityKey) AND structKeyExists(bodyJson, "securityKey")>
                        <cfset out.securityKey = trim("" & bodyJson.securityKey) />
                    </cfif>
                    <cfif out.scheduleId LTE 0 AND structKeyExists(bodyJson, "Schedule_ID")>
                        <cfset out.scheduleId = val(trim("" & bodyJson.Schedule_ID)) />
                    </cfif>
                    <cfif out.scheduleId LTE 0 AND structKeyExists(bodyJson, "schedule_id")>
                        <cfset out.scheduleId = val(trim("" & bodyJson.schedule_id)) />
                    </cfif>
                </cfif>
                <cfcatch type="any"></cfcatch>
            </cftry>
        </cfif>
        <cfreturn out />
    </cffunction>

    <cffunction name="getStructValueCI" access="private" returntype="any" output="false">
        <cfargument name="data" type="struct" required="true" />
        <cfargument name="keyName" type="string" required="true" />
        <cfargument name="defaultValue" required="false" default="" />
        <cfset var k = "" />
        <cfif structKeyExists(arguments.data, arguments.keyName)>
            <cfreturn arguments.data[arguments.keyName] />
        </cfif>
        <cfloop collection="#arguments.data#" item="k">
            <cfif compareNoCase(k, arguments.keyName) EQ 0>
                <cfreturn arguments.data[k] />
            </cfif>
        </cfloop>
        <cfreturn arguments.defaultValue />
    </cffunction>

    <cffunction name="pickSaveFieldValue" access="private" returntype="string" output="false"
        hint="Read flat or nested GET-style field values from a save item payload.">
        <cfargument name="item" type="struct" required="true" />
        <cfargument name="keys" type="string" required="true" />
        <cfargument name="nestedObjectKeys" type="string" required="false" default="" />
        <cfset var keyName = "" />
        <cfset var nestedKey = "" />
        <cfset var nestedObj = {} />
        <cfset var v = "" />

        <cfloop list="#arguments.keys#" index="keyName">
            <cfif structKeyExists(arguments.item, keyName)>
                <cfset v = arguments.item[keyName] />
                <cfif isStruct(v) AND structKeyExists(v, "value")>
                    <cfreturn safeTrim(v.value) />
                </cfif>
                <cfif isArray(v)>
                    <cfreturn arrayToList(v, ",") />
                </cfif>
                <cfreturn safeTrim(v) />
            </cfif>
        </cfloop>

        <cfloop list="#arguments.nestedObjectKeys#" index="nestedKey">
            <cfif structKeyExists(arguments.item, nestedKey) AND isStruct(arguments.item[nestedKey])>
                <cfset nestedObj = arguments.item[nestedKey] />
                <cfif structKeyExists(nestedObj, "value")>
                    <cfset v = nestedObj.value />
                    <cfif isArray(v)>
                        <cfreturn arrayToList(v, ",") />
                    </cfif>
                    <cfreturn safeTrim(v) />
                </cfif>
            </cfif>
        </cfloop>
        <cfreturn "" />
    </cffunction>

    <cffunction name="parseProgressNotesSaveRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="scheduleIdArg" type="string" required="false" default="" />
        <cfargument name="assmtIdArg" type="string" required="false" default="" />
        <cfargument name="itemsArg" type="string" required="false" default="" />
        <cfset var out = {
            securityKey = trim(arguments.securityKeyArg),
            scheduleId = val(trim(arguments.scheduleIdArg)),
            assmtId = val(trim(arguments.assmtIdArg)),
            items = [],
            patient_complaint = {}
        } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var itemsRaw = "" />

        <cfif NOT len(out.securityKey) AND structKeyExists(form, "securityKey")>
            <cfset out.securityKey = trim(form.securityKey) />
        </cfif>
        <cfif out.scheduleId LTE 0 AND structKeyExists(form, "Schedule_ID")>
            <cfset out.scheduleId = val(trim(form.Schedule_ID)) />
        </cfif>
        <cfif out.assmtId LTE 0 AND structKeyExists(form, "Assmt_ID")>
            <cfset out.assmtId = val(trim(form.Assmt_ID)) />
        </cfif>
        <cfif len(trim(arguments.itemsArg))>
            <cftry>
                <cfset itemsRaw = deserializeJSON(trim(arguments.itemsArg)) />
                <cfif isArray(itemsRaw)><cfset out.items = itemsRaw /></cfif>
                <cfcatch type="any"></cfcatch>
            </cftry>
        </cfif>

        <cftry>
            <cfset httpData = getHttpRequestData() />
            <cfif structKeyExists(httpData, "content") AND isBinary(httpData.content)>
                <cfset bodyText = toString(httpData.content) />
            <cfelseif structKeyExists(httpData, "content")>
                <cfset bodyText = "" & httpData.content />
            </cfif>
            <cfif len(trim(bodyText)) AND left(trim(bodyText), 1) EQ "{">
                <cfset bodyJson = deserializeJSON(bodyText) />
                <cfif NOT len(out.securityKey)>
                    <cfset out.securityKey = safeTrim(getStructValueCI(bodyJson, "securityKey", "")) />
                </cfif>
                <cfif out.scheduleId LTE 0>
                    <cfset out.scheduleId = val(safeTrim(getStructValueCI(bodyJson, "Schedule_ID", 0))) />
                    <cfif out.scheduleId LTE 0>
                        <cfset out.scheduleId = val(safeTrim(getStructValueCI(bodyJson, "schedule_id", 0))) />
                    </cfif>
                </cfif>
                <cfif out.assmtId LTE 0>
                    <cfset out.assmtId = val(safeTrim(getStructValueCI(bodyJson, "Assmt_ID", 0))) />
                    <cfif out.assmtId LTE 0>
                        <cfset out.assmtId = val(safeTrim(getStructValueCI(bodyJson, "assmt_id", 0))) />
                    </cfif>
                </cfif>
                <cfif structKeyExists(bodyJson, "items") AND isArray(bodyJson.items)>
                    <cfset out.items = bodyJson.items />
                </cfif>
                <cfif structKeyExists(bodyJson, "patient_complaint") AND isStruct(bodyJson.patient_complaint)>
                    <cfset out.patient_complaint = bodyJson.patient_complaint />
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>
        <cfreturn out />
    </cffunction>

    <cffunction name="normalizeProgressSaveItem" access="private" returntype="struct" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfset var wound = {} />
        <cfset var out = {
            progress_id = val(pickSaveFieldValue(arguments.item, "progress_id,Progress_ID,id,ID")),
            notes = pickSaveFieldValue(arguments.item, "notes,Notes", "notes"),
            yes_or_no = pickSaveFieldValue(arguments.item, "yes_or_no,YesOrNo", "yes_or_no,scale"),
            severity = pickSaveFieldValue(arguments.item, "severity,Severity", "severity"),
            physician_alert = pickSaveFieldValue(arguments.item, "physician_alert,Physician_alert", "physician_alert"),
            value_1 = pickSaveFieldValue(arguments.item, "value_1,Value_1,measure_1,Measure_1"),
            value_2 = pickSaveFieldValue(arguments.item, "value_2,Value_2,measure_2,Measure_2"),
            value_3 = pickSaveFieldValue(arguments.item, "value_3,Value_3,measure_3,Measure_3"),
            value_sum = pickSaveFieldValue(arguments.item, "value_sum,Value_Sum"),
            healing_stage = pickSaveFieldValue(arguments.item, "healing_stage,Healing_stage"),
            description = pickSaveFieldValue(arguments.item, "description,Description"),
            discharge = pickSaveFieldValue(arguments.item, "discharge,Discharge"),
            discharge_reason = pickSaveFieldValue(arguments.item, "discharge_reason,Discharge_Reason"),
            pain_location = pickSaveFieldValue(arguments.item, "pain_location"),
            pain_duration = pickSaveFieldValue(arguments.item, "pain_duration"),
            pain_relief = pickSaveFieldValue(arguments.item, "pain_relief"),
            pain_exacerbate = pickSaveFieldValue(arguments.item, "pain_exacerbate"),
            pain_med_effectiveness = pickSaveFieldValue(arguments.item, "pain_med_effectiveness"),
            activate = pickSaveFieldValue(arguments.item, "activate"),
            wound = {}
        } />

        <cfif structKeyExists(arguments.item, "wound") AND isStruct(arguments.item.wound)>
            <cfset wound = arguments.item.wound />
            <cfset out.wound = {
                wnd_bed = pickSaveFieldValue(wound, "wnd_bed"),
                wnd_margin = pickSaveFieldValue(wound, "wnd_margin", "margin"),
                wnd_exudate = pickSaveFieldValue(wound, "wnd_exudate", "exudate"),
                wnd_odor = pickSaveFieldValue(wound, "wnd_odor", "odor"),
                wnd_pain = pickSaveFieldValue(wound, "wnd_pain", "pain"),
                wnd_tunneling = pickSaveFieldValue(wound, "wnd_tunneling", "tunneling"),
                wnd_underm = pickSaveFieldValue(wound, "wnd_underm", "undermining"),
                wnd_gran = pickSaveFieldValue(wound, "wnd_gran", "pct_granulation"),
                wnd_epith = pickSaveFieldValue(wound, "wnd_epith", "pct_epithelial"),
                wnd_eschar = pickSaveFieldValue(wound, "wnd_eschar", "pct_eschar"),
                wnd_slough = pickSaveFieldValue(wound, "wnd_slough", "pct_slough"),
                wnd_fib = pickSaveFieldValue(wound, "wnd_fib", "pct_fibrinous")
            } />
        </cfif>
        <cfif NOT len(out.healing_stage) AND structKeyExists(arguments.item, "wound") AND isStruct(arguments.item.wound)>
            <cfset out.healing_stage = pickSaveFieldValue(arguments.item.wound, "healing_stage", "healing_stage") />
        </cfif>
        <cfreturn out />
    </cffunction>

    <cffunction name="computeProgressValueSum" access="private" returntype="string" output="false">
        <cfargument name="value1" type="string" required="false" default="" />
        <cfargument name="value2" type="string" required="false" default="" />
        <cfargument name="value3" type="string" required="false" default="" />
        <cfargument name="valueSum" type="string" required="false" default="" />
        <cfset var n1 = 0 />
        <cfset var n2 = 0 />
        <cfset var n3 = 0 />
        <cfset var total = 0 />

        <cfif len(trim(arguments.valueSum))>
            <cfreturn trim(arguments.valueSum) />
        </cfif>
        <cfif len(trim(arguments.value1)) AND isNumeric(arguments.value1)>
            <cfset n1 = val(arguments.value1) />
        </cfif>
        <cfif len(trim(arguments.value2)) AND isNumeric(arguments.value2)>
            <cfset n2 = val(arguments.value2) />
        </cfif>
        <cfif len(trim(arguments.value3)) AND isNumeric(arguments.value3)>
            <cfset n3 = val(arguments.value3) />
        </cfif>
        <cfset total = n1 + n2 + n3 />
        <cfif total NEQ 0>
            <cfreturn "" & total />
        </cfif>
        <cfreturn "" />
    </cffunction>

    <cffunction name="verifyProgressRowForSchedule" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="progressId" type="numeric" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfset var result = { ok = false, message = "", row = {} } />
        <cfset var qRow = "" />

        <cfif arguments.progressId LTE 0>
            <cfset result.message = "progress_id is required" />
            <cfreturn result />
        </cfif>

        <cfquery name="qRow" datasource="#Application.DataSrc#">
            SELECT Progress_ID, Schedule_ID, Assmt_ID, Type, Status
            FROM #arguments.agencySchema#.pProgress
            WHERE Progress_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.progressId#">
              AND Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            LIMIT 1
        </cfquery>
        <cfif qRow.recordcount EQ 0>
            <cfset result.message = "Progress row not found for this schedule" />
            <cfreturn result />
        </cfif>
        <cfset result.ok = true />
        <cfset result.row = {
            Progress_ID = qRow.Progress_ID,
            Schedule_ID = qRow.Schedule_ID,
            Assmt_ID = qRow.Assmt_ID,
            Type = safeTrim(qRow.Type)
        } />
        <cfreturn result />
    </cffunction>

    <cffunction name="hasProgressSaveFields" access="private" returntype="boolean" output="false">
        <cfargument name="normalized" type="struct" required="true" />
        <cfargument name="valueSum" type="string" required="false" default="" />
        <cfset var woundKey = "" />

        <cfif len(arguments.normalized.notes)
            OR len(arguments.normalized.description)
            OR len(arguments.normalized.yes_or_no)
            OR len(arguments.normalized.severity)
            OR len(arguments.normalized.physician_alert)
            OR len(arguments.normalized.value_1)
            OR len(arguments.normalized.value_2)
            OR len(arguments.normalized.value_3)
            OR len(arguments.valueSum)
            OR len(arguments.normalized.healing_stage)
            OR len(arguments.normalized.discharge)
            OR len(arguments.normalized.discharge_reason)
            OR len(arguments.normalized.pain_location)
            OR len(arguments.normalized.pain_duration)
            OR len(arguments.normalized.pain_relief)
            OR len(arguments.normalized.pain_exacerbate)
            OR len(arguments.normalized.pain_med_effectiveness)
            OR len(arguments.normalized.activate)>
            <cfreturn true />
        </cfif>
        <cfloop collection="#arguments.normalized.wound#" item="woundKey">
            <cfif len(arguments.normalized.wound[woundKey])>
                <cfreturn true />
            </cfif>
        </cfloop>
        <cfreturn false />
    </cffunction>

    <cffunction name="saveMobileProgressNoteItem" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="item" type="struct" required="true" />

        <cfset var result = { action = "skipped", progress_id = 0, reason = "" } />
        <cfset var normalized = normalizeProgressSaveItem(arguments.item) />
        <cfset var verify = {} />
        <cfset var woundKey = "" />
        <cfset var valueSum = "" />
        <cfset var hasSet = false />

        <cfset result.progress_id = toJsonInt(normalized.progress_id) />
        <cfif normalized.progress_id LTE 0>
            <cfset result.reason = "missing progress_id" />
            <cfreturn result />
        </cfif>

        <cfset verify = verifyProgressRowForSchedule(
            agencySchema = arguments.agencySchema,
            progressId = normalized.progress_id,
            scheduleId = arguments.scheduleId
        ) />
        <cfif NOT verify.ok>
            <cfset result.reason = verify.message />
            <cfreturn result />
        </cfif>

        <cfset valueSum = computeProgressValueSum(normalized.value_1, normalized.value_2, normalized.value_3, normalized.value_sum) />

        <cfif NOT hasProgressSaveFields(normalized, valueSum)
            AND NOT (verify.row.Type EQ "goal" AND len(normalized.yes_or_no))>
            <cfset result.reason = "no fields to update" />
            <cfreturn result />
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pProgress SET
                <cfif len(normalized.notes)>
                    Notes = <cfqueryparam value="#normalized.notes#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.description)>
                    <cfif hasSet>,</cfif>Description = <cfqueryparam value="#normalized.description#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.yes_or_no)>
                    <cfif hasSet>,</cfif>YesOrNo = <cfqueryparam value="#normalized.yes_or_no#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.severity)>
                    <cfif hasSet>,</cfif>Severity = <cfqueryparam value="#normalized.severity#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.physician_alert)>
                    <cfif hasSet>,</cfif>Physician_alert = <cfqueryparam value="#normalized.physician_alert#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.value_1)>
                    <cfif hasSet>,</cfif>Value_1 = <cfqueryparam value="#normalized.value_1#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.value_2)>
                    <cfif hasSet>,</cfif>Value_2 = <cfqueryparam value="#normalized.value_2#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.value_3)>
                    <cfif hasSet>,</cfif>Value_3 = <cfqueryparam value="#normalized.value_3#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(valueSum)>
                    <cfif hasSet>,</cfif>Value_Sum = <cfqueryparam value="#valueSum#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.healing_stage)>
                    <cfif hasSet>,</cfif>healing_stage = <cfqueryparam value="#normalized.healing_stage#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.discharge)>
                    <cfif hasSet>,</cfif>Discharge = <cfqueryparam value="#normalized.discharge#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.discharge_reason)>
                    <cfif hasSet>,</cfif>Discharge_Reason = <cfqueryparam value="#normalized.discharge_reason#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.pain_location)>
                    <cfif hasSet>,</cfif>pain_location = <cfqueryparam value="#normalized.pain_location#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.pain_duration)>
                    <cfif hasSet>,</cfif>pain_duration = <cfqueryparam value="#normalized.pain_duration#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.pain_relief)>
                    <cfif hasSet>,</cfif>pain_relief = <cfqueryparam value="#normalized.pain_relief#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.pain_exacerbate)>
                    <cfif hasSet>,</cfif>pain_exacerbate = <cfqueryparam value="#normalized.pain_exacerbate#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.pain_med_effectiveness)>
                    <cfif hasSet>,</cfif>pain_med_effectiveness = <cfqueryparam value="#normalized.pain_med_effectiveness#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                </cfif>
                <cfif len(normalized.activate)>
                    <cfif hasSet>,</cfif>activate = <cfqueryparam value="#val(normalized.activate)#" cfsqltype="cf_sql_integer"><cfset hasSet = true />
                <cfelseif verify.row.Type EQ "goal" AND normalized.yes_or_no EQ "4">
                    <cfif hasSet>,</cfif>activate = <cfqueryparam value="0" cfsqltype="cf_sql_integer"><cfset hasSet = true />
                <cfelseif verify.row.Type EQ "goal" AND len(normalized.yes_or_no) AND normalized.yes_or_no NEQ "4">
                    <cfif hasSet>,</cfif>activate = <cfqueryparam value="1" cfsqltype="cf_sql_integer"><cfset hasSet = true />
                </cfif>
                <cfloop collection="#normalized.wound#" item="woundKey">
                    <cfif listFindNoCase("wnd_bed,wnd_margin,wnd_exudate,wnd_odor,wnd_pain,wnd_tunneling,wnd_underm,wnd_gran,wnd_epith,wnd_eschar,wnd_slough,wnd_fib", woundKey)
                        AND len(normalized.wound[woundKey])>
                        <cfif hasSet>,</cfif>#woundKey# = <cfqueryparam value="#normalized.wound[woundKey]#" cfsqltype="cf_sql_varchar"><cfset hasSet = true />
                    </cfif>
                </cfloop>
                <cfif hasSet>,</cfif>
                Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
            WHERE Progress_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#normalized.progress_id#">
              AND Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
        </cfquery>

        <cfset result.action = "updated" />
        <cfset result.type = verify.row.Type />
        <cfreturn result />
    </cffunction>

    <cffunction name="saveMobilePatientComplaint" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="complaint" type="struct" required="true" />

        <cfset var result = { action = "skipped", progress_id = 0, reason = "" } />
        <cfset var progressId = val(pickSaveFieldValue(arguments.complaint, "progress_id,Progress_ID,id,ID")) />
        <cfset var description = pickSaveFieldValue(arguments.complaint, "description,Description,value") />

        <cfif NOT len(description)>
            <cfset result.reason = "patient_complaint description is empty" />
            <cfreturn result />
        </cfif>

        <cfif progressId GT 0>
            <cfquery datasource="#Application.DataSrc#">
                UPDATE #arguments.agencySchema#.pProgress SET
                    Description = <cfqueryparam value="#description#" cfsqltype="cf_sql_varchar">,
                    Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                    Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
                WHERE Progress_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#progressId#">
                  AND Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">
                  AND Type = <cfqueryparam value="patient_complaint" cfsqltype="cf_sql_varchar">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            </cfquery>
            <cfset result.action = "updated" />
            <cfset result.progress_id = toJsonInt(progressId) />
        <cfelse>
            <cfquery datasource="#Application.DataSrc#" result="insComplaint">
                INSERT INTO #arguments.agencySchema#.pProgress (
                    Assmt_ID, Schedule_ID, Description, Status, Type,
                    Date_Create, Created_by, Date_Change, Change_by
                ) VALUES (
                    <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">,
                    <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">,
                    <cfqueryparam cfsqltype="cf_sql_varchar" value="#description#">,
                    <cfqueryparam cfsqltype="cf_sql_integer" value="0">,
                    <cfqueryparam cfsqltype="cf_sql_varchar" value="patient_complaint">,
                    <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                    <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                    <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
                )
            </cfquery>
            <cfset result.action = "inserted" />
            <cfif structKeyExists(insComplaint, "generatedKey") AND val(insComplaint.generatedKey) GT 0>
                <cfset result.progress_id = toJsonInt(insComplaint.generatedKey) />
            <cfelseif structKeyExists(insComplaint, "GENERATEDKEY") AND val(insComplaint.GENERATEDKEY) GT 0>
                <cfset result.progress_id = toJsonInt(insComplaint.GENERATEDKEY) />
            <cfelse>
                <cfset result.progress_id = 0 />
            </cfif>
        </cfif>
        <cfreturn result />
    </cffunction>

    <cffunction name="deactivateResolvedGoalsForSchedule" access="private" returntype="void" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pProgress SET
                activate = <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
            WHERE Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">
              AND Type = <cfqueryparam value="goal" cfsqltype="cf_sql_varchar">
              AND YesOrNo = <cfqueryparam value="4" cfsqltype="cf_sql_varchar">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
        </cfquery>
    </cffunction>

    <cffunction name="scaleOptions0to4" access="private" returntype="array" output="false">
        <cfreturn [
            { value = "0", label = "0" },
            { value = "1", label = "1" },
            { value = "2", label = "2" },
            { value = "3", label = "3" },
            { value = "4", label = "4" }
        ] />
    </cffunction>

    <cffunction name="physicianAlertField" access="private" returntype="struct" output="false">
        <cfargument name="value" required="false" default="" />
        <cfset var alertVal = val(arguments.value) />
        <cfreturn {
            description = "Physician Alerted?",
            description_type = "radio",
            field = "Physician_alert",
            value = toJsonInt(alertVal),
            options = [
                { value = toJsonInt(1), label = "Yes" },
                { value = toJsonInt(0), label = "No" }
            ]
        } />
    </cffunction>

    <cffunction name="notesField" access="private" returntype="struct" output="false">
        <cfargument name="value" required="false" default="" />
        <cfreturn {
            description = "Notes",
            description_type = "textbox",
            field = "Notes",
            value = safeTrim(arguments.value)
        } />
    </cffunction>

    <cffunction name="groupItemsByHeading" access="private" returntype="array" output="false">
        <cfargument name="items" type="array" required="true" />
        <cfargument name="headingKey" type="string" required="false" default="heading" />
        <cfset var groups = [] />
        <cfset var indexMap = {} />
        <cfset var i = 0 />
        <cfset var item = {} />
        <cfset var heading = "" />
        <cfset var gIdx = 0 />

        <cfloop from="1" to="#arrayLen(arguments.items)#" index="i">
            <cfset item = arguments.items[i] />
            <cfset heading = structKeyExists(item, arguments.headingKey) ? safeTrim(item[arguments.headingKey]) : "" />
            <cfif NOT len(heading)>
                <cfset heading = "" />
            </cfif>
            <cfif NOT structKeyExists(indexMap, heading)>
                <cfset arrayAppend(groups, { heading = heading, items = [] }) />
                <cfset indexMap[heading] = arrayLen(groups) />
            </cfif>
            <cfset gIdx = indexMap[heading] />
            <cfset arrayAppend(groups[gIdx].items, item) />
        </cfloop>
        <cfreturn groups />
    </cffunction>

    <!--- Matches progress_notesnew.cfm getunstable: category bar for Careplan_ID=0 symptom prompts --->
    <cffunction name="lookupUnstableSystemDescription" access="private" returntype="string" output="false">
        <cfargument name="description" type="string" required="true" />
        <cfargument name="m0Nos" type="string" required="false" default="" />
        <cfargument name="unstableQ" type="query" required="true" />
        <cfset var prefix = left(arguments.description, 29) />
        <cfset var row = 0 />
        <cfloop from="1" to="#arguments.unstableQ.recordcount#" index="row">
            <cfif left(safeTrim(arguments.unstableQ.Question[row]), 29) NEQ prefix>
                <cfcontinue />
            </cfif>
            <cfif len(arguments.m0Nos) AND listFindNoCase(arguments.m0Nos, safeTrim(arguments.unstableQ.unstable_system[row]))>
                <cfcontinue />
            </cfif>
            <cfreturn safeTrim(arguments.unstableQ.system_description[row]) />
        </cfloop>
        <cfreturn "" />
    </cffunction>

    <!--- Group heading for symptoms (matches progress_notesnew.cfm group="Header"; not the Careplan_ID=0 unstable sub-bar) --->
    <cffunction name="resolveSymptomGroupHeading" access="private" returntype="string" output="false">
        <cfargument name="q" type="query" required="true" />
        <cfargument name="rowIndex" type="numeric" required="true" />
        <cfargument name="careplanId" type="numeric" required="true" />
        <cfargument name="woundTitle" type="string" required="false" default="" />
        <cfset var sqlHeader = safeTrim(queryCol(arguments.q, "Header", arguments.rowIndex, "")) />
        <cfset var pathHeader = safeTrim(queryCol(arguments.q, "path_header", arguments.rowIndex, "")) />
        <cfset var oldHeader = safeTrim(queryCol(arguments.q, "old_header", arguments.rowIndex, "")) />
        <cfif len(arguments.woundTitle)>
            <cfreturn safeTrim(arguments.woundTitle) />
        </cfif>
        <!--- Careplan-linked rows: pathway section (e.g. Case Management) wins over unstable M0_No join in Header CASE --->
        <cfif arguments.careplanId GT 0>
            <cfif len(pathHeader)><cfreturn pathHeader /></cfif>
            <cfif len(sqlHeader)><cfreturn sqlHeader /></cfif>
            <cfif len(oldHeader)><cfreturn oldHeader /></cfif>
            <cfreturn "" />
        </cfif>
        <cfif len(sqlHeader)><cfreturn sqlHeader /></cfif>
        <cfif len(pathHeader)><cfreturn pathHeader /></cfif>
        <cfif len(oldHeader)><cfreturn oldHeader /></cfif>
        <cfreturn "" />
    </cffunction>

    <!--- Unstable sub-bar title for Careplan_ID=0 rows only (progress_notesnew.cfm getunstable) --->
    <cffunction name="resolveSymptomSubheading" access="private" returntype="string" output="false">
        <cfargument name="careplanId" type="numeric" required="true" />
        <cfargument name="description" type="string" required="true" />
        <cfargument name="m0Nos" type="string" required="false" default="" />
        <cfargument name="unstableQ" type="query" required="true" />
        <cfif arguments.careplanId NEQ 0>
            <cfreturn "" />
        </cfif>
        <cfreturn lookupUnstableSystemDescription(
            description = arguments.description,
            m0Nos = arguments.m0Nos,
            unstableQ = arguments.unstableQ
        ) />
    </cffunction>

    <!--- Mobile page title: unstable subheading for report-on rows; otherwise group heading (inherit when blank) --->
    <cffunction name="resolveSymptomPageTitle" access="private" returntype="string" output="false">
        <cfargument name="groupHeading" type="string" required="true" />
        <cfargument name="subheading" type="string" required="true" />
        <cfargument name="previousTitle" type="string" required="false" default="" />
        <cfset var title = "" />
        <cfif len(trim(arguments.subheading))>
            <cfreturn trim(arguments.subheading) />
        </cfif>
        <cfset title = trim(arguments.groupHeading) />
        <cfif len(title)>
            <cfreturn title />
        </cfif>
        <cfreturn trim(arguments.previousTitle) />
    </cffunction>

    <!--- Matches progress_notesnew.cfm: severity radios only when Careplan_ID NEQ 0 (report-on / unstable rows use notes only) --->
    <cffunction name="shouldHideSymptomSeverity" access="private" returntype="boolean" output="false">
        <cfargument name="careplanId" type="numeric" required="true" />
        <cfargument name="description" type="string" required="true" />
        <cfargument name="isWound" type="boolean" required="true" />
        <cfif arguments.isWound>
            <cfreturn false />
        </cfif>
        <cfif arguments.careplanId EQ 0>
            <cfreturn true />
        </cfif>
        <cfif len(arguments.description) GTE 9 AND compareNoCase(left(arguments.description, 9), "report on") EQ 0>
            <cfreturn true />
        </cfif>
        <cfreturn false />
    </cffunction>

    <!--- Drop top-level items when groups are present (avoids duplicate rows in JSON) --->
    <cffunction name="finalizeGroupedSection" access="private" returntype="struct" output="false">
        <cfargument name="section" type="struct" required="true" />
        <cfif structKeyExists(arguments.section, "groups") AND isArray(arguments.section.groups) AND arrayLen(arguments.section.groups) GT 0>
            <cfset structDelete(arguments.section, "items") />
        </cfif>
        <cfreturn arguments.section />
    </cffunction>

    <!--- Skip duplicate Progress_ID rows from join-heavy queries --->
    <cffunction name="markProgressIdSeen" access="private" returntype="boolean" output="false">
        <cfargument name="seenIds" type="struct" required="true" />
        <cfargument name="progressId" type="numeric" required="true" />
        <cfif arguments.progressId LTE 0>
            <cfreturn true />
        </cfif>
        <cfif structKeyExists(arguments.seenIds, arguments.progressId)>
            <cfreturn false />
        </cfif>
        <cfset arguments.seenIds[arguments.progressId] = true />
        <cfreturn true />
    </cffunction>

    <cffunction name="formatPatientComplaint" access="private" returntype="struct" output="false">
        <cfargument name="q" type="query" required="true" />
        <cfargument name="visible" type="boolean" required="true" />
        <cfset var section = {
            key = "patient_complaint",
            heading = "Patient Complaints",
            visible = arguments.visible,
            items = []
        } />
        <cfset var item = {} />
        <cfif arguments.q.recordcount GT 0>
            <cfset item = {
                progress_id = toJsonInt(queryCol(arguments.q, "Progress_ID", 1, 0)),
                description = "Patient Complaints",
                description_type = "textbox",
                field = "Description",
                value = safeTrim(queryCol(arguments.q, "Description", 1, "")),
                type = "patient_complaint"
            } />
            <cfset arrayAppend(section.items, item) />
        </cfif>
        <cfreturn section />
    </cffunction>

    <cffunction name="formatScaleSection" access="private" returntype="struct" output="false"
        hint="Patient goals / goals / discharge: description label + notes textbox + YesOrNo radio 0-4. flatList=true omits GROUPS and per-item HEADING.">
        <cfargument name="key" type="string" required="true" />
        <cfargument name="heading" type="string" required="true" />
        <cfargument name="q" type="query" required="true" />
        <cfargument name="visible" type="boolean" required="true" />
        <cfargument name="scaleLegend" type="string" required="false" default="0-0% 1-25% 2-50% 3-75% 4-100% met" />
        <cfargument name="includePhysicianAlert" type="boolean" required="false" default="false" />
        <cfargument name="includeTargetDate" type="boolean" required="false" default="false" />
        <cfargument name="flatList" type="boolean" required="false" default="false" />

        <cfset var section = {
            key = arguments.key,
            heading = arguments.heading,
            visible = arguments.visible,
            scale_legend = arguments.scaleLegend,
            groups = [],
            items = []
        } />
        <cfset var row = 0 />
        <cfset var item = {} />
        <cfset var headerVal = "" />
        <cfset var targetDate = "" />
        <cfset var seenIds = {} />
        <cfset var progressId = 0 />

        <cfloop from="1" to="#arguments.q.recordcount#" index="row">
            <cfset progressId = val(queryCol(arguments.q, "Progress_ID", row, 0)) />
            <cfif NOT markProgressIdSeen(seenIds, progressId)>
                <cfcontinue />
            </cfif>
            <cfset headerVal = safeTrim(queryCol(arguments.q, "path_header", row, "")) />
            <cfif NOT len(headerVal)>
                <cfset headerVal = safeTrim(queryCol(arguments.q, "Header", row, "")) />
            </cfif>
            <cfset targetDate = "" />
            <cfif arguments.includeTargetDate>
                <cfset targetDate = queryCol(arguments.q, "cp_date_target", row, "") />
                <cfif NOT isDate(targetDate)>
                    <cfset targetDate = queryCol(arguments.q, "Date_Target", row, "") />
                </cfif>
                <cfif isDate(targetDate)>
                    <cfset targetDate = dateFormat(targetDate, "yyyy-mm-dd") />
                <cfelse>
                    <cfset targetDate = safeTrim(targetDate) />
                </cfif>
            </cfif>

            <cfset item = {
                progress_id = toJsonInt(queryCol(arguments.q, "Progress_ID", row, 0)),
                item_id = toJsonInt(queryCol(arguments.q, "Item_ID", row, 0)),
                careplan_id = toJsonInt(queryCol(arguments.q, "Careplan_ID", row, 0)),
                type = safeTrim(queryCol(arguments.q, "Type", row, "")),
                description = safeTrim(queryCol(arguments.q, "Description", row, "")),
                description_type = "radio",
                scale = {
                    description = "Progress / met",
                    description_type = "radio",
                    field = "YesOrNo",
                    value = safeTrim(queryCol(arguments.q, "YesOrNo", row, "")),
                    options = scaleOptions0to4(),
                    legend = arguments.scaleLegend
                },
                notes = notesField(queryCol(arguments.q, "Notes", row, "")),
                date_target = targetDate
            } />
            <cfif NOT arguments.flatList>
                <cfset item.heading = headerVal />
            </cfif>
            <cfif arguments.includePhysicianAlert>
                <cfset item.physician_alert = physicianAlertField(queryCol(arguments.q, "Physician_alert", row, 0)) />
            </cfif>
            <cfif listFindNoCase(arguments.q.columnList, "activate")>
                <cfset item.activate = toJsonInt(queryCol(arguments.q, "activate", row, 0)) />
            </cfif>
            <cfset arrayAppend(section.items, item) />
        </cfloop>
        <cfif arguments.flatList>
            <cfset structDelete(section, "groups") />
            <cfreturn section />
        </cfif>
        <cfset section.groups = groupItemsByHeading(section.items, "heading") />
        <cfreturn finalizeGroupedSection(section) />
    </cffunction>

    <cffunction name="formatYesNoSection" access="private" returntype="struct" output="false"
        hint="Procedures / standing orders: Yes/No radio + notes. flatList=true omits GROUPS and per-item HEADING (mobile procedures list).">
        <cfargument name="key" type="string" required="true" />
        <cfargument name="heading" type="string" required="true" />
        <cfargument name="q" type="query" required="true" />
        <cfargument name="visible" type="boolean" required="true" />
        <cfargument name="flatList" type="boolean" required="false" default="false" />

        <cfset var section = {
            key = arguments.key,
            heading = arguments.heading,
            visible = arguments.visible,
            items = []
        } />
        <cfset var row = 0 />
        <cfset var item = {} />
        <cfset var headerVal = "" />
        <cfset var seenIds = {} />
        <cfset var progressId = 0 />

        <cfloop from="1" to="#arguments.q.recordcount#" index="row">
            <cfset progressId = val(queryCol(arguments.q, "Progress_ID", row, 0)) />
            <cfif NOT markProgressIdSeen(seenIds, progressId)>
                <cfcontinue />
            </cfif>
            <cfset item = {
                progress_id = toJsonInt(queryCol(arguments.q, "Progress_ID", row, 0)),
                item_id = toJsonInt(queryCol(arguments.q, "Item_ID", row, 0)),
                careplan_id = toJsonInt(queryCol(arguments.q, "Careplan_ID", row, 0)),
                type = safeTrim(queryCol(arguments.q, "Type", row, "")),
                description = safeTrim(queryCol(arguments.q, "Description", row, "")),
                description_type = "radio",
                yes_or_no = {
                    description = "Performed",
                    description_type = "radio",
                    field = "YesOrNo",
                    value = safeTrim(queryCol(arguments.q, "YesOrNo", row, "")),
                    options = [
                        { value = "Yes", label = "Yes" },
                        { value = "No", label = "No" }
                    ]
                },
                notes = notesField(queryCol(arguments.q, "Notes", row, ""))
            } />
            <cfif NOT arguments.flatList>
                <cfset headerVal = safeTrim(queryCol(arguments.q, "path_header", row, "")) />
                <cfif NOT len(headerVal)>
                    <cfset headerVal = safeTrim(queryCol(arguments.q, "Header", row, "")) />
                </cfif>
                <cfset item.heading = headerVal />
            </cfif>
            <cfset arrayAppend(section.items, item) />
        </cfloop>
        <cfif arguments.flatList>
            <cfreturn section />
        </cfif>
        <cfset section.groups = groupItemsByHeading(section.items, "heading") />
        <cfreturn finalizeGroupedSection(section) />
    </cffunction>

    <cffunction name="formatVitalsSection" access="private" returntype="struct" output="false">
        <cfargument name="q" type="query" required="true" />
        <cfargument name="visible" type="boolean" required="true" />
        <cfset var section = {
            key = "vitals",
            heading = "Vitals",
            visible = arguments.visible,
            items = []
        } />
        <cfset var row = 0 />
        <cfset var item = {} />
        <cfset var paramLo = "" />
        <cfset var paramHi = "" />
        <cfset var seenIds = {} />
        <cfset var progressId = 0 />

        <cfloop from="1" to="#arguments.q.recordcount#" index="row">
            <cfset progressId = val(queryCol(arguments.q, "Progress_ID", row, 0)) />
            <cfif NOT markProgressIdSeen(seenIds, progressId)>
                <cfcontinue />
            </cfif>
            <cfset paramLo = safeTrim(queryCol(arguments.q, "wound_param_lo", row, "")) />
            <cfif NOT len(paramLo)>
                <cfset paramLo = safeTrim(queryCol(arguments.q, "Param_Lo", row, "")) />
            </cfif>
            <cfset paramHi = safeTrim(queryCol(arguments.q, "wound_param_hi", row, "")) />
            <cfif NOT len(paramHi)>
                <cfset paramHi = safeTrim(queryCol(arguments.q, "Param_Hi", row, "")) />
            </cfif>

            <cfset item = {
                progress_id = toJsonInt(queryCol(arguments.q, "Progress_ID", row, 0)),
                item_id = toJsonInt(queryCol(arguments.q, "Item_ID", row, 0)),
                careplan_id = toJsonInt(queryCol(arguments.q, "Careplan_ID", row, 0)),
                wounds_vital_id = toJsonInt(queryCol(arguments.q, "woundsid", row, 0)),
                type = "vital",
                description = safeTrim(queryCol(arguments.q, "Description", row, "")),
                description_type = "select",
                value_1 = safeTrim(queryCol(arguments.q, "Value_1", row, "")),
                value_2 = safeTrim(queryCol(arguments.q, "Value_2", row, "")),
                value_sum = safeTrim(queryCol(arguments.q, "Value_Sum", row, "")),
                param_lo = paramLo,
                param_hi = paramHi,
                severity = safeTrim(queryCol(arguments.q, "Severity", row, "")),
                notes = notesField(queryCol(arguments.q, "Notes", row, "")),
                physician_alert = physicianAlertField(queryCol(arguments.q, "Physician_alert", row, 0)),
                pain = {
                    location = safeTrim(queryCol(arguments.q, "pain_location", row, "")),
                    duration = safeTrim(queryCol(arguments.q, "pain_duration", row, "")),
                    relief = safeTrim(queryCol(arguments.q, "pain_relief", row, "")),
                    exacerbate = safeTrim(queryCol(arguments.q, "pain_exacerbate", row, "")),
                    med_effectiveness = safeTrim(queryCol(arguments.q, "pain_med_effectiveness", row, ""))
                }
            } />
            <cfset arrayAppend(section.items, item) />
        </cfloop>
        <cfreturn section />
    </cffunction>

    <cffunction name="woundCheckboxGroup" access="private" returntype="struct" output="false">
        <cfargument name="description" type="string" required="true" />
        <cfargument name="field" type="string" required="true" />
        <cfargument name="options" type="array" required="true" />
        <cfargument name="selectedCsv" type="string" required="false" default="" />
        <cfset var selected = [] />
        <cfset var opt = "" />
        <cfset var i = 0 />
        <cfset var csv = lCase(arguments.selectedCsv) />

        <cfloop from="1" to="#arrayLen(arguments.options)#" index="i">
            <cfset opt = arguments.options[i] />
            <cfif len(csv) AND findNoCase(opt, csv)>
                <cfset arrayAppend(selected, opt) />
            </cfif>
        </cfloop>
        <cfreturn {
            description = arguments.description,
            description_type = "checkbox",
            field = arguments.field,
            options = arguments.options,
            value = selected,
            raw = safeTrim(arguments.selectedCsv)
        } />
    </cffunction>

    <cffunction name="formatSymptomsFromQuery" access="private" returntype="struct" output="false">
        <cfargument name="q" type="query" required="true" />
        <cfargument name="visible" type="boolean" required="true" />
        <cfargument name="skill" type="string" required="true" />
        <cfargument name="lookupSchema" type="string" required="true" />
        <cfargument name="m0Nos" type="string" required="false" default="" />

        <cfset var section = {
            key = "symptoms",
            heading = "SYMPTOMS",
            visible = arguments.visible,
            severity_legend = "0 - asymptomatic 1 - well-controlled 2 - controlled with difficulty 3 - poorly controlled 4 - hx hospitalizations",
            groups = [],
            items = []
        } />
        <cfset var row = 0 />
        <cfset var item = {} />
        <cfset var headerVal = "" />
        <cfset var woundTitle = "" />
        <cfset var desc = "" />
        <cfset var careplanId = 0 />
        <cfset var isWound = false />
        <cfset var isGG = false />
        <cfset var scaleOpts = [] />
        <cfset var scaleLegend = "" />
        <cfset var seenIds = {} />
        <cfset var progressId = 0 />
        <cfset var unstableLookup = "" />
        <cfset var subheadingVal = "" />
        <cfset var previousGroupHeading = "" />
        <cfset var previousTitle = "" />
        <cfset var pageTitle = "" />

        <cfquery name="unstableLookup" datasource="#Application.DataSrc#">
            SELECT Question, system_description, `System` AS unstable_system
            FROM #arguments.lookupSchema#.pProgress_Unstable
        </cfquery>

        <cfloop from="1" to="#arguments.q.recordcount#" index="row">
            <cfset progressId = val(queryCol(arguments.q, "Progress_ID", row, 0)) />
            <cfif NOT markProgressIdSeen(seenIds, progressId)>
                <cfcontinue />
            </cfif>
            <cfset desc = safeTrim(queryCol(arguments.q, "Description", row, "")) />
            <cfset woundTitle = safeTrim(queryCol(arguments.q, "woundstitle", row, "")) />
            <cfset careplanId = val(queryCol(arguments.q, "Careplan_ID", row, 0)) />
            <cfset headerVal = resolveSymptomGroupHeading(
                q = arguments.q,
                rowIndex = row,
                careplanId = careplanId,
                woundTitle = woundTitle
            ) />
            <cfset subheadingVal = resolveSymptomSubheading(
                careplanId = careplanId,
                description = desc,
                m0Nos = arguments.m0Nos,
                unstableQ = unstableLookup
            ) />
            <cfif NOT len(headerVal) AND careplanId GT 0>
                <cfset headerVal = previousGroupHeading />
            </cfif>
            <cfset pageTitle = resolveSymptomPageTitle(
                groupHeading = headerVal,
                subheading = subheadingVal,
                previousTitle = previousTitle
            ) />
            <cfif len(headerVal)>
                <cfset previousGroupHeading = headerVal />
            </cfif>
            <cfif len(pageTitle)>
                <cfset previousTitle = pageTitle />
            </cfif>

            <cfset isWound = (len(woundTitle) GT 0) OR (left(desc, 1) EQ "##") />
            <cfset isGG = (left(desc, 2) EQ "GG") OR (findNoCase("GG", desc) EQ 1) />
            <cfset hideSeverity = shouldHideSymptomSeverity(
                careplanId = careplanId,
                description = desc,
                isWound = isWound
            ) />

            <cfif NOT hideSeverity>
                <cfif isGG AND arguments.skill NEQ "SN" AND arguments.skill NEQ "HHA">
                    <cfset scaleOpts = [
                        { value = "6", label = "6" },
                        { value = "5", label = "5" },
                        { value = "4", label = "4" },
                        { value = "3", label = "3" },
                        { value = "2", label = "2" },
                        { value = "1", label = "1" },
                        { value = "10", label = "not assessed" }
                    ] />
                    <cfset scaleLegend = "06 Independent / 05 Setup / 04 Supervision / 03 Partial / 02 Substantial / 01 Dependent" />
                <cfelse>
                    <cfset scaleOpts = scaleOptions0to4() />
                    <cfset scaleLegend = section.severity_legend />
                </cfif>
            </cfif>

            <cfset item = {
                progress_id = toJsonInt(queryCol(arguments.q, "Progress_ID", row, 0)),
                item_id = toJsonInt(queryCol(arguments.q, "Item_ID", row, 0)),
                careplan_id = toJsonInt(queryCol(arguments.q, "Careplan_ID", row, 0)),
                type = safeTrim(queryCol(arguments.q, "Type", row, "")),
                heading = headerVal,
                title = pageTitle,
                subheading = subheadingVal,
                woundstitle = woundTitle,
                description = desc,
                notes = notesField(queryCol(arguments.q, "Notes", row, "")),
                physician_alert = physicianAlertField(queryCol(arguments.q, "Physician_alert", row, 0)),
                value_1 = safeTrim(queryCol(arguments.q, "Value_1", row, "")),
                value_2 = safeTrim(queryCol(arguments.q, "Value_2", row, "")),
                value_3 = safeTrim(queryCol(arguments.q, "Value_3", row, "")),
                value_sum = safeTrim(queryCol(arguments.q, "Value_Sum", row, "")),
                is_wound = isWound,
                is_gg = isGG
            } />

            <cfif NOT hideSeverity>
                <cfset item.description_type = isWound ? "wound_assessment" : "radio" />
                <cfset item.severity = {
                    description = "Severity",
                    description_type = "radio",
                    field = "Severity",
                    value = safeTrim(queryCol(arguments.q, "Severity", row, "")),
                    options = scaleOpts,
                    legend = scaleLegend
                } />
            </cfif>

            <cfif isWound>
                <cfset item.wound = {
                    healing_stage = {
                        description = "Healing Stage",
                        description_type = "select",
                        field = "Value_Sum",
                        value = safeTrim(queryCol(arguments.q, "Value_Sum", row, ""))
                    },
                    dimensions = {
                        description = "Dimensions (L / W / D)",
                        description_type = "select",
                        length = safeTrim(queryCol(arguments.q, "Value_1", row, "")),
                        width = safeTrim(queryCol(arguments.q, "Value_2", row, "")),
                        depth = safeTrim(queryCol(arguments.q, "Value_3", row, ""))
                    },
                    tunneling = {
                        description = "tunneling dep/drctn",
                        description_type = "textbox",
                        field = "wnd_tunneling",
                        value = safeTrim(queryCol(arguments.q, "wnd_tunneling", row, ""))
                    },
                    undermining = {
                        description = "undermn dep/drctn",
                        description_type = "textbox",
                        field = "wnd_underm",
                        value = safeTrim(queryCol(arguments.q, "wnd_underm", row, ""))
                    },
                    pct_granulation = {
                        description = "% granulation",
                        description_type = "textbox",
                        field = "wnd_gran",
                        value = safeTrim(queryCol(arguments.q, "wnd_gran", row, ""))
                    },
                    pct_epithelial = {
                        description = "% epithelial",
                        description_type = "textbox",
                        field = "wnd_epith",
                        value = safeTrim(queryCol(arguments.q, "wnd_epith", row, ""))
                    },
                    pct_eschar = {
                        description = "% eschar",
                        description_type = "textbox",
                        field = "wnd_eschar",
                        value = safeTrim(queryCol(arguments.q, "wnd_eschar", row, ""))
                    },
                    pct_slough = {
                        description = "% slough",
                        description_type = "textbox",
                        field = "wnd_slough",
                        value = safeTrim(queryCol(arguments.q, "wnd_slough", row, ""))
                    },
                    pct_fibrinous = {
                        description = "% fibrinous",
                        description_type = "textbox",
                        field = "wnd_fib",
                        value = safeTrim(queryCol(arguments.q, "wnd_fib", row, ""))
                    },
                    margin = woundCheckboxGroup(
                        "Wound Margin",
                        "wnd_margin",
                        ["regular", "irregular", "rolled", "epithelialized", "macerated", "other"],
                        queryCol(arguments.q, "wnd_margin", row, "")
                    ),
                    exudate = woundCheckboxGroup(
                        "Wound Exudate",
                        "wnd_exudate",
                        ["none", "scant", "moderate", "large", "clear", "sero-sanguinous", "sanguinous", "purulent", "other"],
                        queryCol(arguments.q, "wnd_exudate", row, "")
                    ),
                    odor = woundCheckboxGroup(
                        "Wound Odor",
                        "wnd_odor",
                        ["none", "mild", "foul", "other"],
                        queryCol(arguments.q, "wnd_odor", row, "")
                    ),
                    pain = woundCheckboxGroup(
                        "Wound Pain",
                        "wnd_pain",
                        ["none", "mild", "moderate", "severe", "other"],
                        queryCol(arguments.q, "wnd_pain", row, "")
                    )
                } />
            </cfif>

            <cfset arrayAppend(section.items, item) />
        </cfloop>

        <cfset section.groups = groupItemsByHeading(section.items, "heading") />
        <cfreturn finalizeGroupedSection(section) />
    </cffunction>

    <cffunction name="ggAdlSeverityScale" access="private" returntype="struct" output="false"
        hint="PT/OT use 01-06 ADL scale; SN/HHA use 0-4 (matches progress_notesnew.cfm GG section).">
        <cfargument name="skill" type="string" required="true" />
        <cfargument name="description" type="string" required="true" />
        <cfset var out = { options = [], legend = "" } />
        <cfset var desc = safeTrim(arguments.description) />
        <cfset var useAdlScale = (
            compareNoCase(arguments.skill, "SN") NEQ 0
            AND compareNoCase(arguments.skill, "HHA") NEQ 0
            AND findNoCase("GG", desc) GT 0
        ) />

        <cfif useAdlScale>
            <cfset out.options = [
                { value = "6", label = "6" },
                { value = "5", label = "5" },
                { value = "4", label = "4" },
                { value = "3", label = "3" },
                { value = "2", label = "2" },
                { value = "1", label = "1" }
            ] />
            <cfset out.legend = "06 Independent / 05 Setup or cleanup assistance / 04 Supervision or touching assistance / 03 Partial/moderate assistance / 02 Substantial/maximal assistance / 01 Dependent" />
        <cfelse>
            <cfset out.options = scaleOptions0to4() />
            <cfset out.legend = "0 - asymptomatic 1 - well-controlled 2 - controlled with difficulty 3 - poorly controlled 4 - hx hospitalizations" />
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Mirrors progress_notesnew.cfm show_GG logic for PT/OT/SN --->
    <cffunction name="shouldShowGgSymptomRow" access="private" returntype="boolean" output="false">
        <cfargument name="skill" type="string" required="true" />
        <cfargument name="description" type="string" required="true" />
        <cfargument name="pathId" type="string" required="false" default="" />
        <cfargument name="careplanGoals" type="query" required="true" />
        <cfargument name="lookupSchema" type="string" required="true" />
        <cfargument name="datasource" type="string" required="true" />

        <cfset var qGGpathway = "" />
        <cfset var qGGgoals = "" />
        <cfset var goalsdescription = [] />
        <cfset var desc = safeTrim(arguments.description) />

        <cfif NOT len(trim(arguments.pathId))>
            <cfreturn false />
        </cfif>

        <cfquery name="qGGpathway" datasource="#arguments.datasource#">
            SELECT Item_ID, Description
            FROM #arguments.lookupSchema#.pPathway
            WHERE Pathway_ID = <cfqueryparam value="#arguments.pathId#" cfsqltype="cf_sql_varchar">
              AND type = <cfqueryparam value="goal" cfsqltype="cf_sql_varchar">
              AND Header IS NOT NULL
              AND Description LIKE <cfqueryparam value="% - 0%" cfsqltype="cf_sql_varchar">
        </cfquery>
        <cfif qGGpathway.recordcount EQ 0>
            <cfreturn false />
        </cfif>

        <cfquery name="qGGgoals" dbtype="query">
            SELECT *
            FROM careplanGoals
            WHERE Item_ID IN (<cfqueryparam value="#valueList(qGGpathway.Item_ID)#" cfsqltype="cf_sql_integer" list="true">)
        </cfquery>

        <cfif compareNoCase(arguments.skill, "SN") EQ 0>
            <cfreturn true />
        </cfif>
        <cfif left(desc, 2) NEQ "GG">
            <cfreturn true />
        </cfif>
        <cfif qGGgoals.recordcount GT 0>
            <cfset goalsdescription = listToArray(qGGgoals.Description, "-") />
            <cfif arrayLen(goalsdescription) GTE 2>
                <cfreturn true />
            </cfif>
        </cfif>
        <cfreturn false />
    </cffunction>

    <!--- Goal met + resolved → disable severity radios (block_radiobutton on web) --->
    <cffunction name="evaluateGgGoalMet" access="private" returntype="struct" output="false">
        <cfargument name="severity" type="string" required="true" />
        <cfargument name="pathId" type="string" required="false" default="" />
        <cfargument name="careplanGoals" type="query" required="true" />
        <cfargument name="lookupSchema" type="string" required="true" />
        <cfargument name="datasource" type="string" required="true" />

        <cfset var out = { goal_met = false, disabled = false, linked_goals = [] } />
        <cfset var qGGpathway = "" />
        <cfset var qGGgoals = "" />
        <cfset var goalsdescription = [] />
        <cfset var ggGoalSeverity = 0 />
        <cfset var goalDateResolve = "" />
        <cfset var secondPart = "" />
        <cfset var thirdPart = "" />

        <cfif NOT len(trim(arguments.pathId))>
            <cfreturn out />
        </cfif>

        <cfquery name="qGGpathway" datasource="#arguments.datasource#">
            SELECT Item_ID, Description
            FROM #arguments.lookupSchema#.pPathway
            WHERE Pathway_ID = <cfqueryparam value="#arguments.pathId#" cfsqltype="cf_sql_varchar">
              AND type = <cfqueryparam value="goal" cfsqltype="cf_sql_varchar">
              AND Header IS NOT NULL
              AND Description LIKE <cfqueryparam value="% - 0%" cfsqltype="cf_sql_varchar">
        </cfquery>
        <cfloop query="qGGpathway">
            <cfset arrayAppend(out.linked_goals, safeTrim(qGGpathway.Description)) />
        </cfloop>
        <cfif qGGpathway.recordcount EQ 0>
            <cfreturn out />
        </cfif>

        <cfquery name="qGGgoals" dbtype="query">
            SELECT *
            FROM careplanGoals
            WHERE Item_ID IN (<cfqueryparam value="#valueList(qGGpathway.Item_ID)#" cfsqltype="cf_sql_integer" list="true">)
        </cfquery>
        <cfif qGGgoals.recordcount EQ 0>
            <cfreturn out />
        </cfif>

        <cfset goalDateResolve = safeTrim(qGGgoals.Date_Resolve) />
        <cfset goalsdescription = listToArray(qGGgoals.Description, "-") />
        <cfif arrayLen(goalsdescription) GTE 2>
            <cfset secondPart = left(trim(goalsdescription[2]), 2) />
            <cfif isNumeric(secondPart)>
                <cfset ggGoalSeverity = val(secondPart) />
            <cfelseif arrayLen(goalsdescription) GTE 3>
                <cfset thirdPart = left(trim(goalsdescription[3]), 2) />
                <cfif isNumeric(thirdPart)>
                    <cfset ggGoalSeverity = val(thirdPart) />
                </cfif>
            </cfif>
        </cfif>
        <cfif val(arguments.severity) GTE ggGoalSeverity AND len(goalDateResolve) GT 0>
            <cfset out.goal_met = true />
            <cfset out.disabled = true />
        </cfif>
        <cfreturn out />
    </cffunction>

    <cffunction name="formatGgSymptomsSection" access="private" returntype="struct" output="false"
        hint="GG Symptoms (ADLs) — shown under GOALS on web; skill-filtered for PT/OT/SN.">
        <cfargument name="q" type="query" required="true" />
        <cfargument name="visible" type="boolean" required="true" />
        <cfargument name="skill" type="string" required="true" />
        <cfargument name="careplanGoals" type="query" required="true" />
        <cfargument name="lookupSchema" type="string" required="true" />
        <cfargument name="datasource" type="string" required="true" />

        <cfset var section = {
            key = "gg_symptoms",
            heading = "GG Symptoms (ADLs)",
            visible = arguments.visible,
            severity_legend = "06 Independent / 05 Setup or cleanup assistance / 04 Supervision or touching assistance / 03 Partial/moderate assistance / 02 Substantial/maximal assistance / 01 Dependent",
            groups = [],
            items = []
        } />
        <cfset var row = 0 />
        <cfset var item = {} />
        <cfset var headerVal = "" />
        <cfset var desc = "" />
        <cfset var scale = {} />
        <cfset var goalState = {} />
        <cfset var seenIds = {} />
        <cfset var progressId = 0 />
        <cfset var pathId = "" />
        <cfset var severityVal = "" />
        <cfset var careplanId = 0 />
        <cfset var hideSeverity = false />

        <cfloop from="1" to="#arguments.q.recordcount#" index="row">
            <cfset progressId = val(queryCol(arguments.q, "Progress_ID", row, 0)) />
            <cfif NOT markProgressIdSeen(seenIds, progressId)>
                <cfcontinue />
            </cfif>
            <cfset desc = safeTrim(queryCol(arguments.q, "Description", row, "")) />
            <cfset pathId = safeTrim(queryCol(arguments.q, "Path_ID", row, "")) />
            <cfif NOT shouldShowGgSymptomRow(
                skill = arguments.skill,
                description = desc,
                pathId = pathId,
                careplanGoals = arguments.careplanGoals,
                lookupSchema = arguments.lookupSchema,
                datasource = arguments.datasource
            )>
                <cfcontinue />
            </cfif>

            <cfset headerVal = safeTrim(queryCol(arguments.q, "Header", row, "")) />
            <cfif NOT len(headerVal)>
                <cfset headerVal = "ADLs" />
            </cfif>
            <cfset severityVal = safeTrim(queryCol(arguments.q, "Severity", row, "")) />
            <cfset careplanId = val(queryCol(arguments.q, "Careplan_ID", row, 0)) />
            <cfset hideSeverity = shouldHideSymptomSeverity(
                careplanId = careplanId,
                description = desc,
                isWound = false
            ) />
            <cfset scale = ggAdlSeverityScale(skill = arguments.skill, description = desc) />
            <cfset goalState = evaluateGgGoalMet(
                severity = severityVal,
                pathId = pathId,
                careplanGoals = arguments.careplanGoals,
                lookupSchema = arguments.lookupSchema,
                datasource = arguments.datasource
            ) />

            <cfset item = {
                progress_id = toJsonInt(progressId),
                item_id = toJsonInt(queryCol(arguments.q, "Item_ID", row, 0)),
                careplan_id = toJsonInt(careplanId),
                type = safeTrim(queryCol(arguments.q, "Type", row, "")),
                heading = headerVal,
                description = desc,
                is_gg = true,
                goal_met = goalState.goal_met,
                disabled = goalState.disabled,
                linked_goals = goalState.linked_goals,
                answer = safeTrim(queryCol(arguments.q, "Answer", row, "")),
                notes = notesField(queryCol(arguments.q, "Notes", row, "")),
                physician_alert = physicianAlertField(queryCol(arguments.q, "Physician_alert", row, 0))
            } />
            <cfif NOT hideSeverity>
                <cfset item.description_type = "radio" />
                <cfset item.severity = {
                    description = "Severity",
                    description_type = "radio",
                    field = "Severity",
                    value = severityVal,
                    options = scale.options,
                    legend = scale.legend,
                    disabled = goalState.disabled
                } />
            </cfif>
            <cfset arrayAppend(section.items, item) />
        </cfloop>

        <cfset section.groups = groupItemsByHeading(section.items, "heading") />
        <cfif arrayLen(section.items) EQ 0>
            <cfset section.visible = false />
        </cfif>
        <cfreturn finalizeGroupedSection(section) />
    </cffunction>

    <cffunction name="loadPgProblemsAllQuery" access="private" returntype="query" output="false"
        hint="Base pgproblemsall query shared by regular symptoms and GG symptoms.">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="lookupSchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfargument name="skill" type="string" required="true" />

        <cfset var admitId = 0 />
        <cfset var qAdmit = "" />
        <cfset var pgproblemsall = "" />

        <cfquery name="qAdmit" datasource="#Application.DataSrc#">
            SELECT Admit_ID
            FROM #arguments.agencySchema#.pAssessments
            WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND Status = 0
            LIMIT 1
        </cfquery>
        <cfif qAdmit.recordcount GT 0>
            <cfset admitId = val(qAdmit.Admit_ID) />
        </cfif>

        <cfquery name="pgproblemsall" datasource="#Application.DataSrc#">
            SELECT SUBSTRING_INDEX(pProgress.Description, '(', 1) AS Description1,
                   pProgress.*,
                   pProblem.Oasis_Deficits,
                   pProblem.Answer,
                   pProblem.ques_id,
                   pProblem.M0_No,
                   pPathway.Value_Prog,
                   pPathway.Header AS old_header,
                   pPathway.Header AS path_header,
                   pProblem.Path_ID,
                   CASE
                       WHEN pProgress_Unstable.system_description != '' THEN pProgress_Unstable.system_description
                       WHEN Careplan_ID > 0 THEN (
                           SELECT prob2.Description
                           FROM #arguments.agencySchema#.pProblem AS prob2
                           WHERE prob2.Path_ID = pProblem.Path_ID
                             AND prob2.M0_No LIKE 'M102%'
                             AND pProgress.Assmt_ID = prob2.Assmt_ID
                             AND prob2.status = 0
                           LIMIT 0, 1
                       )
                       ELSE NULL
                   END AS Header,
                   CASE
                       WHEN (pProblem.M0_No LIKE 'SK%' OR pProblem.M0_No = 'wound')
                            AND LEFT(pProgress.Description, 1) = '##'
                       THEN 'Wounds'
                       ELSE ''
                   END AS woundstitle,
                   pProblem.Description AS problemdescription,
                   pProblem.Problem_ID AS prob_id,
                   CASE
                       WHEN LEFT(pProgress.Description, 3) = 'GG0' THEN 20
                       WHEN pProgress_Unstable.system_description != '' THEN pProgress_Unstable.ID
                       WHEN Careplan_ID > 0 THEN 1000
                       ELSE pProgress_Unstable.ID
                   END AS header_title,
                   pPathway.Header_No
            FROM #arguments.agencySchema#.pProgress
            LEFT OUTER JOIN #arguments.agencySchema#.pProblem
                ON pProblem.Problem_ID = pProgress.Careplan_ID
               AND pProblem.status = 0
            INNER JOIN #arguments.agencySchema#.pAssessments
                ON pAssessments.Assmt_ID = pProgress.Assmt_ID
               AND pAssessments.status = 0
            INNER JOIN #arguments.agencySchema#.pAdmit
                ON pAdmit.Admit_ID = pAssessments.Admit_ID
               AND pAdmit.status = 0
            <cfif admitId GT 0>
               AND pAssessments.Admit_ID = <cfqueryparam value="#admitId#" cfsqltype="CF_SQL_INTEGER">
            </cfif>
            LEFT OUTER JOIN #arguments.lookupSchema#.pPathway
                ON pPathway.Item_ID = pProgress.Item_ID
               AND pPathway.Header_No IS NOT NULL
            LEFT OUTER JOIN #arguments.lookupSchema#.pProgress_Unstable
                ON pProgress_Unstable.`System` = pProblem.M0_No
            WHERE pProgress.Schedule_ID = <cfqueryparam value="#arguments.scheduleId#" cfsqltype="CF_SQL_INTEGER">
              AND pProgress.Status = <cfqueryparam value="0" cfsqltype="CF_SQL_INTEGER">
              AND pProgress.type IN (<cfqueryparam value="problem,symptom" cfsqltype="cf_sql_varchar" list="yes">)
              AND pProgress.Item_ID NOT IN (
                  SELECT Item_ID
                  FROM #arguments.lookupSchema#.pPathway
                  WHERE pPathway.Header = 'Vital Signs'
              )
            GROUP BY pProgress.Progress_ID
            ORDER BY woundstitle ASC, header_title ASC, pProgress.Item_ID ASC
        </cfquery>

        <cfreturn pgproblemsall />
    </cffunction>

    <cffunction name="loadSymptomsQuery" access="private" returntype="struct" output="false"
        hint="Non-GG symptoms (pgproblems) — excludes GG% rows. Returns { q, m0Nos }.">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="lookupSchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfargument name="skill" type="string" required="true" />
        <cfset var pgproblemsall = loadPgProblemsAllQuery(
            agencySchema = arguments.agencySchema,
            lookupSchema = arguments.lookupSchema,
            assmtId = arguments.assmtId,
            scheduleId = arguments.scheduleId,
            skill = arguments.skill
        ) />
        <cfset var pgproblems = "" />
        <cfset var getM0No = "" />
        <cfset var m0Nos = "" />

        <cfquery name="getM0No" dbtype="query">
            SELECT *
            FROM pgproblemsall
            WHERE M0_No IS NOT NULL
            ORDER BY M0_No ASC
        </cfquery>
        <cfloop query="getM0No">
            <cfif len(safeTrim(getM0No.M0_No))>
                <cfset m0Nos = listAppend(m0Nos, safeTrim(getM0No.M0_No)) />
            </cfif>
        </cfloop>

        <cfquery name="pgproblems" dbtype="query">
            SELECT *
            FROM pgproblemsall
            WHERE Description NOT LIKE 'GG%'
              AND (prob_id > 0 OR woundstitle != '' OR Careplan_ID = 0)
            ORDER BY woundstitle ASC, header_title ASC, Path_ID ASC, Description ASC, Item_ID ASC
        </cfquery>
        <cfreturn { q = pgproblems, m0Nos = m0Nos } />
    </cffunction>

    <cffunction name="loadGgSymptomsQuery" access="private" returntype="query" output="false"
        hint="GG symptoms (ADLs) — Description LIKE GG%.">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="lookupSchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="scheduleId" type="numeric" required="true" />
        <cfargument name="skill" type="string" required="true" />
        <cfset var pgproblemsall = loadPgProblemsAllQuery(
            agencySchema = arguments.agencySchema,
            lookupSchema = arguments.lookupSchema,
            assmtId = arguments.assmtId,
            scheduleId = arguments.scheduleId,
            skill = arguments.skill
        ) />
        <cfset var ggproblems = "" />

        <cfquery name="ggproblems" dbtype="query">
            SELECT *
            FROM pgproblemsall
            WHERE Description LIKE 'GG%'
            ORDER BY woundstitle ASC, header_title ASC, Item_ID ASC
        </cfquery>
        <cfreturn ggproblems />
    </cffunction>

    <!--- Main remote endpoint --->
    <cffunction name="getProgressNotes" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Schedule_ID. Returns progress note sections with headings, descriptions, description_type, and physician alert fields (skill-filtered like progress_notesnew.cfm).">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Schedule_ID" type="string" required="false" default="" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = {} />
        <cfset var auth = {} />
        <cfset var empId = 0 />
        <cfset var scheduleCtx = {} />
        <cfset var skill = "" />
        <cfset var sections = [] />
        <cfset var symptomsQ = "" />
        <cfset var ggSymptomsQ = "" />
        <cfset var careplanGoals = "" />
        <cfset var visible = {} />

        <cfset payload = parseProgressNotesRequest(
            securityKeyArg = arguments.securityKey,
            scheduleIdArg = arguments.Schedule_ID
        ) />

        <cfif NOT len(payload.securityKey)>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif payload.scheduleId LTE 0>
            <cfset response.message = "Schedule_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

        <cfset auth = authorizeSecurityKeyForAgency(securityKey = payload.securityKey) />
        <cfif NOT auth.ok>
            <cfset response.message = auth.message />
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <cfreturn response />
        </cfif>

        <cfset empId = parseSecurityKeyEmpId(payload.securityKey) />
        <cfset scheduleCtx = getScheduleForEmployee(
            agencySchema = auth.agencySchema,
            scheduleId = payload.scheduleId,
            empId = empId
        ) />
        <cfif NOT scheduleCtx.ok>
            <cfset response.message = scheduleCtx.message />
            <cfheader statuscode="403" statustext="Forbidden" />
            <cfreturn response />
        </cfif>

        <cfif NOT isDefined("Request.prefix_db_lookup") OR NOT len(trim(Request.prefix_db_lookup))>
            <cfset Request.prefix_db_lookup = "hhapowerpath" />
        </cfif>
        <cfset Request.prefix_db_agency = auth.agencySchema />
        <cfset Request.Agency_ID = auth.locId />

        <cftry>
            <cfset url.api_pgnotes_only = "1" />
            <cfset url.SID = payload.scheduleId />
            <cfset url.AID = val(scheduleCtx.schedule.Assmt_ID) />
            <cfset url.ID = val(scheduleCtx.schedule.Patient_ID) />
            <cfinclude template="../../patientadmin_new/patient/progress_notes_download_data.cfm" />

            <cfset skill = safeTrim(skilltype) />
            <cfset visible = {
                patient_complaint = (ptcomplaint_class NEQ "hide"),
                patient_goal = (ptgoal_class NEQ "hide"),
                vitals = (vital_class NEQ "hide"),
                symptoms = (symp_class NEQ "hide"),
                procedures = (procedure_class NEQ "hide"),
                goals = (goals_class NEQ "hide"),
                discharge_plan = (discharge_class NEQ "hide"),
                teaching = (teaching_class NEQ "hide"),
                medications = (meds_class NEQ "hide"),
                gg_symptoms = (goals_class NEQ "hide")
            } />

            <cfset careplanGoals = createObject("component", "pathway.careplan").getscareplan(
                url.AID,
                "goal",
                safeTrim(careplan_skill),
                0
            ) />

            <cfset arrayAppend(sections, formatPatientComplaint(getptcomplaint, visible.patient_complaint)) />
            <cfset arrayAppend(sections, formatScaleSection(
                key = "patient_goal",
                heading = "Patient Goal on Admission",
                q = getptgoals,
                visible = visible.patient_goal,
                scaleLegend = "0-0% 1-25% 2-50% 3-75% 4-100% met"
            )) />
            <cfset arrayAppend(sections, formatVitalsSection(getvitals, visible.vitals)) />

            <cfset symptomsCtx = loadSymptomsQuery(
                agencySchema = auth.agencySchema,
                lookupSchema = Request.prefix_db_lookup,
                assmtId = url.AID,
                scheduleId = payload.scheduleId,
                skill = skill
            ) />
            <cfset arrayAppend(sections, formatSymptomsFromQuery(
                q = symptomsCtx.q,
                visible = visible.symptoms,
                skill = skill,
                lookupSchema = Request.prefix_db_lookup,
                m0Nos = symptomsCtx.m0Nos
            )) />

            <cfset arrayAppend(sections, formatYesNoSection(
                key = "procedures",
                heading = "PROCEDURES",
                q = getprocedure,
                visible = visible.procedures,
                flatList = true
            )) />
            <cfset arrayAppend(sections, formatScaleSection(
                key = "goals",
                heading = "GOALS",
                q = getgoals,
                visible = visible.goals,
                scaleLegend = "0-0% 1-25% 2-50% 3-75% 4-100% met",
                includeTargetDate = true,
                flatList = true
            )) />

            <cfset ggSymptomsQ = loadGgSymptomsQuery(
                agencySchema = auth.agencySchema,
                lookupSchema = Request.prefix_db_lookup,
                assmtId = url.AID,
                scheduleId = payload.scheduleId,
                skill = skill
            ) />
            <cfset arrayAppend(sections, formatGgSymptomsSection(
                q = ggSymptomsQ,
                visible = visible.gg_symptoms,
                skill = skill,
                careplanGoals = careplanGoals,
                lookupSchema = Request.prefix_db_lookup,
                datasource = Application.DataSrc
            )) />

            <cfset arrayAppend(sections, formatScaleSection(
                key = "discharge_plan",
                heading = "Discharge Plan",
                q = getdischargeplan,
                visible = visible.discharge_plan,
                scaleLegend = "0-0% 1-25% 2-50% 3-75% 4-100% met",
                includeTargetDate = true
            )) />
            <cfset arrayAppend(sections, formatScaleSection(
                key = "teaching",
                heading = "Teaching",
                q = getallteaching,
                visible = visible.teaching,
                scaleLegend = "0-0% 1-25% 2-50% 3-75% 4-100% met"
            )) />

            <cfset response.success = true />
            <cfset response.message = "Progress notes loaded" />
            <cfset response.data = {
                schedule = scheduleCtx.schedule,
                skill = skill,
                careplan_skill = safeTrim(careplan_skill),
                section_visibility = visible,
                sections = sections
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />

            <cfcatch type="any">
                <cfset response.message = "Failed to load progress notes: " & cfcatch.message />
                <cfset response.data = {
                    error_detail = safeTrim(cfcatch.detail),
                    error_type = safeTrim(cfcatch.type)
                } />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="saveProgressNotes" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Save progress note field updates to pProgress. Requires securityKey and Schedule_ID.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Schedule_ID" type="string" required="false" default="" />
        <cfargument name="Assmt_ID" type="string" required="false" default="" />
        <cfargument name="items" type="string" required="false" default="" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = {} />
        <cfset var auth = {} />
        <cfset var empId = 0 />
        <cfset var scheduleCtx = {} />
        <cfset var assmtId = 0 />
        <cfset var results = [] />
        <cfset var itemResult = {} />
        <cfset var complaintResult = {} />
        <cfset var i = 0 />
        <cfset var item = {} />
        <cfset var updated = 0 />
        <cfset var skipped = 0 />
        <cfset var errors = [] />

        <cfset payload = parseProgressNotesSaveRequest(
            securityKeyArg = arguments.securityKey,
            scheduleIdArg = arguments.Schedule_ID,
            assmtIdArg = arguments.Assmt_ID,
            itemsArg = arguments.items
        ) />

        <cfif NOT len(payload.securityKey)>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif payload.scheduleId LTE 0>
            <cfset response.message = "Schedule_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

        <cfset auth = authorizeSecurityKeyForAgency(securityKey = payload.securityKey) />
        <cfif NOT auth.ok>
            <cfset response.message = auth.message />
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <cfreturn response />
        </cfif>

        <cfset empId = parseSecurityKeyEmpId(payload.securityKey) />
        <cfset scheduleCtx = getScheduleForEmployee(
            agencySchema = auth.agencySchema,
            scheduleId = payload.scheduleId,
            empId = empId
        ) />
        <cfif NOT scheduleCtx.ok>
            <cfset response.message = scheduleCtx.message />
            <cfheader statuscode="403" statustext="Forbidden" />
            <cfreturn response />
        </cfif>

        <cfset assmtId = payload.assmtId GT 0 ? payload.assmtId : val(scheduleCtx.schedule.Assmt_ID) />

        <cftry>
            <cfif structCount(payload.patient_complaint)>
                <cfset complaintResult = saveMobilePatientComplaint(
                    agencySchema = auth.agencySchema,
                    assmtId = assmtId,
                    scheduleId = payload.scheduleId,
                    empId = empId,
                    complaint = payload.patient_complaint
                ) />
                <cfset arrayAppend(results, {
                    section = "patient_complaint",
                    action = complaintResult.action,
                    progress_id = complaintResult.progress_id,
                    reason = complaintResult.reason
                }) />
                <cfif complaintResult.action NEQ "skipped">
                    <cfset updated = updated + 1 />
                <cfelseif len(complaintResult.reason)>
                    <cfset arrayAppend(errors, { section = "patient_complaint", reason = complaintResult.reason }) />
                </cfif>
            </cfif>

            <cfloop from="1" to="#arrayLen(payload.items)#" index="i">
                <cfset item = payload.items[i] />
                <cfif isStruct(item)>
                    <cfset itemResult = saveMobileProgressNoteItem(
                        agencySchema = auth.agencySchema,
                        scheduleId = payload.scheduleId,
                        empId = empId,
                        item = item
                    ) />
                    <cfset arrayAppend(results, itemResult) />
                    <cfif itemResult.action EQ "updated">
                        <cfset updated = updated + 1 />
                    <cfelse>
                        <cfset skipped = skipped + 1 />
                        <cfif len(itemResult.reason)>
                            <cfset arrayAppend(errors, {
                                progress_id = itemResult.progress_id,
                                reason = itemResult.reason
                            }) />
                        </cfif>
                    </cfif>
                </cfif>
            </cfloop>

            <cfset deactivateResolvedGoalsForSchedule(
                agencySchema = auth.agencySchema,
                scheduleId = payload.scheduleId,
                empId = empId
            ) />

            <cfset response.success = true />
            <cfset response.message = "Progress notes saved" />
            <cfset response.data = {
                schedule_id = toJsonInt(payload.scheduleId),
                assmt_id = toJsonInt(assmtId),
                updated_count = updated,
                skipped_count = skipped,
                errors = errors,
                results = results
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />

            <cfcatch type="any">
                <cfset response.message = "Failed to save progress notes: " & cfcatch.message />
                <cfset response.data = {
                    error_detail = safeTrim(cfcatch.detail),
                    error_type = safeTrim(cfcatch.type)
                } />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

</cfcomponent>
