Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
//= link cliente/budget_approvals.js
//= link controllers/onboarding_welcome_controller.js
//= link controllers/onboarding_checklist_controller.js
//= link controllers/onboarding_tour_controller.js
8 changes: 6 additions & 2 deletions app/controllers/app/onboarding_progress_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class App::OnboardingProgressController < ApplicationController
skip_authorization_check

ALLOWED_OPERATIONS = %w[complete_step dismiss resume finish].freeze
ALLOWED_OPERATIONS = %w[complete_step dismiss resume finish set_last_seen].freeze

def show
render json: response_payload
Expand All @@ -23,6 +23,10 @@ def update
onboarding_progress.resume!
when "finish"
onboarding_progress.finish!
when "set_last_seen"
return render_missing_step_key if params[:step_key].blank?

onboarding_progress.set_last_seen_step!(params[:step_key])
end

render json: response_payload
Expand Down Expand Up @@ -64,6 +68,6 @@ def render_invalid_operation
end

def render_missing_step_key
render json: { success: false, error: "step_key is required for operation complete_step" }, status: :unprocessable_entity
render json: { success: false, error: "step_key is required for this operation" }, status: :unprocessable_entity
end
end
138 changes: 138 additions & 0 deletions app/javascript/controllers/onboarding_tour_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static values = {
endpoint: String,
steps: Array,
autoStart: Boolean
}

connect() {
this.intro = null

if (!this.autoStartValue) return

this.startTour().catch((error) => {
console.warn("[OnboardingTour] failed to start", error)
})
}

disconnect() {
if (!this.intro) return

this.intro.exit()
this.intro = null
}

async startTour() {
if (typeof window.introJs === "undefined") return

const progress = await this.fetchProgress()
const builtSteps = this.buildSteps()
if (builtSteps.length === 0) return

this.intro = window.introJs()
this.intro.setOptions({
steps: builtSteps,
showProgress: true,
showBullets: false,
exitOnOverlayClick: false,
nextLabel: "Próximo",
prevLabel: "Voltar",
doneLabel: "Concluir",
skipLabel: "Pular"
})

this.intro.onchange((targetElement) => {
const stepKey = targetElement?.dataset?.onboardingTourStepKey
if (!stepKey) return

this.persistLastSeen(stepKey)
})

this.intro.onexit(() => this.clearAutoStartParam())
this.intro.oncomplete(() => this.clearAutoStartParam())

const resumeIndex = this.findResumeIndex(progress?.last_seen_step, builtSteps)
this.intro.start()

if (resumeIndex > 0) {
this.intro.goToStep(resumeIndex + 1)
}
}

async fetchProgress() {
if (!this.hasEndpointValue || !this.endpointValue) return null

try {
const response = await fetch(this.endpointValue, {
method: "GET",
headers: { "Accept": "application/json" },
credentials: "same-origin"
})

if (!response.ok) return null

const payload = await response.json()
return payload.onboarding_progress || null
} catch (_error) {
return null
}
}

buildSteps() {
const configured = Array.isArray(this.stepsValue) ? this.stepsValue : []

return configured
.map((step) => {
const element = document.querySelector(step.selector)
if (!element) return null

element.dataset.onboardingTourStepKey = step.key

return {
element,
title: step.title,
intro: step.description,
tooltipClass: "introjs-tooltip-onboarding"
}
})
.filter(Boolean)
}

findResumeIndex(lastSeenStep, steps) {
if (!lastSeenStep) return 0

const idx = steps.findIndex((step) => step.element?.dataset?.onboardingTourStepKey === lastSeenStep)
if (idx < 0) return 0

return Math.min(idx + 1, Math.max(steps.length - 1, 0))
}

async persistLastSeen(stepKey) {
if (!this.hasEndpointValue || !this.endpointValue || !stepKey) return

const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute("content")

try {
await fetch(this.endpointValue, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-CSRF-Token": csrfToken || ""
},
credentials: "same-origin",
body: JSON.stringify({ operation: "set_last_seen", step_key: stepKey })
})
} catch (_error) {
// noop
}
}

