support_desk 0.1.3 → 0.3.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +1101 -22
  4. data/Rakefile +16 -0
  5. data/app/assets/stylesheets/support_desk.css +10 -0
  6. data/app/controllers/support_desk/tickets_controller.rb +28 -4
  7. data/app/helpers/support_desk/engine_helper.rb +23 -1
  8. data/app/views/support_desk/console/tickets/_actions.html.erb +15 -0
  9. data/app/views/support_desk/console/tickets/_assignment.html.erb +16 -2
  10. data/app/views/support_desk/console/tickets/_composer.html.erb +27 -0
  11. data/app/views/support_desk/console/tickets/_context_card.html.erb +33 -0
  12. data/app/views/support_desk/console/tickets/_draft.html.erb +114 -0
  13. data/app/views/support_desk/console/tickets/_message.html.erb +34 -1
  14. data/app/views/support_desk/console/tickets/_new_conversation_form.html.erb +91 -0
  15. data/app/views/support_desk/console/tickets/_ticket_row.html.erb +28 -0
  16. data/app/views/support_desk/console/tickets/_timeline.html.erb +68 -14
  17. data/app/views/support_desk/console/tickets/index.html.erb +14 -2
  18. data/app/views/support_desk/console/tickets/new.html.erb +25 -0
  19. data/app/views/support_desk/console/tickets/show.html.erb +5 -0
  20. data/app/views/support_desk/tickets/_human_door.html.erb +24 -0
  21. data/app/views/support_desk/tickets/_ticket_row.html.erb +13 -0
  22. data/config/console_routes.rb +1 -1
  23. data/config/locales/support_desk.console.en.yml +89 -0
  24. data/config/locales/support_desk.console.es.yml +91 -0
  25. data/config/locales/support_desk.en.yml +18 -0
  26. data/config/locales/support_desk.es.yml +29 -0
  27. data/config/routes.rb +7 -1
  28. data/lib/generators/support_desk/assistant_generator.rb +193 -0
  29. data/lib/generators/support_desk/console_generator.rb +1 -1
  30. data/lib/generators/support_desk/install_generator.rb +26 -0
  31. data/lib/generators/support_desk/templates/add_assistants_to_support_desk.rb.erb +236 -0
  32. data/lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb +79 -0
  33. data/lib/generators/support_desk/templates/assistant/service.rb.erb +60 -0
  34. data/lib/generators/support_desk/templates/assistant/turn_job.rb.erb +72 -0
  35. data/lib/generators/support_desk/templates/assistant/turn_job_test.rb.erb +69 -0
  36. data/lib/generators/support_desk/templates/console/controller.rb.erb +1 -1
  37. data/lib/generators/support_desk/templates/initializer.rb +63 -0
  38. data/lib/generators/support_desk/upgrade_generator.rb +66 -0
  39. data/lib/support_desk/assistant_policy.rb +213 -0
  40. data/lib/support_desk/brief.rb +283 -0
  41. data/lib/support_desk/configuration.rb +699 -3
  42. data/lib/support_desk/console.rb +634 -30
  43. data/lib/support_desk/console_engine.rb +2 -0
  44. data/lib/support_desk/console_routes.rb +20 -8
  45. data/lib/support_desk/context_card.rb +33 -1
  46. data/lib/support_desk/doctor.rb +195 -2
  47. data/lib/support_desk/engine.rb +10 -0
  48. data/lib/support_desk/errors.rb +42 -0
  49. data/lib/support_desk/events.rb +12 -5
  50. data/lib/support_desk/macros.rb +41 -12
  51. data/lib/support_desk/models/assignment.rb +3 -1
  52. data/lib/support_desk/models/assistant.rb +153 -0
  53. data/lib/support_desk/models/concerns/agent.rb +39 -4
  54. data/lib/support_desk/models/concerns/requester.rb +45 -12
  55. data/lib/support_desk/models/desk.rb +26 -3
  56. data/lib/support_desk/models/draft.rb +266 -0
  57. data/lib/support_desk/models/event.rb +15 -1
  58. data/lib/support_desk/models/ticket/assistance.rb +675 -0
  59. data/lib/support_desk/models/ticket.rb +781 -120
  60. data/lib/support_desk/outcome.rb +38 -0
  61. data/lib/support_desk/queue.rb +41 -5
  62. data/lib/support_desk/summary.rb +1 -1
  63. data/lib/support_desk/test_helpers.rb +160 -3
  64. data/lib/support_desk/timeline.rb +40 -11
  65. data/lib/support_desk/topic.rb +22 -0
  66. data/lib/support_desk/topic_tree.rb +7 -1
  67. data/lib/support_desk/transcript.rb +237 -0
  68. data/lib/support_desk/version.rb +1 -1
  69. data/lib/support_desk/wizard.rb +4 -12
  70. data/lib/support_desk.rb +153 -0
  71. data/lib/tasks/support_desk.rake +73 -0
  72. metadata +35 -8
