Finished phase005
This commit is contained in:
@@ -1,7 +1,59 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
AUTH_TOKEN_COOKIE = :auth_token
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
|
||||
# Changes to the importmap will invalidate the etag for HTML responses
|
||||
stale_when_importmap_changes
|
||||
|
||||
before_action :rotate_access_key_if_needed
|
||||
before_action :load_current_user
|
||||
before_action :require_identified_user
|
||||
|
||||
helper_method :current_user, :identified?
|
||||
|
||||
private
|
||||
|
||||
def rotate_access_key_if_needed
|
||||
AccessKey.rotate_if_needed!
|
||||
end
|
||||
|
||||
def load_current_user
|
||||
Current.user_session = UserSession.find_by_token(cookies.encrypted[AUTH_TOKEN_COOKIE])
|
||||
Current.user = Current.user_session&.user
|
||||
Current.user_session&.touch(:last_used_at)
|
||||
end
|
||||
|
||||
def require_identified_user
|
||||
return if identified?
|
||||
|
||||
redirect_to identify_path, alert: "Identify yourself before using Car Tracker."
|
||||
end
|
||||
|
||||
def current_user
|
||||
Current.user
|
||||
end
|
||||
|
||||
def identified?
|
||||
current_user.present?
|
||||
end
|
||||
|
||||
def sign_in(user)
|
||||
user_session, raw_token = UserSession.create_for!(user)
|
||||
Current.user_session = user_session
|
||||
Current.user = user
|
||||
cookies.encrypted[AUTH_TOKEN_COOKIE] = {
|
||||
value: raw_token,
|
||||
httponly: true,
|
||||
same_site: :lax
|
||||
}
|
||||
end
|
||||
|
||||
def sign_out
|
||||
Current.user_session&.destroy!
|
||||
Current.user_session = nil
|
||||
Current.user = nil
|
||||
cookies.delete(AUTH_TOKEN_COOKIE)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
class FuelEntriesController < ApplicationController
|
||||
def create
|
||||
@vehicle = Vehicle.find(params[:vehicle_id])
|
||||
@fuel_entry = @vehicle.fuel_entries.build(fuel_entry_params)
|
||||
@fuel_entry.updated_by_user = Current.user
|
||||
|
||||
if save_fuel_entry
|
||||
redirect_to @vehicle, notice: "Fuel entry recorded."
|
||||
else
|
||||
prepare_vehicle_show
|
||||
render "vehicles/show", status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fuel_entry_params
|
||||
params.require(:fuel_entry).permit(:odometer, :gallons_pumped, :price_paid, :date)
|
||||
end
|
||||
|
||||
def save_fuel_entry
|
||||
saved = false
|
||||
|
||||
FuelEntry.transaction do
|
||||
if @fuel_entry.save
|
||||
update_vehicle_odometer(@fuel_entry.odometer)
|
||||
saved = true
|
||||
end
|
||||
end
|
||||
|
||||
saved
|
||||
end
|
||||
|
||||
def update_vehicle_odometer(odometer)
|
||||
return unless odometer > @vehicle.current_odometer
|
||||
|
||||
@vehicle.update!(current_odometer: odometer)
|
||||
end
|
||||
|
||||
def prepare_vehicle_show
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.order(:maintenance_type)
|
||||
@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
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
class MaintenanceEntriesController < ApplicationController
|
||||
ODOMETER_WARNING_THRESHOLD = 500
|
||||
|
||||
def create
|
||||
@vehicle = Vehicle.find(params[:vehicle_id])
|
||||
original_odometer = @vehicle.current_odometer
|
||||
@maintenance_entry = @vehicle.maintenance_entries.build(maintenance_entry_params)
|
||||
@maintenance_entry.updated_by_user = Current.user
|
||||
|
||||
if save_maintenance_entry
|
||||
set_old_odometer_warning(original_odometer)
|
||||
redirect_to @vehicle, notice: "Maintenance entry recorded."
|
||||
else
|
||||
prepare_vehicle_show
|
||||
render "vehicles/show", status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maintenance_entry_params
|
||||
params.require(:maintenance_entry).permit(:name, :notes, :odometer, :cost, :date)
|
||||
end
|
||||
|
||||
def save_maintenance_entry
|
||||
saved = false
|
||||
|
||||
MaintenanceEntry.transaction do
|
||||
if @maintenance_entry.save
|
||||
update_vehicle_odometer(@maintenance_entry.odometer)
|
||||
saved = true
|
||||
end
|
||||
end
|
||||
|
||||
saved
|
||||
end
|
||||
|
||||
def update_vehicle_odometer(odometer)
|
||||
return unless odometer > @vehicle.current_odometer
|
||||
|
||||
@vehicle.update!(current_odometer: odometer)
|
||||
end
|
||||
|
||||
def set_old_odometer_warning(original_odometer)
|
||||
return unless @maintenance_entry.odometer < original_odometer - ODOMETER_WARNING_THRESHOLD
|
||||
|
||||
message = "Maintenance odometer is more than 500 miles behind the current vehicle odometer."
|
||||
flash[:warning] = message
|
||||
Rails.logger.warn(
|
||||
"#{message} vehicle_id=#{@vehicle.id} maintenance_entry_id=#{@maintenance_entry.id} " \
|
||||
"entry_odometer=#{@maintenance_entry.odometer} vehicle_odometer=#{original_odometer}"
|
||||
)
|
||||
end
|
||||
|
||||
def prepare_vehicle_show
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.order(:maintenance_type)
|
||||
@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
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
class SessionsController < ApplicationController
|
||||
skip_before_action :require_identified_user, only: %i[new create destroy]
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
access_key = AccessKey.current
|
||||
|
||||
unless access_key.matches?(session_params[:key])
|
||||
Rails.logger.warn("Invalid access key identification attempt for email=#{session_params[:email]}")
|
||||
flash.now[:alert] = "The access key is not valid."
|
||||
return render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
user = find_or_build_user
|
||||
return render :new, status: :unprocessable_entity unless user
|
||||
|
||||
sign_in(user)
|
||||
redirect_to vehicles_path, notice: "You have been identified."
|
||||
end
|
||||
|
||||
def destroy
|
||||
sign_out
|
||||
redirect_to identify_path, notice: "You have been logged out."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def session_params
|
||||
params.permit(:name, :email, :key)
|
||||
end
|
||||
|
||||
def find_or_build_user
|
||||
email = session_params[:email].to_s.strip.downcase
|
||||
name = session_params[:name].to_s.strip
|
||||
user = User.find_by(email: email)
|
||||
|
||||
if user
|
||||
return user if user.name == name
|
||||
|
||||
flash.now[:alert] = "The name does not match the existing user for that email."
|
||||
return nil
|
||||
end
|
||||
|
||||
user = User.new(name: name, email: email)
|
||||
return user if user.save
|
||||
|
||||
flash.now[:alert] = user.errors.full_messages.to_sentence
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
class VehiclesController < ApplicationController
|
||||
before_action :set_vehicle, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
@status = params[:status]
|
||||
@vehicles = vehicles_for_status(@status).order(:make, :model, :year)
|
||||
end
|
||||
|
||||
def show
|
||||
prepare_show
|
||||
end
|
||||
|
||||
def new
|
||||
@vehicle = Vehicle.new
|
||||
end
|
||||
|
||||
def create
|
||||
@vehicle = Vehicle.new(vehicle_params)
|
||||
|
||||
if @vehicle.save
|
||||
redirect_to @vehicle, notice: "Vehicle created."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @vehicle.update(vehicle_params)
|
||||
redirect_to @vehicle, notice: "Vehicle updated."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@vehicle.update(active: false)
|
||||
redirect_to vehicles_path, notice: "Vehicle deactivated."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_vehicle
|
||||
@vehicle = Vehicle.find(params[:id])
|
||||
end
|
||||
|
||||
def vehicles_for_status(status)
|
||||
case status
|
||||
when "inactive"
|
||||
Vehicle.where(active: false)
|
||||
when "all"
|
||||
Vehicle.all
|
||||
else
|
||||
Vehicle.where(active: true)
|
||||
end
|
||||
end
|
||||
|
||||
def vehicle_params
|
||||
params.require(:vehicle).permit(
|
||||
:make,
|
||||
:model,
|
||||
:year,
|
||||
:color,
|
||||
:vin,
|
||||
:licence_plate,
|
||||
:current_odometer,
|
||||
:fuel_tank_size,
|
||||
:active
|
||||
)
|
||||
end
|
||||
|
||||
def prepare_show
|
||||
@maintenance_schedules = @vehicle.maintenance_schedules.order(:maintenance_type)
|
||||
@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
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["odometer", "warning"]
|
||||
static values = {
|
||||
currentOdometer: Number,
|
||||
threshold: Number
|
||||
}
|
||||
|
||||
check() {
|
||||
const odometer = Number.parseInt(this.odometerTarget.value, 10)
|
||||
|
||||
if (Number.isNaN(odometer) || odometer >= this.currentOdometerValue - this.thresholdValue) {
|
||||
this.warningTarget.hidden = true
|
||||
return
|
||||
}
|
||||
|
||||
this.warningTarget.hidden = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
class AccessKey < ApplicationRecord
|
||||
DEFAULT_ROTATION_FREQUENCY_DAYS = 30
|
||||
KEY_LENGTH = 32
|
||||
|
||||
validates :raw_key, presence: true
|
||||
validates :generated_at, presence: true
|
||||
validates :auto_rotation_frequency_days,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
||||
def self.current
|
||||
order(generated_at: :desc, id: :desc).first || create_current!
|
||||
end
|
||||
|
||||
def self.create_current!
|
||||
create!(
|
||||
raw_key: generate_raw_key,
|
||||
generated_at: Time.current,
|
||||
auto_rotate: true,
|
||||
auto_rotation_frequency_days: DEFAULT_ROTATION_FREQUENCY_DAYS
|
||||
)
|
||||
end
|
||||
|
||||
def self.rotate!
|
||||
previous_key = current
|
||||
|
||||
transaction do
|
||||
UserSession.delete_all
|
||||
create!(
|
||||
raw_key: generate_raw_key,
|
||||
generated_at: Time.current,
|
||||
auto_rotate: previous_key.auto_rotate,
|
||||
auto_rotation_frequency_days: previous_key.auto_rotation_frequency_days
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def self.rotate_if_needed!
|
||||
key = current
|
||||
return key unless key.auto_rotate? && key.expired?
|
||||
|
||||
rotate!
|
||||
end
|
||||
|
||||
def self.generate_raw_key
|
||||
SecureRandom.urlsafe_base64(KEY_LENGTH)
|
||||
end
|
||||
|
||||
def matches?(submitted_key)
|
||||
ActiveSupport::SecurityUtils.secure_compare(raw_key, submitted_key.to_s)
|
||||
rescue ArgumentError
|
||||
false
|
||||
end
|
||||
|
||||
def expired?
|
||||
generated_at <= auto_rotation_frequency_days.days.ago
|
||||
end
|
||||
|
||||
def disable_auto_rotation!
|
||||
update!(auto_rotate: false)
|
||||
end
|
||||
|
||||
def enable_auto_rotation!
|
||||
update!(auto_rotate: true)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :user, :user_session
|
||||
end
|
||||
@@ -7,6 +7,7 @@ class FuelEntry < ApplicationRecord
|
||||
validates :price_paid, presence: true, numericality: { greater_than: 0 }
|
||||
validates :date, presence: true
|
||||
validate :date_cannot_be_in_the_future
|
||||
validate :odometer_cannot_be_less_than_vehicle_current_odometer
|
||||
|
||||
private
|
||||
|
||||
@@ -15,4 +16,11 @@ class FuelEntry < ApplicationRecord
|
||||
|
||||
errors.add(:date, "can't be in the future")
|
||||
end
|
||||
|
||||
def odometer_cannot_be_less_than_vehicle_current_odometer
|
||||
return if vehicle.blank? || odometer.blank?
|
||||
return if odometer >= vehicle.current_odometer
|
||||
|
||||
errors.add(:odometer, "cannot be less than the vehicle's current odometer")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class User < ApplicationRecord
|
||||
has_many :fuel_entries, foreign_key: :updated_by_user_id, dependent: :restrict_with_error, inverse_of: :updated_by_user
|
||||
has_many :maintenance_entries, foreign_key: :updated_by_user_id, dependent: :restrict_with_error, inverse_of: :updated_by_user
|
||||
has_many :user_sessions, dependent: :destroy
|
||||
|
||||
before_validation :normalize_email
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
class UserSession < ApplicationRecord
|
||||
TOKEN_LENGTH = 32
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :token_digest, presence: true, uniqueness: true
|
||||
|
||||
def self.create_for!(user)
|
||||
raw_token = generate_raw_token
|
||||
user_session = create!(user: user, token_digest: digest(raw_token), last_used_at: Time.current)
|
||||
|
||||
[ user_session, raw_token ]
|
||||
end
|
||||
|
||||
def self.find_by_token(raw_token)
|
||||
return if raw_token.blank?
|
||||
|
||||
find_by(token_digest: digest(raw_token))
|
||||
end
|
||||
|
||||
def self.generate_raw_token
|
||||
SecureRandom.urlsafe_base64(TOKEN_LENGTH)
|
||||
end
|
||||
|
||||
def self.digest(raw_token)
|
||||
Digest::SHA256.hexdigest(raw_token.to_s)
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@ class Vehicle < ApplicationRecord
|
||||
has_many :fuel_entries, dependent: :restrict_with_error
|
||||
has_many :maintenance_entries, dependent: :restrict_with_error
|
||||
|
||||
before_validation :normalize_vin
|
||||
before_validation :normalize_vin, :normalize_licence_plate
|
||||
|
||||
validates :make, :model, :color, :vin, :licence_plate, presence: true
|
||||
validates :vin, uniqueness: true
|
||||
@@ -19,4 +19,8 @@ class Vehicle < ApplicationRecord
|
||||
def normalize_vin
|
||||
self.vin = vin.to_s.strip.upcase if vin.present?
|
||||
end
|
||||
|
||||
def normalize_licence_plate
|
||||
self.licence_plate = licence_plate.to_s.strip.upcase if licence_plate.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,6 +24,15 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<% if identified? %>
|
||||
<header>
|
||||
<p>
|
||||
Identified as <%= current_user.name %> (<%= current_user.email %>)
|
||||
<%= button_to "Log out", logout_path, method: :delete %>
|
||||
</p>
|
||||
</header>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<main>
|
||||
<p><%= notice %></p>
|
||||
<p><%= alert %></p>
|
||||
|
||||
<h1>Identify yourself</h1>
|
||||
|
||||
<p>Enter the current access key along with your name and email.</p>
|
||||
|
||||
<%= form_with url: session_path do |form| %>
|
||||
<div>
|
||||
<%= form.label :name, "User name" %><br>
|
||||
<%= form.text_field :name, value: params[:name], required: true, autocomplete: "name" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :email %><br>
|
||||
<%= form.email_field :email, value: params[:email], required: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :key, "Access key" %><br>
|
||||
<%= form.text_field :key, required: true, autocomplete: "off" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit "Identify" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</main>
|
||||
@@ -0,0 +1,62 @@
|
||||
<%= form_with model: vehicle do |form| %>
|
||||
<% if vehicle.errors.any? %>
|
||||
<section>
|
||||
<h2><%= pluralize(vehicle.errors.count, "error") %> prevented this vehicle from being saved</h2>
|
||||
|
||||
<ul>
|
||||
<% vehicle.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :make %><br>
|
||||
<%= form.text_field :make %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :model %><br>
|
||||
<%= form.text_field :model %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :year %><br>
|
||||
<%= form.number_field :year %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :color %><br>
|
||||
<%= form.text_field :color %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :vin, "VIN" %><br>
|
||||
<%= form.text_field :vin %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :licence_plate %><br>
|
||||
<%= form.text_field :licence_plate %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :current_odometer %><br>
|
||||
<%= form.number_field :current_odometer %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :fuel_tank_size %><br>
|
||||
<%= form.number_field :fuel_tank_size, step: "0.01" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.check_box :active %>
|
||||
<%= form.label :active %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,7 @@
|
||||
<main>
|
||||
<h1>Edit Vehicle</h1>
|
||||
|
||||
<%= render "form", vehicle: @vehicle %>
|
||||
|
||||
<p><%= link_to "Back to vehicle", @vehicle %></p>
|
||||
</main>
|
||||
@@ -0,0 +1,38 @@
|
||||
<main>
|
||||
<p><%= notice %></p>
|
||||
|
||||
<h1>Vehicles</h1>
|
||||
|
||||
<nav aria-label="Vehicle filters">
|
||||
<%= link_to "Active", vehicles_path %> |
|
||||
<%= link_to "Inactive", vehicles_path(status: "inactive") %> |
|
||||
<%= link_to "All", vehicles_path(status: "all") %>
|
||||
</nav>
|
||||
|
||||
<p><%= link_to "New vehicle", new_vehicle_path %></p>
|
||||
|
||||
<% if @vehicles.any? %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vehicle</th>
|
||||
<th>Licence plate</th>
|
||||
<th>Odometer</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @vehicles.each do |vehicle| %>
|
||||
<tr>
|
||||
<td><%= link_to "#{vehicle.year} #{vehicle.make} #{vehicle.model}", vehicle %></td>
|
||||
<td><%= vehicle.licence_plate %></td>
|
||||
<td><%= number_with_delimiter(vehicle.current_odometer) %></td>
|
||||
<td><%= vehicle.active? ? "Active" : "Inactive" %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No vehicles found.</p>
|
||||
<% end %>
|
||||
</main>
|
||||
@@ -0,0 +1,7 @@
|
||||
<main>
|
||||
<h1>New Vehicle</h1>
|
||||
|
||||
<%= render "form", vehicle: @vehicle %>
|
||||
|
||||
<p><%= link_to "Back to vehicles", vehicles_path %></p>
|
||||
</main>
|
||||
@@ -0,0 +1,243 @@
|
||||
<main>
|
||||
<p><%= notice %></p>
|
||||
<p><%= flash[:warning] %></p>
|
||||
|
||||
<p><%= link_to "Back to vehicles", vehicles_path %></p>
|
||||
|
||||
<h1><%= @vehicle.year %> <%= @vehicle.make %> <%= @vehicle.model %></h1>
|
||||
|
||||
<div>
|
||||
<%= link_to "Edit", edit_vehicle_path(@vehicle) %>
|
||||
<% if @vehicle.active? %>
|
||||
| <%= button_to "Deactivate", @vehicle, method: :delete %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Vehicle Details</h2>
|
||||
|
||||
<dl>
|
||||
<dt>Make</dt>
|
||||
<dd><%= @vehicle.make %></dd>
|
||||
|
||||
<dt>Model</dt>
|
||||
<dd><%= @vehicle.model %></dd>
|
||||
|
||||
<dt>Year</dt>
|
||||
<dd><%= @vehicle.year %></dd>
|
||||
|
||||
<dt>Color</dt>
|
||||
<dd><%= @vehicle.color %></dd>
|
||||
|
||||
<dt>VIN</dt>
|
||||
<dd><%= @vehicle.vin %></dd>
|
||||
|
||||
<dt>Licence plate</dt>
|
||||
<dd><%= @vehicle.licence_plate %></dd>
|
||||
|
||||
<dt>Current odometer</dt>
|
||||
<dd><%= number_with_delimiter(@vehicle.current_odometer) %></dd>
|
||||
|
||||
<dt>Fuel tank size</dt>
|
||||
<dd><%= @vehicle.fuel_tank_size %></dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd><%= @vehicle.active? ? "Active" : "Inactive" %></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Maintenance Schedules</h2>
|
||||
|
||||
<% if @maintenance_schedules.any? %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Mileage interval</th>
|
||||
<th>Time interval</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @maintenance_schedules.each do |schedule| %>
|
||||
<tr>
|
||||
<td><%= schedule.maintenance_type %></td>
|
||||
<td><%= number_with_delimiter(schedule.mileage_interval) %> miles</td>
|
||||
<td><%= schedule.time_interval %> days</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No maintenance schedules recorded.</p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Record Fuel</h2>
|
||||
|
||||
<%= form_with model: [@vehicle, @fuel_entry] do |form| %>
|
||||
<% if @fuel_entry.errors.any? %>
|
||||
<section>
|
||||
<h3><%= pluralize(@fuel_entry.errors.count, "error") %> prevented this fuel entry from being saved</h3>
|
||||
|
||||
<ul>
|
||||
<% @fuel_entry.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :odometer %><br>
|
||||
<%= form.number_field :odometer, min: @vehicle.current_odometer, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :gallons_pumped %><br>
|
||||
<%= form.number_field :gallons_pumped, min: 0.01, step: "0.01", required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :price_paid %><br>
|
||||
<%= form.number_field :price_paid, min: 0.01, step: "0.01", required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :date %><br>
|
||||
<%= form.date_field :date, max: Date.current, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit "Record fuel" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Record Maintenance</h2>
|
||||
|
||||
<% if @maintenance_schedules.any? %>
|
||||
<%= form_with model: [@vehicle, @maintenance_entry], data: {
|
||||
controller: "maintenance-odometer-warning",
|
||||
maintenance_odometer_warning_current_odometer_value: @vehicle.current_odometer,
|
||||
maintenance_odometer_warning_threshold_value: 500
|
||||
} do |form| %>
|
||||
<% if @maintenance_entry.errors.any? %>
|
||||
<section>
|
||||
<h3><%= pluralize(@maintenance_entry.errors.count, "error") %> prevented this maintenance entry from being saved</h3>
|
||||
|
||||
<ul>
|
||||
<% @maintenance_entry.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :name, "Maintenance type" %><br>
|
||||
<%= form.select :name, options_for_select(@maintenance_schedules.map(&:maintenance_type), @maintenance_entry.name), { prompt: "Choose maintenance type" }, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :odometer %><br>
|
||||
<%= form.number_field :odometer, min: 0, required: true, data: {
|
||||
maintenance_odometer_warning_target: "odometer",
|
||||
action: "input->maintenance-odometer-warning#check change->maintenance-odometer-warning#check"
|
||||
} %>
|
||||
</div>
|
||||
|
||||
<p hidden data-maintenance-odometer-warning-target="warning">
|
||||
Warning: this maintenance odometer is more than 500 miles behind the current vehicle odometer.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<%= form.label :cost %><br>
|
||||
<%= form.number_field :cost, min: 0, step: "0.01", required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :date %><br>
|
||||
<%= form.date_field :date, max: Date.current, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :notes %><br>
|
||||
<%= form.text_area :notes, required: true %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit "Record maintenance" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p>Add a maintenance schedule before recording maintenance.</p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Fuel History</h2>
|
||||
|
||||
<% if @fuel_entries.any? %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Odometer</th>
|
||||
<th>Gallons</th>
|
||||
<th>Price paid</th>
|
||||
<th>Updated by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @fuel_entries.each do |entry| %>
|
||||
<tr>
|
||||
<td><%= entry.date %></td>
|
||||
<td><%= number_with_delimiter(entry.odometer) %></td>
|
||||
<td><%= entry.gallons_pumped %></td>
|
||||
<td><%= number_to_currency(entry.price_paid) %></td>
|
||||
<td><%= entry.updated_by_user.name %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No fuel entries recorded.</p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Maintenance History</h2>
|
||||
|
||||
<% if @maintenance_entries.any? %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Name</th>
|
||||
<th>Odometer</th>
|
||||
<th>Cost</th>
|
||||
<th>Notes</th>
|
||||
<th>Updated by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @maintenance_entries.each do |entry| %>
|
||||
<tr>
|
||||
<td><%= entry.date %></td>
|
||||
<td><%= entry.name %></td>
|
||||
<td><%= number_with_delimiter(entry.odometer) %></td>
|
||||
<td><%= number_to_currency(entry.cost) %></td>
|
||||
<td><%= entry.notes %></td>
|
||||
<td><%= entry.updated_by_user.name %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No maintenance entries recorded.</p>
|
||||
<% end %>
|
||||
</section>
|
||||
</main>
|
||||
Reference in New Issue
Block a user