support_desk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +32 -0
- data/.simplecov +53 -0
- data/Appraisals +18 -0
- data/CHANGELOG.md +135 -0
- data/LICENSE.txt +21 -0
- data/README.md +469 -0
- data/Rakefile +55 -0
- data/app/assets/stylesheets/support_desk.css +242 -0
- data/app/controllers/support_desk/application_controller.rb +92 -0
- data/app/controllers/support_desk/console/application_controller.rb +18 -0
- data/app/controllers/support_desk/console/tickets_controller.rb +26 -0
- data/app/controllers/support_desk/tickets_controller.rb +119 -0
- data/app/helpers/support_desk/engine_helper.rb +196 -0
- data/app/views/chats/slots/_inbox_top.html.erb +31 -0
- data/app/views/chats/slots/_locked_composer.html.erb +18 -0
- data/app/views/support_desk/console/tickets/_actions.html.erb +29 -0
- data/app/views/support_desk/console/tickets/_assignment.html.erb +60 -0
- data/app/views/support_desk/console/tickets/_composer.html.erb +71 -0
- data/app/views/support_desk/console/tickets/_context_card.html.erb +53 -0
- data/app/views/support_desk/console/tickets/_message.html.erb +48 -0
- data/app/views/support_desk/console/tickets/_nav_badge.html.erb +19 -0
- data/app/views/support_desk/console/tickets/_tabs.html.erb +15 -0
- data/app/views/support_desk/console/tickets/_ticket_row.html.erb +56 -0
- data/app/views/support_desk/console/tickets/_timeline.html.erb +37 -0
- data/app/views/support_desk/console/tickets/_transcript.html.erb +26 -0
- data/app/views/support_desk/console/tickets/index.html.erb +37 -0
- data/app/views/support_desk/console/tickets/show.html.erb +45 -0
- data/app/views/support_desk/tickets/_context_card.html.erb +14 -0
- data/app/views/support_desk/tickets/_door.html.erb +12 -0
- data/app/views/support_desk/tickets/_pick_thing.html.erb +55 -0
- data/app/views/support_desk/tickets/_pick_topic.html.erb +30 -0
- data/app/views/support_desk/tickets/_ticket_row.html.erb +31 -0
- data/app/views/support_desk/tickets/_wizard_header.html.erb +23 -0
- data/app/views/support_desk/tickets/_write.html.erb +56 -0
- data/app/views/support_desk/tickets/index.html.erb +53 -0
- data/app/views/support_desk/tickets/new.html.erb +14 -0
- data/app/views/support_desk/tickets/rate_limited.html.erb +40 -0
- data/config/console_routes.rb +16 -0
- data/config/locales/support_desk.console.en.yml +93 -0
- data/config/locales/support_desk.console.es.yml +93 -0
- data/config/locales/support_desk.en.yml +79 -0
- data/config/locales/support_desk.es.yml +84 -0
- data/config/routes.rb +24 -0
- data/context7.json +4 -0
- data/gemfiles/rails_7.2.gemfile +34 -0
- data/gemfiles/rails_8.0.gemfile +34 -0
- data/gemfiles/rails_8.1.gemfile +34 -0
- data/lib/generators/support_desk/console_generator.rb +94 -0
- data/lib/generators/support_desk/install_generator.rb +86 -0
- data/lib/generators/support_desk/templates/console/controller.rb.erb +43 -0
- data/lib/generators/support_desk/templates/console/resource.rb.erb +44 -0
- data/lib/generators/support_desk/templates/create_support_desk_tables.rb.erb +224 -0
- data/lib/generators/support_desk/templates/initializer.rb +186 -0
- data/lib/generators/support_desk/views_generator.rb +50 -0
- data/lib/support_desk/configuration.rb +675 -0
- data/lib/support_desk/console.rb +487 -0
- data/lib/support_desk/console_engine.rb +63 -0
- data/lib/support_desk/console_routes.rb +107 -0
- data/lib/support_desk/context_card.rb +90 -0
- data/lib/support_desk/current.rb +26 -0
- data/lib/support_desk/doctor.rb +220 -0
- data/lib/support_desk/engine.rb +141 -0
- data/lib/support_desk/errors.rb +49 -0
- data/lib/support_desk/events.rb +122 -0
- data/lib/support_desk/macros.rb +73 -0
- data/lib/support_desk/models/application_record.rb +11 -0
- data/lib/support_desk/models/assignment.rb +83 -0
- data/lib/support_desk/models/concerns/agent.rb +79 -0
- data/lib/support_desk/models/concerns/requester.rb +71 -0
- data/lib/support_desk/models/concerns/supportable.rb +88 -0
- data/lib/support_desk/models/desk.rb +101 -0
- data/lib/support_desk/models/event.rb +72 -0
- data/lib/support_desk/models/ticket.rb +1124 -0
- data/lib/support_desk/queue.rb +171 -0
- data/lib/support_desk/summary.rb +70 -0
- data/lib/support_desk/test_helpers.rb +137 -0
- data/lib/support_desk/timeline.rb +104 -0
- data/lib/support_desk/topic.rb +290 -0
- data/lib/support_desk/topic_tree.rb +214 -0
- data/lib/support_desk/version.rb +5 -0
- data/lib/support_desk/wizard.rb +392 -0
- data/lib/support_desk.rb +288 -0
- metadata +229 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
en:
|
|
2
|
+
support_desk:
|
|
3
|
+
system:
|
|
4
|
+
assigned: "%{agent} is taking care of your request"
|
|
5
|
+
reassigned: "%{agent} is now taking care of your request"
|
|
6
|
+
thread:
|
|
7
|
+
closed_notice: "This conversation is closed. Start a new one if you need anything else."
|
|
8
|
+
reopened_notice: "We reopened this conversation."
|
|
9
|
+
promise: "We usually reply in under %{time}"
|
|
10
|
+
notifications:
|
|
11
|
+
title: "%{desk} ยท new message"
|
|
12
|
+
queue:
|
|
13
|
+
tabs:
|
|
14
|
+
awaiting: "Needs a reply"
|
|
15
|
+
mine: "Mine"
|
|
16
|
+
unassigned: "Unassigned"
|
|
17
|
+
open: "Open"
|
|
18
|
+
snoozed: "Snoozed"
|
|
19
|
+
closed: "Closed"
|
|
20
|
+
summary:
|
|
21
|
+
awaiting_reply: "awaiting reply"
|
|
22
|
+
awaiting_requester: "awaiting requester"
|
|
23
|
+
open: "open"
|
|
24
|
+
closed: "closed"
|
|
25
|
+
status:
|
|
26
|
+
open: "Open"
|
|
27
|
+
snoozed: "Snoozed"
|
|
28
|
+
closed: "Closed"
|
|
29
|
+
channels:
|
|
30
|
+
in_app: "in app"
|
|
31
|
+
email: "email"
|
|
32
|
+
intercom: "Intercom"
|
|
33
|
+
api: "API"
|
|
34
|
+
tickets:
|
|
35
|
+
title: "Support"
|
|
36
|
+
empty_title: "You haven't written to us yet"
|
|
37
|
+
empty_hint: "Conversations you open will show up here."
|
|
38
|
+
closed_section:
|
|
39
|
+
one: "1 closed conversation"
|
|
40
|
+
other: "%{count} closed conversations"
|
|
41
|
+
state:
|
|
42
|
+
awaiting_reply: "We're on it"
|
|
43
|
+
answered: "We replied"
|
|
44
|
+
closed: "Closed"
|
|
45
|
+
doors:
|
|
46
|
+
new: "Need help?"
|
|
47
|
+
about: "Need help with %{subject}?"
|
|
48
|
+
existing: "See your support conversation"
|
|
49
|
+
new_conversation: "Start a new conversation"
|
|
50
|
+
inbox:
|
|
51
|
+
empty_hint: "Need help? Write to us"
|
|
52
|
+
limits:
|
|
53
|
+
title: "You already have conversations open"
|
|
54
|
+
too_many_open_title: "You already have conversations open"
|
|
55
|
+
too_fast_title: "That was quick"
|
|
56
|
+
your_message: "What you wrote, so you can copy it:"
|
|
57
|
+
too_many_open:
|
|
58
|
+
one: "You already have a conversation open. Write there and we'll keep going in it."
|
|
59
|
+
other: "You already have %{count} conversations open. Write in one of them and we'll keep going there."
|
|
60
|
+
too_fast: "You've opened several conversations in a very short time. Give it a moment, or write in one you already have."
|
|
61
|
+
wizard:
|
|
62
|
+
ask_topic: "What do you need help with?"
|
|
63
|
+
ask_subject: "Which one?"
|
|
64
|
+
write: "Tell us what happened"
|
|
65
|
+
placeholder: "Tell us what happened"
|
|
66
|
+
none_of_these: "None of these"
|
|
67
|
+
already_open: "You already have a conversation open about this"
|
|
68
|
+
back: "Back"
|
|
69
|
+
back_to_list: "See your conversations"
|
|
70
|
+
send: "Send"
|
|
71
|
+
sending: "Sending"
|
|
72
|
+
write_anyway: "Write to us anyway"
|
|
73
|
+
message_required: "Write us a few lines so we can help."
|
|
74
|
+
nothing_to_pick: "We couldn't find anything to open this conversation about."
|
|
75
|
+
no_topics: "New conversations can't be opened right now."
|
|
76
|
+
topics:
|
|
77
|
+
other:
|
|
78
|
+
label: "Something else"
|
|
79
|
+
ask: "What is it about?"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
es:
|
|
2
|
+
support_desk:
|
|
3
|
+
# The system message the requester sees when a human picks up their case.
|
|
4
|
+
system:
|
|
5
|
+
assigned: "%{agent} se ocupa de tu consulta"
|
|
6
|
+
reassigned: "%{agent} se ocupa ahora de tu consulta"
|
|
7
|
+
thread:
|
|
8
|
+
closed_notice: "Esta conversaciรณn estรก cerrada. Abre una nueva si necesitas algo mรกs."
|
|
9
|
+
reopened_notice: "Hemos vuelto a abrir esta conversaciรณn."
|
|
10
|
+
promise: "Normalmente respondemos en menos de %{time}"
|
|
11
|
+
notifications:
|
|
12
|
+
# Generic on purpose: una pantalla de bloqueo no debe contar de quรฉ va
|
|
13
|
+
# el caso de nadie. El detalle va en el cuerpo.
|
|
14
|
+
title: "%{desk} ยท nuevo mensaje"
|
|
15
|
+
queue:
|
|
16
|
+
tabs:
|
|
17
|
+
awaiting: "Pendientes"
|
|
18
|
+
mine: "Mi cola"
|
|
19
|
+
unassigned: "Sin asignar"
|
|
20
|
+
open: "Abiertos"
|
|
21
|
+
snoozed: "Pospuestos"
|
|
22
|
+
closed: "Cerrados"
|
|
23
|
+
summary:
|
|
24
|
+
awaiting_reply: "esperando respuesta"
|
|
25
|
+
awaiting_requester: "esperando al cliente"
|
|
26
|
+
open: "abierto"
|
|
27
|
+
closed: "cerrado"
|
|
28
|
+
status:
|
|
29
|
+
open: "Abierto"
|
|
30
|
+
snoozed: "Pospuesto"
|
|
31
|
+
closed: "Cerrado"
|
|
32
|
+
channels:
|
|
33
|
+
in_app: "en la app"
|
|
34
|
+
email: "correo"
|
|
35
|
+
intercom: "Intercom"
|
|
36
|
+
api: "API"
|
|
37
|
+
# The requester's own list of cases.
|
|
38
|
+
tickets:
|
|
39
|
+
title: "Soporte"
|
|
40
|
+
empty_title: "Todavรญa no has escrito a soporte"
|
|
41
|
+
empty_hint: "Cuando abras una conversaciรณn la verรกs aquรญ."
|
|
42
|
+
closed_section:
|
|
43
|
+
one: "1 conversaciรณn cerrada"
|
|
44
|
+
other: "%{count} conversaciones cerradas"
|
|
45
|
+
state:
|
|
46
|
+
awaiting_reply: "Estamos con ello"
|
|
47
|
+
answered: "Te hemos contestado"
|
|
48
|
+
closed: "Cerrada"
|
|
49
|
+
# The ways in, from anywhere in the host app.
|
|
50
|
+
doors:
|
|
51
|
+
new: "ยฟNecesitas ayuda?"
|
|
52
|
+
about: "ยฟNecesitas ayuda con %{subject}?"
|
|
53
|
+
existing: "Ver tu conversaciรณn de soporte"
|
|
54
|
+
new_conversation: "Abrir una nueva conversaciรณn"
|
|
55
|
+
inbox:
|
|
56
|
+
empty_hint: "ยฟNecesitas ayuda? Escrรญbenos"
|
|
57
|
+
limits:
|
|
58
|
+
title: "Ya tienes conversaciones abiertas"
|
|
59
|
+
too_many_open_title: "Ya tienes conversaciones abiertas"
|
|
60
|
+
too_fast_title: "Vas muy rรกpido"
|
|
61
|
+
your_message: "Lo que has escrito, para que puedas copiarlo:"
|
|
62
|
+
too_many_open:
|
|
63
|
+
one: "Ya tienes una conversaciรณn abierta. Escrรญbenos ahรญ y seguimos en la misma."
|
|
64
|
+
other: "Ya tienes %{count} conversaciones abiertas. Escrรญbenos en una de ellas y seguimos ahรญ."
|
|
65
|
+
too_fast: "Has abierto varias conversaciones en muy poco tiempo. Espera un momento o escrรญbenos en una de las que ya tienes."
|
|
66
|
+
wizard:
|
|
67
|
+
ask_topic: "ยฟCon quรฉ necesitas ayuda?"
|
|
68
|
+
ask_subject: "ยฟSobre cuรกl?"
|
|
69
|
+
write: "Cuรฉntanos quรฉ ha pasado"
|
|
70
|
+
placeholder: "Cuรฉntanos quรฉ ha pasado"
|
|
71
|
+
none_of_these: "Ninguno de estos"
|
|
72
|
+
already_open: "Ya tienes una conversaciรณn abierta"
|
|
73
|
+
back: "Atrรกs"
|
|
74
|
+
back_to_list: "Ver tus conversaciones"
|
|
75
|
+
send: "Enviar"
|
|
76
|
+
sending: "Enviando"
|
|
77
|
+
write_anyway: "Escrรญbenos de todos modos"
|
|
78
|
+
message_required: "Escrรญbenos unas lรญneas para que podamos ayudarte."
|
|
79
|
+
nothing_to_pick: "No encontramos nada sobre lo que abrir esta conversaciรณn."
|
|
80
|
+
no_topics: "Ahora mismo no se pueden abrir conversaciones nuevas."
|
|
81
|
+
topics:
|
|
82
|
+
other:
|
|
83
|
+
label: "Otra cosa"
|
|
84
|
+
ask: "ยฟDe quรฉ se trata?"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The requester-facing engine:
|
|
4
|
+
#
|
|
5
|
+
# mount SupportDesk::Engine => "/support"
|
|
6
|
+
#
|
|
7
|
+
# Everything a person who needs help touches lives here โ the wizard, their
|
|
8
|
+
# list of cases, and the doors into them. The agent console is BYOUI: query
|
|
9
|
+
# objects and presenters (Layer 1), a controller concern and a routing
|
|
10
|
+
# concern (Layer 2), or the generated console (Layer 3). See the README.
|
|
11
|
+
SupportDesk::Engine.routes.draw do
|
|
12
|
+
# The wizard is ONE url with three frames, so it gets the short spelling:
|
|
13
|
+
# /support/new, not /support/tickets/new. Every step is a real URL
|
|
14
|
+
# (`/support/new?topic=payments/withdrawal`), which is what makes the back
|
|
15
|
+
# gesture, a bookmark and a cold-boot deep link all work.
|
|
16
|
+
get "new", to: "tickets#new", as: :new_ticket
|
|
17
|
+
|
|
18
|
+
# `show` redirects into the chats conversation โ a stable URL for a case,
|
|
19
|
+
# for emails and notifications, that never has to know where chats is
|
|
20
|
+
# mounted.
|
|
21
|
+
resources :tickets, only: %i[create show]
|
|
22
|
+
|
|
23
|
+
root to: "tickets#index"
|
|
24
|
+
end
|
data/context7.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 7.2.0"
|
|
7
|
+
|
|
8
|
+
group :development do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "brakeman", "~> 8.0", require: false
|
|
11
|
+
gem "rubocop-rails-omakase", require: false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
group :test do
|
|
15
|
+
gem "minitest", "~> 6.0"
|
|
16
|
+
gem "minitest-mock"
|
|
17
|
+
gem "simplecov", require: false
|
|
18
|
+
gem "actioncable"
|
|
19
|
+
gem "actionmailer"
|
|
20
|
+
gem "activejob"
|
|
21
|
+
gem "activestorage"
|
|
22
|
+
gem "pg"
|
|
23
|
+
gem "sqlite3"
|
|
24
|
+
gem "capybara"
|
|
25
|
+
gem "selenium-webdriver"
|
|
26
|
+
gem "importmap-rails"
|
|
27
|
+
gem "propshaft"
|
|
28
|
+
gem "puma"
|
|
29
|
+
gem "stimulus-rails"
|
|
30
|
+
gem "rdoc", ">= 7.0"
|
|
31
|
+
gem "json", "~> 2.7"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.0.0"
|
|
7
|
+
|
|
8
|
+
group :development do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "brakeman", "~> 8.0", require: false
|
|
11
|
+
gem "rubocop-rails-omakase", require: false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
group :test do
|
|
15
|
+
gem "minitest", "~> 6.0"
|
|
16
|
+
gem "minitest-mock"
|
|
17
|
+
gem "simplecov", require: false
|
|
18
|
+
gem "actioncable"
|
|
19
|
+
gem "actionmailer"
|
|
20
|
+
gem "activejob"
|
|
21
|
+
gem "activestorage"
|
|
22
|
+
gem "pg"
|
|
23
|
+
gem "sqlite3"
|
|
24
|
+
gem "capybara"
|
|
25
|
+
gem "selenium-webdriver"
|
|
26
|
+
gem "importmap-rails"
|
|
27
|
+
gem "propshaft"
|
|
28
|
+
gem "puma"
|
|
29
|
+
gem "stimulus-rails"
|
|
30
|
+
gem "rdoc", ">= 7.0"
|
|
31
|
+
gem "json", "~> 2.7"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.1.0"
|
|
7
|
+
|
|
8
|
+
group :development do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "brakeman", "~> 8.0", require: false
|
|
11
|
+
gem "rubocop-rails-omakase", require: false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
group :test do
|
|
15
|
+
gem "minitest", "~> 6.0"
|
|
16
|
+
gem "minitest-mock"
|
|
17
|
+
gem "simplecov", require: false
|
|
18
|
+
gem "actioncable"
|
|
19
|
+
gem "actionmailer"
|
|
20
|
+
gem "activejob"
|
|
21
|
+
gem "activestorage"
|
|
22
|
+
gem "pg"
|
|
23
|
+
gem "sqlite3"
|
|
24
|
+
gem "capybara"
|
|
25
|
+
gem "selenium-webdriver"
|
|
26
|
+
gem "importmap-rails"
|
|
27
|
+
gem "propshaft"
|
|
28
|
+
gem "puma"
|
|
29
|
+
gem "stimulus-rails"
|
|
30
|
+
gem "rdoc", ">= 7.0"
|
|
31
|
+
gem "json", "~> 2.7"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module SupportDesk
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate support_desk:console madmin` โ the agent console as
|
|
8
|
+
# files in YOUR app: a controller that includes the two console
|
|
9
|
+
# concerns, a madmin resource so the nav and search know tickets exist,
|
|
10
|
+
# and the whole view set.
|
|
11
|
+
#
|
|
12
|
+
# The views are copied straight out of `SupportDesk::ConsoleEngine` โ
|
|
13
|
+
# the same templates the mounted console renders, not a second set that
|
|
14
|
+
# can drift from it. They reference nothing private: queue tabs come
|
|
15
|
+
# from `queue.tabs`, rows from the ticket's own predicates, buttons from
|
|
16
|
+
# `actions_for`, paths from the console concern's
|
|
17
|
+
# `console_ticket_path` (which reads THIS controller's route, so the
|
|
18
|
+
# files work under any namespace). Edit them freely.
|
|
19
|
+
#
|
|
20
|
+
# Idempotent: existing files are left alone unless you pass `--force`.
|
|
21
|
+
class ConsoleGenerator < Rails::Generators::Base
|
|
22
|
+
# Where the ConsoleEngine keeps the views both consoles render. One
|
|
23
|
+
# source of truth, copied โ never a symlink or a second copy to
|
|
24
|
+
# maintain.
|
|
25
|
+
VIEWS_ROOT = File.expand_path("../../../app/views/support_desk/console/tickets", __dir__)
|
|
26
|
+
|
|
27
|
+
source_root File.expand_path("templates/console", __dir__)
|
|
28
|
+
|
|
29
|
+
desc "Generate a support console (controller, admin resource and views) in your app"
|
|
30
|
+
|
|
31
|
+
argument :target, type: :string, default: "madmin",
|
|
32
|
+
desc: "The admin namespace to generate into (madmin)"
|
|
33
|
+
|
|
34
|
+
def self.source_paths
|
|
35
|
+
[ source_root, VIEWS_ROOT ]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def copy_controller
|
|
39
|
+
template "controller.rb.erb", "app/controllers/#{target}/support_tickets_controller.rb", **file_options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Only madmin has a resource concept; other targets get the controller
|
|
43
|
+
# and the views, and wire their own nav.
|
|
44
|
+
def copy_admin_resource
|
|
45
|
+
return unless madmin?
|
|
46
|
+
|
|
47
|
+
template "resource.rb.erb", "app/madmin/resources/support_ticket_resource.rb", **file_options
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def copy_views
|
|
51
|
+
Dir.children(VIEWS_ROOT).sort.each do |view|
|
|
52
|
+
copy_file view, "#{views_path}/#{view}", **file_options
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def display_post_install_message
|
|
57
|
+
say "\n๐ซ A support console has been generated in app/controllers/#{target}/ and #{views_path}/.", :green
|
|
58
|
+
say "\nTo finish:"
|
|
59
|
+
|
|
60
|
+
say " 1. One route line:"
|
|
61
|
+
say " # config/routes.rb"
|
|
62
|
+
say " namespace :#{target} do"
|
|
63
|
+
say " resources :support_tickets, only: %i[index show], concerns: :support_console"
|
|
64
|
+
say " end"
|
|
65
|
+
|
|
66
|
+
say " 2. A badge in your admin nav:"
|
|
67
|
+
say " <%= render \"#{target}/support_tickets/nav_badge\", agent: current_user %>"
|
|
68
|
+
say " # links to #{target}_support_tickets_path"
|
|
69
|
+
|
|
70
|
+
say " 3. Check who may open it:"
|
|
71
|
+
say " config.current_agent_method = :current_user # who is answering"
|
|
72
|
+
say " config.visible_desks_for = ->(agent) { โฆ } # which desks they may work"
|
|
73
|
+
say " config.authorize_console = ->(agent, ticket, action) { โฆ }"
|
|
74
|
+
|
|
75
|
+
say "\n The views are Tailwind and all copy comes from your locales, so restyle"
|
|
76
|
+
say " and retranslate freely โ nothing in the gem reads them back."
|
|
77
|
+
say "\nYour team has a queue. ๐\n", :green
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# The module the generated controller lives in ("madmin" โ "Madmin").
|
|
83
|
+
def module_name = target.camelize
|
|
84
|
+
|
|
85
|
+
def views_path = "app/views/#{target}/support_tickets"
|
|
86
|
+
|
|
87
|
+
def madmin? = target == "madmin"
|
|
88
|
+
|
|
89
|
+
# Idempotent by default: a second run leaves your edits alone. `--force`
|
|
90
|
+
# (Thor's own flag) is how you take the new defaults after an upgrade.
|
|
91
|
+
def file_options = options[:force] ? {} : { skip: true }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module SupportDesk
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate support_desk:install` โ copies the adaptive migration
|
|
9
|
+
# (uuid or bigint keys, adapter-aware JSON columns, PostgreSQL partial
|
|
10
|
+
# unique indexes) and the annotated initializer, then prints the three
|
|
11
|
+
# model lines and one route line that finish the install.
|
|
12
|
+
class InstallGenerator < Rails::Generators::Base
|
|
13
|
+
include ActiveRecord::Generators::Migration
|
|
14
|
+
|
|
15
|
+
source_root File.expand_path("templates", __dir__)
|
|
16
|
+
desc "Install support_desk's migration and initializer"
|
|
17
|
+
|
|
18
|
+
# Rails' migration numbering, borrowed from ActiveRecord's generators.
|
|
19
|
+
def self.next_migration_number(dir)
|
|
20
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The schema: four tables, adaptive keys, partial indexes where the
|
|
24
|
+
# adapter has them.
|
|
25
|
+
def create_migration_file
|
|
26
|
+
migration_template "create_support_desk_tables.rb.erb",
|
|
27
|
+
File.join(db_migrate_path, "create_support_desk_tables.rb")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The annotated initializer โ every setting the gem has, with what it
|
|
31
|
+
# means and what it defaults to.
|
|
32
|
+
def create_initializer
|
|
33
|
+
template "initializer.rb", "config/initializers/support_desk.rb"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The three model lines and one route line that finish the install.
|
|
37
|
+
def display_post_install_message
|
|
38
|
+
say "\n๐ซ The `support_desk` gem has been installed.", :green
|
|
39
|
+
say "\nTo complete the setup:"
|
|
40
|
+
|
|
41
|
+
say " 1. Run 'rails db:migrate' to create the support_desk tables."
|
|
42
|
+
say " โ ๏ธ You must run migrations before starting your app!", :yellow
|
|
43
|
+
|
|
44
|
+
say " 2. Three model lines โ who asks, what they ask about, who answers:"
|
|
45
|
+
say " class User < ApplicationRecord"
|
|
46
|
+
say " acts_as_messager # chats"
|
|
47
|
+
say " has_support_tickets"
|
|
48
|
+
say " acts_as_support_agent if: :admin?"
|
|
49
|
+
say " end"
|
|
50
|
+
say ""
|
|
51
|
+
say " class Ride < ApplicationRecord"
|
|
52
|
+
say " supportable topic: :ride"
|
|
53
|
+
say " end"
|
|
54
|
+
|
|
55
|
+
say " 3. One route line:"
|
|
56
|
+
say " # config/routes.rb"
|
|
57
|
+
say " mount SupportDesk::Engine => \"/support\""
|
|
58
|
+
|
|
59
|
+
say " 4. Tell the desk who it is, in config/initializers/support_desk.rb:"
|
|
60
|
+
say " config.name = \"Soporte\""
|
|
61
|
+
say " config.agents { User.admin }"
|
|
62
|
+
say " config.topics { topic :ride, about: Ride; other }"
|
|
63
|
+
|
|
64
|
+
say "\n Locales: es and en ship with the gem. To translate your topic labels,"
|
|
65
|
+
say " add support_desk.topics.<path>.label keys to your own locale files."
|
|
66
|
+
|
|
67
|
+
say "\n The requester's screens render with bundled styles. To restyle them,"
|
|
68
|
+
say " 'rails g support_desk:views' copies them into your app, where they"
|
|
69
|
+
say " shadow the gem's copies (delete yours and the default comes back)."
|
|
70
|
+
|
|
71
|
+
say "\n Email (0.2): when the channel lands, route inbound mail with"
|
|
72
|
+
say " # app/mailboxes/application_mailbox.rb"
|
|
73
|
+
say " # routing(/^support@/i => :support_desk)"
|
|
74
|
+
|
|
75
|
+
say "\nCheck your work any time with SupportDesk.doctor.print"
|
|
76
|
+
say "You now have support tickets that are real conversations. ๐\n", :green
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def migration_version
|
|
82
|
+
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module <%= module_name %>
|
|
4
|
+
# The support console, yours to edit.
|
|
5
|
+
#
|
|
6
|
+
# Everything an agent can do arrives through two concerns:
|
|
7
|
+
#
|
|
8
|
+
# SupportDesk::Console the verbs โ reply, take, assign, hand_off,
|
|
9
|
+
# release, close, reopen, note, change_topic,
|
|
10
|
+
# next โ each one finding the ticket inside
|
|
11
|
+
# `config.visible_desks_for`, asking
|
|
12
|
+
# `config.authorize_console`, and turning
|
|
13
|
+
# every refusal into a flash
|
|
14
|
+
# SupportDesk::Console::Index @queue, @scope and @tickets from params
|
|
15
|
+
#
|
|
16
|
+
# The routes come from the `:support_console` routing concern:
|
|
17
|
+
#
|
|
18
|
+
# namespace :<%= target %> do
|
|
19
|
+
# resources :support_tickets, only: %i[index show], concerns: :support_console
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Both `index` and `show` are yours โ the gem deliberately doesn't own the
|
|
23
|
+
# screens. Everything they need is plain Ruby: `SupportDesk::Queue`,
|
|
24
|
+
# `ticket.context_card`, `ticket.timeline`, `ticket.actions_for(agent)`.
|
|
25
|
+
class SupportTicketsController < <%= module_name %>::ApplicationController
|
|
26
|
+
include SupportDesk::Console
|
|
27
|
+
include SupportDesk::Console::Index
|
|
28
|
+
|
|
29
|
+
# One case. `@ticket` is already set (and already scoped to the desks
|
|
30
|
+
# this agent may work) by the concern.
|
|
31
|
+
def show
|
|
32
|
+
@context_card = @ticket.context_card
|
|
33
|
+
@actions = @ticket.actions_for(current_agent)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Who is answering. Point this at whatever your admin already uses;
|
|
39
|
+
# `config.current_agent_method` is the alternative if you'd rather not
|
|
40
|
+
# write the method at all.
|
|
41
|
+
def current_agent = current_user
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Tells madmin that support tickets exist: the nav entry, the search index
|
|
4
|
+
# and the record links other resources make when they point at a ticket.
|
|
5
|
+
#
|
|
6
|
+
# The console itself is NOT this resource's index and show โ those are
|
|
7
|
+
# <%= module_name %>::SupportTicketsController, which is a real support
|
|
8
|
+
# screen rather than a table of columns. The path overrides below are what
|
|
9
|
+
# send madmin's own links there.
|
|
10
|
+
class SupportTicketResource < Madmin::Resource
|
|
11
|
+
model SupportDesk::Ticket
|
|
12
|
+
|
|
13
|
+
attribute :reference, index: true, form: false
|
|
14
|
+
attribute :title, index: true, form: false
|
|
15
|
+
attribute :topic, index: true, form: false
|
|
16
|
+
attribute :status, index: true, form: false
|
|
17
|
+
attribute :awaiting, index: true, form: false
|
|
18
|
+
attribute :requester, :polymorphic, index: true, form: false
|
|
19
|
+
attribute :assignee, :polymorphic, index: true, form: false
|
|
20
|
+
attribute :subject, :polymorphic, index: false, form: false
|
|
21
|
+
attribute :opened_via, index: false, form: false
|
|
22
|
+
attribute :waiting_since, index: true, form: false
|
|
23
|
+
attribute :opened_at, index: false, form: false
|
|
24
|
+
attribute :closed_at, index: false, form: false
|
|
25
|
+
|
|
26
|
+
scope :open
|
|
27
|
+
scope :closed
|
|
28
|
+
scope :unassigned
|
|
29
|
+
scope :awaiting_reply
|
|
30
|
+
|
|
31
|
+
menu label: "Support"
|
|
32
|
+
|
|
33
|
+
# "T-AB12CD ยท Order SO1" reads better than "Ticket #42" everywhere madmin
|
|
34
|
+
# prints a record: breadcrumbs, search results, association links.
|
|
35
|
+
def self.display_name(record) = "#{record.reference} ยท #{record.label}"
|
|
36
|
+
|
|
37
|
+
def self.index_path(options = {}) = url_helpers.<%= target %>_support_tickets_path(options)
|
|
38
|
+
def self.show_path(record) = url_helpers.<%= target %>_support_ticket_path(record)
|
|
39
|
+
def self.new_path = index_path
|
|
40
|
+
def self.edit_path(record) = show_path(record)
|
|
41
|
+
|
|
42
|
+
def self.default_sort_column = "waiting_since"
|
|
43
|
+
def self.default_sort_direction = "asc"
|
|
44
|
+
end
|