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
512 changes: 387 additions & 125 deletions app/assets/stylesheets/css/app.css

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions app/controllers/app/budgets_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def create
@budget = base_scope.new(budget_params)

if @budget.save
mark_onboarding_step("created_budget")
redirect_to app_budgets_path, notice: "Orçamento criado com sucesso."
else
@budget.service_items.build if @budget.service_items.empty?
Expand Down Expand Up @@ -73,6 +74,7 @@ def send_for_approval
def approve
already_linked = @budget.order_service.present?
@budget.approve_and_create_order_service!(approver_role: :gestor)
mark_onboarding_step("created_first_work_order")
notice = already_linked ? "Orçamento já aprovado. OS vinculada mantida como pendente." : "Orçamento aprovado pelo gestor e OS criada como pendente."
redirect_to app_budget_path(@budget), notice: notice
rescue ActiveRecord::RecordInvalid => e
Expand Down
25 changes: 4 additions & 21 deletions app/controllers/app/dashboard_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
class App::DashboardController < ApplicationController
ONBOARDING_STEP_PATHS = {
"created_technician" => :app_technicians_path,
"created_customer" => :app_clients_path,
"created_first_work_order" => :app_order_services_path,
"moved_work_order_status" => :app_order_services_path,
"viewed_reports" => :app_reports_path
}.freeze

def index
authorize! :read, :dashboard

Expand Down Expand Up @@ -155,7 +147,6 @@ def initialize_default_dashboard_variables
def set_onboarding_welcome_modal
@onboarding_progress = current_user.user_onboarding_progress
@show_onboarding_welcome_modal = onboarding_welcome_eligible?
@onboarding_start_path = next_onboarding_step_path
@show_onboarding_checklist = !current_user.tecnico?
@onboarding_checklist_steps = onboarding_checklist_steps
end
Expand All @@ -165,26 +156,18 @@ def onboarding_welcome_eligible?
return true if @onboarding_progress.nil?
return false if @onboarding_progress.dismissed_at.present?
return false if @onboarding_progress.finished_at.present?
return false if @onboarding_progress.last_seen_step.present?
return false if @onboarding_progress.completed_steps_count.positive?

!@onboarding_progress.finished_all_steps?
end

def next_onboarding_step_path
completed_steps = @onboarding_progress&.completed_steps || {}

next_step = UserOnboardingProgress::STEP_KEYS.find do |step_key|
completed_steps.fetch(step_key, false) != true
end

route_name = ONBOARDING_STEP_PATHS[next_step] || :app_dashboard_path
send(route_name)
end

def onboarding_checklist_steps
[
{ key: "created_technician", label: "Cadastrar técnico", path: app_technicians_path },
{ key: "created_customer", label: "Cadastrar cliente", path: app_clients_path },
{ key: "created_first_work_order", label: "Criar primeira ordem de serviço", path: app_order_services_path },
{ key: "created_budget", label: "Criar primeiro orçamento", path: app_budgets_path },
{ key: "created_first_work_order", label: "Aprovar orçamento para gerar a primeira OS", path: app_budgets_path },
{ key: "moved_work_order_status", label: "Atualizar status da ordem de serviço", path: app_order_services_path },
{ key: "viewed_reports", label: "Visualizar relatórios", path: app_reports_path }
]
Expand Down
1 change: 0 additions & 1 deletion app/controllers/app/order_services_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ def create
@order_service.created_without_budget = true

if @order_service.update(order_service_params_with_auto_schedule_status)
mark_onboarding_step("created_first_work_order")
redirect_to app_order_service_url(@order_service), notice: "Ordem de serviço criada com sucesso."
else
set_other_resources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ export default class extends Controller {
this.detailsExpanded = false
this.summaryTarget.classList.remove("d-none")
} else {
this.detailsExpanded = true
this.summaryTarget.classList.add("d-none")
}