@@ -4,7 +4,12 @@
4
4
 
5
5
  Collapsed by default, because an agent opens a case to answer it, not to
6
6
  audit it. `<details>` rather than a toggle controller: the browser has
7
- had this for a decade and it survives a broken bundle. %>
7
+ had this for a decade and it survives a broken bundle.
8
+
9
+ Three kinds of moment now. A message and an event read as they always
10
+ did. A DRAFT reads greyed, because it is the one thing here that was
11
+ never said to anybody: a proposal somebody discarded, replaced, or sent
12
+ in somebody else's words. %>
8
13
  <details class="rounded-lg border border-slate-200 bg-white p-4">
9
14
  <summary class="cursor-pointer text-sm font-semibold"><%= t("support_desk.console.timeline.title") %></summary>
10
15
 
@@ -15,22 +20,71 @@
15
20
  <%= l(entry.at, format: :short) if entry.at %>
16
21
  </time>
17
22
 
18
- <div class="min-w-0">
19
- <span class="font-medium text-slate-700">
20
- <%= t("support_desk.console.timeline.kinds.#{entry.kind}", default: entry.kind.to_s.humanize) %>
21
- </span>
23
+ <% actor = entry.actor %>
24
+ <% actor_name = if actor.nil?
25
+ nil
26
+ elsif actor.is_a?(Symbol)
27
+ t("support_desk.console.timeline.system")
28
+ else
29
+ actor.try(:support_agent_name) || Chats.display_name_for(actor)
30
+ end %>
22
31
 
23
- <% actor = entry.actor %>
24
- <% if actor %>
25
- <span class="text-slate-500">
26
- · <%= actor.is_a?(Symbol) ? t("support_desk.console.timeline.system") : (actor.try(:support_agent_name) || Chats.display_name_for(actor)) %>
32
+ <% if entry.draft? %>
33
+ <%# What the machine proposed, and what became of it. The event
34
+ beside it says who decided; this is what they decided ABOUT. %>
35
+ <% draft = entry.draft %>
36
+ <div class="min-w-0 text-slate-400">
37
+ <span class="font-medium">
38
+ <% case draft.status %>
39
+ <% when "sent" %>
40
+ <%= t("support_desk.console.draft.sent_by", name: actor_name) %>
41
+ <% when "rejected" %>
42
+ <%= t("support_desk.console.draft.rejected_by", name: actor_name) %>
43
+ <% when "superseded" %>
44
+ <%= t("support_desk.console.timeline.draft_superseded") %>
45
+ <% else %>
46
+ <%= t("support_desk.console.timeline.draft_expired") %>
47
+ <% end %>
27
48
  </span>
28
- <% end %>
49
+ <% if entry.body.present? %>
50
+ <p class="truncate"><%= truncate(entry.body, length: 120) %></p>
51
+ <% end %>
52
+ <% if draft.rejection_reason.present? %>
53
+ <p class="truncate italic"><%= truncate(draft.rejection_reason, length: 120) %></p>
54
+ <% end %>
55
+ </div>
56
+
57
+ <% elsif (sentence = support_console_timeline_sentence(entry, actor_name)) %>
58
+ <%# The kinds the assistants added say the whole thing in one
59
+ sentence, with their own payload in it: "who did what to whom"
60
+ is the only reading of these that helps. %>
61
+ <div class="min-w-0">
62
+ <span class="text-slate-700"><%= sentence %></span>
63
+ <% if entry.kind == :escalated && entry.event.summary.present? %>
64
+ <%# Her hand-over note: the one piece of a machine's reasoning
65
+ a person actually needs, so it gets room rather than a
66
+ truncated line. %>
67
+ <p class="mt-1 whitespace-pre-wrap rounded-md bg-amber-50 p-2 text-xs text-amber-900">
68
+ <%= entry.event.summary %>
69
+ </p>
70
+ <% end %>
71
+ </div>
72
+
73
+ <% else %>
74
+ <div class="min-w-0">
75
+ <span class="font-medium text-slate-700">
76
+ <%= t("support_desk.console.timeline.kinds.#{entry.kind}", default: entry.kind.to_s.humanize) %>
77
+ </span>
78
+
79
+ <% if actor_name %>
80
+ <span class="text-slate-500">· <%= actor_name %></span>
81
+ <% end %>
29
82
 
30
- <% if entry.body.present? %>
31
- <p class="truncate text-slate-600"><%= truncate(entry.body, length: 120) %></p>
32
- <% end %>
33
- </div>
83
+ <% if entry.body.present? %>
84
+ <p class="truncate text-slate-600"><%= truncate(entry.body, length: 120) %></p>
85
+ <% end %>
86
+ </div>
87
+ <% end %>
34
88
  </li>
