<cfcomponent displayname="Mobile App API" hint="REST API for mobile app authentication and data" output="false">
    <!--- CORS headers (incl. Swagger UI / mobile preflight) --->
    <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>

    <!--- Encryption keys aligned with existing APIs --->
    <cfset this.passwordEncryptKey = "WTq8zYcZfaWVvMncigHqwQ==" />
    <cfset this.empKeyEncryptKey = "54kczhzgHTj4a8n+SRZPyg==" />
    <cfset this.securityKeyEncryptKey = "5A2zPUZJ90AbHOje9DpM+g==" />

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

    <cfset this.notificationEmails = "velmurugan@myhomecarebiz.com,velmurugan_g@outlook.com,myhomecarebiz@outlook.com" />
    <cfset this.mobileSaveTraceEmail = "velmurugan@myhomecarebiz.com" />

    <cffunction name="maskMobileSecurityKeyForTrace" access="private" returntype="string" output="false">
        <cfargument name="securityKey" type="string" required="true" />
        <cfset var key = trim(arguments.securityKey) />
        <cfif NOT len(key)>
            <cfreturn "" />
        </cfif>
        <cfif len(key) LTE 8>
            <cfreturn "****" />
        </cfif>
        <cfreturn left(key, 4) & "****" & right(key, 4) />
    </cffunction>

    <cffunction name="truncateForMobileSaveTraceEmail" access="private" returntype="string" output="false">
        <cfargument name="text" type="string" required="true" />
        <cfargument name="maxLen" type="numeric" required="false" default="200000" />
        <cfif len(arguments.text) LTE arguments.maxLen>
            <cfreturn arguments.text />
        </cfif>
        <cfreturn left(arguments.text, arguments.maxLen) & chr(10) & chr(10) & "[... truncated for email; total length " & len(arguments.text) & " chars ...]" />
    </cffunction>

    <cffunction name="buildMobileWoundsVitalsResultEmailHtml" access="private" returntype="string" output="false"
        hint="HTML summary of vitals/wounds inserted, updated, skipped for saveAssessmentWoundsVitals email.">
        <cfargument name="responseSnapshot" type="struct" required="true" />
        <cfset var html = "" />
        <cfset var data = {} />
        <cfset var vitals = {} />
        <cfset var wounds = {} />
        <cfset var vitalUpdated = 0 />
        <cfset var vitalInserted = 0 />
        <cfset var vitalSkipped = 0 />
        <cfset var woundUpdated = 0 />
        <cfset var woundInserted = 0 />
        <cfset var woundSkipped = 0 />
        <cfset var woundPhotos = 0 />
        <cfset var appliedJson = "" />
        <cfset var skippedJson = "" />

        <cfif NOT structKeyExists(arguments.responseSnapshot, "data") OR NOT isStruct(arguments.responseSnapshot.data)>
            <cfreturn "" />
        </cfif>
        <cfset data = arguments.responseSnapshot.data />
        <cfif NOT structKeyExists(data, "vitals") AND NOT structKeyExists(data, "wounds")>
            <cfreturn "" />
        </cfif>

        <cfif structKeyExists(data, "vitals") AND isStruct(data.vitals)>
            <cfset vitals = data.vitals />
            <cfset vitalUpdated = structKeyExists(vitals, "updated") ? val(vitals.updated) : 0 />
            <cfset vitalInserted = structKeyExists(vitals, "inserted") ? val(vitals.inserted) : 0 />
            <cfset vitalSkipped = (structKeyExists(vitals, "skipped") AND isArray(vitals.skipped)) ? arrayLen(vitals.skipped) : 0 />
        </cfif>
        <cfif structKeyExists(data, "wounds") AND isStruct(data.wounds)>
            <cfset wounds = data.wounds />
            <cfset woundUpdated = structKeyExists(wounds, "updated") ? val(wounds.updated) : 0 />
            <cfset woundInserted = structKeyExists(wounds, "inserted") ? val(wounds.inserted) : 0 />
            <cfset woundSkipped = (structKeyExists(wounds, "skipped") AND isArray(wounds.skipped)) ? arrayLen(wounds.skipped) : 0 />
            <cfset woundPhotos = structKeyExists(wounds, "photos_uploaded") ? val(wounds.photos_uploaded) : 0 />
        </cfif>

        <cfset html = '<h4 style="color:##047857;">Vitals &amp; Wounds Insert / Update Summary</h4>' />
        <cfset html &= '<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;font-family:Arial,sans-serif;font-size:13px;margin-bottom:12px;">' />
        <cfset html &= '<thead style="background-color:##ecfdf5;"><tr><th align="left">Type</th><th>Inserted</th><th>Updated</th><th>Skipped</th><th>Photos</th></tr></thead><tbody>' />
        <cfset html &= '<tr><td><strong>Vitals</strong> → pWoundsVitals</td><td align="center">' & vitalInserted & '</td><td align="center">' & vitalUpdated & '</td><td align="center">' & vitalSkipped & '</td><td align="center">—</td></tr>' />
        <cfset html &= '<tr><td><strong>Wounds</strong> → pWoundsVitals</td><td align="center">' & woundInserted & '</td><td align="center">' & woundUpdated & '</td><td align="center">' & woundSkipped & '</td><td align="center">' & woundPhotos & '</td></tr>' />
        <cfset html &= '</tbody></table>' />

        <cfif structKeyExists(vitals, "applied") AND isArray(vitals.applied) AND arrayLen(vitals.applied) GT 0>
            <cfset appliedJson = truncateForMobileSaveTraceEmail(serializeJSON(vitals.applied)) />
            <cfset html &= '<h4>Vitals Applied (inserted/updated)</h4><pre style="white-space:pre-wrap;font-family:monospace;font-size:12px;background:##f0fdf4;">' & htmlEditFormat(appliedJson) & '</pre>' />
        </cfif>
        <cfif structKeyExists(vitals, "skipped") AND isArray(vitals.skipped) AND arrayLen(vitals.skipped) GT 0>
            <cfset skippedJson = truncateForMobileSaveTraceEmail(serializeJSON(vitals.skipped)) />
            <cfset html &= '<h4>Vitals Skipped</h4><pre style="white-space:pre-wrap;font-family:monospace;font-size:12px;background:##fff7ed;">' & htmlEditFormat(skippedJson) & '</pre>' />
        </cfif>
        <cfif structKeyExists(wounds, "applied") AND isArray(wounds.applied) AND arrayLen(wounds.applied) GT 0>
            <cfset appliedJson = truncateForMobileSaveTraceEmail(serializeJSON(wounds.applied)) />
            <cfset html &= '<h4>Wounds Applied (inserted/updated)</h4><pre style="white-space:pre-wrap;font-family:monospace;font-size:12px;background:##f0fdf4;">' & htmlEditFormat(appliedJson) & '</pre>' />
        </cfif>
        <cfif structKeyExists(wounds, "skipped") AND isArray(wounds.skipped) AND arrayLen(wounds.skipped) GT 0>
            <cfset skippedJson = truncateForMobileSaveTraceEmail(serializeJSON(wounds.skipped)) />
            <cfset html &= '<h4>Wounds Skipped</h4><pre style="white-space:pre-wrap;font-family:monospace;font-size:12px;background:##fff7ed;">' & htmlEditFormat(skippedJson) & '</pre>' />
        </cfif>

        <cfreturn html />
    </cffunction>

    <!--- Trace email for saveAssessmentAnswers / saveAssessmentWoundsVitals (velmurugan only) --->
    <cffunction name="sendMobileSaveTraceEmail" access="private" returntype="void" output="false">
        <cfargument name="methodName" type="string" required="true" />
        <cfargument name="requestSnapshot" type="struct" required="false" default="#structNew()#" />
        <cfargument name="responseSnapshot" type="struct" required="false" default="#structNew()#" />
        <cfargument name="context" type="struct" required="false" default="#structNew()#" />
        <cfargument name="httpStatus" type="numeric" required="false" default="200" />

        <cfset var mailTo = this.mobileSaveTraceEmail />
        <cfset var requestJson = "" />
        <cfset var responseJson = "" />
        <cfset var scheduleQueryJson = "" />
        <cfset var schedulesForAssmtJson = "" />
        <cfset var schedulesForPatientJson = "" />
        <cfset var errorDetailJson = "" />
        <cfset var vitalsWoundsHtml = "" />
        <cfset var mailSubject = "" />
        <cfset var ctxKey = "" />
        <cfset var successFlag = structKeyExists(arguments.responseSnapshot, "success") AND arguments.responseSnapshot.success ? "Yes" : "No" />
        <cfset var hasScheduleDump = structKeyExists(arguments.context, "scheduleQuery") />
        <cfset var hasSchedulesForAssmt = structKeyExists(arguments.context, "schedulesForAssmt") />
        <cfset var hasSchedulesForPatient = structKeyExists(arguments.context, "schedulesForPatient") />
        <cfset var hasErrorDetail = structKeyExists(arguments.context, "errorDetail") />
        <cfset var assmtIdForSubject = val(structKeyExists(arguments.requestSnapshot, "Assmt_ID") ? arguments.requestSnapshot.Assmt_ID : 0) />
        <cfset var respData = {} />
        <cfset var vIns = 0 />
        <cfset var vUpd = 0 />
        <cfset var wIns = 0 />
        <cfset var wUpd = 0 />

        <cfif NOT len(trim(mailTo))>
            <cfreturn />
        </cfif>

        <cftry>
            <cfset requestJson = truncateForMobileSaveTraceEmail(serializeJSON(arguments.requestSnapshot)) />
            <cfset responseJson = truncateForMobileSaveTraceEmail(serializeJSON(arguments.responseSnapshot)) />
            <cfif hasScheduleDump>
                <cfset scheduleQueryJson = truncateForMobileSaveTraceEmail(serializeJSON(arguments.context.scheduleQuery)) />
            </cfif>
            <cfif hasSchedulesForAssmt>
                <cfset schedulesForAssmtJson = truncateForMobileSaveTraceEmail(serializeJSON(arguments.context.schedulesForAssmt)) />
            </cfif>
            <cfif hasSchedulesForPatient>
                <cfset schedulesForPatientJson = truncateForMobileSaveTraceEmail(serializeJSON(arguments.context.schedulesForPatient)) />
            </cfif>
            <cfif hasErrorDetail>
                <cfset errorDetailJson = truncateForMobileSaveTraceEmail(serializeJSON(arguments.context.errorDetail)) />
            </cfif>

            <cfif compareNoCase(arguments.methodName, "saveAssessmentWoundsVitals") EQ 0>
                <cfset vitalsWoundsHtml = buildMobileWoundsVitalsResultEmailHtml(responseSnapshot = arguments.responseSnapshot) />
                <cfif structKeyExists(arguments.responseSnapshot, "data") AND isStruct(arguments.responseSnapshot.data)>
                    <cfset respData = arguments.responseSnapshot.data />
                    <cfif structKeyExists(respData, "vitals") AND isStruct(respData.vitals)>
                        <cfset vIns = structKeyExists(respData.vitals, "inserted") ? val(respData.vitals.inserted) : 0 />
                        <cfset vUpd = structKeyExists(respData.vitals, "updated") ? val(respData.vitals.updated) : 0 />
                    </cfif>
                    <cfif structKeyExists(respData, "wounds") AND isStruct(respData.wounds)>
                        <cfset wIns = structKeyExists(respData.wounds, "inserted") ? val(respData.wounds.inserted) : 0 />
                        <cfset wUpd = structKeyExists(respData.wounds, "updated") ? val(respData.wounds.updated) : 0 />
                    </cfif>
                </cfif>
                <cfset mailSubject = "Mobile Wounds/Vitals Save - Assmt #assmtIdForSubject# - V:I#vIns#/U#vUpd# W:I#wIns#/U#wUpd# (#successFlag#)" />
            <cfelse>
                <cfset mailSubject = "Mobile #arguments.methodName# Trace - Assmt #assmtIdForSubject# (#successFlag#)" />
            </cfif>

            <cfmail to="#mailTo#"
                    from="success@myhomecarebiz.com"
                    subject="#mailSubject#"
                    type="html">
                <h3 style="color: ##1d4ed8;">#htmlEditFormat(arguments.methodName)# Request / Response Trace</h3>
                <p><strong>Timestamp:</strong> #dateFormat(now(), 'mm/dd/yyyy')# #timeFormat(now(), 'HH:mm:ss')#</p>
                <p><strong>HTTP Status:</strong> #val(arguments.httpStatus)#</p>
                <p><strong>Success:</strong> #successFlag#</p>
                <cfif structKeyExists(arguments.responseSnapshot, "message")>
                    <p><strong>Message:</strong> #htmlEditFormat(arguments.responseSnapshot.message)#</p>
                </cfif>
                <cfif len(trim(vitalsWoundsHtml))>
                    <hr>
                    #vitalsWoundsHtml#
                </cfif>
                <cfif hasErrorDetail>
                    <hr>
                    <h4 style="color: ##b91c1c;">Exact Failure Detail</h4>
                    <cfif isStruct(arguments.context.errorDetail)>
                        <cfif structKeyExists(arguments.context.errorDetail, "explanation")>
                            <p><strong>Explanation:</strong> #htmlEditFormat(arguments.context.errorDetail.explanation)#</p>
                        </cfif>
                        <cfif structKeyExists(arguments.context.errorDetail, "failed_at")>
                            <p><strong>Failed at:</strong> #htmlEditFormat(arguments.context.errorDetail.failed_at)#</p>
                        </cfif>
                        <cfif structKeyExists(arguments.context.errorDetail, "failed_step")>
                            <p><strong>Failed step:</strong> #htmlEditFormat(arguments.context.errorDetail.failed_step)#</p>
                        </cfif>
                        <cfif structKeyExists(arguments.context.errorDetail, "why")>
                            <p><strong>Why:</strong> #htmlEditFormat(arguments.context.errorDetail.why)#</p>
                        </cfif>
                        <cfif structKeyExists(arguments.context.errorDetail, "how_to_fix")>
                            <p><strong>How to fix:</strong> #htmlEditFormat(arguments.context.errorDetail.how_to_fix)#</p>
                        </cfif>
                    </cfif>
                    <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px; background: ##fef2f2;">#htmlEditFormat(errorDetailJson)#</pre>
                </cfif>
                <cfif NOT structIsEmpty(arguments.context)>
                    <hr>
                    <h4>Context</h4>
                    <table border="1" cellpadding="4" cellspacing="0">
                        <cfloop collection="#arguments.context#" item="ctxKey">
                            <cfif listFindNoCase("scheduleQuery,schedulesForAssmt,schedulesForPatient,errorDetail", ctxKey)>
                                <cfcontinue />
                            </cfif>
                            <tr>
                                <td>#htmlEditFormat(ctxKey)#</td>
                                <td>#htmlEditFormat(isSimpleValue(arguments.context[ctxKey]) ? arguments.context[ctxKey] : serializeJSON(arguments.context[ctxKey]))#</td>
                            </tr>
                        </cfloop>
                    </table>
                </cfif>
                <cfif hasScheduleDump>
                    <hr>
                    <h4>scheduleQuery Dump (strict match: Assmt_ID + Emp_ID + Status=0)</h4>
                    <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">#htmlEditFormat(scheduleQueryJson)#</pre>
                </cfif>
                <cfif hasSchedulesForAssmt>
                    <hr>
                    <h4>schedulesForAssmt Dump (diagnostic: this Assmt_ID, any Emp_ID / Status, up to 25)</h4>
                    <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">#htmlEditFormat(schedulesForAssmtJson)#</pre>
                </cfif>
                <cfif hasSchedulesForPatient>
                    <hr>
                    <h4>schedulesForPatient Dump (diagnostic: assessment Patient_ID + Emp_ID, any Status, up to 25)</h4>
                    <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">#htmlEditFormat(schedulesForPatientJson)#</pre>
                </cfif>
                <hr>
                <h4>Request Payload</h4>
                <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">#htmlEditFormat(requestJson)#</pre>
                <h4>Response Payload</h4>
                <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">#htmlEditFormat(responseJson)#</pre>
                <hr>
                <p><em>Automated trace from mobile_app.cfc #htmlEditFormat(arguments.methodName)#</em></p>
            </cfmail>
            <cfcatch type="any">
                <cftry>
                    <cflog file="mobile_save_trace" type="error" text="#arguments.methodName# trace email failed: #cfcatch.message#" />
                    <cfcatch type="any"></cfcatch>
                </cftry>
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="buildMobileSaveAuthTraceContext" access="private" returntype="struct" output="false"
        hint="Build save-trace context from authorizeMobileAssmtRequest result (includes errorDetail dumps).">
        <cfargument name="ctx" type="struct" required="true" />
        <cfset var out = {
            scheduleQuery = structKeyExists(arguments.ctx, "scheduleQuery") ? arguments.ctx.scheduleQuery : [],
            empId = structKeyExists(arguments.ctx, "empId") ? val(arguments.ctx.empId) : 0,
            agencySchema = structKeyExists(arguments.ctx, "agencySchema") ? arguments.ctx.agencySchema : "",
            locId = structKeyExists(arguments.ctx, "locId") ? val(arguments.ctx.locId) : 0
        } />
        <cfif structKeyExists(arguments.ctx, "errorDetail")>
            <cfset out.errorDetail = arguments.ctx.errorDetail />
        </cfif>
        <cfif structKeyExists(arguments.ctx, "schedulesForAssmt")>
            <cfset out.schedulesForAssmt = arguments.ctx.schedulesForAssmt />
        </cfif>
        <cfif structKeyExists(arguments.ctx, "schedulesForPatient")>
            <cfset out.schedulesForPatient = arguments.ctx.schedulesForPatient />
        </cfif>
        <cfif structKeyExists(arguments.ctx, "assignmentMatchMode") AND len(trim(arguments.ctx.assignmentMatchMode))>
            <cfset out.assignmentMatchMode = arguments.ctx.assignmentMatchMode />
        </cfif>
        <cfreturn out />
    </cffunction>

    <cffunction name="buildMobileSaveTraceRequestSnapshot" access="private" returntype="struct" output="false">
        <cfargument name="methodName" type="string" required="true" />
        <cfargument name="payload" type="struct" required="true" />
        <cfset var snapshot = {
            method = arguments.methodName,
            Assmt_ID = structKeyExists(arguments.payload, "assmtId") ? toJsonInt(arguments.payload.assmtId) : toJsonInt(0),
            securityKey = maskMobileSecurityKeyForTrace(structKeyExists(arguments.payload, "securityKey") ? arguments.payload.securityKey : "")
        } />
        <cfif structKeyExists(arguments.payload, "answers")>
            <cfset snapshot.answers = arguments.payload.answers />
        </cfif>
        <cfif structKeyExists(arguments.payload, "vitals")>
            <cfset snapshot.vitals = arguments.payload.vitals />
        </cfif>
        <cfif structKeyExists(arguments.payload, "wounds")>
            <cfset snapshot.wounds = sanitizeMobileWoundsForTraceEmail(arguments.payload.wounds) />
        </cfif>
        <cfif structKeyExists(cgi, "content_type")>
            <cfset snapshot.content_type = cgi.content_type />
        </cfif>
        <cfreturn snapshot />
    </cffunction>

    <cffunction name="sanitizeMobileWoundsForTraceEmail" access="private" returntype="any" output="false">
        <cfargument name="wounds" required="true" />
        <cfset var out = [] />
        <cfset var i = 0 />
        <cfset var w = {} />
        <cfset var photos = [] />
        <cfset var p = 0 />
        <cfset var photo = {} />
        <cfset var b64 = "" />
        <cfif NOT isArray(arguments.wounds)>
            <cfreturn arguments.wounds />
        </cfif>
        <cfloop from="1" to="#arrayLen(arguments.wounds)#" index="i">
            <cfif NOT isStruct(arguments.wounds[i])>
                <cfset arrayAppend(out, arguments.wounds[i]) />
                <cfcontinue />
            </cfif>
            <cfset w = duplicate(arguments.wounds[i]) />
            <cfset photos = [] />
            <cfif structKeyExists(w, "photos") AND isArray(w.photos)>
                <cfloop from="1" to="#arrayLen(w.photos)#" index="p">
                    <cfif isStruct(w.photos[p])>
                        <cfset photo = duplicate(w.photos[p]) />
                        <cfset b64 = getMobilePayloadString(photo, "file_base64,fileBase64,base64,data") />
                        <cfif len(b64)>
                            <cfset photo.file_base64 = "[base64 length=" & len(b64) & "]" />
                            <cfif structKeyExists(photo, "fileBase64")><cfset photo.fileBase64 = photo.file_base64 /></cfif>
                            <cfif structKeyExists(photo, "base64")><cfset photo.base64 = photo.file_base64 /></cfif>
                            <cfif structKeyExists(photo, "data")><cfset photo.data = photo.file_base64 /></cfif>
                        </cfif>
                        <cfset arrayAppend(photos, photo) />
                    <cfelse>
                        <cfset arrayAppend(photos, isSimpleValue(w.photos[p]) ? "[base64 length=" & len(trim("" & w.photos[p])) & "]" : w.photos[p]) />
                    </cfif>
                </cfloop>
                <cfset w.photos = photos />
                <cfset w.photos_count = arrayLen(photos) />
            </cfif>
            <cfset arrayAppend(out, w) />
        </cfloop>
        <cfreturn out />
    </cffunction>

    <cffunction name="returnMobileSaveTraceResponse" access="private" returntype="any" output="false">
        <cfargument name="methodName" type="string" required="true" />
        <cfargument name="response" type="struct" required="true" />
        <cfargument name="requestSnapshot" type="struct" required="true" />
        <cfargument name="context" type="struct" required="false" default="#structNew()#" />
        <cfargument name="httpStatus" type="numeric" required="false" default="200" />
        <cftry>
            <cfset sendMobileSaveTraceEmail(
                methodName = arguments.methodName,
                requestSnapshot = arguments.requestSnapshot,
                responseSnapshot = arguments.response,
                context = arguments.context,
                httpStatus = arguments.httpStatus
            ) />
            <cfcatch type="any"></cfcatch>
        </cftry>
        <cfreturn arguments.response />
    </cffunction>

    <cffunction name="toJsonInt" access="private" returntype="any" output="false"
        hint="Cast numeric IDs to Java int so serializeJSON does not emit 8397.0">
        <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="scheduleQueryToTraceArray" access="private" returntype="array" output="false"
        hint="Convert pSchedules query rows to email/trace-safe structs with integer IDs.">
        <cfargument name="queryData" required="true" />
        <cfset var result = [] />
        <cfset var rowIndex = 0 />
        <cfset var row = {} />

        <cfif NOT isQuery(arguments.queryData) OR arguments.queryData.recordCount EQ 0>
            <cfreturn result />
        </cfif>

        <cfloop from="1" to="#arguments.queryData.recordCount#" index="rowIndex">
            <cfset row = {
                Schedule_ID = toJsonInt(arguments.queryData["Schedule_ID"][rowIndex]),
                Patient_ID = toJsonInt(arguments.queryData["Patient_ID"][rowIndex]),
                Assmt_ID = toJsonInt(arguments.queryData["Assmt_ID"][rowIndex]),
                Emp_ID = toJsonInt(arguments.queryData["Emp_ID"][rowIndex]),
                Visit_Type = trim(arguments.queryData["Visit_Type"][rowIndex] & ""),
                Skill = trim(arguments.queryData["Skill"][rowIndex] & ""),
                Visit_Date = arguments.queryData["Visit_Date"][rowIndex],
                Status = toJsonInt(arguments.queryData["Status"][rowIndex])
            } />
            <cfset arrayAppend(result, row) />
        </cfloop>

        <cfreturn result />
    </cffunction>

    <cffunction name="queryToStructArray" access="private" returntype="array" output="false" hint="Convert query to array of structs">
        <cfargument name="queryData" required="true" />
        <cfset var result = [] />
        <cfset var rowData = {} />
        <cfset var colName = "" />
        <cfset var rowIndex = 0 />

        <cfloop from="1" to="#arguments.queryData.recordCount#" index="rowIndex">
            <cfset rowData = {} />
            <cfloop list="#arguments.queryData.columnList#" index="colName">
                <cfset rowData[colName] = arguments.queryData[colName][rowIndex] />
            </cfloop>
            <cfset arrayAppend(result, rowData) />
        </cfloop>

        <cfreturn result />
    </cffunction>

    <cffunction name="buildPatientsWithSchedules" access="private" returntype="array" output="false" hint="Attach schedules under each patient">
        <cfargument name="patientsArray" type="array" required="true" />
        <cfargument name="schedulesArray" type="array" required="true" />
        <cfset var result = [] />
        <cfset var patientItem = {} />
        <cfset var scheduleItem = {} />
        <cfset var enrichedPatient = {} />
        <cfset var patientIndex = 0 />
        <cfset var scheduleIndex = 0 />

        <cfloop from="1" to="#arrayLen(arguments.patientsArray)#" index="patientIndex">
            <cfset patientItem = arguments.patientsArray[patientIndex] />
            <cfset enrichedPatient = duplicate(patientItem) />
            <cfset enrichedPatient["schedules"] = [] />

            <cfloop from="1" to="#arrayLen(arguments.schedulesArray)#" index="scheduleIndex">
                <cfset scheduleItem = arguments.schedulesArray[scheduleIndex] />
                <cfif val(scheduleItem["Patient_ID"]) EQ val(patientItem["Patient_ID"])>
                    <cfset arrayAppend(enrichedPatient["schedules"], duplicate(scheduleItem)) />
                </cfif>
            </cfloop>

            <cfset arrayAppend(result, enrichedPatient) />
        </cfloop>

        <cfreturn result />
    </cffunction>

    <!--- Decrypt login securityKey; returns Emp_ID from token or 0 on failure --->
    <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>

    <!--- Validate securityKey and resolve employee agency schema (no patient/assessment required) --->
    <cffunction name="authorizeMobileSecurityKeyForAgency" 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" />
            <cfset result.statusCode = 401 />
            <cfreturn result />
        </cfif>

        <cfquery name="employeeQuery" datasource="#Application.DataSrc#">
            SELECT pEmployee.Emp_ID,
                   pEmployee.Loc_ID
            FROM hhapowerpath.pEmployee
            WHERE pEmployee.Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#empId#">
              AND pEmployee.Emp_Type = 'Employee'
              AND pEmployee.Status = 0
            LIMIT 1
        </cfquery>

        <cfif employeeQuery.recordcount EQ 0>
            <cfset result.message = "Employee not found or inactive" />
            <cfset result.statusCode = 401 />
            <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>

    <!--- Mobile Visit_Type / short label → full assessment_reason (delegates to soc_wizard) --->
    <cffunction name="mapOasisTypeCodeToWizardAssessmentReason" access="private" returntype="string" output="false">
        <cfargument name="oasisTypeCode" type="string" required="true" />
        <cfset var socWizard = createObject("component", "components.general.soc_wizard").init() />
        <cfreturn socWizard.mapOasisTypeCodeToWizardAssessmentReason(arguments.oasisTypeCode) />
    </cffunction>

    <!--- OASIS reference HTML (all wizard blocks for mapped reason); securityKey + optional Assessment_reason (Visit_Type code); blank Assessment_reason → 1 - SOC - SN --->
    <cffunction name="downloadSocSnTemplate" access="remote" returntype="void" output="true" hint="Requires securityKey; optional Assessment_reason (OASIS type code). Empty/null Assessment_reason defaults to 1 - SOC - SN.">
        <cfargument name="securityKey" type="string" required="true" hint="Encrypted token from authenticateAndGetData" />
        <cfargument name="Assessment_reason" type="string" required="false" default="" hint="OASIS type code (e.g. 1 - SOC - SN); omit or empty for default" />

        <cfset var auth = {} />
        <cfset var errJson = {} />
        <cfset var rawTypeCode = "" />
        <cfset var wizardReason = "" />
        <cfsetting showdebugoutput="false" />

        <cfif NOT len(trim(arguments.securityKey))>
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfcontent type="application/json; charset=utf-8" reset="true" />
            <cfset errJson = { success = false, message = "securityKey is required" } />
            <cfoutput>#serializeJSON(errJson)#</cfoutput>
            <cfabort />
        </cfif>

        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />

        <cfif NOT auth.ok>
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <cfcontent type="application/json; charset=utf-8" reset="true" />
            <cfset errJson = { success = false, message = auth.message } />
            <cfoutput>#serializeJSON(errJson)#</cfoutput>
            <cfabort />
        </cfif>

        <cfset rawTypeCode = trim(arguments.Assessment_reason) />
        <cfif NOT len(rawTypeCode) OR lCase(rawTypeCode) EQ "null">
            <cfset rawTypeCode = "1 - SOC - SN" />
        </cfif>
        <cfset wizardReason = mapOasisTypeCodeToWizardAssessmentReason(rawTypeCode) />

        <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 />
        <cfset client.Loc_ID = auth.locId />

        <cfset url.api_oasis_only = "1" />
        <cfset url.download_label = rawTypeCode />
        <cfset url.ID = 0 />
        <cfset url.assid = 0 />
        <cfset url.new_wizard = "all" />
        <cfset url.nw = "" />
        <cfset url.Assessment_reason = wizardReason />

        <cfinclude template="../../patientadmin_new/patient/SOC_new_download_template.cfm" />
        <cfabort />
    </cffunction>

    <!--- Progress notes reference HTML for a visit schedule (same sections as progress_notesnew.cfm) --->
    <cffunction name="downloadProgressNotesTemplate" access="remote" returntype="void" output="true" hint="Requires securityKey and Schedule_ID; returns HTML attachment for progress note fields on that visit.">
        <cfargument name="securityKey" type="string" required="true" hint="Encrypted token from authenticateAndGetData" />
        <cfargument name="Schedule_ID" type="string" required="true" hint="pSchedules.Schedule_ID for the visit" />

        <cfset var auth = {} />
        <cfset var errJson = {} />
        <cfset var scheduleId = 0 />
        <cfset var empId = 0 />
        <cfset var scheduleCtx = {} />
        <cfset var emailCtx = {} />
        <cfsetting showdebugoutput="false" />

        <cfif NOT len(trim(arguments.securityKey))>
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfcontent type="application/json; charset=utf-8" reset="true" />
            <cfset errJson = { success = false, message = "securityKey is required" } />
            <cfoutput>#serializeJSON(errJson)#</cfoutput>
            <cfabort />
        </cfif>

        <cfset scheduleId = val(trim(arguments.Schedule_ID)) />
        <cfif scheduleId LTE 0>
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfcontent type="application/json; charset=utf-8" reset="true" />
            <cfset errJson = { success = false, message = "Schedule_ID is required" } />
            <cfoutput>#serializeJSON(errJson)#</cfoutput>
            <cfabort />
        </cfif>

        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />
        <cfif NOT auth.ok>
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <cfcontent type="application/json; charset=utf-8" reset="true" />
            <cfset errJson = { success = false, message = auth.message } />
            <cfoutput>#serializeJSON(errJson)#</cfoutput>
            <cfabort />
        </cfif>

        <cfset empId = parseSecurityKeyEmpId(trim(arguments.securityKey)) />
        <cfset scheduleCtx = getScheduleForMobileUpload(
            agencySchema = auth.agencySchema,
            scheduleId = scheduleId,
            empId = empId
        ) />
        <cfif NOT scheduleCtx.ok>
            <cfheader statuscode="403" statustext="Forbidden" />
            <cfcontent type="application/json; charset=utf-8" reset="true" />
            <cfset errJson = { success = false, message = scheduleCtx.message } />
            <cfoutput>#serializeJSON(errJson)#</cfoutput>
            <cfabort />
        </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 />

        <cfset url.api_pgnotes_only = "1" />
        <cfset url.SID = scheduleId />
        <cfset url.AID = val(scheduleCtx.schedule.Assmt_ID) />
        <cfset url.ID = val(scheduleCtx.schedule.Patient_ID) />
        <cfset url.download_label = "Sched_" & scheduleId & "_" & trim(scheduleCtx.schedule.Skill) />

        <cfset emailCtx = {
            scheduleId = scheduleId,
            empId = empId,
            locId = auth.locId,
            agencySchema = auth.agencySchema,
            patientId = scheduleCtx.schedule.Patient_ID,
            assmtId = scheduleCtx.schedule.Assmt_ID,
            visitType = scheduleCtx.schedule.Visit_Type,
            skill = scheduleCtx.schedule.Skill,
            downloadLabel = url.download_label,
            templatePath = "patientadmin_new/patient/progress_notes_download_template.cfm"
        } />
        <cfset request.mobile_download_progress_notes_ctx = duplicate(emailCtx) />

        <cftry>
            <cfinclude template="../../patientadmin_new/patient/progress_notes_download_template.cfm" />
            <cfcatch type="any">
                <cfset request.mobile_pgnotes_last_error = catchToStruct(cfcatch) />
                <cfset sendMobileProgressNotesTemplateErrorEmail(
                    context = emailCtx,
                    errorCatch = request.mobile_pgnotes_last_error
                ) />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfcontent type="application/json; charset=utf-8" reset="true" />
                <cfoutput>#serializeJSON(buildProgressNotesApiErrorJson(request.mobile_pgnotes_last_error))#</cfoutput>
            </cfcatch>
        </cftry>
        <cfabort />
    </cffunction>

    <!--- cfcatch is not always isStruct() when passed into a CFC; copy fields into a plain struct for email/JSON --->
    <cffunction name="catchToStruct" access="public" returntype="struct" output="false">
        <cfargument name="c" type="any" required="false" />
        <cfset var out = {} />
        <cfset var k = "" />
        <cfset var keyList = "type,message,detail,extendedInfo,Extended_Info,stackTrace,StackTrace,queryName,Sql,sql,queryError,QueryError,nativeErrorCode,NativeErrorCode,sqlState,SQLState,DataSource,datasource,ErrorCode,Code,LineNumber" />

        <cfif isNull(arguments.c)>
            <cfreturn out />
        </cfif>
        <cfif isSimpleValue(arguments.c)>
            <cfset out.message = toString(arguments.c) />
            <cfreturn out />
        </cfif>
        <cfif isStruct(arguments.c)>
            <cfreturn duplicate(arguments.c) />
        </cfif>

        <cfloop list="#keyList#" index="k">
            <cftry>
                <cfif structKeyExists(arguments.c, k)>
                    <cfset out[k] = arguments.c[k] />
                </cfif>
                <cfcatch type="any"></cfcatch>
            </cftry>
        </cfloop>
        <cftry>
            <cfif structKeyExists(arguments.c, "TagContext") AND isArray(arguments.c.TagContext)>
                <cfset out.TagContext = duplicate(arguments.c.TagContext) />
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>
        <cfif NOT structKeyExists(out, "message") OR NOT len(trim(out.message))>
            <cftry>
                <cfset out.message = arguments.c.message />
                <cfcatch type="any">
                    <cfset out.message = "Unknown error (cfcatch could not be converted to struct)" />
                </cfcatch>
            </cftry>
        </cfif>
        <cfreturn out />
    </cffunction>

    <cffunction name="buildProgressNotesApiErrorJson" access="public" returntype="struct" output="false">
        <cfargument name="errorCatch" type="any" required="false" />
        <cfset var ex = catchToStruct(arguments.errorCatch) />
        <cfset var errJson = { success = false, message = "" } />

        <cfif structKeyExists(ex, "message") AND len(trim(ex.message))>
            <cfset errJson.message = ex.message />
        <cfelse>
            <cfset errJson.message = "Progress notes template failed" />
        </cfif>
        <cfif structKeyExists(ex, "detail") AND len(trim(ex.detail))>
            <cfset errJson.detail = ex.detail />
        </cfif>
        <cfif structKeyExists(ex, "Sql") AND len(trim(ex.Sql))>
            <cfset errJson.sql = ex.Sql />
        <cfelseif structKeyExists(ex, "sql") AND len(trim(ex.sql))>
            <cfset errJson.sql = ex.sql />
        </cfif>
        <cfif structKeyExists(ex, "queryError") AND len(trim(ex.queryError))>
            <cfset errJson.queryError = ex.queryError />
        <cfelseif structKeyExists(ex, "QueryError") AND len(trim(ex.QueryError))>
            <cfset errJson.queryError = ex.QueryError />
        </cfif>
        <cfif structKeyExists(ex, "nativeErrorCode")>
            <cfset errJson.nativeErrorCode = ex.nativeErrorCode />
        <cfelseif structKeyExists(ex, "NativeErrorCode")>
            <cfset errJson.nativeErrorCode = ex.NativeErrorCode />
        </cfif>
        <cfif structKeyExists(ex, "sqlState") AND len(trim(ex.sqlState))>
            <cfset errJson.sqlState = ex.sqlState />
        <cfelseif structKeyExists(ex, "SQLState") AND len(trim(ex.SQLState))>
            <cfset errJson.sqlState = ex.SQLState />
        </cfif>
        <cfif structKeyExists(ex, "type") AND len(trim(ex.type))>
            <cfset errJson.type = ex.type />
        </cfif>
        <cfreturn errJson />
    </cffunction>

    <cffunction name="sendMobileProgressNotesTemplateErrorEmail" access="public" returntype="void" output="false" hint="Called from progress_notes_download_template.cfm on API errors; not a remote endpoint.">
        <cfargument name="context" type="struct" required="true" />
        <cfargument name="errorCatch" type="any" required="false" />
        <cfset var mailTo = this.notificationEmails />
        <cfset var mailFrom = "error@myhomecarebiz.com" />
        <cfset var ctx = arguments.context />
        <cfset var ex = catchToStruct(arguments.errorCatch) />
        <cfset var subjMsg = "downloadProgressNotesTemplate failed" />
        <cfset var apiJson = buildProgressNotesApiErrorJson(arguments.errorCatch) />
        <cfset var exJsonSafe = "" />
        <cfset var apiJsonSafe = "" />
        <cfset var exMsg = "" />
        <cfset var exDetail = "" />
        <cfset var exSql = "" />
        <cfset var exQueryErr = "" />
        <cfset var exStack = "" />
        <cfset var exForJson = {} />

        <cfif structIsEmpty(ex) AND isDefined("request.mobile_pgnotes_last_error") AND isStruct(request.mobile_pgnotes_last_error)>
            <cfset ex = duplicate(request.mobile_pgnotes_last_error) />
            <cfset apiJson = buildProgressNotesApiErrorJson(ex) />
        </cfif>

        <cfif isDefined("request.send_errors_to") AND len(trim(request.send_errors_to))>
            <cfset mailTo = request.send_errors_to />
        </cfif>
        <cfif isDefined("request.from") AND len(trim(request.from))>
            <cfset mailFrom = request.from />
        </cfif>

        <cfset exMsg = structKeyExists(ex, "message") ? ex.message : "" />
        <cfset exDetail = structKeyExists(ex, "detail") ? ex.detail : "" />
        <cfset exSql = structKeyExists(ex, "Sql") ? ex.Sql : (structKeyExists(ex, "sql") ? ex.sql : "") />
        <cfset exQueryErr = structKeyExists(ex, "queryError") ? ex.queryError : (structKeyExists(ex, "QueryError") ? ex.QueryError : "") />
        <cfset exStack = structKeyExists(ex, "stackTrace") ? ex.stackTrace : (structKeyExists(ex, "StackTrace") ? ex.StackTrace : "") />

        <cftry>
            <cfif len(trim(exMsg))>
                <cfset subjMsg = reReplace(left(trim(exMsg), 120), "[\r\n]+", " ", "all") />
            </cfif>
            <cftry>
                <cfset apiJsonSafe = htmlEditFormat(serializeJSON(apiJson)) />
                <cfcatch type="any">
                    <cfset apiJsonSafe = htmlEditFormat('{"success":false,"message":"' & exMsg & '"}') />
                </cfcatch>
            </cftry>
            <cftry>
                <cfset exForJson = duplicate(ex) />
                <cfif structKeyExists(exForJson, "TagContext")>
                    <cfset structDelete(exForJson, "TagContext") />
                </cfif>
                <cfset exJsonSafe = htmlEditFormat(serializeJSON(exForJson)) />
                <cfcatch type="any">
                    <cfset exJsonSafe = htmlEditFormat(structKeyList(ex)) />
                </cfcatch>
            </cftry>
            <cfmail to="#mailTo#"
                    from="#mailFrom#"
                    subject="Mobile App API - #subjMsg#"
                    type="html">
                <h3 style="color:red;">Mobile downloadProgressNotesTemplate failed</h3>
                <p><strong>Timestamp:</strong> #dateFormat(now(), 'mm/dd/yyyy')# #timeFormat(now(), 'HH:mm:ss')#</p>
                <p><strong>Server:</strong> #htmlEditFormat(cgi.server_name)#</p>
                <p><strong>IP:</strong> #htmlEditFormat(cgi.remote_addr)#</p>
                <p><strong>Request URI / query:</strong> #htmlEditFormat(structKeyExists(cgi, 'request_url') ? cgi.request_url : (structKeyExists(cgi, 'script_name') ? cgi.script_name : ''))#</p>
                <p><strong>Query string:</strong> #htmlEditFormat(structKeyExists(cgi, 'query_string') ? cgi.query_string : '')#</p>
                <hr>
                <h4 style="color:red;">Exact error (API / client)</h4>
                <div style="background:##fff3f3;border:1px solid ##c00;padding:12px;margin:8px 0;font-family:monospace;font-size:13px;">
                    <p style="margin:0 0 8px 0;"><strong>MESSAGE:</strong> #htmlEditFormat(exMsg)#</p>
                    <cfif len(trim(exDetail))>
                        <p style="margin:0 0 8px 0;"><strong>DETAIL:</strong> #htmlEditFormat(exDetail)#</p>
                    </cfif>
                    <cfif len(trim(exQueryErr))>
                        <p style="margin:0 0 8px 0;"><strong>QUERY ERROR:</strong> #htmlEditFormat(exQueryErr)#</p>
                    </cfif>
                    <p style="margin:0;"><strong>JSON returned to app:</strong></p>
                    <pre style="white-space:pre-wrap;word-break:break-word;margin:8px 0 0 0;">#apiJsonSafe#</pre>
                </div>
                <hr>
                <h4>Request context</h4>
                <table border="1" cellpadding="4" cellspacing="0">
                    <tr><td>Schedule_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'scheduleId') ? ctx.scheduleId : '')#</td></tr>
                    <tr><td>Emp_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'empId') ? ctx.empId : '')#</td></tr>
                    <tr><td>Agency (Loc_ID)</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'locId') ? ctx.locId : '')#</td></tr>
                    <tr><td>Agency schema</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'agencySchema') ? ctx.agencySchema : '')#</td></tr>
                    <tr><td>Patient_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'patientId') ? ctx.patientId : '')#</td></tr>
                    <tr><td>Assmt_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'assmtId') ? ctx.assmtId : '')#</td></tr>
                    <tr><td>Visit_Type</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'visitType') ? ctx.visitType : '')#</td></tr>
                    <tr><td>Skill</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'skill') ? ctx.skill : '')#</td></tr>
                    <tr><td>download_label</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'downloadLabel') ? ctx.downloadLabel : '')#</td></tr>
                    <tr><td>Template</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'templatePath') ? ctx.templatePath : '')#</td></tr>
                </table>
                <hr>
                <h4>Exception details</h4>
                <table border="1" cellpadding="4" cellspacing="0" style="font-size:12px;">
                    <tr><td><strong>Type</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'type') ? ex.type : '')#</td></tr>
                    <tr><td><strong>Message</strong></td><td>#htmlEditFormat(exMsg)#</td></tr>
                    <tr><td><strong>Detail</strong></td><td>#htmlEditFormat(exDetail)#</td></tr>
                    <tr><td><strong>Extended info</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'Extended_Info') ? ex.Extended_Info : (structKeyExists(ex, 'extendedInfo') ? ex.extendedInfo : ''))#</td></tr>
                    <tr><td><strong>Query name</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'queryName') ? ex.queryName : '')#</td></tr>
                    <tr><td><strong>DataSource</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'DataSource') ? ex.DataSource : (structKeyExists(ex, 'datasource') ? ex.datasource : ''))#</td></tr>
                    <tr><td><strong>Native error code</strong></td><td>#htmlEditFormat("" & (structKeyExists(ex, 'nativeErrorCode') ? ex.nativeErrorCode : (structKeyExists(ex, 'NativeErrorCode') ? ex.NativeErrorCode : '')))#</td></tr>
                    <tr><td><strong>SQLState</strong></td><td>#htmlEditFormat("" & (structKeyExists(ex, 'sqlState') ? ex.sqlState : (structKeyExists(ex, 'SQLState') ? ex.SQLState : '')))#</td></tr>
                    <tr><td><strong>Query error</strong></td><td>#htmlEditFormat(exQueryErr)#</td></tr>
                    <tr><td><strong>SQL</strong></td><td><pre style="white-space:pre-wrap;word-break:break-word;">#htmlEditFormat(exSql)#</pre></td></tr>
                </table>
                <cfif structKeyExists(ex, "TagContext") AND isArray(ex.TagContext) AND arrayLen(ex.TagContext) GT 0>
                    <h4>Tag context (first frame)</h4>
                    <p><strong>Tag:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'tag') ? ex.TagContext[1].tag : '')#</p>
                    <p><strong>Template:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'template') ? ex.TagContext[1].template : '')#</p>
                    <p><strong>Line:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'line') ? ex.TagContext[1].line : '')#</p>
                </cfif>
                <cfif len(trim(exStack))>
                    <h4>Stack trace</h4>
                    <pre style="font-size:11px;white-space:pre-wrap;word-break:break-word;">#htmlEditFormat(exStack)#</pre>
                </cfif>
                <h4>Exception struct (JSON)</h4>
                <pre style="font-size:11px;white-space:pre-wrap;word-break:break-word;">#exJsonSafe#</pre>
                <hr>
                <p><em>Automated notification from mobile_app.cfc downloadProgressNotesTemplate</em></p>
            </cfmail>
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="sendAdmissionSummaryDownloadErrorEmail" access="public" returntype="void" output="false" hint="Called from admission_summary_download_template.cfm on errors; not a remote endpoint.">
        <cfargument name="context" type="struct" required="true" />
        <cfargument name="errorCatch" type="any" required="false" />
        <cfset var mailTo = this.notificationEmails />
        <cfset var mailFrom = "error@myhomecarebiz.com" />
        <cfset var ctx = arguments.context />
        <cfset var ex = catchToStruct(arguments.errorCatch) />
        <cfset var subjMsg = "admissionSummaryDownload failed" />
        <cfset var apiJson = buildProgressNotesApiErrorJson(arguments.errorCatch) />
        <cfset var exJsonSafe = "" />
        <cfset var apiJsonSafe = "" />
        <cfset var exMsg = "" />
        <cfset var exDetail = "" />
        <cfset var exSql = "" />
        <cfset var exQueryErr = "" />
        <cfset var exStack = "" />
        <cfset var exForJson = {} />

        <cfif structIsEmpty(ex) AND isDefined("request.admission_summary_last_error") AND isStruct(request.admission_summary_last_error)>
            <cfset ex = duplicate(request.admission_summary_last_error) />
            <cfset apiJson = buildProgressNotesApiErrorJson(ex) />
        </cfif>

        <cfif isDefined("request.send_errors_to") AND len(trim(request.send_errors_to))>
            <cfset mailTo = request.send_errors_to />
        </cfif>
        <cfif isDefined("request.from") AND len(trim(request.from))>
            <cfset mailFrom = request.from />
        </cfif>

        <cfset exMsg = structKeyExists(ex, "message") ? ex.message : "" />
        <cfset exDetail = structKeyExists(ex, "detail") ? ex.detail : "" />
        <cfset exSql = structKeyExists(ex, "Sql") ? ex.Sql : (structKeyExists(ex, "sql") ? ex.sql : "") />
        <cfset exQueryErr = structKeyExists(ex, "queryError") ? ex.queryError : (structKeyExists(ex, "QueryError") ? ex.QueryError : "") />
        <cfset exStack = structKeyExists(ex, "stackTrace") ? ex.stackTrace : (structKeyExists(ex, "StackTrace") ? ex.StackTrace : "") />

        <cftry>
            <cfif len(trim(exMsg))>
                <cfset subjMsg = reReplace(left(trim(exMsg), 120), "[\r\n]+", " ", "all") />
            </cfif>
            <cftry>
                <cfset apiJsonSafe = htmlEditFormat(serializeJSON(apiJson)) />
                <cfcatch type="any">
                    <cfset apiJsonSafe = htmlEditFormat('{"success":false,"message":"' & exMsg & '"}') />
                </cfcatch>
            </cftry>
            <cftry>
                <cfset exForJson = duplicate(ex) />
                <cfif structKeyExists(exForJson, "TagContext")>
                    <cfset structDelete(exForJson, "TagContext") />
                </cfif>
                <cfset exJsonSafe = htmlEditFormat(serializeJSON(exForJson)) />
                <cfcatch type="any">
                    <cfset exJsonSafe = htmlEditFormat(structKeyList(ex)) />
                </cfcatch>
            </cftry>
            <cfmail to="#mailTo#"
                    from="#mailFrom#"
                    subject="Admission Summary Download - #subjMsg#"
                    type="html">
                <h3 style="color:red;">Admission Summary HTML download failed</h3>
                <p><strong>Timestamp:</strong> #dateFormat(now(), 'mm/dd/yyyy')# #timeFormat(now(), 'HH:mm:ss')#</p>
                <p><strong>Server:</strong> #htmlEditFormat(cgi.server_name)#</p>
                <p><strong>IP:</strong> #htmlEditFormat(cgi.remote_addr)#</p>
                <p><strong>Request URI / query:</strong> #htmlEditFormat(structKeyExists(cgi, 'request_url') ? cgi.request_url : (structKeyExists(cgi, 'script_name') ? cgi.script_name : ''))#</p>
                <p><strong>Query string:</strong> #htmlEditFormat(structKeyExists(cgi, 'query_string') ? cgi.query_string : '')#</p>
                <hr>
                <h4 style="color:red;">Exact error</h4>
                <div style="background:##fff3f3;border:1px solid ##c00;padding:12px;margin:8px 0;font-family:monospace;font-size:13px;">
                    <p style="margin:0 0 8px 0;"><strong>MESSAGE:</strong> #htmlEditFormat(exMsg)#</p>
                    <cfif len(trim(exDetail))>
                        <p style="margin:0 0 8px 0;"><strong>DETAIL:</strong> #htmlEditFormat(exDetail)#</p>
                    </cfif>
                    <cfif len(trim(exQueryErr))>
                        <p style="margin:0 0 8px 0;"><strong>QUERY ERROR:</strong> #htmlEditFormat(exQueryErr)#</p>
                    </cfif>
                    <p style="margin:0;"><strong>Error JSON:</strong></p>
                    <pre style="white-space:pre-wrap;word-break:break-word;margin:8px 0 0 0;">#apiJsonSafe#</pre>
                </div>
                <hr>
                <h4>Request context</h4>
                <table border="1" cellpadding="4" cellspacing="0">
                    <tr><td>Patient_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'patientId') ? ctx.patientId : '')#</td></tr>
                    <tr><td>Assmt_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'assmtId') ? ctx.assmtId : '')#</td></tr>
                    <tr><td>Admit_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'admitId') ? ctx.admitId : '')#</td></tr>
                    <tr><td>Agency (Loc_ID)</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'locId') ? ctx.locId : '')#</td></tr>
                    <tr><td>Agency schema</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'agencySchema') ? ctx.agencySchema : '')#</td></tr>
                    <tr><td>download_label</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'downloadLabel') ? ctx.downloadLabel : '')#</td></tr>
                    <tr><td>Template</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'templatePath') ? ctx.templatePath : '')#</td></tr>
                </table>
                <hr>
                <h4>Exception details</h4>
                <table border="1" cellpadding="4" cellspacing="0" style="font-size:12px;">
                    <tr><td><strong>Type</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'type') ? ex.type : '')#</td></tr>
                    <tr><td><strong>Message</strong></td><td>#htmlEditFormat(exMsg)#</td></tr>
                    <tr><td><strong>Detail</strong></td><td>#htmlEditFormat(exDetail)#</td></tr>
                    <tr><td><strong>Extended info</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'Extended_Info') ? ex.Extended_Info : (structKeyExists(ex, 'extendedInfo') ? ex.extendedInfo : ''))#</td></tr>
                    <tr><td><strong>Query name</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'queryName') ? ex.queryName : '')#</td></tr>
                    <tr><td><strong>DataSource</strong></td><td>#htmlEditFormat(structKeyExists(ex, 'DataSource') ? ex.DataSource : (structKeyExists(ex, 'datasource') ? ex.datasource : ''))#</td></tr>
                    <tr><td><strong>Native error code</strong></td><td>#htmlEditFormat("" & (structKeyExists(ex, 'nativeErrorCode') ? ex.nativeErrorCode : (structKeyExists(ex, 'NativeErrorCode') ? ex.NativeErrorCode : '')))#</td></tr>
                    <tr><td><strong>SQLState</strong></td><td>#htmlEditFormat("" & (structKeyExists(ex, 'sqlState') ? ex.sqlState : (structKeyExists(ex, 'SQLState') ? ex.SQLState : '')))#</td></tr>
                    <tr><td><strong>Query error</strong></td><td>#htmlEditFormat(exQueryErr)#</td></tr>
                    <tr><td><strong>SQL</strong></td><td><pre style="white-space:pre-wrap;word-break:break-word;">#htmlEditFormat(exSql)#</pre></td></tr>
                </table>
                <cfif structKeyExists(ex, "TagContext") AND isArray(ex.TagContext) AND arrayLen(ex.TagContext) GT 0>
                    <h4>Tag context (first frame)</h4>
                    <p><strong>Tag:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'tag') ? ex.TagContext[1].tag : '')#</p>
                    <p><strong>Template:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'template') ? ex.TagContext[1].template : '')#</p>
                    <p><strong>Line:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'line') ? ex.TagContext[1].line : '')#</p>
                </cfif>
                <cfif len(trim(exStack))>
                    <h4>Stack trace</h4>
                    <pre style="font-size:11px;white-space:pre-wrap;word-break:break-word;">#htmlEditFormat(exStack)#</pre>
                </cfif>
                <h4>Exception struct (JSON)</h4>
                <pre style="font-size:11px;white-space:pre-wrap;word-break:break-word;">#exJsonSafe#</pre>
                <hr>
                <p><em>Automated notification from admission_summary_download_template.cfm</em></p>
            </cfmail>
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="maskS3PathForEmail" access="private" returntype="string" output="false">
        <cfargument name="s3Path" type="string" required="true" />
        <cfset var p = arguments.s3Path />
        <cfif findNoCase("s3://", p) AND find("@", p)>
            <cfset p = reReplaceNoCase(p, "s3://[^@]+@", "s3://***:***@", "one") />
        </cfif>
        <cfreturn p />
    </cffunction>

    <cffunction name="sendMobileWhisperUploadErrorEmail" access="private" returntype="void" output="false">
        <cfargument name="uploadStep" type="string" required="true" />
        <cfargument name="context" type="struct" required="true" />
        <cfargument name="errorCatch" type="any" required="false" />
        <cfset var mailTo = this.notificationEmails />
        <cfset var mailFrom = "error@myhomecarebiz.com" />
        <cfset var ctx = arguments.context />
        <cfset var ex = {} />

        <cfif isDefined("request.send_errors_to") AND len(trim(request.send_errors_to))>
            <cfset mailTo = request.send_errors_to />
        </cfif>
        <cfif isDefined("request.from") AND len(trim(request.from))>
            <cfset mailFrom = request.from />
        </cfif>

        <cftry>
            <cfmail to="#mailTo#"
                    from="#mailFrom#"
                    subject="Mobile App API - Whisper upload failed at #arguments.uploadStep#"
                    type="html">
                <h3 style="color:red;">Mobile uploadWhisperFile failed</h3>
                <p><strong>Failed step:</strong> #htmlEditFormat(arguments.uploadStep)#</p>
                <p><strong>Timestamp:</strong> #dateFormat(now(), 'mm/dd/yyyy')# #timeFormat(now(), 'HH:mm:ss')#</p>
                <p><strong>Server:</strong> #htmlEditFormat(cgi.server_name)#</p>
                <p><strong>IP:</strong> #htmlEditFormat(cgi.remote_addr)#</p>
                <hr>
                <h4>Request context</h4>
                <table border="1" cellpadding="4" cellspacing="0">
                    <tr><td>Schedule_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'scheduleId') ? ctx.scheduleId : '')#</td></tr>
                    <tr><td>Emp_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'empId') ? ctx.empId : '')#</td></tr>
                    <tr><td>Agency (Loc_ID)</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'locId') ? ctx.locId : '')#</td></tr>
                    <tr><td>Agency schema</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'agencySchema') ? ctx.agencySchema : '')#</td></tr>
                    <tr><td>Patient_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'patientId') ? ctx.patientId : '')#</td></tr>
                    <tr><td>Assmt_ID</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'assmtId') ? ctx.assmtId : '')#</td></tr>
                    <tr><td>assessment_reason</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'assessmentReason') ? ctx.assessmentReason : '')#</td></tr>
                    <tr><td>Content-Type</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'contentType') ? ctx.contentType : '')#</td></tr>
                    <tr><td>Is multipart</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'isMultipart') ? ctx.isMultipart : '')#</td></tr>
                    <tr><td>Form fields</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'formFieldNames') ? ctx.formFieldNames : '')#</td></tr>
                    <tr><td>File field</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'fileFieldName') ? ctx.fileFieldName : '')#</td></tr>
                    <tr><td>fileBase64 sent</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'hasFileBase64') ? ctx.hasFileBase64 : '')#</td></tr>
                    <tr><td>Upload method</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'uploadMethod') ? ctx.uploadMethod : '')#</td></tr>
                    <tr><td>Client file</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'clientFile') ? ctx.clientFile : '')#</td></tr>
                    <tr><td>Server temp file</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'serverFile') ? ctx.serverFile : '')#</td></tr>
                    <tr><td>Extension</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'fileExt') ? ctx.fileExt : '')#</td></tr>
                    <tr><td>S3 bucket</td><td>#htmlEditFormat("" & structKeyExists(ctx, 's3Bucket') ? ctx.s3Bucket : '')#</td></tr>
                    <tr><td>S3 subfolder</td><td>#htmlEditFormat("" & structKeyExists(ctx, 's3Subfolder') ? ctx.s3Subfolder : '')#</td></tr>
                    <tr><td>S3 dest (masked)</td><td>#htmlEditFormat("" & structKeyExists(ctx, 's3DestMasked') ? ctx.s3DestMasked : '')#</td></tr>
                    <tr><td>Temp path</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'tempPath') ? ctx.tempPath : '')#</td></tr>
                    <tr><td>Temp file exists</td><td>#htmlEditFormat("" & structKeyExists(ctx, 'tempFileExists') ? ctx.tempFileExists : '')#</td></tr>
                </table>
                <cfif NOT isNull(arguments.errorCatch) AND isStruct(arguments.errorCatch)>
                    <cfset ex = arguments.errorCatch />
                    <hr>
                    <h4 style="color:red;">Exception</h4>
                    <p><strong>Type:</strong> #htmlEditFormat(structKeyExists(ex, 'type') ? ex.type : '')#</p>
                    <p><strong>Message:</strong> #htmlEditFormat(structKeyExists(ex, 'message') ? ex.message : '')#</p>
                    <p><strong>Detail:</strong> #htmlEditFormat(structKeyExists(ex, 'detail') ? ex.detail : '')#</p>
                    <cfif structKeyExists(ex, "TagContext") AND isArray(ex.TagContext) AND arrayLen(ex.TagContext) GT 0>
                        <p><strong>Tag:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'tag') ? ex.TagContext[1].tag : '')#</p>
                        <p><strong>Template:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'template') ? ex.TagContext[1].template : '')#</p>
                        <p><strong>Line:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'line') ? ex.TagContext[1].line : '')#</p>
                    </cfif>
                    <cfif structKeyExists(ex, "stacktrace")>
                        <h4>Stack trace</h4>
                        <pre style="font-size:11px;">#htmlEditFormat(ex.stacktrace)#</pre>
                    </cfif>
                </cfif>
                <hr>
                <p><em>Automated notification from mobile_app.cfc uploadWhisperFile</em></p>
            </cfmail>
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <!--- Generic mobile API error notification (vitals, catalog, etc.) --->
    <cffunction name="sendMobileApiErrorEmail" access="private" returntype="void" output="false">
        <cfargument name="methodName" type="string" required="true" />
        <cfargument name="failedStep" type="string" required="false" default="" />
        <cfargument name="context" type="struct" required="false" default="#{}#" />
        <cfargument name="errorCatch" type="any" required="false" />
        <cfset var mailTo = this.notificationEmails />
        <cfset var mailFrom = "error@myhomecarebiz.com" />
        <cfset var ctx = arguments.context />
        <cfset var ex = catchToStruct(arguments.errorCatch) />
        <cfset var ctxKey = "" />
        <cfset var exMsg = structKeyExists(ex, "message") ? ex.message : "" />
        <cfset var exDetail = structKeyExists(ex, "detail") ? ex.detail : "" />
        <cfset var exSql = structKeyExists(ex, "Sql") ? ex.Sql : (structKeyExists(ex, "sql") ? ex.sql : "") />
        <cfset var exQueryErr = structKeyExists(ex, "queryError") ? ex.queryError : (structKeyExists(ex, "QueryError") ? ex.QueryError : "") />
        <cfset var exStack = structKeyExists(ex, "stackTrace") ? ex.stackTrace : (structKeyExists(ex, "StackTrace") ? ex.StackTrace : "") />
        <cfset var subjMsg = arguments.methodName & " failed" />

        <cfif isDefined("request.send_errors_to") AND len(trim(request.send_errors_to))>
            <cfset mailTo = request.send_errors_to />
        </cfif>
        <cfif isDefined("request.from") AND len(trim(request.from))>
            <cfset mailFrom = request.from />
        </cfif>
        <cfif len(trim(exMsg))>
            <cfset subjMsg = arguments.methodName & " - " & reReplace(left(trim(exMsg), 80), "[\r\n]+", " ", "all") />
        </cfif>

        <cftry>
            <cfmail to="#mailTo#"
                    from="#mailFrom#"
                    subject="Mobile App API - #htmlEditFormat(subjMsg)#"
                    type="html">
                <h3 style="color:red;">Mobile #htmlEditFormat(arguments.methodName)# failed</h3>
                <cfif len(trim(arguments.failedStep))>
                    <p><strong>Failed step:</strong> #htmlEditFormat(arguments.failedStep)#</p>
                </cfif>
                <p><strong>Timestamp:</strong> #dateFormat(now(), 'mm/dd/yyyy')# #timeFormat(now(), 'HH:mm:ss')#</p>
                <p><strong>Server:</strong> #htmlEditFormat(cgi.server_name)#</p>
                <p><strong>IP:</strong> #htmlEditFormat(cgi.remote_addr)#</p>
                <p><strong>Request URI:</strong> #htmlEditFormat(structKeyExists(cgi, 'request_url') ? cgi.request_url : (structKeyExists(cgi, 'script_name') ? cgi.script_name : ''))#</p>
                <p><strong>Query string:</strong> #htmlEditFormat(structKeyExists(cgi, 'query_string') ? cgi.query_string : '')#</p>
                <hr>
                <h4 style="color:red;">Error summary</h4>
                <p><strong>Message:</strong> #htmlEditFormat(exMsg)#</p>
                <cfif len(trim(exDetail))>
                    <p><strong>Detail:</strong> #htmlEditFormat(exDetail)#</p>
                </cfif>
                <cfif len(trim(exQueryErr))>
                    <p><strong>Query error:</strong> #htmlEditFormat(exQueryErr)#</p>
                </cfif>
                <cfif len(trim(exSql))>
                    <p><strong>SQL:</strong></p>
                    <pre style="font-size:11px;white-space:pre-wrap;word-break:break-word;">#htmlEditFormat(exSql)#</pre>
                </cfif>
                <cfif NOT structIsEmpty(ctx)>
                    <hr>
                    <h4>Request context</h4>
                    <table border="1" cellpadding="4" cellspacing="0">
                        <cfloop collection="#ctx#" item="ctxKey">
                            <tr>
                                <td>#htmlEditFormat(ctxKey)#</td>
                                <td>#htmlEditFormat(isSimpleValue(ctx[ctxKey]) ? ctx[ctxKey] : serializeJSON(ctx[ctxKey]))#</td>
                            </tr>
                        </cfloop>
                    </table>
                </cfif>
                <cfif structKeyExists(ex, "TagContext") AND isArray(ex.TagContext) AND arrayLen(ex.TagContext) GT 0>
                    <hr>
                    <h4>Location</h4>
                    <p><strong>Template:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'template') ? ex.TagContext[1].template : '')#</p>
                    <p><strong>Line:</strong> #htmlEditFormat(structKeyExists(ex.TagContext[1], 'line') ? ex.TagContext[1].line : '')#</p>
                </cfif>
                <cfif len(trim(exStack))>
                    <h4>Stack trace</h4>
                    <pre style="font-size:11px;white-space:pre-wrap;word-break:break-word;">#htmlEditFormat(exStack)#</pre>
                </cfif>
                <hr>
                <p><em>Automated notification from mobile_app.cfc #htmlEditFormat(arguments.methodName)#</em></p>
            </cfmail>
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <!--- Whisper S3 settings (same as patientadmin_new/patient/whisper_fileupload.cfm) --->
    <cffunction name="getWhisperS3Config" access="private" returntype="struct" output="false">
        <cfargument name="agencyId" type="numeric" required="true" />
        <cfreturn {
            bucket = "s3-pattii-extractaiapi-assets",
            accessKey = "AKIA435RTMNDOUH4J3YW",
            secretKey = "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",
            region = "us-east-1",
            subfolder = "whisperfiles/agency_" & val(arguments.agencyId),
            allowedExts = "mp3,mp4,wav,ogg,webm,m4a,avi,mov,flac,aac,wma,json"
        } />
    </cffunction>

    <!--- S3 object URL; prefix is created implicitly on put (no cfdirectory needed) --->
    <cffunction name="buildWhisperS3ObjectUrl" access="private" returntype="string" output="false">
        <cfargument name="s3cfg" type="struct" required="true" />
        <cfargument name="dupFilename" type="string" required="true" />
        <cfreturn "s3://" & arguments.s3cfg.accessKey & ":" & arguments.s3cfg.secretKey & "@" & arguments.s3cfg.bucket & "/" & arguments.s3cfg.subfolder & "/" & arguments.dupFilename />
    </cffunction>

    <!--- Upload via fileWrite (patients_ai_action.cfm pattern); avoids failing cfdirectory on S3 --->
    <cffunction name="putWhisperFileOnS3" access="private" returntype="void" output="false">
        <cfargument name="localSourcePath" type="string" required="true" />
        <cfargument name="s3cfg" type="struct" required="true" />
        <cfargument name="dupFilename" type="string" required="true" />
        <cfset var s3Url = buildWhisperS3ObjectUrl(arguments.s3cfg, arguments.dupFilename) />
        <cfset var fileContent = "" />

        <cffile action="readBinary" file="#arguments.localSourcePath#" variable="fileContent" />
        <cfscript>
            fileWrite(s3Url, fileContent);
        </cfscript>
    </cffunction>

    <cffunction name="ensureWhisperFilesTable" access="private" returntype="void" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cftry>
            <cfquery datasource="#Application.DataSrc#">
                CREATE TABLE IF NOT EXISTS #arguments.agencySchema#.whisper_files (
                  ID            BIGINT        NOT NULL AUTO_INCREMENT PRIMARY KEY,
                  Status        TINYINT       NOT NULL DEFAULT 0,
                  Review_Status TINYINT       NOT NULL DEFAULT 0,
                  Upload_Source VARCHAR(20)   NOT NULL DEFAULT '',
                  Agency_Id     VARCHAR(50)   NOT NULL DEFAULT '',
                  Patient_Id    VARCHAR(50)   NOT NULL DEFAULT '',
                  Assmt_ID      VARCHAR(50)   NULL,
                  DOC_ID        VARCHAR(50)   NULL,
                  Title         VARCHAR(255)  NOT NULL DEFAULT '',
                  FileName      VARCHAR(255)  NOT NULL DEFAULT '',
                  Dup_file_name VARCHAR(255)  NOT NULL DEFAULT '',
                  FileType      VARCHAR(100)  NOT NULL DEFAULT '',
                  File          MEDIUMBLOB    NULL,
                  Notes         TEXT          NULL,
                  faxsend_to    VARCHAR(255)  NULL,
                  faxsent_time  DATETIME      NULL,
                  Created       DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP,
                  CreatedBy     VARCHAR(50)   NOT NULL DEFAULT '',
                  INDEX idx_whisper_agency_patient (Agency_Id, Patient_Id),
                  INDEX idx_whisper_status (Status),
                  INDEX idx_whisper_review_status (Review_Status),
                  INDEX idx_whisper_assmt_review (Assmt_ID, Review_Status)
                )
            </cfquery>
            <cfset ensureWhisperFilesReviewColumns(arguments.agencySchema) />
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <!--- Review_Status: 0=none, 1=review required (set on upload), 2=clinician review completed --->
    <cffunction name="ensureWhisperFilesReviewColumns" access="private" returntype="void" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfset var existingCols = "" />
        <cfset var existingIndexes = "" />
        <cftry>
            <cfquery name="qRevCols" datasource="#Application.DataSrc#">
                SELECT COLUMN_NAME
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_SCHEMA = <cfqueryparam value="#arguments.agencySchema#" cfsqltype="cf_sql_varchar">
                  AND TABLE_NAME = <cfqueryparam value="whisper_files" cfsqltype="cf_sql_varchar">
                  AND COLUMN_NAME IN ('Review_Status', 'Upload_Source')
            </cfquery>
            <cfset existingCols = valueList(qRevCols.COLUMN_NAME) />
            <cfif NOT listFindNoCase(existingCols, 'Review_Status')>
                <cfquery datasource="#Application.DataSrc#">
                    ALTER TABLE #arguments.agencySchema#.whisper_files
                    ADD COLUMN Review_Status TINYINT NOT NULL DEFAULT 0
                </cfquery>
            </cfif>
            <cfif NOT listFindNoCase(existingCols, 'Upload_Source')>
                <cfquery datasource="#Application.DataSrc#">
                    ALTER TABLE #arguments.agencySchema#.whisper_files
                    ADD COLUMN Upload_Source VARCHAR(20) NOT NULL DEFAULT ''
                </cfquery>
            </cfif>
            <cfquery name="qRevIdx" datasource="#Application.DataSrc#">
                SELECT INDEX_NAME
                FROM INFORMATION_SCHEMA.STATISTICS
                WHERE TABLE_SCHEMA = <cfqueryparam value="#arguments.agencySchema#" cfsqltype="cf_sql_varchar">
                  AND TABLE_NAME = <cfqueryparam value="whisper_files" cfsqltype="cf_sql_varchar">
                  AND INDEX_NAME IN ('idx_whisper_review_status', 'idx_whisper_assmt_review')
                GROUP BY INDEX_NAME
            </cfquery>
            <cfset existingIndexes = valueList(qRevIdx.INDEX_NAME) />
            <cfif NOT listFindNoCase(existingIndexes, 'idx_whisper_review_status')>
                <cfquery datasource="#Application.DataSrc#">
                    ALTER TABLE #arguments.agencySchema#.whisper_files
                    ADD INDEX idx_whisper_review_status (Review_Status)
                </cfquery>
            </cfif>
            <cfif NOT listFindNoCase(existingIndexes, 'idx_whisper_assmt_review')>
                <cfquery datasource="#Application.DataSrc#">
                    ALTER TABLE #arguments.agencySchema#.whisper_files
                    ADD INDEX idx_whisper_assmt_review (Assmt_ID, Review_Status)
                </cfquery>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

  <!--- Resolve schedule for authenticated agency/employee --->
    <cffunction name="getScheduleForMobileUpload" 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 = scheduleQuery.Schedule_ID,
            Patient_ID = scheduleQuery.Patient_ID,
            Assmt_ID = scheduleQuery.Assmt_ID,
            Schedule_Assmt_ID = scheduleQuery.Assmt_ID,
            Emp_ID = scheduleQuery.Emp_ID,
            Visit_Type = scheduleQuery.Visit_Type,
            Visit_Date = scheduleQuery.Visit_Date,
            Skill = scheduleQuery.Skill
        } />
        <cfreturn result />
    </cffunction>

    <!--- #10459: pSchedules.Assmt_ID often points at SOC; resolve assessment for this visit from Visit_Type + visit date --->
    <cffunction name="resolveScheduleAssmtIdByVisitType" access="private" returntype="numeric" output="false"
        hint="Match pAssessments.F26 from Visit_Type (same CASE as assessment_assign); prefer row on schedule visit date">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="scheduleAssmtId" type="numeric" required="true" />
        <cfargument name="visitType" type="string" required="true" />
        <cfargument name="visitDate" type="any" required="false" default="" />

        <cfset var targetF26 = mapOasisTypeCodeToWizardAssessmentReason(trim(arguments.visitType)) />
        <cfset var fallbackAssmtId = val(arguments.scheduleAssmtId) />
        <cfset var admitId = 0 />
        <cfset var visitDateStr = "" />
        <cfset var qAdmit = "" />
        <cfset var qAssmt = "" />

        <cfif NOT len(trim(targetF26)) OR fallbackAssmtId LTE 0 OR arguments.patientId LTE 0>
            <cfreturn fallbackAssmtId />
        </cfif>

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

        <cfif isDate(arguments.visitDate)>
            <cfset visitDateStr = dateFormat(arguments.visitDate, "yyyy-mm-dd") />
        <cfelseif len(trim("" & arguments.visitDate))>
            <cftry>
                <cfset visitDateStr = dateFormat(parseDateTime(trim("" & arguments.visitDate)), "yyyy-mm-dd") />
                <cfcatch type="any">
                    <cfset visitDateStr = left(trim("" & arguments.visitDate), 10) />
                </cfcatch>
            </cftry>
        </cfif>

        <cfif len(visitDateStr)>
            <cfquery name="qAssmt" datasource="#Application.DataSrc#">
                SELECT Assmt_ID
                FROM #arguments.agencySchema#.pAssessments
                WHERE F26 = <cfqueryparam value="#targetF26#" cfsqltype="cf_sql_varchar">
                  AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
                  AND Admit_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#admitId#">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                  AND DATE_FORMAT(F176, '%Y-%m-%d') = <cfqueryparam value="#visitDateStr#" cfsqltype="cf_sql_varchar">
                ORDER BY F177 DESC, AutoAssmt_ID DESC
                LIMIT 1
            </cfquery>
            <cfif qAssmt.recordCount GT 0 AND val(qAssmt.Assmt_ID) GT 0>
                <cfreturn val(qAssmt.Assmt_ID) />
            </cfif>
        </cfif>

        <cfquery name="qAssmt" datasource="#Application.DataSrc#">
            SELECT Assmt_ID
            FROM #arguments.agencySchema#.pAssessments
            WHERE F26 = <cfqueryparam value="#targetF26#" cfsqltype="cf_sql_varchar">
              AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Admit_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#admitId#">
              AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
            ORDER BY F176 DESC, F177 DESC
            LIMIT 1
        </cfquery>
        <cfif qAssmt.recordCount GT 0 AND val(qAssmt.Assmt_ID) GT 0>
            <cfreturn val(qAssmt.Assmt_ID) />
        </cfif>

        <cfreturn fallbackAssmtId />
    </cffunction>

    <!--- Resolve schedule → patient, admit, assessment (Visit_Type + Visit_Date + Admit_ID) --->
    <cffunction name="resolveMobileScheduleAssessmentContext" 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 = "",
            agencySchema = arguments.agencySchema,
            scheduleId = arguments.scheduleId,
            patientId = 0,
            admitId = 0,
            scheduleAssmtId = 0,
            assmtId = 0,
            visitType = "",
            visitDate = "",
            skill = "",
            assessmentReason = "",
            schedule = {}
        } />
        <cfset var scheduleCtx = {} />
        <cfset var infoQuery = "" />
        <cfset var admitQuery = "" />
        <cfset var assmtRow = "" />
        <cfset var assmtLoad = {} />

        <cfset scheduleCtx = getScheduleForMobileUpload(
            agencySchema = arguments.agencySchema,
            scheduleId = arguments.scheduleId,
            empId = arguments.empId
        ) />
        <cfif NOT scheduleCtx.ok>
            <cfset result.message = scheduleCtx.message />
            <cfreturn result />
        </cfif>

        <cfquery name="infoQuery" datasource="#Application.DataSrc#">
            SELECT s.Schedule_ID,
                   s.Patient_ID,
                   s.Assmt_ID,
                   s.Visit_Type,
                   DATE_FORMAT(s.Visit_Date, '%Y-%m-%d') AS Visit_Date,
                   s.Skill,
                   s.Missed,
                   s.patient_sign,
                   IFNULL(pa.Admit_ID, 0) AS Admit_ID,
                   DATE_FORMAT(s.EVV_start, '%Y-%m-%d %H:%i:%s') AS EVV_start,
                   DATE_FORMAT(s.EVV_end, '%Y-%m-%d %H:%i:%s') AS EVV_end,
                   TIME_FORMAT(s.GPS_start_time, '%H:%i:%s') AS GPS_start_time,
                   TIME_FORMAT(s.GPS_end_time, '%H:%i:%s') AS GPS_end_time
            FROM #arguments.agencySchema#.pSchedules s
            LEFT JOIN #arguments.agencySchema#.pAssessments pa
              ON pa.Assmt_ID = s.Assmt_ID
             AND pa.Patient_ID = s.Patient_ID
             AND pa.Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
            WHERE s.Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">
              AND s.Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
            LIMIT 1
        </cfquery>
        <cfif infoQuery.recordCount EQ 0>
            <cfset result.message = "Schedule not found" />
            <cfreturn result />
        </cfif>

        <cfset result.patientId = val(infoQuery.Patient_ID) />
        <cfset result.scheduleAssmtId = val(infoQuery.Assmt_ID) />
        <cfset result.admitId = val(infoQuery.Admit_ID) />
        <cfset result.visitType = trim(infoQuery.Visit_Type) />
        <cfset result.visitDate = trim(infoQuery.Visit_Date) />
        <cfset result.skill = trim(infoQuery.Skill) />
        <cfset result.schedule = {
            Missed = infoQuery.Missed,
            EVV_start = infoQuery.EVV_start,
            EVV_end = infoQuery.EVV_end,
            GPS_start_time = infoQuery.GPS_start_time,
            GPS_end_time = infoQuery.GPS_end_time
        } />

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

        <cfset result.assmtId = resolveScheduleAssmtIdByVisitType(
            agencySchema = arguments.agencySchema,
            patientId = result.patientId,
            scheduleAssmtId = result.scheduleAssmtId,
            visitType = result.visitType,
            visitDate = result.visitDate
        ) />

        <cfif result.assmtId GT 0>
            <cfquery name="assmtRow" datasource="#Application.DataSrc#">
                SELECT F26, F25, Admit_ID
                FROM #arguments.agencySchema#.pAssessments
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.assmtId#">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                LIMIT 1
            </cfquery>
            <cfif assmtRow.recordCount GT 0>
                <cfset result.assessmentReason = trim(assmtRow.F26) />
                <cfif val(assmtRow.Admit_ID) GT 0>
                    <cfset result.admitId = val(assmtRow.Admit_ID) />
                </cfif>
            </cfif>
        </cfif>
        <cfif NOT len(result.assessmentReason)>
            <cfset result.assessmentReason = mapOasisTypeCodeToWizardAssessmentReason(result.visitType) />
        </cfif>

        <cfset assmtLoad = loadMobileAssessmentRowForVisit(
            agencySchema = arguments.agencySchema,
            patientId = result.patientId,
            admitId = result.admitId,
            assessmentReason = result.assessmentReason,
            visitDate = result.visitDate,
            fallbackAssmtId = result.assmtId
        ) />
        <cfif assmtLoad.assmtId GT 0>
            <cfset result.assmtId = assmtLoad.assmtId />
        </cfif>

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

    <!--- Load active pAssessments row for this visit: F26 + Admit_ID + Visit_Date (+ Patient_ID) --->
    <cffunction name="loadMobileAssessmentRowForVisit" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="admitId" type="numeric" required="true" />
        <cfargument name="assessmentReason" type="string" required="true" />
        <cfargument name="visitDate" type="string" required="false" default="" />
        <cfargument name="fallbackAssmtId" type="numeric" required="false" default="0" />
        <cfset var out = { assmtId = 0, assmtQuery = queryNew("") } />
        <cfset var visitDateStr = "" />
        <cfset var qAssmt = "" />

        <cfif arguments.patientId LTE 0 OR NOT len(trim(arguments.assessmentReason))>
            <cfreturn out />
        </cfif>

        <cfif isDate(arguments.visitDate)>
            <cfset visitDateStr = dateFormat(arguments.visitDate, "yyyy-mm-dd") />
        <cfelseif len(trim(arguments.visitDate))>
            <cftry>
                <cfset visitDateStr = dateFormat(parseDateTime(trim(arguments.visitDate)), "yyyy-mm-dd") />
                <cfcatch type="any">
                    <cfset visitDateStr = left(trim(arguments.visitDate), 10) />
                </cfcatch>
            </cftry>
        </cfif>

        <cfif arguments.admitId GT 0 AND len(visitDateStr)>
            <cfquery name="qAssmt" datasource="#Application.DataSrc#">
                SELECT *
                FROM #arguments.agencySchema#.pAssessments
                WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
                  AND Admit_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.admitId#">
                  AND F26 = <cfqueryparam value="#trim(arguments.assessmentReason)#" cfsqltype="cf_sql_varchar">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                  AND DATE_FORMAT(F176, '%Y-%m-%d') = <cfqueryparam value="#visitDateStr#" cfsqltype="cf_sql_varchar">
                ORDER BY AutoAssmt_ID DESC
                LIMIT 1
            </cfquery>
            <cfif qAssmt.recordCount GT 0>
                <cfset out.assmtId = val(qAssmt.Assmt_ID) />
                <cfset out.assmtQuery = qAssmt />
                <cfreturn out />
            </cfif>
        </cfif>

        <cfif arguments.admitId GT 0>
            <cfquery name="qAssmt" datasource="#Application.DataSrc#">
                SELECT *
                FROM #arguments.agencySchema#.pAssessments
                WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
                  AND Admit_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.admitId#">
                  AND F26 = <cfqueryparam value="#trim(arguments.assessmentReason)#" cfsqltype="cf_sql_varchar">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                ORDER BY F176 DESC, AutoAssmt_ID DESC
                LIMIT 1
            </cfquery>
            <cfif qAssmt.recordCount GT 0>
                <cfset out.assmtId = val(qAssmt.Assmt_ID) />
                <cfset out.assmtQuery = qAssmt />
                <cfreturn out />
            </cfif>
        </cfif>

        <cfif val(arguments.fallbackAssmtId) GT 0>
            <cfquery name="qAssmt" datasource="#Application.DataSrc#">
                SELECT *
                FROM #arguments.agencySchema#.pAssessments
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#val(arguments.fallbackAssmtId)#">
                  AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                ORDER BY AutoAssmt_ID DESC
                LIMIT 1
            </cfquery>
            <cfif qAssmt.recordCount GT 0>
                <cfset out.assmtId = val(qAssmt.Assmt_ID) />
                <cfset out.assmtQuery = qAssmt />
            </cfif>
        </cfif>

        <cfreturn out />
    </cffunction>

    <!--- answer_label from lookup is the pAssessments column name (F30, F144, etc.) --->
    <cffunction name="isMobileAssessmentAnswerColumnName" access="private" returntype="boolean" output="false">
        <cfargument name="fieldName" type="string" required="true" />
        <cfset var field = uCase(trim(arguments.fieldName)) />
        <cfif NOT len(field) OR field EQ "F175">
            <cfreturn false />
        </cfif>
        <cfreturn reFind("^(F[0-9]+|J[0-9]+|M[0-9]+|GG[0-9A-Z]+)$", field) GT 0 />
    </cffunction>

    <!--- True when field exists on pAssessments row (case-insensitive column match) --->
    <cffunction name="mobileAssessmentsHasColumn" access="private" returntype="boolean" output="false">
        <cfargument name="assmtQuery" type="query" required="true" />
        <cfargument name="fieldName" type="string" required="true" />
        <cfset var col = "" />
        <cfset var field = uCase(trim(arguments.fieldName)) />
        <cfif NOT len(field) OR arguments.assmtQuery.recordCount EQ 0>
            <cfreturn false />
        </cfif>
        <cfloop list="#arguments.assmtQuery.columnList#" index="col">
            <cfif compareNoCase(col, field) EQ 0>
                <cfreturn true />
            </cfif>
        </cfloop>
        <cfreturn false />
    </cffunction>

    <!--- Read pAssessments column value by answer_label (case-insensitive); empty/null returns "" --->
    <cffunction name="getMobileAssessmentFieldValue" access="private" returntype="string" output="false">
        <cfargument name="assmtQuery" type="query" required="true" />
        <cfargument name="fieldName" type="string" required="true" />
        <cfset var col = "" />
        <cfset var raw = "" />
        <cfset var field = uCase(trim("" & arguments.fieldName)) />
        <cfif NOT len(field) OR arguments.assmtQuery.recordCount EQ 0 OR NOT isMobileAssessmentAnswerColumnName(field)>
            <cfreturn "" />
        </cfif>
        <cfloop list="#arguments.assmtQuery.columnList#" index="col">
            <cfif compareNoCase(col, field) EQ 0>
                <cfset raw = arguments.assmtQuery[col][1] />
                <cfif len(trim("" & raw))>
                    <cfreturn trim("" & raw) />
                </cfif>
                <cfreturn "" />
            </cfif>
        </cfloop>
        <cfreturn "" />
    </cffunction>

    <!--- True when related pAssessments column (answer_label) has a saved value --->
    <cffunction name="isMobileAssessmentAnswerLabelSatisfied" access="private" returntype="boolean" output="false">
        <cfargument name="assmtQuery" type="query" required="true" />
        <cfargument name="answerLabel" type="string" required="true" />
        <cfargument name="oneOfGroups" type="struct" required="true" />
        <cfset var labelKey = uCase(trim(arguments.answerLabel)) />
        <cfset var labelsToCheck = structKeyExists(arguments.oneOfGroups, labelKey)
            ? arguments.oneOfGroups[labelKey]
            : [labelKey] />
        <cfset var checkLabel = "" />
        <cfloop array="#labelsToCheck#" index="checkLabel">
            <cfset checkLabel = uCase(trim("" & checkLabel)) />
            <cfif isMobileAssessmentAnswerColumnName(checkLabel) AND len(getMobileAssessmentFieldValue(arguments.assmtQuery, checkLabel))>
                <cfreturn true />
            </cfif>
        </cfloop>
        <cfreturn false />
    </cffunction>

    <!--- Per wizard block: completed when answer_label column on pAssessments has a value --->
    <cffunction name="getMobileWizardBlockQuestionProgress" access="private" returntype="struct" output="false">
        <cfargument name="assmtQuery" type="query" required="true" />
        <cfargument name="quesQuery" type="query" required="true" />
        <cfargument name="oneOfGroups" type="struct" required="true" />
        <cfset var out = {
            questions_total = 0,
            questions_completed = 0,
            completed = false,
            question_count = 0
        } />
        <cfset var distinctQ = "" />
        <cfset var countedQues = {} />
        <cfset var quesKey = "" />
        <cfset var answerLabel = "" />
        <cfset var hasImportant = false />
        <cfset var allImportantDone = true />
        <cfset var isDone = false />

        <cfif arguments.quesQuery.recordCount EQ 0>
            <cfset out.completed = true />
            <cfreturn out />
        </cfif>

        <cfquery name="distinctQ" dbtype="query">
            SELECT ques_id FROM quesQuery GROUP BY ques_id
        </cfquery>
        <cfset out.question_count = distinctQ.recordCount />

        <cfloop query="quesQuery" group="ques_id">
            <cfset answerLabel = uCase(trim(quesQuery.answer_label)) />
            <cfif answerLabel EQ "F175" OR NOT len(answerLabel) OR NOT isMobileAssessmentAnswerColumnName(answerLabel)>
                <cfcontinue />
            </cfif>
            <cfif arguments.assmtQuery.recordCount GT 0 AND NOT mobileAssessmentsHasColumn(arguments.assmtQuery, answerLabel)>
                <cfcontinue />
            </cfif>
            <cfset quesKey = "" & val(quesQuery.ques_id) />
            <cfif structKeyExists(countedQues, quesKey)><cfcontinue /></cfif>
            <cfset countedQues[quesKey] = true />
            <cfset out.questions_total = out.questions_total + 1 />

            <cfset isDone = false />
            <cfif arguments.assmtQuery.recordCount GT 0>
                <cfset isDone = isMobileAssessmentAnswerLabelSatisfied(arguments.assmtQuery, answerLabel, arguments.oneOfGroups) />
            </cfif>
            <cfif isDone>
                <cfset out.questions_completed = out.questions_completed + 1 />
            </cfif>

            <cfif listFindNoCase(quesQuery.columnList, "important") AND quesQuery.important EQ 1
                AND listFindNoCase(quesQuery.columnList, "new_wizard") GT 0 AND quesQuery.new_wizard NEQ 0>
                <cfset hasImportant = true />
                <cfif NOT isDone>
                    <cfset allImportantDone = false />
                </cfif>
            </cfif>
        </cfloop>

        <cfif hasImportant>
            <cfset out.completed = allImportantDone />
        <cfelse>
            <cfset out.completed = (out.questions_total GT 0 AND out.questions_completed GTE out.questions_total) />
        </cfif>
        <cfset out.progress_label = formatMobileTabProgressLabel(out.questions_completed, out.questions_total) />
        <cfreturn out />
    </cffunction>

    <!--- Tab fraction from actual completed F-column answers (e.g. 12/16); Review & Submit uses em dash --->
    <cffunction name="formatMobileTabProgressLabel" access="private" returntype="string" output="false">
        <cfargument name="questionsCompleted" type="numeric" required="true" />
        <cfargument name="questionsTotal" type="numeric" required="true" />
        <cfargument name="wizard" type="numeric" required="false" default="-1" />
        <cfif val(arguments.wizard) EQ 0>
            <cfreturn "—" />
        </cfif>
        <cfif val(arguments.questionsTotal) GT 0>
            <cfreturn val(arguments.questionsCompleted) & "/" & val(arguments.questionsTotal) />
        </cfif>
        <cfreturn "" />
    </cffunction>

    <!--- Load assessment_tabs for resolved assessment_reason; merge wizard question progress --->
    <cffunction name="getMobileAssessmentTabsSummary" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="assessmentReason" type="string" required="true" />
        <cfargument name="admitId" type="numeric" required="false" default="0" />
        <cfargument name="visitDate" type="string" required="false" default="" />
        <cfset var out = {
            assessment_reason = arguments.assessmentReason,
            tabs = [],
            tab_count = 0,
            sections_total = 0,
            sections_completed = 0,
            questions_total = 0,
            questions_completed = 0,
            progress_percent = 0,
            total_progress_label = "",
            assessment_complete = false
        } />
        <cfset var lookupDb = "hhapowerpath" />
        <cfset var tabsQuery = "" />
        <cfset var assmtQuery = "" />
        <cfset var socWizard = "" />
        <cfset var rulesQuery = "" />
        <cfset var oneOfGroups = {} />
        <cfset var targetsRaw = [] />
        <cfset var targets = [] />
        <cfset var t = "" />
        <cfset var tabRow = {} />
        <cfset var blockProgress = {} />
        <cfset var quesQuery = "" />
        <cfset var progressByWizard = {} />
        <cfset var wKey = "" />
        <cfset var assmtLoad = {} />

        <cfif NOT len(trim(arguments.assessmentReason))>
            <cfreturn out />
        </cfif>
        <cfif NOT isDefined("Request.prefix_db_lookup") OR NOT len(trim(Request.prefix_db_lookup))>
            <cfset Request.prefix_db_lookup = "hhapowerpath" />
        </cfif>
        <cfset lookupDb = trim(Request.prefix_db_lookup) />
        <cfset Request.prefix_db_agency = arguments.agencySchema />
        <cfset assmtLoad = loadMobileAssessmentRowForVisit(
            agencySchema = arguments.agencySchema,
            patientId = arguments.patientId,
            admitId = val(arguments.admitId),
            assessmentReason = arguments.assessmentReason,
            visitDate = arguments.visitDate,
            fallbackAssmtId = arguments.assmtId
        ) />
        <cfset assmtQuery = assmtLoad.assmtQuery />
        <cfif assmtLoad.assmtId GT 0>
            <cfset arguments.assmtId = assmtLoad.assmtId />
        </cfif>

        <cfif assmtQuery.recordCount GT 0 AND val(assmtQuery.F25) EQ 1>
            <cfset out.assessment_complete = true />
        </cfif>

        <cfset socWizard = createObject("component", "components.general.soc_wizard").init() />
        <cftry>
            <cfset rulesQuery = socWizard.get_question_importance_rules(arguments.assessmentReason) />
            <cfif rulesQuery.recordCount GT 0 AND listFindNoCase(rulesQuery.columnList, "Validation_Condition")>
                <cfloop query="rulesQuery">
                    <cfif trim(rulesQuery.Validation_Condition) EQ "one_of_required" AND len(trim(rulesQuery.Target_Questions))>
                        <cfset targetsRaw = listToArray(rulesQuery.Target_Questions, ",", false, true) />
                        <cfset targets = [] />
                        <cfloop array="#targetsRaw#" index="t">
                            <cfset t = trim(t) />
                            <cfif len(t)>
                                <cfset arrayAppend(targets, uCase(t)) />
                                <cfset oneOfGroups[uCase(t)] = targets />
                            </cfif>
                        </cfloop>
                    </cfif>
                </cfloop>
            </cfif>
            <cfcatch type="any"><cfset oneOfGroups = {} /></cfcatch>
        </cftry>

        <cftry>
            <!--- Prefer assessment_tabs.new_wizard (= questions.new_wizard); falls back to wizard --->
            <cfset tabsQuery = socWizard.get_assessment_tabs(arguments.assessmentReason) />
            <cfcatch type="any">
                <cfset tabsQuery = queryNew("assessment_tabs_id,tab_name,wizard,new_wizard,status") />
            </cfcatch>
        </cftry>

        <cfif tabsQuery.recordCount EQ 0>
            <cfset tabsQuery = socWizard.get_new_wizard_blocks(arguments.assessmentReason) />
            <cfloop query="tabsQuery">
                <cfset wKey = "" & val(tabsQuery.new_wizard) />
                <cfif NOT structKeyExists(progressByWizard, wKey)>
                    <cfset quesQuery = socWizard.get_all_ques_ans_new_wizard(
                        assessment_reason = arguments.assessmentReason,
                        new_wizard = val(tabsQuery.new_wizard),
                        view_mode = "wizard"
                    ) />
                    <cfset blockProgress = getMobileWizardBlockQuestionProgress(assmtQuery, quesQuery, oneOfGroups) />
                    <cfset progressByWizard[wKey] = blockProgress />
                </cfif>
                <cfset tabRow = {
                    assessment_tabs_id = 0,
                    tab_name = socWizard.get_new_wizard_tab_label_for_block(arguments.assessmentReason, val(tabsQuery.new_wizard)),
                    wizard = val(tabsQuery.new_wizard),
                    new_wizard = val(tabsQuery.new_wizard),
                    assessment_reason = arguments.assessmentReason,
                    questions_total = progressByWizard[wKey].questions_total,
                    questions_completed = progressByWizard[wKey].questions_completed,
                    progress_label = progressByWizard[wKey].progress_label,
                    tab_status = progressByWizard[wKey].completed ? "complete" : (progressByWizard[wKey].questions_completed GT 0 ? "in_progress" : "not_started")
                } />
                <cfset out.sections_total = out.sections_total + 1 />
                <cfif tabRow.tab_status EQ "complete">
                    <cfset out.sections_completed = out.sections_completed + 1 />
                </cfif>
                <cfset arrayAppend(out.tabs, tabRow) />
            </cfloop>
        <cfelse>
            <cfloop query="tabsQuery">
                <cfset wizardBlock = ListFindNoCase(tabsQuery.columnList, "new_wizard") GT 0 ? val(tabsQuery.new_wizard) : val(tabsQuery.wizard) />
                <cfif wizardBlock LT 1>
                    <cfset wizardBlock = val(tabsQuery.wizard) />
                </cfif>
                <cfset wKey = "" & wizardBlock />
                <cfif wizardBlock GT 0 AND NOT structKeyExists(progressByWizard, wKey)>
                    <cfset quesQuery = socWizard.get_all_ques_ans_new_wizard(
                        assessment_reason = arguments.assessmentReason,
                        new_wizard = wizardBlock,
                        view_mode = "wizard"
                    ) />
                    <cfset blockProgress = getMobileWizardBlockQuestionProgress(assmtQuery, quesQuery, oneOfGroups) />
                    <cfset progressByWizard[wKey] = blockProgress />
                </cfif>
                <cfset tabRow = {
                    assessment_tabs_id = val(tabsQuery.assessment_tabs_id),
                    tab_name = trim(tabsQuery.tab_name),
                    wizard = wizardBlock,
                    new_wizard = wizardBlock,
                    assessment_reason = arguments.assessmentReason,
                    questions_total = 0,
                    questions_completed = 0,
                    progress_label = "",
                    tab_status = "not_started"
                } />
                <cfif wizardBlock GT 0 AND structKeyExists(progressByWizard, wKey)>
                    <cfset tabRow.questions_total = progressByWizard[wKey].questions_total />
                    <cfset tabRow.questions_completed = progressByWizard[wKey].questions_completed />
                    <cfset tabRow.progress_label = progressByWizard[wKey].progress_label />
                    <cfset tabRow.tab_status = progressByWizard[wKey].completed ? "complete" : (tabRow.questions_completed GT 0 ? "in_progress" : "not_started") />
                <cfelseif wizardBlock EQ 0>
                    <cfset tabRow.progress_label = formatMobileTabProgressLabel(0, 0, 0) />
                    <cfset tabRow.tab_status = out.assessment_complete ? "complete" : "pending" />
                </cfif>
                <cfif tabRow.questions_total GT 0 OR wizardBlock EQ 0>
                    <cfset out.sections_total = out.sections_total + 1 />
                    <cfif tabRow.tab_status EQ "complete">
                        <cfset out.sections_completed = out.sections_completed + 1 />
                    </cfif>
                </cfif>
                <cfset arrayAppend(out.tabs, tabRow) />
            </cfloop>
        </cfif>

        <cfset out.tab_count = arrayLen(out.tabs) />
        <cfloop array="#out.tabs#" index="tabRow">
            <cfif val(tabRow.wizard) GT 0>
                <cfset out.questions_total = out.questions_total + val(tabRow.questions_total) />
                <cfset out.questions_completed = out.questions_completed + val(tabRow.questions_completed) />
            </cfif>
        </cfloop>
        <cfset out.total_progress_label = formatMobileTabProgressLabel(out.questions_completed, out.questions_total) />
        <cfif out.sections_total GT 0>
            <cfset out.progress_percent = int((out.sections_completed / out.sections_total) * 100) />
        <cfelseif out.questions_total GT 0>
            <cfset out.progress_percent = int((out.questions_completed / out.questions_total) * 100) />
        </cfif>

        <cfreturn out />
    </cffunction>

    <cffunction name="getWhisperUploadRequestDiagnostics" access="private" returntype="struct" output="false">
        <cfset var diag = {
            contentType = structKeyExists(cgi, "content_type") ? cgi.content_type : "",
            contentLength = structKeyExists(cgi, "content_length") ? cgi.content_length : "",
            formFieldNames = "",
            isMultipart = false
        } />
        <cfif structKeyExists(form, "fieldnames")>
            <cfset diag.formFieldNames = form.fieldnames />
        <cfelseif isStruct(form)>
            <cfset diag.formFieldNames = structKeyList(form) />
        </cfif>
        <cfset diag.isMultipart = findNoCase("multipart/form-data", diag.contentType) GT 0 />
        <cfreturn diag />
    </cffunction>

    <cffunction name="detectWhisperUploadFileField" access="private" returntype="string" output="false">
        <cfset var candidates = "file,wf_file,ufFile,audio,recording,upload,attachment" />
        <cfset var name = "" />
        <cfloop list="#candidates#" index="name">
            <cfif structKeyExists(form, name)>
                <cfreturn name />
            </cfif>
        </cfloop>
        <cfreturn "" />
    </cffunction>

    <cffunction name="resolveMobileFormFieldName" access="private" returntype="string" output="false">
        <cfargument name="fieldHint" type="string" required="true" />
        <cfargument name="formScope" type="struct" required="false" default="#form#" />
        <cfset var key = "" />
        <cfset var hint = trim(arguments.fieldHint) />
        <cfif NOT len(hint) OR NOT isStruct(arguments.formScope)>
            <cfreturn hint />
        </cfif>
        <cfif structKeyExists(arguments.formScope, hint)>
            <cfreturn hint />
        </cfif>
        <cfloop collection="#arguments.formScope#" item="key">
            <cfif compareNoCase(key, hint) EQ 0>
                <cfreturn key />
            </cfif>
        </cfloop>
        <cfreturn hint />
    </cffunction>

    <cffunction name="isMobileMultipartEmptyFileError" access="private" returntype="boolean" output="false">
        <cfargument name="message" type="string" required="false" default="" />
        <cfreturn findNoCase("did not contain a file", arguments.message) GT 0
            OR findNoCase("empty upload", arguments.message) GT 0
            OR findNoCase("no file was uploaded", arguments.message) GT 0 />
    </cffunction>

    <cffunction name="listMobileMultipartFormFieldNames" access="private" returntype="string" output="false">
        <cfset var names = "" />
        <cfif isStruct(form) AND structKeyExists(form, "fieldnames") AND len(trim(form.fieldnames))>
            <cfreturn trim(form.fieldnames) />
        </cfif>
        <cfif isStruct(form)>
            <cfreturn structKeyList(form) />
        </cfif>
        <cfreturn "" />
    </cffunction>

    <!--- Multipart cffile (form.field) or base64 JSON fallback for mobile remote CFC uploads --->
    <cffunction name="receiveWhisperFileToTemp" access="private" returntype="struct" output="false">
        <cfargument name="tempPath" type="string" required="true" />
        <cfargument name="fileBase64" type="string" required="false" default="" />
        <cfargument name="fileName" type="string" required="false" default="" />
        <cfargument name="fileFieldNameOverride" type="string" required="false" default="" hint="Explicit multipart field name (e.g. wound_0_photo_0)" />
        <cfset var result = {
            ok = false,
            message = "",
            serverFile = "",
            clientFile = "",
            contentType = "application/octet-stream",
            contentSubType = "octet-stream",
            uploadMethod = ""
        } />
        <cfset var fileFieldName = len(trim(arguments.fileFieldNameOverride)) ? trim(arguments.fileFieldNameOverride) : detectWhisperUploadFileField() />
        <cfset var resolvedFieldName = resolveMobileFormFieldName(fileFieldName) />
        <cfset var tryField = "" />
        <cfset var tryList = "" />
        <cfset var trySeen = "" />
        <cfset var b64 = trim(arguments.fileBase64) />
        <cfset var origName = trim(arguments.fileName) />
        <cfset var bin = "" />
        <cfset var safeName = "" />
        <cfset var lastCatch = {} />

        <cfif len(b64) AND lCase(b64) NEQ "null">
            <cftry>
                <cfset bin = binaryDecode(b64, "base64") />
                <cfset safeName = len(origName) ? origName : "mobile_upload.bin" />
                <cfset safeName = reReplace(safeName, "[^A-Za-z0-9._-]", "_", "all") />
                <cfset safeName = dateFormat(now(), "YYMMDDHHMMSS") & "_" & createUUID() & "_" & safeName />
                <cfif NOT directoryExists(arguments.tempPath)>
                    <cfdirectory action="create" directory="#arguments.tempPath#" />
                </cfif>
                <cfscript>
                    fileWrite(arguments.tempPath & safeName, bin);
                </cfscript>
                <cfset result.serverFile = safeName />
                <cfset result.clientFile = len(origName) ? reReplace(origName, "[^A-Za-z0-9._-]", "_", "all") : safeName />
                <cfset result.contentType = "image" />
                <cfset result.contentSubType = "jpeg" />
                <cfif reFindNoCase("\.png$", result.clientFile)><cfset result.contentSubType = "png" /></cfif>
                <cfif reFindNoCase("\.gif$", result.clientFile)><cfset result.contentSubType = "gif" /></cfif>
                <cfif reFindNoCase("\.webp$", result.clientFile)><cfset result.contentSubType = "webp" /></cfif>
                <cfset result.uploadMethod = "base64" />
                <cfset result.ok = true />
                <cfreturn result />
                <cfcatch type="any">
                    <cfset result.message = "Invalid fileBase64: " & cfcatch.message />
                    <cfreturn result />
                </cfcatch>
            </cftry>
        </cfif>

        <cfif NOT len(resolvedFieldName)>
            <cfset result.message = "No file in request. Use multipart/form-data with field 'file' (File type in Postman), or send fileBase64 + fileName in JSON." />
            <cfreturn result />
        </cfif>

        <!--- ufFile may contain multiple files (SOC_new.cfm uploadall pattern) --->
        <cfif compareNoCase(resolvedFieldName, "ufFile") EQ 0 OR compareNoCase(fileFieldName, "ufFile") EQ 0>
            <cftry>
                <cfparam name="File.UPLOADALLRESULTS" default="#[]#" />
                <cffile action="uploadall"
                        filefield="form.#resolvedFieldName#"
                        destination="#arguments.tempPath#"
                        nameconflict="makeunique"
                        mode="666" />
                <cfif isDefined("File.UPLOADALLRESULTS") AND isArray(File.UPLOADALLRESULTS) AND arrayLen(File.UPLOADALLRESULTS) GT 0>
                    <cfset result.serverFile = File.UPLOADALLRESULTS[1].serverFile />
                    <cfset result.clientFile = File.UPLOADALLRESULTS[1].clientFile />
                    <cfif structKeyExists(File.UPLOADALLRESULTS[1], "contentType") AND len(trim(File.UPLOADALLRESULTS[1].contentType))>
                        <cfset result.contentType = File.UPLOADALLRESULTS[1].contentType />
                    </cfif>
                    <cfif structKeyExists(File.UPLOADALLRESULTS[1], "contentSubType") AND len(trim(File.UPLOADALLRESULTS[1].contentSubType))>
                        <cfset result.contentSubType = File.UPLOADALLRESULTS[1].contentSubType />
                    </cfif>
                    <cfset result.uploadMethod = "multipart:uploadall:" & resolvedFieldName />
                    <cfset result.ok = true />
                    <cfreturn result />
                </cfif>
                <cfcatch type="any">
                    <cfset lastCatch = duplicate(cfcatch) />
                </cfcatch>
            </cftry>
        </cfif>

        <cfset tryList = resolvedFieldName & ",form." & resolvedFieldName />
        <cfif compareNoCase(resolvedFieldName, fileFieldName) NEQ 0 AND len(fileFieldName)>
            <cfset tryList = tryList & "," & fileFieldName & ",form." & fileFieldName />
        </cfif>
        <cfloop list="#tryList#" index="tryField">
            <cfif listFindNoCase(trySeen, tryField)>
                <cfcontinue />
            </cfif>
            <cfset trySeen = listAppend(trySeen, tryField) />
            <cftry>
                <cfparam name="cffile.serverFile" default="" />
                <cffile action="upload"
                        filefield="#tryField#"
                        destination="#arguments.tempPath#"
                        nameconflict="makeunique"
                        mode="666" />
                <cfset result.serverFile = cffile.serverFile />
                <cfset result.clientFile = cffile.clientFile />
                <cfif structKeyExists(cffile, "contentType") AND len(trim(cffile.contentType))>
                    <cfset result.contentType = cffile.contentType />
                </cfif>
                <cfif structKeyExists(cffile, "contentSubType") AND len(trim(cffile.contentSubType))>
                    <cfset result.contentSubType = cffile.contentSubType />
                </cfif>
                <cfset result.uploadMethod = "multipart:" & tryField />
                <cfset result.ok = true />
                <cfreturn result />
                <cfcatch type="any">
                    <cfset lastCatch = duplicate(cfcatch) />
                </cfcatch>
            </cftry>
        </cfloop>

        <cfset result.message = "cffile upload failed for field " & resolvedFieldName & "." />
        <cfif structKeyExists(lastCatch, "message")>
            <cfset result.message &= " " & lastCatch.message />
        </cfif>
        <cfif isMobileMultipartEmptyFileError(result.message)>
            <cfset result.message = "empty_file_field" />
        </cfif>
        <cfif NOT findNoCase("multipart/form-data", cgi.content_type)>
            <cfset result.message &= " Request Content-Type was '" & cgi.content_type & "' (expected multipart/form-data), or send fileBase64 + fileName." />
        </cfif>
        <cfreturn result />
    </cffunction>

    <cffunction name="parseWhisperUploadJsonExtras" access="private" returntype="struct" output="false">
        <cfargument name="fileBase64Arg" type="string" required="false" default="" />
        <cfargument name="fileNameArg" type="string" required="false" default="" />
        <cfset var out = { fileBase64 = trim(arguments.fileBase64Arg), fileName = trim(arguments.fileNameArg) } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfif len(out.fileBase64)>
            <cfreturn out />
        </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 structKeyExists(bodyJson, "fileBase64")>
                    <cfset out.fileBase64 = trim("" & bodyJson.fileBase64) />
                <cfelseif structKeyExists(bodyJson, "FILEBASE64")>
                    <cfset out.fileBase64 = trim("" & bodyJson.FILEBASE64) />
                </cfif>
                <cfif structKeyExists(bodyJson, "fileName")>
                    <cfset out.fileName = trim("" & bodyJson.fileName) />
                <cfelseif structKeyExists(bodyJson, "FILENAME")>
                    <cfset out.fileName = trim("" & bodyJson.FILENAME) />
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>
        <cfreturn out />
    </cffunction>

    <!---
      Upload audio/video/JSON file to Whisper S3 (whisper_fileupload bucket/path).
      POST multipart: securityKey, Schedule_ID, assessment_reason; file field = file (or fileBase64+fileName in JSON).
    --->
    <cffunction name="uploadWhisperFile" access="remote" returntype="struct" returnformat="json" output="false" hint="Upload file to Whisper S3; requires securityKey, Schedule_ID, assessment_reason; multipart file or fileBase64.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Schedule_ID" type="string" required="true" />
        <cfargument name="assessment_reason" type="string" required="false" default="" hint="OASIS type code (e.g. 1 - SOC - SN); stored with upload metadata" />
        <cfargument name="title" type="string" required="false" default="" />
        <cfargument name="notes" type="string" required="false" default="" />
        <cfargument name="fileBase64" type="string" required="false" default="" hint="Optional base64 file body when multipart upload is unavailable" />
        <cfargument name="fileName" type="string" required="false" default="" hint="Original filename when using fileBase64" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var auth = {} />
        <cfset var scheduleId = 0 />
        <cfset var scheduleCtx = {} />
        <cfset var empId = 0 />
        <cfset var s3cfg = {} />
        <cfset var tempPath = "/var/www/datafiles/patientfileupload/" />
        <cfset var fileFieldName = "" />
        <cfset var rawTypeCode = "" />
        <cfset var wizardReason = "" />
        <cfset var wfServerFile = "" />
        <cfset var wfClientFile = "" />
        <cfset var wfExt = "" />
        <cfset var wfDupFilename = "" />
        <cfset var wfFiletype = "" />
        <cfset var wfTitle = "" />
        <cfset var wfNotes = "" />
        <cfset var s3Dest = "" />
        <cfset var insertId = 0 />
        <cfset var uploadStep = "" />
        <cfset var emailCtx = {} />
        <cfset var uploadExtras = {} />
        <cfset var reqDiag = {} />
        <cfset var receiveResult = {} />

        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>

        <cfset scheduleId = val(trim(arguments.Schedule_ID)) />
        <cfif scheduleId LTE 0>
            <cfset response.message = "Schedule_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

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

        <cfset empId = parseSecurityKeyEmpId(trim(arguments.securityKey)) />
        <cfset Request.prefix_db_agency = auth.agencySchema />
        <cfset Request.Agency_ID = auth.locId />

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

        <cfset scheduleCtx.schedule.Resolved_Assmt_ID = resolveScheduleAssmtIdByVisitType(
            agencySchema = auth.agencySchema,
            patientId = val(scheduleCtx.schedule.Patient_ID),
            scheduleAssmtId = val(scheduleCtx.schedule.Assmt_ID),
            visitType = trim(scheduleCtx.schedule.Visit_Type),
            visitDate = structKeyExists(scheduleCtx.schedule, "Visit_Date") ? scheduleCtx.schedule.Visit_Date : ""
        ) />
        <cfset scheduleCtx.schedule.Assmt_ID = scheduleCtx.schedule.Resolved_Assmt_ID />

        <cfset rawTypeCode = trim(scheduleCtx.schedule.Visit_Type) />
        <cfif NOT len(rawTypeCode) OR lCase(rawTypeCode) EQ "null">
            <cfset rawTypeCode = trim(arguments.assessment_reason) />
        </cfif>
        <cfif NOT len(rawTypeCode) OR lCase(rawTypeCode) EQ "null">
            <cfset rawTypeCode = "1 - SOC - SN" />
        </cfif>
        <cfset wizardReason = mapOasisTypeCodeToWizardAssessmentReason(rawTypeCode) />

        <cfset fileFieldName = detectWhisperUploadFileField() />
        <cfset uploadExtras = parseWhisperUploadJsonExtras(arguments.fileBase64, arguments.fileName) />
        <cfset reqDiag = getWhisperUploadRequestDiagnostics() />

        <cfset emailCtx = {
            scheduleId = scheduleId,
            empId = empId,
            locId = auth.locId,
            agencySchema = auth.agencySchema,
            patientId = scheduleCtx.schedule.Patient_ID,
            assmtId = scheduleCtx.schedule.Assmt_ID,
            scheduleAssmtId = scheduleCtx.schedule.Schedule_Assmt_ID,
            visitType = scheduleCtx.schedule.Visit_Type,
            visitDate = structKeyExists(scheduleCtx.schedule, "Visit_Date") ? scheduleCtx.schedule.Visit_Date : "",
            assessmentReason = rawTypeCode,
            fileFieldName = fileFieldName,
            tempPath = tempPath,
            contentType = reqDiag.contentType,
            formFieldNames = reqDiag.formFieldNames,
            isMultipart = reqDiag.isMultipart,
            hasFileBase64 = len(uploadExtras.fileBase64) GT 0
        } />

        <cftry>
            <cfset uploadStep = "ensure_whisper_files_table" />
            <cfset ensureWhisperFilesTable(auth.agencySchema) />

            <cfset uploadStep = "load_s3_config" />
            <cfset s3cfg = getWhisperS3Config(auth.locId) />
            <cfset emailCtx.s3Bucket = s3cfg.bucket />
            <cfset emailCtx.s3Subfolder = s3cfg.subfolder />

            <cfset uploadStep = "create_temp_upload_directory" />
            <cfif NOT directoryExists(tempPath)>
                <cfdirectory action="create" directory="#tempPath#" />
            </cfif>

            <cfset uploadStep = "receive_file_to_temp" />
            <cfset receiveResult = receiveWhisperFileToTemp(
                tempPath = tempPath,
                fileBase64 = uploadExtras.fileBase64,
                fileName = uploadExtras.fileName
            ) />
            <cfif NOT receiveResult.ok>
                <cfset sendMobileWhisperUploadErrorEmail(
                    uploadStep = uploadStep,
                    context = emailCtx,
                    errorCatch = { type = "receive_file", message = receiveResult.message, detail = "" }
                ) />
                <cfset response.message = receiveResult.message />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>
            <cfset emailCtx.uploadMethod = receiveResult.uploadMethod />

            <cfset wfServerFile = receiveResult.serverFile />
            <cfset wfClientFile = receiveResult.clientFile />
            <cfset wfExt = lCase(listLast(wfServerFile, ".")) />
            <cfset emailCtx.clientFile = wfClientFile />
            <cfset emailCtx.serverFile = wfServerFile />
            <cfset emailCtx.fileExt = wfExt />
            <cfset emailCtx.tempFileExists = fileExists(tempPath & wfServerFile) />

            <cfif NOT listFindNoCase(s3cfg.allowedExts, wfExt)>
                <cfif fileExists("#tempPath##wfServerFile#")>
                    <cffile action="delete" file="#tempPath##wfServerFile#" />
                </cfif>
                <cfset response.message = "Invalid file type (.#wfExt#). Allowed: audio, video, or json transcript." />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>

            <cfset uploadStep = "build_s3_destination_path" />
            <cfset wfDupFilename = dateFormat(now(), "YYMMDDHHMMSS") & wfClientFile />
            <cfset wfFiletype = receiveResult.contentType & "/" & receiveResult.contentSubType />
            <cfset s3Dest = buildWhisperS3ObjectUrl(s3cfg, wfDupFilename) />
            <cfset emailCtx.s3DestMasked = maskS3PathForEmail(s3Dest) />

            <cfset uploadStep = "filewrite_temp_to_s3" />
            <cfset putWhisperFileOnS3(
                localSourcePath = tempPath & wfServerFile,
                s3cfg = s3cfg,
                dupFilename = wfDupFilename
            ) />
            <cfif fileExists(tempPath & wfServerFile)>
                <cffile action="delete" file="#tempPath##wfServerFile#" />
            </cfif>

            <cfset uploadStep = "insert_whisper_files_database" />
            <cfset wfTitle = trim(arguments.title) />
            <cfif NOT len(wfTitle)>
                <cfset wfTitle = rawTypeCode />
            </cfif>

            <cfset wfNotes = trim(arguments.notes) />
            <cfif NOT len(wfNotes)>
                <cfset wfNotes = "assessment_reason=" & rawTypeCode & " | wizard_reason=" & wizardReason />
            <cfelse>
                <cfset wfNotes = wfNotes & " | assessment_reason=" & rawTypeCode & " | wizard_reason=" & wizardReason />
            </cfif>

            <cfquery name="qInsertWhisperFile" datasource="#Application.DataSrc#" result="qInsertWhisperFileResult">
                INSERT INTO #auth.agencySchema#.whisper_files
                  (Status, Review_Status, Upload_Source, Agency_Id, Patient_Id, Assmt_ID, DOC_ID,
                   Title, FileName, Dup_file_name, FileType, File,
                   Notes, faxsend_to, faxsent_time, Created, CreatedBy)
                VALUES
                  (0,
                   1,
                   <cfqueryparam value="mobile" cfsqltype="cf_sql_varchar">,
                   <cfqueryparam value="#auth.locId#" cfsqltype="cf_sql_varchar">,
                   <cfqueryparam value="#scheduleCtx.schedule.Patient_ID#" cfsqltype="cf_sql_varchar">,
                   <cfqueryparam value="#scheduleCtx.schedule.Assmt_ID#" cfsqltype="cf_sql_varchar" null="#(NOT len(trim(scheduleCtx.schedule.Assmt_ID)) OR val(scheduleCtx.schedule.Assmt_ID) EQ 0)#">,
                   <cfqueryparam value="#scheduleId#" cfsqltype="cf_sql_varchar">,
                   <cfqueryparam value="#wfTitle#" cfsqltype="cf_sql_varchar">,
                   <cfqueryparam value="#wfClientFile#" cfsqltype="cf_sql_varchar">,
                   <cfqueryparam value="#wfDupFilename#" cfsqltype="cf_sql_varchar">,
                   <cfqueryparam value="#wfFiletype#" cfsqltype="cf_sql_varchar">,
                   NULL,
                   <cfqueryparam value="#wfNotes#" cfsqltype="cf_sql_longvarchar" null="#(NOT len(trim(wfNotes)))#">,
                   NULL,
                   NULL,
                   <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                   <cfqueryparam value="#empId#" cfsqltype="cf_sql_varchar">)
            </cfquery>

            <cfset insertId = 0 />
            <cfif structKeyExists(qInsertWhisperFileResult, "GENERATED_KEY")>
                <cfset insertId = val(qInsertWhisperFileResult.GENERATED_KEY) />
            <cfelseif structKeyExists(qInsertWhisperFileResult, "generatedKey")>
                <cfset insertId = val(qInsertWhisperFileResult.generatedKey) />
            </cfif>

            <cfset response.success = true />
            <cfset response.message = "File uploaded successfully" />
            <cfset response.data = {
                whisper_file_id = insertId,
                Schedule_ID = scheduleId,
                Patient_ID = scheduleCtx.schedule.Patient_ID,
                Assmt_ID = scheduleCtx.schedule.Assmt_ID,
                Schedule_Assmt_ID = scheduleCtx.schedule.Schedule_Assmt_ID,
                Visit_Type = scheduleCtx.schedule.Visit_Type,
                assessment_reason = rawTypeCode,
                wizard_assessment_reason = wizardReason
            } />
            <!---
                wizard_assessment_reason = wizardReason,
                fileName = wfClientFile,
                dup_file_name = wfDupFilename,
                fileType = wfFiletype,
                s3_bucket = s3cfg.bucket,
                s3_key = s3cfg.subfolder & "/" & wfDupFilename
                --->
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />

            <cfcatch type="any">
                <cfif len(wfServerFile)>
                    <cfset emailCtx.tempFileExists = fileExists(tempPath & wfServerFile) />
                </cfif>
                <cfif NOT len(uploadStep)>
                    <cfset uploadStep = "unknown" />
                </cfif>
                <cfif isDefined("wfServerFile") AND len(wfServerFile) AND fileExists("#tempPath##wfServerFile#")>
                    <cftry><cffile action="delete" file="#tempPath##wfServerFile#"><cfcatch type="any"></cfcatch></cftry>
                </cfif>
                <cfset sendMobileWhisperUploadErrorEmail(
                    uploadStep = uploadStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Upload failed at step [" & uploadStep & "]: " & cfcatch.message />
                <cfset response.data = { failed_step = uploadStep } />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Validate securityKey + assessment access for employee (optional schedule / patient checks) --->
    <cffunction name="authorizeMobileAssmtRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="scheduleId" type="numeric" required="false" default="0" />
        <cfargument name="patientId" type="numeric" required="false" default="0" />
        <cfset var result = { ok = false, statusCode = 401, message = "Unauthorized", agencySchema = "", locId = 0, empId = 0, assessment = {}, scheduleQuery = [], schedulesForAssmt = [], schedulesForPatient = [], assignmentMatchMode = "", errorDetail = {} } />
        <cfset var auth = {} />
        <cfset var assmtQuery = "" />
        <cfset var scheduleQuery = "" />
        <cfset var schedulesForAssmtQuery = "" />
        <cfset var schedulesForPatientQuery = "" />

        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />
        <cfif NOT auth.ok>
            <cfset result.statusCode = auth.statusCode />
            <cfset result.message = auth.message />
            <cfset result.errorDetail = {
                failed_at = "authorizeMobileAssmtRequest",
                failed_step = "securityKey_agency_auth",
                explanation = "securityKey failed authorizeMobileSecurityKeyForAgency before schedule assignment was checked.",
                security_key_status = "INVALID or employee/agency not resolved",
                auth_message = auth.message
            } />
            <cfreturn result />
        </cfif>

        <cfset result.empId = toJsonInt(parseSecurityKeyEmpId(trim(arguments.securityKey))) />
        <cfset result.agencySchema = auth.agencySchema />
        <cfset result.locId = toJsonInt(auth.locId) />

        <cfif arguments.assmtId LTE 0>
            <cfset result.statusCode = 400 />
            <cfset result.message = "Assmt_ID is required" />
            <cfset result.errorDetail = {
                failed_at = "authorizeMobileAssmtRequest",
                failed_step = "assmtId_required",
                explanation = "Assmt_ID was missing or zero after securityKey validated.",
                security_key_status = "VALID",
                empId_from_securityKey = result.empId,
                agencySchema = result.agencySchema,
                assmtId = toJsonInt(arguments.assmtId)
            } />
            <cfreturn result />
        </cfif>

        <cfquery name="assmtQuery" datasource="#Application.DataSrc#">
            SELECT pa.Assmt_ID,
                   pa.Patient_ID,
                   pa.Admit_ID,
                   pa.F26,
                   pa.F27,
                   pa.F176,
                   pa.F177,
                   pa.F178,
                   pa.F25
            FROM #auth.agencySchema#.pAssessments pa
            WHERE pa.Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND pa.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            LIMIT 1
        </cfquery>

        <cfif assmtQuery.recordcount EQ 0>
            <cfset result.statusCode = 404 />
            <cfset result.message = "Assessment not found or inactive" />
            <cfset result.errorDetail = {
                failed_at = "authorizeMobileAssmtRequest",
                failed_step = "assessment_lookup",
                explanation = "No active pAssessments row (Status=0) for this Assmt_ID in agency schema. securityKey was valid.",
                security_key_status = "VALID",
                empId_from_securityKey = result.empId,
                agencySchema = result.agencySchema,
                lookup_table = result.agencySchema & ".pAssessments",
                lookup_filters = { Assmt_ID = toJsonInt(arguments.assmtId), Status = toJsonInt(0) },
                matching_assessment_count = toJsonInt(0)
            } />
            <cfreturn result />
        </cfif>

        <cfif arguments.patientId GT 0 AND val(assmtQuery.Patient_ID) NEQ val(arguments.patientId)>
            <cfset result.statusCode = 403 />
            <cfset result.message = "Patient_ID does not match assessment" />
            <cfset result.errorDetail = {
                failed_at = "authorizeMobileAssmtRequest",
                failed_step = "patient_match",
                explanation = "Request Patient_ID does not match Patient_ID on the assessment row.",
                security_key_status = "VALID",
                empId_from_securityKey = result.empId,
                agencySchema = result.agencySchema,
                assmtId = toJsonInt(arguments.assmtId),
                request_Patient_ID = toJsonInt(arguments.patientId),
                assessment_Patient_ID = toJsonInt(assmtQuery.Patient_ID)
            } />
            <cfreturn result />
        </cfif>

        <cfquery name="scheduleQuery" datasource="#Application.DataSrc#">
            SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Skill, Visit_Date, Status
            FROM #auth.agencySchema#.pSchedules
            WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
              AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.empId#">
            LIMIT 1
        </cfquery>
        <cfset result.scheduleQuery = scheduleQueryToTraceArray(scheduleQuery) />
        <cfset result.assignmentMatchMode = "assmt_id" />

        <!--- #10459: pSchedules.Assmt_ID often points at SOC, not the current assessment.
             Fall back to Patient_ID + Emp_ID assignment (same as authorizeMobilePatientRequest). --->
        <cfif scheduleQuery.recordcount EQ 0 AND val(assmtQuery.Patient_ID) GT 0>
            <cfquery name="scheduleQuery" datasource="#Application.DataSrc#">
                SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Skill, Visit_Date, Status
                FROM #auth.agencySchema#.pSchedules
                WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtQuery.Patient_ID#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.empId#">
                ORDER BY Visit_Date DESC, Schedule_ID DESC
                LIMIT 1
            </cfquery>
            <cfset result.scheduleQuery = scheduleQueryToTraceArray(scheduleQuery) />
            <cfset result.assignmentMatchMode = "patient_id_fallback" />
        </cfif>

        <cfif scheduleQuery.recordcount EQ 0>
            <!--- Diagnostics: schedules by Assmt_ID and by Patient_ID --->
            <cfquery name="schedulesForAssmtQuery" datasource="#Application.DataSrc#">
                SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Skill, Visit_Date, Status
                FROM #auth.agencySchema#.pSchedules
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                ORDER BY Status ASC, Schedule_ID DESC
                LIMIT 25
            </cfquery>
            <cfset result.schedulesForAssmt = scheduleQueryToTraceArray(schedulesForAssmtQuery) />
            <cfquery name="schedulesForPatientQuery" datasource="#Application.DataSrc#">
                SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Skill, Visit_Date, Status
                FROM #auth.agencySchema#.pSchedules
                WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtQuery.Patient_ID#">
                  AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.empId#">
                ORDER BY Status ASC, Visit_Date DESC, Schedule_ID DESC
                LIMIT 25
            </cfquery>
            <cfset result.schedulesForPatient = scheduleQueryToTraceArray(schedulesForPatientQuery) />
            <cfset result.statusCode = 403 />
            <cfset result.message = "Assessment is not assigned to this employee" />
            <cfset result.errorDetail = {
                failed_at = "authorizeMobileAssmtRequest",
                failed_step = "schedule_assignment_check",
                explanation = "securityKey is VALID, but employee has no active schedule for this Assmt_ID and also no active schedule for the assessment Patient_ID.",
                security_key_status = "VALID (authorizeMobileSecurityKeyForAgency passed)",
                why = "Checked (1) pSchedules Assmt_ID+Emp_ID+Status=0 and (2) fallback Patient_ID+Emp_ID+Status=0. pSchedules.Assmt_ID often points at SOC, so Assmt_ID-only match can be empty even when the employee is assigned to the patient.",
                how_to_fix = "Ensure Emp_ID has an active (Status=0) pSchedules row for Patient_ID on the assessment. schedulesForAssmt_count=0 is normal when schedule.Assmt_ID is SOC; check schedulesForPatient dump.",
                agencySchema = result.agencySchema,
                locId = result.locId,
                empId_from_securityKey = result.empId,
                assmtId = toJsonInt(arguments.assmtId),
                patientId_on_assessment = toJsonInt(assmtQuery.Patient_ID),
                lookup_table = result.agencySchema & ".pSchedules",
                lookup_filters_tried = [
                    { Assmt_ID = toJsonInt(arguments.assmtId), Emp_ID = result.empId, Status = toJsonInt(0) },
                    { Patient_ID = toJsonInt(assmtQuery.Patient_ID), Emp_ID = result.empId, Status = toJsonInt(0) }
                ],
                matching_schedule_count = toJsonInt(0),
                schedules_for_this_assmt_count = toJsonInt(arrayLen(result.schedulesForAssmt)),
                schedules_for_patient_emp_count = toJsonInt(arrayLen(result.schedulesForPatient))
            } />
            <cfreturn result />
        </cfif>

        <cfif arguments.scheduleId GT 0>
            <cfquery name="scheduleQuery" datasource="#Application.DataSrc#">
                SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Skill, Visit_Date, Status
                FROM #auth.agencySchema#.pSchedules
                WHERE Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.scheduleId#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.empId#">
                  AND (
                        Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                     OR Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtQuery.Patient_ID#">
                  )
                LIMIT 1
            </cfquery>
            <cfset result.scheduleQuery = scheduleQueryToTraceArray(scheduleQuery) />
            <cfif scheduleQuery.recordcount EQ 0>
                <cfquery name="schedulesForAssmtQuery" datasource="#Application.DataSrc#">
                    SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Skill, Visit_Date, Status
                    FROM #auth.agencySchema#.pSchedules
                    WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                    ORDER BY Status ASC, Schedule_ID DESC
                    LIMIT 25
                </cfquery>
                <cfset result.schedulesForAssmt = scheduleQueryToTraceArray(schedulesForAssmtQuery) />
                <cfquery name="schedulesForPatientQuery" datasource="#Application.DataSrc#">
                    SELECT Schedule_ID, Patient_ID, Assmt_ID, Emp_ID, Visit_Type, Skill, Visit_Date, Status
                    FROM #auth.agencySchema#.pSchedules
                    WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtQuery.Patient_ID#">
                      AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.empId#">
                    ORDER BY Status ASC, Visit_Date DESC, Schedule_ID DESC
                    LIMIT 25
                </cfquery>
                <cfset result.schedulesForPatient = scheduleQueryToTraceArray(schedulesForPatientQuery) />
                <cfset result.statusCode = 403 />
                <cfset result.message = "Schedule not found for this assessment and employee" />
                <cfset result.errorDetail = {
                    failed_at = "authorizeMobileAssmtRequest",
                    failed_step = "scheduleId_match_check",
                    explanation = "securityKey is VALID, but requested Schedule_ID did not match Emp_ID + Status=0 with Assmt_ID or Patient_ID for this assessment.",
                    security_key_status = "VALID",
                    why = "Schedule_ID must belong to this employee (Status=0) and either match Assmt_ID or the assessment Patient_ID (SOC schedules often have a different Assmt_ID).",
                    how_to_fix = "Pick Schedule_ID from schedulesForPatient where Status=0.",
                    agencySchema = result.agencySchema,
                    locId = result.locId,
                    empId_from_securityKey = result.empId,
                    assmtId = toJsonInt(arguments.assmtId),
                    request_Schedule_ID = toJsonInt(arguments.scheduleId),
                    patientId_on_assessment = toJsonInt(assmtQuery.Patient_ID),
                    lookup_table = result.agencySchema & ".pSchedules",
                    matching_schedule_count = toJsonInt(0),
                    schedules_for_this_assmt_count = toJsonInt(arrayLen(result.schedulesForAssmt)),
                    schedules_for_patient_emp_count = toJsonInt(arrayLen(result.schedulesForPatient))
                } />
                <cfreturn result />
            </cfif>
            <cfset result.schedule = {
                Schedule_ID = scheduleQuery.Schedule_ID,
                Patient_ID = scheduleQuery.Patient_ID,
                Assmt_ID = scheduleQuery.Assmt_ID,
                Visit_Type = scheduleQuery.Visit_Type,
                Skill = scheduleQuery.Skill,
                Visit_Date = scheduleQuery.Visit_Date
            } />
        </cfif>

        <cfset result.ok = true />
        <cfset result.message = "" />
        <cfset result.assessment = {
            Assmt_ID = assmtQuery.Assmt_ID,
            Patient_ID = assmtQuery.Patient_ID,
            Admit_ID = assmtQuery.Admit_ID,
            F26 = assmtQuery.F26,
            F27 = assmtQuery.F27,
            F176 = assmtQuery.F176,
            F177 = assmtQuery.F177,
            F178 = assmtQuery.F178,
            F25 = assmtQuery.F25
        } />
        <cfreturn result />
    </cffunction>

    <!--- Split Reading1 (e.g. 98.6) and Notes (descriptor + free text) for mobile clients --->
    <cffunction name="parseVitalReadingAndNotes" access="private" returntype="struct" output="false">
        <cfargument name="reading1" type="string" required="false" default="" />
        <cfargument name="notes" type="string" required="false" default="" />
        <cfset var out = {
            reading = trim(arguments.reading1),
            reading_primary = "",
            reading_secondary = "",
            descriptor = "",
            notes = ""
        } />
        <cfset var notesFull = trim(arguments.notes) />
        <cfset var parts = [] />
        <cfset var i = 0 />

        <cfif len(out.reading) AND find(".", out.reading)>
            <cfset parts = listToArray(out.reading, ".") />
            <cfset out.reading_primary = trim(parts[1]) />
            <cfif arrayLen(parts) GT 1>
                <cfset out.reading_secondary = trim(parts[2]) />
            </cfif>
        <cfelse>
            <cfset out.reading_primary = out.reading />
        </cfif>

        <cfif find(chr(10), notesFull) OR find(chr(13), notesFull)>
            <cfset notesFull = replace(notesFull, chr(13), chr(10), "all") />
            <cfset parts = listToArray(notesFull, chr(10)) />
            <cfset out.descriptor = arrayLen(parts) GT 0 ? trim(parts[1]) : "" />
            <cfif arrayLen(parts) GT 1>
                <cfloop from="2" to="#arrayLen(parts)#" index="i">
                    <cfset out.notes = len(out.notes) ? out.notes & chr(10) & trim(parts[i]) : trim(parts[i]) />
                </cfloop>
            </cfif>
        <cfelse>
            <cfset out.descriptor = notesFull />
        </cfif>

        <cfreturn out />
    </cffunction>

    <!--- Vital sign definitions + allowed readings/descriptors (pValue_Lookup) --->
    <cffunction name="getVitalSignCatalog" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey. Returns vital types, units, required flag, and lookup options for readings/descriptors.">
        <cfargument name="securityKey" type="string" required="true" />

        <cfset var response = { success = false, message = "", data = { vital_types = [] } } />
        <cfset var auth = {} />
        <cfset var lookupQuery = "" />
        <cfset var byItem = {} />
        <cfset var itemId = "" />
        <cfset var row = {} />
        <cfset var opt = {} />
        <cfset var types = [] />

        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>

        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />
        <cfif NOT auth.ok>
            <cfset response.message = auth.message />
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <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>

        <cftry>
            <cfquery name="lookupQuery" datasource="#Application.DataSrc#">
                SELECT Item_ID,
                       Description,
                       Required,
                       Lookup_1,
                       Lookup_2,
                       Measure,
                       Descriptor
                FROM #Request.prefix_db_lookup#.pValue_Lookup
                WHERE Description <> <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                ORDER BY Item_ID, Lookup_1, ID
            </cfquery>

            <cfloop query="lookupQuery">
                <cfset itemId = "" & lookupQuery.Item_ID />
                <cfif NOT structKeyExists(byItem, itemId)>
                    <cfset byItem[itemId] = {
                        item_id = val(lookupQuery.Item_ID),
                        description = trim(lookupQuery.Description),
                        required = (trim(lookupQuery.Required) EQ "*"),
                        measure = "",
                        reading_options = [],
                        reading_secondary_options = [],
                        descriptor_options = [],
                        uses_secondary_reading = false
                    } />
                </cfif>
                <cfset row = byItem[itemId] />
                <cfif NOT len(row.measure) AND len(trim(lookupQuery.Measure))>
                    <cfset row.measure = trim(lookupQuery.Measure) />
                    <cfset row.uses_secondary_reading = listFindNoCase("F,mg/dL", row.measure) GT 0 />
                </cfif>
                <cfif len(trim(lookupQuery.Lookup_1))>
                    <cfset opt = trim(lookupQuery.Lookup_1) />
                    <cfif arrayFind(row.reading_options, opt) EQ 0>
                        <cfset arrayAppend(row.reading_options, opt) />
                    </cfif>
                </cfif>
                <cfif len(trim(lookupQuery.Lookup_2)) AND row.uses_secondary_reading>
                    <cfset opt = trim(lookupQuery.Lookup_2) />
                    <cfif arrayFind(row.reading_secondary_options, opt) EQ 0>
                        <cfset arrayAppend(row.reading_secondary_options, opt) />
                    </cfif>
                </cfif>
                <cfif len(trim(lookupQuery.Descriptor))>
                    <cfset opt = trim(lookupQuery.Descriptor) />
                    <cfif arrayFind(row.descriptor_options, opt) EQ 0>
                        <cfset arrayAppend(row.descriptor_options, opt) />
                    </cfif>
                </cfif>
                <cfset byItem[itemId] = row />
            </cfloop>

            <cfloop collection="#byItem#" item="itemId">
                <cfset arrayAppend(types, byItem[itemId]) />
            </cfloop>

            <cfset response.success = true />
            <cfset response.message = "Vital sign catalog loaded" />
            <cfset response.data = { vital_types = types, vital_type_count = arrayLen(types) } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "getVitalSignCatalog",
                    failedStep = "load_pValue_Lookup",
                    context = { agencySchema = structKeyExists(auth, "agencySchema") ? auth.agencySchema : "" },
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load vital catalog: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Saved vitals for an assessment (+ optional visit schedule progress rows) --->
    <cffunction name="getAssessmentVitals" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Assmt_ID. Optional Schedule_ID, Patient_ID. Returns assessment vitals from pWoundsVitals.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Assmt_ID" type="string" required="true" />
        <cfargument name="Schedule_ID" type="string" required="false" default="" />
        <cfargument name="Patient_ID" type="string" required="false" default="" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var assmtId = val(trim(arguments.Assmt_ID)) />
        <cfset var scheduleId = val(trim(arguments.Schedule_ID)) />
        <cfset var patientId = val(trim(arguments.Patient_ID)) />
        <cfset var ctx = {} />
        <cfset var vitalsQuery = "" />
        <cfset var progressQuery = "" />
        <cfset var vitals = [] />
        <cfset var scheduleVitals = [] />
        <cfset var row = {} />
        <cfset var parsed = {} />
        <cfset var requiredTypes = 0 />
        <cfset var enteredRequired = 0 />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />

        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>

        <cfif assmtId LTE 0>
            <cfset response.message = "Assmt_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

        <cfset ctx = authorizeMobileAssmtRequest(
            securityKey = trim(arguments.securityKey),
            assmtId = assmtId,
            scheduleId = scheduleId,
            patientId = patientId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <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 emailCtx = {
            assmtId = assmtId,
            scheduleId = scheduleId,
            patientId = patientId,
            empId = ctx.empId,
            locId = ctx.locId,
            agencySchema = ctx.agencySchema
        } />

        <cftry>
            <cfset currentStep = "query_pWoundsVitals" />
            <cfquery name="vitalsQuery" datasource="#Application.DataSrc#">
                SELECT wv.ID,
                       wv.Assmt_ID,
                       wv.Item_ID,
                       wv.Pathway_ID,
                       wv.Description,
                       wv.Reading1,
                       wv.Notes,
                       wv.Param_Lo,
                       wv.Param_Hi,
                       wv.pain_location,
                       wv.pain_duration,
                       wv.pain_exacerbate,
                       wv.pain_relief,
                       wv.pain_med_effectiveness,
                       wv.Date_Create,
                       wv.Date_Change,
                       wv.Created_by,
                       wv.Change_by,
                       (SELECT MAX(vl.Measure)
                        FROM #Request.prefix_db_lookup#.pValue_Lookup vl
                        WHERE vl.Item_ID = wv.Item_ID
                          AND vl.Description = wv.Description) AS Measure,
                       (SELECT MAX(vl.Required)
                        FROM #Request.prefix_db_lookup#.pValue_Lookup vl
                        WHERE vl.Item_ID = wv.Item_ID
                          AND vl.Description = wv.Description) AS Required
                FROM #ctx.agencySchema#.pWoundsVitals wv
                WHERE wv.Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtId#">
                  AND wv.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND wv.VitalOrWound = <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">
                ORDER BY wv.Item_ID, wv.ID
            </cfquery>

            <cfset currentStep = "build_vitals_array" />
            <cfloop query="vitalsQuery">
                <cfset parsed = parseVitalReadingAndNotes(vitalsQuery.Reading1, vitalsQuery.Notes) />
                <cfset row = {
                    id = vitalsQuery.ID,
                    assmt_id = vitalsQuery.Assmt_ID,
                    item_id = vitalsQuery.Item_ID,
                    pathway_id = vitalsQuery.Pathway_ID,
                    description = trim(vitalsQuery.Description),
                    reading = parsed.reading,
                    reading_primary = parsed.reading_primary,
                    reading_secondary = parsed.reading_secondary,
                    descriptor = parsed.descriptor,
                    notes = parsed.notes,
                    measure = trim(vitalsQuery.Measure),
                    required = (trim(vitalsQuery.Required) EQ "*"),
                    param_lo = trim(vitalsQuery.Param_Lo),
                    param_hi = trim(vitalsQuery.Param_Hi),
                    pain_location = trim(vitalsQuery.pain_location),
                    pain_duration = trim(vitalsQuery.pain_duration),
                    pain_exacerbate = trim(vitalsQuery.pain_exacerbate),
                    pain_relief = trim(vitalsQuery.pain_relief),
                    pain_med_effectiveness = trim(vitalsQuery.pain_med_effectiveness),
                    date_create = vitalsQuery.Date_Create,
                    date_change = vitalsQuery.Date_Change,
                    created_by = vitalsQuery.Created_by,
                    change_by = vitalsQuery.Change_by,
                    source = "assessment"
                } />
                <cfset arrayAppend(vitals, row) />
            </cfloop>

            <cfif scheduleId GT 0>
                <cfset currentStep = "query_pProgress_schedule_vitals" />
                <cfquery name="progressQuery" datasource="#Application.DataSrc#">
                    SELECT p.Progress_ID,
                           p.Schedule_ID,
                           p.Assmt_ID,
                           p.Careplan_ID,
                           p.Item_ID,
                           p.Description,
                           p.Value_1,
                           p.Value_2,
                           p.Value_Sum,
                           p.Notes,
                           p.pain_location,
                           p.pain_duration,
                           p.pain_exacerbate,
                           p.pain_relief,
                           p.pain_med_effectiveness,
                           p.Date_Create,
                           p.Date_Change
                    FROM #ctx.agencySchema#.pProgress p
                    WHERE p.Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtId#">
                      AND p.Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#scheduleId#">
                      AND p.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                      AND p.Type = <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">
                    ORDER BY p.Item_ID, p.Progress_ID
                </cfquery>

                <cfloop query="progressQuery">
                    <cfset parsed = parseVitalReadingAndNotes(progressQuery.Value_Sum, progressQuery.Notes) />
                    <cfset row = {
                        progress_id = progressQuery.Progress_ID,
                        schedule_id = progressQuery.Schedule_ID,
                        assmt_id = progressQuery.Assmt_ID,
                        careplan_id = progressQuery.Careplan_ID,
                        item_id = progressQuery.Item_ID,
                        description = trim(progressQuery.Description),
                        reading = parsed.reading,
                        reading_primary = len(trim(progressQuery.Value_1)) ? trim(progressQuery.Value_1) : parsed.reading_primary,
                        reading_secondary = len(trim(progressQuery.Value_2)) ? trim(progressQuery.Value_2) : parsed.reading_secondary,
                        descriptor = parsed.descriptor,
                        notes = parsed.notes,
                        pain_location = trim(progressQuery.pain_location),
                        pain_duration = trim(progressQuery.pain_duration),
                        pain_exacerbate = trim(progressQuery.pain_exacerbate),
                        pain_relief = trim(progressQuery.pain_relief),
                        pain_med_effectiveness = trim(progressQuery.pain_med_effectiveness),
                        date_create = progressQuery.Date_Create,
                        date_change = progressQuery.Date_Change,
                        source = "schedule_progress"
                    } />
                    <cfset arrayAppend(scheduleVitals, row) />
                </cfloop>
            </cfif>

            <cfset currentStep = "count_required_vital_types" />
            <cfquery name="requiredLookup" datasource="#Application.DataSrc#">
                SELECT COUNT(DISTINCT Item_ID) AS cnt
                FROM #Request.prefix_db_lookup#.pValue_Lookup
                WHERE Required = <cfqueryparam value="*" cfsqltype="cf_sql_varchar">
                  AND Description <> <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            </cfquery>
            <cfset requiredTypes = val(requiredLookup.cnt) />

            <cfset currentStep = "count_entered_required_vitals" />
            <cfquery name="qEnteredRequired" datasource="#Application.DataSrc#">
                SELECT COUNT(DISTINCT wv.Item_ID) AS cnt
                FROM #ctx.agencySchema#.pWoundsVitals wv
                INNER JOIN #Request.prefix_db_lookup#.pValue_Lookup vl
                  ON vl.Item_ID = wv.Item_ID
                 AND vl.Description = wv.Description
                 AND vl.Required = <cfqueryparam value="*" cfsqltype="cf_sql_varchar">
                WHERE wv.Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtId#">
                  AND wv.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND wv.VitalOrWound = <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">
                  AND TRIM(IFNULL(wv.Reading1, '')) <> ''
            </cfquery>
            <cfset enteredRequired = val(qEnteredRequired.cnt) />

            <cfset response.success = true />
            <cfset response.message = "Assessment vitals loaded" />
            <cfset response.data = {
                assessment = ctx.assessment,
                schedule = structKeyExists(ctx, "schedule") ? ctx.schedule : {},
                vitals = vitals,
                schedule_vitals = scheduleVitals,
                vital_count = arrayLen(vitals),
                schedule_vital_count = arrayLen(scheduleVitals),
                required_vital_types = requiredTypes,
                required_vitals_entered = enteredRequired,
                required_vitals_complete = (requiredTypes EQ 0 OR enteredRequired GTE requiredTypes)
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "getAssessmentVitals",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load vitals: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Normalize bulk schedule JSON keys (mobile UPPER_CASE → pSchedules columns) --->
    <cffunction name="normalizeBulkScheduleItemKeys" access="private" returntype="struct" output="false">
        <cfargument name="rawItem" type="struct" required="true" />
        <cfset var out = {} />
        <cfset var keyMap = {
            "SCHEDULE_ID": "Schedule_ID",
            "STARTTIME": "StartTime",
            "ENDTIME": "EndTime",
            "GPS_START_LONGITUDE": "GPS_start_longitude",
            "GPS_START_LATITUDE": "GPS_start_latitude",
            "GPS_START_TIME": "GPS_start_time",
            "GPS_START_DATE": "GPS_start_date",
            "GPS_END_LONGITUDE": "GPS_end_longitude",
            "GPS_END_LATITUDE": "GPS_end_latitude",
            "GPS_END_TIME": "GPS_end_time",
            "GPS_END_DATE": "GPS_end_date",
            "PATIENT_SIGN": "patient_sign",
            "EVV_START": "EVV_start",
            "EVV_END": "EVV_end",
            "HOMEBOUND": "homebound",
            "NOTES": "Notes"
        } />
        <cfset var k = "" />
        <cfset var nk = "" />
        <cfloop collection="#arguments.rawItem#" item="k">
            <cfset nk = uCase(reReplace(trim(k), "[^A-Za-z0-9_]", "", "all")) />
            <cfif structKeyExists(keyMap, nk)>
                <cfset out[keyMap[nk]] = arguments.rawItem[k] />
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <cffunction name="mobileScheduleFieldProvided" access="private" returntype="boolean" output="false">
        <cfargument name="value" required="true" />
        <cfif isNull(arguments.value)>
            <cfreturn false />
        </cfif>
        <cfreturn true />
    </cffunction>

    <cffunction name="parseMobileScheduleDate" access="private" returntype="any" output="false">
        <cfargument name="value" required="true" />
        <cfset var v = trim("" & arguments.value) />
        <cfif NOT len(v) OR lCase(v) EQ "null">
            <cfreturn "" />
        </cfif>
        <cfif isDate(v)>
            <cfreturn parseDateTime(v) />
        </cfif>
        <cfreturn "" />
    </cffunction>

    <cffunction name="parseMobileScheduleDateTime" access="private" returntype="any" output="false">
        <cfargument name="value" required="true" />
        <cfargument name="fallbackDate" type="any" required="false" default="" />
        <cfset var v = trim("" & arguments.value) />
        <cfset var dPart = "" />
        <cfset var tPart = "" />
        <cfif NOT len(v) OR lCase(v) EQ "null">
            <cfreturn "" />
        </cfif>
        <cfif isDate(v)>
            <cfreturn parseDateTime(v) />
        </cfif>
        <cfif find(" ", v) AND isDate(listFirst(v, " ")) AND isDate(listLast(v, " "))>
            <cfreturn parseDateTime(v) />
        </cfif>
        <cfif isDate(arguments.fallbackDate) AND isDate(v)>
            <cfreturn parseDateTime(dateFormat(arguments.fallbackDate, "yyyy-mm-dd") & " " & timeFormat(v, "HH:mm:ss")) />
        </cfif>
        <cfreturn "" />
    </cffunction>

    <cffunction name="parseBulkSchedulesArray" access="private" returntype="array" output="false">
        <cfargument name="schedulesArg" type="string" required="false" default="" />
        <cfset var parsed = "" />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />

        <cfif len(trim(arguments.schedulesArg))>
            <cfset parsed = deserializeJSON(trim(arguments.schedulesArg)) />
        <cfelseif structKeyExists(form, "schedules") AND len(trim(form.schedules))>
            <cfset parsed = deserializeJSON(trim(form.schedules)) />
        <cfelse>
            <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 structKeyExists(bodyJson, "schedules")>
                        <cfset parsed = bodyJson.schedules />
                    <cfelseif structKeyExists(bodyJson, "SCHEDULES")>
                        <cfset parsed = bodyJson.SCHEDULES />
                    </cfif>
                </cfif>
                <cfcatch type="any">
                    <cfreturn [] />
                </cfcatch>
            </cftry>
        </cfif>

        <cfif isArray(parsed)>
            <cfreturn parsed />
        </cfif>
        <cfif isStruct(parsed)>
            <cfreturn [ parsed ] />
        </cfif>
        <cfreturn [] />
    </cffunction>

    <!--- Same pattern as components/general/progressnotes.cfc retire_schedules: copy row, audit new row, retire old with Status = 2 --->
    <cffunction name="retireMobileScheduleActiveRow" 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 = "" } />
        <cfset var getold = "" />
        <cfset var getcolumns = "" />
        <cfset var myList = "" />
        <cfset var lastSchedules = "" />
        <cfset var newAutoId = 0 />

        <cfquery name="getold" datasource="#Application.DataSrc#">
            SELECT AutoSchedule_ID
            FROM #arguments.agencySchema#.pSchedules
            WHERE Schedule_ID = <cfqueryparam value="#arguments.scheduleId#" cfsqltype="cf_sql_integer">
              AND Status = 0
              AND Emp_ID = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
            ORDER BY AutoSchedule_ID DESC
            LIMIT 1
        </cfquery>

        <cfif getold.recordcount EQ 0>
            <cfset result.message = "Schedule not found or inactive" />
            <cfreturn result />
        </cfif>

        <cfquery name="getcolumns" datasource="#Application.DataSrc#">
            SELECT COLUMN_NAME
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE COLUMN_NAME NOT IN ('AutoSchedule_ID','record_timestamp')
              AND TABLE_SCHEMA = <cfqueryparam value="#arguments.agencySchema#" cfsqltype="cf_sql_varchar">
              AND TABLE_NAME = 'pSchedules'
        </cfquery>

        <cfif getcolumns.recordcount EQ 0>
            <cfset result.message = "Could not resolve pSchedules columns for retire" />
            <cfreturn result />
        </cfif>

        <cfset myList = valueList(getcolumns.COLUMN_NAME) />

        <cfquery datasource="#Application.DataSrc#">
            INSERT INTO #arguments.agencySchema#.pSchedules (#myList#)
            SELECT #myList#
            FROM #arguments.agencySchema#.pSchedules
            WHERE Schedule_ID = <cfqueryparam value="#arguments.scheduleId#" cfsqltype="cf_sql_integer">
              AND Status = 0
              AND AutoSchedule_ID = <cfqueryparam value="#getold.AutoSchedule_ID#" cfsqltype="cf_sql_bigint">
        </cfquery>

        <cfquery name="lastSchedules" datasource="#Application.DataSrc#">
            SELECT LAST_INSERT_ID() AS LastInsertID
        </cfquery>

        <cfset newAutoId = val(lastSchedules.LastInsertID) />

        <cfif newAutoId LTE 0>
            <cfset result.message = "Retire insert did not return new AutoSchedule_ID" />
            <cfreturn result />
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pSchedules
            SET Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                Date_Create = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Created_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">,
                Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
            WHERE AutoSchedule_ID = <cfqueryparam value="#newAutoId#" cfsqltype="cf_sql_bigint">
        </cfquery>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pSchedules
            SET Status = <cfqueryparam value="2" cfsqltype="cf_sql_integer">,
                Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
            WHERE AutoSchedule_ID = <cfqueryparam value="#getold.AutoSchedule_ID#" cfsqltype="cf_sql_bigint">
        </cfquery>

        <cfset result.ok = true />
        <cfset result.message = "Retired previous row and inserted successor" />
        <cfreturn result />
    </cffunction>

    <cffunction name="updateMobileScheduleFromBulkRow" 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="row" type="struct" required="true" />
        <cfset var result = { ok = false, message = "", Schedule_ID = arguments.scheduleId } />
        <cfset var fields = arguments.row />
        <cfset var hasUpdate = false />
        <cfset var gpsStartDt = "" />
        <cfset var gpsEndDt = "" />
        <cfset var evvStartDt = "" />
        <cfset var evvEndDt = "" />
        <cfset var gpsStartDateVal = "" />
        <cfset var gpsEndDateVal = "" />

        <cfif structKeyExists(fields, "GPS_start_date") AND mobileScheduleFieldProvided(fields.GPS_start_date)>
            <cfset gpsStartDateVal = parseMobileScheduleDate(fields.GPS_start_date) />
        </cfif>
        <cfif structKeyExists(fields, "GPS_end_date") AND mobileScheduleFieldProvided(fields.GPS_end_date)>
            <cfset gpsEndDateVal = parseMobileScheduleDate(fields.GPS_end_date) />
        </cfif>
        <cfif structKeyExists(fields, "GPS_start_time") AND mobileScheduleFieldProvided(fields.GPS_start_time)>
            <cfset gpsStartDt = parseMobileScheduleDateTime(fields.GPS_start_time, gpsStartDateVal) />
        </cfif>
        <cfif structKeyExists(fields, "GPS_end_time") AND mobileScheduleFieldProvided(fields.GPS_end_time)>
            <cfset gpsEndDt = parseMobileScheduleDateTime(fields.GPS_end_time, gpsEndDateVal) />
        </cfif>
        <cfif structKeyExists(fields, "EVV_start") AND mobileScheduleFieldProvided(fields.EVV_start)>
            <cfset evvStartDt = parseMobileScheduleDateTime(fields.EVV_start, gpsStartDateVal) />
        </cfif>
        <cfif structKeyExists(fields, "EVV_end") AND mobileScheduleFieldProvided(fields.EVV_end)>
            <cfset evvEndDt = parseMobileScheduleDateTime(fields.EVV_end, gpsEndDateVal) />
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pSchedules
            SET
                <cfif structKeyExists(fields, "StartTime") AND mobileScheduleFieldProvided(fields.StartTime)>
                    StartTime = <cfqueryparam value="#trim("" & fields.StartTime)#" cfsqltype="cf_sql_varchar" null="#(NOT len(trim("" & fields.StartTime)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "EndTime") AND mobileScheduleFieldProvided(fields.EndTime)>
                    EndTime = <cfqueryparam value="#trim("" & fields.EndTime)#" cfsqltype="cf_sql_varchar" null="#(NOT len(trim("" & fields.EndTime)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "GPS_start_longitude") AND mobileScheduleFieldProvided(fields.GPS_start_longitude)>
                    GPS_start_longitude = <cfqueryparam value="#val(fields.GPS_start_longitude)#" cfsqltype="cf_sql_float" null="#(NOT len(trim("" & fields.GPS_start_longitude)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "GPS_start_latitude") AND mobileScheduleFieldProvided(fields.GPS_start_latitude)>
                    GPS_start_latitude = <cfqueryparam value="#val(fields.GPS_start_latitude)#" cfsqltype="cf_sql_float" null="#(NOT len(trim("" & fields.GPS_start_latitude)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif len(gpsStartDateVal) AND isDate(gpsStartDateVal)>
                    GPS_start_date = <cfqueryparam value="#gpsStartDateVal#" cfsqltype="cf_sql_date">,
                    <cfset hasUpdate = true />
                <cfelseif structKeyExists(fields, "GPS_start_date") AND mobileScheduleFieldProvided(fields.GPS_start_date) AND NOT len(trim("" & fields.GPS_start_date))>
                    GPS_start_date = NULL,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif len(gpsStartDt) AND isDate(gpsStartDt)>
                    GPS_start_time = <cfqueryparam value="#gpsStartDt#" cfsqltype="cf_sql_timestamp">,
                    <cfset hasUpdate = true />
                <cfelseif structKeyExists(fields, "GPS_start_time") AND mobileScheduleFieldProvided(fields.GPS_start_time) AND NOT len(trim("" & fields.GPS_start_time))>
                    GPS_start_time = NULL,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "GPS_end_longitude") AND mobileScheduleFieldProvided(fields.GPS_end_longitude)>
                    GPS_end_longitude = <cfqueryparam value="#val(fields.GPS_end_longitude)#" cfsqltype="cf_sql_float" null="#(NOT len(trim("" & fields.GPS_end_longitude)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "GPS_end_latitude") AND mobileScheduleFieldProvided(fields.GPS_end_latitude)>
                    GPS_end_latitude = <cfqueryparam value="#val(fields.GPS_end_latitude)#" cfsqltype="cf_sql_float" null="#(NOT len(trim("" & fields.GPS_end_latitude)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif len(gpsEndDateVal) AND isDate(gpsEndDateVal)>
                    GPS_end_date = <cfqueryparam value="#gpsEndDateVal#" cfsqltype="cf_sql_date">,
                    <cfset hasUpdate = true />
                <cfelseif structKeyExists(fields, "GPS_end_date") AND mobileScheduleFieldProvided(fields.GPS_end_date) AND NOT len(trim("" & fields.GPS_end_date))>
                    GPS_end_date = NULL,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif len(gpsEndDt) AND isDate(gpsEndDt)>
                    GPS_end_time = <cfqueryparam value="#gpsEndDt#" cfsqltype="cf_sql_timestamp">,
                    <cfset hasUpdate = true />
                <cfelseif structKeyExists(fields, "GPS_end_time") AND mobileScheduleFieldProvided(fields.GPS_end_time) AND NOT len(trim("" & fields.GPS_end_time))>
                    GPS_end_time = NULL,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "patient_sign") AND mobileScheduleFieldProvided(fields.patient_sign)>
                    patient_sign = <cfqueryparam value="#trim("" & fields.patient_sign)#" cfsqltype="cf_sql_longvarchar" null="#(NOT len(trim("" & fields.patient_sign)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif len(evvStartDt) AND isDate(evvStartDt)>
                    EVV_start = <cfqueryparam value="#evvStartDt#" cfsqltype="cf_sql_timestamp">,
                    <cfset hasUpdate = true />
                <cfelseif structKeyExists(fields, "EVV_start") AND mobileScheduleFieldProvided(fields.EVV_start) AND NOT len(trim("" & fields.EVV_start))>
                    EVV_start = NULL,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif len(evvEndDt) AND isDate(evvEndDt)>
                    EVV_end = <cfqueryparam value="#evvEndDt#" cfsqltype="cf_sql_timestamp">,
                    <cfset hasUpdate = true />
                <cfelseif structKeyExists(fields, "EVV_end") AND mobileScheduleFieldProvided(fields.EVV_end) AND NOT len(trim("" & fields.EVV_end))>
                    EVV_end = NULL,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "homebound") AND mobileScheduleFieldProvided(fields.homebound)>
                    homebound = <cfqueryparam value="#trim("" & fields.homebound)#" cfsqltype="cf_sql_longvarchar" null="#(NOT len(trim("" & fields.homebound)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                <cfif structKeyExists(fields, "Notes") AND mobileScheduleFieldProvided(fields.Notes)>
                    Notes = <cfqueryparam value="#trim("" & fields.Notes)#" cfsqltype="cf_sql_longvarchar" null="#(NOT len(trim("" & fields.Notes)))#">,
                    <cfset hasUpdate = true />
                </cfif>
                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 Status = 0
              AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
        </cfquery>

        <cfif NOT hasUpdate>
            <cfset result.message = "No updatable fields provided" />
            <cfreturn result />
        </cfif>

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

    <!---
      Bulk save pSchedules for the authenticated employee (same Schedule_ID).
      Per row: retire active record (Status = 2) and insert a copy, then apply mobile field updates
      on the new row — same pattern as components/general/progressnotes.cfc retire_schedules.
      POST JSON or form: securityKey + schedules array.
      Each item: SCHEDULE_ID, STARTTIME, ENDTIME, GPS_*, PATIENT_SIGN, EVV_*, HOMEBOUND, NOTES (UPPER_CASE or camelCase).
    --->
    <cffunction name="bulkSaveSchedules" access="remote" returntype="struct" returnformat="json" output="false" hint="Bulk save active schedules (retire + insert + apply fields) for logged-in employee.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="schedules" type="string" required="false" default="" hint="JSON array of schedule update objects" />

        <cfset var response = { success = false, message = "", data = { updated = [], failed = [], updatedCount = 0, failedCount = 0 } } />
        <cfset var auth = {} />
        <cfset var empId = 0 />
        <cfset var securityKeyVal = trim(arguments.securityKey) />
        <cfset var schedulesList = [] />
        <cfset var httpData = {} />
        <cfset var bodyJson = {} />
        <cfset var bodyText = "" />
        <cfset var idx = 0 />
        <cfset var rawItem = {} />
        <cfset var normItem = {} />
        <cfset var scheduleId = 0 />
        <cfset var scheduleCtx = {} />
        <cfset var updateResult = {} />
        <cfset var retireResult = {} />

        <cftry>
            <cfif NOT len(securityKeyVal)>
                <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 structKeyExists(bodyJson, "securityKey")>
                            <cfset securityKeyVal = trim("" & bodyJson.securityKey) />
                        <cfelseif structKeyExists(bodyJson, "SECURITYKEY")>
                            <cfset securityKeyVal = trim("" & bodyJson.SECURITYKEY) />
                        </cfif>
                    </cfif>
                    <cfcatch type="any"></cfcatch>
                </cftry>
            </cfif>
            <cfif NOT len(securityKeyVal) AND structKeyExists(form, "securityKey")>
                <cfset securityKeyVal = trim(form.securityKey) />
            </cfif>

            <cfif NOT len(securityKeyVal)>
                <cfset response.message = "securityKey is required" />
                <cfheader statuscode="401" statustext="Unauthorized" />
                <cfreturn response />
            </cfif>

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

            <cfset empId = parseSecurityKeyEmpId(securityKeyVal) />
            <cfset schedulesList = parseBulkSchedulesArray(arguments.schedules) />

            <cfif arrayLen(schedulesList) EQ 0>
                <cfset response.message = "schedules array is required (JSON)" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>

            <cfloop from="1" to="#arrayLen(schedulesList)#" index="idx">
                <cfset rawItem = schedulesList[idx] />
                <cfif NOT isStruct(rawItem)>
                    <cfset arrayAppend(response.data.failed, { index = idx, Schedule_ID = 0, message = "Invalid schedule object" }) />
                    <cfcontinue />
                </cfif>

                <cfset normItem = normalizeBulkScheduleItemKeys(rawItem) />
                <cfset scheduleId = 0 />
                <cfif structKeyExists(normItem, "Schedule_ID")>
                    <cfset scheduleId = val(normItem.Schedule_ID) />
                </cfif>

                <cfif scheduleId LTE 0>
                    <cfset arrayAppend(response.data.failed, { index = idx, Schedule_ID = 0, message = "SCHEDULE_ID is required" }) />
                    <cfcontinue />
                </cfif>

                <cfset scheduleCtx = getScheduleForMobileUpload(
                    agencySchema = auth.agencySchema,
                    scheduleId = scheduleId,
                    empId = empId
                ) />
                <cfif NOT scheduleCtx.ok>
                    <cfset arrayAppend(response.data.failed, { index = idx, Schedule_ID = scheduleId, message = scheduleCtx.message }) />
                    <cfcontinue />
                </cfif>

                <cfset structDelete(normItem, "Schedule_ID") />
                <cftry>
                    <cftransaction>
                        <cfset retireResult = retireMobileScheduleActiveRow(
                            agencySchema = auth.agencySchema,
                            scheduleId = scheduleId,
                            empId = empId
                        ) />
                        <cfif NOT retireResult.ok>
                            <cfthrow type="MobileBulkScheduleSave" message="#retireResult.message#" />
                        </cfif>
                        <cfset updateResult = updateMobileScheduleFromBulkRow(
                            agencySchema = auth.agencySchema,
                            scheduleId = scheduleId,
                            empId = empId,
                            row = normItem
                        ) />
                        <cfif NOT updateResult.ok>
                            <cfthrow type="MobileBulkScheduleSave" message="#updateResult.message#" />
                        </cfif>
                    </cftransaction>
                    <cfset arrayAppend(response.data.updated, scheduleId) />
                    <cfcatch type="any">
                        <cfset arrayAppend(response.data.failed, { index = idx, Schedule_ID = scheduleId, message = cfcatch.message }) />
                    </cfcatch>
                </cftry>
            </cfloop>

            <cfset response.data.updatedCount = arrayLen(response.data.updated) />
            <cfset response.data.failedCount = arrayLen(response.data.failed) />
            <cfif response.data.updatedCount GT 0 AND response.data.failedCount EQ 0>
                <cfset response.success = true />
                <cfset response.message = response.data.updatedCount & " schedule(s) saved" />
                <cfheader statuscode="200" statustext="OK" />
            <cfelseif response.data.updatedCount GT 0>
                <cfset response.success = true />
                <cfset response.message = response.data.updatedCount & " saved, " & response.data.failedCount & " failed" />
                <cfheader statuscode="207" statustext="Multi-Status" />
            <cfelse>
                <cfset response.message = "No schedules were saved" />
                <cfheader statuscode="400" statustext="Bad Request" />
            </cfif>
            <cfreturn response />

            <cfcatch type="any">
                <cfset response.success = false />
                <cfset response.message = "bulkSaveSchedules failed: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="authenticateAndGetData" access="remote" returntype="struct" returnformat="json" output="false" hint="Authenticate employee and return today's patient + schedule data">
        <cfargument name="username" type="string" required="true" hint="Employee username" />
        <cfargument name="password" type="string" required="true" hint="Employee password" />

        <cfset var response = {
            "success" = false,
            "message" = "Invalid username or password",
            "employee" = {},
            "patients" = []
        } />
        <cfset var encryptedPassword = "" />
        <cfset var employeeQuery = "" />
        <cfset var schemaCheckQuery = "" />
        <cfset var patientsQuery = "" />
        <cfset var schedulesQuery = "" />
        <cfset var agencySchema = "" />
        <cfset var empKey = "" />
        <cfset var securityKey = "" />
        <cfset var securityTokenPayload = "" />
        <cfset var todayVisitDate = dateFormat(now(), "yyyy-mm-dd") />
        <cfset var patientsArray = [] />
        <cfset var schedulesArray = [] />

        <cfif NOT len(trim(arguments.username)) OR NOT len(trim(arguments.password))>
            <cfset response.message = "Username and password are required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

        <cftry>
            <cfset encryptedPassword = encrypt(trim(arguments.password), this.passwordEncryptKey, "AES", "Base64") />

            <cfquery name="employeeQuery" datasource="#Application.DataSrc#">
                SELECT pEmployee.Emp_ID,
                       pEmployee.Emp_First,
                       pEmployee.Emp_Last,
                       pEmployee.Loc_ID,
                       Agency.Agency_Login
                FROM hhapowerpath.pEmployee
                INNER JOIN hhapowerpath.Agency ON Agency.Agency_ID = pEmployee.Loc_ID
                WHERE pEmployee.Username = <cfqueryparam cfsqltype="cf_sql_varchar" value="#trim(arguments.username)#">
                  AND pEmployee.Password = <cfqueryparam cfsqltype="cf_sql_varchar" value="#encryptedPassword#">
                  AND pEmployee.Emp_Type = 'Employee'
                  AND pEmployee.Status = 0
                LIMIT 1
            </cfquery>

            <cfif employeeQuery.recordcount EQ 0>
                <cfheader statuscode="401" statustext="Unauthorized" />
                <cfreturn response />
            </cfif>

            <!--- Restrict schema construction to numeric agency IDs only --->
            <cfif NOT isNumeric(employeeQuery.Loc_ID)>
                <cfset response.message = "Invalid agency mapping for user" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>

            <cfset agencySchema = "agency_" & 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="#agencySchema#">
            </cfquery>

            <cfif schemaCheckQuery.recordcount EQ 0>
                <cfset response.message = "Agency database not found" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>

            <cfset empKey = encrypt(employeeQuery.Emp_ID, this.empKeyEncryptKey, "AES", "Hex") />

            <!--- Unique per login: Emp_ID | UUID | ISO-like timestamp (decrypt server-side for future session/auth use) --->
            <cfset securityTokenPayload = employeeQuery.Emp_ID & "|" & createUUID() & "|" & dateFormat(now(), "yyyy-mm-dd") & "T" & timeFormat(now(), "HH:mm:ss") />
            <cfset securityKey = encrypt(securityTokenPayload, this.securityKeyEncryptKey, "AES", "Base64") />

            <cfquery name="patientsQuery" datasource="#Application.DataSrc#">
                SELECT Patient_ID,
                       Pt_First,
                       Pt_Middle,
                       Pt_Last,
                       Pt_Suffix,
                       Pt_Street,
                       Pt_City,
                       Pt_State,
                       Pt_Phone,
                       Pt_Zip
                FROM #agencySchema#.pPatients pPatients
                WHERE pPatients.Status IN (0, 1, 4)
                  AND EXISTS (
                        SELECT 1
                        FROM #agencySchema#.pSchedules pSchedules
                        WHERE pSchedules.Patient_ID = pPatients.Patient_ID
                          AND pSchedules.Status = 0
                          AND pSchedules.Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#employeeQuery.Emp_ID#">
                          AND pSchedules.Visit_Date = <cfqueryparam cfsqltype="cf_sql_date" value="#todayVisitDate#">
                  )
                ORDER BY Pt_Last, Pt_First
            </cfquery>

            <cfquery name="schedulesQuery" datasource="#Application.DataSrc#">
                SELECT Schedule_ID,
                       Patient_ID,
                       Assmt_ID,
                       Auth_ID,
                       Skill,
                       Missed,
                       patient_sign,
                       homebound,
                       Visit_Type,
                       DATE_FORMAT(Visit_Date, '%Y-%m-%d') AS Visit_Date,
                       TIME_FORMAT(StartTime, '%H:%i:%s') AS StartTime,
                       TIME_FORMAT(EndTime, '%H:%i:%s') AS EndTime,
                       DATE_FORMAT(GPS_start_date, '%Y-%m-%d') AS GPS_start_date,
                       TIME_FORMAT(GPS_start_time, '%H:%i:%s') AS GPS_start_time,
                       DATE_FORMAT(GPS_end_date, '%Y-%m-%d') AS GPS_end_date,
                       TIME_FORMAT(GPS_end_time, '%H:%i:%s') AS GPS_end_time,
                       GPS_start_latitude,
                       GPS_start_longitude,
                       GPS_end_latitude,
                       GPS_end_longitude,
                       DATE_FORMAT(EVV_start, '%Y-%m-%d %H:%i:%s') AS EVV_start,
                       DATE_FORMAT(EVV_end, '%Y-%m-%d %H:%i:%s') AS EVV_end,
                       Notes
                FROM #agencySchema#.pSchedules
                WHERE Status = 0
                  AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#employeeQuery.Emp_ID#">
                  AND Visit_Date = <cfqueryparam cfsqltype="cf_sql_date" value="#todayVisitDate#">
                ORDER BY Visit_Date DESC, StartTime DESC
            </cfquery>

            <cfset response.success = true />
            <cfset response.message = "Login successful" />
            <cfset response.employee = {
                "Emp_ID" = employeeQuery.Emp_ID,
                "Emp_First" = employeeQuery.Emp_First,
                "Emp_Last" = employeeQuery.Emp_Last,
                "Agency_ID" = employeeQuery.Loc_ID,
                "Agency_Login" = employeeQuery.Agency_Login,
                "Emp_Key" = empKey,
                "securityKey" = securityKey
            } />
            <cfset patientsArray = queryToStructArray(patientsQuery) />
            <cfset schedulesArray = queryToStructArray(schedulesQuery) />
            <cfset response.patients = buildPatientsWithSchedules(patientsArray, schedulesArray) />

            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset response.success = false />
                <cfset response.message = "Internal server error" />
                <cfset response.employee = {} />
                <cfset response.patients = [] />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Patient display helpers for Schedule_information --->
    <cffunction name="formatMobilePatientName" access="private" returntype="string" output="false">
        <cfargument name="first" type="string" required="false" default="" />
        <cfargument name="middle" type="string" required="false" default="" />
        <cfargument name="last" type="string" required="false" default="" />
        <cfargument name="suffix" type="string" required="false" default="" />
        <cfset var parts = [] />
        <cfif len(trim(arguments.first))><cfset arrayAppend(parts, trim(arguments.first)) /></cfif>
        <cfif len(trim(arguments.middle))><cfset arrayAppend(parts, trim(arguments.middle)) /></cfif>
        <cfif len(trim(arguments.last))><cfset arrayAppend(parts, trim(arguments.last)) /></cfif>
        <cfif len(trim(arguments.suffix))><cfset arrayAppend(parts, trim(arguments.suffix)) /></cfif>
        <cfreturn arrayLen(parts) ? arrayToList(parts, " ") : "" />
    </cffunction>

    <cffunction name="calculateMobilePatientAgeYears" access="private" returntype="numeric" output="false">
        <cfargument name="dob" type="any" required="false" default="" />
        <cfset var dobDate = "" />
        <cfif NOT len(trim("" & arguments.dob))>
            <cfreturn 0 />
        </cfif>
        <cftry>
            <cfif isDate(arguments.dob)>
                <cfset dobDate = arguments.dob />
            <cfelse>
                <cfset dobDate = parseDateTime(trim("" & arguments.dob)) />
            </cfif>
            <cfreturn max(0, dateDiff("yyyy", dobDate, now())) />
            <cfcatch type="any">
                <cfreturn 0 />
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="formatMobilePatientGender" access="private" returntype="string" output="false">
        <cfargument name="gender" type="string" required="false" default="" />
        <cfset var g = uCase(left(trim(arguments.gender), 1)) />
        <cfif g EQ "M"><cfreturn "Male" /></cfif>
        <cfif g EQ "F"><cfreturn "Female" /></cfif>
        <cfif len(trim(arguments.gender))><cfreturn trim(arguments.gender) /></cfif>
        <cfreturn "" />
    </cffunction>

    <cffunction name="deriveMobileVisitStatus" access="private" returntype="string" output="false">
        <cfargument name="scheduleRow" type="struct" required="true" />
        <cfargument name="assessmentComplete" type="boolean" required="false" default="false" />
        <cfset var missed = structKeyExists(arguments.scheduleRow, "Missed") ? val(arguments.scheduleRow.Missed) : 0 />
        <cfif missed EQ 1><cfreturn "Missed - Not Attempted" /></cfif>
        <cfif missed EQ 2><cfreturn "Missed - Attempted" /></cfif>
        <cfif missed EQ 3><cfreturn "Cancelled" /></cfif>
        <cfif arguments.assessmentComplete><cfreturn "Complete" /></cfif>
        <cfif structKeyExists(arguments.scheduleRow, "EVV_end") AND len(trim("" & arguments.scheduleRow.EVV_end))>
            <cfreturn "Completed" />
        </cfif>
        <cfif structKeyExists(arguments.scheduleRow, "GPS_end_time") AND len(trim("" & arguments.scheduleRow.GPS_end_time))>
            <cfreturn "Completed" />
        </cfif>
        <cfif structKeyExists(arguments.scheduleRow, "EVV_start") AND len(trim("" & arguments.scheduleRow.EVV_start))>
            <cfreturn "In Progress" />
        </cfif>
        <cfif structKeyExists(arguments.scheduleRow, "GPS_start_time") AND len(trim("" & arguments.scheduleRow.GPS_start_time))>
            <cfreturn "In Progress" />
        </cfif>
        <cfreturn "Scheduled" />
    </cffunction>

    <!--- Wizard section progress (new_wizard blocks + important answers on pAssessments) --->
    <cffunction name="getMobileAssessmentWizardProgress" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="assessmentReason" type="string" required="true" />
        <cfset var out = {
            sections_total = 0,
            sections_completed = 0,
            progress_percent = 0,
            assessment_complete = false,
            blocks = []
        } />
        <cfset var socWizard = "" />
        <cfset var assmtQuery = "" />
        <cfset var blocksQuery = "" />
        <cfset var blockOrder = "" />
        <cfset var blockId = "" />
        <cfset var blockNum = 0 />
        <cfset var quesQuery = "" />
        <cfset var distinctQ = "" />
        <cfset var blockDone = true />
        <cfset var rulesQuery = "" />
        <cfset var oneOfGroups = {} />
        <cfset var targetsRaw = [] />
        <cfset var targets = [] />
        <cfset var t = "" />
        <cfset var checkLabel = "" />
        <cfset var labelsToCheck = [] />
        <cfset var groupSatisfied = false />
        <cfset var col = "" />
        <cfset var val = "" />
        <cfset var blockRow = {} />

        <cfif arguments.assmtId LTE 0 OR NOT len(trim(arguments.assessmentReason))>
            <cfreturn out />
        </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 = arguments.agencySchema />

        <cfquery name="assmtQuery" datasource="#Application.DataSrc#">
            SELECT *
            FROM #arguments.agencySchema#.pAssessments
            WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
            LIMIT 1
        </cfquery>
        <cfif assmtQuery.recordCount EQ 0>
            <cfreturn out />
        </cfif>

        <cfif val(assmtQuery.F25) EQ 1>
            <cfset out.assessment_complete = true />
        </cfif>

        <cfset socWizard = createObject("component", "components.general.soc_wizard").init() />
        <cfset blocksQuery = socWizard.get_new_wizard_blocks(arguments.assessmentReason) />
        <cfif blocksQuery.recordCount EQ 0>
            <cfif out.assessment_complete>
                <cfset out.progress_percent = 100 />
            </cfif>
            <cfreturn out />
        </cfif>

        <cfset blockOrder = valueList(blocksQuery.new_wizard) />
        <cftry>
            <cfset rulesQuery = socWizard.get_question_importance_rules(arguments.assessmentReason) />
            <cfif rulesQuery.recordCount GT 0 AND listFindNoCase(rulesQuery.columnList, "Validation_Condition")>
                <cfloop query="rulesQuery">
                    <cfif trim(rulesQuery.Validation_Condition) EQ "one_of_required" AND len(trim(rulesQuery.Target_Questions))>
                        <cfset targetsRaw = listToArray(rulesQuery.Target_Questions, ",", false, true) />
                        <cfset targets = [] />
                        <cfloop array="#targetsRaw#" index="t">
                            <cfset t = trim(t) />
                            <cfif len(t)>
                                <cfset arrayAppend(targets, uCase(t)) />
                                <cfset oneOfGroups[uCase(t)] = targets />
                            </cfif>
                        </cfloop>
                    </cfif>
                </cfloop>
            </cfif>
            <cfcatch type="any"><cfset oneOfGroups = {} /></cfcatch>
        </cftry>

        <cfloop list="#blockOrder#" index="blockId">
            <cfset blockId = trim(blockId) />
            <cfif NOT len(blockId) OR NOT isNumeric(blockId)><cfcontinue /></cfif>
            <cfset blockNum = int(val(blockId)) />
            <cfset quesQuery = socWizard.get_all_ques_ans_new_wizard(
                assessment_reason = arguments.assessmentReason,
                new_wizard = blockNum,
                view_mode = "wizard"
            ) />
            <cfset blockDone = true />
            <cfset blockRow = {
                new_wizard = blockNum,
                label = socWizard.get_new_wizard_tab_label_for_block(arguments.assessmentReason, blockNum),
                question_count = 0,
                completed = false
            } />

            <cfif quesQuery.recordCount GT 0>
                <cfquery name="distinctQ" dbtype="query">
                    SELECT ques_id FROM quesQuery GROUP BY ques_id
                </cfquery>
                <cfset blockRow.question_count = distinctQ.recordCount />
                <cfloop query="quesQuery" group="answer_label">
                    <cfif listFindNoCase(quesQuery.columnList, "important") AND quesQuery.important EQ 1
                        AND listFindNoCase(quesQuery.columnList, "new_wizard") GT 0 AND quesQuery.new_wizard NEQ 0>
                        <cfset labelsToCheck = structKeyExists(oneOfGroups, trim(quesQuery.answer_label))
                            ? oneOfGroups[trim(quesQuery.answer_label)]
                            : [trim(quesQuery.answer_label)] />
                        <cfset groupSatisfied = false />
                        <cfloop array="#labelsToCheck#" index="checkLabel">
                            <cfset checkLabel = trim("" & checkLabel) />
                            <cfset val = "" />
                            <cfloop list="#assmtQuery.columnList#" index="col">
                                <cfif compareNoCase(col, checkLabel) EQ 0>
                                    <cfif len(trim("" & assmtQuery[col][1]))>
                                        <cfset val = trim("" & assmtQuery[col][1]) />
                                    </cfif>
                                    <cfbreak />
                                </cfif>
                            </cfloop>
                            <cfif len(val)>
                                <cfset groupSatisfied = true />
                                <cfbreak />
                            </cfif>
                        </cfloop>
                        <cfif NOT groupSatisfied>
                            <cfset blockDone = false />
                        </cfif>
                    </cfif>
                </cfloop>
                <cfset blockRow.completed = blockDone />
            <cfelse>
                <cfset blockRow.completed = true />
            </cfif>

            <cfset out.sections_total = out.sections_total + 1 />
            <cfif blockRow.completed>
                <cfset out.sections_completed = out.sections_completed + 1 />
            </cfif>
            <cfset arrayAppend(out.blocks, blockRow) />
        </cfloop>

        <cfif out.sections_total GT 0>
            <cfset out.progress_percent = int((out.sections_completed / out.sections_total) * 100) />
        <cfelseif out.assessment_complete>
            <cfset out.progress_percent = 100 />
        </cfif>
        <cfif out.assessment_complete AND out.sections_total GT 0>
            <cfset out.sections_completed = out.sections_total />
            <cfset out.progress_percent = 100 />
        </cfif>

        <cfreturn out />
    </cffunction>

  <!--- Schedule header: patient, visit status, assessment progress for mobile visit screen --->
    <cffunction name="Schedule_information" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Schedule_ID. Returns patient demographics, visit status, and assessment wizard progress.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Schedule_ID" type="string" required="true" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var auth = {} />
        <cfset var scheduleId = val(trim(arguments.Schedule_ID)) />
        <cfset var empId = 0 />
        <cfset var scheduleCtx = {} />
        <cfset var infoQuery = "" />
        <cfset var scheduleRow = {} />
        <cfset var resolvedAssmtId = 0 />
        <cfset var wizardReason = "" />
        <cfset var assmtQuery = "" />
        <cfset var progress = {} />
        <cfset var ageYears = 0 />

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

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

        <cfset empId = parseSecurityKeyEmpId(trim(arguments.securityKey)) />
        <cfset var schedCtx = resolveMobileScheduleAssessmentContext(
            agencySchema = auth.agencySchema,
            scheduleId = scheduleId,
            empId = empId
        ) />
        <cfif NOT schedCtx.ok>
            <cfset response.message = schedCtx.message />
            <cfheader statuscode="403" statustext="Forbidden" />
            <cfreturn response />
        </cfif>

        <cftry>
            <cfquery name="infoQuery" datasource="#Application.DataSrc#">
                SELECT s.Schedule_ID,
                       s.Patient_ID,
                       s.Assmt_ID,
                       s.Visit_Type,
                       DATE_FORMAT(s.Visit_Date, '%Y-%m-%d') AS Visit_Date,
                       s.Skill,
                       p.Pt_First,
                       p.Pt_Middle,
                       p.Pt_Last,
                       p.Pt_Suffix,
                       p.Pt_DOB,
                       p.Pt_Gender
                FROM #auth.agencySchema#.pSchedules s
                INNER JOIN #auth.agencySchema#.pPatients p
                  ON p.Patient_ID = s.Patient_ID
                WHERE s.Schedule_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#scheduleId#">
                  AND s.Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                LIMIT 1
            </cfquery>

            <cfif infoQuery.recordCount EQ 0>
                <cfset response.message = "Schedule not found" />
                <cfheader statuscode="404" statustext="Not Found" />
                <cfreturn response />
            </cfif>

            <cfset progress = getMobileAssessmentTabsSummary(
                agencySchema = auth.agencySchema,
                patientId = schedCtx.patientId,
                assmtId = schedCtx.assmtId,
                assessmentReason = schedCtx.assessmentReason,
                admitId = schedCtx.admitId,
                visitDate = schedCtx.visitDate
            ) />

            <cfset ageYears = calculateMobilePatientAgeYears(infoQuery.Pt_DOB) />

            <cfset response.success = true />
            <cfset response.message = "Schedule information loaded" />
            <cfset response.data = {
                schedule_id = scheduleId,
                patient_id = schedCtx.patientId,
                admit_id = schedCtx.admitId,
                assmt_id = schedCtx.assmtId,
                schedule_assmt_id = schedCtx.scheduleAssmtId,
                visit_type = schedCtx.visitType,
                visit_date = schedCtx.visitDate,
                skill = schedCtx.skill,
                assessment_reason = schedCtx.assessmentReason,
                patient = {
                    patient_id = schedCtx.patientId,
                    pt_first = trim(infoQuery.Pt_First),
                    pt_middle = trim(infoQuery.Pt_Middle),
                    pt_last = trim(infoQuery.Pt_Last),
                    pt_suffix = trim(infoQuery.Pt_Suffix),
                    patient_name = formatMobilePatientName(
                        first = infoQuery.Pt_First,
                        middle = infoQuery.Pt_Middle,
                        last = infoQuery.Pt_Last,
                        suffix = infoQuery.Pt_Suffix
                    ),
                    age_years = ageYears,
                    age_display = ageYears GT 0 ? ageYears & " yrs" : "",
                    gender = formatMobilePatientGender(infoQuery.Pt_Gender),
                    pt_gender = trim(infoQuery.Pt_Gender),
                    pt_dob = isDate(infoQuery.Pt_DOB) ? dateFormat(infoQuery.Pt_DOB, "yyyy-mm-dd") : trim("" & infoQuery.Pt_DOB)
                },
                visit_status = deriveMobileVisitStatus(
                    scheduleRow = schedCtx.schedule,
                    assessmentComplete = progress.assessment_complete
                ),
                assessment_progress = {
                    questions_total = progress.questions_total,
                    questions_completed = progress.questions_completed,
                    progress_percent = progress.questions_total GT 0
                        ? int((progress.questions_completed / progress.questions_total) * 100)
                        : progress.progress_percent,
                    assessment_complete = progress.assessment_complete,
                    progress_label = progress.total_progress_label
                }
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />

            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "Schedule_information",
                    failedStep = "load_schedule_information",
                    context = { scheduleId = scheduleId, agencySchema = auth.agencySchema },
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load schedule information: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Assessment summary tabs for mobile (Schedule_ID only; assessment_reason from visit context) --->
    <cffunction name="Assessment_summary" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Schedule_ID. Returns assessment_tabs with per-section progress for the visit assessment.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Schedule_ID" type="string" required="true" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var auth = {} />
        <cfset var scheduleId = val(trim(arguments.Schedule_ID)) />
        <cfset var empId = 0 />
        <cfset var schedCtx = {} />
        <cfset var progress = {} />

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

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

        <cfset empId = parseSecurityKeyEmpId(trim(arguments.securityKey)) />
        <cfset schedCtx = resolveMobileScheduleAssessmentContext(
            agencySchema = auth.agencySchema,
            scheduleId = scheduleId,
            empId = empId
        ) />
        <cfif NOT schedCtx.ok>
            <cfset response.message = schedCtx.message />
            <cfheader statuscode="403" statustext="Forbidden" />
            <cfreturn response />
        </cfif>

        <cftry>
            <cfset progress = getMobileAssessmentTabsSummary(
                agencySchema = auth.agencySchema,
                patientId = schedCtx.patientId,
                assmtId = schedCtx.assmtId,
                assessmentReason = schedCtx.assessmentReason,
                admitId = schedCtx.admitId,
                visitDate = schedCtx.visitDate
            ) />

            <cfset response.success = true />
            <cfset response.message = "Assessment summary loaded" />
            <cfset response.data = {
                schedule_id = scheduleId,
                patient_id = schedCtx.patientId,
                admit_id = schedCtx.admitId,
                assmt_id = schedCtx.assmtId,
                schedule_assmt_id = schedCtx.scheduleAssmtId,
                visit_type = schedCtx.visitType,
                visit_date = schedCtx.visitDate,
                assessment_reason = schedCtx.assessmentReason,
                tabs = progress.tabs
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />

            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "Assessment_summary",
                    failedStep = "load_assessment_summary",
                    context = { scheduleId = scheduleId, agencySchema = auth.agencySchema },
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load assessment summary: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Assessment tab Q&A: questions_2 → questions, title_2 → title, answer_2 → answer (whisper_ai style) --->
    <cffunction name="getAssessmentTabQuestions" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Assmt_ID (from Assessment_summary), and new_wizard. Returns questions/answers for that wizard block using questions_2/title_2/answer_2.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Assmt_ID" type="string" required="true" hint="Assmt_ID returned by Assessment_summary" />
        <cfargument name="new_wizard" type="string" required="true" hint="assessment_tabs.wizard / new_wizard block id" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var auth = {} />
        <cfset var assmtId = val(trim(arguments.Assmt_ID)) />
        <cfset var wizardBlock = val(trim(arguments.new_wizard)) />
        <cfset var lookupDb = "hhapowerpath" />
        <cfset var assmtQuery = "" />
        <cfset var assessmentReason = "" />
        <cfset var patientId = 0 />
        <cfset var admitId = 0 />
        <cfset var visitDate = "" />
        <cfset var getQuestions = "" />
        <cfset var getAnswers = "" />
        <cfset var answersByQid = {} />
        <cfset var qid = "" />
        <cfset var qRow = {} />
        <cfset var aRow = {} />
        <cfset var questions = [] />
        <cfset var fieldValue = "" />

        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif assmtId LTE 0>
            <cfset response.message = "Assmt_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>
        <cfif wizardBlock LTE 0>
            <cfset response.message = "new_wizard is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />
        <cfif NOT auth.ok>
            <cfset response.message = auth.message />
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <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 lookupDb = trim(Request.prefix_db_lookup) />
        <cfset Request.prefix_db_agency = auth.agencySchema />

        <cftry>
            <cfquery name="assmtQuery" datasource="#Application.DataSrc#">
                SELECT *
                FROM #auth.agencySchema#.pAssessments
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtId#">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                ORDER BY AutoAssmt_ID DESC
                LIMIT 1
            </cfquery>
            <cfif assmtQuery.recordCount EQ 0>
                <cfset response.message = "Assessment not found" />
                <cfheader statuscode="404" statustext="Not Found" />
                <cfreturn response />
            </cfif>

            <cfset assessmentReason = trim(assmtQuery.F26) />
            <cfset patientId = val(assmtQuery.Patient_ID) />
            <cfset admitId = val(assmtQuery.Admit_ID) />
            <cfif isDate(assmtQuery.F176)>
                <cfset visitDate = dateFormat(assmtQuery.F176, "yyyy-mm-dd") />
            <cfelseif len(trim("" & assmtQuery.F176))>
                <cfset visitDate = left(trim("" & assmtQuery.F176), 10) />
            </cfif>
            <cfif NOT len(assessmentReason)>
                <cfset response.message = "Assessment reason (F26) is missing for this Assmt_ID" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>

            <cfquery name="getQuestions" datasource="#Application.DataSrc#">
                SELECT ques.ques_id,
                       CASE
                           WHEN TRIM(IFNULL(ques.questions_2, '')) <> '' THEN
                               TRIM(CONCAT_WS(' ', NULLIF(TRIM(ques.ques_identifier_2), ''), NULLIF(TRIM(ques.questions_2), '')))
                           ELSE ques.questions
                       END AS questions,
                       IFNULL(ques.ques_identifier_2, '') AS questions_new,
                       ques.quescolor,
                       ques.title_2 AS title,
                       ques.questions_label,
                       ques.answer_label,
                       ques.questions_type,
                       IFNULL(NULLIF(ques.new_order_ID, 0), ques.order_id) AS order_id,
                       ques.assessment_reason,
                       ques.new_wizard AS wizard,
                       IFNULL(ques.ques_identifier_2, ques.ques_identifier) AS ques_identifier,
                       ques.important,
                       ques.other,
                       ques.alert_message,
                       ques.text_labels,
                       ques.display
                FROM #lookupDb#.assessment_questions_lookup AS ques
                WHERE ques.assessment_reason = <cfqueryparam cfsqltype="cf_sql_varchar" value="#assessmentReason#">
                  AND ques.Deleted = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                  AND (
                        ques.new_wizard = <cfqueryparam cfsqltype="cf_sql_integer" value="#wizardBlock#">
                     OR (ques.new_wizard = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                         AND ques.display = <cfqueryparam value="yes" cfsqltype="cf_sql_varchar">)
                  )
                ORDER BY
                    CASE WHEN UPPER(TRIM(ques.answer_label)) = 'F175' THEN 1 ELSE 0 END,
                    IFNULL(NULLIF(ques.new_order_ID, 0), ques.order_id),
                    ques.answer_label
            </cfquery>

            <cfquery name="getAnswers" datasource="#Application.DataSrc#">
                SELECT answ.val_id,
                       CASE
                           WHEN TRIM(IFNULL(answ.answer_2, '')) <> '' THEN TRIM(answ.answer_2)
                           ELSE answ.answer
                       END AS answer,
                       answ.text_labels,
                       answ.assessment_reason,
                       answ.ans_id,
                       answ.item_id,
                       answ.answer_2,
                       answ.question_id,
                       answ.answer_type,
                       answ.srt_id,
                       answ.tooltip_title
                FROM #lookupDb#.assessment_answers_lookup AS answ
                WHERE answ.assessment_reason = <cfqueryparam cfsqltype="cf_sql_varchar" value="#assessmentReason#">
                  AND answ.Deleted = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                  AND answ.question_id IN (
                        SELECT ques_id
                        FROM #lookupDb#.assessment_questions_lookup
                        WHERE assessment_reason = <cfqueryparam cfsqltype="cf_sql_varchar" value="#assessmentReason#">
                          AND Deleted = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                          AND (
                                new_wizard = <cfqueryparam cfsqltype="cf_sql_integer" value="#wizardBlock#">
                             OR (new_wizard = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                                 AND display = <cfqueryparam value="yes" cfsqltype="cf_sql_varchar">)
                          )
                  )
                ORDER BY answ.question_id, answ.srt_id, answ.ans_id
            </cfquery>

            <cfloop query="getAnswers">
                <cfset qid = "" & getAnswers.question_id />
                <cfif NOT structKeyExists(answersByQid, qid)>
                    <cfset answersByQid[qid] = [] />
                </cfif>
                <cfset aRow = {
                    ans_id = val(getAnswers.ans_id),
                    val_id = val(getAnswers.val_id),
                    item_id = val(getAnswers.item_id),
                    answer = trim(getAnswers.answer),
                    answer_2 = trim(getAnswers.answer_2),
                    answer_type = trim(getAnswers.answer_type),
                    text_labels = trim(getAnswers.text_labels),
                    tooltip_title = trim(getAnswers.tooltip_title),
                    srt_id = val(getAnswers.srt_id)
                } />
                <cfset arrayAppend(answersByQid[qid], aRow) />
            </cfloop>

            <cfloop query="getQuestions">
                <cfset qid = "" & getQuestions.ques_id />
                <cfset fieldValue = "" />
                <cfif len(trim(getQuestions.answer_label))>
                    <cfset fieldValue = getMobileAssessmentFieldValue(assmtQuery, trim(getQuestions.answer_label)) />
                </cfif>
                <cfset qRow = {
                    ques_id = val(getQuestions.ques_id),
                    questions = trim(getQuestions.questions),
                    questions_new = trim(getQuestions.questions_new),
                    title = trim(getQuestions.title),
                    questions_label = trim(getQuestions.questions_label),
                    answer_label = trim(getQuestions.answer_label),
                    questions_type = trim(getQuestions.questions_type),
                    ques_identifier = trim(getQuestions.ques_identifier),
                    quescolor = trim(getQuestions.quescolor),
                    order_id = val(getQuestions.order_id),
                    wizard = val(getQuestions.wizard),
                    assessment_reason = trim(getQuestions.assessment_reason),
                    important = val(getQuestions.important),
                    other = val(getQuestions.other),
                    alert_message = trim(getQuestions.alert_message),
                    text_labels = trim(getQuestions.text_labels),
                    display = trim(getQuestions.display),
                    answers = structKeyExists(answersByQid, qid) ? answersByQid[qid] : [],
                    saved_value = fieldValue,
                    answered = len(fieldValue) GT 0
                } />
                <cfset arrayAppend(questions, qRow) />
            </cfloop>

            <cfset response.success = true />
            <cfset response.message = "Assessment tab questions loaded" />
            <cfset response.data = {
                assmt_id = assmtId,
                patient_id = patientId,
                admit_id = admitId,
                visit_date = visitDate,
                assessment_reason = assessmentReason,
                new_wizard = wizardBlock,
                questions = questions,
                count = arrayLen(questions)
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />

            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "getAssessmentTabQuestions",
                    failedStep = "load_assessment_tab_questions",
                    context = {
                        assmtId = assmtId,
                        newWizard = wizardBlock,
                        agencySchema = auth.agencySchema
                    },
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load assessment tab questions: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Vitals and/or wounds from pWoundsVitals for one assessment (Assmt_ID only) --->
    <cffunction name="getAssessmentWoundsVitals" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Assmt_ID. Optional record_type: vital, wound, or all (default). Returns rows from pWoundsVitals with wound photos from pg_files (Wounds_assessments).">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Assmt_ID" type="string" required="true" />
        <cfargument name="record_type" type="string" required="false" default="all" hint="vital | wound | all" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var assmtId = val(trim(arguments.Assmt_ID)) />
        <cfset var typeFilter = lcase(trim(arguments.record_type)) />
        <cfset var ctx = {} />
        <cfset var rowsQuery = "" />
        <cfset var vitals = [] />
        <cfset var wounds = [] />
        <cfset var row = {} />
        <cfset var parsed = {} />
        <cfset var locationVal = "" />
        <cfset var woundTypeVal = "" />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />
        <cfset var photoData = {} />
        <cfset var patientId = 0 />

        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif assmtId LTE 0>
            <cfset response.message = "Assmt_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>
        <cfif NOT listFindNoCase("vital,wound,all", typeFilter)>
            <cfset typeFilter = "all" />
        </cfif>

        <cfset ctx = authorizeMobileAssmtRequest(
            securityKey = trim(arguments.securityKey),
            assmtId = assmtId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <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 emailCtx = {
            assmtId = assmtId,
            recordType = typeFilter,
            empId = ctx.empId,
            locId = ctx.locId,
            agencySchema = ctx.agencySchema
        } />

        <cftry>
            <cfset currentStep = "query_pWoundsVitals" />
            <cfquery name="rowsQuery" datasource="#Application.DataSrc#">
                SELECT wv.ID,
                       wv.Assmt_ID,
                       wv.Item_ID,
                       wv.Pathway_ID,
                       wv.VitalOrWound,
                       wv.Description,
                       wv.Location,
                       wv.other_locations,
                       wv.Length,
                       wv.Width,
                       wv.Depth,
                       wv.Reading1,
                       wv.Notes,
                       wv.Param_Lo,
                       wv.Param_Hi,
                       wv.pain_location,
                       wv.pain_duration,
                       wv.pain_exacerbate,
                       wv.pain_relief,
                       wv.pain_med_effectiveness,
                       wv.healingstage,
                       wv.frompgnote,
                       wv.woundtype,
                       wv.wnd_bed,
                       wv.wnd_margin,
                       wv.wnd_exudate,
                       wv.wnd_odor,
                       wv.wnd_pain,
                       wv.wnd_tunneling,
                       wv.wnd_underm,
                       wv.wnd_gran,
                       wv.wnd_epith,
                       wv.wnd_eschar,
                       wv.wnd_slough,
                       wv.wnd_fib,
                       wv.Date_Create,
                       wv.Date_Change,
                       wv.Created_by,
                       wv.Change_by,
                       (SELECT MAX(vl.Measure)
                        FROM #Request.prefix_db_lookup#.pValue_Lookup vl
                        WHERE vl.Item_ID = wv.Item_ID
                          AND vl.Description = wv.Description) AS Measure,
                       (SELECT MAX(vl.Required)
                        FROM #Request.prefix_db_lookup#.pValue_Lookup vl
                        WHERE vl.Item_ID = wv.Item_ID
                          AND vl.Description = wv.Description) AS Required
                FROM #ctx.agencySchema#.pWoundsVitals wv
                WHERE wv.Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#assmtId#">
                  AND wv.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  <cfif typeFilter EQ "vital">
                      AND wv.VitalOrWound = <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">
                  <cfelseif typeFilter EQ "wound">
                      AND wv.VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                  <cfelse>
                      AND wv.VitalOrWound IN (
                          <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">,
                          <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                      )
                  </cfif>
                ORDER BY wv.VitalOrWound, wv.Item_ID, wv.ID
            </cfquery>

            <cfset currentStep = "build_response_rows" />
            <cfloop query="rowsQuery">
                <cfif compareNoCase(trim(rowsQuery.VitalOrWound), "wound") EQ 0>
                    <cfset locationVal = trim(rowsQuery.Location) />
                    <cfset woundTypeVal = trim(rowsQuery.woundtype) />
                    <cfset row = {
                        id = rowsQuery.ID,
                        assmt_id = rowsQuery.Assmt_ID,
                        item_id = rowsQuery.Item_ID,
                        pathway_id = rowsQuery.Pathway_ID,
                        vital_or_wound = "wound",
                        description = trim(rowsQuery.Description),
                        location = locationVal,
                        other_locations = trim(rowsQuery.other_locations),
                        length = trim(rowsQuery.Length),
                        width = trim(rowsQuery.Width),
                        depth = trim(rowsQuery.Depth),
                        surface_reading = trim(rowsQuery.Reading1),
                        param_lo = trim(rowsQuery.Param_Lo),
                        param_hi = trim(rowsQuery.Param_Hi),
                        notes = trim(rowsQuery.Notes),
                        healing_stage = trim(rowsQuery.healingstage),
                        wound_type = woundTypeVal,
                        custom_group = (compareNoCase(locationVal, "other") EQ 0 AND len(woundTypeVal)) ? woundTypeVal : trim(rowsQuery.Description),
                        from_progress_note = val(rowsQuery.frompgnote),
                        wnd_bed = trim(rowsQuery.wnd_bed),
                        wnd_margin = trim(rowsQuery.wnd_margin),
                        wnd_exudate = trim(rowsQuery.wnd_exudate),
                        wnd_odor = trim(rowsQuery.wnd_odor),
                        wnd_pain = trim(rowsQuery.wnd_pain),
                        wnd_tunneling = trim(rowsQuery.wnd_tunneling),
                        wnd_underm = trim(rowsQuery.wnd_underm),
                        wnd_gran = trim(rowsQuery.wnd_gran),
                        wnd_epith = trim(rowsQuery.wnd_epith),
                        wnd_eschar = trim(rowsQuery.wnd_eschar),
                        wnd_slough = trim(rowsQuery.wnd_slough),
                        wnd_fib = trim(rowsQuery.wnd_fib),
                        date_create = rowsQuery.Date_Create,
                        date_change = rowsQuery.Date_Change,
                        created_by = rowsQuery.Created_by,
                        change_by = rowsQuery.Change_by
                    } />
                    <cfset arrayAppend(wounds, row) />
                <cfelse>
                    <cfset parsed = parseVitalReadingAndNotes(rowsQuery.Reading1, rowsQuery.Notes) />
                    <cfset row = {
                        id = rowsQuery.ID,
                        assmt_id = rowsQuery.Assmt_ID,
                        item_id = rowsQuery.Item_ID,
                        pathway_id = rowsQuery.Pathway_ID,
                        vital_or_wound = "vital",
                        description = trim(rowsQuery.Description),
                        reading = parsed.reading,
                        reading_primary = parsed.reading_primary,
                        reading_secondary = parsed.reading_secondary,
                        descriptor = parsed.descriptor,
                        notes = parsed.notes,
                        measure = trim(rowsQuery.Measure),
                        required = (trim(rowsQuery.Required) EQ "*"),
                        param_lo = trim(rowsQuery.Param_Lo),
                        param_hi = trim(rowsQuery.Param_Hi),
                        pain_location = trim(rowsQuery.pain_location),
                        pain_duration = trim(rowsQuery.pain_duration),
                        pain_exacerbate = trim(rowsQuery.pain_exacerbate),
                        pain_relief = trim(rowsQuery.pain_relief),
                        pain_med_effectiveness = trim(rowsQuery.pain_med_effectiveness),
                        date_create = rowsQuery.Date_Create,
                        date_change = rowsQuery.Date_Change,
                        created_by = rowsQuery.Created_by,
                        change_by = rowsQuery.Change_by
                    } />
                    <cfset arrayAppend(vitals, row) />
                </cfif>
            </cfloop>

            <cfset currentStep = "load_wound_photos" />
            <cfset patientId = val(ctx.assessment.Patient_ID) />
            <cfset photoData = loadMobileAssessmentWoundPhotos(
                agencySchema = ctx.agencySchema,
                locId = ctx.locId,
                patientId = patientId,
                assmtId = assmtId
            ) />
            <cfif typeFilter NEQ "vital" AND arrayLen(wounds) GT 0>
                <cfset wounds = attachMobileWoundPhotosToWounds(wounds, photoData) />
            </cfif>

            <cfset response.success = true />
            <cfset response.message = "Assessment wounds/vitals loaded" />
            <cfset response.data = {
                assmt_id = assmtId,
                patient_id = patientId,
                admit_id = val(ctx.assessment.Admit_ID),
                assessment_reason = trim(ctx.assessment.F26),
                record_type = typeFilter,
                vitals = vitals,
                wounds = wounds,
                vital_count = arrayLen(vitals),
                wound_count = arrayLen(wounds),
                assessment_photos = photoData.assessmentPhotos,
                assessment_photo_count = arrayLen(photoData.assessmentPhotos)
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "getAssessmentWoundsVitals",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load wounds/vitals: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Read first non-empty string from a mobile payload struct (case-insensitive keys) --->
    <cffunction name="getMobilePayloadString" access="private" returntype="string" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfargument name="keys" type="string" required="true" />
        <cfset var k = "" />
        <cfset var v = "" />
        <cfloop list="#arguments.keys#" index="k">
            <cfset v = trim("" & getMobileStructValueCI(arguments.item, k, "")) />
            <cfif len(v)><cfreturn v /></cfif>
        </cfloop>
        <cfreturn "" />
    </cffunction>

    <!--- Build Reading1 from reading or reading_primary/reading_secondary --->
    <cffunction name="buildMobileVitalReadingFromPayload" access="private" returntype="string" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfset var reading = getMobilePayloadString(arguments.item, "reading,Reading,Reading1") />
        <cfset var readingPrimary = "" />
        <cfset var readingSecondary = "" />
        <cfif len(reading)><cfreturn reading /></cfif>
        <cfset readingPrimary = getMobilePayloadString(arguments.item, "reading_primary,readingPrimary") />
        <cfset readingSecondary = getMobilePayloadString(arguments.item, "reading_secondary,readingSecondary") />
        <cfif len(readingPrimary) AND len(readingSecondary)>
            <cfreturn readingPrimary & "." & readingSecondary />
        </cfif>
        <cfif len(readingPrimary)><cfreturn readingPrimary /></cfif>
        <cfreturn readingSecondary />
    </cffunction>

    <!--- Combine descriptor + free-text notes for pWoundsVitals.Notes --->
    <cffunction name="buildMobileVitalNotesFromPayload" access="private" returntype="string" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfset var descriptor = getMobilePayloadString(arguments.item, "descriptor,Descriptor") />
        <cfset var notes = getMobilePayloadString(arguments.item, "notes,Notes") />
        <cfif len(descriptor) AND len(notes)>
            <cfreturn descriptor & chr(10) & notes />
        </cfif>
        <cfif len(descriptor)><cfreturn descriptor /></cfif>
        <cfreturn notes />
    </cffunction>

    <!--- Map wound type text to Pathway_ID (pwoundvital.cfc / whisper_ai pattern) --->
    <cffunction name="resolveMobileWoundPathwayIdFromType" access="private" returntype="numeric" output="false">
        <cfargument name="woundtype" type="string" required="true" />
        <cfset var wt = trim(arguments.woundtype) />
        <cfif NOT len(wt)><cfreturn 32 /></cfif>
        <cfif findNoCase("1 -", wt)><cfreturn 30 /></cfif>
        <cfif findNoCase("2 -", wt)><cfreturn 34 /></cfif>
        <cfif findNoCase("3 -", wt)><cfreturn 171 /></cfif>
        <cfif findNoCase("4 -", wt)><cfreturn 172 /></cfif>
        <cfif findNoCase("Unstageable due to", wt)><cfreturn 173 /></cfif>
        <cfif findNoCase("Abrasion", wt)><cfreturn 174 /></cfif>
        <cfif findNoCase("Bite", wt)><cfreturn 175 /></cfif>
        <cfif findNoCase("Burn", wt)><cfreturn 176 /></cfif>
        <cfif findNoCase("Diabetic Ulcer", wt)><cfreturn 177 /></cfif>
        <cfif findNoCase("Laceration", wt)><cfreturn 178 /></cfif>
        <cfif findNoCase("Puncture", wt)><cfreturn 179 /></cfif>
        <cfif findNoCase("Blister", wt)><cfreturn 180 /></cfif>
        <cfif findNoCase("Open Wound", wt)><cfreturn 181 /></cfif>
        <cfif findNoCase("Surgical", wt)><cfreturn 38 /></cfif>
        <cfif findNoCase("Stasis", wt)><cfreturn 39 /></cfif>
        <cfif findNoCase("Tracheost", wt)><cfreturn 1 /></cfif>
        <cfif findNoCase("GI Ost", wt)><cfreturn 52 /></cfif>
        <cfif findNoCase("GU Ost", wt)><cfreturn 58 /></cfif>
        <cfreturn 32 />
    </cffunction>

    <!--- Progress_files S3 credentials (same bucket as patientadmin_new wound uploads) --->
    <cffunction name="ensureMobileProgressFilesS3Credentials" access="private" returntype="void" output="false">
        <cfif isDefined("Request.bucket_accesskey") AND len(trim(Request.bucket_accesskey))
            AND isDefined("Request.bucket_secretkey") AND len(trim(Request.bucket_secretkey))
            AND isDefined("Request.bucket_name") AND len(trim(Request.bucket_name))>
            <cfreturn />
        </cfif>
        <cfif CGI.SERVER_NAME EQ "secure.myhomecarebiz.com">
            <cfset Request.bucket_accesskey = "AKIA435RTMNDOUH4J3YW" />
            <cfset Request.bucket_secretkey = "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" />
            <cfset Request.bucket_name = "myhomecarebiz" />
        <cfelse>
            <cfset Request.bucket_accesskey = "AKIA435RTMNDALRCL4UV" />
            <cfset Request.bucket_secretkey = "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" />
            <cfset Request.bucket_name = "stage-myhomecarebiz" />
        </cfif>
    </cffunction>

    <cffunction name="buildMobileProgressFilesS3Url" access="private" returntype="string" output="false">
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="dupFilename" type="string" required="true" />
        <cfset var path = "Progress_files/agency_" & val(arguments.locId) />
        <cfset ensureMobileProgressFilesS3Credentials() />
        <cfreturn "s3://" & Request.bucket_accesskey & ":" & Request.bucket_secretkey & "@" & Request.bucket_name & "/" & path & "/" & arguments.dupFilename />
    </cffunction>

    <cffunction name="putMobileProgressFileOnS3" access="private" returntype="void" output="false">
        <cfargument name="localSourcePath" type="string" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="dupFilename" type="string" required="true" />
        <cfset var path = "Progress_files/agency_" & val(arguments.locId) />
        <cfset var s3Dir = "" />
        <cfset var s3Url = "" />
        <cfset var fileContent = "" />

        <cfset ensureMobileProgressFilesS3Credentials() />
        <cfset s3Dir = "s3://" & Request.bucket_accesskey & ":" & Request.bucket_secretkey & "@" & Request.bucket_name & "/" & path />
        <cfset s3Url = s3Dir & "/" & arguments.dupFilename />

        <cfif NOT fileExists(arguments.localSourcePath)>
            <cfthrow type="MobileWoundPhoto" message="Local wound photo missing before S3 upload: #arguments.localSourcePath#" />
        </cfif>

        <!--- Prefer cffile move (same as SOC_new.cfm). Do not cfdirectory/create on S3 — may 409. --->
        <cftry>
            <cffile action="move"
                    source="#arguments.localSourcePath#"
                    destination="#s3Url#" />
            <cfreturn />
            <cfcatch type="any">
                <!--- Fallback: readBinary + fileWrite --->
                <cffile action="readBinary" file="#arguments.localSourcePath#" variable="fileContent" />
                <cfscript>
                    fileWrite(s3Url, fileContent);
                </cfscript>
                <cfif fileExists(arguments.localSourcePath)>
                    <cffile action="delete" file="#arguments.localSourcePath#" />
                </cfif>
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="buildMobileWoundPhotoViewPath" access="private" returntype="string" output="false">
        <cfargument name="pgFileId" type="numeric" required="true" />
        <cfif val(arguments.pgFileId) LTE 0>
            <cfreturn "" />
        </cfif>
        <cfreturn "/patientadmin_new/Progress-File-Viewer.cfm?id=" & val(arguments.pgFileId) />
    </cffunction>

    <cffunction name="parseMobileWoundIdFromPgFileNotes" access="private" returntype="numeric" output="false">
        <cfargument name="notes" type="string" required="false" default="" />
        <cfset var n = trim(arguments.notes) />
        <cfif reFindNoCase("^wound_id:\s*\d+", n)>
            <cfreturn val(reReplace(n, "^wound_id:\s*", "", "one")) />
        </cfif>
        <cfreturn 0 />
    </cffunction>

    <cffunction name="formatMobileWoundPhotoRecord" access="private" returntype="struct" output="false">
        <cfargument name="photoQuery" type="query" required="true" />
        <cfargument name="rowIndex" type="numeric" required="true" />
        <cfset var woundId = parseMobileWoundIdFromPgFileNotes(trim("" & photoQuery.Notes[rowIndex])) />
        <cfreturn {
            id = toJsonInt(photoQuery.id[rowIndex]),
            file_name = trim("" & photoQuery.FileName[rowIndex]),
            file_type = trim("" & photoQuery.FileType[rowIndex]),
            dup_file_name = trim("" & photoQuery.Dup_file_name[rowIndex]),
            view_path = buildMobileWoundPhotoViewPath(photoQuery.id[rowIndex]),
            wound_id = toJsonInt(woundId),
            notes = trim("" & photoQuery.Notes[rowIndex]),
            date_create = photoQuery.Created[rowIndex],
            created_by = toJsonInt(photoQuery.CreatedBy[rowIndex])
        } />
    </cffunction>

    <cffunction name="loadMobileAssessmentWoundPhotos" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfset var out = { byWoundId = {}, assessmentPhotos = [] } />
        <cfset var qPhotos = "" />
        <cfset var i = 0 />
        <cfset var photo = {} />
        <cfset var woundId = 0 />
        <cfset var woundKey = "" />

        <cfif NOT len(trim(arguments.agencySchema)) OR arguments.patientId LTE 0 OR arguments.assmtId LTE 0>
            <cfreturn out />
        </cfif>

        <cfquery name="qPhotos" datasource="#Application.DataSrc#">
            SELECT id, FileName, FileType, Dup_file_name, Notes, Created, CreatedBy
            FROM #arguments.agencySchema#.pg_files
            WHERE Agency_Id = <cfqueryparam value="#arguments.locId#" cfsqltype="cf_sql_varchar">
              AND Patient_Id = <cfqueryparam value="#arguments.patientId#" cfsqltype="cf_sql_varchar">
              AND Schedule_ID = <cfqueryparam value="#arguments.assmtId#" cfsqltype="cf_sql_varchar">
              AND Title = <cfqueryparam value="Wounds_assessments" cfsqltype="cf_sql_varchar">
              AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
            ORDER BY id
        </cfquery>

        <cfloop from="1" to="#qPhotos.recordCount#" index="i">
            <cfset photo = formatMobileWoundPhotoRecord(qPhotos, i) />
            <cfset woundId = photo.wound_id />
            <cfif woundId GT 0>
                <cfset woundKey = "" & woundId />
                <cfif NOT structKeyExists(out.byWoundId, woundKey)>
                    <cfset out.byWoundId[woundKey] = [] />
                </cfif>
                <cfset arrayAppend(out.byWoundId[woundKey], photo) />
            <cfelse>
                <cfset arrayAppend(out.assessmentPhotos, photo) />
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <cffunction name="attachMobileWoundPhotosToWounds" access="private" returntype="array" output="false">
        <cfargument name="wounds" type="array" required="true" />
        <cfargument name="photoData" type="struct" required="true" />
        <cfset var out = [] />
        <cfset var row = {} />
        <cfset var woundKey = "" />
        <cfset var i = 0 />

        <cfloop from="1" to="#arrayLen(arguments.wounds)#" index="i">
            <cfset row = duplicate(arguments.wounds[i]) />
            <cfset row.photos = [] />
            <cfif structKeyExists(row, "id") AND val(row.id) GT 0>
                <cfset woundKey = "" & val(row.id) />
                <cfif structKeyExists(arguments.photoData.byWoundId, woundKey)>
                    <cfset row.photos = arguments.photoData.byWoundId[woundKey] />
                </cfif>
            </cfif>
            <cfset arrayAppend(out, row) />
        </cfloop>
        <cfreturn out />
    </cffunction>

    <cffunction name="normalizeMobileWoundPhotoBase64" access="private" returntype="string" output="false">
        <cfargument name="raw" type="string" required="true" />
        <cfset var b64 = trim(arguments.raw) />
        <cfset var commaPos = 0 />
        <cfif NOT len(b64) OR lCase(b64) EQ "null">
            <cfreturn "" />
        </cfif>
        <cfif left(lCase(b64), 5) EQ "data:">
            <cfset commaPos = find(",", b64) />
            <cfif commaPos GT 0>
                <cfset b64 = mid(b64, commaPos + 1, len(b64) - commaPos) />
            </cfif>
        </cfif>
        <cfreturn trim(b64) />
    </cffunction>

    <cffunction name="extractMobileWoundPhotosFromItem" access="private" returntype="array" output="false">
        <cfargument name="woundItem" type="struct" required="true" />
        <cfset var photos = [] />
        <cfset var photoKeys = "photos,images,Photos,Images" />
        <cfset var keyName = "" />
        <cfset var candidate = "" />
        <cfset var singlePhoto = "" />
        <cfset var out = [] />
        <cfset var i = 0 />
        <cfset var p = {} />
        <cfset var b64 = "" />
        <cfset var fileName = "" />
        <cfset var foundPhotos = false />

        <cfloop list="#photoKeys#" index="keyName">
            <cfset candidate = getMobileStructValueCI(arguments.woundItem, keyName, "") />
            <cfif isArray(candidate)>
                <cfset photos = candidate />
                <cfset foundPhotos = true />
                <cfbreak />
            </cfif>
        </cfloop>
        <cfif NOT foundPhotos>
            <cfset photos = [] />
        </cfif>

        <cfset singlePhoto = getMobileStructValueCI(arguments.woundItem, "photo", "") />
        <cfif NOT isStruct(singlePhoto) AND NOT (isSimpleValue(singlePhoto) AND len(trim("" & singlePhoto)))>
            <cfset singlePhoto = getMobileStructValueCI(arguments.woundItem, "image", "") />
        </cfif>

        <cfif isStruct(singlePhoto)>
            <cfset arrayPrepend(photos, singlePhoto) />
        <cfelseif isSimpleValue(singlePhoto) AND len(trim("" & singlePhoto))>
            <cfset arrayPrepend(photos, { file_base64 = trim("" & singlePhoto), file_name = "wound_photo.jpg" }) />
        </cfif>

        <cfif NOT isArray(photos) OR arrayLen(photos) EQ 0>
            <cfreturn out />
        </cfif>

        <cfloop from="1" to="#arrayLen(photos)#" index="i">
            <cfif isStruct(photos[i])>
                <cfset p = photos[i] />
                <cfset b64 = normalizeMobileWoundPhotoBase64(getMobilePayloadString(p, "file_base64,fileBase64,base64,data,image,photo")) />
                <cfif len(b64)>
                    <cfset fileName = getMobilePayloadString(p, "file_name,fileName,name,filename") />
                    <cfif NOT len(fileName)>
                        <cfset fileName = "wound_photo_" & i & ".jpg" />
                    </cfif>
                    <cfset arrayAppend(out, { file_base64 = b64, file_name = fileName }) />
                </cfif>
            <cfelseif isSimpleValue(photos[i])>
                <cfset b64 = normalizeMobileWoundPhotoBase64(trim("" & photos[i])) />
                <cfif len(b64)>
                    <cfset arrayAppend(out, { file_base64 = b64, file_name = "wound_photo_" & i & ".jpg" }) />
                </cfif>
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <!--- Stage ALL files from one multipart field (uploadall), fall back to single upload --->
    <cffunction name="receiveMobileMultipartAllFilesToTemp" access="private" returntype="array" output="false">
        <cfargument name="tempPath" type="string" required="true" />
        <cfargument name="fileFieldName" type="string" required="true" />
        <cfset var out = [] />
        <cfset var resolvedName = resolveMobileFormFieldName(arguments.fileFieldName) />
        <cfset var i = 0 />
        <cfset var entry = {} />
        <cfset var single = {} />
        <cfset var row = {} />
        <cfset var contentType = "application/octet-stream" />
        <cfset var contentSubType = "octet-stream" />

        <cfif NOT len(resolvedName)>
            <cfreturn out />
        </cfif>

        <cftry>
            <cfparam name="File.UPLOADALLRESULTS" default="#[]#" />
            <cffile action="uploadall"
                    filefield="form.#resolvedName#"
                    destination="#arguments.tempPath#"
                    nameconflict="makeunique"
                    mode="666" />
            <cfif isDefined("File.UPLOADALLRESULTS") AND isArray(File.UPLOADALLRESULTS) AND arrayLen(File.UPLOADALLRESULTS) GT 0>
                <cfloop from="1" to="#arrayLen(File.UPLOADALLRESULTS)#" index="i">
                    <cfset row = File.UPLOADALLRESULTS[i] />
                    <cfif NOT isStruct(row) OR NOT structKeyExists(row, "serverFile") OR NOT len(trim(row.serverFile))>
                        <cfcontinue />
                    </cfif>
                    <cfset contentType = "application/octet-stream" />
                    <cfset contentSubType = "octet-stream" />
                    <cfif structKeyExists(row, "contentType") AND len(trim(row.contentType))>
                        <cfset contentType = trim(row.contentType) />
                    </cfif>
                    <cfif structKeyExists(row, "contentSubType") AND len(trim(row.contentSubType))>
                        <cfset contentSubType = trim(row.contentSubType) />
                    </cfif>
                    <cfset arrayAppend(out, {
                        ok = true,
                        message = "",
                        serverFile = row.serverFile,
                        clientFile = structKeyExists(row, "clientFile") AND len(trim(row.clientFile)) ? row.clientFile : row.serverFile,
                        contentType = contentType,
                        contentSubType = contentSubType,
                        uploadMethod = "multipart:uploadall:" & resolvedName
                    }) />
                </cfloop>
                <cfif arrayLen(out) GT 0>
                    <cfreturn out />
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>

        <cfset single = receiveWhisperFileToTemp(
            tempPath = arguments.tempPath,
            fileFieldNameOverride = resolvedName
        ) />
        <cfif single.ok>
            <cfset arrayAppend(out, single) />
        </cfif>
        <cfreturn out />
    </cffunction>

    <cffunction name="stageMobileWoundMultipartPhotos" access="private" returntype="struct" output="false">
        <cfargument name="tempPath" type="string" required="true" />
        <cfset var out = { byWoundIndex = {}, assessmentFields = [] } />
        <cfset var fieldNamesList = listMobileMultipartFormFieldNames() />
        <cfset var fieldName = "" />
        <cfset var resolvedName = "" />
        <cfset var woundIdx = 0 />
        <cfset var photoIdx = 0 />
        <cfset var woundKey = "" />
        <cfset var stagedList = [] />
        <cfset var staged = {} />
        <cfset var stagedKey = "" />
        <cfset var stagedFields = {} />
        <cfset var i = 0 />

        <cfif NOT len(fieldNamesList)>
            <cfreturn out />
        </cfif>

        <cfloop list="#fieldNamesList#" index="fieldName">
            <cfset resolvedName = resolveMobileFormFieldName(fieldName) />
            <cfset stagedKey = lCase(resolvedName) />
            <cfif structKeyExists(stagedFields, stagedKey)>
                <cfcontinue />
            </cfif>
            <cfif NOT (
                reFindNoCase("^wound_(\d+)_photo_(\d+)$", resolvedName)
                OR reFindNoCase("^wound_photo_(\d+)$", resolvedName)
                OR reFindNoCase("^wound_(\d+)_photos$", resolvedName)
                OR compareNoCase(resolvedName, "ufFile") EQ 0
                OR compareNoCase(resolvedName, "wound_photos") EQ 0
                OR compareNoCase(resolvedName, "wound_images") EQ 0
            )>
                <cfcontinue />
            </cfif>

            <cfset stagedFields[stagedKey] = true />
            <cfset stagedList = receiveMobileMultipartAllFilesToTemp(
                tempPath = arguments.tempPath,
                fileFieldName = resolvedName
            ) />
            <cfif arrayLen(stagedList) EQ 0>
                <cfcontinue />
            </cfif>

            <cfif reFindNoCase("^wound_(\d+)_photo_(\d+)$", resolvedName) OR reFindNoCase("^wound_(\d+)_photos$", resolvedName)>
                <cfif reFindNoCase("^wound_(\d+)_photo_(\d+)$", resolvedName)>
                    <cfset woundIdx = val(reReplace(resolvedName, "^wound_(\d+)_photo_(\d+)$", "\1", "one")) + 1 />
                    <cfset photoIdx = val(reReplace(resolvedName, "^wound_(\d+)_photo_(\d+)$", "\2", "one")) />
                <cfelse>
                    <cfset woundIdx = val(reReplace(resolvedName, "^wound_(\d+)_photos$", "\1", "one")) + 1 />
                    <cfset photoIdx = 0 />
                </cfif>
                <cfset woundKey = "" & woundIdx />
                <cfif NOT structKeyExists(out.byWoundIndex, woundKey)>
                    <cfset out.byWoundIndex[woundKey] = [] />
                </cfif>
                <cfloop from="1" to="#arrayLen(stagedList)#" index="i">
                    <cfset staged = stagedList[i] />
                    <cfif NOT staged.ok>
                        <cfcontinue />
                    </cfif>
                    <cfset arrayAppend(out.byWoundIndex[woundKey], {
                        fieldName = resolvedName,
                        photoIndex = photoIdx + i - 1,
                        staged = staged
                    }) />
                </cfloop>
            <cfelse>
                <cfset photoIdx = compareNoCase(resolvedName, "ufFile") EQ 0 ? 0 : (
                    reFindNoCase("^wound_photo_(\d+)$", resolvedName)
                    ? val(reReplace(resolvedName, "^wound_photo_(\d+)$", "\1", "one"))
                    : 0
                ) />
                <cfloop from="1" to="#arrayLen(stagedList)#" index="i">
                    <cfset staged = stagedList[i] />
                    <cfif NOT staged.ok>
                        <cfcontinue />
                    </cfif>
                    <cfset arrayAppend(out.assessmentFields, {
                        fieldName = resolvedName,
                        photoIndex = photoIdx + i - 1,
                        staged = staged
                    }) />
                </cfloop>
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <cffunction name="collectMobileMultipartWoundPhotoFields" access="private" returntype="struct" output="false">
        <cfargument name="tempPath" type="string" required="false" default="/var/www/datafiles/patientfileupload/" />
        <cfreturn stageMobileWoundMultipartPhotos(tempPath = arguments.tempPath) />
    </cffunction>

    <cffunction name="resolveMobileWoundRowIdForPhotos" access="private" returntype="numeric" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="woundItem" type="struct" required="true" />
        <cfargument name="rowResult" type="struct" required="false" default="#{ id = 0, item_id = 0 }#" />
        <cfset var rowId = val(arguments.rowResult.id) />
        <cfset var itemId = val(arguments.rowResult.item_id) />
        <cfset var qWound = "" />

        <cfif rowId GT 0>
            <cfreturn rowId />
        </cfif>
        <cfif itemId LTE 0>
            <cfset itemId = val(getMobilePayloadString(arguments.woundItem, "item_id,Item_ID,itemId")) />
        </cfif>
        <cfif rowId LTE 0>
            <cfset rowId = val(getMobilePayloadString(arguments.woundItem, "id,ID,Woundid,Wound_ID")) />
        </cfif>
        <cfif rowId GT 0>
            <cfreturn rowId />
        </cfif>
        <cfif itemId GT 0>
            <cfquery name="qWound" datasource="#Application.DataSrc#">
                SELECT ID
                FROM #arguments.agencySchema#.pWoundsVitals
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                  AND Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#itemId#">
                LIMIT 1
            </cfquery>
            <cfif qWound.recordCount GT 0>
                <cfreturn val(qWound.ID) />
            </cfif>
        </cfif>
        <cfreturn 0 />
    </cffunction>

    <cffunction name="saveMobileWoundPhotoToPgFiles" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="woundId" type="numeric" required="false" default="0" />
        <cfargument name="tempPath" type="string" required="true" />
        <cfargument name="serverFile" type="string" required="true" />
        <cfargument name="clientFile" type="string" required="true" />
        <cfargument name="contentType" type="string" required="false" default="application/octet-stream" />
        <cfargument name="contentSubType" type="string" required="false" default="octet-stream" />
        <cfset var result = { ok = false, message = "", photo = {}, sql_detail = "" } />
        <cfset var safeClient = reReplace(trim(arguments.clientFile), "[^A-Za-z0-9._-]", "_", "all") />
        <cfset var shortId = "" />
        <cfset var dupFileName = "" />
        <cfset var fileType = "" />
        <cfset var notesVal = "" />
        <cfset var insertResult = {} />
        <cfset var pgFileId = 0 />
        <cfset var qLastPhoto = "" />
        <cfset var localPath = "" />
        <cfset var fileBlob = "" />

        <cfif NOT len(safeClient)>
            <cfset safeClient = "wound_photo.jpg" />
        </cfif>
        <!--- Keep Dup_file_name short (same pattern as SOC_new.cfm); avoid oversized UUID filenames --->
        <cfset shortId = right(reReplace(createUUID(), "[^A-Za-z0-9]", "", "all"), 8) />
        <cfset dupFileName = dateFormat(now(), "YYMMDDHHMMSS") & shortId & "_" & safeClient />
        <cfif len(dupFileName) GT 120>
            <cfset dupFileName = left(dupFileName, 120) />
        </cfif>
        <cfset fileType = trim(arguments.contentType) & "/" & trim(arguments.contentSubType) />
        <cfif fileType EQ "/" OR NOT len(trim(arguments.contentType))>
            <cfset fileType = "image/jpeg" />
        </cfif>
        <cfset notesVal = arguments.woundId GT 0 ? ("wound_id:" & val(arguments.woundId)) : "" />
        <cfset localPath = arguments.tempPath & arguments.serverFile />

        <cfif NOT fileExists(localPath)>
            <cfset result.message = "temp_file_missing:" & localPath />
            <cfreturn result />
        </cfif>

        <cftry>
            <!--- pg_files.File is NOT NULL on some agencies; read before S3 move removes the temp file --->
            <cffile action="readBinary" file="#localPath#" variable="fileBlob" />

            <cfset putMobileProgressFileOnS3(
                localSourcePath = localPath,
                locId = arguments.locId,
                dupFilename = dupFileName
            ) />
            <cfif fileExists(localPath)>
                <cffile action="delete" file="#localPath#" />
            </cfif>

            <!--- Include File blob — required (no default) on agency pg_files tables --->
            <cfquery datasource="#Application.DataSrc#" result="insertResult">
                INSERT INTO #arguments.agencySchema#.pg_files (
                    Agency_Id, Patient_Id, Schedule_ID, Title, FileName, FileType, Dup_file_name, File, CreatedBy
                ) VALUES (
                    <cfqueryparam value="#val(arguments.locId)#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#val(arguments.patientId)#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#val(arguments.assmtId)#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="Wounds_assessments" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#safeClient#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#fileType#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#dupFileName#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#fileBlob#" cfsqltype="cf_sql_blob">,
                    <cfqueryparam value="#val(arguments.empId)#" cfsqltype="cf_sql_varchar">
                )
            </cfquery>
            <cfif structKeyExists(insertResult, "GENERATED_KEY") AND val(insertResult.GENERATED_KEY) GT 0>
                <cfset pgFileId = val(insertResult.GENERATED_KEY) />
            <cfelseif structKeyExists(insertResult, "generatedKey") AND val(insertResult.generatedKey) GT 0>
                <cfset pgFileId = val(insertResult.generatedKey) />
            </cfif>
            <cfif pgFileId LTE 0>
                <cfquery name="qLastPhoto" datasource="#Application.DataSrc#">
                    SELECT id
                    FROM #arguments.agencySchema#.pg_files
                    WHERE Agency_Id = <cfqueryparam value="#val(arguments.locId)#" cfsqltype="cf_sql_varchar">
                      AND Patient_Id = <cfqueryparam value="#val(arguments.patientId)#" cfsqltype="cf_sql_varchar">
                      AND Schedule_ID = <cfqueryparam value="#val(arguments.assmtId)#" cfsqltype="cf_sql_varchar">
                      AND Dup_file_name = <cfqueryparam value="#dupFileName#" cfsqltype="cf_sql_varchar">
                    ORDER BY id DESC
                    LIMIT 1
                </cfquery>
                <cfif qLastPhoto.recordCount GT 0>
                    <cfset pgFileId = val(qLastPhoto.id) />
                </cfif>
            </cfif>

            <cfif pgFileId GT 0 AND len(notesVal)>
                <cftry>
                    <cfquery datasource="#Application.DataSrc#">
                        UPDATE #arguments.agencySchema#.pg_files
                        SET Notes = <cfqueryparam value="#notesVal#" cfsqltype="cf_sql_varchar">
                        WHERE id = <cfqueryparam value="#pgFileId#" cfsqltype="cf_sql_integer">
                    </cfquery>
                    <cfcatch type="any"></cfcatch>
                </cftry>
            </cfif>

            <cfset result.ok = true />
            <cfset result.photo = {
                id = toJsonInt(pgFileId),
                file_name = safeClient,
                file_type = fileType,
                dup_file_name = dupFileName,
                view_path = buildMobileWoundPhotoViewPath(pgFileId),
                wound_id = toJsonInt(arguments.woundId)
            } />
            <cfcatch type="any">
                <cfif fileExists(localPath)>
                    <cffile action="delete" file="#localPath#" />
                </cfif>
                <cfset result.message = trim(cfcatch.message) />
                <cfif len(trim(cfcatch.detail))>
                    <cfset result.message = result.message & " | " & trim(cfcatch.detail) />
                    <cfset result.sql_detail = trim(cfcatch.detail) />
                </cfif>
                <cfif structKeyExists(cfcatch, "sqlstate") AND len(trim(cfcatch.sqlstate))>
                    <cfset result.message = result.message & " | sqlstate=" & trim(cfcatch.sqlstate) />
                </cfif>
                <cftry>
                    <cflog file="mobile_wound_photos" type="error" text="pg_files insert failed schema=#arguments.agencySchema# assmt=#arguments.assmtId# patient=#arguments.patientId# loc=#arguments.locId# file=#dupFileName# msg=#result.message#" />
                    <cfcatch type="any"></cfcatch>
                </cftry>
            </cfcatch>
        </cftry>
        <cfreturn result />
    </cffunction>

    <cffunction name="processMobileWoundItemPhotos" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="woundIndex" type="numeric" required="true" />
        <cfargument name="woundItem" type="struct" required="true" />
        <cfargument name="woundId" type="numeric" required="false" default="0" />
        <cfargument name="tempPath" type="string" required="true" />
        <cfargument name="multipartPhotoFields" type="struct" required="false" default="#{}#" />
        <cfset var out = { uploaded = 0, skipped = [], photos = [], woundId = val(arguments.woundId) } />
        <cfset var jsonPhotos = extractMobileWoundPhotosFromItem(arguments.woundItem) />
        <cfset var woundKey = "" & val(arguments.woundIndex) />
        <cfset var woundKeyZero = "" & (val(arguments.woundIndex) - 1) />
        <cfset var multipartFields = [] />
        <cfset var i = 0 />
        <cfset var p = {} />
        <cfset var receiveResult = {} />
        <cfset var saveResult = {} />
        <cfset var b64 = "" />
        <cfset var origName = "" />
        <cfset var fieldEntry = {} />

        <cfif NOT directoryExists(arguments.tempPath)>
            <cfdirectory action="create" directory="#arguments.tempPath#" />
        </cfif>

        <cfif structKeyExists(arguments.multipartPhotoFields, "byWoundIndex")>
            <cfif structKeyExists(arguments.multipartPhotoFields.byWoundIndex, woundKey)>
                <cfset multipartFields = arguments.multipartPhotoFields.byWoundIndex[woundKey] />
            <cfelseif val(arguments.woundIndex) GT 0 AND structKeyExists(arguments.multipartPhotoFields.byWoundIndex, woundKeyZero)>
                <!--- Clients may send wound_1_* as 1-based field names --->
                <cfset multipartFields = arguments.multipartPhotoFields.byWoundIndex[woundKeyZero] />
            </cfif>
        </cfif>

        <!--- First wound can also consume assessment-level ufFile / wound_photo_* uploads --->
        <cfif arrayLen(multipartFields) EQ 0
            AND val(arguments.woundIndex) EQ 1
            AND structKeyExists(arguments.multipartPhotoFields, "assessmentFields")
            AND isArray(arguments.multipartPhotoFields.assessmentFields)
            AND arrayLen(arguments.multipartPhotoFields.assessmentFields) GT 0>
            <cfset multipartFields = arguments.multipartPhotoFields.assessmentFields />
            <cfset arguments.multipartPhotoFields.assessmentFields = [] />
        </cfif>

        <cfloop from="1" to="#arrayLen(jsonPhotos)#" index="i">
            <cfset p = jsonPhotos[i] />
            <cfset b64 = getMobilePayloadString(p, "file_base64,fileBase64,base64,data") />
            <cfif NOT len(b64) AND structKeyExists(p, "file_base64")>
                <cfset b64 = trim("" & p.file_base64) />
            </cfif>
            <cfset origName = getMobilePayloadString(p, "file_name,fileName,name,filename") />
            <cfif NOT len(origName)>
                <cfset origName = "wound_photo_" & i & ".jpg" />
            </cfif>
            <cfif NOT len(b64)>
                <cfset arrayAppend(out.skipped, { source = "json", index = i, reason = "missing_base64" }) />
                <cfcontinue />
            </cfif>
            <cfset receiveResult = receiveWhisperFileToTemp(
                tempPath = arguments.tempPath,
                fileBase64 = b64,
                fileName = origName
            ) />
            <cfif NOT receiveResult.ok>
                <cfset arrayAppend(out.skipped, { source = "json", index = i, reason = receiveResult.message }) />
                <cfcontinue />
            </cfif>
            <cfset saveResult = saveMobileWoundPhotoToPgFiles(
                agencySchema = arguments.agencySchema,
                locId = arguments.locId,
                patientId = arguments.patientId,
                assmtId = arguments.assmtId,
                empId = arguments.empId,
                woundId = out.woundId,
                tempPath = arguments.tempPath,
                serverFile = receiveResult.serverFile,
                clientFile = receiveResult.clientFile,
                contentType = receiveResult.contentType,
                contentSubType = receiveResult.contentSubType
            ) />
            <cfif saveResult.ok>
                <cfset out.uploaded = out.uploaded + 1 />
                <cfset arrayAppend(out.photos, saveResult.photo) />
            <cfelse>
                <cfset arrayAppend(out.skipped, { source = "json", index = i, reason = saveResult.message }) />
            </cfif>
        </cfloop>

        <cfloop from="1" to="#arrayLen(multipartFields)#" index="i">
            <cfset fieldEntry = multipartFields[i] />
            <cfif NOT structKeyExists(fieldEntry, "staged") OR NOT isStruct(fieldEntry.staged) OR NOT fieldEntry.staged.ok>
                <cfset arrayAppend(out.skipped, { source = "multipart", index = i, reason = "staged_missing_or_failed" }) />
                <cfcontinue />
            </cfif>
            <cfset receiveResult = fieldEntry.staged />
            <cfset saveResult = saveMobileWoundPhotoToPgFiles(
                agencySchema = arguments.agencySchema,
                locId = arguments.locId,
                patientId = arguments.patientId,
                assmtId = arguments.assmtId,
                empId = arguments.empId,
                woundId = out.woundId,
                tempPath = arguments.tempPath,
                serverFile = receiveResult.serverFile,
                clientFile = receiveResult.clientFile,
                contentType = receiveResult.contentType,
                contentSubType = receiveResult.contentSubType
            ) />
            <cfif saveResult.ok>
                <cfset out.uploaded = out.uploaded + 1 />
                <cfset arrayAppend(out.photos, saveResult.photo) />
            <cfelse>
                <cfset arrayAppend(out.skipped, {
                    source = "multipart",
                    field = structKeyExists(fieldEntry, "fieldName") ? fieldEntry.fieldName : "",
                    reason = saveResult.message
                }) />
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <cffunction name="processMobileAssessmentLevelWoundPhotos" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="tempPath" type="string" required="true" />
        <cfargument name="multipartPhotoFields" type="struct" required="false" default="#{}#" />
        <cfset var out = { uploaded = 0, skipped = [], photos = [] } />
        <cfset var fields = [] />
        <cfset var i = 0 />
        <cfset var fieldEntry = {} />
        <cfset var receiveResult = {} />
        <cfset var saveResult = {} />

        <cfif structKeyExists(arguments.multipartPhotoFields, "assessmentFields")>
            <cfset fields = arguments.multipartPhotoFields.assessmentFields />
        </cfif>

        <cfloop from="1" to="#arrayLen(fields)#" index="i">
            <cfset fieldEntry = fields[i] />
            <cfif NOT structKeyExists(fieldEntry, "staged") OR NOT isStruct(fieldEntry.staged) OR NOT fieldEntry.staged.ok>
                <cfcontinue />
            </cfif>
            <cfset receiveResult = fieldEntry.staged />
            <cfset saveResult = saveMobileWoundPhotoToPgFiles(
                agencySchema = arguments.agencySchema,
                locId = arguments.locId,
                patientId = arguments.patientId,
                assmtId = arguments.assmtId,
                empId = arguments.empId,
                woundId = 0,
                tempPath = arguments.tempPath,
                serverFile = receiveResult.serverFile,
                clientFile = receiveResult.clientFile,
                contentType = receiveResult.contentType,
                contentSubType = receiveResult.contentSubType
            ) />
            <cfif saveResult.ok>
                <cfset out.uploaded = out.uploaded + 1 />
                <cfset arrayAppend(out.photos, saveResult.photo) />
            <cfelse>
                <cfset arrayAppend(out.skipped, { source = "multipart", field = fieldEntry.fieldName, reason = saveResult.message }) />
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <!--- Parse saveAssessmentWoundsVitals JSON body or form fields --->
    <cffunction name="parseMobileWoundsVitalsSaveRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="assmtIdArg" type="string" required="false" default="" />
        <cfargument name="vitalsArg" type="string" required="false" default="" />
        <cfargument name="woundsArg" type="string" required="false" default="" />
        <cfset var out = {
            securityKey = trim(arguments.securityKeyArg),
            assmtId = val(trim(arguments.assmtIdArg)),
            vitals = [],
            wounds = []
        } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var vitalsRaw = "" />
        <cfset var woundsRaw = "" />
        <cfset var isMultipartRequest = false />

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

        <cfif len(trim(arguments.vitalsArg))>
            <cfset vitalsRaw = deserializeJSON(trim(arguments.vitalsArg)) />
        <cfelseif structKeyExists(form, "vitals") AND len(trim(form.vitals))>
            <cfset vitalsRaw = deserializeJSON(trim(form.vitals)) />
        </cfif>
        <cfif len(trim(arguments.woundsArg))>
            <cfset woundsRaw = deserializeJSON(trim(arguments.woundsArg)) />
        <cfelseif structKeyExists(form, "wounds") AND len(trim(form.wounds))>
            <cfset woundsRaw = deserializeJSON(trim(form.wounds)) />
        </cfif>

        <cftry>
            <cfset isMultipartRequest = findNoCase("multipart/form-data", cgi.content_type) GT 0 />
            <cfif NOT isMultipartRequest AND (NOT len(out.securityKey) OR out.assmtId LTE 0 OR (NOT isArray(vitalsRaw) AND NOT isArray(woundsRaw)))>
                <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 = trim("" & getMobileStructValueCI(bodyJson, "securityKey", "")) />
                    </cfif>
                    <cfif out.assmtId LTE 0>
                        <cfset out.assmtId = val(trim("" & getMobileStructValueCI(bodyJson, "Assmt_ID", 0))) />
                    </cfif>
                    <cfif NOT isArray(vitalsRaw)>
                        <cfset vitalsRaw = getMobileStructValueCI(bodyJson, "vitals", []) />
                    </cfif>
                    <cfif NOT isArray(woundsRaw)>
                        <cfset woundsRaw = getMobileStructValueCI(bodyJson, "wounds", []) />
                    </cfif>
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>

        <cfif isArray(vitalsRaw)>
            <cfset out.vitals = vitalsRaw />
        </cfif>
        <cfif isArray(woundsRaw)>
            <cfset out.wounds = woundsRaw />
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Normalize one vital payload item into pWoundsVitals fields --->
    <cffunction name="normalizeMobileVitalPayload" access="private" returntype="struct" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="lookupDb" type="string" required="true" />
        <cfset var out = {
            ok = false,
            rowId = val(getMobilePayloadString(arguments.item, "id,ID")),
            itemId = val(getMobilePayloadString(arguments.item, "item_id,Item_ID,itemId")),
            description = getMobilePayloadString(arguments.item, "description,Description"),
            reading1 = buildMobileVitalReadingFromPayload(arguments.item),
            notes = buildMobileVitalNotesFromPayload(arguments.item),
            pain_location = getMobilePayloadString(arguments.item, "pain_location,painLocation"),
            pain_duration = getMobilePayloadString(arguments.item, "pain_duration,painDuration"),
            pain_exacerbate = getMobilePayloadString(arguments.item, "pain_exacerbate,painExacerbate"),
            pain_relief = getMobilePayloadString(arguments.item, "pain_relief,painRelief"),
            pain_med_effectiveness = getMobilePayloadString(arguments.item, "pain_med_effectiveness,painMedEffectiveness")
        } />
        <cfset var qVitalDesc = "" />
        <cfset var qVitalParam = "" />

        <cfif NOT len(out.description) AND out.itemId GT 0>
            <cfquery name="qVitalDesc" datasource="#Application.DataSrc#">
                SELECT MAX(vl.Description) AS Description
                FROM #arguments.lookupDb#.pValue_Lookup vl
                WHERE vl.Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#out.itemId#">
                  AND vl.Description <> <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            </cfquery>
            <cfif qVitalDesc.recordCount GT 0>
                <cfset out.description = trim("" & qVitalDesc.Description) />
            </cfif>
        </cfif>
        <cfif NOT len(out.description) AND out.itemId GT 0 AND len(trim(arguments.agencySchema))>
            <cfquery name="qVitalParam" datasource="#Application.DataSrc#">
                SELECT Vital_Sign
                FROM #arguments.agencySchema#.pVital_Param
                WHERE Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#out.itemId#">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                LIMIT 1
            </cfquery>
            <cfif qVitalParam.recordCount GT 0>
                <cfset out.description = trim("" & qVitalParam.Vital_Sign) />
            </cfif>
        </cfif>
        <cfif NOT len(out.description) AND out.itemId LTE 0>
            <cfreturn out />
        </cfif>
        <cfif out.itemId LTE 0 AND len(out.description)>
            <cfquery name="qVitalDesc" datasource="#Application.DataSrc#">
                SELECT MAX(vl.Item_ID) AS Item_ID
                FROM #arguments.lookupDb#.pValue_Lookup vl
                WHERE vl.Description = <cfqueryparam value="#out.description#" cfsqltype="cf_sql_varchar">
                  AND vl.Description <> <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            </cfquery>
            <cfif qVitalDesc.recordCount GT 0>
                <cfset out.itemId = val(qVitalDesc.Item_ID) />
            </cfif>
        </cfif>
        <cfif len(out.reading1) OR len(out.notes)>
            <cfset out.ok = true />
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Upsert one vital row in pWoundsVitals for Assmt_ID --->
    <cffunction name="upsertMobileAssessmentVitalRow" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="vitalItem" type="struct" required="true" />
        <cfargument name="lookupDb" type="string" required="true" />
        <cfset var result = { action = "skipped", id = 0, item_id = 0, reason = "" } />
        <cfset var normalized = normalizeMobileVitalPayload(
            item = arguments.vitalItem,
            agencySchema = arguments.agencySchema,
            lookupDb = arguments.lookupDb
        ) />
        <cfset var existingVital = "" />
        <cfset var paramRow = "" />
        <cfset var paramLo = "" />
        <cfset var paramHi = "" />

        <cfif NOT normalized.ok>
            <cfset result.reason = "empty_vital_reading_or_notes" />
            <cfreturn result />
        </cfif>
        <cfif normalized.itemId LTE 0>
            <cfset result.reason = "missing_item_id_or_description" />
            <cfreturn result />
        </cfif>

        <cfquery name="paramRow" datasource="#Application.DataSrc#">
                SELECT Value_Low, Value_High
                FROM #arguments.agencySchema#.pVital_Param
                WHERE Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#normalized.itemId#">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                LIMIT 1
            </cfquery>
            <cfset paramLo = paramRow.recordCount GT 0 ? trim(paramRow.Value_Low) : "" />
            <cfset paramHi = paramRow.recordCount GT 0 ? trim(paramRow.Value_High) : "" />

        <cfif normalized.rowId GT 0>
            <cfquery name="existingVital" datasource="#Application.DataSrc#">
                SELECT ID
                FROM #arguments.agencySchema#.pWoundsVitals
                WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#normalized.rowId#">
                  AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND VitalOrWound = <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">
                LIMIT 1
            </cfquery>
        <cfelseif normalized.itemId GT 0>
            <cfquery name="existingVital" datasource="#Application.DataSrc#">
                SELECT ID
                FROM #arguments.agencySchema#.pWoundsVitals
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND VitalOrWound = <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">
                  AND Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#normalized.itemId#">
                LIMIT 1
            </cfquery>
        </cfif>

        <cfif isQuery(existingVital) AND existingVital.recordCount GT 0>
            <cfquery datasource="#Application.DataSrc#">
                UPDATE #arguments.agencySchema#.pWoundsVitals
                SET Description = <cfqueryparam value="#normalized.description#" cfsqltype="cf_sql_varchar">,
                    Reading1 = <cfqueryparam value="#normalized.reading1#" cfsqltype="cf_sql_varchar">,
                    Notes = <cfqueryparam value="#normalized.notes#" cfsqltype="cf_sql_varchar">,
                    Param_Lo = <cfqueryparam value="#paramLo#" cfsqltype="cf_sql_varchar" null="#NOT len(paramLo)#">,
                    Param_Hi = <cfqueryparam value="#paramHi#" cfsqltype="cf_sql_varchar" null="#NOT len(paramHi)#">,
                    pain_location = <cfqueryparam value="#normalized.pain_location#" cfsqltype="cf_sql_varchar">,
                    pain_duration = <cfqueryparam value="#normalized.pain_duration#" cfsqltype="cf_sql_varchar">,
                    pain_exacerbate = <cfqueryparam value="#normalized.pain_exacerbate#" cfsqltype="cf_sql_varchar">,
                    pain_relief = <cfqueryparam value="#normalized.pain_relief#" cfsqltype="cf_sql_varchar">,
                    pain_med_effectiveness = <cfqueryparam value="#normalized.pain_med_effectiveness#" cfsqltype="cf_sql_varchar">,
                    Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                    Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
                WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#existingVital.ID#">
            </cfquery>
            <cfset result.action = "updated" />
            <cfset result.id = existingVital.ID />
            <cfset result.item_id = normalized.itemId />
        <cfelse>
            <cfquery datasource="#Application.DataSrc#">
                INSERT INTO #arguments.agencySchema#.pWoundsVitals (
                    Assmt_ID, Status, Pathway_ID, Item_ID, VitalOrWound, Description,
                    Param_Lo, Param_Hi, Notes, Reading1,
                    pain_location, pain_duration, pain_exacerbate, pain_relief, pain_med_effectiveness,
                    Date_Create, Created_by, Date_Change, Change_by
                ) VALUES (
                    <cfqueryparam value="#arguments.assmtId#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#normalized.itemId#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#normalized.itemId#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="vital" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#normalized.description#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#paramLo#" cfsqltype="cf_sql_varchar" null="#NOT len(paramLo)#">,
                    <cfqueryparam value="#paramHi#" cfsqltype="cf_sql_varchar" null="#NOT len(paramHi)#">,
                    <cfqueryparam value="#normalized.notes#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#normalized.reading1#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#normalized.pain_location#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#normalized.pain_duration#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#normalized.pain_exacerbate#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#normalized.pain_relief#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#normalized.pain_med_effectiveness#" cfsqltype="cf_sql_varchar">,
                    <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" />
            <cfset result.id = 0 />
            <cfset result.item_id = normalized.itemId />
        </cfif>
        <cfreturn result />
    </cffunction>

    <!--- Normalize one wound payload item into pWoundsVitals column map --->
    <cffunction name="normalizeMobileWoundPayload" access="private" returntype="struct" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfset var out = {
            ok = false,
            action = "auto",
            rowId = val(getMobilePayloadString(arguments.item, "id,ID,Woundid,Wound_ID")),
            itemId = val(getMobilePayloadString(arguments.item, "item_id,Item_ID,itemId")),
            cols = {}
        } />
        <cfset var fieldMap = "description,Description|location,Location|other_locations,other_locations,otherLocations|length,Length|width,Width|depth,Depth|healing_stage,healingstage|healingStage,healingstage|wnd_bed,wnd_bed,woundBed,wnd_bed|wnd_margin,wnd_margin,woundMargin|wnd_exudate,wnd_exudate,woundExudate|wnd_odor,wnd_odor,woundOdor|wnd_pain,wnd_pain,woundPain|wnd_tunneling,wnd_tunneling,woundTunneling|wnd_underm,wnd_underm,woundUnderm|wnd_gran,wnd_gran,woundGran|wnd_epith,wnd_epith,woundEpith|wnd_eschar,wnd_eschar,woundEschar|wnd_slough,wnd_slough,woundSlough|wnd_fib,wnd_fib,woundFib" />
        <cfset var pair = "" />
        <cfset var payloadKeys = "" />
        <cfset var colName = "" />
        <cfset var val = "" />
        <cfset var actionRaw = lcase(trim(getMobilePayloadString(arguments.item, "action,Action"))) />
        <cfset var woundTypeBase = getMobilePayloadString(arguments.item, "wound_type,woundType,WoundType,woundtype") />
        <cfset var pressureStage = getMobilePayloadString(arguments.item, "pressure_stage,pressureStage,selectstage,stage") />
        <cfset var measurements = {} />

        <cfif listFindNoCase("insert,add,create", actionRaw)>
            <cfset out.action = "insert" />
        <cfelseif listFindNoCase("update,edit,save", actionRaw)>
            <cfset out.action = "update" />
        </cfif>

        <cfloop list="#fieldMap#" index="pair" delimiters="|">
            <cfset payloadKeys = listGetAt(pair, 1) />
            <cfset colName = listGetAt(pair, 2) />
            <cfset val = getMobilePayloadString(arguments.item, payloadKeys) />
            <cfif len(val)>
                <cfset out.cols[colName] = val />
            </cfif>
        </cfloop>

        <cfset measurements = getMobileStructValueCI(arguments.item, "measurements", {}) />
        <cfif isStruct(measurements)>
            <cfset val = getMobilePayloadString(measurements, "length,Length") />
            <cfif len(val) AND NOT structKeyExists(out.cols, "Length")><cfset out.cols.Length = val /></cfif>
            <cfset val = getMobilePayloadString(measurements, "width,Width") />
            <cfif len(val) AND NOT structKeyExists(out.cols, "Width")><cfset out.cols.Width = val /></cfif>
            <cfset val = getMobilePayloadString(measurements, "depth,Depth") />
            <cfif len(val) AND NOT structKeyExists(out.cols, "Depth")><cfset out.cols.Depth = val /></cfif>
        </cfif>

        <cfif len(pressureStage)>
            <cfset out.cols.woundtype = pressureStage />
        <cfelseif len(woundTypeBase)>
            <cfset out.cols.woundtype = woundTypeBase />
        </cfif>

        <cfif structCount(out.cols) EQ 0>
            <cfreturn out />
        </cfif>
        <cfif out.action EQ "update" AND out.rowId LTE 0 AND out.itemId LTE 0>
            <cfreturn out />
        </cfif>
        <cfset out.ok = true />
        <cfreturn out />
    </cffunction>

    <!--- Next wound Item_ID for insert (pwoundvital.cfc addEditWounds pattern) --->
    <cffunction name="resolveMobileWoundItemIdForInsert" access="private" returntype="numeric" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="woundtype" type="string" required="false" default="" />
        <cfargument name="requestedItemId" type="numeric" required="false" default="0" />
        <cfset var getpwound = "" />
        <cfset var getwounds = "" />
        <cfset var wounditemid = 201 />

        <cfif arguments.requestedItemId GT 0>
            <cfreturn arguments.requestedItemId />
        </cfif>

        <cfquery name="getpwound" datasource="#Application.DataSrc#">
            SELECT ID, Item_ID, woundtype
            FROM #arguments.agencySchema#.pWoundsVitals
            WHERE VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
              AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            ORDER BY Pathway_ID DESC, Item_ID DESC
        </cfquery>

        <cfif getpwound.recordCount EQ 0>
            <cfreturn wounditemid />
        </cfif>

        <cfif len(trim(arguments.woundtype))>
            <cfloop query="getpwound">
                <cfif compareNoCase(trim(getpwound.woundtype), trim(arguments.woundtype)) EQ 0>
                    <cfreturn val(getpwound.Item_ID) />
                </cfif>
            </cfloop>
        </cfif>

        <cfreturn val(getpwound.Item_ID) + 1 />
    </cffunction>

    <!--- Upsert one wound row in pWoundsVitals for Assmt_ID (insert new or update existing) --->
    <cffunction name="upsertMobileAssessmentWoundRow" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="woundItem" type="struct" required="true" />
        <cfset var result = { action = "skipped", id = 0, item_id = 0, reason = "" } />
        <cfset var normalized = normalizeMobileWoundPayload(item = arguments.woundItem) />
        <cfset var existingWound = "" />
        <cfset var wounditemid = 201 />
        <cfset var woundpathid = 32 />
        <cfset var lengthVal = 0 />
        <cfset var widthVal = 0 />
        <cfset var depthVal = 0 />
        <cfset var readings = 0 />
        <cfset var cols = {} />
        <cfset var woundtypeVal = "" />
        <cfset var insertResult = {} />
        <cfset var forceInsert = false />
        <cfset var forceUpdate = false />

        <cfif NOT normalized.ok>
            <cfif normalized.action EQ "update">
                <cfset result.reason = "update_requires_id_or_item_id" />
            <cfelse>
                <cfset result.reason = "empty_wound_payload" />
            </cfif>
            <cfreturn result />
        </cfif>
        <cfset cols = normalized.cols />
        <cfset forceInsert = (normalized.action EQ "insert") />
        <cfset forceUpdate = (normalized.action EQ "update") />

        <cfset lengthVal = structKeyExists(cols, "Length") ? val(cols.Length) : 0 />
        <cfset widthVal = structKeyExists(cols, "Width") ? val(cols.Width) : 0 />
        <cfset depthVal = structKeyExists(cols, "Depth") ? val(cols.Depth) : 0 />
        <cfset readings = lengthVal + widthVal + depthVal />
        <cfset woundtypeVal = structKeyExists(cols, "woundtype") ? trim(cols.woundtype) : "" />
        <cfif len(woundtypeVal)>
            <cfset woundpathid = resolveMobileWoundPathwayIdFromType(woundtypeVal) />
        </cfif>

        <cfif NOT forceInsert>
            <cfif normalized.rowId GT 0>
            <cfquery name="existingWound" datasource="#Application.DataSrc#">
                SELECT ID, Item_ID
                FROM #arguments.agencySchema#.pWoundsVitals
                WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#normalized.rowId#">
                  AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                LIMIT 1
            </cfquery>
        <cfelseif normalized.itemId GT 0>
            <cfquery name="existingWound" datasource="#Application.DataSrc#">
                SELECT ID, Item_ID
                FROM #arguments.agencySchema#.pWoundsVitals
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                  AND Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#normalized.itemId#">
                LIMIT 1
            </cfquery>
            </cfif>
        </cfif>

        <cfif isQuery(existingWound) AND existingWound.recordCount GT 0>
            <cfquery datasource="#Application.DataSrc#">
                UPDATE #arguments.agencySchema#.pWoundsVitals
                SET
                    <cfif structKeyExists(cols, "Description")>Description = <cfqueryparam value="#cols.Description#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "Location")>Location = <cfqueryparam value="#cols.Location#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "other_locations")>other_locations = <cfqueryparam value="#cols.other_locations#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "Length")>Length = <cfqueryparam value="#cols.Length#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "Width")>Width = <cfqueryparam value="#cols.Width#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "Depth")>Depth = <cfqueryparam value="#cols.Depth#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "healingstage")>healingstage = <cfqueryparam value="#cols.healingstage#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "woundtype")>woundtype = <cfqueryparam value="#cols.woundtype#" cfsqltype="cf_sql_varchar">, Pathway_ID = <cfqueryparam value="#woundpathid#" cfsqltype="cf_sql_integer">,</cfif>
                    <cfif structKeyExists(cols, "wnd_bed")>wnd_bed = <cfqueryparam value="#cols.wnd_bed#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_tunneling")>wnd_tunneling = <cfqueryparam value="#cols.wnd_tunneling#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_underm")>wnd_underm = <cfqueryparam value="#cols.wnd_underm#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_gran")>wnd_gran = <cfqueryparam value="#cols.wnd_gran#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_epith")>wnd_epith = <cfqueryparam value="#cols.wnd_epith#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_eschar")>wnd_eschar = <cfqueryparam value="#cols.wnd_eschar#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_slough")>wnd_slough = <cfqueryparam value="#cols.wnd_slough#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_fib")>wnd_fib = <cfqueryparam value="#cols.wnd_fib#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_margin")>wnd_margin = <cfqueryparam value="#cols.wnd_margin#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_exudate")>wnd_exudate = <cfqueryparam value="#cols.wnd_exudate#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_odor")>wnd_odor = <cfqueryparam value="#cols.wnd_odor#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "wnd_pain")>wnd_pain = <cfqueryparam value="#cols.wnd_pain#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(cols, "Length") OR structKeyExists(cols, "Width") OR structKeyExists(cols, "Depth")>
                        Param_Hi = <cfqueryparam value="#readings#" cfsqltype="cf_sql_varchar">,
                        Reading1 = <cfqueryparam value="#readings#" cfsqltype="cf_sql_varchar">,
                    </cfif>
                    Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                    Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
                WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#existingWound.ID#">
            </cfquery>
            <cfset result.action = "updated" />
            <cfset result.id = existingWound.ID />
            <cfset result.item_id = existingWound.Item_ID />
        <cfelseif forceUpdate>
            <cfset result.reason = "wound_not_found_for_update" />
        <cfelse>
            <cfset wounditemid = resolveMobileWoundItemIdForInsert(
                agencySchema = arguments.agencySchema,
                assmtId = arguments.assmtId,
                woundtype = woundtypeVal,
                requestedItemId = normalized.itemId
            ) />

            <cfquery result="insertResult" datasource="#Application.DataSrc#">
                INSERT INTO #arguments.agencySchema#.pWoundsVitals (
                    Assmt_ID, Status, Pathway_ID, Item_ID, VitalOrWound,
                    Description, Location, other_locations, Length, Width, Depth,
                    Param_Hi, Reading1, Date_Create, Created_by, Date_Change, Change_by,
                    healingstage, frompgnote, woundtype,
                    wnd_bed, wnd_margin, wnd_exudate, wnd_odor, wnd_pain,
                    wnd_tunneling, wnd_underm, wnd_gran, wnd_epith, wnd_eschar, wnd_slough, wnd_fib
                ) VALUES (
                    <cfqueryparam value="#arguments.assmtId#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#woundpathid#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#wounditemid#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'Description') ? cols.Description : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'Location') ? cols.Location : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'other_locations') ? cols.other_locations : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'Length') ? cols.Length : '0'#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'Width') ? cols.Width : '0'#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'Depth') ? cols.Depth : '0'#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#readings#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#readings#" cfsqltype="cf_sql_varchar">,
                    <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">,
                    <cfqueryparam value="#structKeyExists(cols, 'healingstage') ? cols.healingstage : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#woundtypeVal#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_bed') ? cols.wnd_bed : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_margin') ? cols.wnd_margin : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_exudate') ? cols.wnd_exudate : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_odor') ? cols.wnd_odor : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_pain') ? cols.wnd_pain : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_tunneling') ? cols.wnd_tunneling : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_underm') ? cols.wnd_underm : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_gran') ? cols.wnd_gran : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_epith') ? cols.wnd_epith : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_eschar') ? cols.wnd_eschar : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_slough') ? cols.wnd_slough : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(cols, 'wnd_fib') ? cols.wnd_fib : ''#" cfsqltype="cf_sql_varchar">
                )
            </cfquery>
            <cfset result.action = "inserted" />
            <cfif structKeyExists(insertResult, "GENERATED_KEY") AND val(insertResult.GENERATED_KEY) GT 0>
                <cfset result.id = val(insertResult.GENERATED_KEY) />
            <cfelseif structKeyExists(insertResult, "generatedKey") AND val(insertResult.generatedKey) GT 0>
                <cfset result.id = val(insertResult.generatedKey) />
            </cfif>
            <cfset result.item_id = wounditemid />
        </cfif>
        <cfreturn result />
    </cffunction>

    <!--- Save vitals and/or wounds to pWoundsVitals for one assessment --->
    <cffunction name="saveAssessmentWoundsVitals" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Assmt_ID, and vitals[] and/or wounds[]. Wounds support action insert|update (auto if omitted). Optional wound photos via photos[] (base64) or multipart wound_N_photo_M / ufFile.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Assmt_ID" type="string" required="false" default="" />
        <cfargument name="vitals" type="string" required="false" default="" hint="JSON array of vital objects" />
        <cfargument name="wounds" type="string" required="false" default="" hint="JSON array of wound objects" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = {} />
        <cfset var ctx = {} />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />
        <cfset var lookupDb = "" />
        <cfset var vitalResults = { updated = 0, inserted = 0, applied = [], skipped = [] } />
        <cfset var woundResults = { updated = 0, inserted = 0, photos_uploaded = 0, applied = [], skipped = [] } />
        <cfset var idx = 0 />
        <cfset var item = {} />
        <cfset var rowResult = {} />
        <cfset var tempPath = "/var/www/datafiles/patientfileupload/" />
        <cfset var multipartPhotoFields = {} />
        <cfset var patientId = 0 />
        <cfset var woundRowId = 0 />
        <cfset var photoResult = {} />
        <cfset var appliedEntry = {} />
        <cfset var assessmentPhotoResult = {} />
        <cfset var traceRequest = {} />
        <cfset var traceContext = {} />

        <cfset var pendingWoundPhotos = [] />
        <cfset var pendingPhoto = {} />
        <cfset var appliedIdx = 0 />

        <cfset payload = parseMobileWoundsVitalsSaveRequest(
            securityKeyArg = arguments.securityKey,
            assmtIdArg = arguments.Assmt_ID,
            vitalsArg = arguments.vitals,
            woundsArg = arguments.wounds
        ) />
        <cfset traceRequest = buildMobileSaveTraceRequestSnapshot(methodName = "saveAssessmentWoundsVitals", payload = payload) />

        <cfif NOT len(payload.securityKey)>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentWoundsVitals",
                response = response,
                requestSnapshot = traceRequest,
                httpStatus = 401
            ) />
        </cfif>
        <cfif payload.assmtId LTE 0>
            <cfset response.message = "Assmt_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentWoundsVitals",
                response = response,
                requestSnapshot = traceRequest,
                httpStatus = 400
            ) />
        </cfif>
        <cfif (NOT isArray(payload.vitals) OR arrayLen(payload.vitals) EQ 0) AND (NOT isArray(payload.wounds) OR arrayLen(payload.wounds) EQ 0)>
            <cfset response.message = "At least one vital or wound item is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentWoundsVitals",
                response = response,
                requestSnapshot = traceRequest,
                httpStatus = 400
            ) />
        </cfif>

        <cfif NOT directoryExists(tempPath)>
            <cfdirectory action="create" directory="#tempPath#" />
        </cfif>
        <cfif findNoCase("multipart/form-data", cgi.content_type) GT 0
            OR (isDefined("form.fieldnames") AND len(trim(form.fieldnames)))
            OR (isStruct(form) AND structCount(form) GT 0)>
            <cfset multipartPhotoFields = collectMobileMultipartWoundPhotoFields(tempPath = tempPath) />
        </cfif>

        <cfset ctx = authorizeMobileAssmtRequest(
            securityKey = payload.securityKey,
            assmtId = payload.assmtId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentWoundsVitals",
                response = response,
                requestSnapshot = traceRequest,
                context = buildMobileSaveAuthTraceContext(ctx = ctx),
                httpStatus = ctx.statusCode
            ) />
        </cfif>

        <cfif NOT isDefined("Request.prefix_db_lookup") OR NOT len(trim(Request.prefix_db_lookup))>
            <cfset Request.prefix_db_lookup = "hhapowerpath" />
        </cfif>
        <cfset lookupDb = trim(Request.prefix_db_lookup) />
        <cfset patientId = val(ctx.assessment.Patient_ID) />
        <cfset assessmentPhotoResult = { uploaded = 0, skipped = [], photos = [] } />

        <cfset emailCtx = {
            assmtId = payload.assmtId,
            vitalCount = isArray(payload.vitals) ? arrayLen(payload.vitals) : 0,
            woundCount = isArray(payload.wounds) ? arrayLen(payload.wounds) : 0,
            empId = ctx.empId,
            agencySchema = ctx.agencySchema,
            patientId = patientId
        } />
        <cfset traceContext = duplicate(emailCtx) />
        <cfset traceContext.scheduleQuery = structKeyExists(ctx, "scheduleQuery") ? ctx.scheduleQuery : [] />
        <cfif structKeyExists(ctx, "errorDetail") AND NOT structIsEmpty(ctx.errorDetail)>
            <cfset traceContext.errorDetail = ctx.errorDetail />
        </cfif>
        <cfif structKeyExists(ctx, "schedulesForAssmt")>
            <cfset traceContext.schedulesForAssmt = ctx.schedulesForAssmt />
        </cfif>
        <cfif structCount(multipartPhotoFields) GT 0>
            <cfset traceContext.multipart_by_wound = structKeyExists(multipartPhotoFields, "byWoundIndex") ? structKeyList(multipartPhotoFields.byWoundIndex) : "" />
            <cfset traceContext.multipart_assessment_photo_count = (structKeyExists(multipartPhotoFields, "assessmentFields") AND isArray(multipartPhotoFields.assessmentFields)) ? arrayLen(multipartPhotoFields.assessmentFields) : 0 />
        </cfif>

        <cftry>
            <cftransaction>
                <cfif isArray(payload.vitals) AND arrayLen(payload.vitals) GT 0>
                    <cfset currentStep = "upsert_vitals" />
                    <cfloop from="1" to="#arrayLen(payload.vitals)#" index="idx">
                        <cfset item = payload.vitals[idx] />
                        <cfif NOT isStruct(item)>
                            <cfset arrayAppend(vitalResults.skipped, { index = idx, reason = "invalid_vital_object" }) />
                            <cfcontinue />
                        </cfif>
                        <cfset rowResult = upsertMobileAssessmentVitalRow(
                            agencySchema = ctx.agencySchema,
                            assmtId = payload.assmtId,
                            empId = ctx.empId,
                            vitalItem = item,
                            lookupDb = lookupDb
                        ) />
                        <cfif rowResult.action EQ "updated">
                            <cfset vitalResults.updated = vitalResults.updated + 1 />
                            <cfset arrayAppend(vitalResults.applied, {
                                index = idx,
                                action = "updated",
                                id = rowResult.id,
                                item_id = rowResult.item_id,
                                description = getMobilePayloadString(item, "description,Description"),
                                reading = getMobilePayloadString(item, "reading,Reading,Reading1")
                            }) />
                        <cfelseif rowResult.action EQ "inserted">
                            <cfset vitalResults.inserted = vitalResults.inserted + 1 />
                            <cfset arrayAppend(vitalResults.applied, {
                                index = idx,
                                action = "inserted",
                                item_id = rowResult.item_id,
                                description = getMobilePayloadString(item, "description,Description"),
                                reading = getMobilePayloadString(item, "reading,Reading,Reading1")
                            }) />
                        <cfelse>
                            <cfset arrayAppend(vitalResults.skipped, { index = idx, reason = rowResult.reason }) />
                        </cfif>
                    </cfloop>
                </cfif>

                <cfif isArray(payload.wounds) AND arrayLen(payload.wounds) GT 0>
                    <cfset currentStep = "upsert_wounds" />
                    <cfloop from="1" to="#arrayLen(payload.wounds)#" index="idx">
                        <cfset item = payload.wounds[idx] />
                        <cfif NOT isStruct(item)>
                            <cfset arrayAppend(woundResults.skipped, { index = idx, reason = "invalid_wound_object" }) />
                            <cfcontinue />
                        </cfif>
                        <cfset rowResult = upsertMobileAssessmentWoundRow(
                            agencySchema = ctx.agencySchema,
                            assmtId = payload.assmtId,
                            empId = ctx.empId,
                            woundItem = item
                        ) />
                        <cfset woundRowId = resolveMobileWoundRowIdForPhotos(
                            agencySchema = ctx.agencySchema,
                            assmtId = payload.assmtId,
                            woundItem = item,
                            rowResult = rowResult
                        ) />
                        <cfif rowResult.action EQ "updated">
                            <cfset woundResults.updated = woundResults.updated + 1 />
                            <cfset appliedEntry = {
                                index = idx,
                                action = "updated",
                                id = rowResult.id,
                                item_id = rowResult.item_id,
                                location = getMobilePayloadString(item, "location,Location"),
                                description = getMobilePayloadString(item, "description,Description")
                            } />
                            <cfset arrayAppend(woundResults.applied, appliedEntry) />
                        <cfelseif rowResult.action EQ "inserted">
                            <cfset woundResults.inserted = woundResults.inserted + 1 />
                            <cfset appliedEntry = {
                                index = idx,
                                action = "inserted",
                                id = (val(rowResult.id) GT 0 ? rowResult.id : woundRowId),
                                item_id = rowResult.item_id,
                                location = getMobilePayloadString(item, "location,Location"),
                                description = getMobilePayloadString(item, "description,Description")
                            } />
                            <cfset arrayAppend(woundResults.applied, appliedEntry) />
                        <cfelse>
                            <cfset arrayAppend(woundResults.skipped, { index = idx, reason = rowResult.reason }) />
                        </cfif>
                        <!--- Queue photos to run AFTER transaction commit (S3 + pg_files outside txn) --->
                        <cfset arrayAppend(pendingWoundPhotos, {
                            index = idx,
                            woundItem = item,
                            woundId = woundRowId,
                            appliedIndex = arrayLen(woundResults.applied),
                            rowAction = rowResult.action
                        }) />
                    </cfloop>
                </cfif>
            </cftransaction>

            <!--- Upload wound photos after DB commit — matches SOC_new.cfm (no open transaction around S3/pg_files) --->
            <cfif arrayLen(pendingWoundPhotos) GT 0>
                <cfset currentStep = "upload_wound_photos" />
                <cfloop from="1" to="#arrayLen(pendingWoundPhotos)#" index="idx">
                    <cfset pendingPhoto = pendingWoundPhotos[idx] />
                    <cfset photoResult = processMobileWoundItemPhotos(
                        agencySchema = ctx.agencySchema,
                        locId = ctx.locId,
                        patientId = patientId,
                        assmtId = payload.assmtId,
                        empId = ctx.empId,
                        woundIndex = pendingPhoto.index,
                        woundItem = pendingPhoto.woundItem,
                        woundId = pendingPhoto.woundId,
                        tempPath = tempPath,
                        multipartPhotoFields = multipartPhotoFields
                    ) />
                    <cfif photoResult.uploaded GT 0>
                        <cfset woundResults.photos_uploaded = woundResults.photos_uploaded + photoResult.uploaded />
                    </cfif>
                    <cfset appliedIdx = val(pendingPhoto.appliedIndex) />
                    <cfif appliedIdx GT 0 AND appliedIdx LTE arrayLen(woundResults.applied)>
                        <cfif photoResult.uploaded GT 0 OR arrayLen(photoResult.skipped) GT 0>
                            <cfset woundResults.applied[appliedIdx].photos = {
                                uploaded = photoResult.uploaded,
                                items = photoResult.photos,
                                skipped = photoResult.skipped
                            } />
                        </cfif>
                    <cfelseif photoResult.uploaded GT 0>
                        <cfset arrayAppend(woundResults.applied, {
                            index = pendingPhoto.index,
                            action = "photos_only",
                            id = pendingPhoto.woundId,
                            photos = { uploaded = photoResult.uploaded, items = photoResult.photos, skipped = photoResult.skipped }
                        }) />
                    </cfif>
                </cfloop>
            </cfif>

            <cfif isArray(payload.wounds) AND arrayLen(payload.wounds) GT 0>
                <cfset currentStep = "upload_assessment_wound_photos" />
                <cfset assessmentPhotoResult = processMobileAssessmentLevelWoundPhotos(
                    agencySchema = ctx.agencySchema,
                    locId = ctx.locId,
                    patientId = patientId,
                    assmtId = payload.assmtId,
                    empId = ctx.empId,
                    tempPath = tempPath,
                    multipartPhotoFields = multipartPhotoFields
                ) />
                <cfif assessmentPhotoResult.uploaded GT 0>
                    <cfset woundResults.photos_uploaded = woundResults.photos_uploaded + assessmentPhotoResult.uploaded />
                </cfif>
            </cfif>

            <cfif (vitalResults.updated + vitalResults.inserted + woundResults.updated + woundResults.inserted + woundResults.photos_uploaded) EQ 0>
                <cfset response.message = "No vitals or wounds were saved" />
                <cfset response.data = { assmt_id = payload.assmtId, vitals = vitalResults, wounds = woundResults } />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn returnMobileSaveTraceResponse(
                    methodName = "saveAssessmentWoundsVitals",
                    response = response,
                    requestSnapshot = traceRequest,
                    context = traceContext,
                    httpStatus = 400
                ) />
            </cfif>

            <cfset response.success = true />
            <cfset response.message = "Assessment wounds/vitals saved" />
            <cfset response.data = {
                assmt_id = payload.assmtId,
                vitals = vitalResults,
                wounds = woundResults
            } />
            <cfif assessmentPhotoResult.uploaded GT 0 OR arrayLen(assessmentPhotoResult.skipped) GT 0>
                <cfset response.data.assessment_photos = {
                    uploaded = assessmentPhotoResult.uploaded,
                    items = assessmentPhotoResult.photos,
                    skipped = assessmentPhotoResult.skipped
                } />
            </cfif>
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentWoundsVitals",
                response = response,
                requestSnapshot = traceRequest,
                context = traceContext,
                httpStatus = 200
            ) />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "saveAssessmentWoundsVitals",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to save wounds/vitals: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn returnMobileSaveTraceResponse(
                    methodName = "saveAssessmentWoundsVitals",
                    response = response,
                    requestSnapshot = traceRequest,
                    context = traceContext,
                    httpStatus = 500
                ) />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Parse deleteAssessmentWound JSON body or form fields --->
    <cffunction name="parseMobileWoundDeleteRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="assmtIdArg" type="string" required="false" default="" />
        <cfargument name="idArg" type="string" required="false" default="" />
        <cfargument name="woundIdsArg" type="string" required="false" default="" />
        <cfset var out = {
            securityKey = trim(arguments.securityKeyArg),
            assmtId = val(trim(arguments.assmtIdArg)),
            woundIds = []
        } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var idsRaw = "" />
        <cfset var i = 0 />
        <cfset var item = {} />
        <cfset var woundId = 0 />

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

        <cfset woundId = val(trim(arguments.idArg)) />
        <cfif woundId LTE 0 AND structKeyExists(form, "ID")>
            <cfset woundId = val(trim(form.ID)) />
        </cfif>
        <cfif woundId LTE 0 AND structKeyExists(form, "id")>
            <cfset woundId = val(trim(form.id)) />
        </cfif>
        <cfif woundId GT 0>
            <cfset arrayAppend(out.woundIds, woundId) />
        </cfif>

        <cfif len(trim(arguments.woundIdsArg))>
            <cfset idsRaw = deserializeJSON(trim(arguments.woundIdsArg)) />
        <cfelseif structKeyExists(form, "wound_ids") AND len(trim(form.wound_ids))>
            <cfset idsRaw = deserializeJSON(trim(form.wound_ids)) />
        </cfif>
        <cfif isArray(idsRaw)>
            <cfloop from="1" to="#arrayLen(idsRaw)#" index="i">
                <cfif isStruct(idsRaw[i])>
                    <cfset woundId = val(getMobilePayloadString(idsRaw[i], "id,ID,Woundid,Wound_ID")) />
                <cfelse>
                    <cfset woundId = val(idsRaw[i]) />
                </cfif>
                <cfif woundId GT 0 AND arrayFind(out.woundIds, woundId) EQ 0>
                    <cfset arrayAppend(out.woundIds, woundId) />
                </cfif>
            </cfloop>
        </cfif>

        <cftry>
            <cfif NOT len(out.securityKey) OR out.assmtId LTE 0 OR arrayLen(out.woundIds) EQ 0>
                <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 = trim("" & getMobileStructValueCI(bodyJson, "securityKey", "")) />
                    </cfif>
                    <cfif out.assmtId LTE 0>
                        <cfset out.assmtId = val(trim("" & getMobileStructValueCI(bodyJson, "Assmt_ID", 0))) />
                    </cfif>
                    <cfif arrayLen(out.woundIds) EQ 0>
                        <!--- Try each key name individually (getMobileStructValueCI takes one key at a time) --->
                        <cfset woundId = val(trim("" & getMobileStructValueCI(bodyJson, "ID", 0))) />
                        <cfif woundId LTE 0><cfset woundId = val(trim("" & getMobileStructValueCI(bodyJson, "id", 0))) /></cfif>
                        <cfif woundId LTE 0><cfset woundId = val(trim("" & getMobileStructValueCI(bodyJson, "Woundid", 0))) /></cfif>
                        <cfif woundId LTE 0><cfset woundId = val(trim("" & getMobileStructValueCI(bodyJson, "Wound_ID", 0))) /></cfif>
                        <cfif woundId GT 0>
                            <cfset arrayAppend(out.woundIds, woundId) />
                        </cfif>
                        <cfset idsRaw = getMobileStructValueCI(bodyJson, "wound_ids,woundIds", []) />
                        <cfif isArray(idsRaw)>
                            <cfloop from="1" to="#arrayLen(idsRaw)#" index="i">
                                <cfif isStruct(idsRaw[i])>
                                    <cfset woundId = val(getMobilePayloadString(idsRaw[i], "id,ID,Woundid,Wound_ID")) />
                                <cfelse>
                                    <cfset woundId = val(idsRaw[i]) />
                                </cfif>
                                <cfif woundId GT 0 AND arrayFind(out.woundIds, woundId) EQ 0>
                                    <cfset arrayAppend(out.woundIds, woundId) />
                                </cfif>
                            </cfloop>
                        </cfif>
                        <cfset idsRaw = getMobileStructValueCI(bodyJson, "wounds", []) />
                        <cfif isArray(idsRaw)>
                            <cfloop from="1" to="#arrayLen(idsRaw)#" index="i">
                                <cfif isStruct(idsRaw[i])>
                                    <cfset woundId = val(getMobilePayloadString(idsRaw[i], "id,ID,Woundid,Wound_ID")) />
                                <cfelseif isNumeric(idsRaw[i]) OR isSimpleValue(idsRaw[i])>
                                    <cfset woundId = val(idsRaw[i]) />
                                <cfelse>
                                    <cfset woundId = 0 />
                                </cfif>
                                <cfif woundId GT 0 AND arrayFind(out.woundIds, woundId) EQ 0>
                                    <cfset arrayAppend(out.woundIds, woundId) />
                                </cfif>
                            </cfloop>
                        </cfif>
                    </cfif>
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>
        <cfreturn out />
    </cffunction>

    <!--- Soft-delete one wound row (pwoundvital.cfc WoundVitalDelete pattern: Status=2 + pProblem) --->
    <cffunction name="deleteMobileAssessmentWoundRow" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="woundId" type="numeric" required="true" />
        <cfargument name="assessment" type="struct" required="true" />
        <cfset var result = { action = "skipped", id = toJsonInt(arguments.woundId), reason = "" } />
        <cfset var existingWound = "" />
        <cfset var parentAssmt = {} />
        <cfset var updateAssmtId = val(arguments.assmtId) />
        <cfset var assessmentsCfc = "" />

        <cfif arguments.woundId LTE 0>
            <cfset result.reason = "invalid_wound_id" />
            <cfreturn result />
        </cfif>

        <cfquery name="existingWound" datasource="#Application.DataSrc#">
            SELECT ID, Assmt_ID, Item_ID, VitalOrWound, Status
            FROM #arguments.agencySchema#.pWoundsVitals
            WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.woundId#">
              AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            LIMIT 1
        </cfquery>

        <cfif existingWound.recordCount EQ 0>
            <cfset result.reason = "wound_not_found_for_assessment" />
            <cfreturn result />
        </cfif>
        <cfif val(existingWound.Status) EQ 2>
            <cfset result.action = "deleted" />
            <cfset result.reason = "already_deleted" />
            <cfreturn result />
        </cfif>
        <cfif val(existingWound.Status) NEQ 0>
            <cfset result.reason = "wound_not_active" />
            <cfreturn result />
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pWoundsVitals
            SET Status = <cfqueryparam value="2" cfsqltype="cf_sql_integer">,
                Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
            WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.woundId#">
              AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
              AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
        </cfquery>

        <cfif val(arguments.assmtId) GT 0 AND structKeyExists(arguments.assessment, "Admit_ID") AND val(arguments.assessment.Admit_ID) GT 0>
            <cfset Request.prefix_db_agency = arguments.agencySchema />
            <cfset assessmentsCfc = createObject("component", "components.general.assessments") />
            <cfset parentAssmt = assessmentsCfc.getParentOASIS(val(arguments.assessment.Admit_ID)) />
            <cfif isStruct(parentAssmt) AND structKeyExists(parentAssmt, "Assmt_ID") AND val(parentAssmt.Assmt_ID) GT 0>
                <cfset updateAssmtId = val(parentAssmt.Assmt_ID) />
            </cfif>
            <cfif len(trim("" & arguments.assessment.F27))>
                <cfquery datasource="#Application.DataSrc#">
                    UPDATE #arguments.agencySchema#.pProblem
                    SET Status = <cfqueryparam value="2" cfsqltype="cf_sql_integer">,
                        Date_Change = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                        Change_by = <cfqueryparam value="#arguments.empId#" cfsqltype="cf_sql_integer">
                    WHERE ques_id = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.woundId#">
                      AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#updateAssmtId#">
                      AND Skill = <cfqueryparam value="#trim(arguments.assessment.F27)#" cfsqltype="cf_sql_varchar">
                </cfquery>
            </cfif>
        </cfif>

        <cfset result.action = "deleted" />
        <cfreturn result />
    </cffunction>

    <!--- Delete one or more assessment wounds from pWoundsVitals (Status=2) --->
    <cffunction name="deleteAssessmentWound" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Assmt_ID, and wound ID (or wound_ids[]). Mirrors pwoundvital.cfc WoundVitalDelete.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Assmt_ID" type="string" required="false" default="" />
        <cfargument name="ID" type="string" required="false" default="" hint="Single pWoundsVitals.ID to delete" />
        <cfargument name="wound_ids" type="string" required="false" default="" hint="JSON array of wound IDs" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = {} />
        <cfset var ctx = {} />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />
        <cfset var deleteResults = { deleted = 0, skipped = 0, applied = [], skipped_items = [] } />
        <cfset var idx = 0 />
        <cfset var woundId = 0 />
        <cfset var rowResult = {} />

        <cfset payload = parseMobileWoundDeleteRequest(
            securityKeyArg = arguments.securityKey,
            assmtIdArg = arguments.Assmt_ID,
            idArg = arguments.ID,
            woundIdsArg = arguments.wound_ids
        ) />

        <cfif NOT len(payload.securityKey)>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif payload.assmtId LTE 0>
            <cfset response.message = "Assmt_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>
        <cfif arrayLen(payload.woundIds) EQ 0>
            <cfset response.message = "At least one wound ID is required (ID or wound_ids[])" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>  

        <cfset ctx = authorizeMobileAssmtRequest(
            securityKey = payload.securityKey,
            assmtId = payload.assmtId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <cfreturn response />
        </cfif>

        <cfset emailCtx = {
            assmtId = payload.assmtId,
            woundCount = arrayLen(payload.woundIds),
            empId = ctx.empId,
            agencySchema = ctx.agencySchema
        } />

        <cftry>
            <cftransaction>
                <cfset currentStep = "delete_wounds" />
                <cfloop from="1" to="#arrayLen(payload.woundIds)#" index="idx">
                    <cfset woundId = val(payload.woundIds[idx]) />
                    <cfset rowResult = deleteMobileAssessmentWoundRow(
                        agencySchema = ctx.agencySchema,
                        assmtId = payload.assmtId,
                        empId = ctx.empId,
                        woundId = woundId,
                        assessment = ctx.assessment
                    ) />
                    <cfif rowResult.action EQ "deleted">
                        <cfset deleteResults.deleted = deleteResults.deleted + 1 />
                        <cfset arrayAppend(deleteResults.applied, {
                            index = toJsonInt(idx),
                            id = toJsonInt(rowResult.id),
                            reason = len(rowResult.reason) ? rowResult.reason : "deleted"
                        }) />
                    <cfelse>
                        <cfset deleteResults.skipped = deleteResults.skipped + 1 />
                        <cfset arrayAppend(deleteResults.skipped_items, {
                            index = toJsonInt(idx),
                            id = toJsonInt(rowResult.id),
                            reason = rowResult.reason
                        }) />
                    </cfif>
                </cfloop>
            </cftransaction>

            <cfif deleteResults.deleted EQ 0>
                <cfset response.message = "No wounds were deleted" />
                <cfset response.data = {
                    assmt_id = payload.assmtId,
                    wounds = deleteResults
                } />
                <cfheader statuscode="404" statustext="Not Found" />
                <cfreturn response />
            </cfif>

            <cfset response.success = true />
            <cfset response.message = "Assessment wound(s) deleted" />
            <cfset response.data = {
                assmt_id = payload.assmtId,
                wounds = deleteResults
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "deleteAssessmentWound",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to delete wound(s): " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Format dose + units the same way as patient medlist UI --->
    <cffunction name="formatMobileMedDoseLabel" access="private" returntype="string" output="false">
        <cfargument name="dose" type="string" required="false" default="" />
        <cfargument name="doseUnits" type="string" required="false" default="" />
        <cfset var listDose = trim(arguments.dose) />
        <cfset var listDoseUnits = trim(arguments.doseUnits) />

        <cfif NOT len(listDose) OR listDose EQ "0">
            <cfreturn listDoseUnits />
        </cfif>
        <cfif NOT len(listDoseUnits) OR compareNoCase(listDose, listDoseUnits) EQ 0>
            <cfreturn listDose />
        </cfif>
        <cfif findNoCase(listDose, listDoseUnits) GT 0>
            <cfreturn listDoseUnits />
        </cfif>
        <cfif reFind("^[0-9]+(\.[0-9]+)?$", listDose) AND reFind("^[A-Za-z].*$", listDoseUnits)>
            <cfreturn listDose & " " & listDoseUnits />
        </cfif>
        <cfreturn listDose />
    </cffunction>

    <!--- Authorize securityKey + Patient_ID; load General Information fields (same as GeneralInformation.cfm) --->
    <cffunction name="authorizeMobilePatientRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfset var result = { ok = false, statusCode = 401, message = "Unauthorized", agencySchema = "", locId = 0, empId = 0, patient = {} } />
        <cfset var auth = {} />
        <cfset var patientQuery = "" />
        <cfset var scheduleQuery = "" />
        <cfset var admitQuery = "" />
        <cfset var physQuery = "" />
        <cfset var stateQuery = "" />
        <cfset var lookupDb = "hhapowerpath" />
        <cfset var genderRaw = "" />
        <cfset var genderLabel = "" />
        <cfset var statusLabel = "" />
        <cfset var stateName = "" />
        <cfset var physician = {} />
        <cfset var admitInfo = {} />

        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />
        <cfif NOT auth.ok>
            <cfset result.statusCode = auth.statusCode />
            <cfset result.message = auth.message />
            <cfreturn result />
        </cfif>

        <cfset result.empId = parseSecurityKeyEmpId(trim(arguments.securityKey)) />
        <cfset result.agencySchema = auth.agencySchema />
        <cfset result.locId = auth.locId />

        <cfif arguments.patientId LTE 0>
            <cfset result.statusCode = 400 />
            <cfset result.message = "Patient_ID is required" />
            <cfreturn result />
        </cfif>

        <cfif NOT isDefined("Request.prefix_db_lookup") OR NOT len(trim(Request.prefix_db_lookup))>
            <cfset Request.prefix_db_lookup = "hhapowerpath" />
        </cfif>
        <cfset lookupDb = trim(Request.prefix_db_lookup) />

        <cfquery name="patientQuery" datasource="#Application.DataSrc#">
            SELECT Patient_ID,
                   Status,
                   Pt_Agy_ID,
                   Pt_First,
                   Pt_Middle,
                   Pt_Last,
                   Pt_Suffix,
                   Pt_Street,
                   Pt_City,
                   Pt_State,
                   Pt_Zip,
                   Pt_Phone,
                   Email,
                   Pt_DOB,
                   Pt_Gender,
                   Pt_SSN,
                   Pt_SSN_UK,
                   Pt_Race,
                   M0140_ETHNIC,
                   Pt_language,
                   Interpreter,
                   Allergies,
                   Specific_Concerns,
                   Comments,
                   Home_Directions,
                   Pt_Latitude,
                   Pt_Longitude,
                   FileName
            FROM #auth.agencySchema#.pPatients
            WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status IN (0, 1, 4)
            LIMIT 1
        </cfquery>
        <cfif patientQuery.recordcount EQ 0>
            <cfset result.statusCode = 404 />
            <cfset result.message = "Patient not found or inactive" />
            <cfreturn result />
        </cfif>

        <cfquery name="scheduleQuery" datasource="#Application.DataSrc#">
            SELECT Schedule_ID
            FROM #auth.agencySchema#.pSchedules
            WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Emp_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.empId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            LIMIT 1
        </cfquery>
        <cfif scheduleQuery.recordcount EQ 0>
            <cfset result.statusCode = 403 />
            <cfset result.message = "Patient is not assigned to this employee" />
            <cfreturn result />
        </cfif>

        <cfif len(trim(patientQuery.Pt_State))>
            <cfquery name="stateQuery" datasource="#Application.DataSrc#">
                SELECT StateName, StateAbbr
                FROM #lookupDb#.States
                WHERE StateAbbr = <cfqueryparam cfsqltype="cf_sql_varchar" value="#trim(patientQuery.Pt_State)#">
                LIMIT 1
            </cfquery>
            <cfif stateQuery.recordcount GT 0>
                <cfset stateName = trim(stateQuery.StateName) />
            </cfif>
        </cfif>

        <cfquery name="admitQuery" datasource="#Application.DataSrc#">
            SELECT Admit_ID,
                   Render_Phys_ID,
                   Advance_Directive,
                   Code_Status,
                   M0030_START_CARE_DT,
                   Agy_Admit
            FROM #auth.agencySchema#.pAdmit
            WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            ORDER BY M0030_START_CARE_DT DESC
            LIMIT 1
        </cfquery>

        <cfif admitQuery.recordcount GT 0>
            <cfset admitInfo = {
                admit_id = val(admitQuery.Admit_ID),
                render_phys_id = val(admitQuery.Render_Phys_ID),
                advance_directive = trim(admitQuery.Advance_Directive),
                code_status = trim(admitQuery.Code_Status),
                start_of_care_date = isDate(admitQuery.M0030_START_CARE_DT) ? dateFormat(admitQuery.M0030_START_CARE_DT, "yyyy-mm-dd") : "",
                agency_admit_date = isDate(admitQuery.Agy_Admit) ? dateFormat(admitQuery.Agy_Admit, "yyyy-mm-dd") : ""
            } />

            <cfif val(admitQuery.Render_Phys_ID) GT 0>
                <cfquery name="physQuery" datasource="#Application.DataSrc#">
                    SELECT Phys_ID,
                           Phys_First,
                           Phys_Last,
                           Phys_Street,
                           Phys_City,
                           Phys_State,
                           Phys_Zip,
                           Phys_Phone,
                           Phys_Fax,
                           Phys_NPI
                    FROM #lookupDb#.pPhysicians
                    WHERE Phys_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#val(admitQuery.Render_Phys_ID)#">
                      AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                    LIMIT 1
                </cfquery>
                <cfif physQuery.recordcount GT 0>
                    <cfset physician = {
                        phys_id = physQuery.Phys_ID,
                        phys_first = trim(physQuery.Phys_First),
                        phys_last = trim(physQuery.Phys_Last),
                        phys_name = trim(trim(physQuery.Phys_First) & " " & trim(physQuery.Phys_Last)),
                        phys_street = trim(physQuery.Phys_Street),
                        phys_city = trim(physQuery.Phys_City),
                        phys_state = trim(physQuery.Phys_State),
                        phys_zip = trim(physQuery.Phys_Zip),
                        phys_city_state_zip = trim(physQuery.Phys_City) &
                            (len(trim(physQuery.Phys_State)) ? ", " & trim(physQuery.Phys_State) : "") &
                            (len(trim(physQuery.Phys_Zip)) ? " - " & trim(physQuery.Phys_Zip) : ""),
                        phys_phone = trim(physQuery.Phys_Phone),
                        phys_fax = trim(physQuery.Phys_Fax),
                        phys_npi = trim(physQuery.Phys_NPI)
                    } />
                </cfif>
            </cfif>
        </cfif>

        <cfset genderRaw = trim(patientQuery.Pt_Gender) />
        <cfif listFindNoCase("1 - Male,Male", genderRaw)>
            <cfset genderLabel = "Male" />
        <cfelseif listFindNoCase("2 - Female,Female,FeMale", genderRaw)>
            <cfset genderLabel = "Female" />
        <cfelse>
            <cfset genderLabel = formatMobilePatientGender(genderRaw) />
        </cfif>

        <cfswitch expression="#val(patientQuery.Status)#">
            <cfcase value="0"><cfset statusLabel = "Active" /></cfcase>
            <cfcase value="1"><cfset statusLabel = "Inactive" /></cfcase>
            <cfcase value="2"><cfset statusLabel = "Delete" /></cfcase>
            <cfcase value="4"><cfset statusLabel = "Not Taken Under Care" /></cfcase>
            <cfdefaultcase><cfset statusLabel = "" /></cfdefaultcase>
        </cfswitch>

        <cfset var nameParts = [] />
        <cfif len(trim(patientQuery.Pt_First))><cfset arrayAppend(nameParts, trim(patientQuery.Pt_First)) /></cfif>
        <cfif len(trim(patientQuery.Pt_Middle))><cfset arrayAppend(nameParts, trim(patientQuery.Pt_Middle)) /></cfif>
        <cfif len(trim(patientQuery.Pt_Last))><cfset arrayAppend(nameParts, trim(patientQuery.Pt_Last)) /></cfif>
        <cfif len(trim(patientQuery.Pt_Suffix))><cfset arrayAppend(nameParts, trim(patientQuery.Pt_Suffix)) /></cfif>

        <cfset result.ok = true />
        <cfset result.message = "" />
        <cfset result.patient = {
            patient_id = patientQuery.Patient_ID,
            Patient_ID = patientQuery.Patient_ID,
            status = val(patientQuery.Status),
            status_label = statusLabel,
            pt_agy_id = trim(patientQuery.Pt_Agy_ID),
            Pt_Agy_ID = trim(patientQuery.Pt_Agy_ID),
            pt_first = trim(patientQuery.Pt_First),
            Pt_First = trim(patientQuery.Pt_First),
            pt_middle = trim(patientQuery.Pt_Middle),
            Pt_Middle = trim(patientQuery.Pt_Middle),
            pt_last = trim(patientQuery.Pt_Last),
            Pt_Last = trim(patientQuery.Pt_Last),
            pt_suffix = trim(patientQuery.Pt_Suffix),
            Pt_Suffix = trim(patientQuery.Pt_Suffix),
            full_name = arrayToList(nameParts, " "),
            pt_street = trim(patientQuery.Pt_Street),
            Pt_Street = trim(patientQuery.Pt_Street),
            pt_city = trim(patientQuery.Pt_City),
            Pt_City = trim(patientQuery.Pt_City),
            pt_state = trim(patientQuery.Pt_State),
            Pt_State = trim(patientQuery.Pt_State),
            state_name = stateName,
            pt_zip = trim(patientQuery.Pt_Zip),
            Pt_Zip = trim(patientQuery.Pt_Zip),
            pt_phone = trim(patientQuery.Pt_Phone),
            Pt_Phone = trim(patientQuery.Pt_Phone),
            email = trim(patientQuery.Email),
            Email = trim(patientQuery.Email),
            pt_dob = isDate(patientQuery.Pt_DOB) ? dateFormat(patientQuery.Pt_DOB, "yyyy-mm-dd") : "",
            Pt_DOB = isDate(patientQuery.Pt_DOB) ? dateFormat(patientQuery.Pt_DOB, "yyyy-mm-dd") : "",
            age_years = calculateMobilePatientAgeYears(patientQuery.Pt_DOB),
            pt_gender = genderLabel,
            Pt_Gender = genderLabel,
            pt_ssn = reReplace(trim(patientQuery.Pt_SSN), "-", "", "ALL"),
            Pt_SSN = reReplace(trim(patientQuery.Pt_SSN), "-", "", "ALL"),
            pt_ssn_uk = val(patientQuery.Pt_SSN_UK),
            pt_race = trim(patientQuery.Pt_Race),
            Pt_Race = trim(patientQuery.Pt_Race),
            ethnicity = trim(patientQuery.M0140_ETHNIC),
            M0140_ETHNIC = trim(patientQuery.M0140_ETHNIC),
            pt_language = trim(patientQuery.Pt_language),
            Pt_language = trim(patientQuery.Pt_language),
            interpreter = trim(patientQuery.Interpreter),
            Interpreter = trim(patientQuery.Interpreter),
            allergies = trim(patientQuery.Allergies),
            Allergies = trim(patientQuery.Allergies),
            specific_concerns = trim(patientQuery.Specific_Concerns),
            Specific_Concerns = trim(patientQuery.Specific_Concerns),
            notes = trim(patientQuery.Comments),
            Comments = trim(patientQuery.Comments),
            home_directions = trim(patientQuery.Home_Directions),
            Home_Directions = trim(patientQuery.Home_Directions),
            pt_latitude = trim(patientQuery.Pt_Latitude),
            Pt_Latitude = trim(patientQuery.Pt_Latitude),
            pt_longitude = trim(patientQuery.Pt_Longitude),
            Pt_Longitude = trim(patientQuery.Pt_Longitude),
            file_name = trim(patientQuery.FileName),
            admit = admitInfo,
            advance_directive = structKeyExists(admitInfo, "advance_directive") ? admitInfo.advance_directive : "",
            code_status = structKeyExists(admitInfo, "code_status") ? admitInfo.code_status : "",
            physician = physician
        } />
        <cfreturn result />
    </cffunction>

    <!--- Patient General Information screen (GeneralInformation.cfm fields) --->
    <cffunction name="getPatientGeneralInformation" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Patient_ID. Returns patient general information matching GeneralInformation.cfm.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Patient_ID" type="string" required="true" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var patientId = val(trim(arguments.Patient_ID)) />
        <cfset var ctx = {} />

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

        <cftry>
            <cfset ctx = authorizeMobilePatientRequest(
                securityKey = trim(arguments.securityKey),
                patientId = patientId
            ) />
            <cfif NOT ctx.ok>
                <cfset response.message = ctx.message />
                <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
                <cfreturn response />
            </cfif>

            <cfset response.success = true />
            <cfset response.message = "Patient general information loaded" />
            <cfset response.data = {
                patient_id = patientId,
                patient = ctx.patient,
                contacts = loadMobilePatientContactsArray(agencySchema = ctx.agencySchema, patientId = patientId),
                insurance = loadMobilePatientInsuranceArray(agencySchema = ctx.agencySchema, patientId = patientId)
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "getPatientGeneralInformation",
                    failedStep = "load_patient_general_information",
                    context = {
                        patientId = patientId,
                        agencySchema = structKeyExists(ctx, "agencySchema") ? ctx.agencySchema : ""
                    },
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load patient general information: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Active / new-updated / discontinued meds from pPtmeds by Patient_ID --->
    <cffunction name="getActiveMeds" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Patient_ID. Optional med_status: current (default), new_updated, discontinued, all. Returns medications from pPtmeds.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Patient_ID" type="string" required="true" />
        <cfargument name="med_status" type="string" required="false" default="current"
            hint="current|active = Status 0; new_updated = Status 0 and NorC New/Changed; discontinued = Status 1; all = Status 0 or 1" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var patientId = val(trim(arguments.Patient_ID)) />
        <cfset var statusFilter = lcase(trim(arguments.med_status)) />
        <cfset var ctx = {} />
        <cfset var medsQuery = "" />
        <cfset var countQuery = "" />
        <cfset var interactionQuery = "" />
        <cfset var interactionIds = {} />
        <cfset var meds = [] />
        <cfset var row = {} />
        <cfset var doseLabel = "" />
        <cfset var norcVal = "" />
        <cfset var tabKey = "" />
        <cfset var hasWarning = false />
        <cfset var isHighRisk = false />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />
        <cfset var counts = { current = 0, new_updated = 0, discontinued = 0 } />
        <cfset var lookupDb = "hhapowerpath" />

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

        <cfif statusFilter EQ "active">
            <cfset statusFilter = "current" />
        </cfif>
        <cfif NOT listFindNoCase("current,new_updated,discontinued,all", statusFilter)>
            <cfset statusFilter = "current" />
        </cfif>

        <cfset ctx = authorizeMobilePatientRequest(
            securityKey = trim(arguments.securityKey),
            patientId = patientId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <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 lookupDb = trim(Request.prefix_db_lookup) />

        <cfset emailCtx = {
            patientId = patientId,
            medStatus = statusFilter,
            empId = ctx.empId,
            locId = ctx.locId,
            agencySchema = ctx.agencySchema
        } />

        <cftry>
            <cfset currentStep = "count_med_tabs" />
            <cfquery name="countQuery" datasource="#Application.DataSrc#">
                SELECT
                    SUM(CASE WHEN Status = 0 THEN 1 ELSE 0 END) AS current_count,
                    SUM(CASE WHEN Status = 0 AND NorC IN ('New', 'Changed') THEN 1 ELSE 0 END) AS new_updated_count,
                    SUM(CASE WHEN Status = 1 THEN 1 ELSE 0 END) AS discontinued_count
                FROM #ctx.agencySchema#.pPtmeds
                WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#patientId#">
                  AND Status IN (0, 1)
            </cfquery>
            <cfset counts.current = val(countQuery.current_count) />
            <cfset counts.new_updated = val(countQuery.new_updated_count) />
            <cfset counts.discontinued = val(countQuery.discontinued_count) />

            <cfset currentStep = "query_interactions" />
            <cfquery name="interactionQuery" datasource="#Application.DataSrc#">
                SELECT DISTINCT pPtmeds.PtMeds_ID
                FROM #ctx.agencySchema#.pPtmeds
                INNER JOIN #lookupDb#.Meds_New
                    ON pPtmeds.Drug_ID = Meds_New.Trade_Num
                INNER JOIN #ctx.agencySchema#.pPtmeds pPtmeds_Compare
                    ON pPtmeds_Compare.Patient_ID = pPtmeds.Patient_ID
                   AND pPtmeds_Compare.PtMeds_ID <> pPtmeds.PtMeds_ID
                   AND pPtmeds_Compare.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                INNER JOIN #lookupDb#.Meds_New Meds_New_Compare
                    ON pPtmeds_Compare.Drug_ID = Meds_New_Compare.Trade_Num
                INNER JOIN hhapowerpath.Med_Interactions
                    ON LCASE(TRIM(Meds_New.Generic_Name)) LIKE CONCAT('%', LCASE(TRIM(Med_Interactions.drug_generic)), '%')
                   AND LCASE(TRIM(Meds_New_Compare.Generic_Name)) LIKE CONCAT('%', LCASE(TRIM(Med_Interactions.interaction_generic)), '%')
                WHERE pPtmeds.Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#patientId#">
                  AND pPtmeds.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND TRIM(IFNULL(Meds_New.Generic_Name, '')) <> ''
                  AND TRIM(IFNULL(Meds_New_Compare.Generic_Name, '')) <> ''
            </cfquery>
            <cfloop query="interactionQuery">
                <cfset interactionIds["" & interactionQuery.PtMeds_ID] = true />
            </cfloop>

            <cfset currentStep = "query_pPtmeds" />
            <cfquery name="medsQuery" datasource="#Application.DataSrc#">
                SELECT m.PtMeds_ID,
                       m.Patient_ID,
                       m.Drug_ID,
                       m.Drug_Name,
                       m.Dose,
                       m.Dose_Units,
                       m.Route,
                       m.Freq,
                       m.Status,
                       m.NorC,
                       m.Notes,
                       m.In_Home,
                       m.DateOrdered,
                       m.DateDCed,
                       m.CMSOrder_ID,
                       IFNULL(mn.RX_ID, '') AS RX_ID,
                       IFNULL(mn.N0415, '') AS N0415,
                       IFNULL(mn.Generic_Name, '') AS Generic_Name,
                       IFNULL(mn.Classification, '') AS Classification,
                       IFNULL(mn.Med_ID, 0) AS MedsNew_ID
                FROM #ctx.agencySchema#.pPtmeds m
                LEFT OUTER JOIN #lookupDb#.Meds_New mn
                    ON m.Drug_ID = mn.Trade_Num
                WHERE m.Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#patientId#">
                <cfif statusFilter EQ "current">
                    AND m.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                <cfelseif statusFilter EQ "new_updated">
                    AND m.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                    AND m.NorC IN ('New', 'Changed')
                <cfelseif statusFilter EQ "discontinued">
                    AND m.Status = <cfqueryparam cfsqltype="cf_sql_integer" value="1">
                <cfelse>
                    AND m.Status IN (0, 1)
                </cfif>
                ORDER BY m.Status ASC, m.Drug_Name ASC, m.PtMeds_ID ASC
            </cfquery>

            <cfset currentStep = "build_meds_array" />
            <cfloop query="medsQuery">
                <cfset doseLabel = formatMobileMedDoseLabel(medsQuery.Dose, medsQuery.Dose_Units) />
                <cfset norcVal = trim(medsQuery.NorC) />
                <cfif NOT len(norcVal)>
                    <cfset norcVal = "Pending" />
                </cfif>
                <cfif val(medsQuery.Status) EQ 1>
                    <cfset tabKey = "discontinued" />
                <cfelseif listFindNoCase("New,Changed", norcVal)>
                    <cfset tabKey = "new_updated" />
                <cfelse>
                    <cfset tabKey = "current" />
                </cfif>
                <cfset isHighRisk = len(trim(medsQuery.N0415)) GT 0 />
                <cfset hasWarning = structKeyExists(interactionIds, "" & medsQuery.PtMeds_ID) OR isHighRisk />

                <cfset row = {
                    ptmeds_id = medsQuery.PtMeds_ID,
                    patient_id = medsQuery.Patient_ID,
                    drug_id = medsQuery.Drug_ID,
                    drug_name = len(trim(medsQuery.Drug_Name)) ? trim(medsQuery.Drug_Name) : "- Unknown -",
                    dose = trim(medsQuery.Dose),
                    dose_units = trim(medsQuery.Dose_Units),
                    dose_label = doseLabel,
                    route = trim(medsQuery.Route),
                    frequency = trim(medsQuery.Freq),
                    status = val(medsQuery.Status),
                    status_label = (val(medsQuery.Status) EQ 0) ? "active" : "discontinued",
                    norc = norcVal,
                    tab = tabKey,
                    notes = trim(medsQuery.Notes),
                    in_home = val(medsQuery.In_Home),
                    date_ordered = isDate(medsQuery.DateOrdered) ? dateFormat(medsQuery.DateOrdered, "yyyy-mm-dd") : "",
                    date_dced = isDate(medsQuery.DateDCed) ? dateFormat(medsQuery.DateDCed, "yyyy-mm-dd") : "",
                    rx_id = trim(medsQuery.RX_ID),
                    classification = trim(medsQuery.Classification),
                    cms_order_id = val(medsQuery.CMSOrder_ID),
                    generic_name = trim(medsQuery.Generic_Name),
                    meds_new_id = val(medsQuery.MedsNew_ID),
                    high_risk = isHighRisk,
                    has_interaction = structKeyExists(interactionIds, "" & medsQuery.PtMeds_ID),
                    has_warning = hasWarning
                } />
                <cfset arrayAppend(meds, row) />
            </cfloop>

            <cfset response.success = true />
            <cfset response.message = "Medications loaded" />
            <cfset response.data = {
                patient_id = patientId,
                patient = ctx.patient,
                med_status = statusFilter,
                counts = counts,
                medications = meds,
                count = arrayLen(meds)
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "getActiveMeds",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to load medications: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Case-insensitive struct key lookup (mobile JSON may use UPPER_CASE keys) --->
    <cffunction name="getMobileStructValueCI" 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>

    <!--- Parse saveAssessmentAnswers JSON body or form fields --->
    <cffunction name="parseMobileAssessmentSaveRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="assmtIdArg" type="string" required="false" default="" />
        <cfargument name="answersArg" type="string" required="false" default="" />
        <cfset var out = {
            securityKey = trim(arguments.securityKeyArg),
            assmtId = val(trim(arguments.assmtIdArg)),
            answers = []
        } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var answersRaw = "" />

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

        <cfif len(trim(arguments.answersArg))>
            <cfset answersRaw = deserializeJSON(trim(arguments.answersArg)) />
        <cfelseif structKeyExists(form, "answers") AND len(trim(form.answers))>
            <cfset answersRaw = deserializeJSON(trim(form.answers)) />
        </cfif>

        <cftry>
            <cfif NOT len(out.securityKey) OR out.assmtId LTE 0 OR NOT isArray(out.answers) OR arrayLen(out.answers) EQ 0>
                <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 = trim("" & getMobileStructValueCI(bodyJson, "securityKey", "")) />
                    </cfif>
                    <cfif out.assmtId LTE 0>
                        <cfset out.assmtId = val(trim("" & getMobileStructValueCI(bodyJson, "Assmt_ID", 0))) />
                    </cfif>
                    <cfif NOT isArray(answersRaw)>
                        <cfset answersRaw = getMobileStructValueCI(bodyJson, "answers", []) />
                    </cfif>
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>

        <cfif isArray(answersRaw)>
            <cfset out.answers = answersRaw />
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Normalize answer value for pAssessments column storage (checkbox: mobile may send ~ between selections; store as comma-separated) --->
    <cffunction name="normalizeMobileAssessmentAnswerValue" access="private" returntype="string" output="false">
        <cfargument name="rawAnswer" required="true" />
        <cfargument name="questionsType" type="string" required="false" default="" />
        <cfset var qType = lcase(trim(arguments.questionsType)) />
        <cfset var value = "" />

        <cfif isNull(arguments.rawAnswer)>
            <cfreturn "" />
        </cfif>
        <cfif isArray(arguments.rawAnswer)>
            <cfif arrayLen(arguments.rawAnswer) EQ 0>
                <cfreturn "" />
            </cfif>
            <cfreturn trim(arrayToList(arguments.rawAnswer, ",")) />
        </cfif>
        <cfif isStruct(arguments.rawAnswer)>
            <cfreturn trim(serializeJSON(arguments.rawAnswer)) />
        </cfif>

        <cfset value = trim("" & arguments.rawAnswer) />
        <cfif qType EQ "checkbox" AND find("~", value) GT 0>
            <cfreturn trim(reReplace(value, "\s*~\s*", ",", "all")) />
        </cfif>
        <cfreturn value />
    </cffunction>

    <!--- Build answer_label => value map from mobile answers[] payload --->
    <cffunction name="buildMobileAssessmentAnswerUpdates" access="private" returntype="struct" output="false">
        <cfargument name="answers" type="array" required="true" />
        <cfset var updates = {} />
        <cfset var skipped = [] />
        <cfset var item = {} />
        <cfset var idx = 0 />
        <cfset var label = "" />
        <cfset var qType = "" />
        <cfset var rawAnswer = "" />
        <cfset var normalized = "" />
        <cfset var quesId = 0 />

        <cfloop from="1" to="#arrayLen(arguments.answers)#" index="idx">
            <cfset item = arguments.answers[idx] />
            <cfif NOT isStruct(item)>
                <cfset arrayAppend(skipped, { index = idx, reason = "invalid_answer_object" }) />
                <cfcontinue />
            </cfif>

            <cfset label = uCase(trim("" & getMobileStructValueCI(item, "ANSWER_LABEL", ""))) />
            <cfif NOT len(label)>
                <cfset label = uCase(trim("" & getMobileStructValueCI(item, "answer_label", ""))) />
            </cfif>
            <cfset quesId = val(getMobileStructValueCI(item, "QUES_ID", getMobileStructValueCI(item, "ques_id", 0))) />
            <cfset qType = lcase(trim("" & getMobileStructValueCI(item, "QUESTIONS_TYPE", getMobileStructValueCI(item, "questions_type", "")))) />
            <cfset rawAnswer = getMobileStructValueCI(item, "ANSWER", getMobileStructValueCI(item, "answer", "")) />

            <cfif NOT len(label) OR NOT isMobileAssessmentAnswerColumnName(label)>
                <cfset arrayAppend(skipped, { index = idx, ques_id = quesId, answer_label = label, reason = "invalid_answer_label" }) />
                <cfcontinue />
            </cfif>

            <cfset normalized = normalizeMobileAssessmentAnswerValue(rawAnswer = rawAnswer, questionsType = qType) />
            <cfset updates[label] = normalized />
        </cfloop>

        <cfreturn { updates = updates, skipped = skipped } />
    </cffunction>

    <!--- Retire active pAssessments row (Status=2) and insert successor copy (progressnotes retire_assessments pattern) --->
    <cffunction name="retireMobileAssessmentActiveRow" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="assmtId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfset var result = {
            ok = false,
            message = "",
            oldAutoAssmtId = 0,
            newAutoAssmtId = 0,
            columnList = ""
        } />
        <cfset var getold = "" />
        <cfset var getcolumns = "" />
        <cfset var lastAssess = "" />

        <cfquery name="getold" datasource="#Application.DataSrc#">
            SELECT AutoAssmt_ID
            FROM #arguments.agencySchema#.pAssessments
            WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmtId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            ORDER BY AutoAssmt_ID DESC
            LIMIT 1
        </cfquery>

        <cfif getold.recordcount EQ 0>
            <cfset result.message = "Active assessment not found" />
            <cfreturn result />
        </cfif>

        <cfset result.oldAutoAssmtId = val(getold.AutoAssmt_ID) />

        <cfquery name="getcolumns" datasource="#Application.DataSrc#">
            SELECT COLUMN_NAME
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE COLUMN_NAME NOT IN ('AutoAssmt_ID', 'record_timestamp')
              AND TABLE_SCHEMA = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.agencySchema#">
              AND TABLE_NAME = <cfqueryparam cfsqltype="cf_sql_varchar" value="pAssessments">
            ORDER BY ORDINAL_POSITION
        </cfquery>

        <cfif getcolumns.recordcount EQ 0>
            <cfset result.message = "Could not resolve pAssessments columns for retire" />
            <cfreturn result />
        </cfif>

        <cfset result.columnList = valueList(getcolumns.COLUMN_NAME) />

        <cfquery datasource="#Application.DataSrc#">
            INSERT INTO #arguments.agencySchema#.pAssessments (#result.columnList#)
            SELECT #result.columnList#
            FROM #arguments.agencySchema#.pAssessments
            WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.oldAutoAssmtId#">
            LIMIT 1
        </cfquery>

        <cfquery name="lastAssess" datasource="#Application.DataSrc#">
            SELECT LAST_INSERT_ID() AS LastInsertID
        </cfquery>

        <cfset result.newAutoAssmtId = val(lastAssess.LastInsertID) />
        <cfif result.newAutoAssmtId LTE 0>
            <cfset result.message = "Retire insert did not return new AutoAssmt_ID" />
            <cfreturn result />
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pAssessments
            SET Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">,
                Record_Creation_Date = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#now()#">,
                Record_Created_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">,
                Record_Mod_Date = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#now()#">,
                Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.newAutoAssmtId#">
        </cfquery>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pAssessments
            SET Status = <cfqueryparam cfsqltype="cf_sql_integer" value="2">,
                Record_Mod_Date = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#now()#">,
                Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#result.oldAutoAssmtId#">
        </cfquery>

        <cfset result.ok = true />
        <cfset result.message = "Assessment retired and successor row created" />
        <cfreturn result />
    </cffunction>

    <!--- Apply mapped answer_label columns on the new active pAssessments row --->
    <cffunction name="applyMobileAssessmentAnswerUpdatesToRow" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="autoAssmtId" type="numeric" required="true" />
        <cfargument name="answerUpdates" type="struct" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="validColumnList" type="string" required="true" />
        <cfset var result = { applied = [], skipped = [] } />
        <cfset var fieldName = "" />
        <cfset var value = "" />
        <cfset var qColumns = "" />
        <cfset var maxLen = 0 />

        <cfif arguments.autoAssmtId LTE 0 OR structCount(arguments.answerUpdates) EQ 0>
            <cfreturn result />
        </cfif>

        <cfquery name="qColumns" datasource="#Application.DataSrc#">
            SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE TABLE_SCHEMA = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.agencySchema#">
              AND TABLE_NAME = <cfqueryparam cfsqltype="cf_sql_varchar" value="pAssessments">
        </cfquery>

        <cfloop collection="#arguments.answerUpdates#" item="fieldName">
            <cfset fieldName = uCase(trim(fieldName)) />
            <cfif NOT isMobileAssessmentAnswerColumnName(fieldName) OR NOT listFindNoCase(arguments.validColumnList, fieldName)>
                <cfset arrayAppend(result.skipped, { answer_label = fieldName, reason = "column_not_allowed" }) />
                <cfcontinue />
            </cfif>

            <cfset value = trim("" & arguments.answerUpdates[fieldName]) />
            <cfloop query="qColumns">
                <cfif compareNoCase(qColumns.COLUMN_NAME, fieldName) EQ 0 AND val(qColumns.CHARACTER_MAXIMUM_LENGTH) GT 0 AND len(value) GT val(qColumns.CHARACTER_MAXIMUM_LENGTH)>
                    <cfset value = left(value, val(qColumns.CHARACTER_MAXIMUM_LENGTH)) />
                </cfif>
            </cfloop>

            <cftry>
                <cfquery datasource="#Application.DataSrc#">
                    UPDATE #arguments.agencySchema#.pAssessments
                    SET #fieldName# = <cfqueryparam cfsqltype="cf_sql_varchar" value="#value#" null="#NOT len(value)#">,
                        Record_Mod_Date = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#now()#">,
                        Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
                    WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.autoAssmtId#">
                </cfquery>
                <cfset arrayAppend(result.applied, { answer_label = fieldName, value = value }) />
                <cfcatch type="any">
                    <cfset arrayAppend(result.skipped, { answer_label = fieldName, reason = cfcatch.message }) />
                </cfcatch>
            </cftry>
        </cfloop>

        <cfreturn result />
    </cffunction>

    <!--- Save assessment answers: retire Status=0 row (Status=2), insert copy, apply answer_label column updates --->
    <cffunction name="saveAssessmentAnswers" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Assmt_ID, and answers[]. Retires active pAssessments row and inserts updated copy.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Assmt_ID" type="string" required="false" default="" />
        <cfargument name="answers" type="string" required="false" default="" hint="JSON array of answer objects" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = {} />
        <cfset var ctx = {} />
        <cfset var built = {} />
        <cfset var retireResult = {} />
        <cfset var applyResult = {} />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />
        <cfset var skippedAll = [] />
        <cfset var traceRequest = {} />
        <cfset var traceContext = {} />

        <cfset payload = parseMobileAssessmentSaveRequest(
            securityKeyArg = arguments.securityKey,
            assmtIdArg = arguments.Assmt_ID,
            answersArg = arguments.answers
        ) />
        <cfset traceRequest = buildMobileSaveTraceRequestSnapshot(methodName = "saveAssessmentAnswers", payload = payload) />

        <cfif NOT len(payload.securityKey)>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentAnswers",
                response = response,
                requestSnapshot = traceRequest,
                httpStatus = 401
            ) />
        </cfif>
        <cfif payload.assmtId LTE 0>
            <cfset response.message = "Assmt_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentAnswers",
                response = response,
                requestSnapshot = traceRequest,
                httpStatus = 400
            ) />
        </cfif>
        <cfif NOT isArray(payload.answers) OR arrayLen(payload.answers) EQ 0>
            <cfset response.message = "answers array is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentAnswers",
                response = response,
                requestSnapshot = traceRequest,
                httpStatus = 400
            ) />
        </cfif>

        <cfset ctx = authorizeMobileAssmtRequest(
            securityKey = payload.securityKey,
            assmtId = payload.assmtId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentAnswers",
                response = response,
                requestSnapshot = traceRequest,
                context = buildMobileSaveAuthTraceContext(ctx = ctx),
                httpStatus = ctx.statusCode
            ) />
        </cfif>

        <cfset built = buildMobileAssessmentAnswerUpdates(answers = payload.answers) />
        <cfif structCount(built.updates) EQ 0>
            <cfset response.message = "No valid answers to save" />
            <cfset response.data = { skipped = built.skipped } />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentAnswers",
                response = response,
                requestSnapshot = traceRequest,
                context = buildMobileSaveAuthTraceContext(ctx = ctx),
                httpStatus = 400
            ) />
        </cfif>

        <cfset emailCtx = {
            assmtId = payload.assmtId,
            empId = ctx.empId,
            agencySchema = ctx.agencySchema,
            answerCount = arrayLen(payload.answers)
        } />
        <cfset traceContext = duplicate(emailCtx) />
        <cfset traceContext.scheduleQuery = structKeyExists(ctx, "scheduleQuery") ? ctx.scheduleQuery : [] />
        <cfif structKeyExists(ctx, "errorDetail") AND NOT structIsEmpty(ctx.errorDetail)>
            <cfset traceContext.errorDetail = ctx.errorDetail />
        </cfif>
        <cfif structKeyExists(ctx, "schedulesForAssmt")>
            <cfset traceContext.schedulesForAssmt = ctx.schedulesForAssmt />
        </cfif>

        <cftry>
            <cftransaction>
                <cfset currentStep = "retire_and_clone_pAssessments" />
                <cfset retireResult = retireMobileAssessmentActiveRow(
                    agencySchema = ctx.agencySchema,
                    assmtId = payload.assmtId,
                    empId = ctx.empId
                ) />
                <cfif NOT retireResult.ok>
                    <cfthrow type="MobileAssessmentSave" message="#retireResult.message#" />
                </cfif>

                <cfset currentStep = "apply_answer_updates" />
                <cfset applyResult = applyMobileAssessmentAnswerUpdatesToRow(
                    agencySchema = ctx.agencySchema,
                    autoAssmtId = retireResult.newAutoAssmtId,
                    answerUpdates = built.updates,
                    empId = ctx.empId,
                    validColumnList = retireResult.columnList
                ) />
            </cftransaction>

            <cfset response.success = true />
            <cfset response.message = "Assessment answers saved" />
            <cfset skippedAll = duplicate(built.skipped) />
            <cfloop array="#applyResult.skipped#" index="skipItem">
                <cfset arrayAppend(skippedAll, skipItem) />
            </cfloop>
            <cfset response.data = {
                assmt_id = payload.assmtId,
                old_auto_assmt_id = retireResult.oldAutoAssmtId,
                new_auto_assmt_id = retireResult.newAutoAssmtId,
                updated_fields = applyResult.applied,
                updated_count = arrayLen(applyResult.applied),
                skipped = skippedAll
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn returnMobileSaveTraceResponse(
                methodName = "saveAssessmentAnswers",
                response = response,
                requestSnapshot = traceRequest,
                context = traceContext,
                httpStatus = 200
            ) />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "saveAssessmentAnswers",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to save assessment answers: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn returnMobileSaveTraceResponse(
                    methodName = "saveAssessmentAnswers",
                    response = response,
                    requestSnapshot = traceRequest,
                    context = traceContext,
                    httpStatus = 500
                ) />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Search Meds_New catalog (medform Step 2 search) --->
    <cffunction name="searchMedicationCatalog" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and search text. Returns medications from Meds_New for the + Add med picker.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="search" type="string" required="false" default="" />
        <cfargument name="q" type="string" required="false" default="" />
        <cfargument name="limit" type="string" required="false" default="50" />

        <cfset var response = { success = false, message = "", data = { medications = [] } } />
        <cfset var auth = {} />
        <cfset var searchText = trim(arguments.search) />
        <cfset var rowLimit = val(arguments.limit) />
        <cfset var lookupDb = "hhapowerpath" />
        <cfset var medsQuery = "" />
        <cfset var meds = [] />

        <cfif NOT len(searchText)>
            <cfset searchText = trim(arguments.q) />
        </cfif>
        <cfif rowLimit LTE 0 OR rowLimit GT 100>
            <cfset rowLimit = 50 />
        </cfif>

        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif len(searchText) LT 2>
            <cfset response.message = "search text must be at least 2 characters" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />
        <cfif NOT auth.ok>
            <cfset response.message = auth.message />
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <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 lookupDb = trim(Request.prefix_db_lookup) />

        <cftry>
            <cfquery name="medsQuery" datasource="#Application.DataSrc#">
                SELECT Trade_ID,
                       Trade_Num,
                       Name,
                       Trade_Name,
                       Generic_Name,
                       Classification,
                       RX_ID
                FROM #lookupDb#.Meds_New
                WHERE Name LIKE <cfqueryparam value="%#searchText#%" cfsqltype="cf_sql_varchar">
                   OR Trade_Name LIKE <cfqueryparam value="%#searchText#%" cfsqltype="cf_sql_varchar">
                   OR Generic_Name LIKE <cfqueryparam value="%#searchText#%" cfsqltype="cf_sql_varchar">
                ORDER BY Name ASC
                LIMIT <cfqueryparam cfsqltype="cf_sql_integer" value="#rowLimit#">
            </cfquery>

            <cfloop query="medsQuery">
                <cfset arrayAppend(meds, {
                    trade_id = medsQuery.Trade_ID,
                    drug_id = medsQuery.Trade_Num,
                    name = trim(medsQuery.Name),
                    trade_name = trim(medsQuery.Trade_Name),
                    generic_name = trim(medsQuery.Generic_Name),
                    classification = trim(medsQuery.Classification),
                    rx_id = trim(medsQuery.RX_ID),
                    display_name = len(trim(medsQuery.Name)) ? trim(medsQuery.Name) : (len(trim(medsQuery.Trade_Name)) ? trim(medsQuery.Trade_Name) : trim(medsQuery.Generic_Name))
                }) />
            </cfloop>

            <cfset response.success = true />
            <cfset response.message = "Medication catalog search complete" />
            <cfset response.data = { search = searchText, medications = meds, count = arrayLen(meds) } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "searchMedicationCatalog",
                    failedStep = "search_Meds_New",
                    context = { search = searchText, agencySchema = auth.agencySchema },
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to search medications: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Parse savePatientMedication JSON body or form fields --->
    <cffunction name="parseMobileMedicationSaveRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="patientIdArg" type="string" required="false" default="" />
        <cfargument name="medicationArg" type="string" required="false" default="" />
        <cfset var out = {
            securityKey = trim(arguments.securityKeyArg),
            patientId = val(trim(arguments.patientIdArg)),
            medication = {}
        } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var medRaw = "" />

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

        <cfif len(trim(arguments.medicationArg))>
            <cfset medRaw = deserializeJSON(trim(arguments.medicationArg)) />
        <cfelseif structKeyExists(form, "medication") AND len(trim(form.medication))>
            <cfset medRaw = deserializeJSON(trim(form.medication)) />
        </cfif>

        <cftry>
            <cfif NOT len(out.securityKey) OR out.patientId LTE 0 OR NOT isStruct(medRaw)>
                <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 = trim("" & getMobileStructValueCI(bodyJson, "securityKey", "")) />
                    </cfif>
                    <cfif out.patientId LTE 0>
                        <cfset out.patientId = val(trim("" & getMobileStructValueCI(bodyJson, "Patient_ID", getMobileStructValueCI(bodyJson, "patient_id", 0)))) />
                    </cfif>
                    <cfif NOT isStruct(medRaw)>
                        <cfset medRaw = getMobileStructValueCI(bodyJson, "medication", {}) />
                        <cfif NOT isStruct(medRaw) OR structIsEmpty(medRaw)>
                            <cfset medRaw = bodyJson />
                        </cfif>
                    </cfif>
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>

        <cfif isStruct(medRaw)>
            <cfset out.medication = medRaw />
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Normalize medication payload (medform / med_action fields) --->
    <cffunction name="normalizeMobileMedicationPayload" access="private" returntype="struct" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="lookupDb" type="string" required="true" />
        <cfset var out = {
            ok = false,
            action = "insert",
            ptmedsId = val(getMobilePayloadString(arguments.item, "ptmeds_id,PtMeds_ID,MPID,meds_ID")),
            replacePtmedsId = val(getMobilePayloadString(arguments.item, "replace_ptmeds_id,replace_meds_id")),
            patientId = arguments.patientId,
            drugId = val(getMobilePayloadString(arguments.item, "drug_id,Drug_ID,Medication_Trade_Num,trade_num,meds_id")),
            drugName = getMobilePayloadString(arguments.item, "drug_name,Drug_Name,MedName,name"),
            dose = getMobilePayloadString(arguments.item, "dose,Dose"),
            doseUnits = getMobilePayloadString(arguments.item, "dose_units,Dose_Units"),
            route = getMobilePayloadString(arguments.item, "route,Route"),
            freq = getMobilePayloadString(arguments.item, "frequency,Freq,freq"),
            status = val(getMobilePayloadString(arguments.item, "status,Status")),
            notes = getMobilePayloadString(arguments.item, "notes,Notes"),
            dateOrdered = getMobilePayloadString(arguments.item, "date_ordered,DateOrdered,dateordered"),
            dateDced = getMobilePayloadString(arguments.item, "date_dced,DateDCed"),
            referral = getMobilePayloadString(arguments.item, "referral,Referral"),
            inHome = getMobilePayloadString(arguments.item, "in_home,In_Home"),
            norc = getMobilePayloadString(arguments.item, "norc,NorC,new_or_changed")
        } />
        <cfset var actionRaw = lcase(trim(getMobilePayloadString(arguments.item, "action,Action"))) />
        <cfset var medLookup = "" />

        <cfif listFindNoCase("update,edit,save", actionRaw)>
            <cfset out.action = "update" />
        <cfelseif listFindNoCase("insert,add,create", actionRaw)>
            <cfset out.action = "insert" />
        </cfif>

        <cfif out.action EQ "update" AND out.ptmedsId LTE 0>
            <cfreturn out />
        </cfif>
        <cfif NOT len(out.drugName) AND out.drugId GT 0>
            <cfquery name="medLookup" datasource="#Application.DataSrc#">
                SELECT Name, Trade_Name, Generic_Name
                FROM #arguments.lookupDb#.Meds_New
                WHERE Trade_Num = <cfqueryparam cfsqltype="cf_sql_bigint" value="#out.drugId#">
                LIMIT 1
            </cfquery>
            <cfif medLookup.recordCount GT 0>
                <cfif len(trim(medLookup.Name))>
                    <cfset out.drugName = trim(medLookup.Name) />
                <cfelseif len(trim(medLookup.Trade_Name))>
                    <cfset out.drugName = trim(medLookup.Trade_Name) />
                <cfelse>
                    <cfset out.drugName = trim(medLookup.Generic_Name) />
                </cfif>
            </cfif>
        </cfif>
        <cfif NOT len(out.drugName)>
            <cfreturn out />
        </cfif>
        <cfif NOT len(out.norc)>
            <cfset out.norc = "Pending" />
        </cfif>
        <cfif NOT len(out.referral)>
            <cfset out.referral = "0" />
        </cfif>
        <cfif NOT len(out.inHome)>
            <cfset out.inHome = "2" />
        </cfif>
        <cfif NOT len(out.dateOrdered)>
            <cfset out.dateOrdered = dateFormat(now(), "yyyy-mm-dd") />
        </cfif>
        <cfset out.ok = true />
        <cfreturn out />
    </cffunction>

    <!--- Retire active pPtmeds row (Status=3) before versioned insert (med_action.cfm pattern) --->
    <cffunction name="retireMobilePatientMedicationRow" access="private" returntype="boolean" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="ptmedsId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfif arguments.ptmedsId LTE 0>
            <cfreturn false />
        </cfif>
        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pPtmeds
            SET Status = <cfqueryparam cfsqltype="cf_sql_integer" value="3">,
                Record_Mod_Date = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            WHERE PtMeds_ID = <cfqueryparam cfsqltype="cf_sql_bigint" value="#arguments.ptmedsId#">
              AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status IN (0, 1)
        </cfquery>
        <cfreturn true />
    </cffunction>

    <!--- Reset Med_Approve when adding/updating meds (med_action.cfm) --->
    <cffunction name="resetMobilePatientMedApproveFlags" access="private" returntype="void" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfset var getLatestAssmt = "" />
        <cfset var checkCareplan = "" />
        <cfquery name="getLatestAssmt" datasource="#Application.DataSrc#">
            SELECT Assmt_ID
            FROM #arguments.agencySchema#.pAssessments
            WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
              AND (F26 LIKE '1%' OR F26 LIKE '4%')
            ORDER BY Assmt_ID DESC
            LIMIT 1
        </cfquery>
        <cfif getLatestAssmt.recordCount EQ 0>
            <cfreturn />
        </cfif>
        <cfquery name="checkCareplan" datasource="#Application.DataSrc#">
            SELECT COUNT(*) AS careplan_count
            FROM #arguments.agencySchema#.pCareplan
            WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#getLatestAssmt.Assmt_ID#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
        </cfquery>
        <cfif val(checkCareplan.careplan_count) GT 0>
            <cfreturn />
        </cfif>
        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pPatients
            SET Med_Approve = <cfqueryparam cfsqltype="cf_sql_integer" value="0">,
                Med_Recon_Field = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status IN (0, 1, 4)
        </cfquery>
    </cffunction>

    <!--- Insert new pPtmeds row (after retire on update/replace) --->
    <cffunction name="insertMobilePatientMedicationRow" access="private" returntype="struct" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="normalized" type="struct" required="true" />
        <cfargument name="ptmedsId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfset var result = { ok = true, ptmeds_id = arguments.ptmedsId } />
        <cfset var orderedDate = "" />
        <cfset var dcedDate = "" />

        <cfif isDate(arguments.normalized.dateOrdered)>
            <cfset orderedDate = dateFormat(arguments.normalized.dateOrdered, "yyyy-mm-dd") />
        </cfif>
        <cfif isDate(arguments.normalized.dateDced)>
            <cfset dcedDate = dateFormat(arguments.normalized.dateDced, "yyyy-mm-dd") />
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            INSERT INTO #arguments.agencySchema#.pPtmeds (
                PtMeds_ID, Patient_ID, Drug_ID, Drug_Name, Notes, Status,
                Dose, Dose_Units, Route, Freq,
                <cfif len(orderedDate)>DateOrdered,</cfif>
                <cfif len(dcedDate)>DateDCed,</cfif>
                <cfif len(arguments.normalized.referral)>Referral,</cfif>
                <cfif len(arguments.normalized.inHome)>In_Home,</cfif>
                <cfif len(arguments.normalized.norc)>NorC,</cfif>
                Record_Creation_Date, Record_Created_By, Record_Mod_Date, Record_Mod_By
            ) VALUES (
                <cfqueryparam cfsqltype="cf_sql_bigint" value="#arguments.ptmedsId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.patientId#">,
                <cfqueryparam cfsqltype="cf_sql_bigint" value="#arguments.normalized.drugId#" null="#arguments.normalized.drugId LTE 0#">,
                <cfqueryparam value="#arguments.normalized.drugName#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.notes#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.status#">,
                <cfqueryparam value="#arguments.normalized.dose#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.doseUnits#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.route#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.freq#" cfsqltype="cf_sql_varchar">,
                <cfif len(orderedDate)>
                    <cfqueryparam value="#orderedDate#" cfsqltype="cf_sql_date">,
                </cfif>
                <cfif len(dcedDate)>
                    <cfqueryparam value="#dcedDate#" cfsqltype="cf_sql_date">,
                </cfif>
                <cfif len(arguments.normalized.referral)>
                    <cfqueryparam value="#arguments.normalized.referral#" cfsqltype="cf_sql_integer">,
                </cfif>
                <cfif len(arguments.normalized.inHome)>
                    <cfqueryparam value="#arguments.normalized.inHome#" cfsqltype="cf_sql_integer">,
                </cfif>
                <cfif len(arguments.normalized.norc)>
                    <cfqueryparam value="#arguments.normalized.norc#" cfsqltype="cf_sql_varchar">,
                </cfif>
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">,
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            )
        </cfquery>
        <cfreturn result />
    </cffunction>

    <!--- Add or update patient medication (med_action.cfm retire + insert pattern) --->
    <cffunction name="savePatientMedication" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Patient_ID, and medication object. action: insert|add or update|edit. Mirrors medform + Add med.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Patient_ID" type="string" required="false" default="" />
        <cfargument name="medication" type="string" required="false" default="" hint="JSON medication object" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = {} />
        <cfset var ctx = {} />
        <cfset var lookupDb = "hhapowerpath" />
        <cfset var normalized = {} />
        <cfset var ptmedsId = 0 />
        <cfset var existingMed = "" />
        <cfset var nextMed = "" />
        <cfset var insertResult = {} />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />

        <cfset payload = parseMobileMedicationSaveRequest(
            securityKeyArg = arguments.securityKey,
            patientIdArg = arguments.Patient_ID,
            medicationArg = arguments.medication
        ) />

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

        <cfset ctx = authorizeMobilePatientRequest(
            securityKey = payload.securityKey,
            patientId = payload.patientId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <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 lookupDb = trim(Request.prefix_db_lookup) />

        <cfset normalized = normalizeMobileMedicationPayload(
            item = payload.medication,
            patientId = payload.patientId,
            lookupDb = lookupDb
        ) />
        <cfif NOT normalized.ok>
            <cfif normalized.action EQ "update">
                <cfset response.message = "update requires ptmeds_id" />
            <cfelse>
                <cfset response.message = "drug_name or drug_id is required" />
            </cfif>
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>

        <cfset emailCtx = {
            patientId = payload.patientId,
            action = normalized.action,
            ptmedsId = normalized.ptmedsId,
            drugId = normalized.drugId,
            empId = ctx.empId,
            agencySchema = ctx.agencySchema
        } />

        <cftry>
            <cftransaction>
                <cfif normalized.action EQ "update">
                    <cfset currentStep = "verify_existing_med" />
                    <cfquery name="existingMed" datasource="#Application.DataSrc#">
                        SELECT PtMeds_ID
                        FROM #ctx.agencySchema#.pPtmeds
                        WHERE PtMeds_ID = <cfqueryparam cfsqltype="cf_sql_bigint" value="#normalized.ptmedsId#">
                          AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#payload.patientId#">
                          AND Status IN (0, 1)
                        LIMIT 1
                    </cfquery>
                    <cfif existingMed.recordCount EQ 0>
                        <cfthrow type="MobileMedSave" message="Medication not found for update" />
                    </cfif>
                    <cfset ptmedsId = normalized.ptmedsId />
                    <cfset currentStep = "retire_existing_med" />
                    <cfset retireMobilePatientMedicationRow(
                        agencySchema = ctx.agencySchema,
                        ptmedsId = ptmedsId,
                        patientId = payload.patientId,
                        empId = ctx.empId
                    ) />
                <cfelse>
                    <cfif normalized.replacePtmedsId GT 0>
                        <cfset ptmedsId = normalized.replacePtmedsId />
                        <cfset currentStep = "retire_replaced_med" />
                        <cfset retireMobilePatientMedicationRow(
                            agencySchema = ctx.agencySchema,
                            ptmedsId = ptmedsId,
                            patientId = payload.patientId,
                            empId = ctx.empId
                        ) />
                    <cfelse>
                        <cfset currentStep = "next_ptmeds_id" />
                        <cfquery name="nextMed" datasource="#Application.DataSrc#">
                            SELECT IFNULL(MAX(PtMeds_ID), 0) + 1 AS NewPtMeds_ID
                            FROM #ctx.agencySchema#.pPtmeds
                        </cfquery>
                        <cfset ptmedsId = val(nextMed.NewPtMeds_ID) />
                    </cfif>
                </cfif>

                <cfset currentStep = "insert_pPtmeds" />
                <cfset insertResult = insertMobilePatientMedicationRow(
                    agencySchema = ctx.agencySchema,
                    normalized = normalized,
                    ptmedsId = ptmedsId,
                    empId = ctx.empId
                ) />
            </cftransaction>

            <cfset currentStep = "reset_med_approve" />
            <cfset resetMobilePatientMedApproveFlags(
                agencySchema = ctx.agencySchema,
                patientId = payload.patientId
            ) />

            <cfset response.success = true />
            <cfset response.message = (normalized.action EQ "update") ? "Medication updated" : "Medication added" />
            <cfset response.data = {
                patient_id = payload.patientId,
                action = normalized.action,
                ptmeds_id = ptmedsId,
                drug_id = normalized.drugId,
                drug_name = normalized.drugName,
                dose = normalized.dose,
                dose_units = normalized.doseUnits,
                dose_label = formatMobileMedDoseLabel(normalized.dose, normalized.doseUnits),
                route = normalized.route,
                frequency = normalized.freq,
                status = normalized.status,
                norc = normalized.norc,
                notes = normalized.notes,
                date_ordered = isDate(normalized.dateOrdered) ? dateFormat(normalized.dateOrdered, "yyyy-mm-dd") : normalized.dateOrdered,
                date_dced = isDate(normalized.dateDced) ? dateFormat(normalized.dateDced, "yyyy-mm-dd") : normalized.dateDced,
                referral = val(normalized.referral),
                in_home = val(normalized.inHome)
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "savePatientMedication",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to save medication: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- True when any case-insensitive key from keys exists in payload struct --->
    <cffunction name="hasMobilePayloadKey" access="private" returntype="boolean" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfargument name="keys" type="string" required="true" />
        <cfset var k = "" />
        <cfset var structKey = "" />
        <cfloop list="#arguments.keys#" index="k">
            <cfloop collection="#arguments.item#" item="structKey">
                <cfif compareNoCase(structKey, k) EQ 0>
                    <cfreturn true />
                </cfif>
            </cfloop>
        </cfloop>
        <cfreturn false />
    </cffunction>

    <!--- Parse savePatientGeneralInformation JSON body or form fields --->
    <cffunction name="parseMobilePatientGeneralInformationSaveRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="patientIdArg" type="string" required="false" default="" />
        <cfargument name="patientArg" type="string" required="false" default="" />
        <cfset var out = {
            securityKey = trim(arguments.securityKeyArg),
            patientId = val(trim(arguments.patientIdArg)),
            patient = {}
        } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var patientRaw = "" />

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

        <cfif len(trim(arguments.patientArg))>
            <cfset patientRaw = deserializeJSON(trim(arguments.patientArg)) />
        <cfelseif structKeyExists(form, "patient") AND len(trim(form.patient))>
            <cfset patientRaw = deserializeJSON(trim(form.patient)) />
        </cfif>

        <cftry>
            <cfif NOT len(out.securityKey) OR out.patientId LTE 0 OR NOT isStruct(patientRaw)>
                <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 = trim("" & getMobileStructValueCI(bodyJson, "securityKey", "")) />
                    </cfif>
                    <cfif out.patientId LTE 0>
                        <cfset out.patientId = val(trim("" & getMobileStructValueCI(bodyJson, "Patient_ID", getMobileStructValueCI(bodyJson, "patient_id", 0)))) />
                    </cfif>
                    <cfif NOT isStruct(patientRaw)>
                        <cfset patientRaw = getMobileStructValueCI(bodyJson, "patient", {}) />
                        <cfif NOT isStruct(patientRaw) OR structIsEmpty(patientRaw)>
                            <cfset patientRaw = bodyJson />
                        </cfif>
                    </cfif>
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>

        <cfif isStruct(patientRaw)>
            <cfset out.patient = patientRaw />
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Zip/CBSA lookup for patient address (patient_action.cfm) --->
    <cffunction name="lookupMobilePatientZipCbsa" access="private" returntype="struct" output="false">
        <cfargument name="zipCode" type="string" required="true" />
        <cfargument name="lookupDb" type="string" required="true" />
        <cfset var out = {
            found = false,
            zip_latitude = "",
            zip_longitude = "",
            fips_code = "",
            cbsa_code = "",
            urban_rural = "",
            wage_index = ""
        } />
        <cfset var zipQuery = "" />
        <cfset var zip5 = left(reReplace(trim(arguments.zipCode), "[^0-9]", "", "ALL"), 5) />

        <cfif len(zip5) LT 5>
            <cfreturn out />
        </cfif>

        <cfquery name="zipQuery" datasource="#Application.DataSrc#">
            SELECT LATITUDE, LONGITUDE, STATEFIPS, COUNTYANSI, CBSA, UrbanRural, WageIndex
            FROM #arguments.lookupDb#.zip_cbsa
            WHERE ZIPCODE = <cfqueryparam cfsqltype="cf_sql_varchar" value="#zip5#">
            LIMIT 1
        </cfquery>
        <cfif zipQuery.recordcount EQ 0>
            <cfreturn out />
        </cfif>

        <cfset out.found = true />
        <cfset out.zip_latitude = trim("" & zipQuery.LATITUDE) />
        <cfset out.zip_longitude = trim("" & zipQuery.LONGITUDE) />
        <cfset out.fips_code = trim("" & zipQuery.STATEFIPS) & trim("" & zipQuery.COUNTYANSI) />
        <cfset out.cbsa_code = trim("" & zipQuery.CBSA) />
        <cfset out.urban_rural = trim("" & zipQuery.UrbanRural) />
        <cfset out.wage_index = trim("" & zipQuery.WageIndex) />
        <cfreturn out />
    </cffunction>

    <!--- Merge mobile patient payload with existing pPatients row (partial updates supported) --->
    <cffunction name="mergeMobilePatientGeneralInformationPayload" access="private" returntype="struct" output="false">
        <cfargument name="existing" type="query" required="true" />
        <cfargument name="payload" type="struct" required="true" />
        <cfargument name="lookupDb" type="string" required="true" />
        <cfset var out = {
            ok = false,
            status = val(arguments.existing.Status),
            pt_agy_id = trim(arguments.existing.Pt_Agy_ID),
            pt_first = trim(arguments.existing.Pt_First),
            pt_middle = trim(arguments.existing.Pt_Middle),
            pt_last = trim(arguments.existing.Pt_Last),
            pt_suffix = trim(arguments.existing.Pt_Suffix),
            pt_street = trim(arguments.existing.Pt_Street),
            pt_city = trim(arguments.existing.Pt_City),
            pt_state = trim(arguments.existing.Pt_State),
            pt_zip = trim(arguments.existing.Pt_Zip),
            pt_phone = trim(arguments.existing.Pt_Phone),
            email = trim(arguments.existing.Email),
            pt_dob = arguments.existing.Pt_DOB,
            pt_gender = trim(arguments.existing.Pt_Gender),
            pt_ssn = reReplace(trim(arguments.existing.Pt_SSN), "-", "", "ALL"),
            pt_ssn_uk = val(arguments.existing.Pt_SSN_UK),
            pt_race = trim(arguments.existing.Pt_Race),
            m0140_ethnic = trim(arguments.existing.M0140_ETHNIC),
            pt_language = trim(arguments.existing.Pt_language),
            interpreter = trim(arguments.existing.Interpreter),
            allergies = trim(arguments.existing.Allergies),
            specific_concerns = trim(arguments.existing.Specific_Concerns),
            home_directions = trim(arguments.existing.Home_Directions),
            comments = trim(arguments.existing.Comments),
            pt_latitude = trim(arguments.existing.Pt_Latitude),
            pt_longitude = trim(arguments.existing.Pt_Longitude),
            zip_latitude = trim(arguments.existing.Zip_Latitude),
            zip_longitude = trim(arguments.existing.Zip_Longitude),
            fips_code = trim(arguments.existing.FIPS_Code),
            cbsa_code = trim(arguments.existing.CBSA_Code),
            urban_rural = trim(arguments.existing.UrbanRural),
            wage_index = trim(arguments.existing.WageIndex),
            file_name = trim(arguments.existing.FileName)
        } />
        <cfset var zipInfo = {} />
        <cfset var dobRaw = "" />

        <cfif hasMobilePayloadKey(arguments.payload, "status,Status")>
            <cfset out.status = val(getMobilePayloadString(arguments.payload, "status,Status")) />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_agy_id,Pt_Agy_ID")>
            <cfset out.pt_agy_id = getMobilePayloadString(arguments.payload, "pt_agy_id,Pt_Agy_ID") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_first,Pt_First")>
            <cfset out.pt_first = getMobilePayloadString(arguments.payload, "pt_first,Pt_First") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_middle,Pt_Middle")>
            <cfset out.pt_middle = getMobilePayloadString(arguments.payload, "pt_middle,Pt_Middle") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_last,Pt_Last")>
            <cfset out.pt_last = getMobilePayloadString(arguments.payload, "pt_last,Pt_Last") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_suffix,Pt_Suffix")>
            <cfset out.pt_suffix = getMobilePayloadString(arguments.payload, "pt_suffix,Pt_Suffix") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_street,Pt_Street")>
            <cfset out.pt_street = getMobilePayloadString(arguments.payload, "pt_street,Pt_Street") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_city,Pt_City")>
            <cfset out.pt_city = getMobilePayloadString(arguments.payload, "pt_city,Pt_City") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_state,Pt_State")>
            <cfset out.pt_state = getMobilePayloadString(arguments.payload, "pt_state,Pt_State") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_zip,Pt_Zip")>
            <cfset out.pt_zip = getMobilePayloadString(arguments.payload, "pt_zip,Pt_Zip") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_phone,Pt_Phone")>
            <cfset out.pt_phone = getMobilePayloadString(arguments.payload, "pt_phone,Pt_Phone") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "email,Email")>
            <cfset out.email = getMobilePayloadString(arguments.payload, "email,Email") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_dob,Pt_DOB")>
            <cfset dobRaw = getMobilePayloadString(arguments.payload, "pt_dob,Pt_DOB") />
            <cfif isDate(dobRaw)>
                <cfset out.pt_dob = dobRaw />
            </cfif>
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_gender,Pt_Gender")>
            <cfset out.pt_gender = getMobilePayloadString(arguments.payload, "pt_gender,Pt_Gender") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_ssn,Pt_SSN")>
            <cfset out.pt_ssn = reReplace(getMobilePayloadString(arguments.payload, "pt_ssn,Pt_SSN"), "-", "", "ALL") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_ssn_uk,Pt_SSN_UK")>
            <cfset out.pt_ssn_uk = val(getMobilePayloadString(arguments.payload, "pt_ssn_uk,Pt_SSN_UK")) />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_race,Pt_Race")>
            <cfset out.pt_race = getMobilePayloadString(arguments.payload, "pt_race,Pt_Race") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "ethnicity,M0140_ETHNIC,m0140_ethnic")>
            <cfset out.m0140_ethnic = getMobilePayloadString(arguments.payload, "ethnicity,M0140_ETHNIC,m0140_ethnic") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_language,Pt_language")>
            <cfset out.pt_language = getMobilePayloadString(arguments.payload, "pt_language,Pt_language") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "interpreter,Interpreter")>
            <cfset out.interpreter = getMobilePayloadString(arguments.payload, "interpreter,Interpreter") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "allergies,Allergies,otherallergies")>
            <cfset out.allergies = getMobilePayloadString(arguments.payload, "allergies,Allergies,otherallergies") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "specific_concerns,Specific_Concerns,other_concerns")>
            <cfset out.specific_concerns = getMobilePayloadString(arguments.payload, "specific_concerns,Specific_Concerns,other_concerns") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "home_directions,Home_Directions")>
            <cfset out.home_directions = getMobilePayloadString(arguments.payload, "home_directions,Home_Directions") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "comments,Comments,notes")>
            <cfset out.comments = getMobilePayloadString(arguments.payload, "comments,Comments,notes") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_latitude,Pt_Latitude")>
            <cfset out.pt_latitude = getMobilePayloadString(arguments.payload, "pt_latitude,Pt_Latitude") />
        </cfif>
        <cfif hasMobilePayloadKey(arguments.payload, "pt_longitude,Pt_Longitude")>
            <cfset out.pt_longitude = getMobilePayloadString(arguments.payload, "pt_longitude,Pt_Longitude") />
        </cfif>

        <cfif out.pt_first EQ "PLACEHOLDER"><cfset out.pt_first = "" /></cfif>
        <cfif out.pt_last EQ "PLACEHOLDER"><cfset out.pt_last = "" /></cfif>

        <cfif NOT len(out.pt_first) AND NOT len(out.pt_last)>
            <cfreturn out />
        </cfif>
        <cfif NOT listFind("0,1,2,4", "" & out.status)>
            <cfreturn out />
        </cfif>

        <cfif hasMobilePayloadKey(arguments.payload, "pt_zip,Pt_Zip")>
            <cfset zipInfo = lookupMobilePatientZipCbsa(zipCode = out.pt_zip, lookupDb = arguments.lookupDb) />
            <cfif zipInfo.found>
                <cfset out.zip_latitude = zipInfo.zip_latitude />
                <cfset out.zip_longitude = zipInfo.zip_longitude />
                <cfset out.fips_code = zipInfo.fips_code />
                <cfset out.cbsa_code = zipInfo.cbsa_code />
                <cfset out.urban_rural = zipInfo.urban_rural />
                <cfset out.wage_index = zipInfo.wage_index />
            </cfif>
        </cfif>

        <cfif NOT len(out.file_name)>
            <cfset out.file_name = arguments.existing.Patient_ID & ".jpg" />
        </cfif>

        <cfset out.ok = true />
        <cfreturn out />
    </cffunction>

    <!--- Retire active pPatients row (Status=3) before versioned insert (patient_action.cfm) --->
    <cffunction name="retireMobilePatientRow" access="private" returntype="boolean" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pPatients
            SET Status = <cfqueryparam cfsqltype="cf_sql_integer" value="3">,
                Record_Mod_Date = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            WHERE Loc_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.locId#">
              AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status IN (0, 1, 4)
        </cfquery>
        <cfreturn true />
    </cffunction>

    <!--- Insert new pPatients row after retire (patient_action.cfm update path) --->
    <cffunction name="insertMobilePatientGeneralInformationRow" access="private" returntype="void" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="merged" type="struct" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfset var dobValue = "" />

        <cfif isDate(arguments.merged.pt_dob)>
            <cfset dobValue = dateFormat(arguments.merged.pt_dob, "yyyy-mm-dd") />
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            INSERT INTO #arguments.agencySchema#.pPatients (
                Patient_ID, Loc_ID, Status, Pt_Agy_ID,
                Pt_First, Pt_Middle, Pt_Last, Pt_Suffix,
                Pt_Street, Pt_City, Pt_State, Pt_Phone, Pt_Zip,
                <cfif len(dobValue)>Pt_DOB,</cfif>
                Pt_Race, M0140_ETHNIC, Pt_Gender, Pt_language, Interpreter, Allergies,
                Pt_SSN, Pt_SSN_UK, Home_Directions, Specific_Concerns, Email, Comments,
                Zip_Longitude, Zip_Latitude, Pt_Longitude, Pt_Latitude,
                FIPS_Code, CBSA_Code, UrbanRural, WageIndex, FileName,
                Record_Creation_Date, Record_Created_By, Record_Mod_Date, Record_Mod_By
            ) VALUES (
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.locId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.merged.status#">,
                <cfqueryparam value="#arguments.merged.pt_agy_id#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_first#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_middle#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_last#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_suffix#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_street#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_city#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_state#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_phone#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_zip#" cfsqltype="cf_sql_varchar">,
                <cfif len(dobValue)>
                    <cfqueryparam value="#dobValue#" cfsqltype="cf_sql_date">,
                </cfif>
                <cfqueryparam value="#arguments.merged.pt_race#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.m0140_ethnic#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_gender#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_language#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.interpreter#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.allergies#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_ssn#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.merged.pt_ssn_uk#">,
                <cfqueryparam value="#arguments.merged.home_directions#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.specific_concerns#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.email#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.comments#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.zip_longitude#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.zip_latitude#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_longitude#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.pt_latitude#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.fips_code#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.cbsa_code#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.urban_rural#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.wage_index#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.merged.file_name#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">,
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            )
        </cfquery>
    </cffunction>

    <!--- Detect General Information field changes for EVV retransmission (patient_action.cfm) --->
    <cffunction name="mobilePatientGeneralDataChanged" access="private" returntype="boolean" output="false">
        <cfargument name="existing" type="query" required="true" />
        <cfargument name="merged" type="struct" required="true" />
        <cfset var oldDob = arguments.existing.Pt_DOB />
        <cfset var newDob = arguments.merged.pt_dob />

        <cfif val(arguments.merged.status) NEQ val(arguments.existing.Status)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_agy_id) NEQ trim(arguments.existing.Pt_Agy_ID)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_first) NEQ trim(arguments.existing.Pt_First)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_middle) NEQ trim(arguments.existing.Pt_Middle)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_last) NEQ trim(arguments.existing.Pt_Last)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_suffix) NEQ trim(arguments.existing.Pt_Suffix)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_street) NEQ trim(arguments.existing.Pt_Street)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_city) NEQ trim(arguments.existing.Pt_City)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_state) NEQ trim(arguments.existing.Pt_State)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_zip) NEQ trim(arguments.existing.Pt_Zip)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_longitude) NEQ trim(arguments.existing.Pt_Longitude)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_latitude) NEQ trim(arguments.existing.Pt_Latitude)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_phone) NEQ trim(arguments.existing.Pt_Phone)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.email) NEQ trim(arguments.existing.Email)><cfreturn true /></cfif>
        <cfif isDate(oldDob) AND isDate(newDob) AND dateCompare(newDob, oldDob) NEQ 0><cfreturn true /></cfif>
        <cfif (NOT isDate(oldDob) AND isDate(newDob)) OR (isDate(oldDob) AND NOT isDate(newDob))><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_gender) NEQ trim(arguments.existing.Pt_Gender)><cfreturn true /></cfif>
        <cfif reReplace(trim(arguments.merged.pt_ssn), "-", "", "ALL") NEQ reReplace(trim(arguments.existing.Pt_SSN), "-", "", "ALL")><cfreturn true /></cfif>
        <cfif val(arguments.merged.pt_ssn_uk) NEQ val(arguments.existing.Pt_SSN_UK)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_race) NEQ trim(arguments.existing.Pt_Race)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.m0140_ethnic) NEQ trim(arguments.existing.M0140_ETHNIC)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.pt_language) NEQ trim(arguments.existing.Pt_language)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.interpreter) NEQ trim(arguments.existing.Interpreter)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.allergies) NEQ trim(arguments.existing.Allergies)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.specific_concerns) NEQ trim(arguments.existing.Specific_Concerns)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.home_directions) NEQ trim(arguments.existing.Home_Directions)><cfreturn true /></cfif>
        <cfif trim(arguments.merged.comments) NEQ trim(arguments.existing.Comments)><cfreturn true /></cfif>
        <cfreturn false />
    </cffunction>

    <!--- Queue EVV retransmission when patient demographics change (agency 1184) --->
    <cffunction name="triggerMobilePatientEvvRetransmissionIfNeeded" access="private" returntype="void" output="false">
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="dataChanged" type="boolean" required="true" />
        <cfif NOT arguments.dataChanged OR arguments.locId NEQ 1184>
            <cfreturn />
        </cfif>
        <cftry>
            <cfset createObject("component", "components.sandata.AHCCCS").init().markEVVForRetransmission(
                recordType = "patient",
                recordID = arguments.patientId,
                agencyID = arguments.locId
            ) />
            <cfset createObject("component", "components.evv.EVVPatientAutomation").init().queuePatientAutomation(
                patientID = arguments.patientId,
                agencyID = val(arguments.locId),
                triggerSource = "patient_update",
                createdBy = val(arguments.empId)
            ) />
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <!--- Submit patient general information updates (GeneralInformation.cfm / patient_action.cfm) --->
    <cffunction name="savePatientGeneralInformation" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Patient_ID, and patient object. Mirrors Edit Information on General Information. Supports partial field updates.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Patient_ID" type="string" required="false" default="" />
        <cfargument name="patient" type="string" required="false" default="" hint="JSON patient object with fields to update" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = {} />
        <cfset var ctx = {} />
        <cfset var lookupDb = "hhapowerpath" />
        <cfset var existingQuery = "" />
        <cfset var merged = {} />
        <cfset var dataChanged = false />
        <cfset var reloadCtx = {} />
        <cfset var currentStep = "" />
        <cfset var emailCtx = {} />

        <cfset payload = parseMobilePatientGeneralInformationSaveRequest(
            securityKeyArg = arguments.securityKey,
            patientIdArg = arguments.Patient_ID,
            patientArg = arguments.patient
        ) />

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

        <cfset ctx = authorizeMobilePatientRequest(
            securityKey = payload.securityKey,
            patientId = payload.patientId
        ) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <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 lookupDb = trim(Request.prefix_db_lookup) />

        <cfset emailCtx = {
            patientId = payload.patientId,
            empId = ctx.empId,
            agencySchema = ctx.agencySchema,
            locId = ctx.locId
        } />

        <cftry>
            <cfset currentStep = "load_existing_patient" />
            <cfquery name="existingQuery" datasource="#Application.DataSrc#">
                SELECT *
                FROM #ctx.agencySchema#.pPatients
                WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#payload.patientId#">
                  AND Status IN (0, 1, 4)
                LIMIT 1
            </cfquery>
            <cfif existingQuery.recordcount EQ 0>
                <cfset response.message = "Patient not found or inactive" />
                <cfheader statuscode="404" statustext="Not Found" />
                <cfreturn response />
            </cfif>

            <cfset currentStep = "merge_patient_payload" />
            <cfset merged = mergeMobilePatientGeneralInformationPayload(
                existing = existingQuery,
                payload = payload.patient,
                lookupDb = lookupDb
            ) />
            <cfif NOT merged.ok>
                <cfset response.message = "pt_first and pt_last are required; status must be 0, 1, 2, or 4" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>

            <cfset dataChanged = mobilePatientGeneralDataChanged(existing = existingQuery, merged = merged) />

            <cftransaction>
                <cfset currentStep = "retire_existing_patient" />
                <cfset retireMobilePatientRow(
                    agencySchema = ctx.agencySchema,
                    patientId = payload.patientId,
                    locId = ctx.locId,
                    empId = ctx.empId
                ) />
                <cfset currentStep = "insert_pPatients" />
                <cfset insertMobilePatientGeneralInformationRow(
                    agencySchema = ctx.agencySchema,
                    patientId = payload.patientId,
                    locId = ctx.locId,
                    merged = merged,
                    empId = ctx.empId
                ) />
            </cftransaction>

            <cfset currentStep = "evv_retransmission" />
            <cfset triggerMobilePatientEvvRetransmissionIfNeeded(
                patientId = payload.patientId,
                locId = ctx.locId,
                empId = ctx.empId,
                dataChanged = dataChanged
            ) />

            <cfset currentStep = "reload_patient" />
            <cfset reloadCtx = authorizeMobilePatientRequest(
                securityKey = payload.securityKey,
                patientId = payload.patientId
            ) />

            <cfset response.success = true />
            <cfset response.message = "Patient general information updated" />
            <cfset response.data = {
                patient_id = payload.patientId,
                data_changed = dataChanged,
                patient = reloadCtx.ok ? reloadCtx.patient : {}
            } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(
                    methodName = "savePatientGeneralInformation",
                    failedStep = currentStep,
                    context = emailCtx,
                    errorCatch = cfcatch
                ) />
                <cfset response.success = false />
                <cfset response.message = "Failed to save patient information: " & cfcatch.message />
                <cfif len(trim(cfcatch.detail))>
                    <cfset response.message = response.message & " (" & cfcatch.detail & ")" />
                </cfif>
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Format one pPtContacts row for mobile JSON --->
    <cffunction name="formatMobilePatientContactRow" access="private" returntype="struct" output="false">
        <cfargument name="row" type="query" required="true" />
        <cfargument name="rowIndex" type="numeric" required="true" />
        <cfset var statusVal = val(arguments.row.Status[arguments.rowIndex]) />
        <cfset var statusLabel = "Active" />
        <cfset var emergencyVal = trim("" & arguments.row.Emergency[arguments.rowIndex]) />
        <cfset var firstName = trim(arguments.row.First_Name[arguments.rowIndex]) />
        <cfset var lastName = trim(arguments.row.Last_Name[arguments.rowIndex]) />
        <cfswitch expression="#statusVal#">
            <cfcase value="1"><cfset statusLabel = "Inactive" /></cfcase>
            <cfcase value="2"><cfset statusLabel = "Delete" /></cfcase>
            <cfcase value="3"><cfset statusLabel = "Retired" /></cfcase>
        </cfswitch>
        <cfreturn {
            contact_id = val(arguments.row.Contact_ID[arguments.rowIndex]),
            Contact_ID = val(arguments.row.Contact_ID[arguments.rowIndex]),
            patient_id = val(arguments.row.Patient_ID[arguments.rowIndex]),
            status = statusVal,
            status_label = statusLabel,
            first_name = firstName,
            First_Name = firstName,
            last_name = lastName,
            Last_Name = lastName,
            full_name = trim(firstName & " " & lastName),
            relationship = trim(arguments.row.Pt_Realtionship[arguments.rowIndex]),
            Pt_Realtionship = trim(arguments.row.Pt_Realtionship[arguments.rowIndex]),
            emergency = (emergencyVal EQ "1" OR lcase(emergencyVal) EQ "yes"),
            Emergency = val(emergencyVal EQ "1" OR lcase(emergencyVal) EQ "yes"),
            contact_email = trim(arguments.row.Contact_Email[arguments.rowIndex]),
            Contact_Email = trim(arguments.row.Contact_Email[arguments.rowIndex]),
            phone1 = trim(arguments.row.Telephone1[arguments.rowIndex]),
            Telephone1 = trim(arguments.row.Telephone1[arguments.rowIndex]),
            phone2 = trim(arguments.row.Telephone2[arguments.rowIndex]),
            Telephone2 = trim(arguments.row.Telephone2[arguments.rowIndex]),
            location = structKeyExists(arguments.row, "Location") ? trim(arguments.row.Location[arguments.rowIndex]) : (structKeyExists(arguments.row, "location") ? trim(arguments.row.location[arguments.rowIndex]) : ""),
            availability = structKeyExists(arguments.row, "Availability") ? trim(arguments.row.Availability[arguments.rowIndex]) : "",
            notes = structKeyExists(arguments.row, "Notes") ? trim(arguments.row.Notes[arguments.rowIndex]) : "",
            m2102_adl = structKeyExists(arguments.row, "M2102_CARE_TYPE_SRC_ADL") ? trim(arguments.row.M2102_CARE_TYPE_SRC_ADL[arguments.rowIndex]) : "",
            m2102_medication = structKeyExists(arguments.row, "M2102_CARE_TYPE_SRC_MDCTN") ? trim(arguments.row.M2102_CARE_TYPE_SRC_MDCTN[arguments.rowIndex]) : "",
            m2102_procedure = structKeyExists(arguments.row, "M2102_CARE_TYPE_SRC_PRCDR") ? trim(arguments.row.M2102_CARE_TYPE_SRC_PRCDR[arguments.rowIndex]) : "",
            m2102_supervision = structKeyExists(arguments.row, "M2102_CARE_TYPE_SRC_SPRVSN") ? trim(arguments.row.M2102_CARE_TYPE_SRC_SPRVSN[arguments.rowIndex]) : ""
        } />
    </cffunction>

    <!--- Load active/inactive patient contacts from pPtContacts --->
    <cffunction name="loadMobilePatientContactsArray" access="private" returntype="array" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfset var contacts = [] />
        <cfset var contactsQuery = "" />
        <cfquery name="contactsQuery" datasource="#Application.DataSrc#">
            SELECT *
            FROM #arguments.agencySchema#.pPtContacts
            WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status < <cfqueryparam cfsqltype="cf_sql_integer" value="2">
            ORDER BY Emergency DESC, Status ASC, Contact_ID ASC
        </cfquery>
        <cfloop query="contactsQuery">
            <cfset arrayAppend(contacts, formatMobilePatientContactRow(row = contactsQuery, rowIndex = contactsQuery.currentRow)) />
        </cfloop>
        <cfreturn contacts />
    </cffunction>

    <!--- Format one pPtPayer row (joined with pPayer) for mobile JSON --->
    <cffunction name="formatMobilePatientInsuranceRow" access="private" returntype="struct" output="false">
        <cfargument name="row" type="query" required="true" />
        <cfargument name="rowIndex" type="numeric" required="true" />
        <cfset var statusVal = val(arguments.row.Status[arguments.rowIndex]) />
        <cfset var statusLabel = "Active" />
        <cfset var subDob = "" />
        <cfswitch expression="#statusVal#">
            <cfcase value="1"><cfset statusLabel = "Inactive" /></cfcase>
            <cfcase value="2"><cfset statusLabel = "Delete" /></cfcase>
            <cfcase value="3"><cfset statusLabel = "Retired" /></cfcase>
        </cfswitch>
        <cfif isDate(arguments.row.Sub_DOB[arguments.rowIndex])>
            <cfset subDob = dateFormat(arguments.row.Sub_DOB[arguments.rowIndex], "yyyy-mm-dd") />
        </cfif>
        <cfreturn {
            ptpayer_id = val(arguments.row.PtPayer_ID[arguments.rowIndex]),
            PtPayer_ID = val(arguments.row.PtPayer_ID[arguments.rowIndex]),
            patient_id = val(arguments.row.Patient_ID[arguments.rowIndex]),
            status = statusVal,
            status_label = statusLabel,
            payer_id = val(arguments.row.Payer_ID[arguments.rowIndex]),
            Payer_ID = val(arguments.row.Payer_ID[arguments.rowIndex]),
            payer_name = structKeyExists(arguments.row, "Pay_Name") ? trim(arguments.row.Pay_Name[arguments.rowIndex]) : "",
            Pay_Name = structKeyExists(arguments.row, "Pay_Name") ? trim(arguments.row.Pay_Name[arguments.rowIndex]) : "",
            payer_output = structKeyExists(arguments.row, "Output") ? trim(arguments.row.Output[arguments.rowIndex]) : "",
            pt_relationship = trim(arguments.row.Pt_Relationship[arguments.rowIndex]),
            Pt_Relationship = trim(arguments.row.Pt_Relationship[arguments.rowIndex]),
            sub_hic = trim(arguments.row.Sub_HIC[arguments.rowIndex]),
            Sub_HIC = trim(arguments.row.Sub_HIC[arguments.rowIndex]),
            sub_first = trim(arguments.row.Sub_First[arguments.rowIndex]),
            Sub_First = trim(arguments.row.Sub_First[arguments.rowIndex]),
            sub_last = trim(arguments.row.Sub_Last[arguments.rowIndex]),
            Sub_Last = trim(arguments.row.Sub_Last[arguments.rowIndex]),
            sub_mid = trim(arguments.row.Sub_Mid[arguments.rowIndex]),
            Sub_Mid = trim(arguments.row.Sub_Mid[arguments.rowIndex]),
            sub_street = trim(arguments.row.Sub_Street[arguments.rowIndex]),
            sub_city = trim(arguments.row.Sub_City[arguments.rowIndex]),
            sub_state = trim(arguments.row.Sub_State[arguments.rowIndex]),
            sub_zip = trim(arguments.row.Sub_Zip[arguments.rowIndex]),
            sub_gender = trim(arguments.row.Sub_Gender[arguments.rowIndex]),
            sub_dob = subDob,
            Sub_DOB = subDob,
            plan_program = trim(arguments.row.Plan_Program[arguments.rowIndex]),
            plan_type = trim(arguments.row.Plan_type[arguments.rowIndex]),
            msp_code = trim(arguments.row.MSP_code[arguments.rowIndex]),
            policy_group_number = trim(arguments.row.Policy_Group_Number[arguments.rowIndex]),
            policy_group_name = trim(arguments.row.Policy_Group_Name[arguments.rowIndex]),
            payer_responsibility = trim(arguments.row.Payer_Responsibility[arguments.rowIndex]),
            Payer_Responsibility = trim(arguments.row.Payer_Responsibility[arguments.rowIndex]),
            util_force = val(arguments.row.Util_Force[arguments.rowIndex]),
            date_start = isDate(arguments.row.Date_Start[arguments.rowIndex]) ? dateFormat(arguments.row.Date_Start[arguments.rowIndex], "yyyy-mm-dd") : "",
            date_end = isDate(arguments.row.Date_End[arguments.rowIndex]) ? dateFormat(arguments.row.Date_End[arguments.rowIndex], "yyyy-mm-dd") : "",
            deductible_amount = structKeyExists(arguments.row, "Deductible_Amount") ? trim("" & arguments.row.Deductible_Amount[arguments.rowIndex]) : "",
            deductible_met = val(arguments.row.Deductible_Met[arguments.rowIndex]),
            copay_amount = structKeyExists(arguments.row, "Copay_Amount") ? trim("" & arguments.row.Copay_Amount[arguments.rowIndex]) : "",
            op_amount = structKeyExists(arguments.row, "OP_Amount") ? trim("" & arguments.row.OP_Amount[arguments.rowIndex]) : "",
            op_amount_met = val(arguments.row.OP_Amount_Met[arguments.rowIndex]),
            coinsurance = val(arguments.row.Coinsurance[arguments.rowIndex]),
            part_a_start = isDate(arguments.row.Part_A_Start[arguments.rowIndex]) ? dateFormat(arguments.row.Part_A_Start[arguments.rowIndex], "yyyy-mm-dd") : "",
            part_a_term = isDate(arguments.row.Part_A_Term[arguments.rowIndex]) ? dateFormat(arguments.row.Part_A_Term[arguments.rowIndex], "yyyy-mm-dd") : "",
            part_b_start = isDate(arguments.row.Part_B_Start[arguments.rowIndex]) ? dateFormat(arguments.row.Part_B_Start[arguments.rowIndex], "yyyy-mm-dd") : "",
            part_b_term = isDate(arguments.row.Part_B_Term[arguments.rowIndex]) ? dateFormat(arguments.row.Part_B_Term[arguments.rowIndex], "yyyy-mm-dd") : ""
        } />
    </cffunction>

    <!--- Load patient insurance rows from pPtPayer (+ pPayer name) --->
    <cffunction name="loadMobilePatientInsuranceArray" access="private" returntype="array" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfset var insurance = [] />
        <cfset var payerQuery = "" />
        <cfquery name="payerQuery" datasource="#Application.DataSrc#">
            SELECT pPtPayer.*, pPayer.Pay_Name, pPayer.Output
            FROM #arguments.agencySchema#.pPtPayer
            LEFT JOIN #arguments.agencySchema#.pPayer ON pPayer.Pay_ID = pPtPayer.Payer_ID AND pPayer.Status = 0
            WHERE pPtPayer.Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND pPtPayer.Payer_ID <> 0
              AND pPtPayer.Status < 2
            ORDER BY pPtPayer.Payer_Responsibility ASC, pPtPayer.PtPayer_ID ASC
        </cfquery>
        <cfloop query="payerQuery">
            <cfset arrayAppend(insurance, formatMobilePatientInsuranceRow(row = payerQuery, rowIndex = payerQuery.currentRow)) />
        </cfloop>
        <cfreturn insurance />
    </cffunction>

    <!--- Parse savePatientContact JSON body --->
    <cffunction name="parseMobilePatientContactSaveRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="patientIdArg" type="string" required="false" default="" />
        <cfargument name="contactArg" type="string" required="false" default="" />
        <cfset var out = { securityKey = trim(arguments.securityKeyArg), patientId = val(trim(arguments.patientIdArg)), contact = {} } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var contactRaw = "" />
        <cfif NOT len(out.securityKey) AND structKeyExists(form, "securityKey")><cfset out.securityKey = trim(form.securityKey) /></cfif>
        <cfif out.patientId LTE 0 AND structKeyExists(form, "Patient_ID")><cfset out.patientId = val(trim(form.Patient_ID)) /></cfif>
        <cfif len(trim(arguments.contactArg))><cfset contactRaw = deserializeJSON(trim(arguments.contactArg)) />
        <cfelseif structKeyExists(form, "contact") AND len(trim(form.contact))><cfset contactRaw = deserializeJSON(trim(form.contact)) /></cfif>
        <cftry>
            <cfif NOT len(out.securityKey) OR out.patientId LTE 0 OR NOT isStruct(contactRaw)>
                <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 = trim("" & getMobileStructValueCI(bodyJson, "securityKey", "")) /></cfif>
                    <cfif out.patientId LTE 0><cfset out.patientId = val(trim("" & getMobileStructValueCI(bodyJson, "Patient_ID", getMobileStructValueCI(bodyJson, "patient_id", 0)))) /></cfif>
                    <cfif NOT isStruct(contactRaw)>
                        <cfset contactRaw = getMobileStructValueCI(bodyJson, "contact", {}) />
                        <cfif NOT isStruct(contactRaw) OR structIsEmpty(contactRaw)><cfset contactRaw = bodyJson /></cfif>
                    </cfif>
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>
        <cfif isStruct(contactRaw)><cfset out.contact = contactRaw /></cfif>
        <cfreturn out />
    </cffunction>

    <!--- Normalize contact payload (contactaction.cfm fields) --->
    <cffunction name="normalizeMobilePatientContactPayload" access="private" returntype="struct" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfargument name="existing" type="query" required="false" />
        <cfset var out = {
            ok = false, action = "insert", contactId = val(getMobilePayloadString(arguments.item, "contact_id,Contact_ID,CID")),
            status = 0, firstName = "", lastName = "", relationship = "", emergency = 0,
            contactEmail = "", phone1 = "", phone2 = "", location = "", availability = "", notes = "",
            m2102Adl = "", m2102Medication = "", m2102Procedure = "", m2102Supervision = ""
        } />
        <cfset var actionRaw = lcase(trim(getMobilePayloadString(arguments.item, "action,Action"))) />
        <cfset var emergencyRaw = getMobilePayloadString(arguments.item, "emergency,Emergency") />
        <cfif listFindNoCase("update,edit,save", actionRaw)><cfset out.action = "update" />
        <cfelseif listFindNoCase("insert,add,create", actionRaw)><cfset out.action = "insert" /></cfif>
        <cfif arguments.existing.recordcount GT 0>
            <cfset out.status = val(arguments.existing.Status) />
            <cfset out.firstName = trim(arguments.existing.First_Name) />
            <cfset out.lastName = trim(arguments.existing.Last_Name) />
            <cfset out.relationship = trim(arguments.existing.Pt_Realtionship) />
            <cfset out.emergency = val(trim(arguments.existing.Emergency) EQ "1") />
            <cfset out.contactEmail = trim(arguments.existing.Contact_Email) />
            <cfset out.phone1 = trim(arguments.existing.Telephone1) />
            <cfset out.phone2 = trim(arguments.existing.Telephone2) />
            <cfif structKeyExists(arguments.existing, "Location")><cfset out.location = trim(arguments.existing.Location) /></cfif>
            <cfif structKeyExists(arguments.existing, "Availability")><cfset out.availability = trim(arguments.existing.Availability) /></cfif>
            <cfif structKeyExists(arguments.existing, "Notes")><cfset out.notes = trim(arguments.existing.Notes) /></cfif>
        </cfif>
        <cfif hasMobilePayloadKey(arguments.item, "status,Status")><cfset out.status = val(getMobilePayloadString(arguments.item, "status,Status")) /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "first_name,First_Name")><cfset out.firstName = getMobilePayloadString(arguments.item, "first_name,First_Name") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "last_name,Last_Name")><cfset out.lastName = getMobilePayloadString(arguments.item, "last_name,Last_Name") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "relationship,Pt_Realtionship,pt_realtionship")><cfset out.relationship = getMobilePayloadString(arguments.item, "relationship,Pt_Realtionship,pt_realtionship") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "contact_email,Contact_Email,email")><cfset out.contactEmail = getMobilePayloadString(arguments.item, "contact_email,Contact_Email,email") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "phone1,Telephone1,telephone1")><cfset out.phone1 = getMobilePayloadString(arguments.item, "phone1,Telephone1,telephone1") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "phone2,Telephone2,telephone2")><cfset out.phone2 = getMobilePayloadString(arguments.item, "phone2,Telephone2,telephone2") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "location,Location")><cfset out.location = getMobilePayloadString(arguments.item, "location,Location") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "availability,Availability")><cfset out.availability = getMobilePayloadString(arguments.item, "availability,Availability") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "notes,Notes")><cfset out.notes = getMobilePayloadString(arguments.item, "notes,Notes") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "m2102_adl,M2102_CARE_TYPE_SRC_ADL")><cfset out.m2102Adl = getMobilePayloadString(arguments.item, "m2102_adl,M2102_CARE_TYPE_SRC_ADL") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "m2102_medication,M2102_CARE_TYPE_SRC_MDCTN")><cfset out.m2102Medication = getMobilePayloadString(arguments.item, "m2102_medication,M2102_CARE_TYPE_SRC_MDCTN") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "m2102_procedure,M2102_CARE_TYPE_SRC_PRCDR")><cfset out.m2102Procedure = getMobilePayloadString(arguments.item, "m2102_procedure,M2102_CARE_TYPE_SRC_PRCDR") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "m2102_supervision,M2102_CARE_TYPE_SRC_SPRVSN")><cfset out.m2102Supervision = getMobilePayloadString(arguments.item, "m2102_supervision,M2102_CARE_TYPE_SRC_SPRVSN") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "emergency,Emergency")>
            <cfif emergencyRaw EQ "1" OR lcase(emergencyRaw) EQ "yes" OR lcase(emergencyRaw) EQ "true"><cfset out.emergency = 1 />
            <cfelse><cfset out.emergency = 0 /></cfif>
        </cfif>
        <cfif out.firstName EQ "PLACEHOLDER"><cfset out.firstName = "" /></cfif>
        <cfif out.lastName EQ "PLACEHOLDER"><cfset out.lastName = "" /></cfif>
        <cfif out.action EQ "update" AND out.contactId LTE 0><cfreturn out /></cfif>
        <cfif out.action EQ "insert" AND out.contactId GT 0><cfset out.action = "update" /></cfif>
        <cfif NOT len(out.firstName) OR NOT len(out.lastName) OR NOT len(out.phone1)><cfreturn out /></cfif>
        <cfif NOT listFind("0,1,2", "" & out.status)><cfset out.status = 0 /></cfif>
        <cfset out.ok = true />
        <cfreturn out />
    </cffunction>

    <!--- Retire pPtContacts row before versioned insert --->
    <cffunction name="retireMobilePatientContactRow" access="private" returntype="boolean" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="contactId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pPtContacts
            SET Status = <cfqueryparam cfsqltype="cf_sql_integer" value="3">,
                Record_Mod_date = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            WHERE Contact_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.contactId#">
              AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status IN (0, 1)
        </cfquery>
        <cfreturn true />
    </cffunction>

    <!--- Insert pPtContacts row --->
    <cffunction name="insertMobilePatientContactRow" access="private" returntype="void" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="contactId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="normalized" type="struct" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfquery datasource="#Application.DataSrc#">
            INSERT INTO #arguments.agencySchema#.pPtContacts (
                Contact_ID, Patient_ID, Agency_ID, Status, First_Name, Last_Name,
                Pt_Realtionship, Emergency, Contact_Email, Telephone1, Telephone2,
                Location, Availability, Notes,
                M2102_CARE_TYPE_SRC_ADL, M2102_CARE_TYPE_SRC_MDCTN, M2102_CARE_TYPE_SRC_PRCDR, M2102_CARE_TYPE_SRC_SPRVSN,
                Record_Creation_Date, Record_Created_By, Record_Mod_date, Record_Mod_By
            ) VALUES (
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.contactId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.locId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.status#">,
                <cfqueryparam value="#arguments.normalized.firstName#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.lastName#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.relationship#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.emergency#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.contactEmail#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.phone1#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.phone2#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.location#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.availability#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.notes#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.m2102Adl#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.m2102Medication#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.m2102Procedure#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.m2102Supervision#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">,
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            )
        </cfquery>
    </cffunction>

    <!--- Parse savePatientInsurance JSON body --->
    <cffunction name="parseMobilePatientInsuranceSaveRequest" access="private" returntype="struct" output="false">
        <cfargument name="securityKeyArg" type="string" required="false" default="" />
        <cfargument name="patientIdArg" type="string" required="false" default="" />
        <cfargument name="insuranceArg" type="string" required="false" default="" />
        <cfset var out = { securityKey = trim(arguments.securityKeyArg), patientId = val(trim(arguments.patientIdArg)), insurance = {} } />
        <cfset var httpData = {} />
        <cfset var bodyText = "" />
        <cfset var bodyJson = {} />
        <cfset var insuranceRaw = "" />
        <cfif NOT len(out.securityKey) AND structKeyExists(form, "securityKey")><cfset out.securityKey = trim(form.securityKey) /></cfif>
        <cfif out.patientId LTE 0 AND structKeyExists(form, "Patient_ID")><cfset out.patientId = val(trim(form.Patient_ID)) /></cfif>
        <cfif len(trim(arguments.insuranceArg))><cfset insuranceRaw = deserializeJSON(trim(arguments.insuranceArg)) />
        <cfelseif structKeyExists(form, "insurance") AND len(trim(form.insurance))><cfset insuranceRaw = deserializeJSON(trim(form.insurance)) /></cfif>
        <cftry>
            <cfif NOT len(out.securityKey) OR out.patientId LTE 0 OR NOT isStruct(insuranceRaw)>
                <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 = trim("" & getMobileStructValueCI(bodyJson, "securityKey", "")) /></cfif>
                    <cfif out.patientId LTE 0><cfset out.patientId = val(trim("" & getMobileStructValueCI(bodyJson, "Patient_ID", getMobileStructValueCI(bodyJson, "patient_id", 0)))) /></cfif>
                    <cfif NOT isStruct(insuranceRaw)>
                        <cfset insuranceRaw = getMobileStructValueCI(bodyJson, "insurance", {}) />
                        <cfif NOT isStruct(insuranceRaw) OR structIsEmpty(insuranceRaw)><cfset insuranceRaw = bodyJson /></cfif>
                    </cfif>
                </cfif>
            </cfif>
            <cfcatch type="any"></cfcatch>
        </cftry>
        <cfif isStruct(insuranceRaw)><cfset out.insurance = insuranceRaw /></cfif>
        <cfreturn out />
    </cffunction>

    <!--- Normalize insurance payload; copy patient demographics when relationship is 18 Self --->
    <cffunction name="normalizeMobilePatientInsurancePayload" access="private" returntype="struct" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfargument name="existing" type="query" required="false" />
        <cfargument name="patientRow" type="query" required="false" />
        <cfset var out = {
            ok = false, action = "insert", ptPayerId = val(getMobilePayloadString(arguments.item, "ptpayer_id,PtPayer_ID,PPID")),
            status = 0, payerId = 0, ptRelationship = "", subHic = "", subFirst = "", subLast = "", subMid = "",
            subStreet = "", subCity = "", subState = "", subZip = "", subGender = "", subDob = "",
            planProgram = "", planType = "", mspCode = "", policyGroupNumber = "", policyGroupName = "",
            payerResponsibility = "", utilForce = 0, dateStart = "", dateEnd = "",
            deductibleAmount = "", deductibleMet = 0, copayAmount = "", opAmount = "", opAmountMet = 0, coinsurance = 0,
            partAStart = "", partATerm = "", partBStart = "", partBTerm = ""
        } />
        <cfset var actionRaw = lcase(trim(getMobilePayloadString(arguments.item, "action,Action,PPAction"))) />
        <cfset var dobRaw = "" />
        <cfif listFindNoCase("update,edit,save", actionRaw)><cfset out.action = "update" />
        <cfelseif listFindNoCase("insert,add,create,new", actionRaw)><cfset out.action = "insert" /></cfif>
        <cfif arguments.existing.recordcount GT 0>
            <cfset out.status = val(arguments.existing.Status) />
            <cfset out.payerId = val(arguments.existing.Payer_ID) />
            <cfset out.ptRelationship = trim(arguments.existing.Pt_Relationship) />
            <cfset out.subHic = trim(arguments.existing.Sub_HIC) />
            <cfset out.subFirst = trim(arguments.existing.Sub_First) />
            <cfset out.subLast = trim(arguments.existing.Sub_Last) />
            <cfset out.subMid = trim(arguments.existing.Sub_Mid) />
            <cfset out.subStreet = trim(arguments.existing.Sub_Street) />
            <cfset out.subCity = trim(arguments.existing.Sub_City) />
            <cfset out.subState = trim(arguments.existing.Sub_State) />
            <cfset out.subZip = trim(arguments.existing.Sub_Zip) />
            <cfset out.subGender = trim(arguments.existing.Sub_Gender) />
            <cfif isDate(arguments.existing.Sub_DOB)><cfset out.subDob = dateFormat(arguments.existing.Sub_DOB, "yyyy-mm-dd") /></cfif>
            <cfset out.planProgram = trim(arguments.existing.Plan_Program) />
            <cfset out.planType = trim(arguments.existing.Plan_type) />
            <cfset out.mspCode = trim(arguments.existing.MSP_code) />
            <cfset out.policyGroupNumber = trim(arguments.existing.Policy_Group_Number) />
            <cfset out.policyGroupName = trim(arguments.existing.Policy_Group_Name) />
            <cfset out.payerResponsibility = trim(arguments.existing.Payer_Responsibility) />
            <cfset out.utilForce = val(arguments.existing.Util_Force) />
            <cfif isDate(arguments.existing.Date_Start)><cfset out.dateStart = dateFormat(arguments.existing.Date_Start, "yyyy-mm-dd") /></cfif>
            <cfif isDate(arguments.existing.Date_End)><cfset out.dateEnd = dateFormat(arguments.existing.Date_End, "yyyy-mm-dd") /></cfif>
        </cfif>
        <cfif hasMobilePayloadKey(arguments.item, "status,Status")><cfset out.status = val(getMobilePayloadString(arguments.item, "status,Status")) /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "payer_id,Payer_ID")><cfset out.payerId = val(getMobilePayloadString(arguments.item, "payer_id,Payer_ID")) /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "pt_relationship,Pt_Relationship")><cfset out.ptRelationship = getMobilePayloadString(arguments.item, "pt_relationship,Pt_Relationship") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_hic,Sub_HIC")><cfset out.subHic = getMobilePayloadString(arguments.item, "sub_hic,Sub_HIC") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_first,Sub_First")><cfset out.subFirst = getMobilePayloadString(arguments.item, "sub_first,Sub_First") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_last,Sub_Last")><cfset out.subLast = getMobilePayloadString(arguments.item, "sub_last,Sub_Last") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_mid,Sub_Mid")><cfset out.subMid = getMobilePayloadString(arguments.item, "sub_mid,Sub_Mid") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_street,Sub_Street")><cfset out.subStreet = getMobilePayloadString(arguments.item, "sub_street,Sub_Street") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_city,Sub_City")><cfset out.subCity = getMobilePayloadString(arguments.item, "sub_city,Sub_City") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_state,Sub_State")><cfset out.subState = getMobilePayloadString(arguments.item, "sub_state,Sub_State") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_zip,Sub_Zip")><cfset out.subZip = getMobilePayloadString(arguments.item, "sub_zip,Sub_Zip") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_gender,Sub_Gender")><cfset out.subGender = getMobilePayloadString(arguments.item, "sub_gender,Sub_Gender") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "sub_dob,Sub_DOB")>
            <cfset dobRaw = getMobilePayloadString(arguments.item, "sub_dob,Sub_DOB") />
            <cfif isDate(dobRaw)><cfset out.subDob = dateFormat(dobRaw, "yyyy-mm-dd") /></cfif>
        </cfif>
        <cfif hasMobilePayloadKey(arguments.item, "plan_program,Plan_Program")><cfset out.planProgram = getMobilePayloadString(arguments.item, "plan_program,Plan_Program") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "plan_type,Plan_type")><cfset out.planType = getMobilePayloadString(arguments.item, "plan_type,Plan_type") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "msp_code,MSP_code")><cfset out.mspCode = getMobilePayloadString(arguments.item, "msp_code,MSP_code") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "policy_group_number,Policy_Group_Number")><cfset out.policyGroupNumber = getMobilePayloadString(arguments.item, "policy_group_number,Policy_Group_Number") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "policy_group_name,Policy_Group_Name")><cfset out.policyGroupName = getMobilePayloadString(arguments.item, "policy_group_name,Policy_Group_Name") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "payer_responsibility,Payer_Responsibility")><cfset out.payerResponsibility = getMobilePayloadString(arguments.item, "payer_responsibility,Payer_Responsibility") /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "util_force,Util_Force")><cfset out.utilForce = val(getMobilePayloadString(arguments.item, "util_force,Util_Force")) /></cfif>
        <cfif hasMobilePayloadKey(arguments.item, "date_start,Date_Start,primary_DateStart")>
            <cfset dobRaw = getMobilePayloadString(arguments.item, "date_start,Date_Start,primary_DateStart") />
            <cfif isDate(dobRaw)><cfset out.dateStart = dateFormat(dobRaw, "yyyy-mm-dd") /></cfif>
        </cfif>
        <cfif hasMobilePayloadKey(arguments.item, "date_end,Date_End,primary_DateEnd")>
            <cfset dobRaw = getMobilePayloadString(arguments.item, "date_end,Date_End,primary_DateEnd") />
            <cfif isDate(dobRaw)><cfset out.dateEnd = dateFormat(dobRaw, "yyyy-mm-dd") /></cfif>
        </cfif>
        <cfif out.ptRelationship CONTAINS "18 Self" AND isQuery(arguments.patientRow) AND arguments.patientRow.recordcount GT 0>
            <cfset out.subFirst = trim(arguments.patientRow.Pt_First) />
            <cfset out.subMid = trim(arguments.patientRow.Pt_Middle) />
            <cfset out.subLast = trim(arguments.patientRow.Pt_Last) />
            <cfset out.subStreet = trim(arguments.patientRow.Pt_Street) />
            <cfset out.subCity = trim(arguments.patientRow.Pt_City) />
            <cfset out.subState = trim(arguments.patientRow.Pt_State) />
            <cfset out.subZip = trim(arguments.patientRow.Pt_Zip) />
            <cfset out.subGender = trim(arguments.patientRow.Pt_Gender) />
            <cfif isDate(arguments.patientRow.Pt_DOB)><cfset out.subDob = dateFormat(arguments.patientRow.Pt_DOB, "yyyy-mm-dd") /></cfif>
        </cfif>
        <cfif out.action EQ "update" AND out.ptPayerId LTE 0><cfreturn out /></cfif>
        <cfif out.action EQ "insert" AND out.ptPayerId GT 0><cfset out.action = "update" /></cfif>
        <cfif out.payerId LTE 0><cfreturn out /></cfif>
        <cfif NOT listFind("0,1,2", "" & out.status)><cfset out.status = 0 /></cfif>
        <cfset out.ok = true />
        <cfreturn out />
    </cffunction>

    <!--- Retire pPtPayer row before versioned insert --->
    <cffunction name="retireMobilePatientInsuranceRow" access="private" returntype="boolean" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="ptPayerId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agencySchema#.pPtPayer
            SET Status = <cfqueryparam cfsqltype="cf_sql_integer" value="3">,
                Record_Mod_Date = <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                Change_by = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            WHERE PtPayer_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.ptPayerId#">
              AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">
              AND Status IN (0, 1)
        </cfquery>
        <cfreturn true />
    </cffunction>

    <!--- Insert pPtPayer row (patient_payers_action.cfm core fields) --->
    <cffunction name="insertMobilePatientInsuranceRow" access="private" returntype="void" output="false">
        <cfargument name="agencySchema" type="string" required="true" />
        <cfargument name="ptPayerId" type="numeric" required="true" />
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="normalized" type="struct" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfset var subDob = "" />
        <cfset var dateStart = "" />
        <cfset var dateEnd = "" />
        <cfif isDate(arguments.normalized.subDob)><cfset subDob = dateFormat(arguments.normalized.subDob, "yyyy-mm-dd") /></cfif>
        <cfif isDate(arguments.normalized.dateStart)><cfset dateStart = dateFormat(arguments.normalized.dateStart, "yyyy-mm-dd") /></cfif>
        <cfif isDate(arguments.normalized.dateEnd)><cfset dateEnd = dateFormat(arguments.normalized.dateEnd, "yyyy-mm-dd") /></cfif>
        <cfquery datasource="#Application.DataSrc#">
            INSERT INTO #arguments.agencySchema#.pPtPayer (
                PtPayer_ID, Patient_ID, Status, Payer_ID, Pt_Relationship, Sub_HIC, Sub_First, Sub_Last, Sub_Mid,
                Sub_Street, Sub_City, Sub_State, Sub_Zip, Sub_Gender,
                <cfif len(subDob)>Sub_DOB,</cfif>
                Plan_Program, Plan_type, MSP_code, Policy_Group_Number, Policy_Group_Name,
                Payer_Responsibility, Util_Force,
                <cfif len(arguments.normalized.deductibleAmount)>Deductible_Amount,</cfif>
                Deductible_Met,
                <cfif len(arguments.normalized.copayAmount)>Copay_Amount,</cfif>
                <cfif len(arguments.normalized.opAmount)>OP_Amount,</cfif>
                OP_Amount_Met,
                <cfif val(arguments.normalized.coinsurance) GT 0>Coinsurance,</cfif>
                <cfif len(dateStart)>Date_Start,</cfif>
                <cfif len(dateEnd)>Date_End,</cfif>
                Record_Creation_Date, Created_by, Record_Mod_Date, Change_by
            ) VALUES (
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.ptPayerId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.patientId#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.status#">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.payerId#">,
                <cfqueryparam value="#arguments.normalized.ptRelationship#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subHic#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subFirst#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subLast#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subMid#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subStreet#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subCity#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subState#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subZip#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.subGender#" cfsqltype="cf_sql_varchar">,
                <cfif len(subDob)><cfqueryparam value="#subDob#" cfsqltype="cf_sql_date">,</cfif>
                <cfqueryparam value="#arguments.normalized.planProgram#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.planType#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.mspCode#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.policyGroupNumber#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.policyGroupName#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam value="#arguments.normalized.payerResponsibility#" cfsqltype="cf_sql_varchar">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.utilForce#">,
                <cfif len(arguments.normalized.deductibleAmount)>
                    <cfqueryparam value="#arguments.normalized.deductibleAmount#" scale="2" cfsqltype="cf_sql_decimal">,
                </cfif>
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.deductibleMet#">,
                <cfif len(arguments.normalized.copayAmount)>
                    <cfqueryparam value="#arguments.normalized.copayAmount#" scale="2" cfsqltype="cf_sql_decimal">,
                </cfif>
                <cfif len(arguments.normalized.opAmount)>
                    <cfqueryparam value="#arguments.normalized.opAmount#" scale="2" cfsqltype="cf_sql_decimal">,
                </cfif>
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.opAmountMet#">,
                <cfif val(arguments.normalized.coinsurance) GT 0>
                    <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.normalized.coinsurance#">,
                </cfif>
                <cfif len(dateStart)><cfqueryparam value="#dateStart#" cfsqltype="cf_sql_date">,</cfif>
                <cfif len(dateEnd)><cfqueryparam value="#dateEnd#" cfsqltype="cf_sql_date">,</cfif>
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">,
                <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.empId#">
            )
        </cfquery>
    </cffunction>

    <!--- Re-queue EVV when insurance member ID changes --->
    <cffunction name="triggerMobilePatientEvvOnMemberIdChange" access="private" returntype="void" output="false">
        <cfargument name="patientId" type="numeric" required="true" />
        <cfargument name="locId" type="numeric" required="true" />
        <cfargument name="empId" type="numeric" required="true" />
        <cfargument name="oldSubHic" type="string" required="true" />
        <cfargument name="newSubHic" type="string" required="true" />
        <cfif trim(arguments.oldSubHic) EQ trim(arguments.newSubHic)><cfreturn /></cfif>
        <cftry>
            <cfset createObject("component", "components.evv.EVVPatientAutomation").init().requeuePatientClientOnDataChange(
                patientID = val(arguments.patientId),
                agencyID = val(arguments.locId),
                triggerSource = "payer_member_id_update",
                createdBy = val(arguments.empId)
            ) />
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <!--- Get patient emergency/family contacts (pPtContacts) --->
    <cffunction name="getPatientContacts" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Patient_ID. Returns contacts from pPtContacts.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Patient_ID" type="string" required="true" />
        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var patientId = val(trim(arguments.Patient_ID)) />
        <cfset var ctx = {} />
        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif patientId LTE 0>
            <cfset response.message = "Patient_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>
        <cftry>
            <cfset ctx = authorizeMobilePatientRequest(securityKey = trim(arguments.securityKey), patientId = patientId) />
            <cfif NOT ctx.ok>
                <cfset response.message = ctx.message />
                <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
                <cfreturn response />
            </cfif>
            <cfset var contacts = loadMobilePatientContactsArray(agencySchema = ctx.agencySchema, patientId = patientId) />
            <cfset response.success = true />
            <cfset response.message = "Patient contacts loaded" />
            <cfset response.data = { patient_id = patientId, contacts = contacts, count = arrayLen(contacts) } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(methodName = "getPatientContacts", failedStep = "load_contacts", context = { patientId = patientId }, errorCatch = cfcatch) />
                <cfset response.message = "Failed to load contacts: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Add or update patient contact (+ Add / edit on Contacts tab) --->
    <cffunction name="savePatientContact" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Patient_ID, and contact object. action: insert|add or update|edit.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Patient_ID" type="string" required="false" default="" />
        <cfargument name="contact" type="string" required="false" default="" />
        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = parseMobilePatientContactSaveRequest(securityKeyArg = arguments.securityKey, patientIdArg = arguments.Patient_ID, contactArg = arguments.contact) />
        <cfset var ctx = {} />
        <cfset var existingQuery = queryNew("") />
        <cfset var normalized = {} />
        <cfset var contactId = 0 />
        <cfset var nextContact = "" />
        <cfset var contactsQuery = "" />
        <cfset var savedContact = {} />
        <cfset var currentStep = "" />
        <cfif NOT len(payload.securityKey) OR payload.patientId LTE 0 OR structIsEmpty(payload.contact)>
            <cfset response.message = "securityKey, Patient_ID, and contact object are required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>
        <cfset ctx = authorizeMobilePatientRequest(securityKey = payload.securityKey, patientId = payload.patientId) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <cfreturn response />
        </cfif>
        <cftry>
            <cfif val(getMobilePayloadString(payload.contact, "contact_id,Contact_ID,CID")) GT 0>
                <cfquery name="existingQuery" datasource="#Application.DataSrc#">
                    SELECT * FROM #ctx.agencySchema#.pPtContacts
                    WHERE Contact_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#val(getMobilePayloadString(payload.contact, 'contact_id,Contact_ID,CID'))#">
                      AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#payload.patientId#">
                      AND Status IN (0, 1) LIMIT 1
                </cfquery>
            </cfif>
            <cfset normalized = normalizeMobilePatientContactPayload(item = payload.contact, existing = existingQuery) />
            <cfif NOT normalized.ok>
                <cfset response.message = normalized.action EQ "update" ? "update requires contact_id; first_name, last_name, and phone1 are required" : "first_name, last_name, and phone1 are required" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>
            <cfif lcase(trim(ctx.patient.pt_first)) EQ lcase(trim(normalized.firstName)) AND lcase(trim(ctx.patient.pt_last)) EQ lcase(trim(normalized.lastName))>
                <cfset response.message = "Contact cannot be the same person as the patient" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>
            <cftransaction>
                <cfif normalized.action EQ "update">
                    <cfset contactId = normalized.contactId />
                    <cfset retireMobilePatientContactRow(agencySchema = ctx.agencySchema, contactId = contactId, patientId = payload.patientId, empId = ctx.empId) />
                <cfelse>
                    <cfquery name="nextContact" datasource="#Application.DataSrc#">
                        SELECT IFNULL(MAX(Contact_ID), 0) + 1 AS NewContact_ID FROM #ctx.agencySchema#.pPtContacts
                    </cfquery>
                    <cfset contactId = val(nextContact.NewContact_ID) />
                </cfif>
                <cfset insertMobilePatientContactRow(agencySchema = ctx.agencySchema, contactId = contactId, patientId = payload.patientId, locId = ctx.locId, normalized = normalized, empId = ctx.empId) />
            </cftransaction>
            <cfquery name="contactsQuery" datasource="#Application.DataSrc#">
                SELECT * FROM #ctx.agencySchema#.pPtContacts
                WHERE Contact_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#contactId#">
                  AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#payload.patientId#">
                  AND Status IN (0, 1) LIMIT 1
            </cfquery>
            <cfif contactsQuery.recordcount GT 0><cfset savedContact = formatMobilePatientContactRow(row = contactsQuery, rowIndex = 1) /></cfif>
            <cfset response.success = true />
            <cfset response.message = normalized.action EQ "update" ? "Contact updated" : "Contact added" />
            <cfset response.data = { patient_id = payload.patientId, action = normalized.action, contact_id = contactId, contact = savedContact } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(methodName = "savePatientContact", failedStep = currentStep, context = { patientId = payload.patientId }, errorCatch = cfcatch) />
                <cfset response.message = "Failed to save contact: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Get patient insurance / payers (pPtPayer) --->
    <cffunction name="getPatientInsurance" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey and Patient_ID. Returns insurance rows from pPtPayer.">
        <cfargument name="securityKey" type="string" required="true" />
        <cfargument name="Patient_ID" type="string" required="true" />
        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var patientId = val(trim(arguments.Patient_ID)) />
        <cfset var ctx = {} />
        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfif patientId LTE 0>
            <cfset response.message = "Patient_ID is required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>
        <cftry>
            <cfset ctx = authorizeMobilePatientRequest(securityKey = trim(arguments.securityKey), patientId = patientId) />
            <cfif NOT ctx.ok>
                <cfset response.message = ctx.message />
                <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
                <cfreturn response />
            </cfif>
            <cfset var insurance = loadMobilePatientInsuranceArray(agencySchema = ctx.agencySchema, patientId = patientId) />
            <cfset response.success = true />
            <cfset response.message = "Patient insurance loaded" />
            <cfset response.data = { patient_id = patientId, insurance = insurance, count = arrayLen(insurance) } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(methodName = "getPatientInsurance", failedStep = "load_insurance", context = { patientId = patientId }, errorCatch = cfcatch) />
                <cfset response.message = "Failed to load insurance: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Search agency payer catalog (pPayer) for add insurance form --->
    <cffunction name="searchPayerCatalog" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey. Optional search text filters pPayer by name.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="search" type="string" required="false" default="" />
        <cfargument name="q" type="string" required="false" default="" />
        <cfargument name="limit" type="string" required="false" default="100" />
        <cfset var response = { success = false, message = "", data = { payers = [] } } />
        <cfset var auth = {} />
        <cfset var searchText = trim(arguments.search) />
        <cfset var rowLimit = val(arguments.limit) />
        <cfset var payersQuery = "" />
        <cfset var payers = [] />
        <cfif NOT len(searchText)><cfset searchText = trim(arguments.q) /></cfif>
        <cfif rowLimit LTE 0 OR rowLimit GT 200><cfset rowLimit = 100 /></cfif>
        <cfif NOT len(trim(arguments.securityKey))>
            <cfset response.message = "securityKey is required" />
            <cfheader statuscode="401" statustext="Unauthorized" />
            <cfreturn response />
        </cfif>
        <cfset auth = authorizeMobileSecurityKeyForAgency(securityKey = trim(arguments.securityKey)) />
        <cfif NOT auth.ok>
            <cfset response.message = auth.message />
            <cfheader statuscode="#auth.statusCode#" statustext="Error" />
            <cfreturn response />
        </cfif>
        <cftry>
            <cfquery name="payersQuery" datasource="#Application.DataSrc#">
                SELECT Pay_ID, Pay_Name, Output, Payer_No, M0150
                FROM #auth.agencySchema#.pPayer
                WHERE Loc_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#auth.locId#">
                  AND Status = 0
                  <cfif len(searchText) GTE 2>
                    AND Pay_Name LIKE <cfqueryparam value="%#searchText#%" cfsqltype="cf_sql_varchar">
                  </cfif>
                ORDER BY Pay_Name ASC
                LIMIT <cfqueryparam cfsqltype="cf_sql_integer" value="#rowLimit#">
            </cfquery>
            <cfloop query="payersQuery">
                <cfset arrayAppend(payers, {
                    payer_id = payersQuery.Pay_ID,
                    Pay_ID = payersQuery.Pay_ID,
                    pay_name = trim(payersQuery.Pay_Name),
                    Pay_Name = trim(payersQuery.Pay_Name),
                    output = trim(payersQuery.Output),
                    payer_no = trim(payersQuery.Payer_No),
                    m0150 = trim(payersQuery.M0150)
                }) />
            </cfloop>
            <cfset response.success = true />
            <cfset response.message = "Payer catalog loaded" />
            <cfset response.data = { search = searchText, payers = payers, count = arrayLen(payers) } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset response.message = "Failed to search payers: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Add or update patient insurance (patientpayers_forms / patient_payers_action.cfm) --->
    <cffunction name="savePatientInsurance" access="remote" returntype="struct" returnformat="json" output="false"
        hint="Requires securityKey, Patient_ID, and insurance object. action: insert|add or update|edit.">
        <cfargument name="securityKey" type="string" required="false" default="" />
        <cfargument name="Patient_ID" type="string" required="false" default="" />
        <cfargument name="insurance" type="string" required="false" default="" />
        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var payload = parseMobilePatientInsuranceSaveRequest(securityKeyArg = arguments.securityKey, patientIdArg = arguments.Patient_ID, insuranceArg = arguments.insurance) />
        <cfset var ctx = {} />
        <cfset var existingQuery = queryNew("") />
        <cfset var patientQuery = queryNew("") />
        <cfset var normalized = {} />
        <cfset var ptPayerId = 0 />
        <cfset var nextPayer = "" />
        <cfset var payerQuery = "" />
        <cfset var savedInsurance = {} />
        <cfset var oldSubHic = "" />
        <cfif NOT len(payload.securityKey) OR payload.patientId LTE 0 OR structIsEmpty(payload.insurance)>
            <cfset response.message = "securityKey, Patient_ID, and insurance object are required" />
            <cfheader statuscode="400" statustext="Bad Request" />
            <cfreturn response />
        </cfif>
        <cfset ctx = authorizeMobilePatientRequest(securityKey = payload.securityKey, patientId = payload.patientId) />
        <cfif NOT ctx.ok>
            <cfset response.message = ctx.message />
            <cfheader statuscode="#ctx.statusCode#" statustext="Error" />
            <cfreturn response />
        </cfif>
        <cftry>
            <cfquery name="patientQuery" datasource="#Application.DataSrc#">
                SELECT Pt_First, Pt_Middle, Pt_Last, Pt_Street, Pt_City, Pt_State, Pt_Zip, Pt_Gender, Pt_DOB
                FROM #ctx.agencySchema#.pPatients
                WHERE Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#payload.patientId#">
                  AND Status IN (0, 1, 4) LIMIT 1
            </cfquery>
            <cfif val(getMobilePayloadString(payload.insurance, "ptpayer_id,PtPayer_ID,PPID")) GT 0>
                <cfquery name="existingQuery" datasource="#Application.DataSrc#">
                    SELECT * FROM #ctx.agencySchema#.pPtPayer
                    WHERE PtPayer_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#val(getMobilePayloadString(payload.insurance, 'ptpayer_id,PtPayer_ID,PPID'))#">
                      AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#payload.patientId#">
                      AND Status IN (0, 1) LIMIT 1
                </cfquery>
                <cfif existingQuery.recordcount GT 0><cfset oldSubHic = trim(existingQuery.Sub_HIC) /></cfif>
            </cfif>
            <cfset normalized = normalizeMobilePatientInsurancePayload(item = payload.insurance, existing = existingQuery, patientRow = patientQuery) />
            <cfif NOT normalized.ok>
                <cfset response.message = normalized.action EQ "update" ? "update requires ptpayer_id; payer_id is required" : "payer_id is required" />
                <cfheader statuscode="400" statustext="Bad Request" />
                <cfreturn response />
            </cfif>
            <cftransaction>
                <cfif normalized.action EQ "update">
                    <cfset ptPayerId = normalized.ptPayerId />
                    <cfset retireMobilePatientInsuranceRow(agencySchema = ctx.agencySchema, ptPayerId = ptPayerId, patientId = payload.patientId, empId = ctx.empId) />
                <cfelse>
                    <cfquery name="nextPayer" datasource="#Application.DataSrc#">
                        SELECT IFNULL(MAX(PtPayer_ID), 0) + 1 AS NewPtPayer_ID FROM #ctx.agencySchema#.pPtPayer
                    </cfquery>
                    <cfset ptPayerId = val(nextPayer.NewPtPayer_ID) />
                </cfif>
                <cfset insertMobilePatientInsuranceRow(agencySchema = ctx.agencySchema, ptPayerId = ptPayerId, patientId = payload.patientId, normalized = normalized, empId = ctx.empId) />
            </cftransaction>
            <cfset triggerMobilePatientEvvOnMemberIdChange(patientId = payload.patientId, locId = ctx.locId, empId = ctx.empId, oldSubHic = oldSubHic, newSubHic = normalized.subHic) />
            <cfquery name="payerQuery" datasource="#Application.DataSrc#">
                SELECT pPtPayer.*, pPayer.Pay_Name, pPayer.Output
                FROM #ctx.agencySchema#.pPtPayer
                LEFT JOIN #ctx.agencySchema#.pPayer ON pPayer.Pay_ID = pPtPayer.Payer_ID AND pPayer.Status = 0
                WHERE pPtPayer.PtPayer_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ptPayerId#">
                  AND pPtPayer.Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#payload.patientId#">
                  AND pPtPayer.Status IN (0, 1) LIMIT 1
            </cfquery>
            <cfif payerQuery.recordcount GT 0><cfset savedInsurance = formatMobilePatientInsuranceRow(row = payerQuery, rowIndex = 1) /></cfif>
            <cfset response.success = true />
            <cfset response.message = normalized.action EQ "update" ? "Insurance updated" : "Insurance added" />
            <cfset response.data = { patient_id = payload.patientId, action = normalized.action, ptpayer_id = ptPayerId, insurance = savedInsurance } />
            <cfheader statuscode="200" statustext="OK" />
            <cfreturn response />
            <cfcatch type="any">
                <cfset sendMobileApiErrorEmail(methodName = "savePatientInsurance", failedStep = "save_insurance", context = { patientId = payload.patientId }, errorCatch = cfcatch) />
                <cfset response.message = "Failed to save insurance: " & cfcatch.message />
                <cfheader statuscode="500" statustext="Internal Server Error" />
                <cfreturn response />
            </cfcatch>
        </cftry>
    </cffunction>

</cfcomponent>
