diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index fe93333..591c039 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -8,3 +8,458 @@ * * Consider organizing styles into separate files for maintainability. */ + +:root { + --color-bg: #f4f6f8; + --color-surface: #ffffff; + --color-surface-strong: #f9fafb; + --color-text: #172033; + --color-muted: #667085; + --color-border: #d8dee8; + --color-primary: #1f5eff; + --color-primary-dark: #1646bf; + --color-danger: #b42318; + --color-danger-bg: #fff0ed; + --color-warning: #8a5a00; + --color-warning-bg: #fff7d6; + --color-success: #067647; + --color-success-bg: #ecfdf3; + --shadow-card: 0 12px 30px rgba(16, 24, 40, 0.08); + --radius-lg: 18px; + --radius-md: 12px; + --radius-sm: 8px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--color-bg); + color: var(--color-text); + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.5; +} + +a { + color: var(--color-primary); +} + +body > header { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: 0.75rem 1rem; +} + +body > header p { + margin: 0; +} + +body > header .button_to, +.action-row .button_to { + display: inline-block; + margin: 0; +} + +.page-shell { + inline-size: min(1120px, 100%); + margin: 0 auto; + padding: 1rem; +} + +.page-header, +.vehicle-hero { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-block: 1rem 1.25rem; +} + +.page-header h1, +.vehicle-hero h1 { + margin: 0; + font-size: clamp(2rem, 7vw, 3.25rem); + line-height: 1; +} + +.vehicle-hero p { + margin: 0.35rem 0 0; + color: var(--color-muted); +} + +.eyebrow { + margin: 0 0 0.35rem; + color: var(--color-muted); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.filter-nav, +.action-row { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.filter-pill, +.button { + display: inline-flex; + min-block-size: 2.65rem; + align-items: center; + justify-content: center; + border: 1px solid var(--color-border); + border-radius: 999px; + padding: 0.65rem 1rem; + background: var(--color-surface); + color: var(--color-text); + cursor: pointer; + font: inherit; + font-weight: 700; + text-decoration: none; +} + +.filter-pill--active, +.button--primary { + border-color: var(--color-primary); + background: var(--color-primary); + color: #ffffff; +} + +.button--secondary { + background: var(--color-surface-strong); +} + +.button--danger { + border-color: #fecaca; + background: var(--color-danger-bg); + color: var(--color-danger); +} + +.back-link { + font-weight: 700; + text-decoration: none; +} + +.flash, +.empty-state { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 0.85rem 1rem; + background: var(--color-surface); +} + +.flash--notice { + border-color: #b7e4c7; + background: var(--color-success-bg); + color: var(--color-success); +} + +.flash--warning { + border-color: #fedf89; + background: var(--color-warning-bg); + color: var(--color-warning); +} + +.vehicle-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); + gap: 1rem; + margin-block-start: 1rem; +} + +.vehicle-card { + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-surface); + box-shadow: var(--shadow-card); +} + +.vehicle-card--upcoming { + border-color: #f6d06f; + background: var(--color-warning-bg); +} + +.vehicle-card--overdue { + border-color: #fda29b; + background: var(--color-danger-bg); +} + +.vehicle-card--inactive { + opacity: 0.72; +} + +.vehicle-card__link { + display: block; + block-size: 100%; + padding: 1rem; + color: inherit; + text-decoration: none; +} + +.vehicle-card__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; +} + +.vehicle-card h2 { + margin: 0; + font-size: 1.35rem; + line-height: 1.15; +} + +.vehicle-card__facts { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + margin: 1rem 0; +} + +.vehicle-card__facts dt { + color: var(--color-muted); + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.vehicle-card__facts dd { + margin: 0.1rem 0 0; + font-size: 1.1rem; + font-weight: 800; +} + +.maintenance-summary, +.maintenance-current { + border-radius: var(--radius-md); + padding: 0.75rem; + background: rgba(255, 255, 255, 0.65); +} + +.maintenance-summary p, +.maintenance-current { + margin: 0.2rem 0 0; +} + +.maintenance-summary--overdue strong { + color: var(--color-danger); +} + +.maintenance-summary--upcoming strong { + color: var(--color-warning); +} + +.alert-stack, +.section-alerts { + display: grid; + gap: 0.75rem; +} + +.alert-card, +.mini-alert, +.dashboard-section { + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-surface); + box-shadow: var(--shadow-card); +} + +.alert-card, +.mini-alert { + padding: 1rem; +} + +.alert-card h2, +.mini-alert h3 { + margin-block: 0 0.5rem; +} + +.alert-card ul, +.mini-alert ul { + margin-block: 0; + padding-inline-start: 1.25rem; +} + +.alert-card--overdue, +.mini-alert--overdue { + border-color: #fda29b; + background: var(--color-danger-bg); +} + +.alert-card--upcoming, +.mini-alert--upcoming { + border-color: #fedf89; + background: var(--color-warning-bg); +} + +.dashboard-section { + margin-block-start: 1rem; + overflow: hidden; +} + +.dashboard-section--primary { + border-color: #b2ccff; +} + +.dashboard-section > summary { + min-block-size: 3.5rem; + padding: 1rem; + cursor: pointer; + font-size: 1.1rem; + font-weight: 800; + list-style-position: inside; +} + +.dashboard-section > :not(summary) { + margin: 0 1rem 1rem; +} + +.table-scroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +table { + inline-size: 100%; + min-inline-size: 42rem; + border-collapse: collapse; + background: var(--color-surface); +} + +.table-scroll--compact table, +.details-table { + min-inline-size: 28rem; +} + +th, +td { + border-bottom: 1px solid var(--color-border); + padding: 0.75rem; + text-align: left; + vertical-align: top; +} + +th { + background: var(--color-surface-strong); + font-size: 0.78rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.details-table th { + inline-size: 12rem; +} + +.status-badge { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 0.25rem 0.55rem; + background: #eef2f6; + color: #344054; + font-size: 0.78rem; + font-weight: 800; + white-space: nowrap; +} + +.status-badge--active, +.status-badge--current { + background: var(--color-success-bg); + color: var(--color-success); +} + +.status-badge--inactive, +.status-badge--no_history { + background: #eef2f6; + color: #475467; +} + +.status-badge--upcoming { + background: var(--color-warning-bg); + color: var(--color-warning); +} + +.status-badge--overdue { + background: var(--color-danger-bg); + color: var(--color-danger); +} + +.stacked-form { + display: grid; + gap: 1rem; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); + gap: 1rem; +} + +.field { + display: grid; + gap: 0.35rem; +} + +label { + font-weight: 800; +} + +input, +select, +textarea { + inline-size: 100%; + min-block-size: 2.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 0.65rem 0.75rem; + background: #ffffff; + color: var(--color-text); + font: inherit; +} + +textarea { + min-block-size: 7rem; +} + +.form-errors, +.field-warning { + border: 1px solid #fda29b; + border-radius: var(--radius-md); + padding: 0.85rem 1rem; + background: var(--color-danger-bg); + color: var(--color-danger); +} + +@media (max-width: 720px) { + body > header { + font-size: 0.9rem; + } + + .page-header, + .vehicle-hero { + display: grid; + } + + .page-header .button, + .action-row, + .action-row .button_to, + .action-row .button { + inline-size: 100%; + } + + .filter-nav { + display: grid; + grid-template-columns: repeat(3, 1fr); + } + + .filter-pill { + padding-inline: 0.5rem; + } +} diff --git a/app/controllers/vehicles_controller.rb b/app/controllers/vehicles_controller.rb index 7f57bd9..f82e1f5 100644 --- a/app/controllers/vehicles_controller.rb +++ b/app/controllers/vehicles_controller.rb @@ -4,6 +4,7 @@ class VehiclesController < ApplicationController def index @status = params[:status] @vehicles = vehicles_for_status(@status).order(:make, :model, :year) + @vehicle_maintenance_statuses = vehicle_maintenance_statuses(@vehicles) end def show @@ -82,15 +83,24 @@ class VehiclesController < ApplicationController def maintenance_schedule_statuses(schedules) schedules.index_with do |schedule| - latest_entry = latest_maintenance_entry_for(schedule) + latest_entry = latest_maintenance_entry_for(@vehicle, 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 + def vehicle_maintenance_statuses(vehicles) + vehicles.index_with do |vehicle| + vehicle.maintenance_schedules.active.order(:maintenance_type).map do |schedule| + latest_entry = latest_maintenance_entry_for(vehicle, schedule) + MaintenanceScheduleStatus.new(schedule, latest_entry, current_odometer: vehicle.current_odometer) + end + end + end + + def latest_maintenance_entry_for(vehicle, schedule) + vehicle.maintenance_entries .where("lower(name) = ?", schedule.maintenance_type.downcase) .order(date: :desc, created_at: :desc) .first diff --git a/app/views/vehicles/index.html.erb b/app/views/vehicles/index.html.erb index 491ef0e..c952905 100644 --- a/app/views/vehicles/index.html.erb +++ b/app/views/vehicles/index.html.erb @@ -1,38 +1,71 @@ -
-

<%= notice %>

+
+ <% if notice.present? %> +

<%= notice %>

+ <% end %> -

Vehicles

+ -
diff --git a/app/views/vehicles/show.html.erb b/app/views/vehicles/show.html.erb index 38169ec..94293fd 100644 --- a/app/views/vehicles/show.html.erb +++ b/app/views/vehicles/show.html.erb @@ -1,90 +1,252 @@ -
-

<%= notice %>

-

<%= flash[:warning] %>

+<% 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?) %> +<% attention_statuses = overdue_statuses + upcoming_statuses %> -

