From 58e5198d29771fb9f21c6377b1d71f03ddbb928c Mon Sep 17 00:00:00 2001 From: Han Date: Fri, 30 Aug 2024 13:34:39 +0100 Subject: [PATCH 01/22] TOP-203 Planning everything out --- app/models/regular_schedule.rb | 7 + .../editors/_regular-schedule-fields.html.erb | 341 +++++++++++++++++- ...5_add_i_cal_fields_to_regular_schedules.rb | 9 + db/schema.rb | 11 +- 4 files changed, 358 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20240830111345_add_i_cal_fields_to_regular_schedules.rb diff --git a/app/models/regular_schedule.rb b/app/models/regular_schedule.rb index 78bf09b7..dffcf493 100644 --- a/app/models/regular_schedule.rb +++ b/app/models/regular_schedule.rb @@ -7,6 +7,13 @@ class RegularSchedule < ApplicationRecord validates_presence_of :closes_at validate :validate_hours + + # frequency is a string that can be either 'WEEKLY' or 'MONTHLY' + enum freq: { week: 'WEEKLY', month: 'MONTHLY' } + + # byday + enum byday: { monday: 'MO', tuesday: 'TU', wednesday: 'WE', thursday: 'TH', friday: 'FR', saturday: 'SA', sunday: 'SU' } + def validate_hours if opens_at.present? && closes_at.present? && opens_at > closes_at errors.add(:base, :impossible_hours) diff --git a/app/views/admin/services/editors/_regular-schedule-fields.html.erb b/app/views/admin/services/editors/_regular-schedule-fields.html.erb index 560d4ab3..08d01519 100644 --- a/app/views/admin/services/editors/_regular-schedule-fields.html.erb +++ b/app/views/admin/services/editors/_regular-schedule-fields.html.erb @@ -1,6 +1,6 @@
- <% if @service.locations.count > 1 %> + <% if @service.locations.count > 1 %>
<%= sched.label :service_at_location_id, "Location", class: "field__label" %> @@ -10,27 +10,352 @@ <% end %> -
-
+

Opening times

+ +

I'm a shop keeper

+
    +
  • I want to log my opening hours at all locations
      +
    • day, opens, closes
    • +
    +
  • +
  • I want to log my opening hours at specific location
      +
    • service_at_location_id, day, opens, closes
    • +
    +
  • +
+ + + + + +
+
<%= sched.label :weekday, "Day", class: "field__label" %> - <%= sched.select :weekday, weekdays.collect {|d| [ d[:label], d[:value] ] }, {}, class: "field__input", required: true %> + <%= sched.select :weekday, weekdays.collect {|d| [ d[:label], d[:value] ] }, {}, class: "field__input" %>
-
+
<% open_val = sched.object.opens_at.strftime("%H:%M") if sched.object.opens_at.present? %> <%= sched.label :opens_at, class: "field__label" %> - <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60, required: true %> + <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60 %>
-
+
<% close_val = sched.object.closes_at.strftime("%H:%M") if sched.object.closes_at.present? %> <%= sched.label :closes_at, class: "field__label" %> - <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60, required: true %> + <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60 %> +
+
+ + + +

I'm a service owner - one time events

+ +

I'm a service owner

+
    +
  • I want to put in a one time event at all locations
      +
    • date (dtstart), time start (opens_at), time finish (closes_at)
    • +
    +
  • +
  • +

    I want to put in a one time event at specific location

    +
      +
    • service_at_location_id, date, time start, time finish
    • +
    +
  • +
  • +

    I want to put in a number of one time events at all locations

    +
      +
    • date, time start, time finish
    • +
    • +
        +
      • add a schedule
      • +
      +
    • +
    • date, time start, time finish
    • +
    +
  • +
  • +

    I want to put in a number of one time events at specific location

    +
      +
    • service_at_location_id, date, time start, time finish
    • +
    • +
        +
      • add a schedule
      • +
      +
    • +
    • service_at_location_id, , date, time start, time finish
    • +
    +
  • +
+ +
+
+ <%= sched.label :dtstart, "Date", class: "field__label" %> + <%= sched.date_field :dtstart, class: "field__input" %> +
+
+ <% open_val = sched.object.opens_at.strftime("%H:%M") if sched.object.opens_at.present? %> + <%= sched.label :opens_at, "Start time", class: "field__label" %> + <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60 %> +
+
+ <% close_val = sched.object.closes_at.strftime("%H:%M") if sched.object.closes_at.present? %> + <%= sched.label :closes_at, "End time", class: "field__label" %> + <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60 %>
+ +

I'm a service owner - repeating event(s)

