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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +32 -0
  3. data/.simplecov +53 -0
  4. data/Appraisals +18 -0
  5. data/CHANGELOG.md +135 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +469 -0
  8. data/Rakefile +55 -0
  9. data/app/assets/stylesheets/support_desk.css +242 -0
  10. data/app/controllers/support_desk/application_controller.rb +92 -0
  11. data/app/controllers/support_desk/console/application_controller.rb +18 -0
  12. data/app/controllers/support_desk/console/tickets_controller.rb +26 -0
  13. data/app/controllers/support_desk/tickets_controller.rb +119 -0
  14. data/app/helpers/support_desk/engine_helper.rb +196 -0
  15. data/app/views/chats/slots/_inbox_top.html.erb +31 -0
  16. data/app/views/chats/slots/_locked_composer.html.erb +18 -0
  17. data/app/views/support_desk/console/tickets/_actions.html.erb +29 -0
  18. data/app/views/support_desk/console/tickets/_assignment.html.erb +60 -0
  19. data/app/views/support_desk/console/tickets/_composer.html.erb +71 -0
  20. data/app/views/support_desk/console/tickets/_context_card.html.erb +53 -0
  21. data/app/views/support_desk/console/tickets/_message.html.erb +48 -0
  22. data/app/views/support_desk/console/tickets/_nav_badge.html.erb +19 -0
  23. data/app/views/support_desk/console/tickets/_tabs.html.erb +15 -0
  24. data/app/views/support_desk/console/tickets/_ticket_row.html.erb +56 -0
  25. data/app/views/support_desk/console/tickets/_timeline.html.erb +37 -0
  26. data/app/views/support_desk/console/tickets/_transcript.html.erb +26 -0
  27. data/app/views/support_desk/console/tickets/index.html.erb +37 -0
  28. data/app/views/support_desk/console/tickets/show.html.erb +45 -0
  29. data/app/views/support_desk/tickets/_context_card.html.erb +14 -0
  30. data/app/views/support_desk/tickets/_door.html.erb +12 -0
  31. data/app/views/support_desk/tickets/_pick_thing.html.erb +55 -0
  32. data/app/views/support_desk/tickets/_pick_topic.html.erb +30 -0
  33. data/app/views/support_desk/tickets/_ticket_row.html.erb +31 -0
  34. data/app/views/support_desk/tickets/_wizard_header.html.erb +23 -0
  35. data/app/views/support_desk/tickets/_write.html.erb +56 -0
  36. data/app/views/support_desk/tickets/index.html.erb +53 -0
  37. data/app/views/support_desk/tickets/new.html.erb +14 -0
  38. data/app/views/support_desk/tickets/rate_limited.html.erb +40 -0
  39. data/config/console_routes.rb +16 -0
  40. data/config/locales/support_desk.console.en.yml +93 -0
  41. data/config/locales/support_desk.console.es.yml +93 -0
  42. data/config/locales/support_desk.en.yml +79 -0
  43. data/config/locales/support_desk.es.yml +84 -0
  44. data/config/routes.rb +24 -0
  45. data/context7.json +4 -0
  46. data/gemfiles/rails_7.2.gemfile +34 -0
  47. data/gemfiles/rails_8.0.gemfile +34 -0
  48. data/gemfiles/rails_8.1.gemfile +34 -0
  49. data/lib/generators/support_desk/console_generator.rb +94 -0
  50. data/lib/generators/support_desk/install_generator.rb +86 -0
  51. data/lib/generators/support_desk/templates/console/controller.rb.erb +43 -0
  52. data/lib/generators/support_desk/templates/console/resource.rb.erb +44 -0
  53. data/lib/generators/support_desk/templates/create_support_desk_tables.rb.erb +224 -0
  54. data/lib/generators/support_desk/templates/initializer.rb +186 -0
  55. data/lib/generators/support_desk/views_generator.rb +50 -0
  56. data/lib/support_desk/configuration.rb +675 -0
  57. data/lib/support_desk/console.rb +487 -0
  58. data/lib/support_desk/console_engine.rb +63 -0
  59. data/lib/support_desk/console_routes.rb +107 -0
  60. data/lib/support_desk/context_card.rb +90 -0
  61. data/lib/support_desk/current.rb +26 -0
  62. data/lib/support_desk/doctor.rb +220 -0
  63. data/lib/support_desk/engine.rb +141 -0
  64. data/lib/support_desk/errors.rb +49 -0
  65. data/lib/support_desk/events.rb +122 -0
  66. data/lib/support_desk/macros.rb +73 -0
  67. data/lib/support_desk/models/application_record.rb +11 -0
  68. data/lib/support_desk/models/assignment.rb +83 -0
  69. data/lib/support_desk/models/concerns/agent.rb +79 -0
  70. data/lib/support_desk/models/concerns/requester.rb +71 -0
  71. data/lib/support_desk/models/concerns/supportable.rb +88 -0
  72. data/lib/support_desk/models/desk.rb +101 -0
  73. data/lib/support_desk/models/event.rb +72 -0
  74. data/lib/support_desk/models/ticket.rb +1124 -0
  75. data/lib/support_desk/queue.rb +171 -0
  76. data/lib/support_desk/summary.rb +70 -0
  77. data/lib/support_desk/test_helpers.rb +137 -0
  78. data/lib/support_desk/timeline.rb +104 -0
  79. data/lib/support_desk/topic.rb +290 -0
  80. data/lib/support_desk/topic_tree.rb +214 -0
  81. data/lib/support_desk/version.rb +5 -0
  82. data/lib/support_desk/wizard.rb +392 -0
  83. data/lib/support_desk.rb +288 -0
  84. 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,4 @@
1
+ {
2
+ "url": "https://context7.com/rameerez/support_desk",
3
+ "public_key": "pk_HibNJE5rTFvy1txHHXUot"
4
+ }
@@ -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