escalated 0.4.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 (90) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +302 -0
  4. data/app/controllers/escalated/admin/bulk_actions_controller.rb +42 -0
  5. data/app/controllers/escalated/admin/canned_responses_controller.rb +73 -0
  6. data/app/controllers/escalated/admin/departments_controller.rb +135 -0
  7. data/app/controllers/escalated/admin/escalation_rules_controller.rb +121 -0
  8. data/app/controllers/escalated/admin/macros_controller.rb +73 -0
  9. data/app/controllers/escalated/admin/reports_controller.rb +152 -0
  10. data/app/controllers/escalated/admin/settings_controller.rb +111 -0
  11. data/app/controllers/escalated/admin/sla_policies_controller.rb +109 -0
  12. data/app/controllers/escalated/admin/tags_controller.rb +67 -0
  13. data/app/controllers/escalated/admin/tickets_controller.rb +299 -0
  14. data/app/controllers/escalated/agent/bulk_actions_controller.rb +42 -0
  15. data/app/controllers/escalated/agent/dashboard_controller.rb +94 -0
  16. data/app/controllers/escalated/agent/tickets_controller.rb +330 -0
  17. data/app/controllers/escalated/application_controller.rb +110 -0
  18. data/app/controllers/escalated/customer/satisfaction_ratings_controller.rb +44 -0
  19. data/app/controllers/escalated/customer/tickets_controller.rb +169 -0
  20. data/app/controllers/escalated/guest/tickets_controller.rb +231 -0
  21. data/app/controllers/escalated/inbound_controller.rb +79 -0
  22. data/app/jobs/escalated/check_sla_job.rb +36 -0
  23. data/app/jobs/escalated/close_resolved_job.rb +51 -0
  24. data/app/jobs/escalated/evaluate_escalations_job.rb +24 -0
  25. data/app/jobs/escalated/poll_imap_job.rb +74 -0
  26. data/app/jobs/escalated/purge_activities_job.rb +24 -0
  27. data/app/mailers/escalated/application_mailer.rb +6 -0
  28. data/app/mailers/escalated/ticket_mailer.rb +93 -0
  29. data/app/models/escalated/application_record.rb +5 -0
  30. data/app/models/escalated/attachment.rb +46 -0
  31. data/app/models/escalated/canned_response.rb +45 -0
  32. data/app/models/escalated/department.rb +43 -0
  33. data/app/models/escalated/escalated_setting.rb +43 -0
  34. data/app/models/escalated/escalation_rule.rb +96 -0
  35. data/app/models/escalated/inbound_email.rb +60 -0
  36. data/app/models/escalated/macro.rb +18 -0
  37. data/app/models/escalated/reply.rb +42 -0
  38. data/app/models/escalated/satisfaction_rating.rb +21 -0
  39. data/app/models/escalated/sla_policy.rb +54 -0
  40. data/app/models/escalated/tag.rb +28 -0
  41. data/app/models/escalated/ticket.rb +166 -0
  42. data/app/models/escalated/ticket_activity.rb +60 -0
  43. data/app/policies/escalated/canned_response_policy.rb +40 -0
  44. data/app/policies/escalated/department_policy.rb +36 -0
  45. data/app/policies/escalated/escalation_rule_policy.rb +36 -0
  46. data/app/policies/escalated/sla_policy_policy.rb +36 -0
  47. data/app/policies/escalated/tag_policy.rb +36 -0
  48. data/app/policies/escalated/ticket_policy.rb +111 -0
  49. data/config/routes.rb +81 -0
  50. data/db/migrate/001_create_escalated_departments.rb +18 -0
  51. data/db/migrate/002_create_escalated_sla_policies.rb +23 -0
  52. data/db/migrate/003_create_escalated_tags.rb +15 -0
  53. data/db/migrate/004_create_escalated_tickets.rb +48 -0
  54. data/db/migrate/005_create_escalated_replies.rb +21 -0
  55. data/db/migrate/006_create_escalated_attachments.rb +17 -0
  56. data/db/migrate/007_create_escalated_ticket_tags.rb +13 -0
  57. data/db/migrate/008_create_escalated_support_tables.rb +49 -0
  58. data/db/migrate/009_create_escalated_ticket_activities.rb +20 -0
  59. data/db/migrate/010_create_escalated_settings.rb +29 -0
  60. data/db/migrate/011_add_guest_fields_to_escalated_tickets.rb +28 -0
  61. data/db/migrate/012_create_escalated_inbound_emails.rb +30 -0
  62. data/db/migrate/013_create_escalated_macros.rb +18 -0
  63. data/db/migrate/014_create_escalated_ticket_followers.rb +18 -0
  64. data/db/migrate/015_create_escalated_satisfaction_ratings.rb +21 -0
  65. data/db/migrate/016_add_is_pinned_to_escalated_replies.rb +6 -0
  66. data/lib/escalated/configuration.rb +111 -0
  67. data/lib/escalated/drivers/cloud_driver.rb +134 -0
  68. data/lib/escalated/drivers/hosted_api_client.rb +166 -0
  69. data/lib/escalated/drivers/local_driver.rb +341 -0
  70. data/lib/escalated/drivers/synced_driver.rb +124 -0
  71. data/lib/escalated/engine.rb +45 -0
  72. data/lib/escalated/mail/adapters/base_adapter.rb +60 -0
  73. data/lib/escalated/mail/adapters/imap_adapter.rb +209 -0
  74. data/lib/escalated/mail/adapters/mailgun_adapter.rb +93 -0
  75. data/lib/escalated/mail/adapters/postmark_adapter.rb +94 -0
  76. data/lib/escalated/mail/adapters/ses_adapter.rb +179 -0
  77. data/lib/escalated/mail/inbound_message.rb +78 -0
  78. data/lib/escalated/manager.rb +33 -0
  79. data/lib/escalated/services/assignment_service.rb +85 -0
  80. data/lib/escalated/services/attachment_service.rb +110 -0
  81. data/lib/escalated/services/escalation_service.rb +159 -0
  82. data/lib/escalated/services/inbound_email_service.rb +255 -0
  83. data/lib/escalated/services/macro_service.rb +49 -0
  84. data/lib/escalated/services/notification_service.rb +157 -0
  85. data/lib/escalated/services/sla_service.rb +203 -0
  86. data/lib/escalated/services/ticket_service.rb +113 -0
  87. data/lib/escalated.rb +25 -0
  88. data/lib/generators/escalated/install_generator.rb +75 -0
  89. data/lib/generators/escalated/templates/initializer.rb +89 -0
  90. metadata +227 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b01fcbc99a3f41dca9db58cd8bd8d03037d63aa6cc7e308f8b482bc9b555aa79
