Skip to content
Open
2 changes: 1 addition & 1 deletion .opencode/agents/rails-code-auditor.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Review code for quality and Rails conventions (report + suggest on request)
mode: subagent
model: minimax-coding-plan/MiniMax-M2.5
model: minimax-coding-plan/MiniMax-M2.7
permission:
skill:
"rails-code-quality": "allow"
Expand Down
2 changes: 1 addition & 1 deletion .opencode/agents/rails-migration-manager.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Manage Rails migrations - create, run, rollback, and troubleshoot
mode: subagent
model: minimax-coding-plan/MiniMax-M2.5
model: minimax-coding-plan/MiniMax-M2.7
permission:
skill:
"rails-migrations": "allow"
Expand Down
2 changes: 1 addition & 1 deletion .opencode/agents/rails-refactor.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Refactor code following Rails and project conventions
mode: subagent
model: minimax-coding-plan/MiniMax-M2.5
model: minimax-coding-plan/MiniMax-M2.7
permission:
skill:
"rails-code-quality": "allow"
Expand Down
2 changes: 1 addition & 1 deletion .opencode/agents/rails-resource-builder.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Generate complete Rails resources (models, controllers, routes, tests)
mode: subagent
model: minimax-coding-plan/MiniMax-M2.5
model: minimax-coding-plan/MiniMax-M2.7
permission:
skill:
"rails-models": "allow"
Expand Down
2 changes: 1 addition & 1 deletion .opencode/agents/rails-test-runner.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Execute tests and report results only
mode: subagent
model: minimax-coding-plan/MiniMax-M2.5
model: minimax-coding-plan/MiniMax-M2.7
permission:
skill:
"rspec-testing": "allow"
Expand Down
2 changes: 1 addition & 1 deletion .opencode/agents/ruby-gem-updater.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Execute Ruby gem updates with version checking, breaking change analysis, and testing
mode: subagent
model: minimax-coding-plan/MiniMax-M2.5
model: minimax-coding-plan/MiniMax-M2.7
permission:
skill:
"ruby-gem-update": "allow"
Expand Down
6 changes: 4 additions & 2 deletions app/components/facilities/discard_reason_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ class Facilities::DiscardReasonComponent < ViewComponent::Base
attr_reader :discard_reason

VALID_REASONS = {
nil => "None",
none: "None",
closed: "Closed",
duplicated: "Duplicated"
duplicated: "Duplicated",
sync_removed: "Removed by Sync"
}.freeze

def initialize(discard_reason)
Expand All @@ -20,7 +22,7 @@ def self.select_options
end

def call
text = VALID_REASONS[discard_reason] || "Unsupported value '#{discard_reason}'"
text = VALID_REASONS[discard_reason.presence] || "Unsupported value '#{discard_reason}'"
tag.span(text)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@
<div class='column'>
<!-- Descriptions -->
<table class='table'>
<tr>
<th>External ID:</th>
<td><%= facility.external_id %></td>
</tr>
<tr>
<th>Website:</th>
<td><%= link_to_website %></td>
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/admin/facilities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ def load_facilities
)
end

facilities = facilities.order(updated_at: :asc)
facilities = facilities.with_associations.order(updated_at: :desc)

@pagy, @facilities = pagy(facilities)
end

def load_facility
@facility = Facility.find(params[:id])
@facility = Facility.with_associations.find(params[:id])
end

def load_services_dropdown
Expand Down
28 changes: 25 additions & 3 deletions app/controllers/admin/tools_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,40 @@ def import_facilities

result = External::VancouverCity::Syncer.call(
api_key: api_key,
api_client: External::VancouverCity.default_client
api_client: External::VancouverCity.default_client,
full_sync: true
)

if result.success?
total_count = result.data[:total_count] || 0
redirect_to admin_facilities_path(service: "water_fountain"), notice: "#{total_count} Facilities imported successfully from #{External::ApiHelper.api_name(api_key)}."
created = result.data[:created_count] || 0
updated = result.data[:updated_count] || 0
deleted = result.data[:deleted_count] || 0
redirect_to admin_facilities_path(service: "water_fountain"), notice: "Sync complete: #{created} created, #{updated} updated, #{deleted} removed from #{External::ApiHelper.api_name(api_key)}."
else
error_messages = result.errors.join(", ")
redirect_to admin_tools_path, alert: "Failed to import facilities: #{error_messages}"
end
end

def discard_facilities
api_key = params[:api]

unless External::ApiHelper.supported_api?(api_key)
redirect_to admin_tools_path, alert: "Invalid API selected. Please choose from the supported APIs."
return
end

result = External::VancouverCity::DiscardService.call(api_key: api_key)

if result.success?
discarded_count = result.data[:discarded_count] || 0
redirect_to admin_facilities_path(service: "water_fountain"), notice: "#{discarded_count} facilities discarded from #{External::ApiHelper.api_name(api_key)}."
else
error_messages = result.errors.join(", ")
redirect_to admin_tools_path, alert: "Failed to discard facilities: #{error_messages}"
end
end

# Helper method for the view
helper_method :api_options_for_select

Expand Down
7 changes: 1 addition & 6 deletions app/controllers/api/facilities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@ def load_facilities
end