<%= link_to "Back to vehicles", vehicles_path %>

+
+ <% if notice.present? %> +

<%= notice %>

+ <% end %> + <% if flash[:warning].present? %> +

<%= flash[:warning] %>

+ <% end %> -

<%= @vehicle.year %> <%= @vehicle.make %> <%= @vehicle.model %>

+

<%= link_to "Back to vehicles", vehicles_path, class: "back-link" %>

-
- <%= link_to "Edit", edit_vehicle_path(@vehicle) %> - <% if @vehicle.active? %> - | <%= button_to "Deactivate", @vehicle, method: :delete %> +
+
+

<%= @vehicle.licence_plate %>

+

<%= @vehicle.year %> <%= @vehicle.make %> <%= @vehicle.model %>

+

<%= number_with_delimiter(@vehicle.current_odometer) %> miles

+
+
+ + <% if attention_statuses.any? %> +
+ <% if overdue_statuses.any? %> +
+

Overdue maintenance

+
    + <% overdue_statuses.each do |status| %> +
  • + <%= status.schedule.maintenance_type %>: <%= status.reason %>. + Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>. +
  • + <% end %> +
+
+ <% end %> + + <% if upcoming_statuses.any? %> +
+

Upcoming maintenance

+
    + <% upcoming_statuses.each do |status| %> +
  • + <%= status.schedule.maintenance_type %>: <%= status.reason %>. + Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>. +
  • + <% end %> +