35
89
  <% end %>
36
90
  </ol>
@@ -17,8 +17,20 @@
17
17
  <p class="text-sm text-slate-500"><%= support_desk_record.name %></p>
18
18
  </div>
19
19
 
20
- <%= link_to t("support_desk.console.actions.next"), console_tickets_path(:next),
21
- class: "inline-flex items-center rounded-md bg-slate-900 px-3 py-2 text-sm font-medium text-white hover:bg-slate-700" %>
20
+ <div class="flex items-center gap-2">
21
+ <%# The door to writing first. Through console_tickets_path so it
22
+ resolves under an isolated admin engine as well as a host
23
+ namespace, and carrying the desk so a multi-desk console composes
24
+ for the desk being worked. %>
25
+ <% if support_conversation_offered? %>
26
+ <%= link_to t("support_desk.console.actions.new_conversation"),
27
+ console_tickets_path(:new, desk: support_desk_record.key),
28
+ class: "inline-flex items-center rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-900 hover:bg-slate-50" %>
29
+ <% end %>
30
+
31
+ <%= link_to t("support_desk.console.actions.next"), console_tickets_path(:next),
32
+ class: "inline-flex items-center rounded-md bg-slate-900 px-3 py-2 text-sm font-medium text-white hover:bg-slate-700" %>
33
+ </div>
22
34
  </header>
23
35
 
24
36
  <%= render "tabs", queue: @queue, scope: @scope %>
@@ -0,0 +1,25 @@
1
+ <%# Writing first: the desk starting a conversation with somebody who hasn't
2
+ written to us.
3
+
4
+ It is a whole page rather than a modal on purpose — the 422 that a bad
5
+ email or a refused message comes back as has to render the form again
6
+ with everything still typed into it, and a stream refresh would throw
7
+ that away. %>
8
+ <div class="mx-auto max-w-2xl px-4 py-6 text-slate-900">
9
+ <header class="mb-5">
10
+ <h1 class="text-xl font-semibold"><%= t("support_desk.console.new_conversation.title") %></h1>
11
+ <p class="text-sm text-slate-500"><%= support_desk_record.name %></p>
12
+ </header>
13
+
14
+ <%# The refusal is rendered HERE rather than left to your layout: this
15
+ screen answers 422 with itself, and an agent has to read why on the
16
+ page that came back. If your layout already prints flashes, delete
17
+ this block when you eject the views. %>
18
+ <% if flash[:alert] %>
19
+ <p class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
20
+ <%= flash[:alert] %>
21
+ </p>
22
+ <% end %>
23
+
24
+ <%= render "new_conversation_form" %>
25
+ </div>
@@ -33,6 +33,11 @@
33
33
  <div class="grid gap-6 lg:grid-cols-3">
34
34
  <div class="space-y-4 lg:col-span-2">
35
35
  <%= render "transcript", ticket: @ticket %>
36
+ <%# The proposal, when there is one waiting: above the composer,
37
+ because it is the thing on this screen that wants an answer. %>
38
+ <% if (draft = support_pending_draft) %>
39
+ <%= render "draft", ticket: @ticket, draft: draft, actions: @actions %>
40
+ <% end %>
36
41
  <%= render "composer", ticket: @ticket, actions: @actions %>
37
42
  </div>
38
43
 
@@ -0,0 +1,24 @@
1
+ <%# "Prefiero hablar con una persona."
2
+
3
+ The one control on this screen that is never taken away while a machine
4
+ is in play: a customer who wants a person gets a person, and pressing
5
+ this is the whole of it — no wording to get right, no phrase to guess.
6
+
7
+ Three states, and the middle one is the point. While the assistant is in
8
+ play the button is here. Once somebody has asked (the customer, the
9
+ assistant, or the sweep that notices she went quiet) the button becomes
10
+ the promise, because a door that stays pressable after it worked is a
11
+ door people press again wondering whether it did. A case nobody can
12
+ write to shows nothing at all: it is over, and a button that only 500s
13
+ is worse than silence.
14
+
15
+ `ticket.assistant_in_play?` counts HISTORY as well as configuration, so
16
+ the door does not vanish from a live conversation because somebody
17
+ edited an initializer. %>
18
+ <% if ticket.human_required? %>
19
+ <p class="support-desk-notice__title"><%= t("support_desk.thread.handled_by_humans") %></p>
20
+ <% elsif ticket.assistant_in_play? && !ticket.chat_locked? %>
21
+ <%= button_to t("support_desk.thread.human_door"),
22
+ support_desk_routes.request_human_ticket_path(ticket),
23
+ method: :post, class: "support-desk-button support-desk-button--quiet" %>
24
+ <% end %>
@@ -18,6 +18,19 @@
18
18
 
