Finished phase006
This commit is contained in:
@@ -38,9 +38,20 @@ class FuelEntriesController < ApplicationController
|
||||
end
|
||||
|
||||
def prepare_vehicle_show
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.order(:maintenance_type)
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.active.order(:maintenance_type)
|
||||
@maintenance_schedule_statuses = maintenance_schedule_statuses(@maintenance_schedules)
|
||||
@fuel_entries = @vehicle.fuel_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||
@maintenance_entries = @vehicle.maintenance_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||
@maintenance_entry = @vehicle.maintenance_entries.build(date: Date.current)
|
||||
end
|
||||
|
||||
def maintenance_schedule_statuses(schedules)
|
||||
schedules.index_with do |schedule|
|
||||
latest_entry = @vehicle.maintenance_entries
|
||||
.where("lower(name) = ?", schedule.maintenance_type.downcase)
|
||||
.order(date: :desc, created_at: :desc)
|
||||
.first
|
||||
MaintenanceScheduleStatus.new(schedule, latest_entry, current_odometer: @vehicle.current_odometer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,9 +53,20 @@ class MaintenanceEntriesController < ApplicationController
|
||||
end
|
||||
|
||||
def prepare_vehicle_show
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.order(:maintenance_type)
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.active.order(:maintenance_type)
|
||||
@maintenance_schedule_statuses = maintenance_schedule_statuses(@maintenance_schedules)
|
||||
@fuel_entries = @vehicle.fuel_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||
@maintenance_entries = @vehicle.maintenance_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||
@fuel_entry = @vehicle.fuel_entries.build(date: Date.current)
|
||||
end
|
||||
|
||||
def maintenance_schedule_statuses(schedules)
|
||||
schedules.index_with do |schedule|
|
||||
latest_entry = @vehicle.maintenance_entries
|
||||
.where("lower(name) = ?", schedule.maintenance_type.downcase)
|
||||
.order(date: :desc, created_at: :desc)
|
||||
.first
|
||||
MaintenanceScheduleStatus.new(schedule, latest_entry, current_odometer: @vehicle.current_odometer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
class MaintenanceSchedulesController < ApplicationController
|
||||
before_action :set_vehicle
|
||||
before_action :set_maintenance_schedule, only: %i[show edit update deactivate]
|
||||
|
||||
def index
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.order(active: :desc, maintenance_type: :asc)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@maintenance_schedule = @vehicle.maintenance_schedules.build(
|
||||
baseline_odometer: @vehicle.current_odometer,
|
||||
baseline_date: Date.current
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@maintenance_schedule = @vehicle.maintenance_schedules.build(maintenance_schedule_params)
|
||||
|
||||
if @maintenance_schedule.save
|
||||
redirect_to vehicle_maintenance_schedules_path(@vehicle), notice: "Maintenance schedule created."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @maintenance_schedule.update(maintenance_schedule_params)
|
||||
redirect_to vehicle_maintenance_schedules_path(@vehicle), notice: "Maintenance schedule updated."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def deactivate
|
||||
@maintenance_schedule.update!(active: false)
|
||||
redirect_to vehicle_maintenance_schedules_path(@vehicle), notice: "Maintenance schedule deactivated."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_vehicle
|
||||
@vehicle = Vehicle.find(params[:vehicle_id])
|
||||
end
|
||||
|
||||
def set_maintenance_schedule
|
||||
@maintenance_schedule = @vehicle.maintenance_schedules.find(params[:id])
|
||||
end
|
||||
|
||||
def maintenance_schedule_params
|
||||
params.require(:maintenance_schedule).permit(
|
||||
:maintenance_type,
|
||||
:mileage_interval,
|
||||
:time_interval,
|
||||
:baseline_odometer,
|
||||
:baseline_date,
|
||||
:active
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -72,10 +72,27 @@ class VehiclesController < ApplicationController
|
||||
end
|
||||
|
||||
def prepare_show
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.order(:maintenance_type)
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.active.order(:maintenance_type)
|
||||
@maintenance_schedule_statuses = maintenance_schedule_statuses(@maintenance_schedules)
|
||||
@fuel_entries = @vehicle.fuel_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||
@maintenance_entries = @vehicle.maintenance_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||
@fuel_entry ||= @vehicle.fuel_entries.build(date: Date.current)
|
||||
@maintenance_entry ||= @vehicle.maintenance_entries.build(date: Date.current)
|
||||
end
|
||||
|
||||
def maintenance_schedule_statuses(schedules)
|
||||
schedules.index_with do |schedule|
|
||||
latest_entry = latest_maintenance_entry_for(schedule)
|
||||
status = MaintenanceScheduleStatus.new(schedule, latest_entry, current_odometer: @vehicle.current_odometer)
|
||||
MaintenanceNotification.record_status_change(schedule, status)
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
def latest_maintenance_entry_for(schedule)
|
||||
@vehicle.maintenance_entries
|
||||
.where("lower(name) = ?", schedule.maintenance_type.downcase)
|
||||
.order(date: :desc, created_at: :desc)
|
||||
.first
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,9 +20,9 @@ class MaintenanceEntry < ApplicationRecord
|
||||
return if vehicle.blank? || name.blank?
|
||||
|
||||
scheduled = vehicle.maintenance_schedules.any? do |schedule|
|
||||
schedule.maintenance_type.casecmp?(name)
|
||||
schedule.active? && schedule.maintenance_type.casecmp?(name)
|
||||
end
|
||||
scheduled ||= vehicle.maintenance_schedules.where("lower(maintenance_type) = ?", name.downcase).exists? if vehicle.persisted?
|
||||
scheduled ||= vehicle.maintenance_schedules.active.where("lower(maintenance_type) = ?", name.downcase).exists? if vehicle.persisted?
|
||||
return if scheduled
|
||||
|
||||
errors.add(:name, "must match a maintenance schedule for the vehicle")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
class MaintenanceNotification
|
||||
NOTIFIABLE_STATUSES = %w[upcoming overdue].freeze
|
||||
|
||||
def self.record_status_change(schedule, status)
|
||||
return if schedule.last_status == status.status
|
||||
|
||||
previous_status = schedule.last_status.presence || "unknown"
|
||||
schedule.update!(last_status: status.status, last_status_changed_at: Time.current)
|
||||
return unless NOTIFIABLE_STATUSES.include?(status.status)
|
||||
|
||||
Rails.logger.warn(
|
||||
"Maintenance schedule #{status.status}: vehicle_id=#{schedule.vehicle_id} " \
|
||||
"maintenance_schedule_id=#{schedule.id} maintenance_type=#{schedule.maintenance_type.inspect} " \
|
||||
"previous_status=#{previous_status} due_odometer=#{status.due_odometer} due_date=#{status.due_date} " \
|
||||
"reason=#{status.reason.inspect}"
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -4,4 +4,18 @@ class MaintenanceSchedule < ApplicationRecord
|
||||
validates :maintenance_type, presence: true, uniqueness: { scope: :vehicle_id, case_sensitive: false }
|
||||
validates :mileage_interval, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :time_interval, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :baseline_odometer, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :baseline_date, presence: true
|
||||
validates :active, inclusion: { in: [ true, false ] }
|
||||
validate :baseline_date_cannot_be_in_the_future
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
private
|
||||
|
||||
def baseline_date_cannot_be_in_the_future
|
||||
return if baseline_date.blank? || baseline_date <= Date.current
|
||||
|
||||
errors.add(:baseline_date, "can't be in the future")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
class MaintenanceScheduleStatus
|
||||
UPCOMING_PERCENT = 0.05
|
||||
|
||||
attr_reader :schedule, :latest_entry, :status, :due_odometer, :due_date
|
||||
|
||||
def initialize(schedule, latest_entry, current_odometer:)
|
||||
@schedule = schedule
|
||||
@latest_entry = latest_entry
|
||||
@current_odometer = current_odometer
|
||||
@due_odometer = baseline_odometer + schedule.mileage_interval
|
||||
@due_date = baseline_date + schedule.time_interval.days
|
||||
@status = calculate_status
|
||||
end
|
||||
|
||||
def overdue?
|
||||
status == "overdue"
|
||||
end
|
||||
|
||||
def upcoming?
|
||||
status == "upcoming"
|
||||
end
|
||||
|
||||
def no_history?
|
||||
status == "no_history"
|
||||
end
|
||||
|
||||
def current?
|
||||
status == "current"
|
||||
end
|
||||
|
||||
def status_label
|
||||
case status
|
||||
when "overdue" then "Overdue"
|
||||
when "upcoming" then "Upcoming"
|
||||
when "no_history" then "No history"
|
||||
else "Current"
|
||||
end
|
||||
end
|
||||
|
||||
def reason
|
||||
case status
|
||||
when "overdue"
|
||||
overdue_reasons.join(" and ")
|
||||
when "upcoming"
|
||||
upcoming_reasons.join(" and ")
|
||||
when "no_history"
|
||||
"No matching maintenance entry has been recorded."
|
||||
else
|
||||
"Maintenance is current."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_odometer
|
||||
|
||||
def calculate_status
|
||||
return "overdue" if mileage_overdue? || time_overdue?
|
||||
return "upcoming" if mileage_upcoming? || time_upcoming?
|
||||
return "no_history" if latest_entry.blank?
|
||||
|
||||
"current"
|
||||
end
|
||||
|
||||
def baseline_odometer
|
||||
latest_entry&.odometer || schedule.baseline_odometer
|
||||
end
|
||||
|
||||
def baseline_date
|
||||
latest_entry&.date || schedule.baseline_date
|
||||
end
|
||||
|
||||
def mileage_overdue?
|
||||
current_odometer >= due_odometer
|
||||
end
|
||||
|
||||
def time_overdue?
|
||||
Date.current >= due_date
|
||||
end
|
||||
|
||||
def mileage_upcoming?
|
||||
current_odometer >= due_odometer - mileage_upcoming_window
|
||||
end
|
||||
|
||||
def time_upcoming?
|
||||
Date.current >= due_date - time_upcoming_window.days
|
||||
end
|
||||
|
||||
def mileage_upcoming_window
|
||||
(schedule.mileage_interval * UPCOMING_PERCENT).ceil
|
||||
end
|
||||
|
||||
def time_upcoming_window
|
||||
(schedule.time_interval * UPCOMING_PERCENT).ceil
|
||||
end
|
||||
|
||||
def overdue_reasons
|
||||
reasons = []
|
||||
reasons << "mileage interval reached" if mileage_overdue?
|
||||
reasons << "time interval reached" if time_overdue?
|
||||
reasons
|
||||
end
|
||||
|
||||
def upcoming_reasons
|
||||
reasons = []
|
||||
reasons << "within #{mileage_upcoming_window} miles of due odometer" if mileage_upcoming?
|
||||
reasons << "within #{time_upcoming_window} days of due date" if time_upcoming?
|
||||
reasons
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
<%= form_with model: [vehicle, maintenance_schedule] do |form| %>
|
||||
<% if maintenance_schedule.errors.any? %>
|
||||
<section>
|
||||
<h2><%= pluralize(maintenance_schedule.errors.count, "error") %> prevented this maintenance schedule from being saved</h2>
|
||||
|
||||
<ul>
|
||||
<% maintenance_schedule.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :maintenance_type %><br>
|
||||
<%= form.text_field :maintenance_type, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :mileage_interval %><br>
|
||||
<%= form.number_field :mileage_interval, min: 1, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :time_interval, "Time interval in days" %><br>
|
||||
<%= form.number_field :time_interval, min: 1, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :baseline_odometer %><br>
|
||||
<%= form.number_field :baseline_odometer, min: 0, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :baseline_date %><br>
|
||||
<%= form.date_field :baseline_date, max: Date.current, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.check_box :active %>
|
||||
<%= form.label :active %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,7 @@
|
||||
<main>
|
||||
<p><%= link_to "Back to maintenance schedules", vehicle_maintenance_schedules_path(@vehicle) %></p>
|
||||
|
||||
<h1>Edit Maintenance Schedule</h1>
|
||||
|
||||
<%= render "form", vehicle: @vehicle, maintenance_schedule: @maintenance_schedule %>
|
||||
</main>
|
||||
@@ -0,0 +1,46 @@
|
||||
<main>
|
||||
<p><%= notice %></p>
|
||||
|
||||
<p><%= link_to "Back to vehicle", vehicle_path(@vehicle) %></p>
|
||||
|
||||
<h1>Maintenance Schedules</h1>
|
||||
<p><%= @vehicle.year %> <%= @vehicle.make %> <%= @vehicle.model %></p>
|
||||
|
||||
<p><%= link_to "New maintenance schedule", new_vehicle_maintenance_schedule_path(@vehicle) %></p>
|
||||
|
||||
<% if @maintenance_schedules.any? %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Mileage interval</th>
|
||||
<th>Time interval</th>
|
||||
<th>Baseline odometer</th>
|
||||
<th>Baseline date</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @maintenance_schedules.each do |schedule| %>
|
||||
<tr>
|
||||
<td><%= link_to schedule.maintenance_type, vehicle_maintenance_schedule_path(@vehicle, schedule) %></td>
|
||||
<td><%= number_with_delimiter(schedule.mileage_interval) %> miles</td>
|
||||
<td><%= schedule.time_interval %> days</td>
|
||||
<td><%= number_with_delimiter(schedule.baseline_odometer) %></td>
|
||||
<td><%= schedule.baseline_date %></td>
|
||||
<td><%= schedule.active? ? "Active" : "Inactive" %></td>
|
||||
<td>
|
||||
<%= link_to "Edit", edit_vehicle_maintenance_schedule_path(@vehicle, schedule) %>
|
||||
<% if schedule.active? %>
|
||||
<%= button_to "Deactivate", deactivate_vehicle_maintenance_schedule_path(@vehicle, schedule), method: :patch %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No maintenance schedules recorded.</p>
|
||||
<% end %>
|
||||
</main>
|
||||
@@ -0,0 +1,7 @@
|
||||
<main>
|
||||
<p><%= link_to "Back to maintenance schedules", vehicle_maintenance_schedules_path(@vehicle) %></p>
|
||||
|
||||
<h1>New Maintenance Schedule</h1>
|
||||
|
||||
<%= render "form", vehicle: @vehicle, maintenance_schedule: @maintenance_schedule %>
|
||||
</main>
|
||||
@@ -0,0 +1,26 @@
|
||||
<main>
|
||||
<p><%= notice %></p>
|
||||
|
||||
<p><%= link_to "Back to maintenance schedules", vehicle_maintenance_schedules_path(@vehicle) %></p>
|
||||
|
||||
<h1><%= @maintenance_schedule.maintenance_type %></h1>
|
||||
|
||||
<dl>
|
||||
<dt>Mileage interval</dt>
|
||||
<dd><%= number_with_delimiter(@maintenance_schedule.mileage_interval) %> miles</dd>
|
||||
|
||||
<dt>Time interval</dt>
|
||||
<dd><%= @maintenance_schedule.time_interval %> days</dd>
|
||||
|
||||
<dt>Baseline odometer</dt>
|
||||
<dd><%= number_with_delimiter(@maintenance_schedule.baseline_odometer) %></dd>
|
||||
|
||||
<dt>Baseline date</dt>
|
||||
<dd><%= @maintenance_schedule.baseline_date %></dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd><%= @maintenance_schedule.active? ? "Active" : "Inactive" %></dd>
|
||||
</dl>
|
||||
|
||||
<p><%= link_to "Edit", edit_vehicle_maintenance_schedule_path(@vehicle, @maintenance_schedule) %></p>
|
||||
</main>
|
||||
@@ -49,6 +49,54 @@
|
||||
<section>
|
||||
<h2>Maintenance Schedules</h2>
|
||||
|
||||
<p><%= link_to "Manage maintenance schedules", vehicle_maintenance_schedules_path(@vehicle) %></p>
|
||||
|
||||
<% overdue_statuses = @maintenance_schedule_statuses.values.select(&:overdue?) %>
|
||||
<% upcoming_statuses = @maintenance_schedule_statuses.values.select(&:upcoming?) %>
|
||||
<% no_history_statuses = @maintenance_schedule_statuses.values.select(&:no_history?) %>
|
||||
|
||||
<% if overdue_statuses.any? %>
|
||||
<section>
|
||||
<h3>Overdue Maintenance</h3>
|
||||
<ul>
|
||||
<% overdue_statuses.each do |status| %>
|
||||
<li>
|
||||
<%= status.schedule.maintenance_type %> is overdue: <%= status.reason %>.
|
||||
Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>.
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<% if upcoming_statuses.any? %>
|
||||
<section>
|
||||
<h3>Upcoming Maintenance</h3>
|
||||
<ul>
|
||||
<% upcoming_statuses.each do |status| %>
|
||||
<li>
|
||||
<%= status.schedule.maintenance_type %> is upcoming: <%= status.reason %>.
|
||||
Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>.
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<% if no_history_statuses.any? %>
|
||||
<section>
|
||||
<h3>No Maintenance History</h3>
|
||||
<ul>
|
||||
<% no_history_statuses.each do |status| %>
|
||||
<li>
|
||||
<%= status.schedule.maintenance_type %> has no matching maintenance entry yet.
|
||||
Baseline due point is <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>.
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<% if @maintenance_schedules.any? %>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -56,14 +104,21 @@
|
||||
<th>Type</th>
|
||||
<th>Mileage interval</th>
|
||||
<th>Time interval</th>
|
||||
<th>Next due odometer</th>
|
||||
<th>Next due date</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @maintenance_schedules.each do |schedule| %>
|
||||
<% status = @maintenance_schedule_statuses.fetch(schedule) %>
|
||||
<tr>
|
||||
<td><%= schedule.maintenance_type %></td>
|
||||
<td><%= number_with_delimiter(schedule.mileage_interval) %> miles</td>
|
||||
<td><%= schedule.time_interval %> days</td>
|
||||
<td><%= number_with_delimiter(status.due_odometer) %></td>
|
||||
<td><%= status.due_date %></td>
|
||||
<td><%= status.status_label %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user