+
+ <% end %> +
+ <% end %> + +
+ Vehicle Information + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Make<%= @vehicle.make %>
Model<%= @vehicle.model %>
Year<%= @vehicle.year %>
Color<%= @vehicle.color %>
VIN<%= @vehicle.vin %>
Licence plate<%= @vehicle.licence_plate %>
Current odometer<%= number_with_delimiter(@vehicle.current_odometer) %>
Fuel tank size<%= @vehicle.fuel_tank_size %>
Status<%= @vehicle.active? ? "Active" : "Inactive" %>
+
+
+ +
+ Record Fuel + + <%= form_with model: [@vehicle, @fuel_entry], class: "stacked-form" do |form| %> + <% if @fuel_entry.errors.any? %> +
+

<%= pluralize(@fuel_entry.errors.count, "error") %> prevented this fuel entry from being saved

+ +
    + <% @fuel_entry.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + +
+
+ <%= form.label :odometer %> + <%= form.number_field :odometer, min: @vehicle.current_odometer, required: true %> +
+ +
+ <%= form.label :gallons_pumped %> + <%= form.number_field :gallons_pumped, min: 0.01, step: "0.01", required: true %> +
+ +
+ <%= form.label :price_paid %> + <%= form.number_field :price_paid, min: 0.01, step: "0.01", required: true %> +
+ +
+ <%= form.label :date %> + <%= form.date_field :date, max: Date.current, required: true %> +
+
+ +
+ <%= form.submit "Record fuel", class: "button button--primary" %> +
<% end %> -
+ -
-

Vehicle Details

+
> + Record Maintenance -
-
Make
-
<%= @vehicle.make %>
+ <% if @maintenance_schedules.any? %> + <%= form_with model: [@vehicle, @maintenance_entry], class: "stacked-form", 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? %> +
+

