Finished phase006
This commit is contained in:
@@ -38,9 +38,20 @@ class FuelEntriesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def prepare_vehicle_show
|
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)
|
@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_entries = @vehicle.maintenance_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||||
@maintenance_entry = @vehicle.maintenance_entries.build(date: Date.current)
|
@maintenance_entry = @vehicle.maintenance_entries.build(date: Date.current)
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -53,9 +53,20 @@ class MaintenanceEntriesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def prepare_vehicle_show
|
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)
|
@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_entries = @vehicle.maintenance_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||||
@fuel_entry = @vehicle.fuel_entries.build(date: Date.current)
|
@fuel_entry = @vehicle.fuel_entries.build(date: Date.current)
|
||||||
end
|
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
|
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
|
end
|
||||||
|
|
||||||
def prepare_show
|
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)
|
@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_entries = @vehicle.maintenance_entries.includes(:updated_by_user).order(date: :desc, created_at: :desc)
|
||||||
@fuel_entry ||= @vehicle.fuel_entries.build(date: Date.current)
|
@fuel_entry ||= @vehicle.fuel_entries.build(date: Date.current)
|
||||||
@maintenance_entry ||= @vehicle.maintenance_entries.build(date: Date.current)
|
@maintenance_entry ||= @vehicle.maintenance_entries.build(date: Date.current)
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ class MaintenanceEntry < ApplicationRecord
|
|||||||
return if vehicle.blank? || name.blank?
|
return if vehicle.blank? || name.blank?
|
||||||
|
|
||||||
scheduled = vehicle.maintenance_schedules.any? do |schedule|
|
scheduled = vehicle.maintenance_schedules.any? do |schedule|
|
||||||
schedule.maintenance_type.casecmp?(name)
|
schedule.active? && schedule.maintenance_type.casecmp?(name)
|
||||||
end
|
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
|
return if scheduled
|
||||||
|
|
||||||
errors.add(:name, "must match a maintenance schedule for the vehicle")
|
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 :maintenance_type, presence: true, uniqueness: { scope: :vehicle_id, case_sensitive: false }
|
||||||
validates :mileage_interval, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
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 :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
|
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>
|
<section>
|
||||||
<h2>Maintenance Schedules</h2>
|
<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? %>
|
<% if @maintenance_schedules.any? %>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -56,14 +104,21 @@
|
|||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Mileage interval</th>
|
<th>Mileage interval</th>
|
||||||
<th>Time interval</th>
|
<th>Time interval</th>
|
||||||
|
<th>Next due odometer</th>
|
||||||
|
<th>Next due date</th>
|
||||||
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @maintenance_schedules.each do |schedule| %>
|
<% @maintenance_schedules.each do |schedule| %>
|
||||||
|
<% status = @maintenance_schedule_statuses.fetch(schedule) %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= schedule.maintenance_type %></td>
|
<td><%= schedule.maintenance_type %></td>
|
||||||
<td><%= number_with_delimiter(schedule.mileage_interval) %> miles</td>
|
<td><%= number_with_delimiter(schedule.mileage_interval) %> miles</td>
|
||||||
<td><%= schedule.time_interval %> days</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>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ Rails.application.routes.draw do
|
|||||||
resources :vehicles do
|
resources :vehicles do
|
||||||
resources :fuel_entries, only: :create
|
resources :fuel_entries, only: :create
|
||||||
resources :maintenance_entries, only: :create
|
resources :maintenance_entries, only: :create
|
||||||
|
resources :maintenance_schedules, except: :destroy do
|
||||||
|
patch :deactivate, on: :member
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
root "vehicles#index"
|
root "vehicles#index"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
class AddStatusTrackingToMaintenanceSchedules < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
add_column :maintenance_schedules, :baseline_odometer, :integer
|
||||||
|
add_column :maintenance_schedules, :baseline_date, :date
|
||||||
|
add_column :maintenance_schedules, :active, :boolean, default: true, null: false
|
||||||
|
add_column :maintenance_schedules, :last_status, :string
|
||||||
|
add_column :maintenance_schedules, :last_status_changed_at, :datetime
|
||||||
|
|
||||||
|
reversible do |dir|
|
||||||
|
dir.up do
|
||||||
|
execute <<~SQL.squish
|
||||||
|
UPDATE maintenance_schedules
|
||||||
|
SET baseline_odometer = (
|
||||||
|
SELECT vehicles.current_odometer
|
||||||
|
FROM vehicles
|
||||||
|
WHERE vehicles.id = maintenance_schedules.vehicle_id
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute <<~SQL.squish
|
||||||
|
UPDATE maintenance_schedules
|
||||||
|
SET baseline_date = DATE(created_at)
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :maintenance_schedules, :baseline_odometer, false
|
||||||
|
change_column_null :maintenance_schedules, :baseline_date, false
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+6
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_09_16_103647) do
|
ActiveRecord::Schema[8.1].define(version: 2026_09_16_120000) do
|
||||||
create_table "access_keys", force: :cascade do |t|
|
create_table "access_keys", force: :cascade do |t|
|
||||||
t.boolean "auto_rotate", default: true, null: false
|
t.boolean "auto_rotate", default: true, null: false
|
||||||
t.integer "auto_rotation_frequency_days", default: 30, null: false
|
t.integer "auto_rotation_frequency_days", default: 30, null: false
|
||||||
@@ -48,7 +48,12 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_16_103647) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "maintenance_schedules", force: :cascade do |t|
|
create_table "maintenance_schedules", force: :cascade do |t|
|
||||||
|
t.boolean "active", default: true, null: false
|
||||||
|
t.date "baseline_date", null: false
|
||||||
|
t.integer "baseline_odometer", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
t.string "last_status"
|
||||||
|
t.datetime "last_status_changed_at"
|
||||||
t.string "maintenance_type", null: false
|
t.string "maintenance_type", null: false
|
||||||
t.integer "mileage_interval", null: false
|
t.integer "mileage_interval", null: false
|
||||||
t.integer "time_interval", null: false
|
t.integer "time_interval", null: false
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Phase 006 Execution
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Added maintenance schedule management pages.
|
||||||
|
- Added manual schedule baselines for mileage and time calculations.
|
||||||
|
- Added overdue, upcoming, no-history, and current maintenance status calculations.
|
||||||
|
- Added vehicle show alerts for overdue, upcoming, and no-history schedules.
|
||||||
|
- Added logger-backed notification behavior for status changes.
|
||||||
|
- Added specs for schedule CRUD and maintenance status behavior.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
- Added schedule fields for `baseline_odometer`, `baseline_date`, `active`, `last_status`, and `last_status_changed_at`.
|
||||||
|
- Added validation for manual baselines and schedule active state.
|
||||||
|
- Added nested maintenance schedule routes under vehicles.
|
||||||
|
- Added `MaintenanceSchedulesController` with list, show, new, create, edit, update, and deactivate actions.
|
||||||
|
- Added separate maintenance schedule views and a shared form partial.
|
||||||
|
- Added `MaintenanceScheduleStatus` for calculating due odometer/date and status.
|
||||||
|
- Added `MaintenanceNotification` as the isolated notification stub that logs status changes through `Rails.logger`.
|
||||||
|
- Updated `vehicles#show` to show overdue alerts, upcoming warnings, no-history notices, next due odometer, next due date, and status.
|
||||||
|
- Updated maintenance entry validation and forms to use only active schedules for new entries.
|
||||||
|
- Added request specs for schedule CRUD, soft deactivation, vehicle alerts, and status-change logging.
|
||||||
|
- Added model specs for mileage-based, time-based, upcoming, no-history, and current status cases.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Maintenance schedule baselines are manually entered per schedule.
|
||||||
|
- Without matching maintenance history, status uses the schedule baseline.
|
||||||
|
- With matching maintenance history, status uses the latest matching maintenance entry instead of the schedule baseline.
|
||||||
|
- A schedule is overdue exactly when either mileage or time interval is reached.
|
||||||
|
- Upcoming warnings use a 5% lead window for both mileage and time intervals.
|
||||||
|
- Schedules are soft-deactivated instead of deleted.
|
||||||
|
- Vehicle show is allowed to update stored status tracking and log status changes.
|
||||||
|
- Status changes are stored for all statuses, but notifications are logged only for `upcoming` and `overdue` statuses.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Passed: `bundle exec rspec spec/models/maintenance_schedule_spec.rb spec/models/maintenance_schedule_status_spec.rb`
|
||||||
|
- Passed: `bundle exec rspec spec/requests/vehicles_spec.rb`
|
||||||
|
- Passed: `bundle exec rspec`
|
||||||
|
- Passed: `bin/rubocop app/models/maintenance_schedule.rb app/models/maintenance_entry.rb app/models/maintenance_schedule_status.rb app/models/maintenance_notification.rb app/controllers/vehicles_controller.rb app/controllers/fuel_entries_controller.rb app/controllers/maintenance_entries_controller.rb app/controllers/maintenance_schedules_controller.rb spec/models/maintenance_schedule_spec.rb spec/models/maintenance_schedule_status_spec.rb spec/requests/vehicles_spec.rb`
|
||||||
|
- Passed: `bin/ci`
|
||||||
@@ -4,5 +4,8 @@ FactoryBot.define do
|
|||||||
maintenance_type { "Oil Change" }
|
maintenance_type { "Oil Change" }
|
||||||
mileage_interval { 5_000 }
|
mileage_interval { 5_000 }
|
||||||
time_interval { 180 }
|
time_interval { 180 }
|
||||||
|
baseline_odometer { 10_000 }
|
||||||
|
baseline_date { Date.current }
|
||||||
|
active { true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,18 +6,43 @@ RSpec.describe MaintenanceSchedule do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "requires all fields" do
|
it "requires all fields" do
|
||||||
schedule = build(:maintenance_schedule, vehicle: nil, maintenance_type: nil, mileage_interval: nil, time_interval: nil)
|
schedule = build(
|
||||||
|
:maintenance_schedule,
|
||||||
|
vehicle: nil,
|
||||||
|
maintenance_type: nil,
|
||||||
|
mileage_interval: nil,
|
||||||
|
time_interval: nil,
|
||||||
|
baseline_odometer: nil,
|
||||||
|
baseline_date: nil,
|
||||||
|
active: nil
|
||||||
|
)
|
||||||
|
|
||||||
expect(schedule).not_to be_valid
|
expect(schedule).not_to be_valid
|
||||||
expect(schedule.errors.attribute_names).to include(:vehicle, :maintenance_type, :mileage_interval, :time_interval)
|
expect(schedule.errors.attribute_names).to include(
|
||||||
|
:vehicle,
|
||||||
|
:maintenance_type,
|
||||||
|
:mileage_interval,
|
||||||
|
:time_interval,
|
||||||
|
:baseline_odometer,
|
||||||
|
:baseline_date,
|
||||||
|
:active
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "requires positive integer intervals" do
|
it "requires valid numeric values" do
|
||||||
schedule = build(:maintenance_schedule, mileage_interval: 0, time_interval: 0)
|
schedule = build(:maintenance_schedule, mileage_interval: 0, time_interval: 0, baseline_odometer: -1)
|
||||||
|
|
||||||
expect(schedule).not_to be_valid
|
expect(schedule).not_to be_valid
|
||||||
expect(schedule.errors[:mileage_interval]).to be_present
|
expect(schedule.errors[:mileage_interval]).to be_present
|
||||||
expect(schedule.errors[:time_interval]).to be_present
|
expect(schedule.errors[:time_interval]).to be_present
|
||||||
|
expect(schedule.errors[:baseline_odometer]).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects future baseline dates" do
|
||||||
|
schedule = build(:maintenance_schedule, baseline_date: Date.current + 1.day)
|
||||||
|
|
||||||
|
expect(schedule).not_to be_valid
|
||||||
|
expect(schedule.errors[:baseline_date]).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
it "requires maintenance type to be unique per vehicle case-insensitively" do
|
it "requires maintenance type to be unique per vehicle case-insensitively" do
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe MaintenanceScheduleStatus do
|
||||||
|
it "marks a schedule overdue when the mileage interval is reached" do
|
||||||
|
schedule = build(:maintenance_schedule, mileage_interval: 5_000, baseline_odometer: 10_000, time_interval: 180)
|
||||||
|
|
||||||
|
status = described_class.new(schedule, nil, current_odometer: 15_000)
|
||||||
|
|
||||||
|
expect(status.status).to eq("overdue")
|
||||||
|
expect(status.reason).to include("mileage interval reached")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "marks a schedule overdue when the time interval is reached" do
|
||||||
|
schedule = build(:maintenance_schedule, time_interval: 180, baseline_date: Date.current - 180.days)
|
||||||
|
|
||||||
|
status = described_class.new(schedule, nil, current_odometer: 10_000)
|
||||||
|
|
||||||
|
expect(status.status).to eq("overdue")
|
||||||
|
expect(status.reason).to include("time interval reached")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "marks a schedule upcoming within five percent of the mileage interval" do
|
||||||
|
schedule = build(:maintenance_schedule, mileage_interval: 5_000, baseline_odometer: 10_000, baseline_date: Date.current)
|
||||||
|
|
||||||
|
status = described_class.new(schedule, nil, current_odometer: 14_750)
|
||||||
|
|
||||||
|
expect(status.status).to eq("upcoming")
|
||||||
|
expect(status.reason).to include("within 250 miles")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "marks a schedule upcoming within five percent of the time interval" do
|
||||||
|
schedule = build(:maintenance_schedule, time_interval: 180, baseline_date: Date.current - 171.days)
|
||||||
|
|
||||||
|
status = described_class.new(schedule, nil, current_odometer: 10_000)
|
||||||
|
|
||||||
|
expect(status.status).to eq("upcoming")
|
||||||
|
expect(status.reason).to include("within 9 days")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "marks a no-history schedule as no history when it is not upcoming or overdue" do
|
||||||
|
schedule = build(:maintenance_schedule, baseline_odometer: 10_000, mileage_interval: 5_000, baseline_date: Date.current)
|
||||||
|
|
||||||
|
status = described_class.new(schedule, nil, current_odometer: 10_100)
|
||||||
|
|
||||||
|
expect(status.status).to eq("no_history")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "marks a schedule current when matching history exists and no interval is near" do
|
||||||
|
schedule = build(:maintenance_schedule, mileage_interval: 5_000, time_interval: 180)
|
||||||
|
entry = build(:maintenance_entry, odometer: 12_000, date: Date.current - 10.days)
|
||||||
|
|
||||||
|
status = described_class.new(schedule, entry, current_odometer: 12_500)
|
||||||
|
|
||||||
|
expect(status.status).to eq("current")
|
||||||
|
expect(status.due_odometer).to eq(17_000)
|
||||||
|
expect(status.due_date).to eq(entry.date + 180.days)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -111,6 +111,7 @@ RSpec.describe "Vehicles" do
|
|||||||
expect(response.body).to include(vehicle.make)
|
expect(response.body).to include(vehicle.make)
|
||||||
expect(response.body).to include(vehicle.model)
|
expect(response.body).to include(vehicle.model)
|
||||||
expect(response.body).to include("Maintenance Schedules")
|
expect(response.body).to include("Maintenance Schedules")
|
||||||
|
expect(response.body).to include("Manage maintenance schedules")
|
||||||
expect(response.body).to include("Fuel History")
|
expect(response.body).to include("Fuel History")
|
||||||
expect(response.body).to include("Maintenance History")
|
expect(response.body).to include("Maintenance History")
|
||||||
expect(response.body).to include(vehicle.maintenance_schedules.first.maintenance_type)
|
expect(response.body).to include(vehicle.maintenance_schedules.first.maintenance_type)
|
||||||
@@ -121,6 +122,141 @@ RSpec.describe "Vehicles" do
|
|||||||
expect(response.body.index("12,000")).to be < response.body.index("11,000")
|
expect(response.body.index("12,000")).to be < response.body.index("11,000")
|
||||||
expect(response.body.index(newer_maintenance_entry.notes)).to be < response.body.index(older_maintenance_entry.notes)
|
expect(response.body.index(newer_maintenance_entry.notes)).to be < response.body.index(older_maintenance_entry.notes)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "shows overdue maintenance alerts" do
|
||||||
|
vehicle = create(:vehicle, current_odometer: 15_000)
|
||||||
|
create(
|
||||||
|
:maintenance_schedule,
|
||||||
|
vehicle: vehicle,
|
||||||
|
maintenance_type: "Oil Change",
|
||||||
|
mileage_interval: 5_000,
|
||||||
|
baseline_odometer: 10_000,
|
||||||
|
baseline_date: Date.current
|
||||||
|
)
|
||||||
|
|
||||||
|
get vehicle_path(vehicle)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Overdue Maintenance")
|
||||||
|
expect(response.body).to include("Oil Change is overdue")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shows upcoming maintenance warnings" do
|
||||||
|
vehicle = create(:vehicle, current_odometer: 14_750)
|
||||||
|
create(
|
||||||
|
:maintenance_schedule,
|
||||||
|
vehicle: vehicle,
|
||||||
|
maintenance_type: "Oil Change",
|
||||||
|
mileage_interval: 5_000,
|
||||||
|
baseline_odometer: 10_000,
|
||||||
|
baseline_date: Date.current
|
||||||
|
)
|
||||||
|
|
||||||
|
get vehicle_path(vehicle)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Upcoming Maintenance")
|
||||||
|
expect(response.body).to include("Oil Change is upcoming")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shows no-history maintenance status without marking it overdue" do
|
||||||
|
vehicle = create(:vehicle, current_odometer: 10_100)
|
||||||
|
create(
|
||||||
|
:maintenance_schedule,
|
||||||
|
vehicle: vehicle,
|
||||||
|
maintenance_type: "Oil Change",
|
||||||
|
mileage_interval: 5_000,
|
||||||
|
baseline_odometer: 10_000,
|
||||||
|
baseline_date: Date.current
|
||||||
|
)
|
||||||
|
|
||||||
|
get vehicle_path(vehicle)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("No Maintenance History")
|
||||||
|
expect(response.body).to include("Oil Change has no matching maintenance entry yet")
|
||||||
|
expect(response.body).not_to include("Oil Change is overdue")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "logs overdue notifications only when schedule status changes" do
|
||||||
|
vehicle = create(:vehicle, current_odometer: 15_000)
|
||||||
|
schedule = create(
|
||||||
|
:maintenance_schedule,
|
||||||
|
vehicle: vehicle,
|
||||||
|
maintenance_type: "Oil Change",
|
||||||
|
mileage_interval: 5_000,
|
||||||
|
baseline_odometer: 10_000,
|
||||||
|
baseline_date: Date.current
|
||||||
|
)
|
||||||
|
allow(Rails.logger).to receive(:warn)
|
||||||
|
|
||||||
|
get vehicle_path(vehicle)
|
||||||
|
get vehicle_path(vehicle)
|
||||||
|
|
||||||
|
expect(schedule.reload.last_status).to eq("overdue")
|
||||||
|
expect(Rails.logger).to have_received(:warn).once.with(/Maintenance schedule overdue: vehicle_id=#{vehicle.id}/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "maintenance schedule CRUD" do
|
||||||
|
before { identify_user }
|
||||||
|
|
||||||
|
it "lists maintenance schedules" do
|
||||||
|
vehicle = create(:vehicle)
|
||||||
|
create(:maintenance_schedule, vehicle: vehicle, maintenance_type: "Oil Change")
|
||||||
|
|
||||||
|
get vehicle_maintenance_schedules_path(vehicle)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Maintenance Schedules")
|
||||||
|
expect(response.body).to include("Oil Change")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a maintenance schedule" do
|
||||||
|
vehicle = create(:vehicle)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post vehicle_maintenance_schedules_path(vehicle), params: { maintenance_schedule: valid_maintenance_schedule_params }
|
||||||
|
end.to change(MaintenanceSchedule, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(vehicle_maintenance_schedules_path(vehicle))
|
||||||
|
expect(MaintenanceSchedule.last.baseline_odometer).to eq(10_000)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects invalid maintenance schedule params" do
|
||||||
|
vehicle = create(:vehicle)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post vehicle_maintenance_schedules_path(vehicle), params: { maintenance_schedule: valid_maintenance_schedule_params.merge(mileage_interval: 0) }
|
||||||
|
end.not_to change(MaintenanceSchedule, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_content)
|
||||||
|
expect(response.body).to include("prevented this maintenance schedule from being saved")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates a maintenance schedule" do
|
||||||
|
vehicle = create(:vehicle)
|
||||||
|
schedule = create(:maintenance_schedule, vehicle: vehicle, maintenance_type: "Oil Change")
|
||||||
|
|
||||||
|
patch vehicle_maintenance_schedule_path(vehicle, schedule), params: {
|
||||||
|
maintenance_schedule: valid_maintenance_schedule_params.merge(maintenance_type: "Tire Rotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to redirect_to(vehicle_maintenance_schedules_path(vehicle))
|
||||||
|
expect(schedule.reload.maintenance_type).to eq("Tire Rotation")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "soft deactivates a maintenance schedule" do
|
||||||
|
vehicle = create(:vehicle)
|
||||||
|
schedule = create(:maintenance_schedule, vehicle: vehicle, active: true)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
patch deactivate_vehicle_maintenance_schedule_path(vehicle, schedule)
|
||||||
|
end.not_to change(MaintenanceSchedule, :count)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(vehicle_maintenance_schedules_path(vehicle))
|
||||||
|
expect(schedule.reload).not_to be_active
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "POST /vehicles/:vehicle_id/fuel_entries" do
|
describe "POST /vehicles/:vehicle_id/fuel_entries" do
|
||||||
@@ -368,6 +504,17 @@ RSpec.describe "Vehicles" do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def valid_maintenance_schedule_params
|
||||||
|
{
|
||||||
|
maintenance_type: "Oil Change",
|
||||||
|
mileage_interval: 5_000,
|
||||||
|
time_interval: 180,
|
||||||
|
baseline_odometer: 10_000,
|
||||||
|
baseline_date: Date.current,
|
||||||
|
active: "1"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def identify_user(user = create(:user))
|
def identify_user(user = create(:user))
|
||||||
@identified_user = user
|
@identified_user = user
|
||||||
access_key = AccessKey.current
|
access_key = AccessKey.current
|
||||||
|
|||||||
Reference in New Issue
Block a user