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
3 changes: 2 additions & 1 deletion app/assets/config/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@
//= link app/order_service_received_items.js
//= link app/knowledge_base_modal.js
//= link cliente/budget_approvals.js
//= link controllers/onboarding_welcome_controller.js
//= link controllers/onboarding_welcome_controller.js
//= link controllers/onboarding_checklist_controller.js
12 changes: 12 additions & 0 deletions app/controllers/app/dashboard_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ 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

def onboarding_welcome_eligible?
Expand All @@ -177,4 +179,14 @@ def next_onboarding_step_path
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: "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 }
]
end
end
166 changes: 166 additions & 0 deletions app/javascript/controllers/onboarding_checklist_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = [
"progressBar",
"progressLabel",
"titleLabel",
"stepsList",
"summary",
"details",
"toggleButton",
"errorBox"
]

static values = {
endpoint: String,
steps: Array,
refreshMs: { type: Number, default: 20000 }
}

connect() {
this.detailsExpanded = false
this.refresh()
this.startAutoRefresh()
this.boundVisibilityHandler = this.handleVisibilityChange.bind(this)
document.addEventListener("visibilitychange", this.boundVisibilityHandler)
}

disconnect() {
this.stopAutoRefresh()
if (this.boundVisibilityHandler) {
document.removeEventListener("visibilitychange", this.boundVisibilityHandler)
}
}

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

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

if (!response.ok) throw new Error(`HTTP ${response.status}`)

const payload = await response.json()
this.renderPayload(payload.onboarding_progress || {})
this.hideError()
} catch (error) {
this.showError("Não foi possível carregar o checklist de onboarding agora.")
console.warn("[OnboardingChecklist] refresh error", error)
}
}

toggleDetails() {
this.detailsExpanded = !this.detailsExpanded
this.applyDetailsVisibility()
}

handleVisibilityChange() {
if (!document.hidden) {
this.refresh()
}
}

startAutoRefresh() {
this.stopAutoRefresh()
this.refreshInterval = window.setInterval(() => this.refresh(), this.refreshMsValue)
}

stopAutoRefresh() {
if (!this.refreshInterval) return
window.clearInterval(this.refreshInterval)
this.refreshInterval = null
}

renderPayload(data) {
const completedSteps = data.completed_steps || {}
const completedCount = Number(data.completed_steps_count || 0)
const stepsTotal = Number(data.steps_total || this.stepsValue.length || 0)
const progress = Number(data.progress_percentage || 0)
const finished = data.finished === true

if (this.hasProgressBarTarget) {
this.progressBarTarget.style.width = `${progress}%`
this.progressBarTarget.setAttribute("aria-valuenow", String(progress))
}

if (this.hasProgressLabelTarget) {
this.progressLabelTarget.textContent = `${completedCount}/${stepsTotal}`
}

if (this.hasTitleLabelTarget) {
this.titleLabelTarget.textContent = finished ? "Onboarding concluído" : "Primeiros passos"
}

this.renderSteps(completedSteps)

if (finished) {
this.detailsExpanded = false
this.summaryTarget.classList.remove("d-none")
} else {
this.detailsExpanded = true
this.summaryTarget.classList.add("d-none")
}

this.applyDetailsVisibility()
}

renderSteps(completedSteps) {
if (!this.hasStepsListTarget) return

const html = this.stepsValue.map((step) => {
const done = completedSteps[step.key] === true
const statusBadge = done
? '<span class="badge bg-success">Concluído</span>'
: '<span class="badge bg-light text-dark border">Pendente</span>'

const action = done
? ""
: `<a class=\"btn btn-sm btn-outline-primary\" href=\"${step.path}\">Ir agora</a>`

return `
<li class="list-group-item d-flex align-items-center justify-content-between gap-3">
<div class="d-flex align-items-center gap-2">
<i class="bx ${done ? "bx-check-circle text-success" : "bx-circle text-secondary"}"></i>
<span>${step.label}</span>
</div>
<div class="d-flex align-items-center gap-2">
${statusBadge}
${action}
</div>
</li>
`
}).join("")

this.stepsListTarget.innerHTML = html
}

applyDetailsVisibility() {
if (!this.hasDetailsTarget) return

if (this.detailsExpanded) {
this.detailsTarget.classList.remove("d-none")
if (this.hasToggleButtonTarget) this.toggleButtonTarget.textContent = "Ocultar"
} else {
this.detailsTarget.classList.add("d-none")
if (this.hasToggleButtonTarget) this.toggleButtonTarget.textContent = "Ver checklist"
}
}

showError(message) {
if (!this.hasErrorBoxTarget) return

this.errorBoxTarget.textContent = message
this.errorBoxTarget.classList.remove("d-none")
}

hideError() {
if (!this.hasErrorBoxTarget) return

this.errorBoxTarget.classList.add("d-none")
}
}
47 changes: 47 additions & 0 deletions app/views/app/dashboard/_onboarding_checklist.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div class="row mb-3">
<div class="col-12">
<div
class="card radius-10 border-start border-0 border-4 border-primary"
data-controller="onboarding-checklist"
data-onboarding-checklist-endpoint-value="<%= app_onboarding_progress_path %>"
data-onboarding-checklist-steps-value="<%= json_escape(@onboarding_checklist_steps.to_json) %>"
data-onboarding-checklist-refresh-ms-value="20000"
>
<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>
<button
type="button"
class="btn btn-sm btn-outline-secondary"
data-onboarding-checklist-target="toggleButton"
data-action="onboarding-checklist#toggleDetails"
>
Ocultar
</button>
</div>
</div>

<div class="card-body" data-onboarding-checklist-target="details">
<div class="progress mb-3" role="progressbar" aria-label="Progresso onboarding" aria-valuemin="0" aria-valuemax="100">
<div
class="progress-bar bg-primary"
data-onboarding-checklist-target="progressBar"
style="width: 0%"
aria-valuenow="0"
></div>
</div>

<ul class="list-group list-group-flush" data-onboarding-checklist-target="stepsList"></ul>
</div>

<div class="card-body pt-0 d-none" data-onboarding-checklist-target="summary">
<div class="alert alert-success mb-0" role="alert">
Tudo pronto. Seu onboarding inicial está concluído.
</div>
</div>

<div class="card-footer d-none" data-onboarding-checklist-target="errorBox"></div>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions app/views/app/dashboard/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="page-wrapper">
<div class="page-content">
<%= render "onboarding_welcome_modal" %>
<%= render "onboarding_checklist" if @show_onboarding_checklist %>

<% flash.each do |type, message| %>
<% bootstrap_class = { notice: "alert-success", alert: "alert-danger", error: "alert-danger" }[type.to_sym] || "alert-info" %>
Expand Down
Loading