plugins_Awards_Assets_js_controllers_rec-edit-controller.js
import { Controller } from "@hotwired/stimulus";
/**
* Awards Recommendation Edit Form Controller
*
* Manages edit interface for award recommendations with state-driven form behavior,
* dynamic field validation, member discovery, and Turbo Frame integration.
*
* Targets: scaMember, notFound, branch, externalLinks, domain, award, reason,
* gatherings, specialty, state, planToGiveBlock, planToGiveGathering,
* givenBlock, recId, turboFrame, givenDate, closeReason, closeReasonBlock,
* stateRulesBlock
* Values: publicProfileUrl (String), awardListUrl (String), formUrl (String),
* turboFrameUrl (String), gatheringsUrl (String)
* Outlets: outlet-btn
*
* State rules parsed from stateRulesBlock JSON control field Visible/Required/Disabled states.
*/
class AwardsRecommendationEditForm extends Controller {
static targets = [
"scaMember",
"notFound",
"branch",
"externalLinks",
"domain",
"award",
"reason",
"gatherings",
"specialty",
"state",
"planToGiveBlock",
"planToGiveGathering",
"givenBlock",
"recId",
"turboFrame",
"givenDate",
"closeReason",
"closeReasonBlock",
"stateRulesBlock",
];
static values = {
publicProfileUrl: String,
awardListUrl: String,
formUrl: String,
turboFrameUrl: String,
gatheringsUrl: String,
gatheringsLookupUrl: String
};
static outlets = ['outlet-btn'];
/** Set recommendation ID and update Turbo Frame source and form action URL. */
setId(event) {
this.turboFrameTarget.setAttribute("src", this.turboFrameUrlValue + "/" + event.detail.id);
this.element.setAttribute("action", this.formUrlValue + "/" + event.detail.id);
}
/** Register listener when outlet-btn connects. */
outletBtnOutletConnected(outlet, element) {
outlet.addListener(this.setId.bind(this));
}
/** Remove listener when outlet-btn disconnects. */
outletBtnOutletDisconnected(outlet) {
outlet.removeListener(this.setId.bind(this));
}
/** Enable disabled fields before form submission. */
submit(event) {
this.notFoundTarget.disabled = false;
this.scaMemberTarget.disabled = false;
this.specialtyTarget.disabled = false;
}
/** Handle award selection, populate specialties, and update gatherings list. */
setAward(event) {
let awardId = event.target.dataset.awardId;
this.awardTarget.value = awardId;
if (this.awardTarget.value != "") {
this.populateSpecialties(event);
this.updateGatherings(awardId);
}
}
/** Fetch gatherings filtered by award and update checkboxes and dropdown. */
updateGatherings(awardId) {
if (!awardId) {
return;
}
// Get member_id if available
let memberId = this.hasScaMemberTarget ? this.scaMemberTarget.value : '';
// Get status if available
let status = this.hasStateTarget ? this.stateTarget.value : '';
// Build URL with query params
let url = this.gatheringsUrlValue + '/' + awardId;
let params = new URLSearchParams();
if (memberId) {
params.append('member_id', memberId);
}
if (status) {
params.append('status', status);
}
if (params.toString()) {
url += '?' + params.toString();
}
this.updatePlanToGiveLookupUrl(awardId, memberId, status);
fetch(url, this.optionsForFetch())
.then(response => response.json())
.then(data => {
if (data.gatherings) {
// Update the gatherings checkboxes
const gatheringsContainer = document.getElementById('recommendation__gathering_ids');
if (gatheringsContainer) {
// Save currently selected values
const selectedValues = [];
gatheringsContainer.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
selectedValues.push(cb.value);
});
// Clear existing options
gatheringsContainer.innerHTML = '';
// Add new options as checkboxes
data.gatherings.forEach(gathering => {
const div = document.createElement('div');
div.className = 'form-check';
const input = document.createElement('input');
input.type = 'checkbox';
input.className = 'form-check-input';
input.name = 'gatherings[_ids][]';
input.value = gathering.id;
input.id = `gathering-${gathering.id}`;
// Disable cancelled gatherings
if (gathering.cancelled) {
input.disabled = true;
}
// Restore checked state if it was previously selected
if (selectedValues.includes(gathering.id.toString())) {
input.checked = true;
}
const label = document.createElement('label');
label.className = 'form-check-label';
label.htmlFor = `gathering-${gathering.id}`;
label.textContent = gathering.display;
// Style cancelled gatherings
if (gathering.cancelled) {
label.classList.add('text-danger');
}
div.appendChild(input);
div.appendChild(label);
gatheringsContainer.appendChild(div);
});
}
}
})
.catch(error => {
console.error('Error fetching gatherings:', error);
});
}
/** Update autocomplete endpoint URL for "Plan to Give At" gathering lookups. */
updatePlanToGiveLookupUrl(awardId, memberId = '', status = '') {
if (!this.hasPlanToGiveGatheringTarget || !this.hasGatheringsLookupUrlValue || !awardId) {
return;
}
const awardKey = String(awardId);
if (
this.planToGiveGatheringTarget.dataset.lookupAwardId &&
this.planToGiveGatheringTarget.dataset.lookupAwardId !== awardKey
) {
this.planToGiveGatheringTarget.value = '';
this.planToGiveGatheringTarget.dataset.initialValue = '';
}
this.planToGiveGatheringTarget.dataset.lookupAwardId = awardKey;
const currentValue = this.planToGiveGatheringTarget.value ||
this.planToGiveGatheringTarget.dataset.initialValue ||
'';
const params = new URLSearchParams();
if (memberId) {
params.append('member_id', memberId);
}
if (status) {
params.append('status', status);
}
if (this.hasRecIdTarget && this.recIdTarget.value) {
params.append('recommendation_id', this.recIdTarget.value);
}
if (currentValue) {
params.append('selected_id', currentValue);
this.planToGiveGatheringTarget.dataset.initialValue = currentValue;
}
let lookupUrl = `${this.gatheringsLookupUrlValue}/${awardId}`;
if (params.toString()) {
lookupUrl += `?${params.toString()}`;
}
this.planToGiveGatheringTarget.setAttribute('data-ac-url-value', lookupUrl);
}
/** Sync required state to autocomplete text input. */
setPlanToGiveRequired(required) {
if (!this.hasPlanToGiveGatheringTarget) {
return;
}
this.planToGiveGatheringTarget.required = required;
const input = this.planToGiveGatheringTarget.querySelector("[data-ac-target='input']");
if (input) {
input.required = required;
}
}
/** Fetch awards for domain and populate award selection with autocomplete. */
populateAwardDescriptions(event) {
let url = this.awardListUrlValue + "/" + event.target.value;
fetch(url, this.optionsForFetch())
.then(response => response.json())
.then(data => {
this.awardTarget.value = "";
let active = "active";
let show = "show";
let selected = "true";
let awardList = [];
if (data.length > 0) {
data.forEach(function (award) {
awardList.push({ value: award.id, text: award.name, data: award });
});
this.awardTarget.options = awardList;
this.awardTarget.disabled = false;
if (this.awardTarget.dataset.acInitSelectionValue) {
let val = JSON.parse(this.awardTarget.dataset.acInitSelectionValue);
this.awardTarget.value = val.value;
if (this.awardTarget.value != "") {
this.populateSpecialties({ target: { value: val.value } });
}
}
} else {
this.awardTarget.options = [{ value: "No awards available", text: "No awards available" }];
this.awardTarget.value = "No awards available";
this.awardTarget.disabled = true;
this.specialtyTarget.options = [{ value: "No specialties available", text: "No specialties available" }];
this.specialtyTarget.value = "No specialties available";
this.specialtyTarget.disabled = true
this.specialtyTarget.hidden = true;
}
});
}
/** Update specialty dropdown based on selected award's configuration. */
populateSpecialties(event) {
let awardId = this.awardTarget.value;
let options = this.awardTarget.options;
let award = this.awardTarget.options.find(award => award.value == awardId);
let specialtyArray = [];
if (award.data.specialties != null && award.data.specialties.length > 0) {
award.data.specialties.forEach(function (specialty) {
specialtyArray.push({ value: specialty, text: specialty });
});
this.specialtyTarget.options = specialtyArray;
this.specialtyTarget.value = "";
this.specialtyTarget.disabled = false;
this.specialtyTarget.hidden = false;
if (this.specialtyTarget.dataset.acInitSelectionValue) {
let val = JSON.parse(this.specialtyTarget.dataset.acInitSelectionValue);
this.specialtyTarget.value = val.value;
}
} else {
this.specialtyTarget.options = [{ value: "No specialties available", text: "No specialties available" }];
this.specialtyTarget.value = "No specialties available";
this.specialtyTarget.disabled = true
this.specialtyTarget.hidden = true;
}
}
/** Handle member field change, load profile or show branch field if not found. */
loadScaMemberInfo(event) {
this.externalLinksTarget.innerHTML = "";
let memberId = Number(event.target.value.replace(/_/g, ""));
if (memberId > 0) {
this.notFoundTarget.checked = false;
this.branchTarget.hidden = true;
this.branchTarget.disabled = true;
this.loadMember(memberId);
} else {
this.notFoundTarget.checked = true;
this.branchTarget.hidden = false;
this.branchTarget.disabled = false;
this.branchTarget.focus();
}
}
/** Get standard fetch options with JSON headers. */
optionsForFetch() {
return {
headers: {
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json"
}
}
}
/** Fetch and display member profile external links. */
loadMember(memberId) {
let url = this.publicProfileUrlValue + "/" + memberId;
fetch(url, this.optionsForFetch())
.then(response => response.json())
.then(data => {
this.externalLinksTarget.innerHTML = "";
let keys = Object.keys(data.external_links);
if (keys.length > 0) {
var LinksTitle = document.createElement("div");
LinksTitle.innerHTML = "<h5>Public Links</h5>";
LinksTitle.classList.add("col-12");
this.externalLinksTarget.appendChild(LinksTitle);
for (let key in data.external_links) {
let div = document.createElement("div");
div.classList.add("col-12");
let a = document.createElement("a");
a.href = data.external_links[key];
a.text = key;
a.target = "_blank";
div.appendChild(a);
this.externalLinksTarget.appendChild(div);
}
} else {
var noLink = document.createElement("div");
noLink.innerHTML = "<h5>No links available</h5>";
noLink.classList.add("col-12");
this.externalLinksTarget.appendChild(noLink);
}
});
}
/** Load member info when scaMember target connects with existing value. */
scaMemberTargetConnected() {
if (this.scaMemberTarget.value != "") {
this.loadScaMemberInfo({ target: { value: this.scaMemberTarget.value } });
}
}
/** Apply field rules when state target connects. */
stateTargetConnected() {
console.log("status connected");
this.setFieldRules();
}
/** Parse JSON state rules and apply Visible/Required/Disabled field states. */
setFieldRules() {
console.log("setting field rules");
var rulesstring = this.stateRulesBlockTarget.textContent;
var rules = JSON.parse(rulesstring);
if (this.specialtyTarget.options.length == 0) {
this.specialtyTarget.hidden = true;
this.specialtyTarget.disabled = true;
}
this.planToGiveBlockTarget.style.display = "none";
this.givenBlockTarget.style.display = "none";
// Store the current givenDate value before potentially clearing it
if (this.givenDateTarget.value && !this.givenDateTarget.dataset.initialValue) {
this.givenDateTarget.dataset.initialValue = this.givenDateTarget.value;
}
// Only clear givenDate if it doesn't have an initial value stored
if (!this.givenDateTarget.dataset.initialValue) {
this.givenDateTarget.value = "";
} else {
// Restore the initial value if it was cleared
if (!this.givenDateTarget.value) {
this.givenDateTarget.value = this.givenDateTarget.dataset.initialValue;
}
}
this.domainTarget.disabled = false;
this.awardTarget.disabled = false;
this.specialtyTarget.disabled = this.specialtyTarget.hidden;
this.scaMemberTarget.disabled = false;
this.setPlanToGiveRequired(false);
this.givenDateTarget.required = false;
this.closeReasonBlockTarget.style.display = "none";
this.closeReasonTarget.required = false;
if (this.notFoundTarget.checked) {
this.branchTarget.disabled = false;
this.branchTarget.hidden = false;
} else {
this.branchTarget.disabled = true;
this.branchTarget.hidden = true;
}
var state = this.stateTarget.value;
//check status rules for the status
if (rules[state]) {
var statusRules = rules[state];
var controller = this;
if (statusRules["Visible"]) {
statusRules["Visible"].forEach(function (field) {
if (controller[field]) {
controller[field].style.display = "block";
}
});
}
if (statusRules["Disabled"]) {
statusRules["Disabled"].forEach(function (field) {
if (controller[field]) {
controller[field].disabled = true;
}
});
}
if (statusRules["Required"]) {
statusRules["Required"].forEach(function (field) {
if (controller[field]) {
controller[field].required = true;
}
});
}
}
this.setPlanToGiveRequired(!!this.planToGiveGatheringTarget.required);
// Update gatherings when state changes (e.g., to/from "Given")
if (this.hasAwardTarget && this.awardTarget.value) {
this.updateGatherings(this.awardTarget.value);
}
}
/** Store initial gathering value on connect for persistence through updates. */
connect() {
// Store the initial gathering_id value so it persists through option updates
if (this.hasPlanToGiveGatheringTarget && this.planToGiveGatheringTarget.value) {
this.planToGiveGatheringTarget.dataset.initialValue = this.planToGiveGatheringTarget.value;
}
}
/** Update form action URL when recId target connects. */
recIdTargetConnected() {
let recId = this.recIdTarget.value;
let actionUrl = this.element.getAttribute("action");
//trim the last / off of the end of the action url
actionUrl = actionUrl.replace(/\/\d+$/, "");
actionUrl = actionUrl + "/" + recId;
this.element.setAttribute("action", actionUrl);
}
/** Store initial gathering value on target connect. */
planToGiveGatheringTargetConnected() {
// Store the initial value from the server-rendered form
if (this.planToGiveGatheringTarget.value) {
this.planToGiveGatheringTarget.dataset.initialValue = this.planToGiveGatheringTarget.value;
}
}
/** Store initial given date value on target connect. */
givenDateTargetConnected() {
// Store the initial value from the server-rendered form
if (this.givenDateTarget.value) {
this.givenDateTarget.dataset.initialValue = this.givenDateTarget.value;
}
}
}
// add to window.Controllers with a name of the controller
if (!window.Controllers) {
window.Controllers = {};
}
window.Controllers["awards-rec-edit"] = AwardsRecommendationEditForm;