clearAutoStartParam() {
const url = new URL(window.location.href)
url.searchParams.delete("onboarding_tour")
window.history.replaceState({}, "", url.toString())
}
}
4 changes: 3 additions & 1 deletion app/javascript/controllers/onboarding_welcome_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export default class extends Controller {
this.dispatch("started")

if (this.hasStartPathValue && this.startPathValue) {
window.location.href = this.startPathValue
const url = new URL(this.startPathValue, window.location.origin)
url.searchParams.set("onboarding_tour", "1")
window.location.href = url.toString()
return
}

Expand Down
5 changes: 5 additions & 0 deletions app/models/user_onboarding_progress.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ def finish!
update!(finished_at: Time.current)
end

def set_last_seen_step!(step_key)
key = normalize_step_key(step_key)
update!(last_seen_step: key)
end

def finished_all_steps?
(STEP_KEYS - completed_step_keys).empty?
end
Expand Down
40 changes: 40 additions & 0 deletions app/views/app/dashboard/_onboarding_tour.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<%
tour_steps = [
{
key: "created_technician",
selector: "#onboarding-tour-technicians-menu",
title: "Cadastre os técnicos",
description: "Aqui você cadastra os técnicos que vão receber as ordens de serviço."
},
{
key: "created_customer",
selector: "#onboarding-tour-clients-menu",
title: "Cadastre os clientes",
description: "Nesta área você registra os clientes e mantém o histórico centralizado."
},
{
key: "created_first_work_order",
selector: "#onboarding-tour-order-services-menu",
title: "Crie a primeira OS",
description: "Use Ordens de Serviço para abrir, acompanhar e concluir atendimentos."
},
{
key: "moved_work_order_status",
selector: "#onboarding-tour-order-services-menu",
title: "Atualize os status",
description: "Depois de criar a OS, atualize os status para manter a operação rastreável."
},
{
key: "viewed_reports",
selector: "#onboarding-tour-reports-menu",
title: "Acompanhe os relatórios",
description: "Aqui você visualiza indicadores de volume, produtividade e desempenho."
}
]
%>
<div
data-controller="onboarding-tour"
data-onboarding-tour-endpoint-value="<%= app_onboarding_progress_path %>"
data-onboarding-tour-steps-value="<%= json_escape(tour_steps.to_json) %>"
data-onboarding-tour-auto-start-value="<%= params[:onboarding_tour].to_s == '1' %>"
></div>
4 changes: 4 additions & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="<%= asset_path('css/app.css') %>" rel="stylesheet">
<link href="<%= asset_path('css/icons.css') %>" rel="stylesheet">
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/intro.js/minified/introjs.min.css" %>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="<%= asset_path('css/dark-theme.css') %>" />
<link rel="stylesheet" href="<%= asset_path('css/semi-dark.css') %>" />
Expand All @@ -46,6 +47,9 @@
<div class="wrapper">
<%= render 'layouts/partials/sidebar' %>
<%= render 'layouts/partials/navbar' %>
<% if request.subdomain == "app" && current_user.present? && !current_user.tecnico? %>
<%= render "app/dashboard/onboarding_tour" %>
<% end %>

<%= yield %>
</div>
Expand Down
3 changes: 2 additions & 1 deletion app/views/layouts/partials/_scripts.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<%= javascript_include_tag "plugins/vectormap/jquery-jvectormap-2.0.2.min.js" %>
<%= javascript_include_tag "plugins/vectormap/jquery-jvectormap-world-mill-en.js" %>
<%= javascript_include_tag "plugins/chartjs/js/chart.js" %>
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/intro.js/minified/intro.min.js" %>
<%= javascript_include_tag "js/index.js" %>
<!-- app JS -->
<%= javascript_include_tag "js/app.js" %>
<%= javascript_include_tag "js/app.js" %>
10 changes: 5 additions & 5 deletions app/views/layouts/partials/_sidebar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
</div>
<!--navigation-->
<ul class="metismenu" id="menu" style="position: relative; z-index: 10;">
<li class="list-group-item <%= active_menu_class(app_dashboard_path) %>">
<li id="onboarding-tour-dashboard-menu" class="list-group-item <%= active_menu_class(app_dashboard_path) %>">
<%= link_to app_dashboard_path do %>
<div class="parent-icon"><i class='bx bx-home-alt'></i></div>
<div class="menu-title">Dashboard</div>
<% end %>
</li>

<% if can?(:manage, Client) %>
<li class="list-group-item <%= active_menu_class(app_clients_path) %>">
<li id="onboarding-tour-clients-menu" class="list-group-item <%= active_menu_class(app_clients_path) %>">
<%= link_to app_clients_path do %>
<div class="parent-icon"><i class='bx bx-user'></i></div>
<div class="menu-title">Clientes</div>
Expand All @@ -32,7 +32,7 @@
<% end %>

<% if can?(:manage, User) %>
<li class="list-group-item <%= active_menu_class(app_technicians_path) %>">
<li id="onboarding-tour-technicians-menu" class="list-group-item <%= active_menu_class(app_technicians_path) %>">
<%= link_to app_technicians_path do %>
<div class="parent-icon"><i class='bx bx-run'></i></div>
<div class="menu-title">Técnicos</div>
Expand All @@ -41,7 +41,7 @@
<% end %>

<% if can?(:manage, OrderService) || can?(:read, OrderService) %>
<li class="list-group-item <%= active_menu_class(app_order_services_path) %>">
<li id="onboarding-tour-order-services-menu" class="list-group-item <%= active_menu_class(app_order_services_path) %>">
<%= link_to app_order_services_path do %>
<div class="parent-icon"><i class='bx bx-edit'></i></div>
<div class="menu-title">Ordens de Serviço</div>
Expand All @@ -50,7 +50,7 @@
<% end %>

<% if can?(:manage, Report) %>
<li class="list-group-item <%= active_menu_class(app_reports_path) %>">
<li id="onboarding-tour-reports-menu" class="list-group-item <%= active_menu_class(app_reports_path) %>">
<%= link_to app_reports_path do %>
<div class="parent-icon"><i class='bx bx-file'></i></div>
<div class="menu-title">Relatórios</div>
Expand Down
Loading