19
19
  <span class="chats-row__subject"><%= support_ticket_state(ticket) %></span>
20
20
 
21
+ <%# Who is on it, when that is not simply "us". A machine answering is
22
+ something the person is entitled to know before they read the
23
+ answer, so the row says so by her disclosed name — and the moment
24
+ a person has been asked for, it says THAT instead, because it is
25
+ the only one of the two that is a promise. %>
26
+ <% if ticket.human_required? %>
27
+ <span class="chats-row__subject"><%= t("support_desk.thread.handled_by_humans") %></span>
28
+ <% elsif ticket.held_by_assistant? %>
29
+ <span class="chats-row__subject">
30
+ <%= t("support_desk.thread.handled_by_assistant", name: ticket.assignee.support_agent_name) %>
31
+ </span>
32
+ <% end %>
33
+
21
34
  <span class="chats-row__bottom">
22
35
  <span class="chats-row__preview">
23
36
  <%= ticket.conversation ? chats_preview_for(ticket.conversation, current_requester) : "" %>
@@ -10,7 +10,7 @@
10
10
  # SAME `:support_console` concern a host writes in their own routes file —
11
11
  # the turnkey console gets no private API.
12
12
  SupportDesk::ConsoleEngine.routes.draw do
13
- resources :tickets, path: "", only: %i[index show], concerns: :support_console
13
+ resources :tickets, path: "", only: %i[index show new], concerns: :support_console
14
14
 
15
15
  root to: "tickets#index"
16
16
  end
@@ -8,19 +8,36 @@ en:
8
8
  tabs_label: "Queue tabs"
9
9
  empty: "Nothing here right now."
10
10
  row:
11
+ opened_by_support: "Written by support"
11
12
  waiting: "%{duration}"
12
13
  unassigned: "Unassigned"
13
14
  context:
14
15
  title: "Context"
16
+ opened_by: "Opened by"
17
+ not_recorded: "not recorded"
18
+ unavailable: "account unavailable"
15
19
  requester: "Requester"
16
20
  open_tickets: "Open cases"
17
21
  member_since: "Member since"
18
22
  topic: "Topic"
19
23
  subject_status: "Status"
20
24
  open_subject: "Open in admin"
25
+ assistant: "Assistant"
26
+ requester_context: "About this person"
21
27
  transcript:
22
28
  title: "Conversation"
23
29
  empty: "Nothing has been said yet."
30
+ new_conversation:
31
+ title: "Write as support"
32
+ requester: "To"
33
+ requester_query: "Their email"
34
+ requester_hint: "Set `config.find_requester` to look people up from here"
35
+ about: "About %{subject}"
36
+ topic: "Topic"
37
+ message: "Message"
38
+ send: "Send as %{desk}"
39
+ cancel: "Cancel"
40
+ signed_as: "Signed “%{agent}”."
24
41
  composer:
25
42
  reply: "Reply"
26
43
  note: "Internal note"
@@ -29,6 +46,41 @@ en:
29
46
  note_hint: "Notes never leave the desk."
30
47
  closed: "This case is closed. Reopen it to answer."
31
48
  assignee_only: "%{agent} is handling this case, and this desk only lets the assignee reply."
49
+ draft:
50
+ title: "%{name}'s proposal"
51
+ from: "%{name}'s proposal"
52
+ confidence: "declared confidence %{percent} %"
53
+ sources: "Sources"
54
+ attachments: "Attachments"
55
+ stale: "The conversation has changed since this was proposed. Read it before you send."
56
+ editing: "Editing %{name}'s proposal"
57
+ cancel_edit: "cancel"
58
+ reason_placeholder: "Why are you discarding it? (optional)"
59
+ sent_by: "Proposal sent by %{name}"
60
+ rejected_by: "Proposal discarded by %{name}"
61
+ # The verb the "already %{status}" refusal reads.
62
+ statuses:
63
+ pending: "proposed"
64
+ sent: "sent"
65
+ rejected: "discarded"
66
+ superseded: "replaced"
67
+ expired: "expired"
68
+ badges:
69
+ needs_human: "Needs a person"
70
+ pending_draft: "Proposal ready"
71
+ assistant_holds: "%{name} has it · %{used}/%{max} turns"
72
+ assistant_holds_unlimited: "%{name} has it"
73
+ withheld_reasons:
74
+ policy: "this case's policy doesn't allow it"
75
+ not_your_turn: "it wasn't her turn"
76
+ # Why a case ended up with a person. The tokens are written by the gem
77
+ # or by the host's own harness; these are how they read.
78
+ escalation_reasons:
79
+ max_turns: "she ran out of turns"
80
+ assistant_silent: "the assistant didn't answer in time"
81
+ requester_request: "the customer asked for one"
82
+ phrase: "the customer asked in the conversation"
83
+ hand_off_when_error: "the assistant failed"
32
84
  assignment:
33
85
  title: "Who has it"
34
86
  held_by: "Held by %{agent}"
@@ -39,6 +91,16 @@ en:
39
91
  timeline:
40
92
  title: "Timeline"
41
93
  system: "System"
94
+ escalated: "%{actor} handed the case to a person: %{reason}"
95
+ human_requested: "The customer asked for a person"
96
+ assistant_paused: "%{actor} paused the assistant"
97
+ assistant_resumed: "%{actor} resumed the assistant"
98
+ draft_sent: "%{actor} sent %{assistant}'s proposal"
99
+ draft_sent_edited: "%{actor} sent %{assistant}'s proposal (edited)"
100
+ draft_rejected: "%{actor} discarded %{assistant}'s proposal"
101
+ assistant_withheld: "%{assistant} didn't answer: %{reason}"
102
+ draft_superseded: "Proposal replaced"
103
+ draft_expired: "Proposal expired when the case closed"
42
104
  kinds:
43
105
  message: "Message"
44
106
  opened: "Opened"
@@ -53,6 +115,7 @@ en:
53
115
  note: "Internal note"
54
116
  actions:
55
117
  back: "Back to the queue"
118
+ new_conversation: "Write to someone"
56
119
  next: "Next case"
57
120
  reply: "Send"
58
121
  note: "Save note"
@@ -63,7 +126,14 @@ en:
63
126
  close: "Close"
64
127
  reopen: "Reopen"
65
128
  change_topic: "Refile"
129
+ send_draft: "Send"
130
+ send_edited_draft: "Send the edited proposal"
131
+ edit_draft: "Edit"
132
+ reject_draft: "Discard"
133
+ pause_assistant: "Pause the assistant"
134
+ resume_assistant: "Resume the assistant"
66
135
  flashes:
136
+ message_sent: "Message sent to %{requester}."
67
137
  replied: "Answer sent."
68
138
  taken: "This case is yours."
69
139
  assigned: "Assigned to %{agent}."
@@ -74,6 +144,10 @@ en:
74
144
  noted: "Note saved."
75
145
  topic_changed: "Refiled."
76
146
  queue_empty: "Nothing is waiting for you."
147
+ draft_sent: "Proposal sent."
148
+ draft_rejected: "Proposal discarded."
149
+ assistant_paused: "The assistant is paused on this case."
150
+ assistant_resumed: "The assistant is back on this case."
77
151
  errors:
78
152
  forbidden: "You don't have access to the support console."
79
153
  not_allowed: "You can't do that on this case."
@@ -90,4 +164,19 @@ en:
90
164
  take_it_first: "Take this case first — this desk only lets the assignee reply."
91
165
  held_by_somebody_else: "%{holder} is handling this case."
92
166
  unavailable_action: "You can't do that on this case right now."
167
+ unknown_requester: "We can't find that person."
168
+ not_a_requester: "That account can't receive support messages."
169
+ invalid_requester: "That link doesn't point at anybody we can write to."
170
+ invalid_subject: "That link doesn't point at anything we can open a conversation about."
171
+ invalid_input: "Something in the form didn't come through. Try again."
172
+ no_requester_lookup: "Open this form from the person's own page: there's no way to look them up from here."
173
+ writing_to_yourself: "You can't open a support conversation with yourself. Ask for help as a customer instead."
174
+ off_duty: "You're off duty, so you can't write to anybody right now."
175
+ attachments_again: "If you attach files and something goes wrong, you'll have to pick them again."
176
+ unknown_draft: "That proposal is gone."
177
+ draft_already_reviewed: "That proposal was already %{status}."
178
+ no_pending_draft: "There is no proposal waiting on this case."
179
+ stale_turn: "The conversation has changed. Read it and send again."
180
+ assistant_not_allowed: "The assistant can’t do that on this case."
181
+ not_an_assistant: "That action is the assistant’s only."
93
182
  generic: "That didn't work."
@@ -8,19 +8,36 @@ es:
8
8
  tabs_label: "Pestañas de la cola"
9
9
  empty: "Aquí no hay nada ahora mismo."
10
10
  row:
11
+ opened_by_support: "Escrita por soporte"
11
12
  waiting: "%{duration}"
12
13
  unassigned: "Sin asignar"
13
14
  context:
14
15
  title: "Contexto"
16
+ opened_by: "Abierta por"
17
+ not_recorded: "sin registrar"
18
+ unavailable: "cuenta no disponible"
15
19
  requester: "Cliente"
16
20
  open_tickets: "Casos abiertos"
17
21
  member_since: "Cliente desde"
18
22
  topic: "Tema"
19
23
  subject_status: "Estado"
20
24
  open_subject: "Abrir en el admin"
25
+ assistant: "Asistente"
26
+ requester_context: "Sobre la persona"
21
27
  transcript:
22
28
  title: "Conversación"
23
29
  empty: "Todavía no se ha dicho nada."
30
+ new_conversation:
31
+ title: "Escribir por soporte"
32
+ requester: "Para"
33
+ requester_query: "Email de la persona"
34
+ requester_hint: "Configura `config.find_requester` para buscar a alguien desde aquí"
35
+ about: "Sobre %{subject}"
36
+ topic: "Tema"
37
+ message: "Mensaje"
38
+ send: "Enviar como %{desk}"
39
+ cancel: "Cancelar"
40
+ signed_as: "Firmado «%{agent}»."
24
41
  composer:
25
42
  reply: "Responder"
26
43
  note: "Nota interna"
@@ -29,6 +46,43 @@ es:
29
46
  note_hint: "Las notas nunca salen del equipo."
30
47
  closed: "Este caso está cerrado. Vuelve a abrirlo para responder."
31
48
  assignee_only: "%{agent} lleva este caso y en esta mesa solo responde quien lo tiene."
49
+ draft:
50
+ title: "Propuesta de %{name}"
51
+ from: "propuesta de %{name}"
52
+ confidence: "confianza declarada %{percent} %"
53
+ sources: "Fuentes"
54
+ attachments: "Adjuntos"
55
+ stale: "La conversación ha cambiado desde esta propuesta. Léela antes de enviar."
56
+ editing: "Editando la propuesta de %{name}"
57
+ cancel_edit: "cancelar"
58
+ reason_placeholder: "¿Por qué la descartas? (opcional)"
59
+ sent_by: "Propuesta enviada por %{name}"
60
+ rejected_by: "Propuesta descartada por %{name}"
61
+ # The verb the "ya se %{status}" refusal reads. Never the English
62
+ # status: a Spanish desk reading "ya se sent" is the whole reason
63
+ # this map exists.
64
+ statuses:
65
+ pending: "propuso"
66
+ sent: "envió"
67
+ rejected: "descartó"
68
+ superseded: "sustituyó"
69
+ expired: "caducó"
70
+ badges:
71
+ needs_human: "Necesita persona"
72
+ pending_draft: "Propuesta lista"
73
+ assistant_holds: "%{name} se ocupa · %{used}/%{max} turnos"
74
+ assistant_holds_unlimited: "%{name} se ocupa"
75
+ withheld_reasons:
76
+ policy: "la política de este caso no se lo permite"
77
+ not_your_turn: "no era su turno"
78
+ # Por qué un caso acabó en manos de una persona. Los tokens los
79
+ # escribe el gem o el harness del host; aquí se leen en castellano.
80
+ escalation_reasons:
81
+ max_turns: "se quedó sin turnos"
82
+ assistant_silent: "el asistente no contestó a tiempo"
83
+ requester_request: "lo pidió el cliente"
84
+ phrase: "el cliente lo pidió en la conversación"
85
+ hand_off_when_error: "el asistente falló"
32
86
  assignment:
33
87
  title: "Quién lo lleva"
34
88
  held_by: "Lo lleva %{agent}"
@@ -39,6 +93,16 @@ es:
39
93
  timeline:
40
94
  title: "Historial"
41
95
  system: "Sistema"
96
+ escalated: "%{actor} pasó el caso a una persona: %{reason}"
97
+ human_requested: "El cliente pidió hablar con una persona"
98
+ assistant_paused: "%{actor} pausó el asistente"
99
+ assistant_resumed: "%{actor} reanudó el asistente"
100
+ draft_sent: "%{actor} envió la propuesta de %{assistant}"
101
+ draft_sent_edited: "%{actor} envió la propuesta de %{assistant} (editada)"
102
+ draft_rejected: "%{actor} descartó la propuesta de %{assistant}"
103
+ assistant_withheld: "%{assistant} no respondió: %{reason}"
104
+ draft_superseded: "Propuesta sustituida"
105
+ draft_expired: "Propuesta caducada al cerrar"
42
106
  kinds:
43
107
  message: "Mensaje"
44
108
  opened: "Abierto"
@@ -53,6 +117,7 @@ es:
53
117
  note: "Nota interna"
54
118
  actions:
55
119
  back: "Volver a la cola"
120
+ new_conversation: "Escribir a alguien"
56
121
  next: "Siguiente caso"
57
122
  reply: "Enviar"
58
123
  note: "Guardar nota"
@@ -63,7 +128,14 @@ es:
63
128
  close: "Cerrar"
64
129
  reopen: "Reabrir"
65
130
  change_topic: "Cambiar tema"
131
+ send_draft: "Enviar"
132
+ send_edited_draft: "Enviar propuesta editada"
133
+ edit_draft: "Editar"
134
+ reject_draft: "Descartar"
135
+ pause_assistant: "Pausar asistente"
136
+ resume_assistant: "Reanudar asistente"
66
137
  flashes:
138
+ message_sent: "Mensaje enviado a %{requester}."
67
139
  replied: "Respuesta enviada."
68
140
  taken: "El caso es tuyo."
69
141
  assigned: "Asignado a %{agent}."
@@ -74,6 +146,10 @@ es:
74
146
  noted: "Nota guardada."
75
147
  topic_changed: "Tema cambiado."
76
148
  queue_empty: "No te espera nada."
149
+ draft_sent: "Propuesta enviada."
150
+ draft_rejected: "Propuesta descartada."
151
+ assistant_paused: "Asistente pausado en este caso."
152
+ assistant_resumed: "Asistente reanudado en este caso."
77
153
  errors:
78
154
  forbidden: "No tienes acceso a la consola de soporte."
79
155
  not_allowed: "No puedes hacer eso en este caso."
@@ -90,4 +166,19 @@ es:
90
166
  take_it_first: "Toma el caso antes de responder: en esta mesa solo responde quien lo lleva."
91
167
  held_by_somebody_else: "Lo lleva %{holder}."
92
168
  unavailable_action: "Ahora mismo no puedes hacer eso en este caso."
169
+ unknown_requester: "No encontramos a esa persona."
170
+ not_a_requester: "Esa cuenta no puede recibir mensajes de soporte."
171
+ invalid_requester: "Ese enlace no apunta a nadie a quien podamos escribir."
172
+ invalid_subject: "Ese enlace no apunta a nada sobre lo que podamos abrir la conversación."
173
+ invalid_input: "Algo en el formulario no se ha enviado bien. Vuelve a intentarlo."
174
+ no_requester_lookup: "Abre este formulario desde la ficha de la persona: aquí no podemos buscarla."
175
+ writing_to_yourself: "No puedes abrir una conversación de soporte contigo. Si necesitas ayuda, pídela como cliente."
176
+ off_duty: "No estás de guardia, así que no puedes escribir a nadie ahora mismo."
177
+ attachments_again: "Si adjuntas archivos y algo falla, tendrás que volver a seleccionarlos."
178
+ unknown_draft: "Esa propuesta ya no existe."
179
+ draft_already_reviewed: "Esa propuesta ya se %{status}."
180
+ no_pending_draft: "No hay ninguna propuesta pendiente en este caso."
181
+ stale_turn: "La conversación ha cambiado. Léela y vuelve a enviar."
182
+ assistant_not_allowed: "El asistente no puede hacer eso en este caso."
183
+ not_an_assistant: "Esa acción es solo para el asistente."
93
184
  generic: "No ha funcionado."
@@ -3,15 +3,31 @@ en:
3
3
  system:
4
4
  assigned: "%{agent} is taking care of your request"
5
5
  reassigned: "%{agent} is now taking care of your request"
6
+ assistant_disclosure: "%{name} is %{desk}’s virtual assistant. If you’d rather talk to a person, use the button in this conversation."
7
+ handed_off_to_humans: "I’m passing you to somebody on the team."
8
+ handed_off_to_humans_with_promise: "I’m passing you to somebody on the team. We usually reply in under %{reply_within}."
9
+ human_requested: "Somebody from the team will take it from here."
10
+ human_requested_with_promise: "Somebody from the team will take it from here. We usually reply in under %{reply_within}."
11
+ assistant:
12
+ disclosed_name: "%{name} · virtual assistant"
6
13
  thread:
7
14
  closed_notice: "This conversation is closed. Start a new one if you need anything else."
8
15
  reopened_notice: "We reopened this conversation."
9
16
  promise: "We usually reply in under %{time}"
17
+ opened_by_support: "%{desk} opened this conversation with you about “%{label}”. You can reply right here."
18
+ unavailable_notice: "This account can no longer send or receive messages. The conversation stays here to read."
19
+ human_door: "I’d rather talk to a person"
20
+ human_requested_notice: "Done. Somebody from the team will take it from here."
21
+ handled_by_assistant: "Handled by %{name}"
22
+ handled_by_humans: "Somebody from the team will take it from here"
23
+ transcript:
24
+ deleted: "[message deleted]"
10
25
  notifications:
11
26
  title: "%{desk} · new message"
12
27
  queue:
13
28
  tabs:
14
29
  awaiting: "Needs a reply"
30
+ needs_human: "Needs a person"
15
31
  mine: "Mine"
16
32
  unassigned: "Unassigned"
17
33
  open: "Open"
@@ -19,6 +35,7 @@ en:
19
35
  closed: "Closed"
20
36
  summary:
21
37
  awaiting_reply: "awaiting reply"
38
+ needs_human: "needs a person"
22
39
  awaiting_requester: "awaiting requester"
23
40
  open: "open"
24
41
  closed: "closed"