4
+ data.tar.gz: 66dd31da20521e8e20cfcbdf86b0ea349d2c794d2660c8ba2a56ede336cc7fdc
5
+ SHA512:
6
+ metadata.gz: 48df414672dc4b1ed5e50ffebc6c0ad3fa5ffce96f98244ab7667cc87a2861e815499daa3db7bc229df6634d95a89953daaf79039951b3bc23b182c832edb236
7
+ data.tar.gz: e0b2c2c4adbdac476e4abd16ea84a872bf8eea70ead4427f285ff622f7c837fb24c1cbac7e32543e1ca30d8465fa01cb468bf8373d98ef0e77f22a522d418dbe
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Escalated Dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,302 @@
1
+ # Escalated for Rails
2
+
3
+ A full-featured, embeddable support ticket system for Rails. Drop it into any app — get a complete helpdesk with SLA tracking, escalation rules, agent workflows, and a customer portal. No external services required.
4
+
5
+ **Three hosting modes.** Run entirely self-hosted, sync to a central cloud for multi-app visibility, or proxy everything to the cloud. Switch modes with a single config change.
6
+
7
+ ## Features
8
+
9
+ - **Ticket lifecycle** — Create, assign, reply, resolve, close, reopen with configurable status transitions
10
+ - **SLA engine** — Per-priority response and resolution targets, business hours calculation, automatic breach detection
11
+ - **Escalation rules** — Condition-based rules that auto-escalate, reprioritize, reassign, or notify
12
+ - **Agent dashboard** — Ticket queue with filters, bulk actions, internal notes, canned responses
13
+ - **Customer portal** — Self-service ticket creation, replies, and status tracking
14
+ - **Admin panel** — Manage departments, SLA policies, escalation rules, tags, and view reports
15
+ - **File attachments** — Drag-and-drop uploads with configurable storage and size limits
16
+ - **Activity timeline** — Full audit log of every action on every ticket
17
+ - **Email notifications** — Configurable per-event notifications with webhook support
18
+ - **Department routing** — Organize agents into departments with auto-assignment (round-robin)
19
+ - **Tagging system** — Categorize tickets with colored tags
20
+ - **Inertia.js + Vue 3 UI** — Shared frontend via [`@escalated-dev/escalated`](https://github.com/escalated-dev/escalated)
21
+
22
+ ### v0.4.0 — Advanced Features
23
+
24
+ - **Bulk actions** — Assign, change status/priority, add tags, close, or delete multiple tickets at once
25
+ - **Macros** — Reusable multi-step automations (set status + assign + add note in one click)
26
+ - **Ticket followers** — Agents follow tickets and receive the same notifications as the assignee
27
+ - **Satisfaction ratings** — 1-5 star CSAT ratings with optional comments after resolution
28
+ - **Pinned notes** — Pin important internal notes to the top of the ticket thread
29
+ - **Keyboard shortcuts** — Full keyboard navigation for power users
30
+ - **Quick filters** — One-click filter chips (My Tickets, Unassigned, Urgent, SLA Breaching)
31
+ - **Presence indicators** — See who else is viewing a ticket in real-time
32
+ - **Enhanced dashboard** — CSAT metrics, resolution times, SLA breach tracking
33
+
34
+ ## Requirements
35
+
36
+ - Ruby 3.1+
37
+ - Rails 7.1+
38
+ - Node.js 18+ (for frontend assets)
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ bundle add escalated
44
+ npm install @escalated-dev/escalated
45
+ rails generate escalated:install
46
+ rails db:migrate
47
+ ```
48
+
49
+ Add the `Ticketable` concern to your User model:
50
+
51
+ ```ruby
52
+ class User < ApplicationRecord
53
+ include Escalated::Ticketable
54
+ end
55
+ ```
56
+
57
+ Define authorization in your `ApplicationController` or an initializer:
58
+
59
+ ```ruby
60
+ # config/initializers/escalated.rb
61
+ Escalated.configure do |config|
62
+ config.admin_check = ->(user) { user.admin? }
63
+ config.agent_check = ->(user) { user.agent? || user.admin? }
64
+ end
65
+ ```
66
+
67
+ Visit `/support` — you're live.
68
+
69
+ ## Frontend Setup
70
+
71
+ Escalated uses Inertia.js with Vue 3. The frontend components are provided by the [`@escalated-dev/escalated`](https://github.com/escalated-dev/escalated) npm package.
72
+
73
+ ### Tailwind Content
74
+
75
+ Add the Escalated package to your Tailwind `content` config so its classes aren't purged:
76
+
77
+ ```js
78
+ // tailwind.config.js
79
+ content: [
80
+ // ... your existing paths
81
+ './node_modules/@escalated-dev/escalated/src/**/*.vue',
82
+ ],
83
+ ```
84
+
85
+ ### Page Resolver
86
+
87
+ Add the Escalated pages to your Inertia page resolver:
88
+
89
+ ```javascript
90
+ // app/javascript/entrypoints/application.js
91
+ import { createApp, h } from 'vue'
92
+ import { createInertiaApp } from '@inertiajs/vue3'
93
+
94
+ createInertiaApp({
95
+ resolve: name => {
96
+ if (name.startsWith('Escalated/')) {
97
+ const escalatedPages = import.meta.glob(
98
+ '../../../node_modules/@escalated-dev/escalated/src/pages/**/*.vue',
99
+ { eager: true }
100
+ )
101
+ const pageName = name.replace('Escalated/', '')
102
+ return escalatedPages[`../../../node_modules/@escalated-dev/escalated/src/pages/${pageName}.vue`]
103
+ }
104
+
105
+ const pages = import.meta.glob('../pages/**/*.vue', { eager: true })
106
+ return pages[`../pages/${name}.vue`]
107
+ },
108
+ setup({ el, App, props, plugin }) {
109
+ createApp({ render: () => h(App, props) })
110
+ .use(plugin)
111
+ .mount(el)
112
+ },
113
+ })
114
+ ```
115
+
116
+ ### Theming (Optional)
117
+
118
+ Register the `EscalatedPlugin` to render Escalated pages inside your app's layout — no page duplication needed:
119
+
120
+ ```javascript
121
+ import { EscalatedPlugin } from '@escalated-dev/escalated'
122
+ import AppLayout from '@/layouts/AppLayout.vue'
123
+
124
+ createInertiaApp({
125
+ setup({ el, App, props, plugin }) {
126
+ createApp({ render: () => h(App, props) })
127
+ .use(plugin)
128
+ .use(EscalatedPlugin, {
129
+ layout: AppLayout,
130
+ theme: {
131
+ primary: '#3b82f6',
132
+ radius: '0.75rem',
133
+ }
134
+ })
135
+ .mount(el)
136
+ },
137
+ })
138
+ ```
139
+
140
+ Your layout component must accept a `#header` slot and a default slot. Escalated will render its sub-navigation in the header and page content in the default slot. Without the plugin, Escalated uses its own standalone layout.
141
+
142
+ See the [`@escalated-dev/escalated` README](https://github.com/escalated-dev/escalated) for full theming documentation and CSS custom properties.
143
+
144
+ ## Hosting Modes
145
+
146
+ ### Self-Hosted (default)
147
+
148
+ Everything stays in your database. No external calls. Full autonomy.
149
+
150
+ ```ruby
151
+ Escalated.configure do |config|
152
+ config.mode = :self_hosted
153
+ end
154
+ ```
155
+
156
+ ### Synced
157
+
158
+ Local database + automatic sync to `cloud.escalated.dev` for unified inbox across multiple apps. If the cloud is unreachable, your app keeps working — events queue and retry.
159
+
160
+ ```ruby
161
+ Escalated.configure do |config|
162
+ config.mode = :synced
163
+ config.hosted_api_url = "https://cloud.escalated.dev/api/v1"
164
+ config.hosted_api_key = ENV["ESCALATED_API_KEY"]
165
+ end
166
+ ```
167
+
168
+ ### Cloud
169
+
170
+ All ticket data proxied to the cloud API. Your app handles auth and renders UI, but storage lives in the cloud.
171
+
172
+ ```ruby
173
+ Escalated.configure do |config|
174
+ config.mode = :cloud
175
+ config.hosted_api_url = "https://cloud.escalated.dev/api/v1"
176
+ config.hosted_api_key = ENV["ESCALATED_API_KEY"]
177
+ end
178
+ ```
179
+
180
+ All three modes share the same controllers, UI, and business logic. The driver pattern handles the rest.
181
+
182
+ ## Configuration
183
+
184
+ Create or edit `config/initializers/escalated.rb`:
185
+
186
+ ```ruby
187
+ Escalated.configure do |config|
188
+ config.mode = :self_hosted
189
+ config.user_class = "User"
190
+ config.table_prefix = "escalated_"
191
+ config.route_prefix = "support"
192
+ config.default_priority = :medium
193
+
194
+ # Middleware
195
+ config.middleware = [:authenticate_user!]
196
+ config.admin_middleware = nil
197
+
198
+ # Tickets
199
+ config.allow_customer_close = true
200
+ config.auto_close_resolved_after_days = 7
201
+ config.max_attachments_per_reply = 5
202
+ config.max_attachment_size_kb = 10240
203
+
204
+ # SLA
205
+ config.sla = {
206
+ enabled: true,
207
+ business_hours_only: true,
208
+ business_hours: {
209
+ start: 9, end: 17,
210
+ timezone: "UTC",
211
+ working_days: [1, 2, 3, 4, 5]
212
+ }
213
+ }
214
+
215
+ # Notifications
216
+ config.notification_channels = [:email]
217
+ config.webhook_url = nil
218
+
219
+ # Storage (ActiveStorage)
220
+ config.storage_service = :local
221
+ end
222
+ ```
223
+
224
+ ## Scheduling
225
+
226
+ Add these to your scheduler for SLA and escalation automation:
227
+
228
+ ```ruby
229
+ # config/schedule.rb (whenever gem) or use solid_queue/sidekiq-cron
230
+ every 1.minute do
231
+ runner "Escalated::CheckSlaJob.perform_now"
232
+ end
233
+
234
+ every 5.minutes do
235
+ runner "Escalated::EvaluateEscalationsJob.perform_now"
236
+ end
237
+
238
+ every 1.day do
239
+ runner "Escalated::CloseResolvedJob.perform_now"
240
+ end
241
+
242
+ every 1.week do
243
+ runner "Escalated::PurgeActivitiesJob.perform_now"
244
+ end
245
+ ```
246
+
247
+ ## Routes
248
+
249
+ Routes are automatically mounted when the engine loads. By default they mount at `/support`.
250
+
251
+ | Route | Method | Description |
252
+ |-------|--------|-------------|
253
+ | `/support` | GET | Customer ticket list |
254
+ | `/support/create` | GET | New ticket form |
255
+ | `/support/{ticket}` | GET | Ticket detail |
256
+ | `/support/agent` | GET | Agent dashboard |
257
+ | `/support/agent/tickets` | GET | Agent ticket queue |
258
+ | `/support/agent/tickets/{ticket}` | GET | Agent ticket view |
259
+ | `/support/admin/reports` | GET | Admin reports |
260
+ | `/support/admin/departments` | GET | Department management |
261
+ | `/support/admin/sla-policies` | GET | SLA policy management |
262
+ | `/support/admin/escalation-rules` | GET | Escalation rule management |
263
+ | `/support/admin/tags` | GET | Tag management |
264
+ | `/support/admin/canned-responses` | GET | Canned response management |
265
+ | `/support/agent/tickets/bulk` | POST | Bulk actions on multiple tickets |
266
+ | `/support/agent/tickets/{ticket}/follow` | POST | Follow/unfollow a ticket |
267
+ | `/support/agent/tickets/{ticket}/macro` | POST | Apply a macro to a ticket |
268
+ | `/support/agent/tickets/{ticket}/presence` | POST | Update presence on a ticket |
269
+ | `/support/agent/tickets/{ticket}/pin/{reply}` | POST | Pin/unpin an internal note |
270
+ | `/support/{ticket}/rate` | POST | Submit satisfaction rating |
271
+
272
+ ## Events
273
+
274
+ Connect to ticket lifecycle events via ActiveSupport::Notifications:
275
+
276
+ ```ruby
277
+ ActiveSupport::Notifications.subscribe("escalated.ticket_created") do |event|
278
+ ticket = event.payload[:ticket]
279
+ # Handle new ticket
280
+ end
281
+ ```
282
+
283
+ ## Also Available For
284
+
285
+ - **[Escalated for Laravel](https://github.com/escalated-dev/escalated-laravel)** — Laravel Composer package
286
+ - **[Escalated for Rails](https://github.com/escalated-dev/escalated-rails)** — Ruby on Rails engine (you are here)
287
+ - **[Escalated for Django](https://github.com/escalated-dev/escalated-django)** — Django reusable app
288
+ - **[Escalated for AdonisJS](https://github.com/escalated-dev/escalated-adonis)** — AdonisJS v6 package
289
+ - **[Escalated for Filament](https://github.com/escalated-dev/escalated-filament)** — Filament v3 admin panel plugin
290
+ - **[Shared Frontend](https://github.com/escalated-dev/escalated)** — Vue 3 + Inertia.js UI components
291
+
292
+ Same architecture, same Vue UI, same three hosting modes — for every major backend framework.
293
+
294
+ ## Testing
295
+
296
+ ```bash
297
+ bundle exec rspec
298
+ ```
299
+
300
+ ## License
301
+
302
+ MIT
@@ -0,0 +1,42 @@
1
+ module Escalated
2
+ module Admin
3
+ class BulkActionsController < Escalated::ApplicationController
4
+ before_action :require_admin!
5
+
6
+ def create
7
+ ticket_ids = params[:ticket_ids]
8
+ action = params[:action]
9
+ value = params[:value]
10
+ success_count = 0
11
+
12
+ tickets = Escalated::Ticket.where(id: ticket_ids)
13
+
14
+ tickets.each do |ticket|
15
+ begin
16
+ case action.to_s
17
+ when "status"
18
+ Services::TicketService.transition_status(ticket, value, actor: escalated_current_user)
19
+ when "priority"
20
+ Services::TicketService.change_priority(ticket, value, actor: escalated_current_user)
21
+ when "assign"
22
+ agent = Escalated.configuration.user_model.find(value)
23
+ Services::AssignmentService.assign(ticket, agent, actor: escalated_current_user)
24
+ when "tag"
25
+ Services::TicketService.add_tags(ticket, Array(value), actor: escalated_current_user)
26
+ when "close"
27
+ Services::TicketService.close(ticket, actor: escalated_current_user)
28
+ when "delete"
29
+ ticket.destroy!
30
+ end
31
+ success_count += 1
32
+ rescue StandardError => e
33
+ Rails.logger.warn("[Escalated::BulkActions] Failed to #{action} ticket ##{ticket.id}: #{e.message}")
34
+ end
35
+ end
36
+
37
+ redirect_back fallback_location: escalated.admin_tickets_path,
38
+ notice: "#{success_count} ticket(s) updated."
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ module Escalated
2
+ module Admin
3
+ class CannedResponsesController < Escalated::ApplicationController
4
+ before_action :require_admin!
5
+ before_action :set_canned_response, only: [:update, :destroy]
6
+
7
+ def index
8
+ responses = Escalated::CannedResponse.ordered
9
+
10
+ render inertia: "Escalated/Admin/CannedResponses/Index", props: {
11
+ canned_responses: responses.map { |r| canned_response_json(r) }
12
+ }
13
+ end
14
+
15
+ def create
16
+ response = Escalated::CannedResponse.new(canned_response_params)
17
+ response.created_by = escalated_current_user.id
18
+
19
+ if response.save
20
+ redirect_to admin_canned_responses_path, notice: "Canned response created."
21
+ else
22
+ redirect_back fallback_location: admin_canned_responses_path,
23
+ alert: response.errors.full_messages.join(", ")
24
+ end
25
+ end
26
+
27
+ def update
28
+ if @canned_response.update(canned_response_params)
29
+ redirect_to admin_canned_responses_path, notice: "Canned response updated."
30
+ else
31
+ redirect_back fallback_location: admin_canned_responses_path,
32
+ alert: @canned_response.errors.full_messages.join(", ")
33
+ end
34
+ end
35
+
36
+ def destroy
37
+ @canned_response.destroy!
38
+ redirect_to admin_canned_responses_path, notice: "Canned response deleted."
39
+ end
40
+
41
+ private
42
+
43
+ def set_canned_response
44
+ @canned_response = Escalated::CannedResponse.find(params[:id])
45
+ end
46
+
47
+ def canned_response_params
48
+ params.require(:canned_response).permit(:title, :body, :shortcode, :category, :is_shared)
49
+ end
50
+
51
+ def canned_response_json(response)
52
+ {
53
+ id: response.id,
54
+ title: response.title,
55
+ body: response.body,
56
+ shortcode: response.shortcode,
57
+ category: response.category,
58
+ is_shared: response.is_shared,
59
+ creator: response.creator ? {
60
+ id: response.creator.id,
61
+ name: response.creator.respond_to?(:name) ? response.creator.name : response.creator.email
62
+ } : nil,
63
+ created_at: response.created_at&.iso8601,
64
+ updated_at: response.updated_at&.iso8601
65
+ }
66
+ end
67
+
68
+ def admin_canned_responses_path
69
+ escalated.admin_canned_responses_path
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,135 @@
1
+ module Escalated
2
+ module Admin
3
+ class DepartmentsController < Escalated::ApplicationController
4
+ before_action :require_admin!
5
+ before_action :set_department, only: [:show, :edit, :update, :destroy]
6
+
7
+ def index
8
+ departments = Escalated::Department.ordered
9
+
10
+ render inertia: "Escalated/Admin/Departments/Index", props: {
11
+ departments: departments.map { |d| department_json(d) }
12
+ }
13
+ end
14
+
15
+ def new
16
+ render inertia: "Escalated/Admin/Departments/Form", props: {
17
+ department: nil,
18
+ sla_policies: Escalated::SlaPolicy.active.ordered.map { |p| { id: p.id, name: p.name } },
19
+ agents: agent_list
20
+ }
21
+ end
22
+
23
+ def create
24
+ department = Escalated::Department.new(department_params)
25
+
26
+ if department.save
27
+ sync_agents(department, params[:agent_ids])
28
+ redirect_to admin_department_path(department), notice: "Department created."
29
+ else
30
+ redirect_back fallback_location: new_admin_department_path,
31
+ alert: department.errors.full_messages.join(", ")
32
+ end
33
+ end
34
+
35
+ def show
36
+ render inertia: "Escalated/Admin/Departments/Show", props: {
37
+ department: department_json(@department),
38
+ agents: @department.agents.map { |a|
39
+ { id: a.id, name: a.respond_to?(:name) ? a.name : a.email, email: a.email }
40
+ },
41
+ stats: {
42
+ total_tickets: @department.tickets.count,
43
+ open_tickets: @department.open_ticket_count,
44
+ agent_count: @department.agent_count
45
+ }
46
+ }
47
+ end
48
+
49
+ def edit
50
+ render inertia: "Escalated/Admin/Departments/Form", props: {
51
+ department: department_json(@department),
52
+ sla_policies: Escalated::SlaPolicy.active.ordered.map { |p| { id: p.id, name: p.name } },
53
+ agents: agent_list,
54
+ current_agent_ids: @department.agents.pluck(:id)
55
+ }
56
+ end
57
+
58
+ def update
59
+ if @department.update(department_params)
60
+ sync_agents(@department, params[:agent_ids]) if params.key?(:agent_ids)
61
+ redirect_to admin_department_path(@department), notice: "Department updated."
62
+ else
63
+ redirect_back fallback_location: edit_admin_department_path(@department),
64
+ alert: @department.errors.full_messages.join(", ")
65
+ end
66
+ end
67
+
68
+ def destroy
69
+ @department.destroy!
70
+ redirect_to admin_departments_path, notice: "Department deleted."
71
+ end
72
+
73
+ private
74
+
75
+ def set_department
76
+ @department = Escalated::Department.find(params[:id])
77
+ end
78
+
79
+ def department_params
80
+ params.require(:department).permit(:name, :description, :email, :is_active, :default_sla_policy_id)
81
+ end
82
+
83
+ def sync_agents(department, agent_ids)
84
+ return unless agent_ids.present?
85
+
86
+ agents = Escalated.configuration.user_model.where(id: agent_ids)
87
+ department.agents = agents
88
+ end
89
+
90
+ def department_json(department)
91
+ {
92
+ id: department.id,
93
+ name: department.name,
94
+ slug: department.slug,
95
+ description: department.description,
96
+ email: department.email,
97
+ is_active: department.is_active,
98
+ default_sla_policy: department.default_sla_policy ? {
99
+ id: department.default_sla_policy.id,
100
+ name: department.default_sla_policy.name
101
+ } : nil,
102
+ agent_count: department.agent_count,
103
+ open_ticket_count: department.open_ticket_count,
104
+ created_at: department.created_at&.iso8601
105
+ }
106
+ end
107
+
108
+ def agent_list
109
+ if Escalated.configuration.user_model.respond_to?(:escalated_agents)
110
+ Escalated.configuration.user_model.escalated_agents.map { |a|
111
+ { id: a.id, name: a.respond_to?(:name) ? a.name : a.email, email: a.email }
112
+ }
113
+ else
114
+ []
115
+ end
116
+ end
117
+
118
+ def admin_department_path(department)
119
+ escalated.admin_department_path(department)
120
+ end
121
+
122
+ def new_admin_department_path
123
+ escalated.new_admin_department_path
124
+ end
125
+
126
+ def edit_admin_department_path(department)
127
+ escalated.edit_admin_department_path(department)
128
+ end
129
+
130
+ def admin_departments_path
131
+ escalated.admin_departments_path
132
+ end
133
+ end
134
+ end
135
+ end