<%= pluralize(@maintenance_entry.errors.count, "error") %> prevented this maintenance entry from being saved

-
Model
-
<%= @vehicle.model %>
+
    + <% @maintenance_entry.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> -
Year
-
<%= @vehicle.year %>
+
+
+ <%= form.label :name, "Maintenance type" %> + <%= form.select :name, options_for_select(@maintenance_schedules.map(&:maintenance_type), @maintenance_entry.name), { prompt: "Choose maintenance type" }, required: true %> +
-
Color
-
<%= @vehicle.color %>
+
+ <%= form.label :odometer %> + <%= 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" + } %> +
-
VIN
-
<%= @vehicle.vin %>
+
+ <%= form.label :cost %> + <%= form.number_field :cost, min: 0, step: "0.01", required: true %> +
-
Licence plate
-
<%= @vehicle.licence_plate %>
+
+ <%= form.label :date %> + <%= form.date_field :date, max: Date.current, required: true %> +
+
-
Current odometer
-
<%= number_with_delimiter(@vehicle.current_odometer) %>
+ -
Fuel tank size
-
<%= @vehicle.fuel_tank_size %>
+
+ <%= form.label :notes %> + <%= form.text_area :notes, required: true %> +
-
Status
-
<%= @vehicle.active? ? "Active" : "Inactive" %>
-
-
- -
-

Maintenance Schedules

- -

<%= link_to "Manage maintenance schedules", vehicle_maintenance_schedules_path(@vehicle) %>

- - <% 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? %> -
-

Overdue Maintenance

-
    - <% overdue_statuses.each do |status| %> -
  • - <%= status.schedule.maintenance_type %> is overdue: <%= status.reason %>. - Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>. -
  • - <% end %> -
-
+
+ <%= form.submit "Record maintenance", class: "button button--primary" %> +
+ <% end %> + <% else %> +

Add a maintenance schedule before recording maintenance.

<% end %> + - <% if upcoming_statuses.any? %> -
-

Upcoming Maintenance

-
    - <% upcoming_statuses.each do |status| %> -
  • - <%= status.schedule.maintenance_type %> is upcoming: <%= status.reason %>. - Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>. -
  • - <% end %> -
-
+
+ Maintenance Schedules + +

<%= link_to "Manage maintenance schedules", vehicle_maintenance_schedules_path(@vehicle), class: "button button--secondary" %>

+ + <% if attention_statuses.any? %> +
+ <% if overdue_statuses.any? %> +
+

Overdue maintenance

+
    + <% overdue_statuses.each do |status| %> +
  • + <%= status.schedule.maintenance_type %> is overdue: <%= status.reason %>. + Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>. +
  • + <% end %> +
+
+ <% end %> + + <% if upcoming_statuses.any? %> +
+

Upcoming maintenance

+
    + <% upcoming_statuses.each do |status| %> +
  • + <%= status.schedule.maintenance_type %> is upcoming: <%= status.reason %>. + Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>. +
  • + <% end %> +
+
+ <% end %> +
<% end %> <% if no_history_statuses.any? %> -
+

No Maintenance History

    <% no_history_statuses.each do |status| %> @@ -98,201 +260,114 @@ <% end %> <% if @maintenance_schedules.any? %> - - - - - - - - - - - - - <% @maintenance_schedules.each do |schedule| %> - <% status = @maintenance_schedule_statuses.fetch(schedule) %> +
    +
    TypeMileage intervalTime intervalNext due odometerNext due dateStatus
    + - - - - - - + + + + + + - <% end %> - -
    <%= schedule.maintenance_type %><%= number_with_delimiter(schedule.mileage_interval) %> miles<%= schedule.time_interval %> days<%= number_with_delimiter(status.due_odometer) %><%= status.due_date %><%= status.status_label %>TypeMileage intervalTime intervalNext due odometerNext due dateStatus
    - <% else %> -

    No maintenance schedules recorded.

    - <% end %> -
- -
-

Record Fuel

- - <%= form_with model: [@vehicle, @fuel_entry] do |form| %> - <% if @fuel_entry.errors.any? %> -
-

<%= pluralize(@fuel_entry.errors.count, "error") %> prevented this fuel entry from being saved