@@ -41,6 +58,7 @@ en:
41
58
  state:
42
59
  awaiting_reply: "We're on it"
43
60
  answered: "We replied"
61
+ opened_by_support: "We wrote to you"
44
62
  closed: "Closed"
45
63
  doors:
46
64
  new: "Need help?"
@@ -4,10 +4,36 @@ es:
4
4
  system:
5
5
  assigned: "%{agent} se ocupa de tu consulta"
6
6
  reassigned: "%{agent} se ocupa ahora de tu consulta"
7
+ # Las tres líneas del asistente. Cada una tiene su variante con
8
+ # promesa: una mesa que no promete un plazo no debe inventarlo.
9
+ assistant_disclosure: "%{name} es el asistente virtual de %{desk}. Si prefieres hablar con una persona, usa el botón de esta conversación."
10
+ handed_off_to_humans: "Te paso con una persona del equipo."
11
+ handed_off_to_humans_with_promise: "Te paso con una persona del equipo. Normalmente contestamos en menos de %{reply_within}."
12
+ human_requested: "Una persona del equipo seguirá con tu consulta."
13
+ human_requested_with_promise: "Una persona del equipo seguirá con tu consulta. Normalmente contestamos en menos de %{reply_within}."
14
+ assistant:
15
+ # La divulgación no es una insignia que alguien pueda no ver: cuando
16
+ # se divulga, el nombre mismo dice qué es.
17
+ disclosed_name: "%{name} · asistente virtual"
7
18
  thread:
8
19
  closed_notice: "Esta conversación está cerrada. Abre una nueva si necesitas algo más."
9
20
  reopened_notice: "Hemos vuelto a abrir esta conversación."
10
21
  promise: "Normalmente respondemos en menos de %{time}"
22
+ # La línea con la que se abre un hilo que ha empezado el equipo: quien
23
+ # recibe un mensaje de un sitio al que nunca ha escrito necesita saber
24
+ # qué es esto.
25
+ opened_by_support: "%{desk} ha abierto esta conversación contigo sobre «%{label}». Puedes responder aquí mismo."
26
+ unavailable_notice: "Esta cuenta ya no puede enviar ni recibir mensajes. La conversación se queda aquí para consultarla."
27
+ human_door: "Prefiero hablar con una persona"
28
+ human_requested_notice: "Hecho. Una persona del equipo seguirá con tu consulta."
29
+ handled_by_assistant: "Atendida por %{name}"
30
+ handled_by_humans: "Una persona del equipo seguirá con tu consulta"
31
+ transcript:
32
+ # Un mensaje borrado sigue siendo un turno. Quien lea la conversación
33
+ # después, una persona o el modelo al que el host se la pasa, tiene
34
+ # que ver que ahí se dijo algo y se retiró: un hueco sin marcar se lee
35
+ # como que nunca pasó nada.
36
+ deleted: "[mensaje eliminado]"
11
37
  notifications:
12
38
  # Generic on purpose: una pantalla de bloqueo no debe contar de qué va
13
39
  # el caso de nadie. El detalle va en el cuerpo.
@@ -15,6 +41,7 @@ es:
15
41
  queue:
16
42
  tabs:
17
43
  awaiting: "Pendientes"
44
+ needs_human: "Necesitan persona"
18
45
  mine: "Mi cola"
19
46
  unassigned: "Sin asignar"
20
47
  open: "Abiertos"
@@ -22,6 +49,7 @@ es:
22
49
  closed: "Cerrados"
23
50
  summary:
24
51
  awaiting_reply: "esperando respuesta"
52
+ needs_human: "necesita una persona"
25
53
  awaiting_requester: "esperando al cliente"
26
54
  open: "abierto"
27
55
  closed: "cerrado"
@@ -45,6 +73,7 @@ es:
45
73
  state:
46
74
  awaiting_reply: "Estamos con ello"
47
75
  answered: "Te hemos contestado"
76
+ opened_by_support: "Te hemos escrito"
48
77
  closed: "Cerrada"
49
78
  # The ways in, from anywhere in the host app.
50
79
  doors:
data/config/routes.rb CHANGED
@@ -18,7 +18,13 @@ SupportDesk::Engine.routes.draw do
18
18
  # `show` redirects into the chats conversation — a stable URL for a case,
19
19
  # for emails and notifications, that never has to know where chats is
20
20
  # mounted.
21
- resources :tickets, only: %i[create show]
21
+ resources :tickets, only: %i[create show] do
22
+ # The door out of a machine. It is a POST on the CASE rather than
23
+ # anything to do with chats, because it is a fact about the case: from
24
+ # here on a person is expected, whatever the assistant would have done
25
+ # next.
26
+ post :request_human, on: :member
27
+ end
22
28
 
23
29
  root to: "tickets#index"
24
30
  end