Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TOP-249 allow admins to add location to regular schedules without sav… #370

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading