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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +302 -0
- data/app/controllers/escalated/admin/bulk_actions_controller.rb +42 -0
- data/app/controllers/escalated/admin/canned_responses_controller.rb +73 -0
- data/app/controllers/escalated/admin/departments_controller.rb +135 -0
- data/app/controllers/escalated/admin/escalation_rules_controller.rb +121 -0
- data/app/controllers/escalated/admin/macros_controller.rb +73 -0
- data/app/controllers/escalated/admin/reports_controller.rb +152 -0
- data/app/controllers/escalated/admin/settings_controller.rb +111 -0
- data/app/controllers/escalated/admin/sla_policies_controller.rb +109 -0
- data/app/controllers/escalated/admin/tags_controller.rb +67 -0
- data/app/controllers/escalated/admin/tickets_controller.rb +299 -0
- data/app/controllers/escalated/agent/bulk_actions_controller.rb +42 -0
- data/app/controllers/escalated/agent/dashboard_controller.rb +94 -0
- data/app/controllers/escalated/agent/tickets_controller.rb +330 -0
- data/app/controllers/escalated/application_controller.rb +110 -0
- data/app/controllers/escalated/customer/satisfaction_ratings_controller.rb +44 -0
- data/app/controllers/escalated/customer/tickets_controller.rb +169 -0
- data/app/controllers/escalated/guest/tickets_controller.rb +231 -0
- data/app/controllers/escalated/inbound_controller.rb +79 -0
- data/app/jobs/escalated/check_sla_job.rb +36 -0
- data/app/jobs/escalated/close_resolved_job.rb +51 -0
- data/app/jobs/escalated/evaluate_escalations_job.rb +24 -0
- data/app/jobs/escalated/poll_imap_job.rb +74 -0
- data/app/jobs/escalated/purge_activities_job.rb +24 -0
- data/app/mailers/escalated/application_mailer.rb +6 -0
- data/app/mailers/escalated/ticket_mailer.rb +93 -0
- data/app/models/escalated/application_record.rb +5 -0
- data/app/models/escalated/attachment.rb +46 -0
- data/app/models/escalated/canned_response.rb +45 -0
- data/app/models/escalated/department.rb +43 -0
- data/app/models/escalated/escalated_setting.rb +43 -0
- data/app/models/escalated/escalation_rule.rb +96 -0
- data/app/models/escalated/inbound_email.rb +60 -0
- data/app/models/escalated/macro.rb +18 -0
- data/app/models/escalated/reply.rb +42 -0
- data/app/models/escalated/satisfaction_rating.rb +21 -0
- data/app/models/escalated/sla_policy.rb +54 -0
- data/app/models/escalated/tag.rb +28 -0
- data/app/models/escalated/ticket.rb +166 -0
- data/app/models/escalated/ticket_activity.rb +60 -0
- data/app/policies/escalated/canned_response_policy.rb +40 -0
- data/app/policies/escalated/department_policy.rb +36 -0
- data/app/policies/escalated/escalation_rule_policy.rb +36 -0
- data/app/policies/escalated/sla_policy_policy.rb +36 -0
- data/app/policies/escalated/tag_policy.rb +36 -0
- data/app/policies/escalated/ticket_policy.rb +111 -0
- data/config/routes.rb +81 -0
- data/db/migrate/001_create_escalated_departments.rb +18 -0
- data/db/migrate/002_create_escalated_sla_policies.rb +23 -0
- data/db/migrate/003_create_escalated_tags.rb +15 -0
- data/db/migrate/004_create_escalated_tickets.rb +48 -0
- data/db/migrate/005_create_escalated_replies.rb +21 -0
- data/db/migrate/006_create_escalated_attachments.rb +17 -0
- data/db/migrate/007_create_escalated_ticket_tags.rb +13 -0
- data/db/migrate/008_create_escalated_support_tables.rb +49 -0
- data/db/migrate/009_create_escalated_ticket_activities.rb +20 -0
- data/db/migrate/010_create_escalated_settings.rb +29 -0
- data/db/migrate/011_add_guest_fields_to_escalated_tickets.rb +28 -0
- data/db/migrate/012_create_escalated_inbound_emails.rb +30 -0
- data/db/migrate/013_create_escalated_macros.rb +18 -0
- data/db/migrate/014_create_escalated_ticket_followers.rb +18 -0
- data/db/migrate/015_create_escalated_satisfaction_ratings.rb +21 -0
- data/db/migrate/016_add_is_pinned_to_escalated_replies.rb +6 -0
- data/lib/escalated/configuration.rb +111 -0
- data/lib/escalated/drivers/cloud_driver.rb +134 -0
- data/lib/escalated/drivers/hosted_api_client.rb +166 -0
- data/lib/escalated/drivers/local_driver.rb +341 -0
- data/lib/escalated/drivers/synced_driver.rb +124 -0
- data/lib/escalated/engine.rb +45 -0
- data/lib/escalated/mail/adapters/base_adapter.rb +60 -0
- data/lib/escalated/mail/adapters/imap_adapter.rb +209 -0
- data/lib/escalated/mail/adapters/mailgun_adapter.rb +93 -0
- data/lib/escalated/mail/adapters/postmark_adapter.rb +94 -0
- data/lib/escalated/mail/adapters/ses_adapter.rb +179 -0
- data/lib/escalated/mail/inbound_message.rb +78 -0
- data/lib/escalated/manager.rb +33 -0
- data/lib/escalated/services/assignment_service.rb +85 -0
- data/lib/escalated/services/attachment_service.rb +110 -0
- data/lib/escalated/services/escalation_service.rb +159 -0
- data/lib/escalated/services/inbound_email_service.rb +255 -0
- data/lib/escalated/services/macro_service.rb +49 -0
- data/lib/escalated/services/notification_service.rb +157 -0
- data/lib/escalated/services/sla_service.rb +203 -0
- data/lib/escalated/services/ticket_service.rb +113 -0
- data/lib/escalated.rb +25 -0
- data/lib/generators/escalated/install_generator.rb +75 -0
- data/lib/generators/escalated/templates/initializer.rb +89 -0
- 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
|