JS new Date() was parsing dates without Z as local time, showing UTC values as-is. Adding Z suffix tells JS the dates are UTC so it converts to the user's local timezone automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
144 lines
6.3 KiB
Text
144 lines
6.3 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfcontent type="application/json; charset=utf-8">
|
|
<cfscript>
|
|
function readJsonBody() {
|
|
var raw = getHttpRequestData().content;
|
|
if (isNull(raw) || len(trim(raw)) == 0) return {};
|
|
try {
|
|
var data = deserializeJSON(raw);
|
|
return isStruct(data) ? data : {};
|
|
} catch (any e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
try {
|
|
data = readJsonBody();
|
|
|
|
// Token can come from URL param or JSON body
|
|
token = "";
|
|
if (structKeyExists(url, "token")) token = trim(url.token);
|
|
if (len(token) == 0 && structKeyExists(data, "token")) token = trim(data.token);
|
|
|
|
if (len(token) == 0) {
|
|
writeOutput(serializeJSON({ "OK": false, "ERROR": "missing_token", "MESSAGE": "Rating token is required." }));
|
|
abort;
|
|
}
|
|
|
|
// Look up the rating by token
|
|
qRating = queryTimed("
|
|
SELECT r.*, t.Title,
|
|
u_for.FirstName AS ForFirstName, u_for.LastName AS ForLastName,
|
|
u_by.FirstName AS ByFirstName, u_by.LastName AS ByLastName
|
|
FROM TaskRatings r
|
|
JOIN Tasks t ON t.ID = r.TaskID
|
|
LEFT JOIN Users u_for ON u_for.ID = r.ForUserID
|
|
LEFT JOIN Users u_by ON u_by.ID = r.ByUserID
|
|
WHERE r.AccessToken = ?
|
|
LIMIT 1
|
|
", [{ value = token, cfsqltype = "cf_sql_varchar" }]);
|
|
|
|
if (qRating.recordCount == 0) {
|
|
writeOutput(serializeJSON({ "OK": false, "ERROR": "invalid_token", "MESSAGE": "Rating not found or link is invalid." }));
|
|
abort;
|
|
}
|
|
|
|
// Check if expired
|
|
if (qRating.ExpiresOn < now()) {
|
|
writeOutput(serializeJSON({ "OK": false, "ERROR": "expired", "MESSAGE": "This rating link has expired." }));
|
|
abort;
|
|
}
|
|
|
|
// Check if already completed
|
|
if (len(trim(qRating.CompletedOn)) > 0) {
|
|
writeOutput(serializeJSON({ "OK": false, "ERROR": "already_submitted", "MESSAGE": "This rating has already been submitted." }));
|
|
abort;
|
|
}
|
|
|
|
// If GET request (or no rating data), return rating info for display
|
|
isSubmission = structKeyExists(data, "onTime") || structKeyExists(data, "completedScope")
|
|
|| structKeyExists(data, "requiredFollowup") || structKeyExists(data, "continueAllow")
|
|
|| structKeyExists(data, "prepared") || structKeyExists(data, "respectful")
|
|
|| structKeyExists(data, "wouldAutoAssign");
|
|
|
|
if (!isSubmission) {
|
|
// Return rating details for UI to display
|
|
result = {
|
|
"OK": true,
|
|
"RatingID": qRating.ID,
|
|
"Direction": qRating.Direction,
|
|
"Title": qRating.Title,
|
|
"ForUserName": trim(qRating.ForFirstName & " " & qRating.ForLastName),
|
|
"ExpiresOn": dateTimeFormat(qRating.ExpiresOn, "yyyy-mm-dd'T'HH:nn:ss") & "Z"
|
|
};
|
|
|
|
// Include appropriate questions based on direction
|
|
if (qRating.Direction == "customer_rates_worker" || qRating.Direction == "admin_rates_worker") {
|
|
result["Questions"] = {
|
|
"onTime": "Was the worker on time?",
|
|
"completedScope": "Was the scope completed?",
|
|
"requiredFollowup": "Was follow-up required?",
|
|
"continueAllow": "Continue to allow these tasks?"
|
|
};
|
|
} else if (qRating.Direction == "worker_rates_customer") {
|
|
result["Questions"] = {
|
|
"prepared": "Was the customer prepared?",
|
|
"completedScope": "Was the scope clear?",
|
|
"respectful": "Was the customer respectful?",
|
|
"wouldAutoAssign": "Would you serve this customer again?"
|
|
};
|
|
}
|
|
|
|
writeOutput(serializeJSON(result));
|
|
abort;
|
|
}
|
|
|
|
// Process submission based on direction
|
|
if (qRating.Direction == "customer_rates_worker" || qRating.Direction == "admin_rates_worker") {
|
|
queryTimed("
|
|
UPDATE TaskRatings SET
|
|
OnTime = ?,
|
|
CompletedScope = ?,
|
|
RequiredFollowup = ?,
|
|
ContinueAllow = ?,
|
|
CompletedOn = NOW()
|
|
WHERE ID = ?
|
|
", [
|
|
{ value = structKeyExists(data,"onTime") ? (data.onTime ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"onTime") },
|
|
{ value = structKeyExists(data,"completedScope") ? (data.completedScope ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"completedScope") },
|
|
{ value = structKeyExists(data,"requiredFollowup") ? (data.requiredFollowup ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"requiredFollowup") },
|
|
{ value = structKeyExists(data,"continueAllow") ? (data.continueAllow ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"continueAllow") },
|
|
{ value = qRating.ID, cfsqltype = "cf_sql_integer" }
|
|
]);
|
|
} else if (qRating.Direction == "worker_rates_customer") {
|
|
queryTimed("
|
|
UPDATE TaskRatings SET
|
|
Prepared = ?,
|
|
CompletedScope = ?,
|
|
Respectful = ?,
|
|
WouldAutoAssign = ?,
|
|
CompletedOn = NOW()
|
|
WHERE ID = ?
|
|
", [
|
|
{ value = structKeyExists(data,"prepared") ? (data.prepared ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"prepared") },
|
|
{ value = structKeyExists(data,"completedScope") ? (data.completedScope ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"completedScope") },
|
|
{ value = structKeyExists(data,"respectful") ? (data.respectful ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"respectful") },
|
|
{ value = structKeyExists(data,"wouldAutoAssign") ? (data.wouldAutoAssign ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"wouldAutoAssign") },
|
|
{ value = qRating.ID, cfsqltype = "cf_sql_integer" }
|
|
]);
|
|
}
|
|
|
|
writeOutput(serializeJSON({
|
|
"OK": true,
|
|
"MESSAGE": "Rating submitted successfully. Thank you for your feedback!"
|
|
}));
|
|
|
|
} catch (any e) {
|
|
writeOutput(serializeJSON({
|
|
"OK": false,
|
|
"ERROR": "server_error",
|
|
"MESSAGE": "Error submitting rating",
|
|
"DETAIL": e.message
|
|
}));
|
|
}
|
|
</cfscript>
|