Skip to content
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
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ group :development, :test do
end

# Faker gem - available in development/test, and in production when ALLOW_FAKE_DATA is set
gem "faker", "~> 3.6", groups: [:development, :test].tap { |groups|
gem "faker", "~> 3.8", groups: [:development, :test].tap { |groups|
groups << :production if ENV['ALLOW_FAKE_DATA'] == 'true'
}

Expand All @@ -76,7 +76,7 @@ group :development do
# powerful developer console.
gem "pry", "~> 0.16.0"
gem "pry-rails"
gem "pry-stack_explorer", "~> 0.6.2"
gem "pry-stack_explorer", "~> 0.6.3"
gem "pry-remote-reloaded"
gem "pry-byebug", "~> 3.12.0"

Expand Down
20 changes: 10 additions & 10 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ GEM
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
faker (3.6.1)
faker (3.8.0)
i18n (>= 1.8.11, < 2)
faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5)
Expand Down Expand Up @@ -203,7 +203,7 @@ GEM
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.19.5)
json (2.19.7)
jwt (3.2.0)
base64
language_server-protocol (3.17.0.5)
Expand Down Expand Up @@ -233,7 +233,7 @@ GEM
msgpack (1.8.0)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.4)
net-imap (0.6.4.1)
date
net-protocol
net-pop (0.1.2)
Expand Down Expand Up @@ -265,7 +265,7 @@ GEM
json
uri
yaml
parallel (2.0.1)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
Expand Down Expand Up @@ -297,14 +297,14 @@ GEM
drb (~> 2.2)
pry (>= 0.15)
slop (~> 4.10)
pry-stack_explorer (0.6.2)
pry-stack_explorer (0.6.3)
binding_of_caller (>= 1.0)
pry (~> 0.13)
psych (5.3.1)
date
stringio
public_suffix (7.0.5)
puma (8.0.1)
puma (8.0.2)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.2.6)
Expand Down Expand Up @@ -393,7 +393,7 @@ GEM
rspec-mocks (>= 3.13.0, < 5.0.0)
rspec-support (>= 3.13.0, < 5.0.0)
rspec-support (3.13.7)
rubocop (1.86.1)
rubocop (1.86.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
Expand Down Expand Up @@ -462,7 +462,7 @@ GEM
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2026.1)
tzinfo-data (1.2026.2)
tzinfo (>= 1.0.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
Expand Down Expand Up @@ -512,7 +512,7 @@ DEPENDENCIES
devise (~> 5.0.4)
dotenv-rails
factory_bot_rails (~> 6.4.3)
faker (~> 3.6)
faker (~> 3.8)
faraday (~> 2.14.2)
geo_coord
geocoder (~> 1.8)
Expand All @@ -531,7 +531,7 @@ DEPENDENCIES
pry-byebug (~> 3.12.0)
pry-rails
pry-remote-reloaded
pry-stack_explorer (~> 0.6.2)
pry-stack_explorer (~> 0.6.3)
puma (~> 8.0)
rack-cors
rack-mini-profiler (~> 4.0)
Expand Down
24 changes: 16 additions & 8 deletions app/controllers/admin/facilities_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
# frozen_string_literal: true

class Admin::FacilitiesController < Admin::BaseController
before_action :load_facilities, only: [:index]
before_action :load_services_dropdown, only: [:index]
before_action :load_welcomes_dropdown, only: [:index]
before_action :load_facility, only: %i[show edit update destroy switch_status]

def index; end
def index
facilities = apply_filters(Facility.all).with_associations.order(updated_at: :desc)
@pagy, @facilities = pagy(facilities)
end

def export
facilities = apply_filters(Facility.all).with_associations.order(updated_at: :desc)
result = Facilities::CsvExporter.call(facilities)

send_data result.data,
filename: "facilities-#{Date.current.iso8601}.csv",
type: "text/csv; charset=utf-8",
disposition: "attachment"
end

def show; end

Expand Down Expand Up @@ -73,15 +85,11 @@ def switch_status

private

def load_facilities
facilities = Facility.all
def apply_filters(facilities)
facilities = filter_by_status(facilities)
facilities = filter_by_service(facilities)
facilities = filter_by_welcome(facilities)
facilities = filter_by_search(facilities)
facilities = facilities.with_associations.order(updated_at: :desc)

@pagy, @facilities = pagy(facilities)
filter_by_search(facilities)
end

def filter_by_status(facilities)
Expand Down
24 changes: 7 additions & 17 deletions app/models/facility_time_slot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,13 @@ def overlapping_time_slots
return FacilityTimeSlot.none unless [from_hour, from_min, to_hour, to_min].all?(&:present?)

start_i = (from_hour + (from_min/60r)).to_f
end_i = (to_hour + (to_min/60r)).to_f

sql_start_i = Arel.sql("(from_hour + (from_min / 60.0))")
sql_end_i = Arel.sql("(to_hour + (to_min / 60.0))")

query_sql = <<~SQL.squish
(
SELECT ts.*,
#{sql_start_i} as start_i,
#{sql_end_i} as end_i
FROM facility_time_slots ts
WHERE (#{sql_start_i} <= #{end_i})
AND (#{sql_end_i} >= #{start_i})
) as facility_time_slots
SQL

FacilityTimeSlot.from(query_sql).where(id: siblings_time_slots)
end_i = (to_hour + (to_min/60r)).to_f

siblings_time_slots.where(
"(from_hour + (from_min / 60.0)) <= ? AND (to_hour + (to_min / 60.0)) >= ?",
end_i,
start_i
)
end

private
Expand Down
97 changes: 97 additions & 0 deletions app/services/facilities/csv_exporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# frozen_string_literal: true

require "csv"

class Facilities::CsvExporter < ApplicationService
WEEKDAYS = %w[sunday monday tuesday wednesday thursday friday saturday].freeze

HEADERS = [
"ID",
"Name",
"Status",
"Address",
"Phone",
"Website",
"Lat",
"Long",
"Zone",
"Services",
"Welcomes",
"Notes",
"External ID",
"Created At",
"Updated At"
] + WEEKDAYS.map(&:titleize)

def initialize(facilities)
super()

@facilities = facilities
end

def call
csv_string = CSV.generate(headers: true) do |csv|
csv << HEADERS

@facilities.each do |facility|
csv << build_row(facility)
end
end

success(csv_string)
end

private

def build_row(facility)
[
facility.id,
facility.name,
facility.status.to_s.titleize,
facility.address,
facility.phone,
facility.website,
facility.lat,
facility.long,
facility.zone&.name,
format_services(facility),
format_welcomes(facility),
facility.notes,
facility.external_id,
facility.created_at&.iso8601,
facility.updated_at&.iso8601
] + format_schedules(facility)
end

def format_services(facility)
facility.facility_services.map do |fs|
if fs.note.present?
"#{fs.service.name} (#{fs.note})"
else
fs.service.name
end
end.join(", ")
end

def format_welcomes(facility)
facility.facility_welcomes.map(&:name).join(", ")
end

def format_schedules(facility)
schedules_by_day = facility.schedules.index_by(&:week_day)

WEEKDAYS.map do |weekday|
schedule = schedules_by_day[weekday]
format_schedule(schedule)
end
end

def format_schedule(schedule)
return "Closed" if schedule.nil? || schedule.closed_all_day?
return "Open All Day" if schedule.open_all_day?

schedule.time_slots.map do |ts|
"#{ts.start_time_for_displaying} - #{ts.end_time_for_displaying}"
end.join(", ")
end
end
1 change: 1 addition & 0 deletions app/views/admin/facilities/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</div>

<div class="level-right">
<%= link_to "Export CSV", export_admin_facilities_path(request.query_parameters), class: "button is-light level-item" %>
<%= link_to "New Facility", new_admin_facility_path, class: "button is-light level-item" %>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
end

resources :facilities do
collection do
get :export
end

member do
put :switch_status
end
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"bulma": "^1.0.2",
"bulma-tooltip": "^3.0.2",
"flatpickr": "^4.6.9",
"sass": "^1.100.0",
"sass": "^1.101.0",
"trix": "^2.1.19"
},
"version": "0.1.0",
Expand Down
62 changes: 62 additions & 0 deletions spec/controllers/admin/facilities_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,68 @@ def stub_facility_find_with(facility)
end
end

describe "GET #export" do
subject(:get_export) { get :export, params: params }

let(:params) { {} }

it "returns a CSV file download" do
get_export
expect(response).to have_http_status(:success)
expect(response.content_type).to include("text/csv")
expect(response.headers["Content-Disposition"]).to include("attachment")
expect(response.headers["Content-Disposition"]).to include("facilities-")
expect(response.headers["Content-Disposition"]).to include(".csv")
end

describe "CSV content" do
before do
create(:facility, name: "Export Test Facility")
get_export
end

it "includes CSV headers" do
expect(response.body).to include("ID,Name,Status,Address")
end

it "includes facility data" do
expect(response.body).to include("Export Test Facility")
end
end

describe "filtering" do
let(:live_facility) { create(:facility, :with_verified, name: "Live Export Facility") }
let(:pending_facility) { create(:facility, verified: false, name: "Pending Export Facility") }

before do
live_facility
pending_facility
end

context "with status filter" do
let(:params) { { status: "live" } }

it "exports only filtered facilities" do
get_export
expect(response.body).to include("Live Export Facility")
expect(response.body).not_to include("Pending Export Facility")
end
end
end

describe "bypasses pagination" do
let(:facilities) { create_list(:facility, 25) }

before { facilities }

it "exports all facilities regardless of pagination" do
get_export
csv = CSV.parse(response.body, headers: true)
expect(csv.length).to eq(25)
end
end
end

describe "GET #show" do
let(:facility) { create(:facility) }

Expand Down
Loading
Loading