- -
    - <% @fuel_entry.errors.full_messages.each do |message| %> -
  • <%= message %>
  • + + + <% @maintenance_schedules.each do |schedule| %> + <% status = @maintenance_schedule_statuses.fetch(schedule) %> + + <%= schedule.maintenance_type %> + <%= number_with_delimiter(schedule.mileage_interval) %> miles + <%= schedule.time_interval %> days + <%= number_with_delimiter(status.due_odometer) %> + <%= status.due_date %> + <%= status.status_label %> + <% end %> -
-
- <% end %> - -
- <%= form.label :odometer %>
- <%= form.number_field :odometer, min: @vehicle.current_odometer, required: true %> + +
- -
- <%= form.label :gallons_pumped %>
- <%= form.number_field :gallons_pumped, min: 0.01, step: "0.01", required: true %> -
- -
- <%= form.label :price_paid %>
- <%= form.number_field :price_paid, min: 0.01, step: "0.01", required: true %> -
- -
- <%= form.label :date %>
- <%= form.date_field :date, max: Date.current, required: true %> -
- -
- <%= form.submit "Record fuel" %> -
- <% end %> -
- -
-

Record Maintenance

- - <% 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? %> -
-

<%= pluralize(@maintenance_entry.errors.count, "error") %> prevented this maintenance entry from being saved

- -
    - <% @maintenance_entry.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> - -
- <%= form.label :name, "Maintenance type" %>
- <%= form.select :name, options_for_select(@maintenance_schedules.map(&:maintenance_type), @maintenance_entry.name), { prompt: "Choose maintenance type" }, required: true %> -
- -
- <%= form.label :odometer %>
- <%= 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" - } %> -
- - - -
- <%= form.label :cost %>
- <%= form.number_field :cost, min: 0, step: "0.01", required: true %> -
- -
- <%= form.label :date %>
- <%= form.date_field :date, max: Date.current, required: true %> -
- -
- <%= form.label :notes %>
- <%= form.text_area :notes, required: true %> -
- -
- <%= form.submit "Record maintenance" %> -
- <% end %> <% else %> -

Add a maintenance schedule before recording maintenance.

+

No maintenance schedules recorded.

<% end %> -
+
-
-

Fuel History

- - <% if @fuel_entries.any? %> - - - - - - - - - - - - <% @fuel_entries.each do |entry| %> - - - - - - - - <% end %> - -
DateOdometerGallonsPrice paidUpdated by
<%= entry.date %><%= number_with_delimiter(entry.odometer) %><%= entry.gallons_pumped %><%= number_to_currency(entry.price_paid) %><%= entry.updated_by_user.name %>
- <% else %> -

No fuel entries recorded.

- <% end %> -
- -
-

Maintenance History

+
+ Maintenance History <% if @maintenance_entries.any? %> - - - - - - - - - - - - - <% @maintenance_entries.each do |entry| %> +
+
DateNameOdometerCostNotesUpdated by
+ - - - - - - + + + + + + - <% end %> - -
<%= entry.date %><%= entry.name %><%= number_with_delimiter(entry.odometer) %><%= number_to_currency(entry.cost) %><%= entry.notes %><%= entry.updated_by_user.name %>DateNameOdometerCostNotesUpdated by
+ + + <% @maintenance_entries.each do |entry| %> + + <%= entry.date %> + <%= entry.name %> + <%= number_with_delimiter(entry.odometer) %> + <%= number_to_currency(entry.cost) %> + <%= entry.notes %> + <%= entry.updated_by_user.name %> + + <% end %> + + + <% else %> -

No maintenance entries recorded.

+

No maintenance entries recorded.

<% end %> -
+ + +
+ Fuel History + + <% if @fuel_entries.any? %> +
+ + + + + + + + + + + + <% @fuel_entries.each do |entry| %> + + + + + + + + <% end %> + +
DateOdometerGallonsPrice paidUpdated by
<%= entry.date %><%= number_with_delimiter(entry.odometer) %><%= entry.gallons_pumped %><%= number_to_currency(entry.price_paid) %><%= entry.updated_by_user.name %>
+
+ <% else %> +

No fuel entries recorded.

