Made UI pretty!
This commit is contained in:
@@ -8,3 +8,458 @@
|
|||||||
*
|
*
|
||||||
* Consider organizing styles into separate files for maintainability.
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class VehiclesController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@status = params[:status]
|
@status = params[:status]
|
||||||
@vehicles = vehicles_for_status(@status).order(:make, :model, :year)
|
@vehicles = vehicles_for_status(@status).order(:make, :model, :year)
|
||||||
|
@vehicle_maintenance_statuses = vehicle_maintenance_statuses(@vehicles)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -82,15 +83,24 @@ class VehiclesController < ApplicationController
|
|||||||
|
|
||||||
def maintenance_schedule_statuses(schedules)
|
def maintenance_schedule_statuses(schedules)
|
||||||
schedules.index_with do |schedule|
|
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)
|
status = MaintenanceScheduleStatus.new(schedule, latest_entry, current_odometer: @vehicle.current_odometer)
|
||||||
MaintenanceNotification.record_status_change(schedule, status)
|
MaintenanceNotification.record_status_change(schedule, status)
|
||||||
status
|
status
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def latest_maintenance_entry_for(schedule)
|
def vehicle_maintenance_statuses(vehicles)
|
||||||
@vehicle.maintenance_entries
|
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)
|
.where("lower(name) = ?", schedule.maintenance_type.downcase)
|
||||||
.order(date: :desc, created_at: :desc)
|
.order(date: :desc, created_at: :desc)
|
||||||
.first
|
.first
|
||||||
|
|||||||
@@ -1,38 +1,71 @@
|
|||||||
<main>
|
<main class="page-shell">
|
||||||
<p><%= notice %></p>
|
<% if notice.present? %>
|
||||||
|
<p class="flash flash--notice"><%= notice %></p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<h1>Vehicles</h1>
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Fleet dashboard</p>
|
||||||
|
<h1>Vehicles</h1>
|
||||||
|
</div>
|
||||||
|
<%= link_to "New vehicle", new_vehicle_path, class: "button button--primary" %>
|
||||||
|
</header>
|
||||||
|
|
||||||
<nav aria-label="Vehicle filters">
|
<nav class="filter-nav" aria-label="Vehicle filters">
|
||||||
<%= link_to "Active", vehicles_path %> |
|
<%= link_to "Active", vehicles_path, class: ["filter-pill", ("filter-pill--active" if @status.blank?)] %>
|
||||||
<%= link_to "Inactive", vehicles_path(status: "inactive") %> |
|
<%= link_to "Inactive", vehicles_path(status: "inactive"), class: ["filter-pill", ("filter-pill--active" if @status == "inactive")] %>
|
||||||
<%= link_to "All", vehicles_path(status: "all") %>
|
<%= link_to "All", vehicles_path(status: "all"), class: ["filter-pill", ("filter-pill--active" if @status == "all")] %>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<p><%= link_to "New vehicle", new_vehicle_path %></p>
|
|
||||||
|
|
||||||
<% if @vehicles.any? %>
|
<% if @vehicles.any? %>
|
||||||
<table>
|
<section class="vehicle-grid" aria-label="Vehicles">
|
||||||
<thead>
|
<% @vehicles.each do |vehicle| %>
|
||||||
<tr>
|
<% statuses = @vehicle_maintenance_statuses.fetch(vehicle, []) %>
|
||||||
<th>Vehicle</th>
|
<% overdue_statuses = statuses.select(&:overdue?) %>
|
||||||
<th>Licence plate</th>
|
<% upcoming_statuses = statuses.select(&:upcoming?) %>
|
||||||
<th>Odometer</th>
|
<% card_classes = ["vehicle-card"] %>
|
||||||
<th>Status</th>
|
<% card_classes << "vehicle-card--overdue" if overdue_statuses.any? %>
|
||||||
</tr>
|
<% card_classes << "vehicle-card--upcoming" if overdue_statuses.empty? && upcoming_statuses.any? %>
|
||||||
</thead>
|
<% card_classes << "vehicle-card--inactive" unless vehicle.active? %>
|
||||||
<tbody>
|
|
||||||
<% @vehicles.each do |vehicle| %>
|
<article class="<%= card_classes.compact.join(" ") %>">
|
||||||
<tr>
|
<%= link_to vehicle, class: "vehicle-card__link" do %>
|
||||||
<td><%= link_to "#{vehicle.year} #{vehicle.make} #{vehicle.model}", vehicle %></td>
|
<div class="vehicle-card__header">
|
||||||
<td><%= vehicle.licence_plate %></td>
|
<h2><%= vehicle.year %> <%= vehicle.make %> <%= vehicle.model %></h2>
|
||||||
<td><%= number_with_delimiter(vehicle.current_odometer) %></td>
|
<span class="status-badge <%= vehicle.active? ? "status-badge--active" : "status-badge--inactive" %>">
|
||||||
<td><%= vehicle.active? ? "Active" : "Inactive" %></td>
|
<%= vehicle.active? ? "Active" : "Inactive" %>
|
||||||
</tr>
|
</span>
|
||||||
<% end %>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
<dl class="vehicle-card__facts">
|
||||||
|
<div>
|
||||||
|
<dt>Plate</dt>
|
||||||
|
<dd><%= vehicle.licence_plate %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Odometer</dt>
|
||||||
|
<dd><%= number_with_delimiter(vehicle.current_odometer) %></dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<% if overdue_statuses.any? %>
|
||||||
|
<div class="maintenance-summary maintenance-summary--overdue">
|
||||||
|
<strong>Overdue maintenance</strong>
|
||||||
|
<p><%= overdue_statuses.map { |status| status.schedule.maintenance_type }.to_sentence %></p>
|
||||||
|
</div>
|
||||||
|
<% elsif upcoming_statuses.any? %>
|
||||||
|
<div class="maintenance-summary maintenance-summary--upcoming">
|
||||||
|
<strong>Upcoming maintenance</strong>
|
||||||
|
<p><%= upcoming_statuses.map { |status| status.schedule.maintenance_type }.to_sentence %></p>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="maintenance-current">No upcoming maintenance</p>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</article>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>No vehicles found.</p>
|
<p class="empty-state">No vehicles found.</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+327
-252
@@ -1,90 +1,252 @@
|
|||||||
<main>
|
<% overdue_statuses = @maintenance_schedule_statuses.values.select(&:overdue?) %>
|
||||||
<p><%= notice %></p>
|
<% upcoming_statuses = @maintenance_schedule_statuses.values.select(&:upcoming?) %>
|
||||||
<p><%= flash[:warning] %></p>
|
<% no_history_statuses = @maintenance_schedule_statuses.values.select(&:no_history?) %>
|
||||||
|
<% attention_statuses = overdue_statuses + upcoming_statuses %>
|
||||||
|
|
||||||
<p><%= link_to "Back to vehicles", vehicles_path %></p>
|
<main class="page-shell vehicle-show">
|
||||||
|
<% if notice.present? %>
|
||||||
|
<p class="flash flash--notice"><%= notice %></p>
|
||||||
|
<% end %>
|
||||||
|
<% if flash[:warning].present? %>
|
||||||
|
<p class="flash flash--warning"><%= flash[:warning] %></p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<h1><%= @vehicle.year %> <%= @vehicle.make %> <%= @vehicle.model %></h1>
|
<p><%= link_to "Back to vehicles", vehicles_path, class: "back-link" %></p>
|
||||||
|
|
||||||
<div>
|
<header class="vehicle-hero">
|
||||||
<%= link_to "Edit", edit_vehicle_path(@vehicle) %>
|
<div>
|
||||||
<% if @vehicle.active? %>
|
<p class="eyebrow"><%= @vehicle.licence_plate %></p>
|
||||||
| <%= button_to "Deactivate", @vehicle, method: :delete %>
|
<h1><%= @vehicle.year %> <%= @vehicle.make %> <%= @vehicle.model %></h1>
|
||||||
|
<p><%= number_with_delimiter(@vehicle.current_odometer) %> miles</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<% if attention_statuses.any? %>
|
||||||
|
<section class="alert-stack" aria-label="Maintenance attention needed">
|
||||||
|
<% if overdue_statuses.any? %>
|
||||||
|
<div class="alert-card alert-card--overdue">
|
||||||
|
<h2>Overdue maintenance</h2>
|
||||||
|
<ul>
|
||||||
|
<% overdue_statuses.each do |status| %>
|
||||||
|
<li>
|
||||||
|
<strong><%= status.schedule.maintenance_type %></strong>: <%= status.reason %>.
|
||||||
|
Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>.
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if upcoming_statuses.any? %>
|
||||||
|
<div class="alert-card alert-card--upcoming">
|
||||||
|
<h2>Upcoming maintenance</h2>
|
||||||
|
<ul>
|
||||||
|
<% upcoming_statuses.each do |status| %>
|
||||||
|
<li>
|
||||||
|
<strong><%= status.schedule.maintenance_type %></strong>: <%= status.reason %>.
|
||||||
|
Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>.
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<details class="dashboard-section">
|
||||||
|
<summary>Vehicle Information</summary>
|
||||||
|
|
||||||
|
<div class="table-scroll table-scroll--compact">
|
||||||
|
<table class="details-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Make</th>
|
||||||
|
<td><%= @vehicle.make %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Model</th>
|
||||||
|
<td><%= @vehicle.model %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Year</th>
|
||||||
|
<td><%= @vehicle.year %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Color</th>
|
||||||
|
<td><%= @vehicle.color %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>VIN</th>
|
||||||
|
<td><%= @vehicle.vin %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Licence plate</th>
|
||||||
|
<td><%= @vehicle.licence_plate %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Current odometer</th>
|
||||||
|
<td><%= number_with_delimiter(@vehicle.current_odometer) %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Fuel tank size</th>
|
||||||
|
<td><%= @vehicle.fuel_tank_size %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<td><%= @vehicle.active? ? "Active" : "Inactive" %></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="dashboard-section dashboard-section--primary" open>
|
||||||
|
<summary>Record Fuel</summary>
|
||||||
|
|
||||||
|
<%= form_with model: [@vehicle, @fuel_entry], class: "stacked-form" do |form| %>
|
||||||
|
<% if @fuel_entry.errors.any? %>
|
||||||
|
<section class="form-errors">
|
||||||
|
<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 class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<%= form.label :odometer %>
|
||||||
|
<%= form.number_field :odometer, min: @vehicle.current_odometer, required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<%= form.label :gallons_pumped %>
|
||||||
|
<%= form.number_field :gallons_pumped, min: 0.01, step: "0.01", required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<%= form.label :price_paid %>
|
||||||
|
<%= form.number_field :price_paid, min: 0.01, step: "0.01", required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<%= form.label :date %>
|
||||||
|
<%= form.date_field :date, max: Date.current, required: true %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.submit "Record fuel", class: "button button--primary" %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</details>
|
||||||
|
|
||||||
<section>
|
<details class="dashboard-section" <%= "open" if attention_statuses.any? %>>
|
||||||
<h2>Vehicle Details</h2>
|
<summary>Record Maintenance</summary>
|
||||||
|
|
||||||
<dl>
|
<% if @maintenance_schedules.any? %>
|
||||||
<dt>Make</dt>
|
<%= form_with model: [@vehicle, @maintenance_entry], class: "stacked-form", data: {
|
||||||
<dd><%= @vehicle.make %></dd>
|
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 class="form-errors">
|
||||||
|
<h3><%= pluralize(@maintenance_entry.errors.count, "error") %> prevented this maintenance entry from being saved</h3>
|
||||||
|
|
||||||
<dt>Model</dt>
|
<ul>
|
||||||
<dd><%= @vehicle.model %></dd>
|
<% @maintenance_entry.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<dt>Year</dt>
|
<div class="form-grid">
|
||||||
<dd><%= @vehicle.year %></dd>
|
<div class="field">
|
||||||
|
<%= 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 %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<dt>Color</dt>
|
<div class="field">
|
||||||
<dd><%= @vehicle.color %></dd>
|
<%= 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"
|
||||||
|
} %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<dt>VIN</dt>
|
<div class="field">
|
||||||
<dd><%= @vehicle.vin %></dd>
|
<%= form.label :cost %>
|
||||||
|
<%= form.number_field :cost, min: 0, step: "0.01", required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<dt>Licence plate</dt>
|
<div class="field">
|
||||||
<dd><%= @vehicle.licence_plate %></dd>
|
<%= form.label :date %>
|
||||||
|
<%= form.date_field :date, max: Date.current, required: true %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<dt>Current odometer</dt>
|
<p class="field-warning" hidden data-maintenance-odometer-warning-target="warning">
|
||||||
<dd><%= number_with_delimiter(@vehicle.current_odometer) %></dd>
|
Warning: this maintenance odometer is more than 500 miles behind the current vehicle odometer.
|
||||||
|
</p>
|
||||||
|
|
||||||
<dt>Fuel tank size</dt>
|
<div class="field">
|
||||||
<dd><%= @vehicle.fuel_tank_size %></dd>
|
<%= form.label :notes %>
|
||||||
|
<%= form.text_area :notes, required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<dt>Status</dt>
|
<div>
|
||||||
<dd><%= @vehicle.active? ? "Active" : "Inactive" %></dd>
|
<%= form.submit "Record maintenance", class: "button button--primary" %>
|
||||||
</dl>
|
</div>
|
||||||
</section>
|
<% end %>
|
||||||
|
<% else %>
|
||||||
<section>
|
<p class="empty-state">Add a maintenance schedule before recording maintenance.</p>
|
||||||
<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 %>
|
<% end %>
|
||||||
|
</details>
|
||||||
|
|
||||||
<% if upcoming_statuses.any? %>
|
<details class="dashboard-section">
|
||||||
<section>
|
<summary>Maintenance Schedules</summary>
|
||||||
<h3>Upcoming Maintenance</h3>
|
|
||||||
<ul>
|
<p><%= link_to "Manage maintenance schedules", vehicle_maintenance_schedules_path(@vehicle), class: "button button--secondary" %></p>
|
||||||
<% upcoming_statuses.each do |status| %>
|
|
||||||
<li>
|
<% if attention_statuses.any? %>
|
||||||
<%= status.schedule.maintenance_type %> is upcoming: <%= status.reason %>.
|
<div class="section-alerts">
|
||||||
Due at <%= number_with_delimiter(status.due_odometer) %> miles or <%= status.due_date %>.
|
<% if overdue_statuses.any? %>
|
||||||
</li>
|
<section class="mini-alert mini-alert--overdue">
|
||||||
<% end %>
|
<h3>Overdue maintenance</h3>
|
||||||
</ul>
|
<ul>
|
||||||
</section>
|
<% 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 class="mini-alert mini-alert--upcoming">
|
||||||
|
<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 %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if no_history_statuses.any? %>
|
<% if no_history_statuses.any? %>
|
||||||
<section>
|
<section class="mini-alert">
|
||||||
<h3>No Maintenance History</h3>
|
<h3>No Maintenance History</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<% no_history_statuses.each do |status| %>
|
<% no_history_statuses.each do |status| %>
|
||||||
@@ -98,201 +260,114 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if @maintenance_schedules.any? %>
|
<% if @maintenance_schedules.any? %>
|
||||||
<table>
|
<div class="table-scroll">
|
||||||
<thead>
|
<table>
|
||||||
<tr>
|
<thead>
|
||||||
<th>Type</th>
|
|
||||||
<th>Mileage interval</th>
|
|
||||||
<th>Time interval</th>
|
|
||||||
<th>Next due odometer</th>
|
|
||||||
<th>Next due date</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% @maintenance_schedules.each do |schedule| %>
|
|
||||||
<% status = @maintenance_schedule_statuses.fetch(schedule) %>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= schedule.maintenance_type %></td>
|
<th>Type</th>
|
||||||
<td><%= number_with_delimiter(schedule.mileage_interval) %> miles</td>
|
<th>Mileage interval</th>
|
||||||
<td><%= schedule.time_interval %> days</td>
|
<th>Time interval</th>
|
||||||
<td><%= number_with_delimiter(status.due_odometer) %></td>
|
<th>Next due odometer</th>
|
||||||
<td><%= status.due_date %></td>
|
<th>Next due date</th>
|
||||||
<td><%= status.status_label %></td>
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
<% @maintenance_schedules.each do |schedule| %>
|
||||||
<% else %>
|
<% status = @maintenance_schedule_statuses.fetch(schedule) %>
|
||||||
<p>No maintenance schedules recorded.</p>
|
<tr>
|
||||||
<% end %>
|
<td><%= schedule.maintenance_type %></td>
|
||||||
</section>
|
<td><%= number_with_delimiter(schedule.mileage_interval) %> miles</td>
|
||||||
|
<td><%= schedule.time_interval %> days</td>
|
||||||
<section>
|
<td><%= number_with_delimiter(status.due_odometer) %></td>
|
||||||
<h2>Record Fuel</h2>
|
<td><%= status.due_date %></td>
|
||||||
|
<td><span class="status-badge status-badge--<%= status.status %>"><%= status.status_label %></span></td>
|
||||||
<%= form_with model: [@vehicle, @fuel_entry] do |form| %>
|
</tr>
|
||||||
<% 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 %>
|
<% end %>
|
||||||
</ul>
|
</tbody>
|
||||||
</section>
|
</table>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<%= form.label :odometer %><br>
|
|
||||||
<%= form.number_field :odometer, min: @vehicle.current_odometer, required: true %>
|
|
||||||
</div>
|
</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 %>
|
<% else %>
|
||||||
<p>Add a maintenance schedule before recording maintenance.</p>
|
<p class="empty-state">No maintenance schedules recorded.</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
</details>
|
||||||
|
|
||||||
<section>
|
<details class="dashboard-section">
|
||||||
<h2>Fuel History</h2>
|
<summary>Maintenance History</summary>
|
||||||
|
|
||||||
<% 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? %>
|
<% if @maintenance_entries.any? %>
|
||||||
<table>
|
<div class="table-scroll">
|
||||||
<thead>
|
<table>
|
||||||
<tr>
|
<thead>
|
||||||
<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>
|
<tr>
|
||||||
<td><%= entry.date %></td>
|
<th>Date</th>
|
||||||
<td><%= entry.name %></td>
|
<th>Name</th>
|
||||||
<td><%= number_with_delimiter(entry.odometer) %></td>
|
<th>Odometer</th>
|
||||||
<td><%= number_to_currency(entry.cost) %></td>
|
<th>Cost</th>
|
||||||
<td><%= entry.notes %></td>
|
<th>Notes</th>
|
||||||
<td><%= entry.updated_by_user.name %></td>
|
<th>Updated by</th>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
<% @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>
|
||||||
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>No maintenance entries recorded.</p>
|
<p class="empty-state">No maintenance entries recorded.</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
</details>
|
||||||
|
|
||||||
|
<details class="dashboard-section">
|
||||||
|
<summary>Fuel History</summary>
|
||||||
|
|
||||||
|
<% if @fuel_entries.any? %>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="empty-state">No fuel entries recorded.</p>
|
||||||
|
<% end %>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="dashboard-section">
|
||||||
|
<summary>Vehicle Management</summary>
|
||||||
|
<div class="action-row">
|
||||||
|
<%= 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 %>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -81,6 +81,44 @@ RSpec.describe "Vehicles" do
|
|||||||
expect(response.body).to include(active_vehicle.model)
|
expect(response.body).to include(active_vehicle.model)
|
||||||
expect(response.body).to include(inactive_vehicle.model)
|
expect(response.body).to include(inactive_vehicle.model)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "GET /vehicles/:id" do
|
describe "GET /vehicles/:id" do
|
||||||
@@ -110,6 +148,7 @@ RSpec.describe "Vehicles" do
|
|||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
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("Vehicle Information")
|
||||||
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("Manage maintenance schedules")
|
||||||
expect(response.body).to include("Fuel History")
|
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).to include(older_maintenance_entry.notes)
|
||||||
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)
|
||||||
|
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
|
end
|
||||||
|
|
||||||
it "shows overdue maintenance alerts" do
|
it "shows overdue maintenance alerts" do
|
||||||
@@ -137,8 +178,11 @@ RSpec.describe "Vehicles" do
|
|||||||
get vehicle_path(vehicle)
|
get vehicle_path(vehicle)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
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("Oil Change is overdue")
|
||||||
|
expect(response.body).to include("aria-label=\"Maintenance attention needed\"")
|
||||||
|
expect(response.body).to include("<summary>Record Maintenance</summary>")
|
||||||
|
expect(response.body).to include("<details class=\"dashboard-section\" open>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "shows upcoming maintenance warnings" do
|
it "shows upcoming maintenance warnings" do
|
||||||
@@ -155,8 +199,28 @@ RSpec.describe "Vehicles" do
|
|||||||
get vehicle_path(vehicle)
|
get vehicle_path(vehicle)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
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("Oil Change is upcoming")
|
||||||
|
expect(response.body).to include("aria-label=\"Maintenance attention needed\"")
|
||||||
|
expect(response.body).to include("<summary>Record Maintenance</summary>")
|
||||||
|
expect(response.body).to include("<details class=\"dashboard-section\" open>")
|
||||||
|
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
|
end
|
||||||
|
|
||||||
it "shows no-history maintenance status without marking it overdue" do
|
it "shows no-history maintenance status without marking it overdue" do
|
||||||
|
|||||||
Reference in New Issue
Block a user