Skip to content

Commit

Permalink
Merge pull request #370 from wearefuturegov/TOP-249-allow-admins-to-a…
Browse files Browse the repository at this point in the history
…dd-location-to-regular-schedules-without-saving-first

TOP-249 allow admins to add location to regular schedules without sav…
  • Loading branch information
apricot13 authored Sep 18, 2024
2 parents 2715538 + 2e97770 commit 04f91c5
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 52 deletions.
5 changes: 5 additions & 0 deletions app/assets/stylesheets/outpost-design-library/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
.field-group {
margin-top: 45px;

// if the first item is hidden=true, remove the top margin
&:is([hidden="true"]) + * {
margin-top: 0px;
}

&:first-child {
margin-top: 0px;
}
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/admin/services_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def service_params
regular_schedules_attributes: [
:id,
:service_at_location_id,
:location_object_id,
:weekday,
:opens_at,
:closes_at,
Expand Down Expand Up @@ -185,6 +186,7 @@ def service_params
:visible,
:mask_exact_address,
:preferred_for_post,
:location_object_id,
:_destroy,
accessibility_ids: []
],
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def link_to_add_fields(name, f, association, view)
fields = f.fields_for(association, new_object, child_index: id) do |builder|
render(view, l: builder, c: builder, sched: builder)
end
link_to name, '#', class: "button button--secondary button--add", data: {id: id, fields: fields.gsub("\n", ""), add: true}
link_to name, '#', class: "button button--secondary button--add", data: {id: id, fields: fields.gsub("\n", ""), add: true, association: association}
end

def local_offer_checkbox(builder, view)
Expand Down
240 changes: 238 additions & 2 deletions app/javascript/packs/regular-schedule.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
document.addEventListener("turbolinks:load", () => {
// regular schedules
setupTimeTypePanelListeners();
const timeTypePanels = document.querySelectorAll(
"[data-regular-schedule-panel]"
);
timeTypePanels.forEach((panel) => {
setTimeTypePanelState(panel);
});

// regular schedules & locations (admin only)
let locations = document.querySelector("#location_panels.repeater");
if (locations) {
setupRegularScheduleLocationListeners();

runOnEachRegularSchedulePanel((panel) => {
setRegularScheduleLocationState(panel);
});
}
});

/*************
*
* REGULAR SCHEDULES
*
*************/

/**
* Sets up event listeners inside the regular schedule panels
*/
Expand All @@ -16,7 +33,7 @@ const setupTimeTypePanelListeners = () => {
"#regular_schedule_panels.repeater"
);

// listens for every change event inside the repeater
// listens for every change event inside the regular schedules repeater
if (regularSchedules) {
regularSchedules.addEventListener("change", (e) => {
// the panel object
Expand Down Expand Up @@ -54,7 +71,6 @@ const setupTimeTypePanelListeners = () => {
*/
const setTimeTypePanelState = (panel) => {
const timeType = getPanelTimeType(panel);
console.log(`setTimeTypePanelState to: ${timeType}`);

const opening_time = panel.querySelector(".regular_schedule__opening_time");
const event_time = panel.querySelector(".regular_schedule__event_time");
Expand Down Expand Up @@ -106,6 +122,158 @@ const setFreqVisibility = (panel, freq) => {
}
};

/*************
*
* REGULAR SCHEDULES & LOCATIONS
*
*************/

/**
* Sets up event listeners inside the regular schedule panels
*/
const setupRegularScheduleLocationListeners = () => {
let locations = document.querySelector("#location_panels.repeater");
let regularSchedules = document.querySelector(
"#regular_schedule_panels.repeater"
);
if (locations) {
locations.addEventListener("click", (e) => {
// add a location
if (
e.target.getAttribute("data-add") &&
e.target.getAttribute("data-association") === "locations"
) {
runOnEachRegularSchedulePanel((panel) => {
setRegularScheduleLocationState(panel);
});
}

// remove a location
if (e.target.getAttribute("data-close")) {
runOnEachRegularSchedulePanel((panel) => {
setRegularScheduleLocationState(panel);
});
}
});

// update a location
locations.addEventListener("change", (e) => {
runOnEachRegularSchedulePanel((panel) => {
setRegularScheduleLocationState(panel);
});
});
}

if (regularSchedules) {
regularSchedules.addEventListener("click", (e) => {
// add a regularSchedules
if (
e.target.getAttribute("data-add") &&
e.target.getAttribute("data-association") === "regular_schedules"
) {
runOnEachRegularSchedulePanel((panel) => {
setRegularScheduleLocationState(panel);
});
}
});

// when regular schedule field changes
regularSchedules.addEventListener("change", (e) => {
// the panel object
const panel = e.target.closest("[data-regular-schedule-panel]");
const serviceAtLocationIdMatcher = e.target.name.match(
/^service\[regular_schedules_attributes\]\[(\d+)\]\[(service_at_location_id)\]$/
);

if (serviceAtLocationIdMatcher) {
updateScheduleLocationObjectId(e.target, panel);
}
});
}
};

/**
* Set location select visibility
* @param {*} panel
*/
const setRegularScheduleLocationState = (panel) => {
const locationNames = getAllLocationNames();

// toggle visibility of location select
if (locationNames.length <= 1) {
toggleHidden(panel, true, "data-location");
} else {
toggleHidden(panel, false, "data-location");
}

// set dropdown state

const dropdown = panel.querySelector('[name*="service_at_location_id"]');
// get the 'title option' cant rely on it being the first option or it being the only blank option
var titleOption = Array.from(dropdown.options).find(
(o) => o.innerHTML === "All locations" && o.value === ""
);
// is there an option currently selected?
const selectedOption = Array.from(dropdown.options).find(
(option) => option.selected && option.innerHTML !== "All locations"
);
const selectedOptionValues = {
service_at_location_id: selectedOption?.value,
location_object_id: selectedOption?.dataset.locationObjectId,
};
// create new options based on the location names
const newOptions = locationNames.map((location) => {
return createOption(
location.service_at_location_id ?? "",
location.value,
location.location_object_id
);
});
// clear the dropdown options
dropdown.innerHTML = "";
// add them back in
[titleOption, ...newOptions].forEach((option) => {
dropdown.appendChild(option);
});
// and select the one that was already selected if it still exists
const existingLocationOption = Array.from(
dropdown.querySelectorAll(
`option[value='${selectedOptionValues.service_at_location_id}']:not([data-location-object-id])`
)
).find((option) => option.textContent.trim() !== "All locations");
const newLocationOption = dropdown.querySelector(
`option[value=''][data-location-object-id='${selectedOptionValues.location_object_id}']`
);

if (existingLocationOption) {
existingLocationOption.selected = true;
} else if (newLocationOption) {
newLocationOption.selected = true;
}
};

/**
* Updates the schedule location object id when location is selected in dropdown
* @param {*} panel
*/
const updateScheduleLocationObjectId = (locationSelector, panel) => {
const value = locationSelector.value;
const selectedIndex = locationSelector.selectedIndex;
const selectedLocationObjectId =
locationSelector[selectedIndex].dataset.locationObjectId;

const location_object_id_field = panel.querySelector(
'[name$="[location_object_id]"]'
);

// if we have selected a new location update the location_object_id_field
if (!value && selectedLocationObjectId) {
location_object_id_field.value = selectedLocationObjectId;
} else {
location_object_id_field.value = "";
}
};

/*************
*
* UTILITIES
Expand Down Expand Up @@ -196,3 +364,71 @@ const toggleRequired = (panel, required, dataField = "data-required") => {
}
});
};

/**
* Gets all the location names, uses the same formatting logic as elsewhere in the code base
* @returns {Array} An array of objects containing the location service_at_location_id and name
*/
const getAllLocationNames = () => {
const location_data = [];
runOnEachLocationPanel((panel) => {
const destroy = panel.querySelector('[name*="_destroy"]').value;
if (destroy !== "true") {
const name = panel.querySelector('[name*="name"]').value;
const address_1 = panel.querySelector('[name*="address_1"]').value;
const city = panel.querySelector('[name*="city"]').value;
const postal_code = panel.querySelector('[name*="postal_code"]').value;

const panel_location_fields = {
value: name || address_1 || postal_code || "No location name provided",
location_object_id: panel.dataset.locationObjectId ?? null,
service_at_location_id:
panel.dataset.locationServiceAtLocationId ?? null,
};
location_data.push(panel_location_fields);
}
});

return location_data;
};

/**
* Callback method to run on each regular schedule panel
* @param {*} callback
*/
const runOnEachRegularSchedulePanel = function (callback) {
const regularSchedulePanels = document.querySelectorAll(
"[data-regular-schedule-panel]"
);
regularSchedulePanels.forEach((panel) => {
callback(panel);
});
};

/**
* Callback method to run on each location panel
* @param {*} callback
*/
const runOnEachLocationPanel = function (callback) {
const locationPanels = document.querySelectorAll("[data-location-panel]");
locationPanels.forEach((panel) => {
callback(panel);
});
};

/**
* Create an <option> element
* @param {*} key
* @param {*} value
* @param {*} location_object_id
* @returns
*/
const createOption = (key, value, location_object_id) => {
const option = document.createElement("option");
option.value = key;
option.textContent = value;
if (location_object_id !== null) {
option.dataset.locationObjectId = location_object_id;
}
return option;
};
2 changes: 2 additions & 0 deletions app/models/location.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Location < ApplicationRecord
validates :postal_code, presence: true, unless: :skip_postcode_validation
validate :postal_code_is_valid, unless: :skip_postcode_validation

attr_accessor :location_object_id

before_validation :geocode
geocoded_by :postal_code

Expand Down
2 changes: 2 additions & 0 deletions app/models/regular_schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class RegularSchedule < ApplicationRecord
belongs_to :service
belongs_to :service_at_location, optional: true

attr_accessor :location_object_id

validates_presence_of :weekday
validates_presence_of :opens_at
validates_presence_of :closes_at
Expand Down
20 changes: 20 additions & 0 deletions app/models/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def all_blank_except_service_at_location_id(attributes)
before_save :add_parent_taxonomies, unless: :skip_add_parent_taxonomies
before_save :skip_nested_indexes
before_save :update_directories
after_save :update_regular_schedules

filterrific(
default_filter_params: { sorted_by: "recent" },
Expand Down Expand Up @@ -359,4 +360,23 @@ def destroy_associated_data
meta.destroy_all
links.destroy_all
end

private

# updates regular_schedule with service_at_location_id if location_object_id is set
def update_regular_schedules
if regular_schedules.any? && locations.any?
service_id = id
regular_schedules.each do |schedule|
if schedule.location_object_id.present?
location = locations.find { |loc| loc.location_object_id == schedule.location_object_id }
if location
service_at_location = service_at_locations.find_by(location_id: location.id, service_id: service_id)
schedule.update(service_at_location_id: service_at_location.id) if service_at_location
end
end
end
end
end

end
2 changes: 1 addition & 1 deletion app/models/service_at_location.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ class ServiceAtLocation < ApplicationRecord

has_many :contacts, through: :service
has_many :taxonomies, through: :service
has_many :regular_schedules
has_many :regular_schedules, dependent: :destroy
end
Loading

0 comments on commit 04f91c5

Please sign in to comment.