+ <% end %> +
+ +
+ Vehicle Management +
+ <%= link_to "Edit", edit_vehicle_path(@vehicle), class: "button button--secondary" %> + <% if @vehicle.active? %> + <%= button_to "Deactivate", @vehicle, method: :delete, class: "button button--danger" %> + <% end %> +
+
+
diff --git a/executions/phase007.md b/executions/phase007.md new file mode 100644 index 0000000..9f3999c --- /dev/null +++ b/executions/phase007.md @@ -0,0 +1,14 @@ +# Phase 007 Execution + +## Goal +Improve the vehicle tracker UI for mobile-first use with dashboard-style vehicle tiles, clearer vehicle detail layout, collapsible sections, and maintenance status visibility. + +## Planned Changes +- Show vehicle tiles on the vehicle index instead of a plain table. +- Surface overdue/upcoming maintenance names on vehicle tiles. +- Reorder the vehicle detail page around the mobile fuel-entry workflow. +- Add top-of-page overdue/upcoming maintenance alerts only when needed. +- Convert vehicle details to a compact table. +- Make sections collapsible, with fuel open by default and maintenance open when attention is needed. +- Add lightweight dashboard styling in plain CSS. +- Update request specs for the new structure and status behavior. diff --git a/spec/requests/vehicles_spec.rb b/spec/requests/vehicles_spec.rb index 7947487..6e19731 100644 --- a/spec/requests/vehicles_spec.rb +++ b/spec/requests/vehicles_spec.rb @@ -81,6 +81,44 @@ RSpec.describe "Vehicles" do expect(response.body).to include(active_vehicle.model) expect(response.body).to include(inactive_vehicle.model) end + + it "shows upcoming maintenance names on vehicle tiles" 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 vehicles_path + + expect(response).to have_http_status(:ok) + expect(response.body).to include("vehicle-card--upcoming") + expect(response.body).to include("Upcoming maintenance") + expect(response.body).to include("Oil Change") + end + + it "shows overdue maintenance names on vehicle tiles" 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 vehicles_path + + expect(response).to have_http_status(:ok) + expect(response.body).to include("vehicle-card--overdue") + expect(response.body).to include("Overdue maintenance") + expect(response.body).to include("Oil Change") + end end describe "GET /vehicles/:id" do @@ -110,6 +148,7 @@ RSpec.describe "Vehicles" do expect(response).to have_http_status(:ok) expect(response.body).to include(vehicle.make) expect(response.body).to include(vehicle.model) + expect(response.body).to include("Vehicle Information") expect(response.body).to include("Maintenance Schedules") expect(response.body).to include("Manage maintenance schedules") expect(response.body).to include("Fuel History") @@ -121,6 +160,8 @@ RSpec.describe "Vehicles" do expect(response.body).to include(older_maintenance_entry.notes) 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("Record Fuel")).to be < response.body.index("Record Maintenance") + expect(response.body.index("Maintenance History")).to be < response.body.index("Fuel History") end it "shows overdue maintenance alerts" do @@ -137,8 +178,11 @@ RSpec.describe "Vehicles" do get vehicle_path(vehicle) expect(response).to have_http_status(:ok) - expect(response.body).to include("Overdue Maintenance") + expect(response.body).to include("Overdue maintenance") expect(response.body).to include("Oil Change is overdue") + expect(response.body).to include("aria-label=\"Maintenance attention needed\"") + expect(response.body).to include("Record Maintenance") + expect(response.body).to include("
") end it "shows upcoming maintenance warnings" do @@ -155,8 +199,28 @@ RSpec.describe "Vehicles" do get vehicle_path(vehicle) expect(response).to have_http_status(:ok) - expect(response.body).to include("Upcoming Maintenance") + expect(response.body).to include("Upcoming maintenance") expect(response.body).to include("Oil Change is upcoming") + expect(response.body).to include("aria-label=\"Maintenance attention needed\"") + expect(response.body).to include("Record Maintenance") + expect(response.body).to include("
") + end + + it "hides the top maintenance alert area when maintenance is current" do + vehicle = create(:vehicle, current_odometer: 10_100) + create(:maintenance_schedule, vehicle: vehicle, maintenance_type: "Oil Change") + create( + :maintenance_entry, + vehicle: vehicle, + name: "Oil Change", + odometer: 10_000, + date: Date.current + ) + + get vehicle_path(vehicle) + + expect(response).to have_http_status(:ok) + expect(response.body).not_to include("aria-label=\"Maintenance attention needed\"") end it "shows no-history maintenance status without marking it overdue" do