# Includes related objects to avoid N+1 queries
@facilities = @facilities.includes(
:zone,
:facility_welcomes,
{ facility_services: [:service] },
{ schedules: [:time_slots] }
)
@facilities = @facilities.with_associations
end

def register_impressions
Expand Down
6 changes: 6 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ application.register("navigate", NavigateController)

import PagyController from "controllers/pagy_controller"
application.register("pagy", PagyController)

import ProgressController from "controllers/progress_controller"
application.register("progress", ProgressController)

import TabsController from "controllers/tabs_controller"
application.register("tabs", TabsController)
36 changes: 36 additions & 0 deletions app/javascript/controllers/progress_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["indicator", "message"]

connect() {
console.log("Progress Controller Connected", this.element)
}

submit(event) {
const form = event.currentTarget
const button = form.querySelector('input[type="submit"], button[type="submit"]')
const operation = form.dataset.operation

if (this.hasMessageTarget) {
this.messageTarget.textContent = this.getMessage(operation)
}

if (button) {
button.classList.add("is-loading")
button.disabled = true
}

if (this.hasIndicatorTarget) {
this.indicatorTarget.classList.remove("is-hidden")
}
}

getMessage(operation) {
const messages = {
import: "Importing facilities from Vancouver City API...",
discard: "Discarding facilities from Vancouver City API..."
}
return messages[operation] || "Processing..."
}
}
52 changes: 52 additions & 0 deletions app/javascript/controllers/tabs_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["link", "content"]

initialize() {
this.activeTab = this.element.dataset.activeTab || "sync"
}

connect() {
console.log("Tabs Controller Connected", this.element);

this.boundSwitchTab = this.switchTab.bind(this)
this.linkTargets.forEach((link) => {
link.addEventListener('click', this.boundSwitchTab)
})
this.activateTab(this.activeTab)
}

disconnect() {
this.linkTargets.forEach((link) => {
link.removeEventListener('click', this.boundSwitchTab)
})
}

switchTab(event) {
event.preventDefault()

const tab = event.currentTarget.dataset.tab
this.activateTab(tab)
}

activateTab(tab) {
this.linkTargets.forEach((link) => {
if (link.dataset.tab === tab) {
link.closest("li").classList.add("is-active")
} else {
link.closest("li").classList.remove("is-active")
}
})

this.contentTargets.forEach((content) => {
if (content.id === tab) {
content.classList.remove("is-hidden")
} else {
content.classList.add("is-hidden")
}
})

this.activeTab = tab
}
}
11 changes: 0 additions & 11 deletions app/jobs/facilities_static_generator_job.rb

This file was deleted.

40 changes: 24 additions & 16 deletions app/models/facility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ class Facility < ApplicationRecord
has_many :time_slots, through: :schedules

enum :discard_reason, {
none: nil,
closed: "closed",
duplicated: "duplicated"
}, prefix: true, default: :none
duplicated: "duplicated",
sync_removed: "sync_removed"
}, prefix: true, default: nil

validates :name, presence: true
validates :external_id, uniqueness: { allow_nil: true }
validate :validate_website

with_options if: :verified? do
Expand All @@ -42,23 +43,30 @@ class Facility < ApplicationRecord
scope :without_welcomes, -> { where.not(facility_welcomes: FacilityWelcome.all) }
scope :external, -> { where.not(external_id: nil) }
scope :not_external, -> { where(external_id: nil) }
scope :with_associations, lambda {
includes(
:zone,
:facility_welcomes,
{ facility_services: [:service] },
{ schedules: [:time_slots] }
)
}

def discard_reason_none?
discard_reason.nil?
end

def managed_by?(user_or_user_id)
return false if user_or_user_id.blank?
f_user = if user_or_user_id.respond_to? :id
user_or_user_id
else
User.find_by(id: user_or_user_id)
end

f_user_id = if user_or_user_id.respond_to? :id
user_or_user_id.id
else
user_or_user_id
end
return false if f_user.blank?

# Case Facility's User is the same
return true if user_id == f_user_id
# Case Zone of the Facility has the user as admin
return true if User.find(f_user_id).manages.any?

# Otherwise return FALSE
false
user_id == f_user.id || f_user.manages.exists?(id: id)
end

def self.managed_by(user)
Expand Down Expand Up @@ -157,7 +165,7 @@ def clean_data
end

# handles discard
self.discard_reason = :none if undiscarded?
self.discard_reason = nil if undiscarded?
end

def distance(to_coord: nil, to_lat: nil, to_long: nil, to_facility: nil)
Expand Down
8 changes: 8 additions & 0 deletions app/services/application_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ def invalid?
!valid?
end

def success(data = {})
Result.new(data: data, errors: [])
end

def failure(errors)
Result.new(data: nil, errors: Array(errors))
end

private

def errors
Expand Down
24 changes: 24 additions & 0 deletions app/services/external/vancouver_city/discard_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

class External::VancouverCity::DiscardService < ApplicationService
attr_reader :api_key

def initialize(api_key:)
super()
@api_key = api_key
end

def call
return failure(["Unsupported API: #{api_key}"]) unless External::ApiHelper.supported_api?(api_key)

discarded_count = 0

Facility.external.kept.find_each do |facility|
facility.discard_reason = :sync_removed
facility.discard!
discarded_count += 1
end

success({ discarded_count: discarded_count })
end
end
Loading
Loading