+ + + +
    +
  • +

    I want to put in a repeating event at all locations

    +
      +
    • starting from (dtstart)
    • +
    • number (interval)
    • +
    • every WEEKLY|MONTHLY (freq)
    • +
    • if weekly
        +
      • on any combo of one of these MO,TU,WE,TH,FR,SA,SU + (byday)
      • +
      +
    • +
    • if monthly
        +
      • every +/-n[MO,TU,WE,TH,FR,SA,SU] (byday)
      • +
      • or every 1-30/31? (bymonthday)
      • +
      +
    • +
    +
  • +
  • I want to put in a repeating event at specific locations
      +
    • as above but add service_at_location_id
    • +
    +
  • +
+ + + +
+
+ <%= sched.label :dtstart, "Date", class: "field__label" %> + <%= sched.date_field :dtstart, class: "field__input" %> +
+
+ <% open_val = sched.object.opens_at.strftime("%H:%M") if sched.object.opens_at.present? %> + <%= sched.label :opens_at, "Start time", class: "field__label" %> + <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60 %> +
+
+ <% close_val = sched.object.closes_at.strftime("%H:%M") if sched.object.closes_at.present? %> + <%= sched.label :closes_at, "End time", class: "field__label" %> + <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60 %> +
+
+ + +

Repeats

+ +
+
+ <%= sched.label :interval, "Every", class: "field__label" %> + <%= sched.number_field :interval, class: "field__input"%> +
+
+ <%= sched.label :freq, "Weekly/Monthly", class: "field__label" %> + <%= sched.collection_select :freq, RegularSchedule.freqs.keys, :to_s, :humanize, {}, { class: "field__input" } %> +
+
+ + + +

If weekly - on the

+ + + +
+
+

On the following days

+ <% RegularSchedule.bydays.each do |day, code| %> +
+ <% check_val = sched.object.byday.include?(code) if sched.object.byday.present? %> + <%= check_box_tag "regular_schedule[byday][]", code, check_val, id: "regular_schedule_byday_#{code}", class: "checkbox__input" %> + <%= label_tag "regular_schedule_byday_#{code}", day.humanize, class: "checkbox__label" %> +
+ <% end %> +
+
+ + + +

If monthly - on the

+ +
+
+ <%= sched.label :bymonthday, "On the [blank] date each month", class: "field__label" %> + <%= sched.number_field :bymonthday, min: 1, max: 31, class: "field__input"%> +
+
+ + +

or

