<cfcomponent displayname="Whisper AI API" hint="REST API for Whisper AI patient data imports" output="true">
    
    <!--- Add CORS headers --->
    <cfheader name="Access-Control-Allow-Origin" value="*">
    <cfheader name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE">
    <cfheader name="Access-Control-Allow-Headers" value="Content-Type, Authorization">
    
    <!--- Set password encryption key (same as main application) --->
    <cfset this.passwordEncryptKey = "WTq8zYcZfaWVvMncigHqwQ==" />

    <!--- Dedicated API key for the public getAssessmentQuestionsByReason endpoint --->
    <!--- This key is separate from Application.enckey used by other codebases --->  
    <cfset this.assessmentLookupApiKey = "WhisperQL_9f3Kx2mP8vTd5Zn7Yw4Rb6Qe1Aj" />
    
    <!--- Email notification recipients --->
    <cfset this.notificationEmails = "velmurugan@myhomecarebiz.com" />
    <cfset this.whisperVitalsWoundsEmail = "velmurugan@myhomecarebiz.com,janice.l@myhomecarebiz.com" />
    
    <!--- Set datasource name (fallback if Application scope not available) --->
    <cfif not isDefined("Application.DataSrc")>
        <cfset Application.DataSrc = "hhapowerpath" />
    </cfif>
    
    <!--- Validate Basic Authentication --->
    <cffunction name="validateBasicAuth" access="private" returntype="struct" hint="Validate HTTP Basic Authentication">
        <cfargument name="appkey" required="yes" hint="appkey">
        <cfargument name="agency_id" required="no" default="0" hint="Agency ID for logging">
        <cfargument name="emp_id" required="no" default="0" hint="Employee ID for logging">
        <cfargument name="patient_id" required="no" default="0" hint="Patient ID for auth generation">
        
        <cfset var result = {
            "valid": false,
            "message": "Authentication required",
            "agency_db": "",
            "emp_id": 0
        } />
        
        <cftry>
           
            <cfset var agencyKey = trim("" & arguments.agency_id) />
            <cfset var patientKey = trim("" & arguments.patient_id) />
            <cfset var expectedKey = "" />

            <cfif len(agencyKey) AND len(patientKey)>
                <cfset expectedKey = agencyKey & "_" & patientKey />
            </cfif>
            <cfheader statuscode="200" statustext="success">
            <cfset result.valid = true />
            <cfset result.message = "Authentication successful" />
                
            <cfif len(expectedKey) AND appkey EQ expectedKey>
                <!--- Authentication successful --->
                <cfheader statuscode="200" statustext="success">
                <cfset result.valid = true />
                <cfset result.message = "Authentication successful" />
                
                <!--- Send success email notification 
                <cftry>
                    <cfmail to="#this.notificationEmails#" 
                            from="success@myhomecarebiz.com" 
                            subject="Bulk Import AI API - Authentication Success" 
                            type="html">
                        <h3 style="color: green;">Bulk Import AI - Authentication Successful</h3>
                        <p><strong>Timestamp:</strong> #DateFormat(Now(), 'mm/dd/yyyy')# #TimeFormat(Now(), 'HH:mm:ss')#</p>
                        <p><strong>Agency ID:</strong> #arguments.agency_id#</p>
                        <p><strong>Employee ID:</strong> #arguments.emp_id#</p>
                        <p><strong>IP Address:</strong> #CGI.REMOTE_ADDR#</p>
                        <p><strong>User Agent:</strong> #CGI.HTTP_USER_AGENT#</p>
                        <p><strong>expectedKey:</strong> #expectedKey#</p>
                        <hr>
                        <p><em>This is an automated notification from the Bulk Import AI API</em></p>
                    </cfmail>
                    <cfcatch type="any">
                     </cfcatch>
                </cftry>--->
            <cfelse>   
                <cfheader statuscode="401" statustext="Unauthorized">
                <cfset result.message = "Invalid username or password" />
                
                <!--- Send authentication failure email notification --->
                <cftry>
                    <cfmail to="#this.notificationEmails#" 
                            from="error@myhomecarebiz.com" 
                            subject="Bulk Import AI API - AUTHENTICATION FAILED" 
                            type="html">
                        <h3 style="color: red;">Bulk Import AI - Authentication Failed!</h3>
                        <p><strong>Timestamp:</strong> #DateFormat(Now(), 'mm/dd/yyyy')# #TimeFormat(Now(), 'HH:mm:ss')#</p>
                        <p><strong>Reason:</strong> Invalid API Key</p>
                        <p><strong>Agency ID:</strong> #arguments.agency_id#</p>
                        <p><strong>Employee ID:</strong> #arguments.emp_id#</p>
                        <p><strong>IP Address:</strong> #CGI.REMOTE_ADDR#</p>
                        <p><strong>User Agent:</strong> #CGI.HTTP_USER_AGENT#</p>
                        <p><strong>Provided Key (masked):</strong> #Left(arguments.appkey, 10)#***</p>
                        <hr>
                        <p style="color: red;"><strong>Security Alert:</strong> Unauthorized access attempt detected.</p>
                        <hr>
                        <p><em>This is an automated security notification from the Bulk Import AI API</em></p>
                    </cfmail>
                    <cfcatch type="any">
                        <!--- Silently fail if email notification fails --->
                    </cfcatch>
                </cftry>
                
                <cfreturn result />
            </cfif> 
            
            

            <cfcatch type="any">
                <cfheader statuscode="500" statustext="Internal Server Error">
                <cfset result.valid = false />
                <cfset result.message = "Authentication error: " & cfcatch.message />
                
                <!--- Send authentication error email notification --->
                <cftry>
                    <cfmail to="#this.notificationEmails#" 
                            from="error@myhomecarebiz.com" 
                            subject="Bulk Import AI API - AUTHENTICATION ERROR" 
                            type="html">
                        <h3 style="color: red;">Bulk Import AI - Authentication Error!</h3>
                        <p><strong>Timestamp:</strong> #DateFormat(Now(), 'mm/dd/yyyy')# #TimeFormat(Now(), 'HH:mm:ss')#</p>
                        <p><strong>Agency ID:</strong> #arguments.agency_id#</p>
                        <p><strong>Employee ID:</strong> #arguments.emp_id#</p>
                        <p><strong>IP Address:</strong> #CGI.REMOTE_ADDR#</p>
                        <p><strong>User Agent:</strong> #CGI.HTTP_USER_AGENT#</p>
                        <hr>
                        <h4 style="color: red;">Error Details:</h4>
                        <p><strong>Error Type:</strong> #cfcatch.type#</p>
                        <p><strong>Error Message:</strong> #cfcatch.message#</p>
                        <p><strong>Error Detail:</strong> #cfcatch.detail#</p>
                        <cfif structKeyExists(cfcatch, "TagContext") and arrayLen(cfcatch.TagContext) gt 0>
                            <p><strong>File:</strong> #cfcatch.TagContext[1].template#</p>
                            <p><strong>Line:</strong> #cfcatch.TagContext[1].line#</p>
                        </cfif>
                        <hr>
                        <h4>Stack Trace:</h4>
                        <pre>#cfcatch.stacktrace#</pre>
                        <hr>
                        <p><em>This is an automated error notification from the Bulk Import AI API</em></p>
                    </cfmail>
                    <cfcatch>
                        <!--- Silently fail if error email notification fails --->
                    </cfcatch>
                </cftry>
            </cfcatch>
        </cftry>
         <cfreturn result />
    </cffunction>
    
    <!--- Helper function to parse date --->
    <cffunction name="parseDate" access="private" returntype="any" hint="Parse date string to valid date or null">
        <cfargument name="dateString" required="yes" hint="Date string to parse">
        
        <cfif len(trim(arguments.dateString)) eq 0>
            <cfreturn "" />
        </cfif>
        
        <cftry>
            <!--- Try to parse MM/DD/YYYY format --->
            <cfif find("/", arguments.dateString)>
                <cfset var dateParts = listToArray(arguments.dateString, "/") />
                <cfif arrayLen(dateParts) eq 3>
                    <cfset var month = dateParts[1] />
                    <cfset var day = dateParts[2] />
                    <cfset var year = dateParts[3] />
                    <cfreturn createDate(year, month, day) />
                </cfif>
            </cfif>
            
            <!--- Try standard date parsing --->
            <cfif isDate(arguments.dateString)>
                <cfreturn parseDateTime(arguments.dateString) />
            </cfif>
            
            <cfcatch type="any">
                <cfreturn "" />
            </cfcatch>
        </cftry>
        
        <cfreturn "" />
    </cffunction>

    <!--- Helper: Pull latest job payload from transcript jobs API --->
    <cffunction name="fetchTranscriptJobPayload" access="private" returntype="struct" hint="Fetch job payload from transcript jobs API">
        <cfargument name="jobID" type="string" required="yes" hint="Job identifier">
        <cfargument name="jobsApiUrl" type="string" required="no" default="https://transcriptapi.myhomecarebiz.com/jobs/" hint="Base Jobs API URL">

        <cfset var result = {
            "success": false,
            "statusCode": 0,
            "message": "",
            "data": {}
        } />
        <cfset var targetUrl = "" />
        <cfset var httpResponse = "" />
        <cfset var bodyText = "" />
        <cfset var parsedBody = {} />
        <cfset var baseUrl = trim(arguments.jobsApiUrl) />
        <cfset var cleanJobID = trim(arguments.jobID) />

        <cfif NOT len(cleanJobID)>
            <cfset result.message = "Job ID is required" />
            <cfreturn result />
        </cfif>

        <cfif NOT len(baseUrl)>
            <cfset baseUrl = "https://transcriptapi.myhomecarebiz.com/jobs/" />
        </cfif>

        <!--- Normalize URL and fetch by path parameter first --->
        <cfif right(baseUrl, 1) NEQ "/">
            <cfset baseUrl = baseUrl & "/" />
        </cfif>
        <cfset targetUrl = baseUrl & cleanJobID />

        <cftry>
            <cfhttp method="get" url="#targetUrl#" timeout="30" result="httpResponse" throwOnError="false" />

            <cfset result.statusCode = val(httpResponse.statusCode) />
            <cfset bodyText = structKeyExists(httpResponse, "FileContent") ? trim(httpResponse.FileContent) : "" />

            <cfif result.statusCode GTE 200 AND result.statusCode LT 300 AND len(bodyText) AND isJSON(bodyText)>
                <cfset parsedBody = deserializeJSON(bodyText) />
                <cfset result.success = true />
                <cfset result.message = "Jobs API fetch successful" />
                <cfset result.data = parsedBody />
                <cfreturn result />
            </cfif>

            <!--- Fallback: query parameter style if path style is unavailable --->
            <cfset targetUrl = baseUrl & "?job_id=" & urlEncodedFormat(cleanJobID) />
            <cfhttp method="get" url="#targetUrl#" timeout="30" result="httpResponse" throwOnError="false" />

            <cfset result.statusCode = val(httpResponse.statusCode) />
            <cfset bodyText = structKeyExists(httpResponse, "FileContent") ? trim(httpResponse.FileContent) : "" />

            <cfif result.statusCode GTE 200 AND result.statusCode LT 300 AND len(bodyText) AND isJSON(bodyText)>
                <cfset parsedBody = deserializeJSON(bodyText) />
                <cfset result.success = true />
                <cfset result.message = "Jobs API fetch successful" />
                <cfset result.data = parsedBody />
            <cfelse>
                <cfset result.message = "Jobs API fetch failed with HTTP " & result.statusCode />
            </cfif>

            <cfcatch type="any">
                <cfset result.success = false />
                <cfset result.message = "Jobs API error: " & cfcatch.message />
            </cfcatch>
        </cftry>

        <cfreturn result />
    </cffunction>
    
    <!--- Helper to safely attempt decryption using application key --->
    <cffunction name="maybeDecrypt" access="private" returntype="any" hint="Attempt to decrypt hex-encoded AES values; fall back to original on failure">
        <cfargument name="value" required="yes" hint="Value to decrypt">
        <cfargument name="expectNumeric" required="no" default="false" hint="Whether the decrypted result must be numeric">
        
        <cfset var decryptedValue = arguments.value />
        <cfset var trimmedValue = "" />
        
        <cfif NOT len(trim(arguments.value))>
            <cfreturn arguments.value />
        </cfif>
        
        <cfif NOT structKeyExists(Application, "enckey") OR NOT len(Application.enckey)>
            <cfreturn arguments.value />
        </cfif>
        
        <cfset trimmedValue = trim(arguments.value) />
        
        <cftry>
            <cfset decryptedValue = decrypt(trimmedValue, Application.enckey, "AES", "Hex") />
            
            <cfif arguments.expectNumeric AND NOT isNumeric(decryptedValue)>
                <cfreturn arguments.value />
            </cfif>
            
            <cfreturn decryptedValue />
            <cfcatch type="any">
                <cfreturn arguments.value />
            </cfcatch>
        </cftry>
    </cffunction>
    
   
    
    <!--- Helper: Send success email notification --->
    <cffunction name="sendSuccessEmail" access="private" returntype="void" hint="Send success email notification">
        <cfargument name="insertedPatientID" type="numeric" required="yes">
        <cfargument name="patientAction" type="string" required="yes">
        <cfargument name="insertedContactID" type="numeric" required="yes">
        <cfargument name="contactInserted" type="boolean" required="yes">
        <cfargument name="insertedPayerID" type="numeric" required="yes">
        <cfargument name="payerInserted" type="boolean" required="yes">
        <cfargument name="insertedAdmitID" type="numeric" required="yes">
        <cfargument name="admitInserted" type="boolean" required="yes">
        <cfargument name="medicationCount" type="numeric" required="yes">
        <cfargument name="problemCount" type="numeric" required="yes">
        <cfargument name="errors" type="array" required="yes">
        <cfargument name="agencyID" type="numeric" required="yes">
        <cfargument name="empID" type="numeric" required="yes">
        <cfargument name="patientID" type="numeric" required="yes">
        <cfargument name="assessmentID" type="numeric" required="yes">
        <cfargument name="websiteName" type="string" required="yes">
        <cfargument name="extractedJSONString" type="string" required="yes">
        <cfargument name="originalAgencyID" type="string" required="yes">
        <cfargument name="originalEmpID" type="string" required="yes">
        <cfargument name="originalPatientID" type="string" required="yes">
        <cfargument name="originalAssessmentID" type="string" required="yes">
        <cfargument name="originalAppKey" type="string" required="yes">
        <cfargument name="decryptedAgencyID" type="string" required="yes">
        <cfargument name="decryptedEmpID" type="string" required="yes">
        <cfargument name="decryptedPatientID" type="string" required="yes">
        <cfargument name="decryptedAssessmentID" type="string" required="yes">
        <cfargument name="decryptedAppKey" type="string" required="yes">
        <cfargument name="agency_db" type="string" required="yes">
        
        <cftry>
            <cfmail to="#this.notificationEmails#" 
                    from="success@myhomecarebiz.com" 
                    subject="Bulk Import AI API - Success: Patient AI ID #arguments.insertedPatientID#" 
                    type="html">
                <h3 style="color: green;">Bulk Import AI - Patient Data Imported Successfully!</h3>
                <p><strong>Agency ID:</strong> #arguments.agencyID#</p>
                <p><strong>Employee ID:</strong> #arguments.empID#</p>
                <p><strong>Patient ID:</strong> #arguments.patientID#</p>
                <p><strong>Assessment ID:</strong> #arguments.assessmentID#</p>
                <cfif len(trim(arguments.websiteName)) gt 0>
                    <p><strong>Website:</strong> #arguments.websiteName#</p>
                </cfif>
                <p><strong>Timestamp:</strong> #DateFormat(Now(), 'mm/dd/yyyy')# #TimeFormat(Now(), 'HH:mm:ss')#</p>
                <hr>
                <h4>Encryption IDs and AppKeys:</h4>
                <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse; width: 100%;">
                    <tr style="background-color: ##f0f0f0;">
                        <th style="text-align: left;">Field</th>
                        <th style="text-align: left;">Original (Encrypted)</th>
                        <th style="text-align: left;">Decrypted Value</th>
                        <th style="text-align: left;">Was Encrypted</th>
                    </tr>
                    <tr>
                        <td><strong>Agency_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalAgencyID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedAgencyID)#</code></td>
                        <td>#iif(arguments.decryptedAgencyID NEQ arguments.originalAgencyID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>Emp_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalEmpID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedEmpID)#</code></td>
                        <td>#iif(arguments.decryptedEmpID NEQ arguments.originalEmpID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>Patient_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalPatientID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedPatientID)#</code></td>
                        <td>#iif(arguments.decryptedPatientID NEQ arguments.originalPatientID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>Assessment_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalAssessmentID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedAssessmentID)#</code></td>
                        <td>#iif(arguments.decryptedAssessmentID NEQ arguments.originalAssessmentID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>appkey</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalAppKey)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedAppKey)#</code></td>
                        <td>#iif(len(arguments.decryptedAppKey) GT 0 AND arguments.decryptedAppKey NEQ arguments.originalAppKey, DE('Yes'), DE('No'))#</td>
                    </tr>
                </table>
                <cfif structKeyExists(Application, "enckey") AND len(Application.enckey)>
                    <p><strong>Encryption Key Available:</strong> Yes (Length: #len(Application.enckey)#)</p>
                <cfelse>
                    <p><strong>Encryption Key Available:</strong> No</p>
                </cfif>
                <hr>
                <h4>Import Results:</h4>
                <ul>
                    <li><strong>Patient AI ID:</strong> #arguments.insertedPatientID# (#arguments.patientAction#)</li>
                    <cfif arguments.insertedContactID gt 0>
                        <li><strong>Emergency Contact:</strong> #iif(arguments.contactInserted, DE('Inserted'), DE('Updated'))# (ID: #arguments.insertedContactID#)</li>
                    </cfif>
                    <cfif arguments.insertedPayerID gt 0>
                        <li><strong>Payer/Subscriber:</strong> #iif(arguments.payerInserted, DE('Inserted'), DE('Updated'))# (ID: #arguments.insertedPayerID#)</li>
                    </cfif>
                    <cfif arguments.insertedAdmitID gt 0>
                        <li><strong>Admission Record:</strong> #iif(arguments.admitInserted, DE('Inserted'), DE('Updated'))# (ID: #arguments.insertedAdmitID#)</li>
                    </cfif>
                    <cfif arguments.medicationCount gt 0>
                        <li><strong>Medications:</strong> #arguments.medicationCount# record(s) inserted</li>
                    </cfif>
                    <cfif arguments.problemCount gt 0>
                        <li><strong>Problems/Diagnoses:</strong> #arguments.problemCount# record(s) inserted/updated</li>
                    </cfif>
                </ul>
                <cfif len(trim(arguments.extractedJSONString)) gt 0>
                    <hr>
                    <h4>Extracted Data (JSON):</h4>
                    <pre style="white-space: pre-wrap; font-family: monospace;">#htmlEditFormat(arguments.extractedJSONString)#</pre>
                </cfif>
                <cfif arrayLen(arguments.errors) gt 0>
                    <hr>
                    <h4 style="color: orange;">Partial Errors:</h4>
                    <ul>
                        <cfloop array="#arguments.errors#" index="err">
                            <li style="color: orange;">#err#</li>
                        </cfloop>
                    </ul>
                </cfif>
                <hr>
                <h4>Debug Information:</h4>
                <ul>
                    <li><strong>Datasource:</strong> #Application.DataSrc#</li>
                    <li><strong>Agency Database:</strong> #arguments.agency_db#</li>
                    <li><strong>Transaction Status:</strong> Committed Successfully</li>
                </ul>
                <hr>
                <p><em>This is an automated notification from the Bulk Import AI API</em></p>
            </cfmail>
            <cfcatch type="any">
                <!--- Silently fail if email notification fails --->
            </cfcatch>
        </cftry>
    </cffunction>
    
    <!--- Helper: Send error email notification --->
    <cffunction name="sendErrorEmail" access="private" returntype="void" hint="Send error email notification">
        <cfargument name="error" type="any" required="yes">
        <cfargument name="agencyID" type="numeric" required="yes">
        <cfargument name="empID" type="numeric" required="yes">
        <cfargument name="patientID" type="numeric" required="yes">
        <cfargument name="assessmentID" type="numeric" required="yes">
        <cfargument name="websiteName" type="string" required="yes">
        <cfargument name="extractedJSONString" type="string" required="yes">
        <cfargument name="originalAgencyID" type="string" required="yes">
        <cfargument name="originalEmpID" type="string" required="yes">
        <cfargument name="originalPatientID" type="string" required="yes">
        <cfargument name="originalAssessmentID" type="string" required="yes">
        <cfargument name="originalAppKey" type="string" required="yes">
        <cfargument name="decryptedAgencyID" type="string" required="yes">
        <cfargument name="decryptedEmpID" type="string" required="yes">
        <cfargument name="decryptedPatientID" type="string" required="yes">
        <cfargument name="decryptedAssessmentID" type="string" required="yes">
        <cfargument name="decryptedAppKey" type="string" required="yes">
        <cfargument name="insertedPatientID" type="numeric" required="yes">
        <cfargument name="medicationCount" type="numeric" required="yes">
        <cfargument name="problemCount" type="numeric" required="yes">
        <cfargument name="errors" type="array" required="yes">
        <cfargument name="stepErrors" type="array" required="no" default="#ArrayNew(1)#">
        <cfargument name="currentStep" type="string" required="no" default="Unknown">
        
        <cfset var errorType = structKeyExists(arguments.error, "type") ? arguments.error.type : "Unknown" />
        <cfset var errorMessage = structKeyExists(arguments.error, "message") ? arguments.error.message : "Unknown error" />
        <cfset var errorDetail = structKeyExists(arguments.error, "detail") ? arguments.error.detail : "" />
        <cfset var errorStacktrace = structKeyExists(arguments.error, "stacktrace") ? arguments.error.stacktrace : "" />
        <cfset var errorTagContext = structKeyExists(arguments.error, "TagContext") ? arguments.error.TagContext : [] />
        
        <cftry>
            <cfmail to="#this.notificationEmails#" 
                    from="error@myhomecarebiz.com" 
                    subject="Bulk Import AI API - ERROR: Agency #arguments.agencyID#" 
                    type="html">
                <h3 style="color: red;">Bulk Import AI - Patient Data Import Failed!</h3>
                <p><strong>Agency ID:</strong> #arguments.agencyID#</p>
                <p><strong>Employee ID:</strong> #arguments.empID#</p>
                <p><strong>Patient ID:</strong> #arguments.patientID#</p>
                <p><strong>Assessment ID:</strong> #arguments.assessmentID#</p>
                <cfif len(trim(arguments.websiteName)) gt 0>
                    <p><strong>Website:</strong> #arguments.websiteName#</p>
                </cfif>
                <p><strong>Timestamp:</strong> #DateFormat(Now(), 'mm/dd/yyyy')# #TimeFormat(Now(), 'HH:mm:ss')#</p>
                <hr>
                <h4>Error Information:</h4>
                <p><strong>Error Type:</strong> #errorType#</p>
                <p><strong>Error Message:</strong> #errorMessage#</p>
                <p><strong>Error Detail:</strong> #errorDetail#</p>
                <p><strong>Failed at Step:</strong> <span style="background-color: ##ffcccc; padding: 5px;">#htmlEditFormat(arguments.currentStep)#</span></p>
                <cfif arrayLen(errorTagContext) gt 0>
                    <p><strong>File:</strong> #errorTagContext[1].template#</p>
                    <p><strong>Line:</strong> #errorTagContext[1].line#</p>
                </cfif>
                <hr>
                <cfif arrayLen(arguments.stepErrors) gt 0>
                    <h4 style="color: ##cc0000;">Step-by-Step Error Details:</h4>
                    <ol>
                        <cfloop array="#arguments.stepErrors#" index="stepErr">
                            <li style="margin-bottom: 10px; padding: 8px; background-color: ##ffe6e6; border-left: 4px solid ##cc0000;">
                                #htmlEditFormat(stepErr)#
                            </li>
                        </cfloop>
                    </ol>
                </cfif>
                <hr>
                <h4>Encryption IDs and AppKeys:</h4>
                <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse; width: 100%;">
                    <tr style="background-color: ##f0f0f0;">
                        <th style="text-align: left;">Field</th>
                        <th style="text-align: left;">Original (Encrypted)</th>
                        <th style="text-align: left;">Decrypted Value</th>
                        <th style="text-align: left;">Was Encrypted</th>
                    </tr>
                    <tr>
                        <td><strong>Agency_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalAgencyID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedAgencyID)#</code></td>
                        <td>#iif(arguments.decryptedAgencyID NEQ arguments.originalAgencyID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>Emp_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalEmpID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedEmpID)#</code></td>
                        <td>#iif(arguments.decryptedEmpID NEQ arguments.originalEmpID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>Patient_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalPatientID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedPatientID)#</code></td>
                        <td>#iif(arguments.decryptedPatientID NEQ arguments.originalPatientID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>Assessment_ID</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalAssessmentID)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedAssessmentID)#</code></td>
                        <td>#iif(arguments.decryptedAssessmentID NEQ arguments.originalAssessmentID, DE('Yes'), DE('No'))#</td>
                    </tr>
                    <tr>
                        <td><strong>appkey</strong></td>
                        <td><code>#htmlEditFormat(arguments.originalAppKey)#</code></td>
                        <td><code>#htmlEditFormat(arguments.decryptedAppKey)#</code></td>
                        <td>#iif(len(arguments.decryptedAppKey) GT 0 AND arguments.decryptedAppKey NEQ arguments.originalAppKey, DE('Yes'), DE('No'))#</td>
                    </tr>
                </table>
                <cfif structKeyExists(Application, "enckey") AND len(Application.enckey)>
                    <p><strong>Encryption Key Available:</strong> Yes (Length: #len(Application.enckey)#)</p>
                <cfelse>
                    <p><strong>Encryption Key Available:</strong> No</p>
                </cfif>
                <hr>
                <h4>Partial Import Results (before error):</h4>
                <ul>
                    <li><strong>Patient AI ID:</strong> #arguments.insertedPatientID#</li>
                    <li><strong>Medications:</strong> #arguments.medicationCount# record(s)</li>
                    <li><strong>Problems/Diagnoses:</strong> #arguments.problemCount# record(s)</li>
                </ul>
                <cfif arrayLen(arguments.errors) gt 0>
                    <hr>
                    <h4>Previous Errors:</h4>
                    <ul>
                        <cfloop array="#arguments.errors#" index="err">
                            <li style="color: orange;">#err#</li>
                        </cfloop>
                    </ul>
                </cfif>
                <cfif len(trim(arguments.extractedJSONString)) gt 0>
                    <hr>
                    <h4>Extracted Data (JSON):</h4>
                    <pre style="white-space: pre-wrap; font-family: monospace;">#htmlEditFormat(arguments.extractedJSONString)#</pre>
                </cfif>
                <hr>
                <h4>Stack Trace:</h4>
                <pre style="white-space: pre-wrap; font-family: monospace;">#htmlEditFormat(errorStacktrace)#</pre>
                <hr>
                <p><em>This is an automated error notification from the Bulk Import AI API</em></p>
            </cfmail>
            <cfcatch type="any">
                <!--- Silently fail if email notification fails --->
            </cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="sendAssessmentSuccessEmail" access="private" returntype="void" hint="Send assessment import success email notification">
        <cfargument name="agencyID" type="numeric" required="yes">
        <cfargument name="empID" type="numeric" required="yes">
        <cfargument name="patientID" type="numeric" required="yes">
        <cfargument name="assessmentID" type="numeric" required="yes">
        <cfargument name="websiteName" type="string" required="yes">
        <cfargument name="assessmentReason" type="string" required="yes">
        <cfargument name="mappedAnswerCount" type="numeric" required="yes">
        <cfargument name="skippedAnswers" type="array" required="yes">
        <cfargument name="stepErrors" type="array" required="yes">
        <cfargument name="jobID" type="string" required="no" default="">
        <cfargument name="extractedJSONString" type="string" required="no" default="">
        <cfargument name="agency_db" type="string" required="yes">

        <cftry>
            <cfmail to="#this.notificationEmails#"
                    from="success@myhomecarebiz.com"
                    subject="Whisper Assessment Import Success - Assessment #arguments.assessmentID#"
                    type="html">
                <h3 style="color: green;">Whisper Assessment Import Completed Successfully</h3>
                <p><strong>Agency ID:</strong> #arguments.agencyID#</p>
                <p><strong>Employee ID:</strong> #arguments.empID#</p>
                <p><strong>Patient ID:</strong> #arguments.patientID#</p>
                <p><strong>Assessment ID:</strong> #arguments.assessmentID#</p>
                <cfif len(trim(arguments.websiteName)) gt 0>
                    <p><strong>Website:</strong> #arguments.websiteName#</p>
                </cfif>
                <cfif len(trim(arguments.jobID)) gt 0>
                    <p><strong>Job ID:</strong> #arguments.jobID#</p>
                </cfif>
                <p><strong>Timestamp:</strong> #DateFormat(Now(), 'mm/dd/yyyy')# #TimeFormat(Now(), 'HH:mm:ss')#</p>
                <hr>
                <h4>Assessment Update Results</h4>
                <ul>
                    <li><strong>Assessment Reason:</strong> #htmlEditFormat(arguments.assessmentReason)#</li>
                    <li><strong>Mapped F-columns Updated:</strong> #arguments.mappedAnswerCount#</li>
                    <li><strong>Skipped Answers:</strong> #arrayLen(arguments.skippedAnswers)#</li>
                    <li><strong>Transaction Status:</strong> Committed Successfully</li>
                    <li><strong>Datasource:</strong> #Application.DataSrc#</li>
                    <li><strong>Agency Database:</strong> #arguments.agency_db#</li>
                </ul>
                <cfif arrayLen(arguments.skippedAnswers) gt 0>
                    <hr>
                    <h4 style="color: ##cc7a00;">Skipped Answers</h4>
                    <ul>
                        <cfloop array="#arguments.skippedAnswers#" index="skippedAnswer">
                            <li>#htmlEditFormat(skippedAnswer)#</li>
                        </cfloop>
                    </ul>
                </cfif>
                <cfif arrayLen(arguments.stepErrors) gt 0>
                    <hr>
                    <h4 style="color: ##cc7a00;">Warnings Captured During Import</h4>
                    <ul>
                        <cfloop array="#arguments.stepErrors#" index="stepErr">
                            <li>#htmlEditFormat(stepErr)#</li>
                        </cfloop>
                    </ul>
                </cfif>
                <cfif len(trim(arguments.extractedJSONString)) gt 0>
                    <hr>
                    <h4>Extracted Data (JSON)</h4>
                    <pre style="white-space: pre-wrap; font-family: monospace;">#htmlEditFormat(arguments.extractedJSONString)#</pre>
                </cfif>
                <hr>
                <p><em>This is an automated notification from the Whisper Assessment Import API</em></p>
            </cfmail>
            <cfcatch type="any">
                <!--- Silently fail if success email notification fails --->
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Format a filled answer value for trace email display --->
    <cffunction name="formatTraceFilledAnswerValue" access="private" returntype="string" output="false">
        <cfargument name="qaItem" type="any" required="true" />
        <cfargument name="questionId" type="string" required="false" default="" />

        <cfset var parts = [] />
        <cfset var rawAnswer = "" />
        <cfset var normalizedTraceAnswer = "" />
        <cfset var isVital = reFindNoCase("^vital_", arguments.questionId) GT 0 />

        <cfif NOT isStruct(arguments.qaItem)>
            <cfreturn htmlEditFormat(trim("" & arguments.qaItem)) />
        </cfif>

        <cfif structKeyExists(arguments.qaItem, "answer") AND NOT isNull(arguments.qaItem.answer)>
            <cfset rawAnswer = arguments.qaItem.answer />
            <cfset normalizedTraceAnswer = normalizeWhisperFilledAnswerValue(rawAnswer) />
            <cfif len(normalizedTraceAnswer)>
                <cfset arrayAppend(parts, normalizedTraceAnswer) />
            </cfif>
        </cfif>

        <cfif isVital>
            <cfif structKeyExists(arguments.qaItem, "reading") AND len(trim("" & arguments.qaItem.reading))>
                <cfset arrayAppend(parts, "Reading: " & trim("" & arguments.qaItem.reading)) />
            </cfif>
            <cfif structKeyExists(arguments.qaItem, "reading_primary") AND len(trim("" & arguments.qaItem.reading_primary))>
                <cfset arrayAppend(parts, "Primary: " & trim("" & arguments.qaItem.reading_primary)) />
            </cfif>
            <cfif structKeyExists(arguments.qaItem, "reading_secondary") AND len(trim("" & arguments.qaItem.reading_secondary))>
                <cfset arrayAppend(parts, "Secondary: " & trim("" & arguments.qaItem.reading_secondary)) />
            </cfif>
            <cfif structKeyExists(arguments.qaItem, "descriptor") AND len(trim("" & arguments.qaItem.descriptor))>
                <cfset arrayAppend(parts, "Descriptor: " & trim("" & arguments.qaItem.descriptor)) />
            </cfif>
            <cfif structKeyExists(arguments.qaItem, "notes") AND len(trim("" & arguments.qaItem.notes))>
                <cfset arrayAppend(parts, "Notes: " & trim("" & arguments.qaItem.notes)) />
            </cfif>
        </cfif>

        <cfif arrayLen(parts) EQ 0>
            <cfreturn "" />
        </cfif>
        <cfreturn htmlEditFormat(arrayToList(parts, " | ")) />
    </cffunction>

    <!--- Build HTML table of questions and filled answers for import trace email --->
    <cffunction name="buildFilledAnswersTraceTableHtml" access="private" returntype="string" output="false">
        <cfargument name="filledAnswers" type="struct" required="true" />
        <cfargument name="assessmentReason" type="string" required="false" default="" />
        <cfargument name="lookup_db" type="string" required="false" default="hhapowerpath" />
        <cfargument name="agency_db" type="string" required="false" default="" />
        <cfargument name="answerLabelMap" type="struct" required="false" default="#structNew()#" />
        <cfargument name="answerUpdates" type="struct" required="false" default="#structNew()#" />
        <cfargument name="skippedAnswers" type="array" required="false" default="#arrayNew(1)#" />

        <cfset var html = "" />
        <cfset var questionMeta = {} />
        <cfset var questionIds = "" />
        <cfset var mapQid = "" />
        <cfset var qaItem = {} />
        <cfset var questionText = "" />
        <cfset var questionDescription = "" />
        <cfset var answerLabel = "" />
        <cfset var filledValue = "" />
        <cfset var importStatus = "" />
        <cfset var skippedReason = "" />
        <cfset var skippedPrefix = "" />
        <cfset var qMeta = {} />
        <cfset var getQuestions = "" />
        <cfset var vitalQuestions = [] />
        <cfset var vitalRow = {} />
        <cfset var woundQuestions = [] />
        <cfset var woundRow = {} />

        <cfif structCount(arguments.filledAnswers) EQ 0>
            <cfreturn "<p><em>No filled answers in payload.</em></p>" />
        </cfif>

        <cfif len(trim(arguments.assessmentReason))>
            <!--- #10641: PATTII lookup tables when feature flag is ON --->
            <cfif NOT structKeyExists(Request, "assessment_questions_lookup")>
                <cfinclude template="../../Application_assessment_lookup_PATTII.cfm" />
            </cfif>
            <cfquery name="getQuestions" datasource="#Application.DataSrc#">
                SELECT ques_id,
                       answer_label,
                       questions_label,
                       CASE
                           WHEN questions_2 <> '' THEN
                               TRIM(CONCAT_WS(' ', NULLIF(TRIM(ques_identifier_2), ''), NULLIF(TRIM(questions_2), '')))
                           ELSE TRIM(CONCAT_WS(' ', NULLIF(TRIM(ques_identifier), ''), NULLIF(TRIM(questions), '')))
                       END AS question_description
                FROM #arguments.lookup_db#.#Request.assessment_questions_lookup#
                WHERE assessment_reason = <cfqueryparam cfsqltype="cf_sql_varchar" value="#trim(arguments.assessmentReason)#">
                  AND Deleted = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
            </cfquery>
            <cfloop query="getQuestions">
                <cfset questionMeta[trim("" & getQuestions.ques_id)] = {
                    questions = trim("" & getQuestions.question_description),
                    questions_label = trim("" & getQuestions.questions_label),
                    answer_label = trim("" & getQuestions.answer_label)
                } />
            </cfloop>
        </cfif>

        <cfif len(trim(arguments.agency_db))>
            <cfset vitalQuestions = buildVitalAssessmentQuestions(agency_db = arguments.agency_db, lookup_db = arguments.lookup_db) />
            <cfloop array="#vitalQuestions#" index="vitalRow">
                <cfset questionMeta[trim("" & vitalRow.ques_id)] = {
                    questions = len(trim("" & vitalRow.description)) ? trim("" & vitalRow.description) : trim("" & vitalRow.questions),
                    questions_label = "",
                    answer_label = trim("" & vitalRow.answer_label)
                } />
            </cfloop>
            <cfset woundQuestions = buildWoundAssessmentQuestions(lookup_db = arguments.lookup_db) />
            <cfif arrayLen(woundQuestions) GT 0 AND structKeyExists(woundQuestions[1], "fields")>
                <cfloop array="#woundQuestions[1].fields#" index="woundRow">
                    <cfset questionMeta[trim("" & woundRow.ques_id)] = {
                        questions = len(trim("" & woundRow.description)) ? trim("" & woundRow.description) : trim("" & woundRow.questions),
                        questions_label = "",
                        answer_label = trim("" & woundRow.answer_label)
                    } />
                </cfloop>
            </cfif>
        </cfif>

        <cfset questionIds = structKeyArray(arguments.filledAnswers) />
        <cftry>
            <cfset questionIds = listSort(arrayToList(questionIds), "text", "asc") />
            <cfcatch type="any">
                <!--- keep original order if sort unavailable --->
            </cfcatch>
        </cftry>

        <cfset html = html & '<h4>Questions and Filled Answers</h4>' />
        <cfif len(trim(arguments.assessmentReason))>
            <cfset html = html & '<p><strong>Assessment Reason:</strong> ' & htmlEditFormat(trim(arguments.assessmentReason)) & '</p>' />
        </cfif>
        <cfset html = html & '<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%;font-family:Arial,sans-serif;font-size:13px;">' />
        <cfset html = html & '<thead style="background-color:##f3f4f6;"><tr>' />
        <cfset html = html & '<th align="left">Question</th><th align="left">Answer</th><th align="left">Import Status</th>' />
        <cfset html = html & '</tr></thead><tbody>' />

        <cfloop list="#questionIds#" index="mapQid">
            <cfset qaItem = arguments.filledAnswers[mapQid] />
            <cfset questionText = "" />
            <cfset questionDescription = "" />
            <cfset answerLabel = "" />
            <cfset importStatus = "" />
            <cfset skippedReason = "" />

            <cfif structKeyExists(questionMeta, mapQid)>
                <cfset qMeta = questionMeta[mapQid] />
                <cfset questionText = qMeta.questions />
                <cfset questionDescription = structKeyExists(qMeta, "questions_label") ? qMeta.questions_label : "" />
                <cfif len(qMeta.answer_label)>
                    <cfset answerLabel = uCase(qMeta.answer_label) />
                </cfif>
            </cfif>

            <cfif isStruct(qaItem)>
                <cfif structKeyExists(qaItem, "question") AND len(trim("" & qaItem.question))>
                    <cfset questionText = trim("" & qaItem.question) />
                <cfelseif structKeyExists(qaItem, "questions") AND len(trim("" & qaItem.questions))>
                    <cfset questionText = trim("" & qaItem.questions) />
                </cfif>
                <cfif structKeyExists(qaItem, "description") AND len(trim("" & qaItem.description))>
                    <cfset questionDescription = trim("" & qaItem.description) />
                <cfelseif structKeyExists(qaItem, "questions_label") AND len(trim("" & qaItem.questions_label))>
                    <cfset questionDescription = trim("" & qaItem.questions_label) />
                </cfif>
            </cfif>

            <cfif isStruct(qaItem) AND structKeyExists(qaItem, "answer_label") AND len(trim("" & qaItem.answer_label))>
                <cfset answerLabel = uCase(trim("" & qaItem.answer_label)) />
            <cfelseif NOT len(answerLabel) AND structKeyExists(arguments.answerLabelMap, mapQid)>
                <cfset answerLabel = arguments.answerLabelMap[mapQid] />
            </cfif>

            <cfif NOT len(questionText) AND len(questionDescription)>
                <cfset questionText = questionDescription />
            </cfif>
            <cfif NOT len(questionText) AND reFindNoCase("^vital_", mapQid)>
                <cfset questionText = "Vital sign (Item " & reReplaceNoCase(mapQid, "^vital_", "", "all") & ")" />
            </cfif>
            <cfif NOT len(questionText) AND reFindNoCase("^wound_", mapQid)>
                <cfset questionText = reReplaceNoCase(mapQid, "^wound_", "", "all") />
            </cfif>
            <cfif NOT len(questionText)>
                <cfset questionText = mapQid />
            </cfif>
            <cfif NOT len(answerLabel) AND reFindNoCase("^vital_", mapQid)>
                <cfset answerLabel = uCase(reReplaceNoCase(mapQid, "^", "", "all")) />
            </cfif>

            <cfset filledValue = formatTraceFilledAnswerValue(qaItem = qaItem, questionId = mapQid) />

            <cfloop array="#arguments.skippedAnswers#" index="skippedPrefix">
                <cfif left(skippedPrefix, len(mapQid) + 1) EQ mapQid & ":">
                    <cfset skippedReason = listRest(skippedPrefix, ":") />
                    <cfbreak />
                </cfif>
            </cfloop>

            <cfif len(skippedReason)>
                <cfset importStatus = "Skipped: " & htmlEditFormat(skippedReason) />
            <cfelseif reFindNoCase("^vital_", mapQid) AND len(filledValue)>
                <cfset importStatus = "Vital → pWoundsVitals" />
            <cfelseif reFindNoCase("^wound_", mapQid) AND len(filledValue)>
                <cfset importStatus = "Wound → pWoundsVitals" />
            <cfelseif isAssessmentFilledAnswerKey(mapQid) AND structKeyExists(arguments.answerUpdates, uCase(trim(mapQid)))>
                <cfset importStatus = "Applied to " & htmlEditFormat(uCase(trim(mapQid))) />
            <cfelseif len(answerLabel) AND reFind("^F[0-9]+$", answerLabel) AND structKeyExists(arguments.answerUpdates, answerLabel)>
                <cfset importStatus = "Applied to " & htmlEditFormat(answerLabel) />
            <cfelseif len(filledValue)>
                <cfset importStatus = "Received (not mapped)" />
            <cfelse>
                <cfset importStatus = "Empty" />
            </cfif>

            <cfset html = html & '<tr>' />
            <cfset html = html & '<td>' & htmlEditFormat(questionText) & '</td>' />
            <cfset html = html & '<td>' & filledValue & '</td>' />
            <cfset html = html & '<td>' & importStatus & '</td>' />
            <cfset html = html & '</tr>' />
        </cfloop>

        <cfset html = html & '</tbody></table>' />
        <cfreturn html />
    </cffunction>

    <!--- Keep trace email bodies within mail server limits --->
    <cffunction name="truncateForTraceEmail" 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="resolveWhisperSourceChannel" access="private" returntype="string" output="false">
        <cfargument name="websiteName" type="string" required="false" default="" />
        <cfargument name="sourceChannel" type="string" required="false" default="" />
        <cfif len(trim(arguments.sourceChannel))>
            <cfreturn trim(arguments.sourceChannel) />
        </cfif>
        <cfif findNoCase("mobile", arguments.websiteName)>
            <cfreturn "Mobile App" />
        </cfif>
        <cfreturn "Web" />
    </cffunction>

    <cffunction name="countWhisperImportSkipped" access="private" returntype="numeric" output="false">
        <cfargument name="importResult" type="struct" required="true" />
        <cfif structKeyExists(arguments.importResult, "skippedItems") AND isArray(arguments.importResult.skippedItems)>
            <cfreturn arrayLen(arguments.importResult.skippedItems) />
        </cfif>
        <cfif structKeyExists(arguments.importResult, "skipped") AND isArray(arguments.importResult.skipped)>
            <cfreturn arrayLen(arguments.importResult.skipped) />
        </cfif>
        <cfif structKeyExists(arguments.importResult, "skipped")>
            <cfreturn val(arguments.importResult.skipped) />
        </cfif>
        <cfreturn 0 />
    </cffunction>

    <cffunction name="buildWhisperVitalsWoundsResultEmailHtml" access="private" returntype="string" output="false"
        hint="HTML summary matching mobile saveAssessmentWoundsVitals email.">
        <cfargument name="vitalImportResult" type="struct" required="true" />
        <cfargument name="woundImportResult" type="struct" required="true" />
        <cfset var html = "" />
        <cfset var vitalUpdated = structKeyExists(arguments.vitalImportResult, "updated") ? val(arguments.vitalImportResult.updated) : 0 />
        <cfset var vitalInserted = structKeyExists(arguments.vitalImportResult, "inserted") ? val(arguments.vitalImportResult.inserted) : 0 />
        <cfset var vitalSkipped = countWhisperImportSkipped(arguments.vitalImportResult) />
        <cfset var woundUpdated = structKeyExists(arguments.woundImportResult, "updated") ? val(arguments.woundImportResult.updated) : 0 />
        <cfset var woundInserted = structKeyExists(arguments.woundImportResult, "inserted") ? val(arguments.woundImportResult.inserted) : 0 />
        <cfset var woundSkipped = countWhisperImportSkipped(arguments.woundImportResult) />
        <cfset var woundPhotos = structKeyExists(arguments.woundImportResult, "photos_uploaded") ? val(arguments.woundImportResult.photos_uploaded) : 0 />
        <cfset var appliedJson = "" />
        <cfset var skippedJson = "" />
        <cfset var skippedPayload = [] />

        <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(arguments.vitalImportResult, "applied") AND isArray(arguments.vitalImportResult.applied) AND arrayLen(arguments.vitalImportResult.applied) GT 0>
            <cfset appliedJson = truncateForTraceEmail(serializeJSON(arguments.vitalImportResult.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>
        <cfset skippedPayload = [] />
        <cfif structKeyExists(arguments.vitalImportResult, "skippedItems") AND isArray(arguments.vitalImportResult.skippedItems)>
            <cfset skippedPayload = arguments.vitalImportResult.skippedItems />
        <cfelseif structKeyExists(arguments.vitalImportResult, "skipped") AND isArray(arguments.vitalImportResult.skipped)>
            <cfset skippedPayload = arguments.vitalImportResult.skipped />
        </cfif>
        <cfif arrayLen(skippedPayload) GT 0>
            <cfset skippedJson = truncateForTraceEmail(serializeJSON(skippedPayload)) />
            <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(arguments.woundImportResult, "applied") AND isArray(arguments.woundImportResult.applied) AND arrayLen(arguments.woundImportResult.applied) GT 0>
            <cfset appliedJson = truncateForTraceEmail(serializeJSON(arguments.woundImportResult.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>
        <cfset skippedPayload = [] />
        <cfif structKeyExists(arguments.woundImportResult, "skippedItems") AND isArray(arguments.woundImportResult.skippedItems)>
            <cfset skippedPayload = arguments.woundImportResult.skippedItems />
        <cfelseif structKeyExists(arguments.woundImportResult, "skipped") AND isArray(arguments.woundImportResult.skipped)>
            <cfset skippedPayload = arguments.woundImportResult.skipped />
        </cfif>
        <cfif arrayLen(skippedPayload) GT 0>
            <cfset skippedJson = truncateForTraceEmail(serializeJSON(skippedPayload)) />
            <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>

    <cffunction name="sendWhisperVitalsWoundsUpdateEmail" access="private" returntype="void" output="false"
        hint="Dedicated email matching mobile saveAssessmentWoundsVitals when Whisper imports vitals/wounds.">
        <cfargument name="agencyID" type="numeric" required="true" />
        <cfargument name="empID" type="numeric" required="true" />
        <cfargument name="patientID" type="numeric" required="true" />
        <cfargument name="assessmentID" type="numeric" required="true" />
        <cfargument name="websiteName" type="string" required="false" default="" />
        <cfargument name="sourceChannel" type="string" required="false" default="" />
        <cfargument name="jobID" type="string" required="false" default="" />
        <cfargument name="whisperFilesId" type="numeric" required="false" default="0" />
        <cfargument name="success" type="boolean" required="false" default="true" />
        <cfargument name="message" type="string" required="false" default="" />
        <cfargument name="vitalImportResult" type="struct" required="true" />
        <cfargument name="woundImportResult" type="struct" required="true" />
        <cfargument name="requestJSONString" type="string" required="false" default="" />
        <cfargument name="responseJSONString" type="string" required="false" default="" />

        <cfset var mailTo = this.whisperVitalsWoundsEmail />
        <cfset var channel = resolveWhisperSourceChannel(websiteName = arguments.websiteName, sourceChannel = arguments.sourceChannel) />
        <cfset var vitalUpdated = structKeyExists(arguments.vitalImportResult, "updated") ? val(arguments.vitalImportResult.updated) : 0 />
        <cfset var vitalInserted = structKeyExists(arguments.vitalImportResult, "inserted") ? val(arguments.vitalImportResult.inserted) : 0 />
        <cfset var woundUpdated = structKeyExists(arguments.woundImportResult, "updated") ? val(arguments.woundImportResult.updated) : 0 />
        <cfset var woundInserted = structKeyExists(arguments.woundImportResult, "inserted") ? val(arguments.woundImportResult.inserted) : 0 />
        <cfset var summaryHtml = "" />
        <cfset var successFlag = arguments.success ? "Yes" : "No" />
        <cfset var mailSubject = "" />
        <cfset var requestBody = truncateForTraceEmail(arguments.requestJSONString) />
        <cfset var responseBody = truncateForTraceEmail(arguments.responseJSONString) />

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

        <cfset summaryHtml = buildWhisperVitalsWoundsResultEmailHtml(
            vitalImportResult = arguments.vitalImportResult,
            woundImportResult = arguments.woundImportResult
        ) />
        <cfset mailSubject = "Whisper Wounds/Vitals Save - #channel# - Assmt #arguments.assessmentID# - V:I#vitalInserted#/U#vitalUpdated# W:I#woundInserted#/U#woundUpdated# (#successFlag#)" />

        <cftry>
            <cfmail to="#mailTo#"
                    from="success@myhomecarebiz.com"
                    subject="#mailSubject#"
                    type="html">
                <h3 style="color: ##1d4ed8;">importAssessmentData Wounds/Vitals Save Trace</h3>
                <p><strong>Source:</strong> #htmlEditFormat(channel)#</p>
                <p><strong>Timestamp:</strong> #dateFormat(now(), 'mm/dd/yyyy')# #timeFormat(now(), 'HH:mm:ss')#</p>
                <p><strong>Success:</strong> #successFlag#</p>
                <cfif len(trim(arguments.message))>
                    <p><strong>Message:</strong> #htmlEditFormat(arguments.message)#</p>
                </cfif>
                <p><strong>Agency ID:</strong> #arguments.agencyID#</p>
                <p><strong>Employee ID:</strong> #arguments.empID#</p>
                <p><strong>Patient ID:</strong> #arguments.patientID#</p>
                <p><strong>Assessment ID:</strong> #arguments.assessmentID#</p>
                <cfif len(trim(arguments.websiteName))>
                    <p><strong>Website:</strong> #htmlEditFormat(arguments.websiteName)#</p>
                </cfif>
                <cfif len(trim(arguments.jobID))>
                    <p><strong>Job ID:</strong> #htmlEditFormat(arguments.jobID)#</p>
                </cfif>
                <cfif val(arguments.whisperFilesId) GT 0>
                    <p><strong>Whisper File ID:</strong> #val(arguments.whisperFilesId)#</p>
                </cfif>
                <hr>
                #summaryHtml#
                <hr>
                <h4>Request Payload</h4>
                <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">#htmlEditFormat(requestBody)#</pre>
                <h4>Response Payload</h4>
                <pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">#htmlEditFormat(responseBody)#</pre>
                <hr>
                <p><em>Automated trace from whisper_ai.cfc importAssessmentData (same format as mobile saveAssessmentWoundsVitals)</em></p>
            </cfmail>
            <cfcatch type="any"></cfcatch>
        </cftry>
    </cffunction>

    <cffunction name="sendImportAssessmentTraceEmail" access="private" returntype="void" hint="Send importAssessmentData request, response, and SQL trace email">
        <cfargument name="agencyID" type="numeric" required="yes">
        <cfargument name="empID" type="numeric" required="yes">
        <cfargument name="patientID" type="numeric" required="yes">
        <cfargument name="assessmentID" type="numeric" required="yes">
        <cfargument name="websiteName" type="string" required="yes">
        <cfargument name="requestJSONString" type="string" required="yes">
        <cfargument name="responseJSONString" type="string" required="yes">
        <cfargument name="queryTrace" type="array" required="yes">
        <cfargument name="currentStep" type="string" required="yes">
        <cfargument name="stepErrors" type="array" required="no" default="#ArrayNew(1)#">
        <cfargument name="jobID" type="string" required="no" default="">
        <cfargument name="filledAnswers" type="struct" required="no" default="#structNew()#">
        <cfargument name="assessmentReason" type="string" required="no" default="">
        <cfargument name="lookup_db" type="string" required="no" default="hhapowerpath">
        <cfargument name="agency_db" type="string" required="no" default="">
        <cfargument name="answerLabelMap" type="struct" required="no" default="#structNew()#">
        <cfargument name="answerUpdates" type="struct" required="no" default="#structNew()#">
        <cfargument name="skippedAnswers" type="array" required="no" default="#arrayNew(1)#">
        <cfargument name="vitalImportResult" type="struct" required="no" default="#structNew()#">
        <cfargument name="woundImportResult" type="struct" required="no" default="#structNew()#">

        <cfset var traceEntry = "" />
        <cfset var questionsAnswersTableHtml = "" />
        <cfset var vitalsWoundsSummaryHtml = "" />
        <cfset var requestBody = "" />
        <cfset var responseBody = "" />
        <cfset var mailError = "" />
        <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 sendto_email = "velmurugan@myhomecarebiz.com,janice.l@myhomecarebiz.com,melissa@myhomecarebiz.com,jasmin.s@myhomecarebiz.com" />

        <cfset requestBody = truncateForTraceEmail(arguments.requestJSONString) />
        <cfset responseBody = truncateForTraceEmail(arguments.responseJSONString) />

        <cfif structKeyExists(arguments.vitalImportResult, "updated")>
            <cfset vitalUpdated = val(arguments.vitalImportResult.updated) />
        </cfif>
        <cfif structKeyExists(arguments.vitalImportResult, "inserted")>
            <cfset vitalInserted = val(arguments.vitalImportResult.inserted) />
        </cfif>
        <cfif structKeyExists(arguments.vitalImportResult, "skipped")>
            <cfset vitalSkipped = val(arguments.vitalImportResult.skipped) />
        </cfif>
        <cfif structKeyExists(arguments.woundImportResult, "updated")>
            <cfset woundUpdated = val(arguments.woundImportResult.updated) />
        </cfif>
        <cfif structKeyExists(arguments.woundImportResult, "inserted")>
            <cfset woundInserted = val(arguments.woundImportResult.inserted) />
        </cfif>
        <cfif structKeyExists(arguments.woundImportResult, "skipped")>
            <cfset woundSkipped = val(arguments.woundImportResult.skipped) />
        </cfif>

        <cfset vitalsWoundsSummaryHtml = '<h4>Vitals &amp; Wounds Import Response</h4>' />
        <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & '<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;font-family:Arial,sans-serif;font-size:13px;margin-bottom:12px;">' />
        <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & '<thead style="background-color:##f3f4f6;"><tr><th align="left">Type</th><th align="left">Updated</th><th align="left">Inserted</th><th align="left">Skipped</th><th align="left">Notes</th></tr></thead><tbody>' />
        <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & '<tr><td>Vitals (vital_* → pWoundsVitals)</td><td>' & vitalUpdated & '</td><td>' & vitalInserted & '</td><td>' & vitalSkipped & '</td><td>' />
        <cfif (vitalUpdated + vitalInserted + vitalSkipped) EQ 0>
            <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & 'No vital_* keys in payload (optional)' />
        <cfelse>
            <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & 'Applied' />
        </cfif>
        <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & '</td></tr>' />
        <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & '<tr><td>Wounds (wound_* → pWoundsVitals)</td><td>' & woundUpdated & '</td><td>' & woundInserted & '</td><td>' & woundSkipped & '</td><td>' />
        <cfif (woundUpdated + woundInserted + woundSkipped) EQ 0>
            <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & 'No wound_* keys in payload (optional)' />
        <cfelse>
            <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & 'Applied' />
        </cfif>
        <cfset vitalsWoundsSummaryHtml = vitalsWoundsSummaryHtml & '</td></tr></tbody></table>' />

        <cftry>
            <cfset questionsAnswersTableHtml = buildFilledAnswersTraceTableHtml(
                filledAnswers = arguments.filledAnswers,
                assessmentReason = arguments.assessmentReason,
                lookup_db = arguments.lookup_db,
                agency_db = arguments.agency_db,
                answerLabelMap = arguments.answerLabelMap,
                answerUpdates = arguments.answerUpdates,
                skippedAnswers = arguments.skippedAnswers
            ) />
            <cfcatch type="any">
                <cfset questionsAnswersTableHtml = "<p style=""color:##b45309;""><strong>Questions table could not be built:</strong> " & htmlEditFormat(cfcatch.message) & "</p>" />
            </cfcatch>
        </cftry>

        <cftry>
            <cfmail to="#sendto_email#,bairammarcelo03@gmail.com,ivan_buhain@yahoo.com,shereenalainah@gmail.com,maecalija15@gmail.com,crstldizon@gmail.com,malizane.cupcupin@gmail.com,janicesexon@yahoo.com"
                    from="success@myhomecarebiz.com"
                    subject="Whisper importAssessmentData Trace - Assessment #arguments.assessmentID#"
                    type="html">
                <h3 style="color: ##1d4ed8;">importAssessmentData Request / Response Trace</h3>
                <p><strong>Agency ID:</strong> #arguments.agencyID#</p>
                <p><strong>Employee ID:</strong> #arguments.empID#</p>
                <p><strong>Patient ID:</strong> #arguments.patientID#</p>
                <p><strong>Assessment ID:</strong> #arguments.assessmentID#</p>
                <cfif len(trim(arguments.websiteName)) gt 0>
                    <p><strong>Website:</strong> #htmlEditFormat(arguments.websiteName)#</p>
                </cfif>
                <cfif len(trim(arguments.jobID)) gt 0>
                    <p><strong>Job ID:</strong> #htmlEditFormat(arguments.jobID)#</p>
                </cfif>
                <p><strong>Timestamp:</strong> #DateFormat(Now(), 'mm/dd/yyyy')# #TimeFormat(Now(), 'HH:mm:ss')#</p>
                <p><strong>Last Step:</strong> #htmlEditFormat(arguments.currentStep)#</p>
                <hr>
                #vitalsWoundsSummaryHtml#
                <hr>
                #questionsAnswersTableHtml#
                <hr>
                <h4>Request Payload</h4>
                <pre style="white-space: pre-wrap; font-family: monospace;">#htmlEditFormat(requestBody)#</pre>
                <h4>Response Payload</h4>
                <pre style="white-space: pre-wrap; font-family: monospace;">#htmlEditFormat(responseBody)#</pre>
                <cfif arrayLen(arguments.stepErrors) gt 0>
                    <h4>Step Errors / Warnings</h4>
                    <ul>
                        <cfloop array="#arguments.stepErrors#" index="traceEntry">
                            <li>#htmlEditFormat(traceEntry)#</li>
                        </cfloop>
                    </ul>
                </cfif>
                <hr>
                <p><em>This trace email is sent only for importAssessmentData.</em></p>
            </cfmail>
            <cfcatch type="any">
                <cfset mailError = cfcatch.message & (len(trim(cfcatch.detail)) ? " | " & cfcatch.detail : "") />
                <cftry>
                    <cflog file="whisper_import_trace" type="error" text="Trace email failed for Assessment #arguments.assessmentID#: #mailError#" />
                    <cfcatch type="any"></cfcatch>
                </cftry>
                <cftry>
                    <cfmail to="#sendto_email#"
                            from="success@myhomecarebiz.com"
                            subject="Whisper importAssessmentData Trace FAILED - Assessment #arguments.assessmentID#"
                            type="html">
                        <p>The full trace email could not be sent.</p>
                        <p><strong>Error:</strong> #htmlEditFormat(mailError)#</p>
                        <p><strong>Assessment ID:</strong> #arguments.assessmentID#</p>
                        <p><strong>Last Step:</strong> #htmlEditFormat(arguments.currentStep)#</p>
                    </cfmail>
                    <cfcatch type="any"></cfcatch>
                </cftry>
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Helper: Log query for debugging --->
    <cffunction name="logQuery" access="private" returntype="void" hint="Log query for debugging purposes">
        <cfargument name="queryName" type="string" required="yes">
        <cfargument name="querySQL" type="string" required="yes">
        <cfargument name="queryResult" type="any" required="no">
        
        <!--- Store queries in request scope for email dumping --->
        <cfif not structKeyExists(request, "bulk_import_queries")>
            <cfset request.bulk_import_queries = [] />
        </cfif>
        
        <cfset var queryInfo = {
            name: arguments.queryName,
            sql: arguments.querySQL,
            timestamp: now(),
            hasResult: structKeyExists(arguments, "queryResult"),
            recordCount: structKeyExists(arguments, "queryResult") and isQuery(arguments.queryResult) ? arguments.queryResult.recordcount : 0
        } />
        
        <cfset arrayAppend(request.bulk_import_queries, queryInfo) />
    </cffunction>

    <!--- Serialize as JSON integer (avoids 355.0 from CF val()/double) --->
    <cffunction name="toJsonInt" access="private" returntype="any" output="false">
        <cfargument name="value" required="true" />
        <cftry>
            <cfreturn javacast("int", int(val(arguments.value))) />
            <cfcatch type="any">
                <cfreturn int(val(arguments.value)) />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- Helper: Validate key for public assessment lookup endpoint --->
    <!--- Uses this.assessmentLookupApiKey — a dedicated key, not Application.enckey --->  
    <cffunction name="isAssessmentLookupKeyValid" access="private" returntype="boolean" hint="Validate API key for assessment lookup endpoint">
        <cfargument name="api_key" type="string" required="yes" hint="API key">

        <!--- Use Application-scope override if configured, otherwise fall back to the dedicated component key --->
        <cfset var expectedKey = structKeyExists(Application, "whisperLookupApiKey") AND len(trim(Application.whisperLookupApiKey))
            ? trim(Application.whisperLookupApiKey)
            : trim(this.assessmentLookupApiKey) />

        <cfreturn len(trim(arguments.api_key)) GT 0 AND trim(arguments.api_key) EQ expectedKey />
    </cffunction>

    <!--- Title-case Vital_Sign from pVital_Param for pWoundsVitals.Description display --->
    <cffunction name="formatVitalSignLabel" access="private" returntype="string" output="false">
        <cfargument name="vital_sign" type="string" required="true" />
        <cfset var parts = listToArray(trim(arguments.vital_sign), " ") />
        <cfset var out = "" />
        <cfset var i = 0 />
        <cfset var part = "" />
        <cfloop from="1" to="#arrayLen(parts)#" index="i">
            <cfset part = trim(parts[i]) />
            <cfif len(part)>
                <!--- Right() requires count GT 0; single-char tokens cannot use right(part, 0) --->
                <cfif len(part) EQ 1>
                    <cfset part = uCase(part) />
                <cfelse>
                    <cfset part = uCase(left(part, 1)) & lCase(right(part, len(part) - 1)) />
                </cfif>
                <cfset out = len(out) ? out & " " & part : part />
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <!--- Build { ANSWER: value } options for dropdown/radio/checkbox questions --->
    <cffunction name="buildAnswerOptionsFromArray" access="private" returntype="array" output="false">
        <cfargument name="values" type="array" required="true" />
        <cfset var answers = [] />
        <cfset var valItem = "" />
        <cfloop array="#arguments.values#" index="valItem">
            <cfset valItem = trim("" & valItem) />
            <cfif len(valItem)>
                <cfset arrayAppend(answers, { "ANSWER" = valItem }) />
            </cfif>
        </cfloop>
        <cfreturn answers />
    </cffunction>

    <cffunction name="buildAnswerOptionsFromList" access="private" returntype="array" output="false">
        <cfargument name="values" type="string" required="true" />
        <cfargument name="delimiter" type="string" required="false" default="," />
        <cfreturn buildAnswerOptionsFromArray(listToArray(arguments.values, arguments.delimiter)) />
    </cffunction>

    <!--- Standard assessment-question row for mobile / Whisper (matches lookup question shape) --->
    <cffunction name="makeFormAssessmentQuestion" access="private" returntype="struct" output="false">
        <cfargument name="quesId" type="string" required="true" />
        <cfargument name="label" type="string" required="true" />
        <cfargument name="answerLabel" type="string" required="true" hint="pWoundsVitals / save column name" />
        <cfargument name="questionsType" type="string" required="true" />
        <cfargument name="answers" type="array" required="false" default="#[]#" />
        <cfargument name="extra" type="struct" required="false" default="#{}#" />

        <cfset var qRow = {
            "ques_id" = arguments.quesId,
            "questions" = arguments.label,
            "description" = arguments.label,
            "answer_label" = arguments.answerLabel,
            "questions_type" = arguments.questionsType,
            "answers" = arguments.answers
        } />
        <cfset structAppend(qRow, arguments.extra, true) />
        <cfreturn qRow />
    </cffunction>

    <!--- Location dropdown values from woundsadd.cfm txtmap --->
    <cffunction name="getWoundLocationOptions" access="private" returntype="array" output="false">
        <cfreturn [
            "Anterior Left Shoulder", "Anterior Right Shoulder", "Anterior Right Upper Chest",
            "Anterior Right Above Knee", "Anterior Right Ankle", "Anterior Left Upper Chest",
            "Anterior Right Face", "Anterior Left Face", "Anterior Right Upper Arm", "Anterior Left Upper Arm",
            "Anterior Right Abdomen", "Anterior Midline", "Anterior Right Knee", "Anterior Left Abdomen",
            "Anterior Right Elbow/Forearm", "Anterior Left Elbow/Forearm", "Anterior Right Wrist/Hand",
            "Anterior Left Wrist/Hand", "Anterior Right Thigh", "Anterior Left Thigh", "Anterior Right Hip",
            "Anterior Left Hip", "Anterior Left Groin", "Anterior Left Above Knee", "Anterior Left Ankle",
            "Anterior Left Knee", "Anterior Right Below Knee", "Anterior Left Below Knee",
            "Anterior Right Foot", "Anterior Left Foot", "Posterior Right Head", "Posterior Left Neck",
            "Posterior Right Neck", "Posterior Midline", "Posterior Left Shoulder/Upper Back",
            "Posterior Right Shoulder/Upper Back", "Posterior Right Middle/Lower Back", "Posterior Sacrum",
            "Posterior Left Middle Back", "Posterior Left Upper Arm", "Posterior Right Shoulder/Upper Arm",
            "Posterior Left Elbow/Forearm", "Posterior Right Elbow/Forearm", "Posterior Right Wrist",
            "Posterior Right Hand", "Posterior Left Hand", "Posterior Left Thigh", "Posterior Right Thigh",
            "Posterior Left Calf", "Posterior Right Buttock", "Posterior Right Calf", "Posterior Left Buttock",
            "Posterior Left Knee", "Posterior Right Knee", "Posterior Left Ankle/Foot", "Posterior Right Ankle/Foot",
            "Posterior Left Heel", "Posterior Right Heel", "Other"
        ] />
    </cffunction>

    <!--- Wound form fields from woundsadd.cfm mapped to pWoundsVitals columns (pwoundvital.cfc addEditWounds) --->
    <cffunction name="buildWoundAssessmentQuestions" access="private" returntype="array" output="false"
        hint="Returns one wound container with fields[] sub-questions; ques_id prefix wound_ on each field">
        <cfargument name="lookup_db" type="string" required="true" />

        <cfset var woundFields = [] />
        <cfset var measureQuery = "" />
        <cfset var measurementAnswers = [] />
        <cfset var locAnswers = [] />
        <cfset var loc = "" />
        <cfset var measureUnit = "cm" />

        <cfquery name="measureQuery" datasource="#Application.DataSrc#">
            SELECT Lookup_1, Lookup_2, Measure
            FROM #arguments.lookup_db#.pValue_Lookup
            WHERE Item_ID = <cfqueryparam value="201" cfsqltype="cf_sql_integer">
            ORDER BY ID
        </cfquery>
        <cfloop query="measureQuery">
            <cfif measureQuery.currentRow EQ 1 AND len(trim(measureQuery.Measure))>
                <cfset measureUnit = trim(measureQuery.Measure) />
            </cfif>
            <cfset arrayAppend(measurementAnswers, {
                "ANSWER" = trim(measureQuery.Lookup_1),
                "ANSWER_2" = trim(measureQuery.Lookup_2)
            }) />
        </cfloop>

        <cfloop array="#getWoundLocationOptions()#" index="loc">
            <cfset arrayAppend(locAnswers, { "ANSWER" = loc }) />
        </cfloop>

        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_woundtype",
            label = "Wound Type",
            answerLabel = "woundtype",
            questionsType = "radio",
            answers = buildAnswerOptionsFromArray([
                "Abrasion", "Bite", "Burn", "Contusion", "Diabetic Ulcer", "Blister", "Laceration", "Nodule",
                "IV Access - Peripheral", "IV Access - Midline", "IV Access - Central", "Tracheostomy", "GI Ostomy",
                "GU Ostomy", "Open Wound", "Pressure Ulcer", "Puncture", "Observable Stasis Ulcer",
                "Non-Observable Stasis Ulcer", "Observable Surgical Wound", "Non-Observable Surgical Wound", "Other"
            ]),
            extra = { "form_field" = "WoundType" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_pressure_stage",
            label = "If pressure ulcer stage",
            answerLabel = "woundtype",
            questionsType = "dropdown",
            answers = buildAnswerOptionsFromArray([
                "1 - Intact skin with non-blanchable redness of a localized area usually over a bony prominence.",
                "2 - Partial thickness loss of dermis presenting as a shallow open ulcer with red pink wound bed.",
                "3 - Full thickness tissue loss. Subcutaneous fat may be visible but bone, tendon, or muscles not exposed.",
                "4 - Full thickness tissue loss with visible bone, tendon, or muscle. Slough or eschar may be present.",
                "5 - Unstageable due to non-removable dressing, slough or eschar.",
                "6 - Unstageable due to coverage of wound bed by slough and/or eschar.",
                "7 - Unstageable due to suspected deep tissue injury in evolution."
            ]),
            extra = { "form_field" = "selectstage", "show_when_woundtype" = "Pressure Ulcer" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_location",
            label = "Location",
            answerLabel = "Location",
            questionsType = "dropdown",
            answers = locAnswers,
            extra = { "form_field" = "txtmap" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_other_locations",
            label = "Other Location",
            answerLabel = "other_locations",
            questionsType = "text",
            extra = { "form_field" = "other_locations", "show_when_location" = "Other" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_healingstage",
            label = "Healing Stage",
            answerLabel = "healingstage",
            questionsType = "dropdown",
            answers = buildAnswerOptionsFromArray([
                "0 - Re-epithelialized or healed",
                "1 - Fully granulating",
                "2 - Early/partial granulation",
                "3 - Not healing",
                "NA - Not applicable or no observable wound/ulcer"
            ]),
            extra = { "form_field" = "healingStage" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_length",
            label = "Length",
            answerLabel = "Length",
            questionsType = "measurement_dropdown",
            answers = measurementAnswers,
            extra = { "form_field_primary" = "lengthmin", "form_field_secondary" = "lengthmin2", "measure" = measureUnit }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_width",
            label = "Width",
            answerLabel = "Width",
            questionsType = "measurement_dropdown",
            answers = measurementAnswers,
            extra = { "form_field_primary" = "widthmin", "form_field_secondary" = "widthmin2", "measure" = measureUnit }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_depth",
            label = "Depth",
            answerLabel = "Depth",
            questionsType = "measurement_dropdown",
            answers = measurementAnswers,
            extra = { "form_field_primary" = "depthmin", "form_field_secondary" = "depthmin2", "measure" = measureUnit }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_tunneling",
            label = "tunneling dep/drctn",
            answerLabel = "wnd_tunneling",
            questionsType = "text",
            extra = { "form_field" = "WoundTunneling" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_underm",
            label = "undermn dep/drctn",
            answerLabel = "wnd_underm",
            questionsType = "text",
            extra = { "form_field" = "WoundUnderm" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_gran",
            label = "% granulation",
            answerLabel = "wnd_gran",
            questionsType = "text",
            extra = { "form_field" = "WoundGran" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_epith",
            label = "% epithelial",
            answerLabel = "wnd_epith",
            questionsType = "text",
            extra = { "form_field" = "WoundEpith" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_eschar",
            label = "% eschar",
            answerLabel = "wnd_eschar",
            questionsType = "text",
            extra = { "form_field" = "WoundEschar" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_slough",
            label = "% slough",
            answerLabel = "wnd_slough",
            questionsType = "text",
            extra = { "form_field" = "WoundSlough" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_fib",
            label = "% fibrinous",
            answerLabel = "wnd_fib",
            questionsType = "text",
            extra = { "form_field" = "WoundFib" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_margin",
            label = "Wound Margin",
            answerLabel = "wnd_margin",
            questionsType = "checkbox",
            answers = buildAnswerOptionsFromList("regular,irregular,rolled,epithelialized,macerated,other"),
            extra = { "form_field" = "WoundMargin", "other_form_field" = "otherWoundMargin_textarea" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_exudate",
            label = "Wound Exudate",
            answerLabel = "wnd_exudate",
            questionsType = "checkbox",
            answers = buildAnswerOptionsFromList("none,scant,moderate,large clear,sanguinous,purulent,other"),
            extra = { "form_field" = "WoundExudate", "other_form_field" = "otherWoundExudate_textarea" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_odor",
            label = "Wound Odor",
            answerLabel = "wnd_odor",
            questionsType = "checkbox",
            answers = buildAnswerOptionsFromList("none,mild,foul,other"),
            extra = { "form_field" = "WoundOdor", "other_form_field" = "otherWoundOdor_textarea" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_wnd_pain",
            label = "Wound Pain",
            answerLabel = "wnd_pain",
            questionsType = "checkbox",
            answers = buildAnswerOptionsFromList("none,mild,strong,severe,other"),
            extra = { "form_field" = "WoundPain", "other_form_field" = "otherWoundPain_textarea" }
        )) />
        <cfset arrayAppend(woundFields, makeFormAssessmentQuestion(
            quesId = "wound_description",
            label = "Notes",
            answerLabel = "Description",
            questionsType = "text",
            extra = { "form_field" = "txtNotes" }
        )) />

        <!--- Single wound entry in questions list; all form fields live under fields[] --->
        <cfreturn [{
            "ques_id" = "wound",
            "questions" = "Wounds",
            "description" = "Wounds",
            "questions_type" = "wound",
            "answer_label" = "",
            "answers" = [],
            "fields" = woundFields,
            "field_count" = toJsonInt(arrayLen(woundFields))
        }] />
    </cffunction>

    <!--- Build vital sign questions: Description from lookup, valid range from pVital_Param (no answers list) --->
    <cffunction name="buildVitalAssessmentQuestions" access="private" returntype="array" output="false"
        hint="Returns vital question structs; ques_id vital_{item_id}, answer_label Reading1, range param_lo/param_hi">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="lookup_db" type="string" required="true" />

        <cfset var vitalQuestions = [] />
        <cfset var paramRows = "" />
        <cfset var qRow = {} />
        <cfset var valueLow = "" />
        <cfset var valueHigh = "" />
        <cfset var description = "" />

        <cfif NOT len(trim(arguments.agency_db))>
            <cfreturn vitalQuestions />
        </cfif>

        <cfquery name="paramRows" datasource="#Application.DataSrc#">
            SELECT vp.Item_ID,
                   vp.Value_Low,
                   vp.Value_High,
                   MAX(vl.Description) AS Description,
                   MAX(vl.Measure) AS Measure,
                   MAX(vl.Required) AS Required
            FROM #arguments.agency_db#.pVital_Param vp
            INNER JOIN #arguments.lookup_db#.pValue_Lookup vl
              ON vl.Item_ID = vp.Item_ID
             AND vl.Description <> <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            WHERE vp.Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
              AND vp.Vital_Sign <> <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            GROUP BY vp.Item_ID, vp.Value_Low, vp.Value_High
            ORDER BY vp.Item_ID
        </cfquery>

        <cfloop query="paramRows">
            <cfset valueLow = trim(paramRows.Value_Low) />
            <cfset valueHigh = trim(paramRows.Value_High) />
            <cfset description = trim(paramRows.Description) />
            <cfif NOT len(description)>
                <cfcontinue />
            </cfif>
            <cfset qRow = makeFormAssessmentQuestion(
                quesId = "vital_" & val(paramRows.Item_ID),
                label = description,
                answerLabel = "Reading1",
                questionsType = "numeric",
                answers = [],
                extra = {
                    "item_id" = toJsonInt(paramRows.Item_ID),
                    "param_lo" = valueLow,
                    "param_hi" = valueHigh,
                    "measure" = trim(paramRows.Measure),
                    "required" = (trim(paramRows.Required) EQ "*"),
                    "notes_answer_label" = "Notes"
                }
            ) />
            <cfset arrayAppend(vitalQuestions, qRow) />
        </cfloop>

        <cfreturn vitalQuestions />
    </cffunction>

    <!--- Normalize filled answer payload value to string for DB storage --->
    <cffunction name="normalizeWhisperFilledAnswerValue" access="private" returntype="string" output="false">
        <cfargument name="rawAnswer" required="true" />
        <cfif isNull(arguments.rawAnswer)>
            <cfreturn "" />
        </cfif>
        <cfif isArray(arguments.rawAnswer)>
            <cfif arrayLen(arguments.rawAnswer) EQ 0>
                <cfreturn "" />
            </cfif>
            <cfreturn arrayToList(arguments.rawAnswer, ", ") />
        </cfif>
        <cfif isStruct(arguments.rawAnswer)>
            <cfreturn serializeJSON(arguments.rawAnswer) />
        </cfif>
        <cfreturn trim("" & arguments.rawAnswer) />
    </cffunction>

    <!--- True when filled answer key is an assessment F-column (F30, F242, etc.) --->
    <cffunction name="isAssessmentFilledAnswerKey" access="private" returntype="boolean" output="false">
        <cfargument name="key" type="string" required="true" />
        <cfreturn reFind("^F[0-9]+$", uCase(trim(arguments.key))) GT 0 />
    </cffunction>

    <cffunction name="isVitalFilledAnswerKey" access="private" returntype="boolean" output="false">
        <cfargument name="key" type="string" required="true" />
        <cfreturn reFindNoCase("^vital_", trim(arguments.key)) GT 0 />
    </cffunction>

    <cffunction name="isWoundFilledAnswerKey" access="private" returntype="boolean" output="false">
        <cfargument name="key" type="string" required="true" />
        <cfreturn reFindNoCase("^wound_", trim(arguments.key)) GT 0 />
    </cffunction>

    <!--- Normalize AI vital payload into pWoundsVitals fields --->
    <cffunction name="normalizeVitalFilledAnswer" access="private" returntype="struct" output="false">
        <cfargument name="qaItem" type="struct" required="true" />
        <cfargument name="itemId" type="numeric" required="true" />
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="lookup_db" type="string" required="false" default="hhapowerpath" />

        <cfset var out = {
            ok = false,
            item_id = arguments.itemId,
            description = "",
            reading1 = "",
            notes = "",
            pain_location = "",
            pain_duration = "",
            pain_exacerbate = "",
            pain_relief = "",
            pain_med_effectiveness = ""
        } />
        <cfset var rawAnswer = "" />
        <cfset var readingPrimary = "" />
        <cfset var readingSecondary = "" />
        <cfset var descriptor = "" />
        <cfset var freeNotes = "" />
        <cfset var qVitalParam = "" />

        <cfif structKeyExists(arguments.qaItem, "question") AND len(trim("" & arguments.qaItem.question))>
            <cfset out.description = formatVitalSignLabel(trim("" & arguments.qaItem.question)) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "description") AND len(trim("" & arguments.qaItem.description))>
            <cfset out.description = trim("" & arguments.qaItem.description) />
        </cfif>
        <cfif NOT len(out.description) AND structKeyExists(arguments.qaItem, "vital_sign") AND len(trim("" & arguments.qaItem.vital_sign))>
            <cfset out.description = formatVitalSignLabel(trim("" & arguments.qaItem.vital_sign)) />
        </cfif>
        <cfif NOT len(out.description) AND arguments.itemId GT 0>
            <cfquery name="qVitalDesc" datasource="#Application.DataSrc#">
                SELECT MAX(vl.Description) AS Description
                FROM #arguments.lookup_db#.pValue_Lookup vl
                WHERE vl.Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.itemId#">
                  AND vl.Description <> <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            </cfquery>
            <cfif qVitalDesc.recordCount GT 0 AND len(trim("" & qVitalDesc.Description))>
                <cfset out.description = trim("" & qVitalDesc.Description) />
            </cfif>
        </cfif>
        <cfif NOT len(out.description) AND arguments.itemId GT 0 AND len(trim(arguments.agency_db))>
            <cfquery name="qVitalParam" datasource="#Application.DataSrc#">
                SELECT Vital_Sign
                FROM #arguments.agency_db#.pVital_Param
                WHERE Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.itemId#">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
                LIMIT 1
            </cfquery>
            <cfif qVitalParam.recordCount GT 0>
                <cfset out.description = formatVitalSignLabel(trim("" & qVitalParam.Vital_Sign)) />
            </cfif>
        </cfif>
        <cfif NOT len(out.description)>
            <cfreturn out />
        </cfif>

        <cfif structKeyExists(arguments.qaItem, "reading") AND len(trim("" & arguments.qaItem.reading))>
            <cfset out.reading1 = trim("" & arguments.qaItem.reading) />
        <cfelseif structKeyExists(arguments.qaItem, "reading_primary") OR structKeyExists(arguments.qaItem, "reading_secondary")>
            <cfset readingPrimary = structKeyExists(arguments.qaItem, "reading_primary") ? trim("" & arguments.qaItem.reading_primary) : "" />
            <cfset readingSecondary = structKeyExists(arguments.qaItem, "reading_secondary") ? trim("" & arguments.qaItem.reading_secondary) : "" />
            <cfif len(readingPrimary) AND len(readingSecondary)>
                <cfset out.reading1 = readingPrimary & "." & readingSecondary />
            <cfelseif len(readingPrimary)>
                <cfset out.reading1 = readingPrimary />
            <cfelseif len(readingSecondary)>
                <cfset out.reading1 = readingSecondary />
            </cfif>
        </cfif>

        <cfif NOT len(out.reading1) AND structKeyExists(arguments.qaItem, "answer")>
            <cfset out.reading1 = normalizeWhisperFilledAnswerValue(arguments.qaItem.answer) />
        </cfif>

        <cfif structKeyExists(arguments.qaItem, "descriptor")>
            <cfset descriptor = trim("" & arguments.qaItem.descriptor) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "notes")>
            <cfset freeNotes = trim("" & arguments.qaItem.notes) />
        </cfif>
        <cfif len(descriptor) AND len(freeNotes)>
            <cfset out.notes = descriptor & chr(10) & freeNotes />
        <cfelseif len(descriptor)>
            <cfset out.notes = descriptor />
        <cfelse>
            <cfset out.notes = freeNotes />
        </cfif>

        <cfif structKeyExists(arguments.qaItem, "pain_location")>
            <cfset out.pain_location = trim("" & arguments.qaItem.pain_location) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "pain_duration")>
            <cfset out.pain_duration = trim("" & arguments.qaItem.pain_duration) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "pain_exacerbate")>
            <cfset out.pain_exacerbate = trim("" & arguments.qaItem.pain_exacerbate) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "pain_relief")>
            <cfset out.pain_relief = trim("" & arguments.qaItem.pain_relief) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "pain_med_effectiveness")>
            <cfset out.pain_med_effectiveness = trim("" & arguments.qaItem.pain_med_effectiveness) />
        </cfif>

        <cfif len(out.reading1) OR len(out.notes)>
            <cfset out.ok = true />
        </cfif>

        <cfreturn out />
    </cffunction>

    <!--- Resolve result node from full job payload or filled_answers body --->
    <cffunction name="getWhisperPayloadNode" access="private" returntype="struct" output="false">
        <cfargument name="data" type="struct" required="true" />
        <cfif structKeyExists(arguments.data, "result") AND isStruct(arguments.data.result)>
            <cfreturn arguments.data.result />
        </cfif>
        <cfreturn arguments.data />
    </cffunction>

    <!--- Read array payload from result node or top-level body --->
    <cffunction name="getWhisperPayloadArray" access="private" returntype="array" output="false">
        <cfargument name="data" type="struct" required="true" />
        <cfargument name="key" type="string" required="true" />
        <cfset var node = getWhisperPayloadNode(data = arguments.data) />
        <cfif structKeyExists(node, arguments.key) AND isArray(node[arguments.key])>
            <cfreturn node[arguments.key] />
        </cfif>
        <cfif structKeyExists(arguments.data, arguments.key) AND isArray(arguments.data[arguments.key])>
            <cfreturn arguments.data[arguments.key] />
        </cfif>
        <cfreturn [] />
    </cffunction>

    <!--- Extract scalar value from Whisper vital/wound answer object (answer, reading, value, etc.) --->
    <cffunction name="extractWhisperFilledAnswerValue" access="private" returntype="string" output="false">
        <cfargument name="qaItem" type="struct" required="true" />
        <cfset var val = "" />
        <cfset var readingPrimary = "" />
        <cfset var readingSecondary = "" />

        <cfif structKeyExists(arguments.qaItem, "answer")>
            <cfset val = normalizeWhisperFilledAnswerValue(arguments.qaItem.answer) />
            <cfif len(val)><cfreturn val /></cfif>
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "reading") AND len(trim("" & arguments.qaItem.reading))>
            <cfreturn trim("" & arguments.qaItem.reading) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "value") AND len(trim("" & arguments.qaItem.value))>
            <cfreturn trim("" & arguments.qaItem.value) />
        </cfif>
        <cfif structKeyExists(arguments.qaItem, "reading_primary") OR structKeyExists(arguments.qaItem, "reading_secondary")>
            <cfset readingPrimary = structKeyExists(arguments.qaItem, "reading_primary") ? trim("" & arguments.qaItem.reading_primary) : "" />
            <cfset readingSecondary = structKeyExists(arguments.qaItem, "reading_secondary") ? trim("" & arguments.qaItem.reading_secondary) : "" />
            <cfif len(readingPrimary) AND len(readingSecondary)>
                <cfreturn readingPrimary & "." & readingSecondary />
            <cfelseif len(readingPrimary)>
                <cfreturn readingPrimary />
            <cfelseif len(readingSecondary)>
                <cfreturn readingSecondary />
            </cfif>
        </cfif>
        <cfreturn "" />
    </cffunction>

    <!--- Map wound column aliases to pWoundsVitals column names --->
    <cffunction name="normalizeWhisperWoundColumnName" access="private" returntype="string" output="false">
        <cfargument name="col" type="string" required="true" />
        <cfset var key = lCase(trim(arguments.col)) />
        <cfset var map = {
            "length" = "Length",
            "width" = "Width",
            "depth" = "Depth",
            "location" = "Location",
            "description" = "Description",
            "other_locations" = "other_locations",
            "healingstage" = "healingstage",
            "woundtype" = "woundtype",
            "wnd_tunneling" = "wnd_tunneling",
            "wnd_underm" = "wnd_underm",
            "wnd_gran" = "wnd_gran",
            "wnd_epith" = "wnd_epith",
            "wnd_eschar" = "wnd_eschar",
            "wnd_slough" = "wnd_slough",
            "wnd_fib" = "wnd_fib",
            "wnd_margin" = "wnd_margin",
            "wnd_exudate" = "wnd_exudate",
            "wnd_odor" = "wnd_odor",
            "wnd_pain" = "wnd_pain",
            "pressure_stage" = "woundtype"
        } />
        <cfif structKeyExists(map, key)>
            <cfreturn map[key] />
        </cfif>
        <cfreturn trim(arguments.col) />
    </cffunction>

    <!--- Merge filled_vitals/filled_wounds and vitals[]/wounds[] arrays into filledAnswers for import --->
    <cffunction name="mergeWhisperVitalsWoundsIntoFilledAnswers" access="private" returntype="struct" output="false">
        <cfargument name="data" type="struct" required="true" />
        <cfargument name="filledAnswers" type="struct" required="true" />
        <cfset var out = {
            filledAnswers = duplicate(arguments.filledAnswers),
            vitalsArray = [],
            woundsArray = []
        } />
        <cfset var node = getWhisperPayloadNode(data = arguments.data) />
        <cfset var mergeStruct = {} />
        <cfset var mapQid = "" />
        <cfset var vitalItem = {} />
        <cfset var woundItem = {} />
        <cfset var itemId = 0 />
        <cfset var vitalKey = "" />
        <cfset var idx = 0 />

        <cfif structKeyExists(node, "filled_vitals") AND isStruct(node.filled_vitals)>
            <cfset mergeStruct = node.filled_vitals />
        <cfelseif structKeyExists(arguments.data, "filled_vitals") AND isStruct(arguments.data.filled_vitals)>
            <cfset mergeStruct = arguments.data.filled_vitals />
        </cfif>
        <cfif isStruct(mergeStruct)>
            <cfloop collection="#mergeStruct#" item="mapQid">
                <cfif NOT structKeyExists(out.filledAnswers, mapQid)>
                    <cfset out.filledAnswers[mapQid] = mergeStruct[mapQid] />
                </cfif>
            </cfloop>
        </cfif>

        <cfset mergeStruct = {} />
        <cfif structKeyExists(node, "filled_wounds") AND isStruct(node.filled_wounds)>
            <cfset mergeStruct = node.filled_wounds />
        <cfelseif structKeyExists(arguments.data, "filled_wounds") AND isStruct(arguments.data.filled_wounds)>
            <cfset mergeStruct = arguments.data.filled_wounds />
        </cfif>
        <cfif isStruct(mergeStruct)>
            <cfloop collection="#mergeStruct#" item="mapQid">
                <cfif NOT structKeyExists(out.filledAnswers, mapQid)>
                    <cfset out.filledAnswers[mapQid] = mergeStruct[mapQid] />
                </cfif>
            </cfloop>
        </cfif>

        <cfset out.vitalsArray = getWhisperPayloadArray(data = arguments.data, key = "vitals") />
        <cfif arrayLen(out.vitalsArray) EQ 0>
            <cfset out.vitalsArray = getWhisperPayloadArray(data = arguments.data, key = "Vitals") />
        </cfif>
        <cfloop from="1" to="#arrayLen(out.vitalsArray)#" index="idx">
            <cfset vitalItem = out.vitalsArray[idx] />
            <cfif NOT isStruct(vitalItem)>
                <cfcontinue />
            </cfif>
            <cfset itemId = structKeyExists(vitalItem, "item_id") ? val(vitalItem.item_id) : (structKeyExists(vitalItem, "Item_ID") ? val(vitalItem.Item_ID) : 0) />
            <cfif itemId LTE 0 AND structKeyExists(vitalItem, "itemId")>
                <cfset itemId = val(vitalItem.itemId) />
            </cfif>
            <cfif itemId GT 0>
                <cfset vitalKey = "vital_" & itemId />
                <cfif NOT structKeyExists(out.filledAnswers, vitalKey)>
                    <cfset out.filledAnswers[vitalKey] = vitalItem />
                </cfif>
            </cfif>
        </cfloop>

        <cfset out.woundsArray = getWhisperPayloadArray(data = arguments.data, key = "wounds") />
        <cfif arrayLen(out.woundsArray) EQ 0>
            <cfset out.woundsArray = getWhisperPayloadArray(data = arguments.data, key = "Wounds") />
        </cfif>

        <cfreturn out />
    </cffunction>

    <!--- Combine two vitals/wounds import result structs --->
    <cffunction name="mergeWhisperImportResults" access="private" returntype="struct" output="false">
        <cfargument name="base" type="struct" required="true" />
        <cfargument name="extra" type="struct" required="true" />
        <cfset var out = duplicate(arguments.base) />
        <cfif structKeyExists(arguments.extra, "updated")>
            <cfset out.updated = val(out.updated) + val(arguments.extra.updated) />
        </cfif>
        <cfif structKeyExists(arguments.extra, "inserted")>
            <cfset out.inserted = val(out.inserted) + val(arguments.extra.inserted) />
        </cfif>
        <cfif structKeyExists(arguments.extra, "skipped")>
            <cfset out.skipped = val(out.skipped) + val(arguments.extra.skipped) />
        </cfif>
        <cfif structKeyExists(arguments.extra, "applied") AND isArray(arguments.extra.applied)>
            <cfif NOT structKeyExists(out, "applied") OR NOT isArray(out.applied)>
                <cfset out.applied = [] />
            </cfif>
            <cfloop array="#arguments.extra.applied#" index="appliedItem">
                <cfset arrayAppend(out.applied, appliedItem) />
            </cfloop>
        </cfif>
        <cfif structKeyExists(arguments.extra, "skippedItems") AND isArray(arguments.extra.skippedItems)>
            <cfif NOT structKeyExists(out, "skippedItems") OR NOT isArray(out.skippedItems)>
                <cfset out.skippedItems = [] />
            </cfif>
            <cfloop array="#arguments.extra.skippedItems#" index="skippedItem">
                <cfset arrayAppend(out.skippedItems, skippedItem) />
            </cfloop>
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Normalize mobile-style wound array item into pWoundsVitals column map --->
    <cffunction name="normalizeWhisperWoundArrayItem" access="private" returntype="struct" output="false">
        <cfargument name="item" type="struct" required="true" />
        <cfset var out = { ok = false, rowId = 0, itemId = 0, cols = {} } />
        <cfset var fieldPairs = "description,Description|location,Location|other_locations,other_locations|length,Length|width,Width|depth,Depth|healingstage,healingstage|healing_stage,healingstage|wnd_tunneling,wnd_tunneling|wnd_underm,wnd_underm|wnd_gran,wnd_gran|wnd_epith,wnd_epith|wnd_eschar,wnd_eschar|wnd_slough,wnd_slough|wnd_fib,wnd_fib|wnd_margin,wnd_margin|wnd_exudate,wnd_exudate|wnd_odor,wnd_odor|wnd_pain,wnd_pain" />
        <cfset var pair = "" />
        <cfset var keys = "" />
        <cfset var colName = "" />
        <cfset var val = "" />
        <cfset var woundTypeBase = "" />
        <cfset var pressureStage = "" />
        <cfset var measurements = {} />

        <cfset out.rowId = structKeyExists(arguments.item, "id") ? val(arguments.item.id) : (structKeyExists(arguments.item, "ID") ? val(arguments.item.ID) : 0) />
        <cfset out.itemId = structKeyExists(arguments.item, "item_id") ? val(arguments.item.item_id) : (structKeyExists(arguments.item, "Item_ID") ? val(arguments.item.Item_ID) : 0) />

        <cfloop list="#fieldPairs#" index="pair" delimiters="|">
            <cfset keys = listGetAt(pair, 1) />
            <cfset colName = listGetAt(pair, 2) />
            <cfset val = "" />
            <cfloop list="#keys#" index="keyName">
                <cfif structKeyExists(arguments.item, keyName) AND len(trim("" & arguments.item[keyName]))>
                    <cfset val = trim("" & arguments.item[keyName]) />
                    <cfbreak />
                </cfif>
            </cfloop>
            <cfif len(val)>
                <cfset out.cols[colName] = val />
            </cfif>
        </cfloop>

        <cfif structKeyExists(arguments.item, "measurements") AND isStruct(arguments.item.measurements)>
            <cfset measurements = arguments.item.measurements />
            <cfif structKeyExists(measurements, "length") AND len(trim("" & measurements.length)) AND NOT structKeyExists(out.cols, "Length")>
                <cfset out.cols.Length = trim("" & measurements.length) />
            </cfif>
            <cfif structKeyExists(measurements, "width") AND len(trim("" & measurements.width)) AND NOT structKeyExists(out.cols, "Width")>
                <cfset out.cols.Width = trim("" & measurements.width) />
            </cfif>
            <cfif structKeyExists(measurements, "depth") AND len(trim("" & measurements.depth)) AND NOT structKeyExists(out.cols, "Depth")>
                <cfset out.cols.Depth = trim("" & measurements.depth) />
            </cfif>
        </cfif>

        <cfset woundTypeBase = structKeyExists(arguments.item, "wound_type") ? trim("" & arguments.item.wound_type) : (structKeyExists(arguments.item, "woundtype") ? trim("" & arguments.item.woundtype) : "") />
        <cfset pressureStage = structKeyExists(arguments.item, "pressure_stage") ? trim("" & arguments.item.pressure_stage) : (structKeyExists(arguments.item, "stage") ? trim("" & arguments.item.stage) : "") />
        <cfif len(pressureStage)>
            <cfset out.cols.woundtype = pressureStage />
        <cfelseif len(woundTypeBase)>
            <cfset out.cols.woundtype = woundTypeBase />
        </cfif>

        <cfif structCount(out.cols) GT 0>
            <cfset out.ok = true />
        </cfif>
        <cfreturn out />
    </cffunction>

    <!--- Upsert one wound row from mobile-style wounds[] payload item --->
    <cffunction name="upsertWhisperWoundRowFromArrayItem" access="private" returntype="struct" output="false">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="assmt_id" type="numeric" required="true" />
        <cfargument name="created_by" type="numeric" required="true" />
        <cfargument name="woundItem" type="struct" required="true" />
        <cfargument name="queryTrace" type="array" required="true" />

        <cfset var result = { action = "skipped", id = 0, item_id = 0, reason = "" } />
        <cfset var normalized = normalizeWhisperWoundArrayItem(item = arguments.woundItem) />
        <cfset var existingWound = "" />
        <cfset var getpwound = "" />
        <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 = {} />

        <cfif NOT normalized.ok>
            <cfset result.reason = "empty_wound_payload" />
            <cfreturn result />
        </cfif>
        <cfset cols = normalized.cols />
        <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 />
        <cfif structKeyExists(cols, "woundtype") AND len(trim(cols.woundtype))>
            <cfset woundpathid = resolveWoundPathwayIdFromType(cols.woundtype) />
        </cfif>

        <cfif normalized.rowId GT 0>
            <cfquery name="existingWound" datasource="#Application.DataSrc#">
                SELECT ID, Item_ID
                FROM #arguments.agency_db#.pWoundsVitals
                WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#normalized.rowId#">
                  AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmt_id#">
                  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.agency_db#.pWoundsVitals
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmt_id#">
                  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>
        <cfelse>
            <cfquery name="existingWound" datasource="#Application.DataSrc#">
                SELECT ID, Item_ID
                FROM #arguments.agency_db#.pWoundsVitals
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmt_id#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                  AND VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                ORDER BY Pathway_ID DESC
                LIMIT 1
            </cfquery>
        </cfif>

        <cfif isQuery(existingWound) AND existingWound.recordCount GT 0>
            <cfquery datasource="#Application.DataSrc#">
                UPDATE #arguments.agency_db#.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_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.created_by#" cfsqltype="cf_sql_integer">
                WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#existingWound.ID#">
            </cfquery>
            <cfset arrayAppend(arguments.queryTrace, "update_pWoundsVitals wound_array ID=" & existingWound.ID) />
            <cfset result.action = "updated" />
            <cfset result.id = existingWound.ID />
            <cfset result.item_id = existingWound.Item_ID />
        <cfelse>
            <cfquery name="getpwound" datasource="#Application.DataSrc#">
                SELECT Item_ID
                FROM #arguments.agency_db#.pWoundsVitals
                WHERE VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                  AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmt_id#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                ORDER BY Pathway_ID DESC
            </cfquery>
            <cfif getpwound.recordCount GT 0>
                <cfset wounditemid = val(getpwound.Item_ID) + 1 />
            </cfif>
            <cfquery datasource="#Application.DataSrc#">
                INSERT INTO #arguments.agency_db#.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_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.assmt_id#" 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.created_by#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                    <cfqueryparam value="#arguments.created_by#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#structKeyExists(cols, 'healingstage') ? cols.healingstage : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#structKeyExists(cols, 'woundtype') ? cols.woundtype : ''#" 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 arrayAppend(arguments.queryTrace, "insert_pWoundsVitals wound_array Assmt_ID=" & arguments.assmt_id) />
            <cfset result.action = "inserted" />
            <cfset result.item_id = wounditemid />
        </cfif>
        <cfreturn result />
    </cffunction>

    <!--- Apply wounds[] array payloads (mobile-style) to pWoundsVitals --->
    <cffunction name="applyWoundsArrayToPWoundsVitals" access="private" returntype="struct" output="false">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="assmt_id" type="numeric" required="true" />
        <cfargument name="woundsArray" type="array" required="true" />
        <cfargument name="created_by" type="numeric" required="true" />
        <cfargument name="queryTrace" type="array" required="true" />

        <cfset var result = { updated = 0, inserted = 0, skipped = 0, applied = [], skippedItems = [] } />
        <cfset var idx = 0 />
        <cfset var woundItem = {} />
        <cfset var rowResult = {} />

        <cfif arguments.assmt_id LTE 0 OR NOT isArray(arguments.woundsArray) OR arrayLen(arguments.woundsArray) EQ 0>
            <cfreturn result />
        </cfif>

        <cfloop from="1" to="#arrayLen(arguments.woundsArray)#" index="idx">
            <cfset woundItem = arguments.woundsArray[idx] />
            <cfif NOT isStruct(woundItem)>
                <cfset arrayAppend(result.skippedItems, { index = idx, reason = "invalid_wound_object" }) />
                <cfset result.skipped = result.skipped + 1 />
                <cfcontinue />
            </cfif>
            <cfset rowResult = upsertWhisperWoundRowFromArrayItem(
                agency_db = arguments.agency_db,
                assmt_id = arguments.assmt_id,
                created_by = arguments.created_by,
                woundItem = woundItem,
                queryTrace = arguments.queryTrace
            ) />
            <cfif rowResult.action EQ "updated">
                <cfset result.updated = result.updated + 1 />
                <cfset arrayAppend(result.applied, { index = idx, action = "updated", id = rowResult.id, item_id = rowResult.item_id }) />
            <cfelseif rowResult.action EQ "inserted">
                <cfset result.inserted = result.inserted + 1 />
                <cfset arrayAppend(result.applied, { index = idx, action = "inserted", item_id = rowResult.item_id }) />
            <cfelse>
                <cfset arrayAppend(result.skippedItems, { index = idx, reason = rowResult.reason }) />
                <cfset result.skipped = result.skipped + 1 />
            </cfif>
        </cfloop>
        <cfreturn result />
    </cffunction>

    <!--- Apply all Whisper vitals/wounds payload shapes to pWoundsVitals (web + mobile import) --->
    <cffunction name="applyWhisperVitalsWoundsToPWoundsVitals" access="private" returntype="struct" output="false">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="assmt_id" type="numeric" required="true" />
        <cfargument name="filledAnswers" type="struct" required="true" />
        <cfargument name="woundsArray" type="array" required="false" default="#[]#" />
        <cfargument name="created_by" type="numeric" required="true" />
        <cfargument name="skippedAnswers" type="array" required="true" />
        <cfargument name="queryTrace" type="array" required="true" />
        <cfargument name="lookup_db" type="string" required="false" default="hhapowerpath" />

        <cfset var vitalResult = {} />
        <cfset var woundResult = {} />
        <cfset var woundArrayResult = {} />

        <cfset vitalResult = applyFilledVitalsToPWoundsVitals(
            agency_db = arguments.agency_db,
            assmt_id = arguments.assmt_id,
            filledAnswers = arguments.filledAnswers,
            created_by = arguments.created_by,
            skippedAnswers = arguments.skippedAnswers,
            queryTrace = arguments.queryTrace,
            lookup_db = arguments.lookup_db
        ) />
        <cfset woundResult = applyFilledWoundsToPWoundsVitals(
            agency_db = arguments.agency_db,
            assmt_id = arguments.assmt_id,
            filledAnswers = arguments.filledAnswers,
            created_by = arguments.created_by,
            skippedAnswers = arguments.skippedAnswers,
            queryTrace = arguments.queryTrace
        ) />
        <cfif isArray(arguments.woundsArray) AND arrayLen(arguments.woundsArray) GT 0>
            <cfset woundArrayResult = applyWoundsArrayToPWoundsVitals(
                agency_db = arguments.agency_db,
                assmt_id = arguments.assmt_id,
                woundsArray = arguments.woundsArray,
                created_by = arguments.created_by,
                queryTrace = arguments.queryTrace
            ) />
            <cfset woundResult = mergeWhisperImportResults(base = woundResult, extra = woundArrayResult) />
        </cfif>

        <cfreturn { vitals = vitalResult, wounds = woundResult } />
    </cffunction>

    <!--- Persist Whisper vital answers into pWoundsVitals for Assmt_ID --->
    <cffunction name="applyFilledVitalsToPWoundsVitals" access="private" returntype="struct" output="false">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="assmt_id" type="numeric" required="true" />
        <cfargument name="filledAnswers" type="struct" required="true" />
        <cfargument name="created_by" type="numeric" required="true" />
        <cfargument name="skippedAnswers" type="array" required="true" />
        <cfargument name="queryTrace" type="array" required="true" />
        <cfargument name="lookup_db" type="string" required="false" default="hhapowerpath" />

        <cfset var result = { updated = 0, inserted = 0, skipped = 0, applied = [], skippedItems = [] } />
        <cfset var mapQid = "" />
        <cfset var qaItem = {} />
        <cfset var itemId = 0 />
        <cfset var normalized = {} />
        <cfset var existingVital = "" />
        <cfset var paramRow = "" />
        <cfset var paramLo = "" />
        <cfset var paramHi = "" />
        <cfset var appliedEntry = {} />

        <cfif arguments.assmt_id LTE 0>
            <cfreturn result />
        </cfif>

        <cfloop collection="#arguments.filledAnswers#" item="mapQid">
            <cfif NOT reFindNoCase("^vital_", mapQid)>
                <cfcontinue />
            </cfif>

            <cfset qaItem = arguments.filledAnswers[mapQid] />
            <cfif NOT isStruct(qaItem)>
                <cfset arrayAppend(arguments.skippedAnswers, mapQid & ":invalid_vital_payload") />
                <cfset arrayAppend(result.skippedItems, { map_key = mapQid, reason = "invalid_vital_payload" }) />
                <cfset result.skipped = result.skipped + 1 />
                <cfcontinue />
            </cfif>

            <cfset itemId = val(reReplaceNoCase(mapQid, "^vital_", "", "all")) />
            <cfif itemId LTE 0 AND structKeyExists(qaItem, "item_id")>
                <cfset itemId = val(qaItem.item_id) />
            </cfif>
            <cfif itemId LTE 0>
                <cfset arrayAppend(arguments.skippedAnswers, mapQid & ":missing_item_id") />
                <cfset arrayAppend(result.skippedItems, { map_key = mapQid, reason = "missing_item_id" }) />
                <cfset result.skipped = result.skipped + 1 />
                <cfcontinue />
            </cfif>

            <cfset normalized = normalizeVitalFilledAnswer(
                qaItem = qaItem,
                itemId = itemId,
                agency_db = arguments.agency_db,
                lookup_db = arguments.lookup_db
            ) />
            <cfif NOT normalized.ok>
                <cfset arrayAppend(arguments.skippedAnswers, mapQid & ":empty_vital_reading_or_notes") />
                <cfset arrayAppend(result.skippedItems, { map_key = mapQid, item_id = itemId, reason = "empty_vital_reading_or_notes" }) />
                <cfset result.skipped = result.skipped + 1 />
                <cfcontinue />
            </cfif>

            <cfquery name="paramRow" datasource="#Application.DataSrc#">
                SELECT Value_Low, Value_High
                FROM #arguments.agency_db#.pVital_Param
                WHERE Item_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#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) : "" />

            <cfquery name="existingVital" datasource="#Application.DataSrc#">
                SELECT ID
                FROM #arguments.agency_db#.pWoundsVitals
                WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmt_id#">
                  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="#itemId#">
                LIMIT 1
            </cfquery>

            <cfif existingVital.recordCount GT 0>
                <cfquery datasource="#Application.DataSrc#">
                    UPDATE #arguments.agency_db#.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.created_by#" cfsqltype="cf_sql_integer">
                    WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#existingVital.ID#">
                </cfquery>
                <cfset arrayAppend(arguments.queryTrace, "update_pWoundsVitals vital_" & itemId & " ID=" & existingVital.ID) />
                <cfset appliedEntry = {
                    action = "updated",
                    map_key = mapQid,
                    item_id = itemId,
                    pWoundsVitals_id = existingVital.ID,
                    reading1 = normalized.reading1,
                    description = normalized.description,
                    notes = normalized.notes
                } />
                <cfset arrayAppend(result.applied, appliedEntry) />
                <cfset result.updated = result.updated + 1 />
            <cfelse>
                <cfquery datasource="#Application.DataSrc#">
                    INSERT INTO #arguments.agency_db#.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.assmt_id#" cfsqltype="cf_sql_integer">,
                        <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                        <cfqueryparam value="#itemId#" cfsqltype="cf_sql_integer">,
                        <cfqueryparam value="#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.created_by#" cfsqltype="cf_sql_integer">,
                        <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                        <cfqueryparam value="#arguments.created_by#" cfsqltype="cf_sql_integer">
                    )
                </cfquery>
                <cfset arrayAppend(arguments.queryTrace, "insert_pWoundsVitals vital_" & itemId & " Assmt_ID=" & arguments.assmt_id) />
                <cfset appliedEntry = {
                    action = "inserted",
                    map_key = mapQid,
                    item_id = itemId,
                    reading1 = normalized.reading1,
                    description = normalized.description,
                    notes = normalized.notes
                } />
                <cfset arrayAppend(result.applied, appliedEntry) />
                <cfset result.inserted = result.inserted + 1 />
            </cfif>
        </cfloop>

        <cfreturn result />
    </cffunction>

    <!--- Map woundtype text to Pathway_ID (matches pwoundvital.cfc addEditWounds) --->
    <cffunction name="resolveWoundPathwayIdFromType" 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>

    <!--- Normalize pWoundsVitals column name from wound_* filled answer --->
    <cffunction name="resolveWoundColumnFromFilledAnswer" access="private" returntype="string" output="false">
        <cfargument name="mapQid" type="string" required="true" />
        <cfargument name="qaItem" type="struct" required="true" />
        <cfset var col = "" />
        <cfif structKeyExists(arguments.qaItem, "answer_label") AND len(trim("" & arguments.qaItem.answer_label))>
            <cfset col = trim("" & arguments.qaItem.answer_label) />
        </cfif>
        <cfif len(col)><cfreturn normalizeWhisperWoundColumnName(col) /></cfif>
        <cfreturn normalizeWhisperWoundColumnName(reReplaceNoCase(trim(arguments.mapQid), "^wound_", "", "all")) />
    </cffunction>

    <!--- Aggregate wound_* filled answers and upsert one pWoundsVitals wound row --->
    <cffunction name="applyFilledWoundsToPWoundsVitals" access="private" returntype="struct" output="false">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="assmt_id" type="numeric" required="true" />
        <cfargument name="filledAnswers" type="struct" required="true" />
        <cfargument name="created_by" type="numeric" required="true" />
        <cfargument name="skippedAnswers" type="array" required="true" />
        <cfargument name="queryTrace" type="array" required="true" />

        <cfset var result = { updated = 0, inserted = 0, skipped = 0, applied = [], skippedItems = [] } />
        <cfset var mapQid = "" />
        <cfset var qaItem = {} />
        <cfset var colName = "" />
        <cfset var colValue = "" />
        <cfset var woundCols = {} />
        <cfset var hasData = false />
        <cfset var existingWound = "" />
        <cfset var getpwound = "" />
        <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 appliedEntry = {} />

        <cfif arguments.assmt_id LTE 0>
            <cfreturn result />
        </cfif>

        <cfloop collection="#arguments.filledAnswers#" item="mapQid">
            <cfif NOT isWoundFilledAnswerKey(mapQid)>
                <cfcontinue />
            </cfif>
            <cfset qaItem = arguments.filledAnswers[mapQid] />
            <cfif NOT isStruct(qaItem)>
                <cfset arrayAppend(arguments.skippedAnswers, mapQid & ":invalid_wound_payload") />
                <cfset arrayAppend(result.skippedItems, { map_key = mapQid, reason = "invalid_wound_payload" }) />
                <cfcontinue />
            </cfif>
            <cfset colValue = extractWhisperFilledAnswerValue(qaItem = qaItem) />
            <cfif NOT len(colValue)>
                <cfcontinue />
            </cfif>
            <cfset colName = resolveWoundColumnFromFilledAnswer(mapQid = mapQid, qaItem = qaItem) />
            <cfif NOT len(colName)>
                <cfcontinue />
            </cfif>
            <cfset woundCols[colName] = colValue />
            <cfset hasData = true />
        </cfloop>

        <cfif NOT hasData>
            <cfreturn result />
        </cfif>

        <cfset lengthVal = structKeyExists(woundCols, "Length") ? val(woundCols.Length) : 0 />
        <cfset widthVal = structKeyExists(woundCols, "Width") ? val(woundCols.Width) : 0 />
        <cfset depthVal = structKeyExists(woundCols, "Depth") ? val(woundCols.Depth) : 0 />
        <cfset readings = lengthVal + widthVal + depthVal />

        <cfif structKeyExists(woundCols, "woundtype") AND len(trim(woundCols.woundtype))>
            <cfset woundpathid = resolveWoundPathwayIdFromType(woundCols.woundtype) />
        </cfif>

        <cfquery name="existingWound" datasource="#Application.DataSrc#">
            SELECT ID, Item_ID, Pathway_ID
            FROM #arguments.agency_db#.pWoundsVitals
            WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmt_id#">
              AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
              AND VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
            ORDER BY Pathway_ID DESC
            LIMIT 1
        </cfquery>

        <cfif existingWound.recordCount GT 0>
            <cfquery datasource="#Application.DataSrc#">
                UPDATE #arguments.agency_db#.pWoundsVitals
                SET
                    <cfif structKeyExists(woundCols, "Description")>Description = <cfqueryparam value="#woundCols.Description#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "Location")>Location = <cfqueryparam value="#woundCols.Location#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "other_locations")>other_locations = <cfqueryparam value="#woundCols.other_locations#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "Length")>Length = <cfqueryparam value="#woundCols.Length#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "Width")>Width = <cfqueryparam value="#woundCols.Width#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "Depth")>Depth = <cfqueryparam value="#woundCols.Depth#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "healingstage")>healingstage = <cfqueryparam value="#woundCols.healingstage#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "woundtype")>woundtype = <cfqueryparam value="#woundCols.woundtype#" cfsqltype="cf_sql_varchar">, Pathway_ID = <cfqueryparam value="#woundpathid#" cfsqltype="cf_sql_integer">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_tunneling")>wnd_tunneling = <cfqueryparam value="#woundCols.wnd_tunneling#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_underm")>wnd_underm = <cfqueryparam value="#woundCols.wnd_underm#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_gran")>wnd_gran = <cfqueryparam value="#woundCols.wnd_gran#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_epith")>wnd_epith = <cfqueryparam value="#woundCols.wnd_epith#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_eschar")>wnd_eschar = <cfqueryparam value="#woundCols.wnd_eschar#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_slough")>wnd_slough = <cfqueryparam value="#woundCols.wnd_slough#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_fib")>wnd_fib = <cfqueryparam value="#woundCols.wnd_fib#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_margin")>wnd_margin = <cfqueryparam value="#woundCols.wnd_margin#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_exudate")>wnd_exudate = <cfqueryparam value="#woundCols.wnd_exudate#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_odor")>wnd_odor = <cfqueryparam value="#woundCols.wnd_odor#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "wnd_pain")>wnd_pain = <cfqueryparam value="#woundCols.wnd_pain#" cfsqltype="cf_sql_varchar">,</cfif>
                    <cfif structKeyExists(woundCols, "Length") OR structKeyExists(woundCols, "Width") OR structKeyExists(woundCols, "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.created_by#" cfsqltype="cf_sql_integer">
                WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#existingWound.ID#">
            </cfquery>
            <cfset arrayAppend(arguments.queryTrace, "update_pWoundsVitals wound ID=" & existingWound.ID) />
            <cfset appliedEntry = {
                action = "updated",
                pWoundsVitals_id = existingWound.ID,
                columns = duplicate(woundCols)
            } />
            <cfset arrayAppend(result.applied, appliedEntry) />
            <cfset result.updated = 1 />
        <cfelse>
            <cfquery name="getpwound" datasource="#Application.DataSrc#">
                SELECT Item_ID
                FROM #arguments.agency_db#.pWoundsVitals
                WHERE VitalOrWound = <cfqueryparam value="wound" cfsqltype="cf_sql_varchar">
                  AND Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.assmt_id#">
                  AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                ORDER BY Pathway_ID DESC
            </cfquery>
            <cfif getpwound.recordCount GT 0>
                <cfset wounditemid = val(getpwound.Item_ID) + 1 />
            </cfif>

            <cfquery datasource="#Application.DataSrc#">
                INSERT INTO #arguments.agency_db#.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_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.assmt_id#" 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(woundCols, 'Description') ? woundCols.Description : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'Location') ? woundCols.Location : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'other_locations') ? woundCols.other_locations : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'Length') ? woundCols.Length : '0'#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'Width') ? woundCols.Width : '0'#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'Depth') ? woundCols.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.created_by#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp">,
                    <cfqueryparam value="#arguments.created_by#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'healingstage') ? woundCols.healingstage : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="0" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'woundtype') ? woundCols.woundtype : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_margin') ? woundCols.wnd_margin : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_exudate') ? woundCols.wnd_exudate : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_odor') ? woundCols.wnd_odor : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_pain') ? woundCols.wnd_pain : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_tunneling') ? woundCols.wnd_tunneling : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_underm') ? woundCols.wnd_underm : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_gran') ? woundCols.wnd_gran : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_epith') ? woundCols.wnd_epith : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_eschar') ? woundCols.wnd_eschar : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_slough') ? woundCols.wnd_slough : ''#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#structKeyExists(woundCols, 'wnd_fib') ? woundCols.wnd_fib : ''#" cfsqltype="cf_sql_varchar">
                )
            </cfquery>
            <cfset arrayAppend(arguments.queryTrace, "insert_pWoundsVitals wound Assmt_ID=" & arguments.assmt_id) />
            <cfset appliedEntry = {
                action = "inserted",
                columns = duplicate(woundCols)
            } />
            <cfset arrayAppend(result.applied, appliedEntry) />
            <cfset result.inserted = 1 />
        </cfif>

        <cfreturn result />
    </cffunction>

    <cffunction name="findRelatedProgressSchedule" access="private" returntype="struct" output="false"
        hint="Load pSchedules row by Schedule_ID">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="schedule_id" type="string" required="true" />

        <cfset var out = {
            "schedule_id" = 0,
            "visit_type" = "",
            "visit_date" = "",
            "patient_id" = 0,
            "assmt_id" = 0,
            "skill" = ""
        } />
        <cfset var qSched = "" />

        <cfif NOT len(trim(arguments.agency_db)) OR val(arguments.schedule_id) LTE 0>
            <cfreturn out />
        </cfif>

        <cfquery name="qSched" datasource="#Application.DataSrc#">
            SELECT Schedule_ID,
                   Patient_ID,
                   Assmt_ID,
                   Visit_Type,
                   DATE_FORMAT(Visit_Date, '%Y-%m-%d') AS Visit_Date,
                   Skill
            FROM #arguments.agency_db#.pSchedules
            WHERE Schedule_ID = <cfqueryparam value="#val(arguments.schedule_id)#" cfsqltype="cf_sql_integer">
              AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
            LIMIT 1
        </cfquery>
        <cfif qSched.recordcount GT 0>
            <cfset out.schedule_id = toJsonInt(qSched.Schedule_ID) />
            <cfset out.visit_type = trim(qSched.Visit_Type) />
            <cfset out.visit_date = trim(qSched.Visit_Date) />
            <cfset out.patient_id = toJsonInt(qSched.Patient_ID) />
            <cfset out.assmt_id = toJsonInt(qSched.Assmt_ID) />
            <cfset out.skill = trim(qSched.Skill) />
        </cfif>
        <cfreturn out />
    </cffunction>

    <cffunction name="getProgressNoteSectionMeta" access="private" returntype="struct" output="false">
        <cfargument name="progressType" type="string" required="true" />
        <cfargument name="description" type="string" required="false" default="" />
        <cfset var pType = lCase(trim(arguments.progressType)) />
        <cfset var desc = trim(arguments.description) />
        <cfset var meta = {
            "section" = pType,
            "title" = "",
            "order" = 99,
            "main_field" = "YesOrNo",
            "questions_type" = "radio",
            "include_notes" = true
        } />

        <cfif pType EQ "patient_complaint">
            <cfset meta.section = "patient_complaint" />
            <cfset meta.title = "Patient Complaints" />
            <cfset meta.order = 1 />
            <cfset meta.main_field = "Description" />
            <cfset meta.questions_type = "textarea" />
            <cfset meta.include_notes = false />
        <cfelseif pType EQ "patient_goal">
            <cfset meta.title = "Patient Goal on Admission" />
            <cfset meta.order = 2 />
        <cfelseif pType EQ "vital">
            <cfset meta.title = "Vitals" />
            <cfset meta.order = 3 />
            <cfset meta.main_field = "Value_1" />
            <cfset meta.questions_type = "text" />
        <cfelseif pType EQ "problem" OR pType EQ "symptom">
            <cfif left(desc, 2) EQ "GG">
                <cfset meta.section = "gg_adls" />
                <cfset meta.title = "GG ADLs" />
                <cfset meta.order = 5 />
            <cfelse>
                <cfset meta.section = "symptoms" />
                <cfset meta.title = "SYMPTOMS" />
                <cfset meta.order = 4 />
            </cfif>
            <cfset meta.main_field = "severity" />
        <cfelseif pType EQ "procedure">
            <cfset meta.title = "PROCEDURES" />
            <cfset meta.order = 6 />
        <cfelseif pType EQ "standing_order">
            <cfset meta.title = "STANDING ORDERS" />
            <cfset meta.order = 7 />
        <cfelseif pType EQ "goal">
            <cfset meta.title = "GOALS" />
            <cfset meta.order = 8 />
        <cfelseif pType EQ "discharge_plan">
            <cfset meta.title = "Discharge Plan" />
            <cfset meta.order = 9 />
        <cfelseif pType EQ "teach">
            <cfset meta.title = "Teaching" />
            <cfset meta.order = 10 />
        <cfelse>
            <cfset meta.title = len(pType) ? uCase(left(pType, 1)) & mid(pType, 2, len(pType)) : "Progress Notes" />
        </cfif>
        <cfreturn meta />
    </cffunction>

    <cffunction name="getProgressNoteAnswerOptions" access="private" returntype="array" output="false">
        <cfargument name="progressType" type="string" required="true" />
        <cfargument name="description" type="string" required="false" default="" />
        <cfargument name="mainField" type="string" required="false" default="YesOrNo" />
        <cfargument name="skill" type="string" required="false" default="" />
        <cfset var pType = lCase(trim(arguments.progressType)) />
        <cfset var desc = trim(arguments.description) />
        <cfset var skillCode = uCase(trim(arguments.skill)) />
        <cfset var isGg = (left(desc, 2) EQ "GG") />
        <cfset var isSnOrHha = (skillCode EQ "SN" OR skillCode EQ "HHA") />

        <cfif arguments.mainField EQ "Description" OR arguments.mainField EQ "Value_1" OR arguments.mainField EQ "Notes">
            <cfreturn [] />
        </cfif>
        <cfif pType EQ "procedure" OR pType EQ "standing_order">
            <cfreturn buildAnswerOptionsFromArray(["Yes", "No"]) />
        </cfif>
        <cfif (pType EQ "problem" OR pType EQ "symptom") AND isGg AND NOT isSnOrHha>
            <cfreturn buildAnswerOptionsFromArray(["1", "2", "3", "4", "5", "6", "10"]) />
        </cfif>
        <cfreturn buildAnswerOptionsFromArray(["0", "1", "2", "3", "4"]) />
    </cffunction>

    <cffunction name="getVitalLookupOptions" access="private" returntype="struct" output="false">
        <cfargument name="lookup_db" type="string" required="true" />
        <cfargument name="description" type="string" required="true" />
        <cfset var out = { "value1" = [], "value2" = [], "measure" = "" } />
        <cfset var qLookup = "" />
        <cfset var seen1 = {} />
        <cfset var seen2 = {} />
        <cfset var v1 = "" />
        <cfset var v2 = "" />

        <cfif NOT len(trim(arguments.lookup_db)) OR NOT len(trim(arguments.description))>
            <cfreturn out />
        </cfif>
        <cfquery name="qLookup" datasource="#Application.DataSrc#">
            SELECT Lookup_1, Lookup_2, Measure
            FROM #arguments.lookup_db#.pValue_Lookup
            WHERE Description = <cfqueryparam value="#trim(arguments.description)#" cfsqltype="cf_sql_varchar">
        </cfquery>
        <cfset arrayAppend(out.value1, { "ANSWER" = "0" }) />
        <cfset seen1["0"] = true />
        <cfloop query="qLookup">
            <cfif qLookup.currentRow EQ 1 AND len(trim(qLookup.Measure))>
                <cfset out.measure = trim(qLookup.Measure) />
            </cfif>
            <cfset v1 = trim(qLookup.Lookup_1) />
            <cfif len(v1) AND NOT structKeyExists(seen1, v1)>
                <cfset seen1[v1] = true />
                <cfset arrayAppend(out.value1, { "ANSWER" = v1 }) />
            </cfif>
            <cfset v2 = trim(qLookup.Lookup_2) />
            <cfif len(v2) AND val(v2) GTE 0 AND NOT structKeyExists(seen2, v2)>
                <cfset seen2[v2] = true />
                <cfset arrayAppend(out.value2, { "ANSWER" = v2 }) />
            </cfif>
        </cfloop>
        <cfreturn out />
    </cffunction>

    <cffunction name="mapProgressValueTypeToQuestionsType" access="private" returntype="string" output="false">
        <cfargument name="valueType" type="string" required="true" />
        <cfset var vt = lCase(trim(arguments.valueType)) />
        <cfif vt EQ "dropdown"><cfreturn "select" /></cfif>
        <cfif vt EQ "notes"><cfreturn "textarea" /></cfif>
        <cfif vt EQ "radio"><cfreturn "radio" /></cfif>
        <cfif vt EQ "checkbox"><cfreturn "checkbox" /></cfif>
        <cfreturn "text" />
    </cffunction>

    <cffunction name="makeProgressNoteField" access="private" returntype="struct" output="false"
        hint="One answer field for a pProgress row: Progress_ID, title, Description, ValueType">
        <cfargument name="progressId" type="numeric" required="true" />
        <cfargument name="sectionTitle" type="string" required="true" />
        <cfargument name="rowDescription" type="string" required="true" hint="pProgress.Description" />
        <cfargument name="displayDescription" type="string" required="true" />
        <cfargument name="questionsLabel" type="string" required="true" />
        <cfargument name="fieldLabel" type="string" required="true" />
        <cfargument name="answerLabel" type="string" required="true" />
        <cfargument name="valueType" type="string" required="true" />
        <cfargument name="answers" type="array" required="false" default="#[]#" />
        <cfargument name="extra" type="struct" required="false" default="#{}#" />

        <cfset var qRow = {
            "progress_id" = toJsonInt(arguments.progressId),
            "Progress_ID" = toJsonInt(arguments.progressId),
            "title" = trim(arguments.sectionTitle),
            "description" = trim(arguments.displayDescription),
            "Description" = trim(arguments.displayDescription),
            "questions_label" = trim(arguments.questionsLabel),
            "ValueType" = lCase(trim(arguments.valueType)),
            "value_type" = lCase(trim(arguments.valueType)),
            "ques_id" = "progress_" & toJsonInt(arguments.progressId) & "_" & lCase(trim(arguments.answerLabel)),
            "questions" = trim(arguments.fieldLabel),
            "answer_label" = trim(arguments.answerLabel),
            "questions_type" = mapProgressValueTypeToQuestionsType(arguments.valueType),
            "answers" = arguments.answers
        } />
        <cfset structAppend(qRow, arguments.extra, true) />
        <cfreturn qRow />
    </cffunction>

    <cffunction name="appendProgressNoteField" access="private" returntype="array" output="false">
        <cfargument name="questions" type="array" required="true" />
        <cfargument name="field" type="struct" required="true" />

        <cfif lCase(trim(arguments.field.answer_label)) EQ "physician_alert">
            <cfreturn arguments.questions />
        </cfif>
        <cfset arrayAppend(arguments.questions, arguments.field) />
        <cfreturn arguments.questions />
    </cffunction>

    <cffunction name="getWoundMeasureLookupOptions" access="private" returntype="array" output="false">
        <cfargument name="lookup_db" type="string" required="true" />
        <cfset var qLookup = "" />
        <cfset var answers = [] />
        <cfif NOT len(trim(arguments.lookup_db))>
            <cfreturn answers />
        </cfif>
        <cfquery name="qLookup" datasource="#Application.DataSrc#">
            SELECT Lookup_1, Lookup_2, Measure
            FROM #arguments.lookup_db#.pValue_Lookup
            WHERE Item_ID = <cfqueryparam value="201" cfsqltype="cf_sql_integer">
            ORDER BY ID
        </cfquery>
        <cfloop query="qLookup">
            <cfset arrayAppend(answers, {
                "ANSWER" = trim(qLookup.Lookup_1),
                "ANSWER_2" = trim(qLookup.Lookup_2)
            }) />
        </cfloop>
        <cfreturn answers />
    </cffunction>

    <cffunction name="appendProgressNoteRowFields" access="private" returntype="array" output="false"
        hint="Emit all answer fields for one pProgress row (matches progress_notesnew.cfm)">
        <cfargument name="questions" type="array" required="true" />
        <cfargument name="qProgress" type="query" required="true" />
        <cfargument name="row" type="numeric" required="true" />
        <cfargument name="progressCols" type="struct" required="true" />
        <cfargument name="lookup_db" type="string" required="true" />
        <cfargument name="skillCode" type="string" required="false" default="" />

        <cfset var out = arguments.questions />
        <cfset var progressId = val(arguments.qProgress.Progress_ID[arguments.row]) />
        <cfset var pType = trim(arguments.qProgress.Type[arguments.row]) />
        <cfset var desc = trim(arguments.qProgress.Description[arguments.row]) />
        <cfset var isGg = (left(desc, 2) EQ "GG") />
        <cfset var meta = getProgressNoteSectionMeta(pType, desc) />
        <cfset var sectionTitle = meta.title />
        <cfif (pType EQ "problem" OR pType EQ "symptom") AND NOT isGg AND structKeyExists(arguments.qProgress, "Header")>
            <cfset var headerVal = trim(arguments.qProgress.Header[arguments.row]) />
            <cfif len(headerVal)>
                <cfset sectionTitle = headerVal />
            </cfif>
        </cfif>
        <cfset var displayDescription = desc />
        <cfset var questionsLabel = desc />
        <cfset var baseExtra = {
            "progress_type" = pType,
            "section" = meta.section,
            "item_id" = toJsonInt(arguments.qProgress.Item_ID[arguments.row])
        } />
        <cfset var field = {} />
        <cfset var vitalLookup = {} />
        <cfset var woundTitle = "" />
        <cfset var isWound = false />
        <cfset var isSnOrHha = (uCase(trim(arguments.skillCode)) EQ "SN" OR uCase(trim(arguments.skillCode)) EQ "HHA") />
        <cfset var severityAnswers = [] />
        <cfset var woundMeasureAnswers = getWoundMeasureLookupOptions(arguments.lookup_db) />
        <cfset var careplanId = structKeyExists(arguments.progressCols, "CAREPLAN_ID") ? val(arguments.qProgress.Careplan_ID[arguments.row]) : 0 />

        <cfif progressId LTE 0 OR lCase(pType) EQ "physician_alert">
            <cfreturn out />
        </cfif>
        <cfif NOT len(desc) AND pType NEQ "patient_complaint">
            <cfreturn out />
        </cfif>

        <cfif pType EQ "patient_complaint">
            <cfset displayDescription = "Patient Complaints" />
            <cfset questionsLabel = "Patient Complaints" />
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Patient Complaints",
                answerLabel = "Description",
                valueType = "notes",
                answers = [],
                extra = baseExtra
            ) />
            <cfreturn appendProgressNoteField(out, field) />
        </cfif>

        <cfif structKeyExists(arguments.qProgress, "woundstitle")>
            <cfset woundTitle = trim(arguments.qProgress.woundstitle[arguments.row]) />
        </cfif>
        <cfset isWound = (len(woundTitle) GT 0 OR left(desc, 1) EQ "##") />

        <cfif pType EQ "vital">
            <cfset vitalLookup = getVitalLookupOptions(arguments.lookup_db, desc) />
            <cfif len(vitalLookup.measure)>
                <cfset baseExtra["measure"] = vitalLookup.measure />
            </cfif>
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = desc,
                answerLabel = "Value_1",
                valueType = "dropdown",
                answers = vitalLookup.value1,
                extra = baseExtra
            ) />
            <cfset out = appendProgressNoteField(out, field) />
            <cfif arrayLen(vitalLookup.value2) AND structKeyExists(arguments.progressCols, "VALUE_2")>
                <cfset field = makeProgressNoteField(
                    progressId = progressId,
                    sectionTitle = sectionTitle,
                    rowDescription = desc,
                    displayDescription = displayDescription,
                    questionsLabel = questionsLabel,
                    fieldLabel = "Value 2",
                    answerLabel = "Value_2",
                    valueType = "dropdown",
                    answers = vitalLookup.value2,
                    extra = baseExtra
                ) />
                <cfset out = appendProgressNoteField(out, field) />
            </cfif>
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Notes",
                answerLabel = "Notes",
                valueType = "notes",
                answers = [],
                extra = baseExtra
            ) />
            <cfset out = appendProgressNoteField(out, field) />
            <cfif lCase(desc) EQ "pain">
                <cfif structKeyExists(arguments.progressCols, "PAIN_LOCATION")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Pain Location", "pain_location", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "PAIN_DURATION")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Pain Duration", "pain_duration", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "PAIN_RELIEF")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Alleviating factors", "pain_relief", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "PAIN_EXACERBATE")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Aggravating factors", "pain_exacerbate", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "PAIN_MED_EFFECTIVENESS")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Current treatment effectiveness", "pain_med_effectiveness", "text", [], baseExtra)) />
                </cfif>
            </cfif>
            <cfreturn out />
        </cfif>

        <cfif pType EQ "procedure" OR pType EQ "standing_order">
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Yes/No",
                answerLabel = "YesOrNo",
                valueType = "radio",
                answers = buildAnswerOptionsFromArray(["Yes", "No"]),
                extra = baseExtra
            ) />
            <cfset out = appendProgressNoteField(out, field) />
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Notes",
                answerLabel = "Notes",
                valueType = "notes",
                answers = [],
                extra = baseExtra
            ) />
            <cfreturn appendProgressNoteField(out, field) />
        </cfif>

        <cfif pType EQ "patient_goal" OR pType EQ "goal" OR pType EQ "teach" OR pType EQ "discharge_plan">
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Progress",
                answerLabel = "YesOrNo",
                valueType = "radio",
                answers = buildAnswerOptionsFromArray(["0", "1", "2", "3", "4"]),
                extra = baseExtra
            ) />
            <cfset out = appendProgressNoteField(out, field) />
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Notes",
                answerLabel = "Notes",
                valueType = "notes",
                answers = [],
                extra = baseExtra
            ) />
            <cfset out = appendProgressNoteField(out, field) />
            <cfif pType EQ "discharge_plan">
                <cfif structKeyExists(arguments.progressCols, "DATE_TARGET")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Target date", "Date_Target", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "DISCHARGE_REASON")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(
                        progressId = progressId,
                        sectionTitle = sectionTitle,
                        rowDescription = desc,
                        displayDescription = displayDescription,
                        questionsLabel = questionsLabel,
                        fieldLabel = "Discharge reason",
                        answerLabel = "Discharge_Reason",
                        valueType = "checkbox",
                        answers = buildAnswerOptionsFromArray([
                            "goals met",
                            "patient/family is requesting discharge",
                            "funding source expiring",
                            "patient is non-compliant",
                            "care environment pose safety risks to home health staf"
                        ]),
                        extra = baseExtra
                    )) />
                </cfif>
            </cfif>
            <cfreturn out />
        </cfif>

        <cfif (pType EQ "problem" OR pType EQ "symptom") AND careplanId GT 0>
            <cfif isGg AND NOT isSnOrHha AND NOT isWound>
                <cfset severityAnswers = buildAnswerOptionsFromArray(["1", "2", "3", "4", "5", "6"]) />
            <cfelseif isWound>
                <cfset severityAnswers = buildAnswerOptionsFromArray(["0", "1", "2", "3", "4", "10"]) />
            <cfelse>
                <cfset severityAnswers = buildAnswerOptionsFromArray(["0", "1", "2", "3", "4"]) />
            </cfif>
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Severity",
                answerLabel = "severity",
                valueType = "radio",
                answers = severityAnswers,
                extra = baseExtra
            ) />
            <cfset out = appendProgressNoteField(out, field) />
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Notes",
                answerLabel = "Notes",
                valueType = "notes",
                answers = [],
                extra = baseExtra
            ) />
            <cfset out = appendProgressNoteField(out, field) />

            <cfif isWound>
                <cfif arrayLen(woundMeasureAnswers)>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Length", "Value_1", "dropdown", woundMeasureAnswers, baseExtra)) />
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Width", "Value_2", "dropdown", woundMeasureAnswers, baseExtra)) />
                    <cfif structKeyExists(arguments.progressCols, "VALUE_3")>
                        <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "Depth", "Value_3", "dropdown", woundMeasureAnswers, baseExtra)) />
                    </cfif>
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "HEALING_STAGE")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(
                        progressId = progressId,
                        sectionTitle = sectionTitle,
                        rowDescription = desc,
                        displayDescription = displayDescription,
                        questionsLabel = questionsLabel,
                        fieldLabel = "Healing Stage",
                        answerLabel = "healing_stage",
                        valueType = "dropdown",
                        answers = buildAnswerOptionsFromArray([
                            "0 - Re-epithelialized or healed",
                            "1 - Fully granulating",
                            "2 - Early/partial granulation",
                            "3 - Not healing",
                            "NA - Not applicable or no observable wound/ulcer"
                        ]),
                        extra = baseExtra
                    )) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_TUNNELING")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "tunneling dep/drctn", "wnd_tunneling", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_UNDERM")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "undermn dep/drctn", "wnd_underm", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_GRAN")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "% granulation", "wnd_gran", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_EPITH")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "% epithelial", "wnd_epith", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_ESCHAR")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "% eschar", "wnd_eschar", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_SLOUGH")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "% slough", "wnd_slough", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_FIB")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(progressId, sectionTitle, desc, displayDescription, questionsLabel, "% fibrinous", "wnd_fib", "text", [], baseExtra)) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_MARGIN")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(
                        progressId = progressId,
                        sectionTitle = sectionTitle,
                        rowDescription = desc,
                        displayDescription = displayDescription,
                        questionsLabel = questionsLabel,
                        fieldLabel = "Wound Margin",
                        answerLabel = "wnd_margin",
                        valueType = "checkbox",
                        answers = buildAnswerOptionsFromList("regular,irregular,rolled,epithelialized,macerated,other"),
                        extra = baseExtra
                    )) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_EXUDATE")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(
                        progressId = progressId,
                        sectionTitle = sectionTitle,
                        rowDescription = desc,
                        displayDescription = displayDescription,
                        questionsLabel = questionsLabel,
                        fieldLabel = "Wound Exudate",
                        answerLabel = "wnd_exudate",
                        valueType = "checkbox",
                        answers = buildAnswerOptionsFromList("none,scant,moderate,large clear,sanguinous,purulent,other"),
                        extra = baseExtra
                    )) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_ODOR")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(
                        progressId = progressId,
                        sectionTitle = sectionTitle,
                        rowDescription = desc,
                        displayDescription = displayDescription,
                        questionsLabel = questionsLabel,
                        fieldLabel = "Wound Odor",
                        answerLabel = "wnd_odor",
                        valueType = "checkbox",
                        answers = buildAnswerOptionsFromList("none,mild,foul,other"),
                        extra = baseExtra
                    )) />
                </cfif>
                <cfif structKeyExists(arguments.progressCols, "WND_PAIN")>
                    <cfset out = appendProgressNoteField(out, makeProgressNoteField(
                        progressId = progressId,
                        sectionTitle = sectionTitle,
                        rowDescription = desc,
                        displayDescription = displayDescription,
                        questionsLabel = questionsLabel,
                        fieldLabel = "Wound Pain",
                        answerLabel = "wnd_pain",
                        valueType = "checkbox",
                        answers = buildAnswerOptionsFromList("none,mild,strong,severe,other"),
                        extra = baseExtra
                    )) />
                </cfif>
            </cfif>
            <cfreturn out />
        </cfif>

        <cfif (pType EQ "problem" OR pType EQ "symptom") AND careplanId EQ 0>
            <cfset field = makeProgressNoteField(
                progressId = progressId,
                sectionTitle = sectionTitle,
                rowDescription = desc,
                displayDescription = displayDescription,
                questionsLabel = questionsLabel,
                fieldLabel = "Notes",
                answerLabel = "Notes",
                valueType = "notes",
                answers = [],
                extra = baseExtra
            ) />
            <cfreturn appendProgressNoteField(out, field) />
        </cfif>

        <cfreturn out />
    </cffunction>

    <cffunction name="buildProgressNoteAssessmentQuestions" access="private" returntype="struct" output="false"
        hint="pProgress rows for a visit as assessment questions (all sections except Physician Alert)">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="lookup_db" type="string" required="true" />
        <cfargument name="scheduleInfo" type="struct" required="true" />

        <cfset var result = {
            "questions" = [],
            "count" = 0,
            "schedule_id" = 0,
            "section_count" = 0
        } />
        <cfset var scheduleId = structKeyExists(arguments.scheduleInfo, "schedule_id") ? val(arguments.scheduleInfo.schedule_id) : 0 />
        <cfset var progressCols = {} />
        <cfset var qCols = "" />
        <cfset var qProgress = "" />
        <cfset var colName = "" />
        <cfset var pType = "" />
        <cfset var desc = "" />
        <cfset var headerVal = "" />
        <cfset var progressId = 0 />
        <cfset var meta = {} />
        <cfset var extra = {} />
        <cfset var sectionKeys = {} />
        <cfset var questionLabel = "" />
        <cfset var hasCol = false />
        <cfset var skillCode = structKeyExists(arguments.scheduleInfo, "skill") ? trim(arguments.scheduleInfo.skill) : "" />
        <cfset var vitalLookup = {} />
        <cfset var scaleLegend = "" />

        <cfif scheduleId LTE 0 OR NOT len(trim(arguments.agency_db))>
            <cfreturn result />
        </cfif>
        <cfset result.schedule_id = toJsonInt(scheduleId) />

        <cfquery name="qCols" datasource="#Application.DataSrc#">
            SELECT COLUMN_NAME
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE TABLE_SCHEMA = <cfqueryparam value="#arguments.agency_db#" cfsqltype="cf_sql_varchar">
              AND TABLE_NAME = <cfqueryparam value="pProgress" cfsqltype="cf_sql_varchar">
        </cfquery>
        <cfloop query="qCols">
            <cfset progressCols[uCase(trim(qCols.COLUMN_NAME))] = true />
        </cfloop>
        <cfset hasCol = structKeyExists(progressCols, "PROGRESS_ID") />
        <cfif NOT hasCol>
            <cfreturn result />
        </cfif>

        <cfquery name="qProgress" datasource="#Application.DataSrc#">
            SELECT pProgress.Progress_ID,
                   pProgress.Assmt_ID,
                   pProgress.Schedule_ID,
                   pProgress.Item_ID,
                   pProgress.Type,
                   pProgress.Description,
                   pProgress.Notes
                   <cfif structKeyExists(progressCols, "CAREPLAN_ID")>, pProgress.Careplan_ID</cfif>
                   <cfif structKeyExists(progressCols, "YESORNO")>, pProgress.YesOrNo</cfif>
                   <cfif structKeyExists(progressCols, "SEVERITY")>, pProgress.severity</cfif>
                   <cfif structKeyExists(progressCols, "VALUE_1")>, pProgress.Value_1</cfif>
                   <cfif structKeyExists(progressCols, "VALUE_2")>, pProgress.Value_2</cfif>
                   <cfif structKeyExists(progressCols, "VALUE_3")>, pProgress.Value_3</cfif>
                   <cfif structKeyExists(progressCols, "HEALING_STAGE")>, pProgress.healing_stage</cfif>
                   <cfif structKeyExists(progressCols, "WND_TUNNELING")>, pProgress.wnd_tunneling</cfif>
                   <cfif structKeyExists(progressCols, "WND_UNDERM")>, pProgress.wnd_underm</cfif>
                   <cfif structKeyExists(progressCols, "WND_GRAN")>, pProgress.wnd_gran</cfif>
                   <cfif structKeyExists(progressCols, "WND_EPITH")>, pProgress.wnd_epith</cfif>
                   <cfif structKeyExists(progressCols, "WND_ESCHAR")>, pProgress.wnd_eschar</cfif>
                   <cfif structKeyExists(progressCols, "WND_SLOUGH")>, pProgress.wnd_slough</cfif>
                   <cfif structKeyExists(progressCols, "WND_FIB")>, pProgress.wnd_fib</cfif>
                   <cfif structKeyExists(progressCols, "WND_MARGIN")>, pProgress.wnd_margin</cfif>
                   <cfif structKeyExists(progressCols, "WND_EXUDATE")>, pProgress.wnd_exudate</cfif>
                   <cfif structKeyExists(progressCols, "WND_ODOR")>, pProgress.wnd_odor</cfif>
                   <cfif structKeyExists(progressCols, "WND_PAIN")>, pProgress.wnd_pain</cfif>
                   <cfif structKeyExists(progressCols, "DATE_TARGET")>, pProgress.Date_Target</cfif>
                   <cfif structKeyExists(progressCols, "DISCHARGE")>, pProgress.Discharge</cfif>
                   <cfif structKeyExists(progressCols, "DISCHARGE_REASON")>, pProgress.Discharge_Reason</cfif>
                   <cfif structKeyExists(progressCols, "PAIN_LOCATION")>, pProgress.pain_location</cfif>
                   <cfif structKeyExists(progressCols, "PAIN_DURATION")>, pProgress.pain_duration</cfif>
                   <cfif structKeyExists(progressCols, "PAIN_RELIEF")>, pProgress.pain_relief</cfif>
                   <cfif structKeyExists(progressCols, "PAIN_EXACERBATE")>, pProgress.pain_exacerbate</cfif>
                   <cfif structKeyExists(progressCols, "PAIN_MED_EFFECTIVENESS")>, pProgress.pain_med_effectiveness</cfif>
                   ,
                   (SELECT MAX(pw.Header)
                    FROM #arguments.lookup_db#.pPathway pw
                    WHERE pw.Item_ID = pProgress.Item_ID
                      AND pw.Header IS NOT NULL) AS Header,
                   CASE
                       WHEN (pProblem.M0_No LIKE 'SK%' OR pProblem.M0_No = 'wound')
                            AND LEFT(pProgress.Description, 1) = '##'
                       THEN 'Wounds'
                       ELSE ''
                   END AS woundstitle
            FROM #arguments.agency_db#.pProgress
            LEFT OUTER JOIN #arguments.agency_db#.pProblem
                ON pProblem.Problem_ID = pProgress.Careplan_ID
               AND pProblem.status = 0
            WHERE pProgress.Schedule_ID = <cfqueryparam value="#scheduleId#" cfsqltype="cf_sql_integer">
              AND pProgress.Status = <cfqueryparam value="0" cfsqltype="cf_sql_integer">
              AND NOT (
                    pProgress.Type IN ('problem','symptom')
                    AND pProgress.Item_ID IN (
                        SELECT Item_ID
                        FROM #arguments.lookup_db#.pPathway
                        WHERE Header = <cfqueryparam value="Vital Signs" cfsqltype="cf_sql_varchar">
                    )
              )
            GROUP BY pProgress.Progress_ID
            ORDER BY FIELD(pProgress.Type,
                        'patient_complaint','patient_goal','vital','problem','symptom',
                        'procedure','standing_order','goal','discharge_plan','teach'),
                     pProgress.Progress_ID
        </cfquery>

        <cfloop query="qProgress">
            <cfset pType = trim(qProgress.Type) />
            <cfset desc = trim(qProgress.Description) />
            <cfset meta = getProgressNoteSectionMeta(pType, desc) />
            <cfset sectionKeys[meta.section] = true />
            <cfset result.questions = appendProgressNoteRowFields(
                questions = result.questions,
                qProgress = qProgress,
                row = qProgress.currentRow,
                progressCols = progressCols,
                lookup_db = arguments.lookup_db,
                skillCode = skillCode
            ) />
        </cfloop>

        <cfset result.count = toJsonInt(arrayLen(result.questions)) />
        <cfset result.section_count = toJsonInt(structCount(sectionKeys)) />
        <cfreturn result />
    </cffunction>

    <cffunction name="getAssessmentQuestionsByReason" access="remote" returnformat="JSON" hint="Return assessment questions and answers filtered by assessment_reason">
        <cfargument name="assessment_reason" type="string" required="yes" hint="Assessment reason filter">
        <cfargument name="api_key" type="string" required="yes" hint="Public endpoint API key">
        <cfargument name="agency_id" type="string" required="no" default="" hint="Agency ID — required for vital questions from pVital_Param">
        <cfargument name="patient_id" type="string" required="no" default="" hint="Patient ID used to find the related visit">
        <cfargument name="visit_type" type="string" required="no" default="" hint="pSchedules.Visit_Type or assessment reason">
        <cfargument name="visit_date" type="string" required="no" default="" hint="Visit date (yyyy-mm-dd) for the related schedule">
        <cfargument name="schedule_id" type="string" required="no" default="" hint="pSchedules.Schedule_ID — used to load progress note questions">
        <cfargument name="assessment_id" type="string" required="no" default="" hint="Optional pAssessments.Assmt_ID">
        <cfargument name="whisper_files_id" type="string" required="no" default="" hint="whisper_files.ID — Schedule_ID is read from this row when schedule_id is not passed">

        <cfset var response = {} />
        <cfset var result = [] />
        <cfset var vitalQuestions = [] />
        <cfset var woundQuestions = [] />
        <cfset var woundFieldCount = 0 />
        <cfset var answersByQid = {} />
        <cfset var qid = "" />
        <cfset var qRow = {} />
        <cfset var aRow = {} />
        <cfset var col = "" />
        <cfset var val = "" />
        <cfset var lookup_db = "hhapowerpath" />
        <cfset var agency_db = "" />
        <cfset var progressQuestions = { "questions" = [], "count" = 0, "schedule_id" = 0, "section_count" = 0 } />
        <cfset var progressSchedule = {} />
        <cfset var progressRow = {} />
        <cfset var resolvedScheduleId = 0 />
        <cfset var qWhisperSched = "" />

        <cftry>
            <cfif NOT isAssessmentLookupKeyValid(arguments.api_key)>
                <cfheader statuscode="401" statustext="Unauthorized">
                <cfset response = {
                    "success": false,
                    "message": "Invalid API key",
                    "assessment_reason": trim(arguments.assessment_reason),
                    "questions": [],
                    "count": toJsonInt(0),
                    "data": {
                        "assessment_reason": trim(arguments.assessment_reason),
                        "questions": [],
                        "count": toJsonInt(0)
                    }
                } />
                <cfreturn serializeJSON(response) />
            </cfif>

            <cfif structKeyExists(Request, "prefix_db_lookup") AND len(trim(Request.prefix_db_lookup))>
                <cfset lookup_db = trim(Request.prefix_db_lookup) />
            </cfif>
            <!--- #10641: PATTII lookup tables when feature flag is ON --->
            <cfif NOT structKeyExists(Request, "assessment_questions_lookup")>
                <cfinclude template="../../Application_assessment_lookup_PATTII.cfm" />
            </cfif>

            <cfif val(arguments.agency_id) GT 0>
                <cfset agency_db = "agency_" & val(arguments.agency_id) />
            </cfif>

            <cfset resolvedScheduleId = val(arguments.schedule_id) />
            <cfif resolvedScheduleId LTE 0 AND len(trim(agency_db)) AND val(arguments.whisper_files_id) GT 0>
                <cftry>
                    <cfquery name="qWhisperSched" datasource="#Application.DataSrc#">
                        SELECT Schedule_ID
                        FROM #agency_db#.whisper_files
                        WHERE ID = <cfqueryparam value="#val(arguments.whisper_files_id)#" cfsqltype="cf_sql_bigint">
                          AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_tinyint">
                        LIMIT 1
                    </cfquery>
                    <cfif qWhisperSched.recordcount GT 0>
                        <cfset resolvedScheduleId = val(qWhisperSched.Schedule_ID) />
                    </cfif>
                    <cfcatch type="any"></cfcatch>
                </cftry>
            </cfif>

            <cfquery name="get_questions" datasource="#Application.DataSrc#">
                SELECT  ques_id,
                CASE
                    WHEN questions_2 <> '' THEN
                        TRIM(CONCAT_WS(' ', NULLIF(TRIM(ques_identifier_2), ''), NULLIF(TRIM(questions_2), '')))
                    ELSE questions
                END AS questions,
                    ques_identifier_2 AS questions_new, quescolor,title_2 as title, 
                 questions_label, answer_label, questions_type, 
                  new_order_id AS order_id , assessment_reason, new_wizard AS wizard,
                  ques_identifier_2 AS ques_identifier
                FROM #lookup_db#.#Request.assessment_questions_lookup#
                WHERE assessment_reason = <cfqueryparam cfsqltype="cf_sql_varchar" value="#trim(arguments.assessment_reason)#">
                  AND Deleted = 0
                ORDER BY order_id, wizard, answer_label
            </cfquery>

            <cfquery name="get_answers" datasource="#Application.DataSrc#">
                                SELECT
                                        val_id AS VAL_ID,
                                        answer_2 AS ANSWER,
                                        text_labels AS TEXT_LABELS,
                                        assessment_reason AS ASSESSMENT_REASON,
                                        ans_id AS ANS_ID,
                                        item_id AS ITEM_ID,
                                        answer_2 AS ANSWER_2,
                                        question_id AS QUESTION_ID,
                                        answer_type AS ANSWER_TYPE
                FROM #lookup_db#.#Request.assessment_answers_lookup#
                WHERE assessment_reason = <cfqueryparam cfsqltype="cf_sql_varchar" value="#trim(arguments.assessment_reason)#">
                  AND Deleted = 0
                ORDER BY question_id, srt_id, answer_label
            </cfquery>

            <cfloop query="get_answers">
                <cfset qid = get_answers.question_id />
                <cfif NOT structKeyExists(answersByQid, qid)>
                    <cfset answersByQid[qid] = [] />
                </cfif>

                <cfset aRow = {} />
                <cfloop list="#get_answers.columnList#" index="col">
                    <cfset val = get_answers[col][get_answers.currentRow] />
                    <cfif isBinary(val)>
                        <cfcontinue>
                    </cfif>
                    <cfset aRow[col] = val />
                </cfloop>

                <cfset arrayAppend(answersByQid[qid], aRow) />
            </cfloop>

            <cfloop query="get_questions">
                <cfset qRow = {} />
                <cfloop list="#get_questions.columnList#" index="col">
                    <cfset val = get_questions[col][get_questions.currentRow] />
                    <cfif isBinary(val)>
                        <cfcontinue>
                    </cfif>
                    <cfset qRow[col] = val />
                </cfloop>

                <cfset qid = get_questions.ques_id />
                <cfif structKeyExists(answersByQid, qid)>
                    <cfset qRow["answers"] = answersByQid[qid] />
                <cfelse>
                    <cfset qRow["answers"] = [] />
                </cfif>

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

            <cfset vitalQuestions = buildVitalAssessmentQuestions(agency_db = agency_db, lookup_db = lookup_db) />
            <cfloop array="#vitalQuestions#" index="qRow">
                <cfset arrayAppend(result, qRow) />
            </cfloop>

            <cfset woundQuestions = buildWoundAssessmentQuestions(lookup_db = lookup_db) />
            <cfloop array="#woundQuestions#" index="qRow">
                <cfset arrayAppend(result, qRow) />
                <cfif structKeyExists(qRow, "field_count")>
                    <cfset woundFieldCount = val(qRow.field_count) />
                </cfif>
            </cfloop>

            <cfif len(trim(agency_db)) AND val(resolvedScheduleId) GT 0>
                <cftry>
                    <cfset progressSchedule = findRelatedProgressSchedule(
                        agency_db = agency_db,
                        schedule_id = resolvedScheduleId
                    ) />
                    <cfif val(progressSchedule.schedule_id) GT 0>
                        <cfset progressQuestions = buildProgressNoteAssessmentQuestions(
                            agency_db = agency_db,
                            lookup_db = lookup_db,
                            scheduleInfo = progressSchedule
                        ) />
                        <cfif arrayLen(progressQuestions.questions) GT 0>
                            <cfset arrayAppend(result, {
                                "ques_id" = "pProgress",
                                "questions" = "Progress Notes",
                                "description" = "Progress Notes",
                                "questions_type" = "pProgress",
                                "answer_label" = "",
                                "answers" = [],
                                "pProgress" = {
                                    "schedule_id" = toJsonInt(progressQuestions.schedule_id),
                                    "section_count" = toJsonInt(progressQuestions.section_count),
                                    "count" = toJsonInt(progressQuestions.count),
                                    "questions" = progressQuestions.questions
                                }
                            }) />
                        </cfif>
                    </cfif>
                    <cfcatch type="any">
                        <cfset progressQuestions = {
                            "questions" = [],
                            "count" = toJsonInt(0),
                            "schedule_id" = toJsonInt(resolvedScheduleId),
                            "section_count" = toJsonInt(0)
                        } />
                    </cfcatch>
                </cftry>
            </cfif>

            <cfset response = {
                "success": true,
                "message": "Questions loaded successfully",
                "assessment_reason": trim(arguments.assessment_reason),
                "questions": result,
                "count": toJsonInt(arrayLen(result)),
                "vital_question_count": toJsonInt(arrayLen(vitalQuestions)),
                "wound_question_count": toJsonInt(arrayLen(woundQuestions)),
                "wound_field_count": toJsonInt(woundFieldCount),
                "progress_question_count": toJsonInt(progressQuestions.count),
                "progress_schedule_id": toJsonInt(progressQuestions.schedule_id),
                "progress_section_count": toJsonInt(progressQuestions.section_count),
                "data": {
                    "assessment_reason": trim(arguments.assessment_reason),
                    "questions": result,
                    "count": toJsonInt(arrayLen(result)),
                    "vital_question_count": toJsonInt(arrayLen(vitalQuestions)),
                    "wound_question_count": toJsonInt(arrayLen(woundQuestions)),
                    "wound_field_count": toJsonInt(woundFieldCount),
                    "progress_question_count": toJsonInt(progressQuestions.count),
                    "progress_schedule_id": toJsonInt(progressQuestions.schedule_id),
                    "progress_section_count": toJsonInt(progressQuestions.section_count)
                }
            } />

            <cfcatch type="any">
                <cfheader statuscode="500" statustext="Internal Server Error">
                <cfset response = {
                    "success": false,
                    "message": "Failed to load assessment questions",
                    "error": cfcatch.message,
                    "detail": cfcatch.detail,
                    "assessment_reason": trim(arguments.assessment_reason),
                    "questions": [],
                    "count": toJsonInt(0),
                    "data": {
                        "assessment_reason": trim(arguments.assessment_reason),
                        "questions": [],
                        "count": toJsonInt(0)
                    }
                } />
            </cfcatch>
        </cftry>

        <cfreturn serializeJSON(response) />
    </cffunction>

    <!--- Assessment Update Data --->
    <!--- After AI import: set Review_Status 1 if upload left it at 0 (legacy rows) --->
    <cffunction name="markWhisperFileAIReviewRequired" access="private" returntype="void" output="false">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="assessment_id" type="numeric" required="true" />
        <cfargument name="whisper_files_id" type="numeric" required="false" default="0" />
        <cfif val(arguments.whisper_files_id) GT 0>
            <cfquery datasource="#Application.DataSrc#">
                UPDATE #arguments.agency_db#.whisper_files
                SET Review_Status = <cfqueryparam value="1" cfsqltype="cf_sql_tinyint">
                WHERE ID = <cfqueryparam value="#val(arguments.whisper_files_id)#" cfsqltype="cf_sql_bigint">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_tinyint">
                  AND Review_Status = <cfqueryparam value="0" cfsqltype="cf_sql_tinyint">
            </cfquery>
        <cfelseif val(arguments.assessment_id) GT 0>
            <cfquery name="qLatestMobileWhisper" datasource="#Application.DataSrc#">
                SELECT ID
                FROM #arguments.agency_db#.whisper_files
                WHERE Assmt_ID = <cfqueryparam value="#val(arguments.assessment_id)#" cfsqltype="cf_sql_varchar">
                  AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_tinyint">
                  AND Review_Status = <cfqueryparam value="0" cfsqltype="cf_sql_tinyint">
                ORDER BY ID DESC
                LIMIT 1
            </cfquery>
            <cfif qLatestMobileWhisper.recordcount GT 0>
                <cfquery datasource="#Application.DataSrc#">
                    UPDATE #arguments.agency_db#.whisper_files
                    SET Review_Status = <cfqueryparam value="1" cfsqltype="cf_sql_tinyint">
                    WHERE ID = <cfqueryparam value="#qLatestMobileWhisper.ID#" cfsqltype="cf_sql_bigint">
                </cfquery>
            </cfif>
        </cfif>
    </cffunction>

    <cffunction name="clearWhisperFileAIReviewRequired" access="public" returntype="void" output="false" hint="Call when clinician completes assessment review (Review_Status 1 -> 2)">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="assessment_id" type="numeric" required="true" />
        <cfif val(arguments.assessment_id) LTE 0>
            <cfreturn />
        </cfif>
        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agency_db#.whisper_files
            SET Review_Status = <cfqueryparam value="2" cfsqltype="cf_sql_tinyint">
            WHERE Assmt_ID = <cfqueryparam value="#val(arguments.assessment_id)#" cfsqltype="cf_sql_varchar">
              AND Status = <cfqueryparam value="0" cfsqltype="cf_sql_tinyint">
              AND Review_Status = <cfqueryparam value="1" cfsqltype="cf_sql_tinyint">
        </cfquery>
    </cffunction>

    <cffunction name="importAssessmentDataFromJobResponse" access="remote" returnformat="JSON" hint="Update assessment data directly from parsed job status response">
        <cfargument name="parsedJobResponse" required="yes" hint="Struct or JSON from jobs API response">
        <cfargument name="jobStatusResponse" required="no" default="" hint="Original cfhttp job status response (optional)">
        <cfargument name="whisper_files_id" required="no" default="0" hint="whisper_files row to flag for AI review after import">
        <cfargument name="source_channel" required="no" default="" hint="Web or Mobile App label for vitals/wounds notification email">

        <cfset var payload = {} />
        <cfset var parsedPayload = {} />
        <cfset var resultNode = {} />
        <cfset var apiResponse = "" />
        <cfset var response = {} />

        <cftry>
            <cfif isStruct(arguments.parsedJobResponse)>
                <cfset parsedPayload = arguments.parsedJobResponse />
            <cfelseif isSimpleValue(arguments.parsedJobResponse) AND len(trim(arguments.parsedJobResponse)) AND isJSON(arguments.parsedJobResponse)>
                <cfset parsedPayload = deserializeJSON(arguments.parsedJobResponse) />
            <cfelse>
                <cfthrow type="AssessmentImport" message="Invalid parsedJobResponse" detail="Expected struct or JSON payload" />
            </cfif>

            <cfif NOT isStruct(parsedPayload)>
                <cfthrow type="AssessmentImport" message="Invalid parsedJobResponse" detail="Payload is not a struct" />
            </cfif>

            <cfif structKeyExists(parsedPayload, "result") AND isStruct(parsedPayload.result)>
                <cfset resultNode = parsedPayload.result />
            <cfelse>
                <cfset resultNode = parsedPayload />
            </cfif>

            <cfset payload.Agency_ID = structKeyExists(resultNode, "AGENCY_ID") ? resultNode.AGENCY_ID : "" />
            <cfset payload.Patient_ID = structKeyExists(resultNode, "PATIENT_ID") ? resultNode.PATIENT_ID : "" />
            <cfset payload.Assessment_ID = structKeyExists(resultNode, "ASSESSMENT_ID") ? resultNode.ASSESSMENT_ID : "" />
            <cfset payload.Emp_ID = structKeyExists(resultNode, "Emp_ID") ? resultNode.Emp_ID : "0" />
            <cfset payload.appkey = structKeyExists(resultNode, "appkey") ? resultNode.appkey : (structKeyExists(resultNode, "appKey") ? resultNode.appKey : "") />
            <cfset payload.Website_name = structKeyExists(resultNode, "Website_name") ? resultNode.Website_name : "" />

            <cfif NOT len(trim(payload.Agency_ID)) OR NOT len(trim(payload.Patient_ID)) OR NOT len(trim(payload.Assessment_ID)) OR NOT len(trim(payload.appkey))>
                <cfthrow type="AssessmentImport" message="Missing required encrypted fields" detail="AGENCY_ID, PATIENT_ID, ASSESSMENT_ID and appKey/appkey are required in parsedJobResponse.result" />
            </cfif>

            <cfset apiResponse = importAssessmentData(
                Agency_ID = payload.Agency_ID,
                appkey = payload.appkey,
                Emp_ID = payload.Emp_ID,
                Patient_ID = payload.Patient_ID,
                Assessment_ID = payload.Assessment_ID,
                Website_name = payload.Website_name,
                filled_answers = serializeJSON(parsedPayload),
                jobStatusResponse = arguments.jobStatusResponse,
                job_id = structKeyExists(parsedPayload, "job_id") ? parsedPayload.job_id : "",
                whisper_files_id = val(arguments.whisper_files_id),
                source_channel = arguments.source_channel
            ) />

            <cfif isSimpleValue(apiResponse) AND len(trim(apiResponse)) AND isJSON(apiResponse)>
                <cfset response = deserializeJSON(apiResponse) />
            <cfelseif isStruct(apiResponse)>
                <cfset response = apiResponse />
            <cfelse>
                <cfset response = {
                    "success": false,
                    "message": "Invalid response from importAssessmentData",
                    "data": {}
                } />
            </cfif>

            <cfreturn serializeJSON(response) />

            <cfcatch type="any">
                <!--- Extract what we can from the payload for error reporting --->
                <cfset var errorAgencyID = structKeyExists(payload, "Agency_ID") ? payload.Agency_ID : 0 />
                <cfset var errorPatientID = structKeyExists(payload, "Patient_ID") ? payload.Patient_ID : 0 />
                <cfset var errorAssessmentID = structKeyExists(payload, "Assessment_ID") ? payload.Assessment_ID : 0 />
                <cfset var errorEmpID = structKeyExists(payload, "Emp_ID") ? payload.Emp_ID : 0 />
                <cfset var errorWebsiteName = structKeyExists(payload, "Website_name") ? payload.Website_name : "" />
                
                <!--- Send error email for wrapper function errors --->
                <cftry>
                    <cfset sendErrorEmail(
                        error = cfcatch,
                        agencyID = val(errorAgencyID),
                        empID = val(errorEmpID),
                        patientID = val(errorPatientID),
                        assessmentID = val(errorAssessmentID),
                        websiteName = errorWebsiteName,
                        extractedJSONString = serializeJSON(parsedPayload),
                        originalAgencyID = structKeyExists(resultNode, "AGENCY_ID") ? resultNode.AGENCY_ID : "",
                        originalEmpID = structKeyExists(resultNode, "Emp_ID") ? resultNode.Emp_ID : "0",
                        originalPatientID = structKeyExists(resultNode, "PATIENT_ID") ? resultNode.PATIENT_ID : "",
                        originalAssessmentID = structKeyExists(resultNode, "ASSESSMENT_ID") ? resultNode.ASSESSMENT_ID : "",
                        originalAppKey = structKeyExists(resultNode, "appkey") ? resultNode.appkey : (structKeyExists(resultNode, "appKey") ? resultNode.appKey : ""),
                        decryptedAgencyID = "",
                        decryptedEmpID = "",
                        decryptedPatientID = "",
                        decryptedAssessmentID = "",
                        decryptedAppKey = "",
                        insertedPatientID = 0,
                        medicationCount = 0,
                        problemCount = 0,
                        errors = [],
                        stepErrors = ["Wrapper Function Error: " & cfcatch.message],
                        currentStep = "Parsing job response in wrapper function"
                    ) />
                    <cfcatch>
                        <!--- Error email failed silently --->
                    </cfcatch>
                </cftry>
                
                <cfset response = {
                    "success": false,
                    "message": "Failed to import assessment data from job response",
                    "error": cfcatch.message,
                    "detail": cfcatch.detail,
                    "data": {}
                } />
                <cfreturn serializeJSON(response) />
            </cfcatch>
        </cftry>
    </cffunction>

    <!--- OASIS date fields F176-F199 (matches assessments.cfc update logic) --->
    <cffunction name="isAssessmentDateField" access="private" returntype="boolean" output="false">
        <cfargument name="fieldName" type="string" required="true" />
        <cfset var fieldNum = val(reReplace(arguments.fieldName, "^F", "", "all")) />
        <cfreturn reFind("^F[0-9]+$", arguments.fieldName) AND len(arguments.fieldName) EQ 4 AND fieldNum GT 175 AND fieldNum LT 200 />
    </cffunction>

  <!--- Prepare one F-column value for pAssessments UPDATE (validate column, type, length) --->
    <cffunction name="prepareAssessmentAnswerUpdate" access="private" returntype="struct" output="false">
        <cfargument name="fieldName" type="string" required="true" />
        <cfargument name="answerValue" type="string" required="true" />
        <cfargument name="columnMeta" type="struct" required="true" />

        <cfset var out = {
            ok = false,
            fieldName = uCase(trim(arguments.fieldName)),
            sqlType = "varchar",
            value = trim(arguments.answerValue),
            useNull = false,
            skipReason = "",
            truncated = false
        } />
        <cfset var maxLen = 0 />
        <cfset var dataType = "" />
        <cfset var parsedDate = "" />

        <cfif NOT structKeyExists(arguments.columnMeta, out.fieldName)>
            <cfset out.skipReason = "column_not_in_pAssessments" />
            <cfreturn out />
        </cfif>

        <cfset dataType = lCase(trim(arguments.columnMeta[out.fieldName].data_type)) />
        <cfset maxLen = val(arguments.columnMeta[out.fieldName].max_length) />

        <cfif isAssessmentDateField(out.fieldName) OR listFindNoCase("date,datetime,timestamp", dataType)>
            <cfif NOT len(out.value)>
                <cfset out.ok = true />
                <cfset out.useNull = true />
                <cfset out.sqlType = "date" />
                <cfreturn out />
            </cfif>
            <cftry>
                <cfset parsedDate = parseDateTime(out.value) />
                <cfset out.ok = true />
                <cfset out.sqlType = "date" />
                <cfset out.value = dateFormat(parsedDate, "yyyy-mm-dd") />
                <cfcatch type="any">
                    <cfset out.skipReason = "invalid_date_value" />
                </cfcatch>
            </cftry>
            <cfreturn out />
        </cfif>

        <cfif maxLen GT 0 AND len(out.value) GT maxLen>
            <cfset out.value = left(out.value, maxLen) />
            <cfset out.truncated = true />
        </cfif>

        <cfset out.ok = true />
        <cfset out.sqlType = "varchar" />
        <cfreturn out />
    </cffunction>

    <!--- Apply mapped F-column answers in validated batches; fall back to per-field updates on batch failure --->
    <cffunction name="applyMappedAnswersToAssessmentRow" access="private" returntype="struct" output="false">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="insertedAutoAssmtID" type="numeric" required="true" />
        <cfargument name="answerUpdates" type="struct" required="true" />
        <cfargument name="validColumnList" type="string" required="true" />
        <cfargument name="created_by" type="numeric" required="true" />
        <cfargument name="skippedAnswers" type="array" required="true" />
        <cfargument name="stepErrors" type="array" required="true" />
        <cfargument name="queryTrace" type="array" required="true" />
        <cfargument name="batchSize" type="numeric" required="false" default="25" />

        <cfset var result = { applied = 0, skipped = 0, batches = 0, prepared = 0 } />
        <cfset var columnMeta = {} />
        <cfset var qColumns = "" />
        <cfset var preparedUpdates = {} />
        <cfset var fieldName = "" />
        <cfset var prep = {} />
        <cfset var preparedFields = [] />
        <cfset var batchFields = [] />
        <cfset var batchStart = 0 />
        <cfset var batchEnd = 0 />
        <cfset var idx = 0 />
        <cfset var i = 0 />
        <cfset var batchNum = 0 />
        <cfset var updateResult = {} />

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

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

        <cfloop query="qColumns">
            <cfset columnMeta[uCase(trim(qColumns.COLUMN_NAME))] = {
                data_type = trim(qColumns.DATA_TYPE),
                max_length = val(qColumns.CHARACTER_MAXIMUM_LENGTH)
            } />
        </cfloop>

        <cfloop collection="#arguments.answerUpdates#" item="fieldName">
            <cfset fieldName = uCase(trim(fieldName)) />
            <cfif NOT reFind("^F[0-9]+$", fieldName)>
                <cfset arrayAppend(arguments.skippedAnswers, fieldName & ":invalid_field_name") />
                <cfset result.skipped = result.skipped + 1 />
                <cfcontinue />
            </cfif>
            <cfif NOT listFindNoCase(arguments.validColumnList, fieldName)>
                <cfset arrayAppend(arguments.skippedAnswers, fieldName & ":column_not_in_pAssessments") />
                <cfset result.skipped = result.skipped + 1 />
                <cfcontinue />
            </cfif>

            <cfset prep = prepareAssessmentAnswerUpdate(
                fieldName = fieldName,
                answerValue = "" & arguments.answerUpdates[fieldName],
                columnMeta = columnMeta
            ) />
            <cfif NOT prep.ok>
                <cfset arrayAppend(arguments.skippedAnswers, fieldName & ":" & prep.skipReason) />
                <cfset result.skipped = result.skipped + 1 />
                <cfcontinue />
            </cfif>
            <cfif prep.truncated>
                <cfset arrayAppend(arguments.stepErrors, "Answer truncated for " & fieldName & " to fit column max length") />
            </cfif>
            <cfset preparedUpdates[fieldName] = prep />
            <cfset arrayAppend(preparedFields, fieldName) />
            <cfset result.prepared = result.prepared + 1 />
        </cfloop>

        <cfif arrayLen(preparedFields) EQ 0>
            <cfreturn result />
        </cfif>

        <cftry>
            <cfset preparedFields = listToArray(listSort(arrayToList(preparedFields), "text", "asc")) />
            <cfcatch type="any"></cfcatch>
        </cftry>

        <cfloop from="1" to="#arrayLen(preparedFields)#" index="batchStart" step="#val(arguments.batchSize)#">
            <cfset batchEnd = min(batchStart + val(arguments.batchSize) - 1, arrayLen(preparedFields)) />
            <cfset batchFields = [] />
            <cfloop from="#batchStart#" to="#batchEnd#" index="idx">
                <cfset arrayAppend(batchFields, preparedFields[idx]) />
            </cfloop>
            <cfset batchNum = batchNum + 1 />

            <cftry>
                <cfset arrayAppend(arguments.queryTrace, "update_assessment_answers batch " & batchNum & " fields=" & arrayToList(batchFields, ",")) />
                <cfquery name="update_assessment_answers" datasource="#Application.DataSrc#" result="updateResult">
                    UPDATE #arguments.agency_db#.pAssessments
                    SET
                    <cfloop from="1" to="#arrayLen(batchFields)#" index="i">
                        #batchFields[i]# =
                        <cfif preparedUpdates[batchFields[i]].useNull>
                            NULL
                        <cfelseif preparedUpdates[batchFields[i]].sqlType EQ "date">
                            <cfqueryparam cfsqltype="cf_sql_date" value="#preparedUpdates[batchFields[i]].value#">
                        <cfelse>
                            <cfqueryparam cfsqltype="cf_sql_varchar" value="#preparedUpdates[batchFields[i]].value#">
                        </cfif><cfif i LT arrayLen(batchFields)>,</cfif>
                    </cfloop>,
                        Record_Mod_Date = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#">,
                        Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.created_by#">
                    WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.insertedAutoAssmtID#">
                </cfquery>
                <cfset result.applied = result.applied + arrayLen(batchFields) />
                <cfset result.batches = result.batches + 1 />
                <cfcatch type="any">
                    <cfset arrayAppend(arguments.stepErrors, "Batch update failed (batch " & batchNum & "): " & cfcatch.message & (len(trim(cfcatch.detail)) ? " | " & cfcatch.detail : "")) />
                    <cfset arrayAppend(arguments.queryTrace, "update_assessment_answers batch " & batchNum & " error: " & cfcatch.message) />
                    <cfloop from="1" to="#arrayLen(batchFields)#" index="i">
                        <cfset fieldName = batchFields[i] />
                        <cftry>
                            <cfquery datasource="#Application.DataSrc#">
                                UPDATE #arguments.agency_db#.pAssessments
                                SET
                                    #fieldName# =
                                    <cfif preparedUpdates[fieldName].useNull>
                                        NULL
                                    <cfelseif preparedUpdates[fieldName].sqlType EQ "date">
                                        <cfqueryparam cfsqltype="cf_sql_date" value="#preparedUpdates[fieldName].value#">
                                    <cfelse>
                                        <cfqueryparam cfsqltype="cf_sql_varchar" value="#preparedUpdates[fieldName].value#">
                                    </cfif>,
                                    Record_Mod_Date = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#">,
                                    Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.created_by#">
                                WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.insertedAutoAssmtID#">
                            </cfquery>
                            <cfset result.applied = result.applied + 1 />
                            <cfcatch type="any">
                                <cfset arrayAppend(arguments.skippedAnswers, fieldName & ":update_failed_" & replace(cfcatch.message, ":", "-", "all")) />
                                <cfset arrayAppend(arguments.stepErrors, "Field update failed for " & fieldName & ": " & cfcatch.message) />
                                <cfset result.skipped = result.skipped + 1 />
                            </cfcatch>
                        </cftry>
                    </cfloop>
                </cfcatch>
            </cftry>
        </cfloop>

        <cfreturn result />
    </cffunction>

    <cffunction name="importAssessmentData" access="remote" returnformat="JSON" hint="Update assessment data only from extracted AI payload">
        <cfargument name="Agency_ID" required="yes" hint="Agency ID">
        <cfargument name="appkey" required="yes" hint="appkey">
        <cfargument name="Emp_ID" required="no" default="0" hint="Emp ID (optional, defaults to 0)">
        <cfargument name="Patient_ID" required="yes" hint="Patient ID">
        <cfargument name="Assessment_ID" required="yes" hint="Assessment ID">
        <cfargument name="Website_name" required="no" default="" hint="Website name">
        <cfargument name="questions_URL" required="no" default="" hint="questions URL">
        <cfargument name="jobStatusResponse" required="no" default="" hint="Jobs API response payload or cfhttp result struct">
        <cfargument name="job_id" required="no" default="" hint="Transcript job ID">
        <cfargument name="jobs_api_url" required="no" default="https://transcriptapi.myhomecarebiz.com/jobs/" hint="Transcript jobs API URL">
        <cfargument name="filled_answers" required="yes" hint="JSON string or struct with filled answers payload">
        <cfargument name="whisper_files_id" required="no" default="0" hint="whisper_files.ID to flag AI review required after successful import">
        <cfargument name="source_channel" required="no" default="" hint="Web or Mobile App label for vitals/wounds notification email">
        
        <cfset var response = {} />
        <cfset var insertedPatientID = 0 />
        <cfset var insertedContactID = 0 />
        <cfset var insertedPayerID = 0 />
        <cfset var insertedAdmitID = 0 />
        <cfset var insertedMedicationIDs = [] />
        <cfset var insertedProblemIDs = [] />
        <cfset var medicationCount = 0 />
        <cfset var problemCount = 0 />
        <cfset var contactInserted = false />
        <cfset var payerInserted = false />
        <cfset var admitInserted = false />
        <cfset var errors = [] />
        <cfset var stepErrors = [] />
        <cfset var currentStep = "" />
        <cfset var decryptedAgencyID = "" />
        <cfset var decryptedEmpID = "" />
        <cfset var decryptedPatientID = "" />
        <cfset var decryptedAssessmentID = "" />
        <cfset var decryptedAppKey = "" />
        <cfset var extractedJSONString = "" />
        <cfset var lookup_db = "hhapowerpath" />
        <cfset var filledAnswers = {} />
        <cfset var answerUpdates = {} />
        <cfset var skippedAnswers = [] />
        <cfset var updateFields = [] />
        <cfset var fieldName = "" />
        <cfset var i = 0 />
        <cfset var qaItem = {} />
        <cfset var rawAnswer = "" />
        <cfset var normalizedAnswer = "" />
        <cfset var mapQid = "" />
        <cfset var protectedDateField = "" />
        <cfset var whisperVisitType = "" />
        <cfset var get_whisper_visit_type = "" />
        <cfset var updateResult = {} />
        <cfset var currentAssessmentReason = "" />
        <cfset var assessmentReasonForLookup = "" />
        <cfset var answerLabelMap = {} />
        <cfset var data = {} />
        <cfset var jobStatusPayloadRaw = "" />
        <cfset var jobStatusPayloadUsed = false />
        <cfset var effectiveJobID = "" />
        <cfset var sourceAutoAssmtID = 0 />
        <cfset var insertedAutoAssmtID = 0 />
        <cfset var assessmentColumnList = "" />
        <cfset var queryTrace = [] />
        <cfset var requestSnapshot = {} />
        <cfset var requestJSONString = "" />
        <cfset var responseJSONString = "" />
        <cfset var cloneAssessmentSql = "" />
        <cfset var retireSourceSql = "" />
        <cfset var activateInsertedSql = "" />
        <cfset var updateAssessmentSql = "" />
        <cfset var vitalImportResult = { updated = 0, inserted = 0, skipped = 0, applied = [], skippedItems = [] } />
        <cfset var woundImportResult = { updated = 0, inserted = 0, skipped = 0, applied = [], skippedItems = [] } />
        <cfset var filledVitals = {} />
        <cfset var vitalsArray = [] />
        <cfset var woundsArray = [] />
        <cfset var mergedVitalsWounds = {} />
        <cfset var answerApplyResult = { applied = 0, skipped = 0, batches = 0, prepared = 0 } />
        <!--- Store original encrypted values for email reporting --->
        <cfset var originalAgencyID = arguments.Agency_ID />
        <cfset var originalEmpID = arguments.Emp_ID />
        <cfset var originalPatientID = arguments.Patient_ID />
        <cfset var originalAssessmentID = arguments.Assessment_ID />
        <cfset var originalAppKey = arguments.appkey />
        
        <cftry>
            <!--- Step 1: Attempt to decrypt incoming values (handles encrypted payloads) --->
            <cfset currentStep = "Decrypting authentication parameters" />
            <cftry>
                <cfset decryptedAgencyID = maybeDecrypt(arguments.Agency_ID, true) />
                <cfset decryptedEmpID = maybeDecrypt(arguments.Emp_ID, true) />
                <cfset decryptedPatientID = maybeDecrypt(arguments.Patient_ID, true) />
                <cfset decryptedAssessmentID = maybeDecrypt(arguments.Assessment_ID, true) />
                <cfset decryptedAppKey = maybeDecrypt(arguments.appkey) />
 
                <cfif decryptedAgencyID NEQ arguments.Agency_ID>
                    <cfset arguments.Agency_ID = decryptedAgencyID />
                </cfif>
                <cfif decryptedEmpID NEQ arguments.Emp_ID>
                    <cfset arguments.Emp_ID = decryptedEmpID />
                </cfif>
                <cfif decryptedPatientID NEQ arguments.Patient_ID>
                    <cfset arguments.Patient_ID = decryptedPatientID />
                </cfif>
                <cfif decryptedAssessmentID NEQ arguments.Assessment_ID>
                    <cfset arguments.Assessment_ID = decryptedAssessmentID />
                </cfif>
                <cfif len(decryptedAppKey)>
                    <cfset arguments.appkey = decryptedAppKey />
                </cfif> 

                <!--- Normalize numeric arguments --->
                <cfset arguments.Agency_ID = val(arguments.Agency_ID) />
                <cfset arguments.Emp_ID = val(arguments.Emp_ID) />
                <cfset arguments.Patient_ID = val(arguments.Patient_ID) />
                <cfset arguments.Assessment_ID = val(arguments.Assessment_ID) />
                <cfcatch type="any">
                    <cfset arrayAppend(stepErrors, "Decryption Error: " & cfcatch.message) />
                </cfcatch>
            </cftry>

            <!--- Step 2: Validate Basic Authentication --->
            <cfset currentStep = "Validating authentication" />
            <cftry>
                <cfset var authResult = validateBasicAuth(arguments.appkey, arguments.Agency_ID, arguments.Emp_ID, arguments.Patient_ID) />
                <cfif not authResult.valid>
                    <cfset response = {
                        "success": false,
                        "message": authResult.message,
                        "data": {}
                    } />
                    <cfreturn serializeJSON(response) />
                </cfif>
                <cfcatch type="any">
                    <cfset arrayAppend(stepErrors, "Authentication Validation Error: " & cfcatch.message) />
                </cfcatch>
            </cftry>
            
            <cfset var agency_db = 'agency_' & arguments.Agency_ID   />
            <cfset var created_by = arguments.Emp_ID   />

            <!--- Step 3: Parse job status response payload --->
            <cfset currentStep = "Parsing job status response payload" />
            <cftry>
                <!--- Prefer upstream jobStatusResponse passed from call_whisper_api.cfm --->
                <cfif isStruct(arguments.jobStatusResponse)>
                    <cfif structKeyExists(arguments.jobStatusResponse, "FileContent") AND len(trim(arguments.jobStatusResponse.FileContent)) AND isJSON(arguments.jobStatusResponse.FileContent)>
                        <cfset data = deserializeJSON(arguments.jobStatusResponse.FileContent) />
                        <cfset extractedJSONString = trim(arguments.jobStatusResponse.FileContent) />
                        <cfset jobStatusPayloadUsed = true />
                    <cfelse>
                        <cfset data = arguments.jobStatusResponse />
                        <cftry>
                            <cfset extractedJSONString = serializeJSON(data) />
                            <cfset jobStatusPayloadUsed = true />
                            <cfcatch>
                                <cfset extractedJSONString = "" />
                            </cfcatch>
                        </cftry>
                    </cfif>
                <cfelseif isSimpleValue(arguments.jobStatusResponse) AND len(trim(arguments.jobStatusResponse))>
                    <cfset jobStatusPayloadRaw = trim(arguments.jobStatusResponse) />
                    <cfif isJSON(jobStatusPayloadRaw)>
                        <cfset data = deserializeJSON(jobStatusPayloadRaw) />
                        <cfset extractedJSONString = jobStatusPayloadRaw />
                        <cfset jobStatusPayloadUsed = true />
                    </cfif>
                </cfif>

                <!--- Fallback to filled_answers when no jobStatusResponse payload is provided --->
                <cfif NOT jobStatusPayloadUsed>
                    <cfif isJSON(arguments.filled_answers)>
                        <cfset data = deserializeJSON(arguments.filled_answers) />
                        <cfset extractedJSONString = arguments.filled_answers />
                    <cfelse>
                        <cfset data = arguments.filled_answers />
                        <cftry>
                            <cfset extractedJSONString = serializeJSON(data) />
                            <cfcatch>
                                <cfset extractedJSONString = "" />
                            </cfcatch>
                        </cftry>
                    </cfif>
                </cfif>

                <cfset effectiveJobID = trim(arguments.job_id) />
                <cfif NOT len(effectiveJobID) AND isStruct(data) AND structKeyExists(data, "job_id")>
                    <cfset effectiveJobID = trim("" & data.job_id) />
                <cfelseif NOT len(effectiveJobID) AND isStruct(data) AND structKeyExists(data, "result") AND isStruct(data.result) AND structKeyExists(data.result, "job_id")>
                    <cfset effectiveJobID = trim("" & data.result.job_id) />
                </cfif>
                
                <cfcatch type="any">
                    <cfset arrayAppend(stepErrors, "Payload Parsing Error: " & cfcatch.message & " | Detail: " & cfcatch.detail) />
                </cfcatch>
            </cftry>

            <cfset request.bulk_import_queries = [] />

            <cfif structKeyExists(Request, "prefix_db_lookup") AND len(trim(Request.prefix_db_lookup))>
                <cfset lookup_db = trim(Request.prefix_db_lookup) />
            </cfif>

            <!--- Step 4: Extract filled answers from payload --->
            <cfset currentStep = "Extracting filled answers from payload" />
            <cftry>
                <cfset filledAnswers = {} />
                <!--- Support payloads where filled_answers is nested under result --->
                <cfif isStruct(data) AND structKeyExists(data, "result") AND isStruct(data.result) AND structKeyExists(data.result, "filled_answers") AND isStruct(data.result.filled_answers)>
                    <cfset filledAnswers = data.result.filled_answers />
                <cfelseif isStruct(data) AND structKeyExists(data, "filled_answers") AND isStruct(data.filled_answers)>
                    <cfset filledAnswers = data.filled_answers />
                <cfelseif isStruct(data) AND NOT structKeyExists(data, "status") AND NOT structKeyExists(data, "stage") AND NOT structKeyExists(data, "result")>
                    <!--- Support payloads where the body itself is the filled_answers object --->
                    <cfset filledAnswers = data />
                </cfif>

                <!--- Merge filled_vitals, filled_wounds, vitals[], wounds[] into filledAnswers --->
                <cfset mergedVitalsWounds = mergeWhisperVitalsWoundsIntoFilledAnswers(data = data, filledAnswers = filledAnswers) />
                <cfset filledAnswers = mergedVitalsWounds.filledAnswers />
                <cfset vitalsArray = mergedVitalsWounds.vitalsArray />
                <cfset woundsArray = mergedVitalsWounds.woundsArray />
                <cfset arrayAppend(queryTrace, "vitals_wounds_merge: filled_answer_keys=" & structCount(filledAnswers) & ", vitals_array=" & arrayLen(vitalsArray) & ", wounds_array=" & arrayLen(woundsArray)) />
                <cfcatch type="any">
                    <cfset arrayAppend(stepErrors, "Answer Extraction Error: " & cfcatch.message) />
                </cfcatch>
            </cftry>
             <!--- Start transaction --->
            <cftransaction>
                <!--- Step 5: Read current active assessment to determine the source row for cloning --->
                <cfset currentStep = "Reading current active assessment source row" />
                <cftry>
                    <cfquery name="get_current_assessment" datasource="#Application.DataSrc#">
                        SELECT AutoAssmt_ID, F26, F25, F176, F177, F178
                        FROM #agency_db#.pAssessments
                        WHERE Assmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.Assessment_ID#">
                          AND Patient_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.Patient_ID#">
                          AND Status = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                        ORDER BY AutoAssmt_ID DESC
                        LIMIT 1
                    </cfquery>

                    <cfif get_current_assessment.recordCount EQ 0>
                        <cfset arrayAppend(stepErrors, "Active Assessment Lookup: No active assessment found with Status=0 for Assmt_ID=#arguments.Assessment_ID#, Patient_ID=#arguments.Patient_ID#") />
                        <cfthrow type="AssessmentImport" message="No active assessment row found" detail="Assmt_ID #arguments.Assessment_ID# with Status=0 was not found for patient #arguments.Patient_ID#" />
                    </cfif>

                    <cfset sourceAutoAssmtID = val(get_current_assessment.AutoAssmt_ID[1]) />
                    <cfset currentAssessmentReason = trim("" & get_current_assessment.F26[1]) />
                    <cfif len(currentAssessmentReason) EQ 0>
                        <cfset arrayAppend(stepErrors, "Assessment Reason (F26) Empty: F26 column is empty for Assmt_ID=#arguments.Assessment_ID#") />
                        <cfthrow type="AssessmentImport" message="Assessment reason (F26) is empty" detail="Cannot map questions without F26 for Assmt_ID #arguments.Assessment_ID#" />
                    </cfif>
                    <!--- A Recert visit's answers must only go into a Recert (never the SOC the visit is scheduled against) --->
                    <cfif val(arguments.whisper_files_id) GT 0 AND Left(currentAssessmentReason, 1) NEQ "4">
                        <cftry>
                            <cfquery name="get_whisper_visit_type" datasource="#Application.DataSrc#">
                                SELECT s.Visit_Type
                                FROM #agency_db#.whisper_files wf
                                INNER JOIN #agency_db#.pSchedules s
                                    ON s.Schedule_ID = wf.Schedule_ID
                                   AND s.Status = 0
                                WHERE wf.ID = <cfqueryparam cfsqltype="cf_sql_bigint" value="#val(arguments.whisper_files_id)#">
                                LIMIT 1
                            </cfquery>
                            <cfset whisperVisitType = trim("" & get_whisper_visit_type.Visit_Type) />
                            <cfcatch type="any">
                                <cfset whisperVisitType = "" />
                            </cfcatch>
                        </cftry>
                        <cfif Left(whisperVisitType, 1) EQ "4">
                            <cfset arrayAppend(stepErrors, "Recert visit (#whisperVisitType#) targeted non-Recert Assmt_ID=#arguments.Assessment_ID# (#currentAssessmentReason#); answers not applied") />
                            <cfthrow type="AssessmentImport" message="Recert visit answers not applied" detail="Visit type #whisperVisitType# does not match assessment #arguments.Assessment_ID# (#currentAssessmentReason#). No Recert assessment was found for this visit." />
                        </cfif>
                    </cfif>
                    <cfif sourceAutoAssmtID LTE 0>
                        <cfset arrayAppend(stepErrors, "Active Assessment Lookup: AutoAssmt_ID is missing for Assmt_ID=#arguments.Assessment_ID#") />
                        <cfthrow type="AssessmentImport" message="Active assessment source row is missing AutoAssmt_ID" detail="Cannot create an insert-based assessment version without AutoAssmt_ID for Assmt_ID #arguments.Assessment_ID#" />
                    </cfif>
                    <cfcatch type="any">
                        <cfif arrayLen(stepErrors) EQ 0>
                            <cfset arrayAppend(stepErrors, "Current Assessment Lookup Error: " & cfcatch.message & " | Detail: " & cfcatch.detail & " | agency_db=" & agency_db & ", Assmt_ID=" & arguments.Assessment_ID & ", Patient_ID=" & arguments.Patient_ID) />
                        </cfif>
                        <!--- Preserve original exception (bare cfthrow can yield empty message/detail in CF2023) --->
                        <cfthrow type="#cfcatch.type#" message="#len(trim(cfcatch.message)) ? cfcatch.message : (arrayLen(stepErrors) ? stepErrors[arrayLen(stepErrors)] : 'Assessment source row lookup failed')#" detail="#len(trim(cfcatch.detail)) ? cfcatch.detail : ('agency_db=' & agency_db & ', Assmt_ID=' & arguments.Assessment_ID & ', Patient_ID=' & arguments.Patient_ID)#" />
                    </cfcatch>
                </cftry>

                <!--- Step 6: Build the clone column list for insert-based versioning --->
                <cfset currentStep = "Loading assessment column list for insert-based versioning" />
                <cftry>
                    <cfquery name="get_assessment_columns" datasource="#Application.DataSrc#">
                        SELECT COLUMN_NAME
                        FROM INFORMATION_SCHEMA.COLUMNS
                        WHERE TABLE_SCHEMA = <cfqueryparam cfsqltype="cf_sql_varchar" value="#agency_db#">
                          AND TABLE_NAME = <cfqueryparam cfsqltype="cf_sql_varchar" value="pAssessments">
                          AND COLUMN_NAME NOT IN (
                              <cfqueryparam cfsqltype="cf_sql_varchar" value="AutoAssmt_ID">,
                              <cfqueryparam cfsqltype="cf_sql_varchar" value="record_timestamp">
                          )
                        ORDER BY ORDINAL_POSITION
                    </cfquery>
                    <cfset assessmentColumnList = valueList(get_assessment_columns.COLUMN_NAME) />
                    <cfif NOT len(trim(assessmentColumnList))>
                        <cfset arrayAppend(stepErrors, "Assessment Clone Column Lookup Error: No insertable columns found for #agency_db#.pAssessments") />
                        <cfthrow type="AssessmentImport" message="Could not determine assessment clone columns" detail="INFORMATION_SCHEMA returned no insertable columns for #agency_db#.pAssessments" />
                    </cfif>
                    <cfcatch type="any">
                        <cfset arrayAppend(stepErrors, "Assessment Clone Column Error: " & cfcatch.message & " | Failed to load INFORMATION_SCHEMA columns for pAssessments") />
                        <cfthrow>
                    </cfcatch>
                </cftry>

                <!--- Step 7: Clone the active assessment row so this import is insert-based --->
                <cfset currentStep = "Cloning active assessment row into a new inserted version" />
                <cftry>
                    <cfset cloneAssessmentSql = "INSERT INTO #agency_db#.pAssessments (#assessmentColumnList#) SELECT #assessmentColumnList# FROM #agency_db#.pAssessments WHERE AutoAssmt_ID = #sourceAutoAssmtID# LIMIT 1" />
                    <cfset arrayAppend(queryTrace, cloneAssessmentSql) />
                    <cfquery name="clone_assessment_row" datasource="#Application.DataSrc#">
                        INSERT INTO #agency_db#.pAssessments
                        (#preserveSingleQuotes(assessmentColumnList)#)
                        SELECT #preserveSingleQuotes(assessmentColumnList)#
                        FROM #agency_db#.pAssessments
                        WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#sourceAutoAssmtID#">
                        LIMIT 1
                    </cfquery>

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

                    <cfset insertedAutoAssmtID = val(get_inserted_assessment_row.LastInsertID) />
                    <cfif insertedAutoAssmtID LTE 0>
                        <cfset arrayAppend(stepErrors, "Assessment Clone Error: INSERT succeeded but LAST_INSERT_ID() returned 0 for Assmt_ID=#arguments.Assessment_ID#") />
                        <cfthrow type="AssessmentImport" message="Failed to determine inserted assessment row" detail="LAST_INSERT_ID() returned 0 after cloning AutoAssmt_ID #sourceAutoAssmtID#" />
                    </cfif>

                    <cfset retireSourceSql = "UPDATE #agency_db#.pAssessments SET Status = 2, Record_Mod_Date = '#DateTimeFormat(Now(), 'yyyy-mm-dd HH:nn:ss')#', Record_Mod_By = #created_by# WHERE AutoAssmt_ID = #sourceAutoAssmtID#" />
                    <cfset arrayAppend(queryTrace, retireSourceSql) />
                    <cfquery name="retire_source_assessment" datasource="#Application.DataSrc#">
                        UPDATE #agency_db#.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="#created_by#">
                        WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#sourceAutoAssmtID#">
                    </cfquery>

                    <cfset activateInsertedSql = "UPDATE #agency_db#.pAssessments SET Status = 0, Record_Creation_Date = '#DateTimeFormat(Now(), 'yyyy-mm-dd HH:nn:ss')#', Record_Created_By = #created_by#, Record_Mod_Date = '#DateTimeFormat(Now(), 'yyyy-mm-dd HH:nn:ss')#', Record_Mod_By = #created_by# WHERE AutoAssmt_ID = #insertedAutoAssmtID#" />
                    <cfset arrayAppend(queryTrace, activateInsertedSql) />
                    <cfquery name="activate_inserted_assessment" datasource="#Application.DataSrc#">
                        UPDATE #agency_db#.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="#created_by#">,
                            Record_Mod_Date = <cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#">,
                            Record_Mod_By = <cfqueryparam cfsqltype="cf_sql_integer" value="#created_by#">
                        WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#insertedAutoAssmtID#">
                    </cfquery>
                    <cfcatch type="any">
                        <cfset arrayAppend(stepErrors, "Assessment Clone Error: " & cfcatch.message & " | Failed to clone active assessment row AutoAssmt_ID=#sourceAutoAssmtID#") />
                        <cfthrow>
                    </cfcatch>
                </cftry>

                <!--- Step 8: Verify the inserted assessment row is active and retrieve assessment reason --->
                <cfset currentStep = "Verifying inserted assessment version and retrieving assessment reason" />
                <cftry>
                    <cfquery name="get_active_reason" datasource="#Application.DataSrc#">
                        SELECT F26
                        FROM #agency_db#.pAssessments
                        WHERE AutoAssmt_ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#insertedAutoAssmtID#">
                    </cfquery>

                    <cfif get_active_reason.recordCount EQ 0>
                        <cfset arrayAppend(stepErrors, "Inserted Version Verification Failed: Could not find cloned assessment row AutoAssmt_ID=#insertedAutoAssmtID#") />
                        <cfthrow type="AssessmentImport" message="Unable to verify inserted assessment row" detail="AutoAssmt_ID #insertedAutoAssmtID# was not found after cloning Assmt_ID #arguments.Assessment_ID#" />
                    </cfif>

                    <cfset assessmentReasonForLookup = trim("" & get_active_reason.F26[1]) />
                    <cfcatch type="any">
                        <cfif arrayLen(stepErrors) EQ 0>
                            <cfset arrayAppend(stepErrors, "Active Reason Lookup Error: " & cfcatch.message) />
                        </cfif>
                        <cfthrow>
                    </cfcatch>
                </cftry>

                <!--- Step 9: Build lookup map: question_id -> answer_label(Fxx) using F26 --->
                <cfset currentStep = "Building question ID to F-column answer label mapping (Assessment Reason: #assessmentReasonForLookup#)" />
                <cftry>
                    <!--- #10641: PATTII lookup tables when feature flag is ON --->
                    <cfif NOT structKeyExists(Request, "assessment_questions_lookup")>
                        <cfinclude template="../../Application_assessment_lookup_PATTII.cfm" />
                    </cfif>
                    <cfquery name="get_question_map" datasource="#Application.DataSrc#">
                        SELECT ques_id, answer_label
                        FROM #lookup_db#.#Request.assessment_questions_lookup#
                        WHERE assessment_reason = <cfqueryparam cfsqltype="cf_sql_varchar" value="#assessmentReasonForLookup#">
                          AND Deleted = <cfqueryparam cfsqltype="cf_sql_integer" value="0">
                          AND answer_label IS NOT NULL
                          AND trim(answer_label) != ''
                    </cfquery>

                    <cfloop query="get_question_map">
                        <cfset mapQid = trim("" & get_question_map.ques_id) />
                        <cfif len(mapQid)>
                            <cfset answerLabelMap[mapQid] = uCase(trim("" & get_question_map.answer_label)) />
                        </cfif>
                    </cfloop>
                    
                    <cfif structCount(answerLabelMap) EQ 0>
                        <cfset arrayAppend(stepErrors, "Question Lookup Warning: No questions found in #Request.assessment_questions_lookup# for assessment_reason=#assessmentReasonForLookup#") />
                    </cfif>
                    <cfcatch type="any">
                        <cfset arrayAppend(stepErrors, "Question Mapping Error: " & cfcatch.message & " | Failed to query #Request.assessment_questions_lookup#") />
                        <cfthrow>
                    </cfcatch>
                </cftry>

                <!--- Step 10: Convert incoming answers into F-column updates --->
                <cfset currentStep = "Mapping filled answers to F-columns" />
                <cftry>
                    <cfset var shouldUpdateField = false />
                    <cfloop collection="#filledAnswers#" item="mapQid">
                        <cfset qaItem = filledAnswers[mapQid] />
                        <cfif isVitalFilledAnswerKey(mapQid) OR isWoundFilledAnswerKey(mapQid)>
                            <cfcontinue />
                        </cfif>
                        <cfif NOT isStruct(qaItem) OR NOT structKeyExists(qaItem, "answer")>
                            <cfset arrayAppend(skippedAnswers, mapQid & ":missing_answer_field_in_payload") />
                            <cfcontinue>
                        </cfif>

                        <!--- Resolve F-column: key F242, payload answer_label, or DB lookup map --->
                        <cfset fieldName = "" />
                        <cfif isAssessmentFilledAnswerKey(mapQid)>
                            <cfset fieldName = uCase(trim(mapQid)) />
                        <cfelseif isStruct(qaItem) AND structKeyExists(qaItem, "answer_label") AND len(trim("" & qaItem.answer_label))>
                            <cfset fieldName = uCase(trim("" & qaItem.answer_label)) />
                        <cfelseif structKeyExists(answerLabelMap, mapQid)>
                            <cfset fieldName = answerLabelMap[mapQid] />
                        </cfif>

                        <cfif NOT reFind("^F[0-9]+$", fieldName)>
                            <cfset arrayAppend(skippedAnswers, mapQid & ":invalid_or_missing_answer_label_" & fieldName) />
                            <cfcontinue>
                        </cfif>

                        <cfset rawAnswer = qaItem.answer />
                        <cfset shouldUpdateField = false />

                        <!--- Skip null or empty values - no update intent --->
                        <cfif isNull(rawAnswer)>
                            <cfcontinue>
                        </cfif>

                        <cfif isArray(rawAnswer)>
                            <cfif arrayLen(rawAnswer) EQ 0>
                                <cfcontinue>
                            </cfif>
                            <cfset normalizedAnswer = normalizeWhisperFilledAnswerValue(rawAnswer) />
                            <cfset shouldUpdateField = len(normalizedAnswer) GT 0 />
                        <cfelseif isSimpleValue(rawAnswer)>
                            <cfset normalizedAnswer = trim("" & rawAnswer) />
                            <cfif NOT len(normalizedAnswer)>
                                <cfcontinue>
                            </cfif>
                            <cfset shouldUpdateField = true />
                        <cfelse>
                            <cfset normalizedAnswer = serializeJSON(rawAnswer) />
                            <cfset shouldUpdateField = true />
                        </cfif>

                        <cfif shouldUpdateField>
                            <cfset answerUpdates[fieldName] = normalizedAnswer />
                        </cfif>
                    </cfloop>
                    <cfcatch type="any">
                        <cfset arrayAppend(stepErrors, "Answer Mapping Error: " & cfcatch.message & " | Error while processing filled_answers iteration") />
                        <cfthrow>
                    </cfcatch>
                </cftry>

                <!--- Step 11: Apply mapped answers into the newly inserted assessment row --->
                <cfset currentStep = "Updating inserted assessment record with mapped answers (#structCount(answerUpdates)# F-columns)" />
                <cftry>
                    <!--- M0090 / cert dates are never overwritten by AI: locked once the OASIS is completed or already set
                         (a Recert visit scheduled against the SOC Assmt_ID must not move the SOC's M0090) --->
                    <cfloop list="F176,F177,F178" index="protectedDateField">
                        <cfif structKeyExists(answerUpdates, protectedDateField)
                            AND (Val(get_current_assessment.F25[1]) EQ 1 OR IsDate(get_current_assessment[protectedDateField][1]))>
                            <cfset structDelete(answerUpdates, protectedDateField) />
                            <cfset arrayAppend(skippedAnswers, protectedDateField & ":protected_date_already_set") />
                        </cfif>
                    </cfloop>
                    <cfif structCount(answerUpdates) GT 0>
                        <cfset answerApplyResult = applyMappedAnswersToAssessmentRow(
                            agency_db = agency_db,
                            insertedAutoAssmtID = insertedAutoAssmtID,
                            answerUpdates = answerUpdates,
                            validColumnList = assessmentColumnList,
                            created_by = created_by,
                            skippedAnswers = skippedAnswers,
                            stepErrors = stepErrors,
                            queryTrace = queryTrace
                        ) />
                        <cfset arrayAppend(queryTrace, "update_assessment_answers summary: prepared=" & answerApplyResult.prepared & ", applied=" & answerApplyResult.applied & ", skipped=" & answerApplyResult.skipped & ", batches=" & answerApplyResult.batches) />
                        <cfif answerApplyResult.applied EQ 0 AND answerApplyResult.prepared GT 0>
                            <cfset arrayAppend(stepErrors, "Assessment Update Warning: No F-columns were applied although " & answerApplyResult.prepared & " were prepared") />
                        </cfif>
                    <cfelse>
                        <cfset arrayAppend(queryTrace, "update_assessment_answers skipped: no mapped answers found. filled_answers keys may not map to assessment_questions_lookup.answer_label for assessment_reason='" & assessmentReasonForLookup & "'") />
                    </cfif>
                    <cfcatch type="any">
                        <cfset arrayAppend(stepErrors, "Database Update Error: " & cfcatch.message & (len(trim(cfcatch.detail)) ? " | Detail: " & cfcatch.detail : "") & " | Failed to update cloned pAssessments row AutoAssmt_ID=#insertedAutoAssmtID#") />
                        <cfset arrayAppend(queryTrace, "update_assessment_answers error: " & cfcatch.message & " | detail: " & cfcatch.detail) />
                        <cfthrow>
                    </cfcatch>
                </cftry>

                <!--- Step 12-13: Apply vitals and wounds to pWoundsVitals (same as mobile saveAssessmentWoundsVitals) --->
                <cfset currentStep = "Updating pWoundsVitals from Whisper vitals and wounds payload" />
                <cftry>
                    <cfset var vitalsWoundsApplyResult = applyWhisperVitalsWoundsToPWoundsVitals(
                        agency_db = agency_db,
                        assmt_id = arguments.Assessment_ID,
                        filledAnswers = filledAnswers,
                        woundsArray = woundsArray,
                        created_by = created_by,
                        skippedAnswers = skippedAnswers,
                        queryTrace = queryTrace,
                        lookup_db = lookup_db
                    ) />
                    <cfset vitalImportResult = vitalsWoundsApplyResult.vitals />
                    <cfset woundImportResult = vitalsWoundsApplyResult.wounds />
                    <cfset arrayAppend(queryTrace, "vitals_import summary: updated=" & vitalImportResult.updated & ", inserted=" & vitalImportResult.inserted & ", skipped=" & vitalImportResult.skipped) />
                    <cfset arrayAppend(queryTrace, "wounds_import summary: updated=" & woundImportResult.updated & ", inserted=" & woundImportResult.inserted & ", skipped=" & woundImportResult.skipped) />
                    <cfcatch type="any">
                        <cfset arrayAppend(stepErrors, "Vitals/Wounds Import Error: " & cfcatch.message & " | Failed to update pWoundsVitals for Assmt_ID=#arguments.Assessment_ID#") />
                        <cfthrow>
                    </cfcatch>
                </cftry>
            </cftransaction>
            <!--- Transaction auto-commits here if successful --->

            <cftry>
                <cfset markWhisperFileAIReviewRequired(
                    agency_db = agency_db,
                    assessment_id = arguments.Assessment_ID,
                    whisper_files_id = val(arguments.whisper_files_id)
                ) />
                <cfcatch type="any"></cfcatch>
            </cftry>
            
            <!--- Build success response --->
            <cfset response = {
                "success": true,
                "message": "Assessment data inserted successfully",
                "data": {
                    "assessment_id": arguments.Assessment_ID,
                    "source_auto_assmt_id": sourceAutoAssmtID,
                    "inserted_auto_assmt_id": insertedAutoAssmtID,
                    "errors": errors,
                    "step_errors": stepErrors,
                    "assessment_reason": assessmentReasonForLookup,
                    "mapped_answers": answerApplyResult.applied,
                    "mapped_answers_prepared": answerApplyResult.prepared,
                    "mapped_answers_skipped": answerApplyResult.skipped,
                    "vitals": vitalImportResult,
                    "wounds": woundImportResult,
                    "vitals_updated": vitalImportResult.updated,
                    "vitals_inserted": vitalImportResult.inserted,
                    "vitals_skipped": vitalImportResult.skipped,
                    "wounds_updated": woundImportResult.updated,
                    "wounds_inserted": woundImportResult.inserted,
                    "wounds_skipped": woundImportResult.skipped,
                    "skipped_answers": skippedAnswers,
                    "job_id": effectiveJobID,
                    "job_status_response_used": jobStatusPayloadUsed,
                    "debug_info": {
                        "datasource": Application.DataSrc,
                        "agency_db": agency_db,
                        "transaction_completed": true,
                        "last_step": currentStep
                    }
                }
            } />

            <cfset requestSnapshot = {
                "method": "importAssessmentData",
                "request": {
                    "Agency_ID": arguments.Agency_ID,
                    "Emp_ID": arguments.Emp_ID,
                    "Patient_ID": arguments.Patient_ID,
                    "Assessment_ID": arguments.Assessment_ID,
                    "Website_name": arguments.Website_name,
                    "job_id": effectiveJobID,
                    "job_status_response_used": jobStatusPayloadUsed,
                    "filled_answers": filledAnswers,
                    "source_auto_assmt_id": sourceAutoAssmtID,
                    "inserted_auto_assmt_id": insertedAutoAssmtID,
                    "mapped_answer_updates": answerUpdates
                }
            } />
            <cfset requestJSONString = serializeJSON(requestSnapshot) />
            <cfset responseJSONString = serializeJSON(response) />

            <cfset sendImportAssessmentTraceEmail(
                agencyID = arguments.Agency_ID,
                empID = arguments.Emp_ID,
                patientID = arguments.Patient_ID,
                assessmentID = arguments.Assessment_ID,
                websiteName = arguments.Website_name,
                requestJSONString = requestJSONString,
                responseJSONString = responseJSONString,
                queryTrace = queryTrace,
                currentStep = currentStep,
                stepErrors = stepErrors,
                jobID = effectiveJobID,
                filledAnswers = filledAnswers,
                assessmentReason = assessmentReasonForLookup,
                lookup_db = lookup_db,
                agency_db = agency_db,
                answerLabelMap = answerLabelMap,
                answerUpdates = answerUpdates,
                skippedAnswers = skippedAnswers,
                vitalImportResult = vitalImportResult,
                woundImportResult = woundImportResult
            ) />

            <cfset sendWhisperVitalsWoundsUpdateEmail(
                agencyID = arguments.Agency_ID,
                empID = arguments.Emp_ID,
                patientID = arguments.Patient_ID,
                assessmentID = arguments.Assessment_ID,
                websiteName = arguments.Website_name,
                sourceChannel = arguments.source_channel,
                jobID = effectiveJobID,
                whisperFilesId = val(arguments.whisper_files_id),
                success = true,
                message = structKeyExists(response, "message") ? response.message : "",
                vitalImportResult = vitalImportResult,
                woundImportResult = woundImportResult,
                requestJSONString = requestJSONString,
                responseJSONString = responseJSONString
            ) />

            <!--- <cfset sendAssessmentSuccessEmail(
                agencyID = arguments.Agency_ID,
                empID = arguments.Emp_ID,
                patientID = arguments.Patient_ID,
                assessmentID = arguments.Assessment_ID,
                websiteName = arguments.Website_name,
                assessmentReason = assessmentReasonForLookup,
                mappedAnswerCount = structCount(answerUpdates),
                skippedAnswers = skippedAnswers,
                stepErrors = stepErrors,
                jobID = effectiveJobID,
                extractedJSONString = extractedJSONString,
                agency_db = agency_db
            ) /> --->
            
            <cfcatch type="any">
                <!--- Send query dump email (even on error) --->
                <cfif structKeyExists(this, "sendQueryDumpEmail")>
                    <cfset sendQueryDumpEmail(
                        agencyID = arguments.Agency_ID,
                        empID = arguments.Emp_ID,
                        patientID = arguments.Patient_ID,
                        agency_db = agency_db
                    ) />
                </cfif>
                
                <!--- Send error email notification with step-by-step details --->
                <cfset sendErrorEmail(
                    error = cfcatch,
                    agencyID = arguments.Agency_ID,
                    empID = arguments.Emp_ID,
                    patientID = arguments.Patient_ID,
                    assessmentID = arguments.Assessment_ID,
                    websiteName = arguments.Website_name,
                    extractedJSONString = extractedJSONString,
                    originalAgencyID = originalAgencyID,
                    originalEmpID = originalEmpID,
                    originalPatientID = originalPatientID,
                    originalAssessmentID = originalAssessmentID,
                    originalAppKey = originalAppKey,
                    decryptedAgencyID = decryptedAgencyID,
                    decryptedEmpID = decryptedEmpID,
                    decryptedPatientID = decryptedPatientID,
                    decryptedAssessmentID = decryptedAssessmentID,
                    decryptedAppKey = decryptedAppKey,
                    insertedPatientID = insertedPatientID,
                    medicationCount = medicationCount,
                    problemCount = problemCount,
                    errors = errors,
                    stepErrors = stepErrors,
                    currentStep = currentStep
                ) />
                
                <cfset response = {
                    "success": false,
                    "message": "Failed to update assessment data",
                    "error": cfcatch.message,
                    "detail": cfcatch.detail,
                    "failed_at_step": currentStep,
                    "step_errors": stepErrors,
                    "data": {
                        "assessment_id": arguments.Assessment_ID,
                        "errors": errors
                    }
                } />

                <cfset requestSnapshot = {
                    "method": "importAssessmentData",
                    "request": {
                        "Agency_ID": arguments.Agency_ID,
                        "Emp_ID": arguments.Emp_ID,
                        "Patient_ID": arguments.Patient_ID,
                        "Assessment_ID": arguments.Assessment_ID,
                        "Website_name": arguments.Website_name,
                        "job_id": effectiveJobID,
                        "job_status_response_used": jobStatusPayloadUsed,
                        "filled_answers": filledAnswers,
                        "source_auto_assmt_id": sourceAutoAssmtID,
                        "inserted_auto_assmt_id": insertedAutoAssmtID,
                        "mapped_answer_updates": answerUpdates
                    }
                } />
                <cfset requestJSONString = serializeJSON(requestSnapshot) />
                <cfset responseJSONString = serializeJSON(response) />

                <cfset sendImportAssessmentTraceEmail(
                    agencyID = arguments.Agency_ID,
                    empID = arguments.Emp_ID,
                    patientID = arguments.Patient_ID,
                    assessmentID = arguments.Assessment_ID,
                    websiteName = arguments.Website_name,
                    requestJSONString = requestJSONString,
                    responseJSONString = responseJSONString,
                    queryTrace = queryTrace,
                    currentStep = currentStep,
                    stepErrors = stepErrors,
                    jobID = effectiveJobID,
                    filledAnswers = filledAnswers,
                    assessmentReason = assessmentReasonForLookup,
                    lookup_db = lookup_db,
                    agency_db = agency_db,
                    answerLabelMap = answerLabelMap,
                    answerUpdates = answerUpdates,
                    skippedAnswers = skippedAnswers,
                    vitalImportResult = vitalImportResult,
                    woundImportResult = woundImportResult
                ) />

                <cfset sendWhisperVitalsWoundsUpdateEmail(
                    agencyID = arguments.Agency_ID,
                    empID = arguments.Emp_ID,
                    patientID = arguments.Patient_ID,
                    assessmentID = arguments.Assessment_ID,
                    websiteName = arguments.Website_name,
                    sourceChannel = arguments.source_channel,
                    jobID = effectiveJobID,
                    whisperFilesId = val(arguments.whisper_files_id),
                    success = false,
                    message = structKeyExists(response, "message") ? response.message : cfcatch.message,
                    vitalImportResult = vitalImportResult,
                    woundImportResult = woundImportResult,
                    requestJSONString = requestJSONString,
                    responseJSONString = responseJSONString
                ) />
            </cfcatch>
        </cftry>
        
        <cfreturn serializeJSON(response) />
    </cffunction>

    <cffunction name="fetchTranscriptApiToken" access="public" returntype="struct" output="false"
        hint="Authenticate with transcriptapi.myhomecarebiz.com and return bearer token.">
        <cfset var out = { ok = false, accessToken = "", tokenType = "Bearer", statusCode = "", responseBody = "" } />
        <cfset var tokenURL = "https://transcriptapi.myhomecarebiz.com/auth/token" />
        <cfset var tokenResponse = "" />
        <cfset var tokenData = {} />

        <cfhttp url="#tokenURL#" method="post" timeout="60" result="tokenResponse" throwonerror="false">
            <cfhttpparam type="header" name="accept" value="application/json">
            <cfhttpparam type="header" name="Content-Type" value="application/x-www-form-urlencoded">
            <cfhttpparam type="formField" name="username" value="superman">
            <cfhttpparam type="formField" name="password" value="superman">
        </cfhttp>

        <cfset out.statusCode = tokenResponse.StatusCode />
        <cfset out.responseBody = trim(tokenResponse.FileContent) />

        <cfif Left(out.statusCode, 3) EQ "200" AND len(out.responseBody) AND isJSON(out.responseBody)>
            <cfset tokenData = deserializeJSON(out.responseBody) />
            <cfif structKeyExists(tokenData, "access_token")>
                <cfset out.accessToken = trim(tokenData.access_token) />
            <cfelseif structKeyExists(tokenData, "accessToken")>
                <cfset out.accessToken = trim(tokenData.accessToken) />
            </cfif>
            <cfif structKeyExists(tokenData, "token_type") AND len(trim(tokenData.token_type))>
                <cfset out.tokenType = trim(tokenData.token_type) />
            <cfelseif structKeyExists(tokenData, "tokenType") AND len(trim(tokenData.tokenType))>
                <cfset out.tokenType = trim(tokenData.tokenType) />
            </cfif>
        </cfif>

        <cfset out.ok = len(out.accessToken) GT 0 />
        <cfreturn out />
    </cffunction>

    <cffunction name="checkWhisperJobStatusForAgency" access="public" returntype="struct" output="false"
        hint="Poll transcript job status and import assessment data when completed (shared by web/mobile).">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="whisper_files_id" type="numeric" required="false" default="0" />
        <cfargument name="whisper_job_id" type="string" required="false" default="" />
        <cfargument name="source_channel" type="string" required="false" default="" />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var getWhipserJob = "" />
        <cfset var token = {} />
        <cfset var jobStatusUrl = "" />
        <cfset var jobStatusResponse = "" />
        <cfset var jobHttpStatus = "" />
        <cfset var jobResponseBody = "" />
        <cfset var parsedJobResponse = {} />
        <cfset var latestJobStatus = "" />
        <cfset var importAssessmentResult = {} />

        <cfif val(arguments.whisper_files_id) LTE 0 AND NOT len(trim(arguments.whisper_job_id))>
            <cfset response.message = "Provide whisper_files_id or whisper_job_id to check job status." />
            <cfreturn response />
        </cfif>

        <cfquery name="getWhipserJob" datasource="#Application.DataSrc#">
            SELECT ID, whisper_files_id, job_id, request_id, job_status, Patient_ID, Assmt_ID
            FROM #arguments.agency_db#.whipser_jobs
            WHERE Status = 0
            <cfif len(trim(arguments.whisper_job_id))>
                AND job_id = <cfqueryparam value="#trim(arguments.whisper_job_id)#" cfsqltype="cf_sql_varchar">
            <cfelseif val(arguments.whisper_files_id) GT 0>
                AND whisper_files_id = <cfqueryparam value="#val(arguments.whisper_files_id)#" cfsqltype="cf_sql_bigint">
            </cfif>
            ORDER BY ID DESC
            LIMIT 1
        </cfquery>

        <cfif getWhipserJob.recordCount EQ 0>
            <cfset response.message = "Whisper job record not found." />
            <cfreturn response />
        </cfif>

        <cfset token = fetchTranscriptApiToken() />
        <cfif NOT token.ok>
            <cfset response.message = "Whisper authentication failed (token unavailable)." />
            <cfset response.data = { token_status = token.statusCode, token_response = token.responseBody } />
            <cfreturn response />
        </cfif>

        <cfset jobStatusUrl = "https://transcriptapi.myhomecarebiz.com/jobs/" & urlEncodedFormat(getWhipserJob.job_id) />
        <cfhttp url="#jobStatusUrl#" method="get" timeout="120" result="jobStatusResponse" throwonerror="false">
            <cfhttpparam type="header" name="Authorization" value="#token.tokenType# #token.accessToken#">
            <cfhttpparam type="header" name="accept" value="application/json">
        </cfhttp>

        <cfset jobHttpStatus = jobStatusResponse.StatusCode />
        <cfset jobResponseBody = trim(jobStatusResponse.FileContent) />
        <cfif len(jobResponseBody) AND isJSON(jobResponseBody)>
            <cfset parsedJobResponse = deserializeJSON(jobResponseBody) />
        </cfif>

        <cfif isStruct(parsedJobResponse)>
            <cfif structKeyExists(parsedJobResponse, "status")>
                <cfset latestJobStatus = trim(parsedJobResponse.status & "") />
            <cfelseif structKeyExists(parsedJobResponse, "job_status")>
                <cfset latestJobStatus = trim(parsedJobResponse.job_status & "") />
            </cfif>
        </cfif>
        <cfif NOT len(latestJobStatus)>
            <cfset latestJobStatus = Left(jobHttpStatus, 3) EQ "200" ? "running" : "failed" />
        </cfif>

        <cfif Left(jobHttpStatus, 3) EQ "200"
            AND isStruct(parsedJobResponse)
            AND structKeyExists(parsedJobResponse, "status")
            AND lCase(trim(parsedJobResponse.status & "")) EQ "success"
            AND structKeyExists(parsedJobResponse, "stage")
            AND lCase(trim(parsedJobResponse.stage & "")) EQ "completed"
            AND structKeyExists(parsedJobResponse, "result")
            AND isStruct(parsedJobResponse.result)>
            <cftry>
                <cfset importAssessmentResult = importAssessmentDataFromJobResponse(
                    parsedJobResponse = parsedJobResponse,
                    jobStatusResponse = jobStatusResponse,
                    whisper_files_id = val(getWhipserJob.whisper_files_id),
                    source_channel = arguments.source_channel
                ) />
                <cfif isSimpleValue(importAssessmentResult) AND len(trim(importAssessmentResult)) AND isJSON(importAssessmentResult)>
                    <cfset importAssessmentResult = deserializeJSON(importAssessmentResult) />
                </cfif>
                <cfcatch type="any">
                    <cfset importAssessmentResult = {
                        success = false,
                        message = "Assessment import invocation failed",
                        error = cfcatch.message,
                        detail = cfcatch.detail
                    } />
                </cfcatch>
            </cftry>
        </cfif>

        <cfquery datasource="#Application.DataSrc#">
            UPDATE #arguments.agency_db#.whipser_jobs
            SET job_status = <cfqueryparam value="#latestJobStatus#" cfsqltype="cf_sql_varchar">,
                status_response = <cfqueryparam value="#jobResponseBody#" cfsqltype="cf_sql_longvarchar">,
                Last_Checked = NOW()
            WHERE ID = <cfqueryparam value="#getWhipserJob.ID#" cfsqltype="cf_sql_bigint">
        </cfquery>

        <cfset response.success = Left(jobHttpStatus, 3) EQ "200" />
        <cfset response.message = "Whisper job status retrieved." />
        <cfset response.data = {
            whipser_job_row_id = getWhipserJob.ID,
            whisper_files_id = getWhipserJob.whisper_files_id,
            job_id = getWhipserJob.job_id,
            request_id = getWhipserJob.request_id,
            job_status = latestJobStatus,
            statusCode = jobHttpStatus,
            job_response = parsedJobResponse,
            assessment_import = importAssessmentResult,
            raw_response = jobResponseBody
        } />
        <cfreturn response />
    </cffunction>

    <cffunction name="processWhisperFileForAgency" access="public" returntype="struct" output="false"
        hint="Submit whisper_files row to transcript /process API (shared by web/mobile).">
        <cfargument name="agency_db" type="string" required="true" />
        <cfargument name="agency_id" type="string" required="true" />
        <cfargument name="whisper_file_id" type="numeric" required="true" />
        <cfargument name="emp_id" type="numeric" required="false" default="0" />
        <cfargument name="website_name" type="string" required="false" default="mobile-app" hint="Stored in transcript payload; use mobile-app for mobile source email." />
        <cfargument name="host_name" type="string" required="false" default="" hint="Hostname for questions_url; defaults to CGI.SERVER_NAME." />

        <cfset var response = { success = false, message = "", data = {} } />
        <cfset var getWhisperFile = "" />
        <cfset var dupFileName = "" />
        <cfset var patientId = "" />
        <cfset var assessmentId = "" />
        <cfset var assessmentReason = "1 - Start of care - further visits planned" />
        <cfset var visitDate = "" />
        <cfset var scheduleId = "" />
        <cfset var questionsUrl = "" />
        <cfset var s3Url = "" />
        <cfset var encryptionKey = structKeyExists(Application, "enckey") ? Application.enckey : "" />
        <cfset var appKeyValue = "" />
        <cfset var payload = {} />
        <cfset var token = {} />
        <cfset var whisperResponse = "" />
        <cfset var httpStatus = "" />
        <cfset var responseBody = "" />
        <cfset var parsedRemoteResponse = {} />
        <cfset var responseJobId = "" />
        <cfset var responseRequestId = "" />
        <cfset var responseJobStatus = "" />
        <cfset var whisperLookupApiKey = structKeyExists(Application, "whisperLookupApiKey") AND len(trim(Application.whisperLookupApiKey)) ? trim(Application.whisperLookupApiKey) : "WhisperQL_9f3Kx2mP8vTd5Zn7Yw4Rb6Qe1Aj" />
        <cfset var resolvedHost = trim(arguments.host_name) />

        <cfif NOT len(resolvedHost) AND structKeyExists(cgi, "SERVER_NAME")>
            <cfset resolvedHost = trim(cgi.SERVER_NAME) />
        </cfif>
        <cfset resolvedHost = reReplaceNoCase(resolvedHost, "^https?://", "", "all") />
        <cfset resolvedHost = reReplaceNoCase(resolvedHost, "/.*$", "", "all") />
        <cfset resolvedHost = reReplaceNoCase(resolvedHost, ":\d+$", "", "all") />

        <cfif NOT len(encryptionKey)>
            <cfset response.message = "Encryption key configuration is missing." />
            <cfreturn response />
        </cfif>
        <cfif val(arguments.whisper_file_id) LTE 0>
            <cfset response.message = "A valid whisper_file_id is required." />
            <cfreturn response />
        </cfif>

        <cfquery name="getWhisperFile" datasource="#Application.DataSrc#">
            SELECT ID, Agency_Id, Patient_Id, Assmt_ID, Schedule_ID, Dup_file_name, Notes, FileName, CreatedBy
            FROM #arguments.agency_db#.whisper_files
            WHERE ID = <cfqueryparam value="#val(arguments.whisper_file_id)#" cfsqltype="cf_sql_integer">
              AND Agency_Id = <cfqueryparam value="#trim(arguments.agency_id)#" cfsqltype="cf_sql_varchar">
              AND Status = 0
            LIMIT 1
        </cfquery>

        <cfif getWhisperFile.recordCount EQ 0>
            <cfset response.message = "Selected Whisper file not found or unavailable." />
            <cfreturn response />
        </cfif>

        <cfset dupFileName = trim(getWhisperFile.Dup_file_name) />
        <cfif NOT len(dupFileName)>
            <cfset response.message = "The selected file does not have an associated S3 object." />
            <cfreturn response />
        </cfif>

        <cfset patientId = trim(getWhisperFile.Patient_Id) />
        <cfset assessmentId = trim(getWhisperFile.Assmt_ID) />
        <cfif NOT len(patientId)><cfset patientId = "0" /></cfif>
        <cfif NOT len(assessmentId)>
            <cfquery name="findAssessment" datasource="#Application.DataSrc#">
                SELECT pa.Assmt_ID
                FROM #arguments.agency_db#.pAssessments pa
                WHERE pa.Patient_ID = <cfqueryparam value="#patientId#" cfsqltype="cf_sql_varchar">
                  AND pa.Status = 0
                ORDER BY pa.Assmt_ID DESC
                LIMIT 1
            </cfquery>
            <cfif findAssessment.recordCount GT 0>
                <cfset assessmentId = trim(findAssessment.Assmt_ID) />
            </cfif>
        </cfif>
        <cfif NOT len(assessmentId)><cfset assessmentId = "0" /></cfif>

        <cfif listFindNoCase(getWhisperFile.columnList, "Schedule_ID") AND val(getWhisperFile.Schedule_ID) GT 0>
            <cfset scheduleId = trim(getWhisperFile.Schedule_ID) />
        </cfif>
        <cfif val(assessmentId) GT 0>
            <cfquery name="getAssessmentReason" datasource="#Application.DataSrc#">
                SELECT F26, DATE_FORMAT(F176, '%Y-%m-%d') AS Visit_Date
                FROM #arguments.agency_db#.pAssessments
                WHERE Assmt_ID = <cfqueryparam value="#assessmentId#" cfsqltype="cf_sql_varchar">
                  AND Status = 0
                LIMIT 1
            </cfquery>
            <cfif getAssessmentReason.recordCount GT 0 AND len(trim(getAssessmentReason.F26))>
                <cfset assessmentReason = trim(getAssessmentReason.F26) />
            </cfif>
            <cfif getAssessmentReason.recordCount GT 0 AND len(trim(getAssessmentReason.Visit_Date))>
                <cfset visitDate = trim(getAssessmentReason.Visit_Date) />
            </cfif>
        </cfif>

        <cfset questionsUrl = "https://" & resolvedHost & "/components/API/whisper_ai.cfc?method=getAssessmentQuestionsByReason&returnformat=JSON&assessment_reason=" & urlEncodedFormat(assessmentReason) & "&api_key=" & urlEncodedFormat(whisperLookupApiKey) & "&agency_id=" & urlEncodedFormat(trim(arguments.agency_id)) />
        <cfif val(patientId) GT 0>
            <cfset questionsUrl = questionsUrl & "&patient_id=" & urlEncodedFormat(trim(patientId)) />
        </cfif>
        <cfif val(assessmentId) GT 0>
            <cfset questionsUrl = questionsUrl & "&assessment_id=" & urlEncodedFormat(trim(assessmentId)) />
        </cfif>
        <cfif val(scheduleId) GT 0>
            <cfset questionsUrl = questionsUrl & "&schedule_id=" & urlEncodedFormat(trim(scheduleId)) />
        </cfif>
        <cfif val(arguments.whisper_file_id) GT 0>
            <cfset questionsUrl = questionsUrl & "&whisper_files_id=" & urlEncodedFormat(trim(arguments.whisper_file_id)) />
        </cfif>
        <cfif len(trim(visitDate))>
            <cfset questionsUrl = questionsUrl & "&visit_date=" & urlEncodedFormat(trim(visitDate)) />
        </cfif>
        <cfif len(trim(assessmentReason))>
            <cfset questionsUrl = questionsUrl & "&visit_type=" & urlEncodedFormat(trim(assessmentReason)) />
        </cfif>

        <cfset s3Url = "s3://s3-pattii-extractaiapi-assets/whisperfiles/agency_" & trim(arguments.agency_id) & "/" & dupFileName />
        <cfset appKeyValue = trim(arguments.agency_id) & "_" & trim(patientId) />
        <cfset payload = {
            AGENCY_ID = encrypt(trim(arguments.agency_id), encryptionKey, "AES", "Hex"),
            PATIENT_ID = encrypt(trim(patientId), encryptionKey, "AES", "Hex"),
            ASSESSMENT_ID = encrypt(trim(assessmentId), encryptionKey, "AES", "Hex"),
            Emp_ID = encrypt(trim(val(arguments.emp_id)), encryptionKey, "AES", "Hex"),
            appKey = encrypt(appKeyValue, encryptionKey, "AES", "Hex"),
            Website_name = trim(arguments.website_name),
            s3_url = s3Url,
            questions_url = questionsUrl
        } />

        <cfset token = fetchTranscriptApiToken() />
        <cfif NOT token.ok>
            <cfset response.message = "Whisper authentication failed (token unavailable)." />
            <cfset response.data = { token_status = token.statusCode, token_response = token.responseBody } />
            <cfreturn response />
        </cfif>

        <cfhttp url="https://transcriptapi.myhomecarebiz.com/process" method="post" timeout="240" result="whisperResponse" throwonerror="false">
            <cfhttpparam type="header" name="Authorization" value="#token.tokenType# #token.accessToken#">
            <cfhttpparam type="header" name="accept" value="application/json">
            <cfloop collection="#payload#" item="payloadKey">
                <cfif len(payload[payloadKey])>
                    <cfhttpparam type="formField" name="#payloadKey#" value="#payload[payloadKey]#">
                </cfif>
            </cfloop>
        </cfhttp>

        <cfset httpStatus = whisperResponse.StatusCode />
        <cfset responseBody = trim(whisperResponse.FileContent) />
        <cfif len(responseBody) AND isJSON(responseBody)>
            <cfset parsedRemoteResponse = deserializeJSON(responseBody) />
        </cfif>
        <cfif isStruct(parsedRemoteResponse)>
            <cfif structKeyExists(parsedRemoteResponse, "job_id")>
                <cfset responseJobId = trim(parsedRemoteResponse.job_id & "") />
            <cfelseif structKeyExists(parsedRemoteResponse, "jobId")>
                <cfset responseJobId = trim(parsedRemoteResponse.jobId & "") />
            </cfif>
            <cfif structKeyExists(parsedRemoteResponse, "request_id")>
                <cfset responseRequestId = trim(parsedRemoteResponse.request_id & "") />
            <cfelseif structKeyExists(parsedRemoteResponse, "requestId")>
                <cfset responseRequestId = trim(parsedRemoteResponse.requestId & "") />
            </cfif>
            <cfif structKeyExists(parsedRemoteResponse, "status")>
                <cfset responseJobStatus = trim(parsedRemoteResponse.status & "") />
            <cfelseif structKeyExists(parsedRemoteResponse, "job_status")>
                <cfset responseJobStatus = trim(parsedRemoteResponse.job_status & "") />
            </cfif>
        </cfif>
        <cfif NOT len(responseJobStatus)>
            <cfset responseJobStatus = Left(httpStatus, 3) EQ "200" ? "submitted" : "failed" />
        </cfif>

        <cftry>
            <cfquery datasource="#Application.DataSrc#">
                INSERT INTO #arguments.agency_db#.whipser_jobs (
                    Patient_ID, Assmt_ID, whisper_files_id, job_id, request_id, job_status, Status, raw_response, CreatedBy
                ) VALUES (
                    <cfqueryparam value="#val(patientId)#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#val(assessmentId)#" cfsqltype="cf_sql_integer">,
                    <cfqueryparam value="#val(arguments.whisper_file_id)#" cfsqltype="cf_sql_bigint">,
                    <cfqueryparam value="#responseJobId#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#responseRequestId#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#responseJobStatus#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="0" cfsqltype="cf_sql_tinyint">,
                    <cfqueryparam value="#responseBody#" cfsqltype="cf_sql_longvarchar">,
                    <cfqueryparam value="#trim(val(arguments.emp_id))#" cfsqltype="cf_sql_varchar">
                )
            </cfquery>
            <cfcatch type="any"></cfcatch>
        </cftry>

        <cfset response.success = Left(httpStatus, 3) EQ "200" />
        <cfset response.message = response.success ? "Whisper API call completed successfully." : "Whisper API call returned status " & httpStatus & "." />
        <cfset response.data = {
            statusCode = httpStatus,
            s3_url = s3Url,
            questions_url = questionsUrl,
            job_id = responseJobId,
            request_id = responseRequestId,
            job_status = responseJobStatus,
            whisperResponse = parsedRemoteResponse,
            rawResponse = responseBody
        } />
        <cfreturn response />
    </cffunction>
    
</cfcomponent>