Expand Down
57 changes: 55 additions & 2 deletions app/javascript/controllers/onboarding_tour_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export default class extends Controller {

connect() {
this.intro = null
this.persistableStepKeys = []
this.onWelcomeStarted = this.onWelcomeStarted.bind(this)
window.addEventListener("onboarding-welcome:started", this.onWelcomeStarted)

if (!this.autoStartValue) return

Expand All @@ -18,18 +21,27 @@ export default class extends Controller {
}

disconnect() {
window.removeEventListener("onboarding-welcome:started", this.onWelcomeStarted)

if (!this.intro) return

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

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

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

await this.resumeIfRequested()

const progress = await this.fetchProgress()
this.persistableStepKeys = Array.isArray(progress?.step_keys) ? progress.step_keys : []
const builtSteps = this.buildSteps()
if (builtSteps.length === 0) return

Expand All @@ -49,17 +61,24 @@ export default class extends Controller {
const stepKey = targetElement?.dataset?.onboardingTourStepKey
if (!stepKey) return

this.persistLastSeen(stepKey)
const persistStepKey = targetElement?.dataset?.onboardingTourPersistStepKey || stepKey
if (this.persistableStepKeys.includes(persistStepKey)) {
this.persistLastSeen(persistStepKey)
}
this.enforceSkipButtonLayout()
})

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

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

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

Expand Down Expand Up @@ -98,6 +117,9 @@ export default class extends Controller {
if (!element) return null

element.dataset.onboardingTourStepKey = step.key
if (step.persist_step_key) {
element.dataset.onboardingTourPersistStepKey = step.persist_step_key
}

return {
element,
Expand All @@ -112,7 +134,11 @@ export default class extends Controller {
findResumeIndex(lastSeenStep, steps) {
if (!lastSeenStep) return 0

const idx = steps.findIndex((step) => step.element?.dataset?.onboardingTourStepKey === lastSeenStep)
const idx = steps.findIndex((step) => {
const visibleStepKey = step.element?.dataset?.onboardingTourStepKey
const persistStepKey = step.element?.dataset?.onboardingTourPersistStepKey
return visibleStepKey === lastSeenStep || persistStepKey === lastSeenStep
})
if (idx < 0) return 0

return Math.min(idx + 1, Math.max(steps.length - 1, 0))
Expand Down Expand Up @@ -151,4 +177,31 @@ export default class extends Controller {
url.searchParams.delete("resume_onboarding")
window.history.replaceState({}, "", url.toString())
}

enforceSkipButtonLayout() {
window.requestAnimationFrame(() => {
const tooltip = document.querySelector(".introjs-tooltip")
const skipButton = document.querySelector(".introjs-skipbutton")
const title = document.querySelector(".introjs-tooltip .introjs-tooltip-title")

if (!tooltip || !skipButton) return

tooltip.style.position = "relative"
skipButton.style.position = "absolute"
skipButton.style.top = "10px"
skipButton.style.right = "12px"
skipButton.style.left = "auto"
skipButton.style.width = "auto"
skipButton.style.height = "auto"
skipButton.style.lineHeight = "1.2"
skipButton.style.padding = "4px 9px"
skipButton.style.margin = "0"
skipButton.style.display = "inline-block"
skipButton.style.borderRadius = "999px"
skipButton.style.textDecoration = "none"
skipButton.style.zIndex = "20"

if (title) title.style.paddingRight = "72px"
})
}
}
16 changes: 3 additions & 13 deletions app/javascript/controllers/onboarding_welcome_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["modal", "startButton", "dismissButton"]
static values = {
autoShow: Boolean,
startPath: String
}
static values = { autoShow: Boolean }

connect() {
this.modalInstance = null
Expand Down Expand Up @@ -43,15 +40,8 @@ export default class extends Controller {

await this.sendOperation("resume")
this.hideModal()

this.dispatch("started")

if (this.hasStartPathValue && this.startPathValue) {
const url = new URL(this.startPathValue, window.location.origin)
url.searchParams.set("onboarding_tour", "1")
window.location.href = url.toString()
return
}
// Wait for the modal animation/backdrop teardown, then start the dashboard tour in place.
window.setTimeout(() => this.dispatch("started"), 220)

this.setButtonsDisabled(false)
}
Expand Down
1 change: 1 addition & 0 deletions app/models/user_onboarding_progress.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class UserOnboardingProgress < ApplicationRecord
STEP_KEYS = %w[
created_technician
created_customer
created_budget
created_first_work_order
moved_work_order_status
viewed_reports
Expand Down
4 changes: 2 additions & 2 deletions app/views/app/dashboard/_onboarding_checklist.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
<div class="card-header d-flex align-items-center justify-content-between">
<h6 class="mb-0" data-onboarding-checklist-target="titleLabel">Primeiros passos</h6>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-light text-dark border" data-onboarding-checklist-target="progressLabel">0/5</span>
<span class="badge bg-light text-dark border" data-onboarding-checklist-target="progressLabel">0/6</span>
<button
type="button"
class="btn btn-sm btn-outline-secondary"
data-onboarding-checklist-target="toggleButton"
data-action="onboarding-checklist#toggleDetails"
>
Ocultar
Ver checklist
</button>
</div>
</div>
Expand Down
69 changes: 52 additions & 17 deletions app/views/app/dashboard/_onboarding_tour.html.erb
Original file line number Diff line number Diff line change
@@ -1,34 +1,69 @@
<%
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: "dashboard_menu",
selector: "#onboarding-tour-dashboard-menu",
title: "Dashboard",
description: "Aqui você acompanha os principais números da operação: usuários, orçamentos, ordens e visão geral do dia."
},
{
key: "created_customer",
key: "clients_menu",
persist_step_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."
title: "Clientes",
description: "Nesta tela você consulta os clientes cadastrados e pode incluir, editar ou remover registros quando precisar."
},
{
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: "budgets_menu",
persist_step_key: "created_budget",
selector: "#onboarding-tour-budgets-menu",
title: "Orçamentos",
description: "Aqui você monta propostas, envia para aprovação e acompanha cada orçamento por status."
},
{
key: "moved_work_order_status",
key: "technicians_menu",
persist_step_key: "created_technician",
selector: "#onboarding-tour-technicians-menu",
title: "Técnicos",
description: "Neste menu você gerencia a equipe técnica: cadastro, ajustes de dados e manutenção dos profissionais ativos."
},
{
key: "order_services_menu",
persist_step_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."
title: "Ordens de Serviço",
description: "Área para controlar as OS: criação, agendamento, andamento e atualização de status da execução."
},
{
key: "viewed_reports",
key: "reports_menu",
persist_step_key: "viewed_reports",
selector: "#onboarding-tour-reports-menu",
title: "Acompanhe os relatórios",
description: "Aqui você visualiza indicadores de volume, produtividade e desempenho."
title: "Relatórios",
description: "Aqui você gera análises de desempenho, incluindo indicadores de orçamentos e ordens concluídas."
},
{
key: "financial_menu",
selector: "#onboarding-tour-financial-menu",
title: "Financeiro",
description: "Neste módulo você visualiza valores, totais e indicadores financeiros ligados a orçamentos e ordens."
},
{
key: "calendar_menu",
selector: "#onboarding-tour-calendar-menu",
title: "Calendário",
description: "Visão de agenda geral das ordens para facilitar organização diária, distribuição e planejamento da equipe."
},
{
key: "configurations_menu",
selector: "#onboarding-tour-configurations-menu",
title: "Configurações",
description: "Aqui você ajusta preferências da empresa e também dados do seu perfil para personalizar o sistema."
},
{
key: "support_menu",
selector: "#onboarding-tour-support-menu",
title: "Suporte",
description: "Canal para abrir chamados, consultar a base de conhecimento e resolver dúvidas sobre uso do sistema."
}
]
%>
Expand Down
19 changes: 9 additions & 10 deletions app/views/app/dashboard/_onboarding_welcome_modal.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<div
data-controller="onboarding-welcome"
data-onboarding-welcome-auto-show-value="<%= @show_onboarding_welcome_modal %>"
data-onboarding-welcome-start-path-value="<%= @onboarding_start_path %>"
>
<div
class="modal fade"
Expand All @@ -11,26 +10,26 @@
data-bs-backdrop="static"
data-bs-keyboard="false"
>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Bem-vindo ao CatalystOps</h5>
<div class="modal-dialog modal-dialog-centered onboarding-welcome-modal-dialog">
<div class="modal-content onboarding-welcome-modal-content">
<div class="modal-header onboarding-welcome-modal-header">
<h5 class="modal-title onboarding-welcome-modal-title">Bem-vindo ao CatalystOps</h5>
</div>
<div class="modal-body">
<p class="mb-0">Em 2 minutos você configura o básico e já começa a operar.</p>
<div class="modal-body onboarding-welcome-modal-body">
<p class="mb-0 onboarding-welcome-modal-subtitle">Em 2 minutos você configura o básico e já começa a operar.</p>
</div>
<div class="modal-footer">
<div class="modal-footer onboarding-welcome-modal-footer">
<button
type="button"
class="btn btn-outline-secondary"
class="btn btn-outline-secondary onboarding-welcome-modal-skip"
data-action="onboarding-welcome#dismiss"
data-onboarding-welcome-target="dismissButton"
>
Pular
</button>
<button
type="button"
class="btn btn-primary"
class="btn btn-primary onboarding-welcome-modal-start"
data-action="onboarding-welcome#start"
data-onboarding-welcome-target="startButton"
>
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
<link href="<%= asset_path('css/bootstrap.min.css') %>" rel="stylesheet">
<link href="<%= asset_path('css/bootstrap-extended.css') %>" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/intro.js/minified/introjs.min.css" %>
<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 Down
Loading
Loading