+ + +
+
+ <%= label_tag "week_of_month", "Week of the month", class: "field__label" %> + <%= select_tag "week_of_month", options_for_select([["first", 1], ["second", 2], ["third", 3], ["fourth", 4], ["fifth", 5], ["last", -1]]), class: "field__input" %> +
+
+ <% RegularSchedule.bydays.each do |day, code| %> +
+ <% check_val = sched.object.byday.include?(code) if sched.object.byday.present? %> + <%= check_box_tag "regular_schedule[byday][]", code, check_val, id: "regular_schedule_byday_#{code}", class: "checkbox__input" %> + <%= label_tag "regular_schedule_byday_#{code}", day.humanize, class: "checkbox__label" %> +
+ <% end %> +
+
+ + + + + <%= sched.hidden_field :_destroy, data: {destroy_field: true} %>
+ + \ No newline at end of file diff --git a/db/migrate/20240830111345_add_i_cal_fields_to_regular_schedules.rb b/db/migrate/20240830111345_add_i_cal_fields_to_regular_schedules.rb new file mode 100644 index 00000000..b5b55a27 --- /dev/null +++ b/db/migrate/20240830111345_add_i_cal_fields_to_regular_schedules.rb @@ -0,0 +1,9 @@ +class AddICalFieldsToRegularSchedules < ActiveRecord::Migration[6.0] + def change + add_column :regular_schedules, :dtstart, :date + add_column :regular_schedules, :freq, :string + add_column :regular_schedules, :interval, :integer + add_column :regular_schedules, :byday, :string + add_column :regular_schedules, :bymonthday, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 5c5fe52a..d0332ebf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_05_10_153947) do +ActiveRecord::Schema.define(version: 2024_08_30_111345) do # These are extensions that must be enabled in order to support this database + enable_extension "pg_stat_statements" enable_extension "pg_trgm" enable_extension "plpgsql" @@ -279,6 +280,11 @@ t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "service_at_location_id" + t.date "dtstart" + t.string "freq" + t.integer "interval" + t.string "byday" + t.string "bymonthday" t.index ["service_at_location_id"], name: "index_regular_schedules_on_service_at_location_id" t.index ["service_id"], name: "index_regular_schedules_on_service_id" end @@ -287,6 +293,7 @@ t.string "postcode" t.string "ward" t.string "family_centre" + t.string "area" end create_table "send_needs", force: :cascade do |t| @@ -481,7 +488,7 @@ add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id" add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id" - add_foreign_key "regular_schedules", "service_at_locations" + add_foreign_key "regular_schedules", "service_at_locations", on_delete: :cascade add_foreign_key "regular_schedules", "services" add_foreign_key "service_meta", "services" add_foreign_key "services", "ofsted_items" From 9bc3043fab1e4cea1c8561b0271fbd376b7ae8f7 Mon Sep 17 00:00:00 2001 From: Han Date: Mon, 2 Sep 2024 12:51:40 +0100 Subject: [PATCH 02/22] TOP-203 add in JS and some small text changes --- app/javascript/packs/regular-schedule.js | 54 +++++++++++++------ .../editors/_regular-schedule.html.erb | 2 +- app/views/admin/services/show.html.erb | 5 +- .../services/_edit_opening_times.html.erb | 2 +- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/app/javascript/packs/regular-schedule.js b/app/javascript/packs/regular-schedule.js index 04320c43..736f1119 100644 --- a/app/javascript/packs/regular-schedule.js +++ b/app/javascript/packs/regular-schedule.js @@ -1,20 +1,40 @@ -const editor = document.querySelector(".schedule-editor") +document.addEventListener("DOMContentLoaded", () => { + let regularSchedules = document.querySelector( + "#regular_schedule_panels.repeater" + ); -const update = checkbox => { - let inputs = checkbox.parentNode.parentNode.parentNode.querySelectorAll("input[type='time']") - if(checkbox.checked){ - inputs.forEach(input => input.removeAttribute("disabled")) - } else { - inputs.forEach(input => input.setAttribute("disabled", "true")) + // listens for every change event inside the repeater + regularSchedules.addEventListener("change", (e) => { + // the panel object + const panel = e.target.closest("[data-regular-schedule-panel]"); + + // if the change event is on the time_type radio button + const TimeTypeRadio = e.target.name.match( + /^service\[regular_schedules_attributes\]\[(\d+)\]\[(time_type)\]$/ + ); + if (TimeTypeRadio) { + const selectedRadio = document.querySelector( + `input[name="${e.target.name}"]:checked` + ); + ToggleTimeType(selectedRadio.value, panel); } -} + }); +}); + +/** + * Toggles the visibility of the opening_time and event_time selections + * @param {*} selected + * @param {*} panel + */ +const ToggleTimeType = (selected, panel) => { + const opening_time = panel.querySelector(".regular_schedule__opening_time"); + const event_time = panel.querySelector(".regular_schedule__event_time"); -if(editor){ - let checkboxes = editor.querySelectorAll("input[type='checkbox']") - checkboxes.forEach(checkbox => { - update(checkbox) - checkbox.addEventListener("click", () => { - update(checkbox) - }) - }) -} \ No newline at end of file + if (selected === "opening_time") { + opening_time.removeAttribute("hidden"); + event_time.setAttribute("hidden", true); + } else if (selected === "event_time") { + opening_time.setAttribute("hidden", true); + event_time.removeAttribute("hidden"); + } +}; diff --git a/app/views/admin/services/editors/_regular-schedule.html.erb b/app/views/admin/services/editors/_regular-schedule.html.erb index c9040ab5..27f6f56c 100644 --- a/app/views/admin/services/editors/_regular-schedule.html.erb +++ b/app/views/admin/services/editors/_regular-schedule.html.erb @@ -6,7 +6,7 @@
<%= mark_unapproved_array("regular_schedules") do %> -
+
    <%= s.fields_for :regular_schedules do |sched| %>
  • diff --git a/app/views/admin/services/show.html.erb b/app/views/admin/services/show.html.erb index c51feb5d..6ace7fd4 100644 --- a/app/views/admin/services/show.html.erb +++ b/app/views/admin/services/show.html.erb @@ -19,6 +19,9 @@
    + + + <%= form_for(@service, as: :service, url: admin_service_path(@service), method: :put, data: {warn_unsaved_changes: true}, builder: OutpostFormBuilder) do |s| %> <%= render "shared/errors", model: @service %> @@ -58,7 +61,7 @@ <%= render "admin/services/editors/cost-options", s: s %> <% end %> - <%= render "shared/collapsible", name: "Opening times", count: @service.regular_schedules.length, id: "schedule-editor", help_text: "Build a set of regular opening times for the service." do %> + <%= render "shared/collapsible", name: "Opening and event times", count: @service.regular_schedules.length, id: "schedule-editor", help_text: "Build a set of regular opening times for the service." do %> <%= render "admin/services/editors/regular-schedule", s: s %> <% end %> diff --git a/app/views/services/_edit_opening_times.html.erb b/app/views/services/_edit_opening_times.html.erb index 15228070..88de8acb 100644 --- a/app/views/services/_edit_opening_times.html.erb +++ b/app/views/services/_edit_opening_times.html.erb @@ -4,7 +4,7 @@ <%= link_to "Go back", service_path(@service), class: "go-back" %>

    Opening times

    - Adding opening times helps people understand when your service runs. + Adding opening and event times helps people understand when your service runs.
    If your service runs in multiple locations you can add different opening times for each location. You can add additional locations <%= link_to 'here', edit_service_path(@service, :section => 'locations') %>.

    From 9ae69dbe7084f9a20222a0aa06de77004ea20077 Mon Sep 17 00:00:00 2001 From: Han Date: Tue, 3 Sep 2024 20:49:07 +0100 Subject: [PATCH 03/22] UI and a LOT of tests --- .../outpost-design-library/_forms.scss | 22 + app/controllers/services_controller.rb | 14 + app/helpers/regular_schedule_helper.rb | 33 - app/javascript/packs/regular-schedule.js | 193 ++++- app/models/regular_schedule.rb | 212 +++++- .../regular_schedule_serializer.rb | 2 +- .../editors/_regular-schedule-fields.html.erb | 430 ++++------- ...5_add_i_cal_fields_to_regular_schedules.rb | 7 +- db/schema.rb | 4 +- spec/models/regular_schedule_spec.rb | 672 ++++++++++++++++++ 10 files changed, 1230 insertions(+), 359 deletions(-) diff --git a/app/assets/stylesheets/outpost-design-library/_forms.scss b/app/assets/stylesheets/outpost-design-library/_forms.scss index 16687247..89d9fcf8 100644 --- a/app/assets/stylesheets/outpost-design-library/_forms.scss +++ b/app/assets/stylesheets/outpost-design-library/_forms.scss @@ -109,6 +109,28 @@ } } + &--three-cols--or { + @media screen and (min-width: $breakpoint-m) { + display: grid; + grid-template-columns: 1fr minmax(auto, max-content) 1fr; + column-gap: 25px; + align-items: flex-end; + } + } + + &--four-cols { + @media screen and (min-width: $breakpoint-m) { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + column-gap: 25px; + } + } + + &--flex-cols { + display: flex; + flex-direction: column; + } + &--no-top-margin { margin-top: 0px; } diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb index ff9af8d5..fac6cba8 100644 --- a/app/controllers/services_controller.rb +++ b/app/controllers/services_controller.rb @@ -116,6 +116,12 @@ def service_params :opens_at, :closes_at, :weekday, + :dtstart, + :interval, + :freq, + :bymonthday, + :until, + :count, :_destroy, ], contacts_attributes: [ @@ -156,6 +162,14 @@ def service_params result_params['local_offer_attributes']['survey_answers'] = result_params['local_offer_attributes']['survey_answers'].to_h.map{|k,v| { id: k.to_i, answer: v['answer']}} end + + + # combine bydate + + if result_params['regular_schedules_attributes']&.[]('byday') + puts params[:regular_schedule][:byday].reject(&:blank?).join(',') + end + result_params end diff --git a/app/helpers/regular_schedule_helper.rb b/app/helpers/regular_schedule_helper.rb index 7a8e8c8e..8a2452d5 100644 --- a/app/helpers/regular_schedule_helper.rb +++ b/app/helpers/regular_schedule_helper.rb @@ -4,37 +4,4 @@ def pretty_weekday(s) weekdays.select{ |w| w[:value] === s["weekday"]}.last[:label] end - def weekdays - [ - { - label: "Monday", - value: 1 - }, - { - label: "Tuesday", - value: 2 - }, - { - label: "Wednesday", - value: 3 - }, - { - label: "Thursday", - value: 4 - }, - { - label: "Friday", - value: 5 - }, - { - label: "Saturday", - value: 6 - }, - { - label: "Sunday", - value: 7 - }, - ] - end - end \ No newline at end of file diff --git a/app/javascript/packs/regular-schedule.js b/app/javascript/packs/regular-schedule.js index 736f1119..c4c13057 100644 --- a/app/javascript/packs/regular-schedule.js +++ b/app/javascript/packs/regular-schedule.js @@ -1,4 +1,17 @@ document.addEventListener("DOMContentLoaded", () => { + setupTimeTypePanelListeners(); + const timeTypePanels = document.querySelectorAll( + "[data-regular-schedule-panel]" + ); + timeTypePanels.forEach((panel) => { + setTimeTypePanelState(panel); + }); +}); + +/** + * Sets up event listeners inside the regular schedule panels + */ +const setupTimeTypePanelListeners = () => { let regularSchedules = document.querySelector( "#regular_schedule_panels.repeater" ); @@ -7,34 +20,176 @@ document.addEventListener("DOMContentLoaded", () => { regularSchedules.addEventListener("change", (e) => { // the panel object const panel = e.target.closest("[data-regular-schedule-panel]"); - - // if the change event is on the time_type radio button - const TimeTypeRadio = e.target.name.match( + const timeTypeMatcher = e.target.name.match( /^service\[regular_schedules_attributes\]\[(\d+)\]\[(time_type)\]$/ ); - if (TimeTypeRadio) { - const selectedRadio = document.querySelector( - `input[name="${e.target.name}"]:checked` - ); - ToggleTimeType(selectedRadio.value, panel); + const repeatMatcher = e.target.name.match( + /^service\[regular_schedules_attributes\]\[(\d+)\]\[(repeats)\]$/ + ); + const freqMatcher = e.target.name.match( + /^service\[regular_schedules_attributes\]\[(\d+)\]\[(freq)\]$/ + ); + + if (timeTypeMatcher) { + setTimeTypePanelState(panel); + } + + if (repeatMatcher) { + const repeat = e.target.checked; + toggleRequired(panel, repeat, "data-required-repeat"); + toggleHidden(panel, !repeat, "data-repeats"); + } + + if (freqMatcher) { + setFreqVisibility(panel, e.target.value); } }); -}); +}; /** - * Toggles the visibility of the opening_time and event_time selections - * @param {*} selected - * @param {*} panel + * Sets the correct state depending on the time type and selected options + * @param {} panel */ -const ToggleTimeType = (selected, panel) => { +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"); - if (selected === "opening_time") { - opening_time.removeAttribute("hidden"); - event_time.setAttribute("hidden", true); - } else if (selected === "event_time") { - opening_time.setAttribute("hidden", true); - event_time.removeAttribute("hidden"); + const repeat = getRepeatState(panel); + const freq = getFreqState(panel); + setFreqVisibility(panel, freq); + + if (timeType === "opening_time") { + // set opening time required fields + toggleRequired(opening_time, true); + // undo event time required fields + toggleRequired(event_time, false); + // remove required from repeat + toggleRequired(panel, false, "data-required-repeat"); + // show opening time panel + toggleHidden(panel, false, "data-opening-time"); + // hide event time panel + toggleHidden(panel, true, "data-event-time"); + } else if (timeType === "event_time") { + // set opening time required fields + toggleRequired(opening_time, false); + // set event time required fields + toggleRequired(event_time, true); + // if repeat checked add back in required if checked + toggleRequired(panel, repeat, "data-required-repeat"); + // hide opening time panel + toggleHidden(panel, true, "data-opening-time"); + // show event time panel + toggleHidden(panel, false, "data-event-time"); + } else { + // set it to opening time and re run this? } }; + +/** + * Sets visibility of the frequency fields + * @param {*} panel + */ +const setFreqVisibility = (panel, freq) => { + // toggle weekly and monthly visibility + if (freq === "week") { + toggleHidden(panel, false, "data-repeats-weekly"); + toggleHidden(panel, true, "data-repeats-monthly"); + } else if (freq === "month") { + toggleHidden(panel, true, "data-repeats-weekly"); + toggleHidden(panel, false, "data-repeats-monthly"); + } +}; + +/************* + * + * UTILITIES + * + *************/ + +/** + * Get the type that the panel is set to rn + * @param {*} panel + * @returns + */ +const getPanelTimeType = (panel) => { + const type = panel.querySelector('[name*="time_type"]:checked'); + return type.value; +}; + +/** + * Gets the repeat state for current panel + * @param {*} panel + * @returns + */ +const getRepeatState = (panel) => { + const repeat = panel.querySelector('[name*="repeat"]'); + return repeat.checked; +}; + +/** + * Gets the freq state for current panel + * @param {*} panel + */ +const getFreqState = (panel) => { + const repeat = panel.querySelector('[name*="freq"]'); + return repeat.value; +}; + +/** + * Toggles the hidden attribute of the panels + * @param {*} panel + * @param {*} hidden + * @param {*} dataField + */ +const toggleHidden = (panel, hidden, dataField) => { + const element = panel.querySelectorAll(`[${dataField}]`); + // console.log("toggleHidden", panel, hidden, dataField, `[${dataField}]`); + // console.log(element); + + element.forEach((elm) => { + console.log(elm, hidden); + if (hidden) { + elm.setAttribute("hidden", true); + } else { + elm.removeAttribute("hidden"); + } + }); +}; + +/** + * Toggles the required attribute of the fields, optionally takes a different datafield + * @param {*} panel + * @param {*} required + * @param {*} dataField + */ +const toggleRequired = (panel, required, dataField = "data-required") => { + // console.log("ToggleRequired", required, dataField); + // console.log(panel.querySelectorAll(`[${dataField}]`)); + panel.querySelectorAll(`[${dataField}]`).forEach((input) => { + const inputField = input.querySelector("input"); + const selectField = input.querySelector("select"); + + if (required) { + input.classList.add("field--required"); + if (inputField) { + inputField.setAttribute("required", true); + } + + if (selectField) { + selectField.setAttribute("required", true); + } + } else { + input.classList.remove("field--required"); + if (inputField) { + inputField.removeAttribute("required"); + } + + if (selectField) { + selectField.removeAttribute("required"); + } + } + }); +}; diff --git a/app/models/regular_schedule.rb b/app/models/regular_schedule.rb index dffcf493..16aaace6 100644 --- a/app/models/regular_schedule.rb +++ b/app/models/regular_schedule.rb @@ -1,23 +1,227 @@ class RegularSchedule < ApplicationRecord + # weekday + enum weekday: { monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 7 } + + # frequency is a string that can be either 'WEEKLY' or 'MONTHLY' + enum freq: { week: 'WEEKLY', month: 'MONTHLY' } + + belongs_to :service belongs_to :service_at_location, optional: true validates_presence_of :weekday validates_presence_of :opens_at validates_presence_of :closes_at + validate :validate_hours + validate :validate_byday_format + validate :validate_bymonthday_range + validate :validate_event_type + validate :validate_repeated_event + validate :validate_bymonthday_and_dtstart + validate :validate_interval + + # byday + def self.byday + { + 'monday' => 'MO', + 'tuesday' => 'TU', + 'wednesday' => 'WE', + 'thursday' => 'TH', + 'friday' => 'FR', + 'saturday' => 'SA', + 'sunday' => 'SU' + } + end + # weekofmonth + # only allowing specific values for weekofmonth to make things easier + def self.weekofmonth + { + first: "1", + second: "2", + third: "3", + fourth: "4", + fifth: "5", + last: "-1" + } + end - # frequency is a string that can be either 'WEEKLY' or 'MONTHLY' - enum freq: { week: 'WEEKLY', month: 'MONTHLY' } - # byday - enum byday: { monday: 'MO', tuesday: 'TU', wednesday: 'WE', thursday: 'TH', friday: 'FR', saturday: 'SA', sunday: 'SU' } + # callbacks + + before_validation :set_weekday_from_dtstart + before_validation :set_byday_bymonthday_from_dtstart + before_validation :set_interval + + private + + + # validations + + + # cant have opens_at after closes_at def validate_hours if opens_at.present? && closes_at.present? && opens_at > closes_at errors.add(:base, :impossible_hours) end end + + # byday + # and must be SU,MO,TU,WE,TH,FR,SA or SU + # byday must be in the format (-)[1-5]SU,(-)[1-5]MO,(-)[1-5]TU,(-)[1-5]WE,(-)[1-5]TH,(-)[1-5]FR,(-)[1-5]SA + # validate byday format + # if week + # `MO`, `MO,TU` is valid + # `MO,MO` is invalid + # if month + # `[-1,1,2,3,4,5][MO,TU,WE,TH,FR,SA,SU]` is valid + # `MO,TU` is invalid + def validate_byday_format + if byday.present? + valid_days = RegularSchedule.byday.values + if freq == 'week' + days = byday.split(',') + if days.uniq.length != days.length + errors.add(:base, "byday values must be unique") + end + days.each do |day| + unless valid_days.include?(day) + errors.add(:base, "byday must be one of #{valid_days.join(', ')}") + end + end + elsif freq == 'month' + unless byday.match?(/^((-?[1-5]#{valid_days.join('|-?[1-5]')})(,(-?[1-5]#{valid_days.join('|-?[1-5]')}))*)$/) + errors.add(:base, "byday must be in the format (-)[1-5]#{valid_days.join(',')}") + end + end + end + end + + # interval must be greater than or equal to 1 + def validate_interval + if interval.present? && interval < 1 + errors.add(:base, "Interval must be greater than or equal to 1") + end + end + + + # bymonthday must be between 1 and 31 + def validate_bymonthday_range + if bymonthday.present? && (bymonthday < 1 || bymonthday > 31) + errors.add(:base, "By month day must be between 1 and 31") + end + end + + # bymonthday must be the same as the day of the month in dtstart + def validate_bymonthday_and_dtstart + if bymonthday.present? && dtstart.present? && bymonthday.to_i != dtstart.strftime('%-d').to_i + errors.add(:base, "By month day must be the same as the day of the month in dtstart") + end + end + + + + + # validations for event times vs opening times + # opening_time + # weekday, opens_at, closes_at (also byday, bymonthday, until, count are never permitted) + # event_time + # dtstart, weekday, opens_at, closes_at, (also byday, bymonthday, until, count are permitted) + def validate_event_type + + if dtstart.present? + # Event time: dtstart, weekday, opens_at, closes_at are required + if weekday.blank? || opens_at.blank? || closes_at.blank? + errors.add(:base, "dtstart, weekday, opens_at, and closes_at are required for event times") + end + else + # Opening time: weekday, opens_at, closes_at are required + if weekday.blank? || opens_at.blank? || closes_at.blank? + errors.add(:base, "weekday, opens_at, and closes_at are required for opening times") + end + # byday, bymonthday, until, count must be empty + if byday.present? + errors.add(:base, "byday should be empty for opening times") + end + if bymonthday.present? + errors.add(:base, "bymonthday should be empty for opening times") + end + if self.until.present? + errors.add(:base, "until should be empty for opening times") + end + if count.present? + errors.add(:base, "count should be empty for opening times") + end + end + + end + + # validations for repeated events + def validate_repeated_event + + # freq and interval tells us its a repeatable event + # to make life easier we set interval to 1 if its not set + if freq.present? && interval.present? + if freq == 'week' + # if freq is weekly + # only byday can be set, bymonthday is not allowed at all + if bymonthday.present? + errors.add(:base, "bymonthday is not allowed for weekly schedules") + end + elsif freq == 'month' + # if freq is monthly + # only byday or bymonthday can be set + if self.byday.present? && bymonthday.present? + errors.add(:base, "Only byday or bymonthday can be set, not both") + end + end + + # Only until or count can be set, but not both + if self.until.present? && count.present? + errors.add(:base, "Only until or count can be set, not both") + end + + end + + end + + # callbacks + + # Weekday is required in open referral but doesn't always make sense for users to input it + def set_weekday_from_dtstart + if dtstart.present? && weekday.blank? + day_name = dtstart.strftime('%A').downcase + day_value = RegularSchedule.weekdays[day_name].to_i + self[:weekday] = day_name + end + end + + # set interval if its not set + def set_interval + if freq.present? && interval.blank? + self[:interval] = 1 + end + end + + + # set byday and bymonthday if they're not set + def set_byday_bymonthday_from_dtstart + # if weekly - byday is required but the days might not be selected by the user + if freq == 'week' && dtstart.present? + if byday.blank? + day_name = dtstart.strftime('%A').downcase + day_value = RegularSchedule.byday[day_name] + self[:byday] = day_value + end + # if monthly - bymonthday is required but the days might not be selected by the user + elsif freq == 'month' && dtstart.present? + if !self.byday.present? && bymonthday.blank? + self[:bymonthday] = dtstart.strftime('%-d') + end + end + end + end diff --git a/app/serializers/regular_schedule_serializer.rb b/app/serializers/regular_schedule_serializer.rb index a43c004a..06036247 100644 --- a/app/serializers/regular_schedule_serializer.rb +++ b/app/serializers/regular_schedule_serializer.rb @@ -4,7 +4,7 @@ class RegularScheduleSerializer < ActiveModel::Serializer attributes :id, :weekday, :opens_at, :closes_at def weekday - weekdays.find{ |d| d[:value] === object.weekday }[:label] + RegularSchedule.weekdays.find{ |d| d[:value] === object.weekday }[:label] end def opens_at diff --git a/app/views/admin/services/editors/_regular-schedule-fields.html.erb b/app/views/admin/services/editors/_regular-schedule-fields.html.erb index 08d01519..1a5afb35 100644 --- a/app/views/admin/services/editors/_regular-schedule-fields.html.erb +++ b/app/views/admin/services/editors/_regular-schedule-fields.html.erb @@ -1,4 +1,4 @@ -
    +
    <% if @service.locations.count > 1 %>
    @@ -9,353 +9,183 @@
    <% end %> - -

    Opening times

    - -

    I'm a shop keeper

    -
      -
    • I want to log my opening hours at all locations
        -
      • day, opens, closes
      • -
      -
    • -
    • I want to log my opening hours at specific location
        -
      • service_at_location_id, day, opens, closes
      • -
      -
    • -
    - - - - - -
    -
    - <%= sched.label :weekday, "Day", class: "field__label" %> - <%= sched.select :weekday, weekdays.collect {|d| [ d[:label], d[:value] ] }, {}, class: "field__input" %> -
    -
    - <% open_val = sched.object.opens_at.strftime("%H:%M") if sched.object.opens_at.present? %> - <%= sched.label :opens_at, class: "field__label" %> - <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60 %> -
    -
    - <% close_val = sched.object.closes_at.strftime("%H:%M") if sched.object.closes_at.present? %> - <%= sched.label :closes_at, class: "field__label" %> - <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60 %> -
    -
    - - - -

    I'm a service owner - one time events

    - -

    I'm a service owner

    -
      -
    • I want to put in a one time event at all locations
        -
      • date (dtstart), time start (opens_at), time finish (closes_at)
      • -
      -
    • -
    • -

      I want to put in a one time event at specific location

      -
        -
      • service_at_location_id, date, time start, time finish
      • -
      -
    • -
    • -

      I want to put in a number of one time events at all locations

      -
        -
      • date, time start, time finish
      • -
      • -
          -
        • add a schedule
        • -
        -
      • -
      • date, time start, time finish
      • -
      -
    • -
    • -

      I want to put in a number of one time events at specific location

      -
        -
      • service_at_location_id, date, time start, time finish
      • -
      • -
          -
        • add a schedule
        • -
        -
      • -
      • service_at_location_id, , date, time start, time finish
      • -
      -
    • -
    - -
    -
    - <%= sched.label :dtstart, "Date", class: "field__label" %> - <%= sched.date_field :dtstart, class: "field__input" %> -
    -
    - <% open_val = sched.object.opens_at.strftime("%H:%M") if sched.object.opens_at.present? %> - <%= sched.label :opens_at, "Start time", class: "field__label" %> - <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60 %> -
    -
    - <% close_val = sched.object.closes_at.strftime("%H:%M") if sched.object.closes_at.present? %> - <%= sched.label :closes_at, "End time", class: "field__label" %> - <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60 %> -
    -
    - - - -

    I'm a service owner - repeating event(s)

    - - - -
      -
    • -

      I want to put in a repeating event at all locations

      -
        -
      • starting from (dtstart)
      • -
      • number (interval)
      • -
      • every WEEKLY|MONTHLY (freq)
      • -
      • if weekly
          -
        • on any combo of one of these MO,TU,WE,TH,FR,SA,SU - (byday)
        • -
        -
      • -
      • if monthly
          -
        • every +/-n[MO,TU,WE,TH,FR,SA,SU] (byday)
        • -
        • or every 1-30/31? (bymonthday)
        • -
        -
      • -
      -
    • -
    • I want to put in a repeating event at specific locations
        -
      • as above but add service_at_location_id
      • -
      -
    • -
    - - - -
    -
    - <%= sched.label :dtstart, "Date", class: "field__label" %> - <%= sched.date_field :dtstart, class: "field__input" %> -
    +
    - <% open_val = sched.object.opens_at.strftime("%H:%M") if sched.object.opens_at.present? %> - <%= sched.label :opens_at, "Start time", class: "field__label" %> - <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60 %> -
    -
    - <% close_val = sched.object.closes_at.strftime("%H:%M") if sched.object.closes_at.present? %> - <%= sched.label :closes_at, "End time", class: "field__label" %> - <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60 %> -
    -
    - - -

    Repeats

    - -
    -
    - <%= sched.label :interval, "Every", class: "field__label" %> - <%= sched.number_field :interval, class: "field__input"%> +
    + <%= radio_button_tag "service[regular_schedules_attributes][#{sched.index}][time_type]", 'opening_time', true, class: "radio__input" %> + <%= label_tag "service_regular_schedules_attributes_#{sched.index}_time_type_opening_time", 'Opening time', class: "radio__label" %> +
    - <%= sched.label :freq, "Weekly/Monthly", class: "field__label" %> - <%= sched.collection_select :freq, RegularSchedule.freqs.keys, :to_s, :humanize, {}, { class: "field__input" } %> +
    + <%= radio_button_tag "service[regular_schedules_attributes][#{sched.index}][time_type]", 'event_time', false, class: "radio__input" %> + <%= label_tag "service_regular_schedules_attributes_#{sched.index}_time_type_event_time", 'Event time', class: "radio__label" %> +
    + - -

    If weekly - on the

    - - - -
    -
    -

    On the following days

    - <% RegularSchedule.bydays.each do |day, code| %> -
    - <% check_val = sched.object.byday.include?(code) if sched.object.byday.present? %> - <%= check_box_tag "regular_schedule[byday][]", code, check_val, id: "regular_schedule_byday_#{code}", class: "checkbox__input" %> - <%= label_tag "regular_schedule_byday_#{code}", day.humanize, class: "checkbox__label" %> + <%# OPENING TIMES %> +
    +
    +
    + <%= sched.label :weekday, "Day", class: "field__label" %> + <%= sched.select :weekday, options_for_select(RegularSchedule.weekdays.keys.map { |k| [k.humanize, k] }), {}, class: "field__input", required: true %> +
    +
    + <% open_val = sched.object.opens_at.strftime("%H:%M") if sched.object.opens_at.present? %> + <%= sched.label :opens_at, class: "field__label" %> + <%= sched.time_field :opens_at, value: open_val, class: "field__input", step: 60, required: true %> +
    +
    + <% close_val = sched.object.closes_at.strftime("%H:%M") if sched.object.closes_at.present? %> + <%= sched.label :closes_at, class: "field__label" %> + <%= sched.time_field :closes_at, value: close_val, class: "field__input", step: 60, required: true %>
    - <% end %>
    + <%# EVENT TIMES %> +