sincerely 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/app/assets/config/sincerely.js +2 -0
- data/app/assets/images/sincerely/arrow_left.svg +1 -0
- data/app/assets/images/sincerely/check.svg +3 -0
- data/app/assets/images/sincerely/chevron_left.svg +1 -0
- data/app/assets/images/sincerely/clock.svg +1 -0
- data/app/assets/images/sincerely/code.svg +1 -0
- data/app/assets/images/sincerely/eye.svg +1 -0
- data/app/assets/images/sincerely/logout.svg +1 -0
- data/app/assets/images/sincerely/menu.svg +1 -0
- data/app/assets/images/sincerely/moon.svg +1 -0
- data/app/assets/images/sincerely/pencil.svg +4 -0
- data/app/assets/images/sincerely/plus.svg +3 -0
- data/app/assets/images/sincerely/progress_spinner.svg +3 -0
- data/app/assets/images/sincerely/spinner.svg +3 -0
- data/app/assets/images/sincerely/sun.svg +1 -0
- data/app/assets/images/sincerely/warning_circle.svg +4 -0
- data/app/assets/javascripts/sincerely/charts.js +115 -0
- data/app/assets/javascripts/sincerely/clickable_rows.js +29 -0
- data/app/assets/javascripts/sincerely/layout.js +104 -0
- data/app/assets/javascripts/sincerely/send_form.js +243 -0
- data/app/assets/javascripts/sincerely/template_form.js +44 -0
- data/app/assets/javascripts/sincerely/template_preview.js +12 -0
- data/app/assets/javascripts/sincerely/theme_init.js +9 -0
- data/app/assets/stylesheets/sincerely/charts.css +250 -0
- data/app/assets/stylesheets/sincerely/components.css +632 -0
- data/app/assets/stylesheets/sincerely/layout.css +233 -0
- data/app/assets/stylesheets/sincerely/navigation.css +198 -0
- data/app/assets/stylesheets/sincerely/send_form.css +287 -0
- data/app/assets/stylesheets/sincerely/template_form.css +74 -0
- data/app/assets/stylesheets/sincerely/template_preview.css +75 -0
- data/app/assets/stylesheets/sincerely/variables.css +88 -0
- data/app/controllers/sincerely/application_controller.rb +3 -72
- data/app/controllers/sincerely/concerns/authorization.rb +21 -0
- data/app/controllers/sincerely/concerns/delivery_metrics.rb +51 -0
- data/app/controllers/sincerely/concerns/event_filtering.rb +58 -0
- data/app/controllers/sincerely/concerns/event_timeline.rb +41 -0
- data/app/controllers/sincerely/concerns/notification_timeline.rb +73 -0
- data/app/controllers/sincerely/concerns/notifications.rb +47 -0
- data/app/controllers/sincerely/concerns/pagination.rb +39 -0
- data/app/controllers/sincerely/concerns/periods.rb +40 -0
- data/app/controllers/sincerely/dashboard_controller.rb +30 -88
- data/app/controllers/sincerely/delivery_events_controller.rb +13 -21
- data/app/controllers/sincerely/engagement_events_controller.rb +13 -22
- data/app/controllers/sincerely/notifications_controller.rb +68 -49
- data/app/controllers/sincerely/send_controller.rb +82 -67
- data/app/controllers/sincerely/templates_controller.rb +37 -8
- data/app/helpers/sincerely/application_helper.rb +22 -0
- data/app/helpers/sincerely/chart_helper.rb +79 -0
- data/app/views/layouts/sincerely/application.html.erb +24 -528
- data/app/views/sincerely/dashboard/index.html.erb +25 -256
- data/app/views/sincerely/delivery_events/index.html.erb +6 -38
- data/app/views/sincerely/engagement_events/index.html.erb +6 -38
- data/app/views/sincerely/notifications/index.html.erb +7 -23
- data/app/views/sincerely/notifications/show.html.erb +4 -4
- data/app/views/sincerely/send/new.html.erb +8 -522
- data/app/views/sincerely/shared/_event.html.erb +21 -0
- data/app/views/sincerely/shared/_line_chart.html.erb +38 -0
- data/app/views/sincerely/shared/_notification_type_filter.html.erb +9 -0
- data/app/views/sincerely/shared/_status_option.html.erb +1 -0
- data/app/views/sincerely/shared/_template_option.html.erb +3 -0
- data/app/views/sincerely/templates/_form.html.erb +8 -107
- data/app/views/sincerely/templates/edit.html.erb +2 -2
- data/app/views/sincerely/templates/index.html.erb +7 -15
- data/app/views/sincerely/templates/new.html.erb +2 -2
- data/app/views/sincerely/templates/preview.html.erb +7 -26
- data/app/views/sincerely/templates/show.html.erb +3 -3
- data/lib/config/sincerely_config.rb +2 -2
- data/lib/sincerely/delivery_systems/email_aws_ses.rb +2 -2
- data/lib/sincerely/engine.rb +4 -0
- data/lib/sincerely/mixins/notification_model.rb +5 -1
- data/lib/sincerely/services/process_delivery_event.rb +9 -7
- data/lib/sincerely/templates/notification_template.rb +1 -1
- data/lib/sincerely/version.rb +1 -1
- metadata +52 -4
- data/sincerely.gemspec +0 -37
|
@@ -27,153 +27,9 @@
|
|
|
27
27
|
<h2 class="card-title">Notifications Over Time</h2>
|
|
28
28
|
</div>
|
|
29
29
|
<% if @notifications_timeline[:series].any? %>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'accepted' => '#B9E0B8',
|
|
34
|
-
'delivered' => '#10b981',
|
|
35
|
-
'opened' => '#78c6cc',
|
|
36
|
-
'clicked' => '#BDD9DB',
|
|
37
|
-
'bounced' => '#D4183D',
|
|
38
|
-
'rejected' => '#D4183D',
|
|
39
|
-
'complained' => '#D4183D',
|
|
40
|
-
'delayed' => '#f97316'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
labels = @notifications_timeline[:labels]
|
|
44
|
-
series = @notifications_timeline[:series]
|
|
45
|
-
|
|
46
|
-
# Chart dimensions
|
|
47
|
-
chart_width = 800
|
|
48
|
-
chart_height = 220
|
|
49
|
-
padding_left = 45
|
|
50
|
-
padding_right = 20
|
|
51
|
-
padding_top = 20
|
|
52
|
-
padding_bottom = 30
|
|
53
|
-
|
|
54
|
-
plot_width = chart_width - padding_left - padding_right
|
|
55
|
-
plot_height = chart_height - padding_top - padding_bottom
|
|
56
|
-
|
|
57
|
-
max_value = series.values.flatten.max || 0
|
|
58
|
-
max_value = [max_value, 1].max # Minimum of 1 to avoid division by zero
|
|
59
|
-
|
|
60
|
-
magnitude = 10 ** Math.log10([max_value, 1].max).floor
|
|
61
|
-
max_y = ((max_value.to_f / magnitude).ceil * magnitude).to_i
|
|
62
|
-
max_y = [max_y, 1].max
|
|
63
|
-
|
|
64
|
-
x_step = labels.length > 1 ? plot_width.to_f / (labels.length - 1) : plot_width
|
|
65
|
-
%>
|
|
66
|
-
<div class="line-chart-container" id="lineChart">
|
|
67
|
-
<div class="line-chart-wrapper">
|
|
68
|
-
<svg class="line-chart" viewBox="0 0 <%= chart_width %> <%= chart_height %>" preserveAspectRatio="xMidYMid meet">
|
|
69
|
-
<%# Grid lines %>
|
|
70
|
-
<% 5.times do |i| %>
|
|
71
|
-
<% y = padding_top + (plot_height * i / 4.0) %>
|
|
72
|
-
<line class="line-chart-grid" x1="<%= padding_left %>" y1="<%= y %>" x2="<%= chart_width - padding_right %>" y2="<%= y %>" />
|
|
73
|
-
<text class="line-chart-label line-chart-label-y" x="<%= padding_left - 8 %>" y="<%= y + 3 %>"><%= (max_y * (4 - i) / 4.0).round %></text>
|
|
74
|
-
<% end %>
|
|
75
|
-
|
|
76
|
-
<%# X-axis labels (show subset to avoid overlap) %>
|
|
77
|
-
<% label_step = [1, (labels.length / 8.0).ceil].max %>
|
|
78
|
-
<% labels.each_with_index do |label, i| %>
|
|
79
|
-
<% if i % label_step == 0 || i == labels.length - 1 %>
|
|
80
|
-
<% x = padding_left + (i * x_step) %>
|
|
81
|
-
<text class="line-chart-label line-chart-label-x" x="<%= x %>" y="<%= chart_height - 8 %>"><%= label %></text>
|
|
82
|
-
<% end %>
|
|
83
|
-
<% end %>
|
|
84
|
-
|
|
85
|
-
<%# Lines for each state %>
|
|
86
|
-
<% series.each do |state, values| %>
|
|
87
|
-
<% color = state_colors[state.to_s] || '#6b7280' %>
|
|
88
|
-
<%
|
|
89
|
-
points = values.each_with_index.map do |value, i|
|
|
90
|
-
x = padding_left + (i * x_step)
|
|
91
|
-
y = padding_top + plot_height - (value.to_f / max_y * plot_height)
|
|
92
|
-
"#{x.round(2)},#{y.round(2)}"
|
|
93
|
-
end.join(' ')
|
|
94
|
-
|
|
95
|
-
# Area fill points
|
|
96
|
-
area_points = "#{padding_left},#{padding_top + plot_height} #{points} #{padding_left + ((values.length - 1) * x_step)},#{padding_top + plot_height}"
|
|
97
|
-
%>
|
|
98
|
-
<polygon class="line-chart-area" data-state="<%= state %>" fill="<%= color %>" points="<%= area_points %>" />
|
|
99
|
-
<polyline class="line-chart-line" data-state="<%= state %>" stroke="<%= color %>" points="<%= points %>" />
|
|
100
|
-
<% end %>
|
|
101
|
-
</svg>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<%# Legend %>
|
|
105
|
-
<div class="line-chart-legend">
|
|
106
|
-
<% series.each do |state, values| %>
|
|
107
|
-
<% color = state_colors[state.to_s] || '#6b7280' %>
|
|
108
|
-
<div class="line-chart-legend-item" data-state="<%= state %>">
|
|
109
|
-
<span class="donut-legend-color" style="background: <%= color %>"></span>
|
|
110
|
-
<span class="donut-legend-text"><%= state.to_s.humanize %></span>
|
|
111
|
-
</div>
|
|
112
|
-
<% end %>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
<script>
|
|
117
|
-
(function() {
|
|
118
|
-
const chart = document.getElementById('lineChart');
|
|
119
|
-
if (!chart) return;
|
|
120
|
-
|
|
121
|
-
const lines = chart.querySelectorAll('.line-chart-line');
|
|
122
|
-
const areas = chart.querySelectorAll('.line-chart-area');
|
|
123
|
-
const legendItems = chart.querySelectorAll('.line-chart-legend-item');
|
|
124
|
-
|
|
125
|
-
function highlightState(state) {
|
|
126
|
-
lines.forEach(line => {
|
|
127
|
-
if (line.dataset.state === state) {
|
|
128
|
-
line.classList.add('highlighted');
|
|
129
|
-
line.classList.remove('dimmed');
|
|
130
|
-
} else {
|
|
131
|
-
line.classList.add('dimmed');
|
|
132
|
-
line.classList.remove('highlighted');
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
areas.forEach(area => {
|
|
136
|
-
if (area.dataset.state === state) {
|
|
137
|
-
area.classList.add('highlighted');
|
|
138
|
-
area.classList.remove('dimmed');
|
|
139
|
-
} else {
|
|
140
|
-
area.classList.add('dimmed');
|
|
141
|
-
area.classList.remove('highlighted');
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
legendItems.forEach(item => {
|
|
145
|
-
if (item.dataset.state === state) {
|
|
146
|
-
item.classList.add('highlighted');
|
|
147
|
-
item.classList.remove('dimmed');
|
|
148
|
-
} else {
|
|
149
|
-
item.classList.add('dimmed');
|
|
150
|
-
item.classList.remove('highlighted');
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function resetHighlight() {
|
|
156
|
-
lines.forEach(line => line.classList.remove('highlighted', 'dimmed'));
|
|
157
|
-
areas.forEach(area => area.classList.remove('highlighted', 'dimmed'));
|
|
158
|
-
legendItems.forEach(item => item.classList.remove('highlighted', 'dimmed'));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
legendItems.forEach(item => {
|
|
162
|
-
item.addEventListener('mouseenter', () => highlightState(item.dataset.state));
|
|
163
|
-
item.addEventListener('mouseleave', resetHighlight);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
lines.forEach(line => {
|
|
167
|
-
line.addEventListener('mouseenter', () => highlightState(line.dataset.state));
|
|
168
|
-
line.addEventListener('mouseleave', resetHighlight);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
areas.forEach(area => {
|
|
172
|
-
area.addEventListener('mouseenter', () => highlightState(area.dataset.state));
|
|
173
|
-
area.addEventListener('mouseleave', resetHighlight);
|
|
174
|
-
});
|
|
175
|
-
})();
|
|
176
|
-
</script>
|
|
30
|
+
<%= render 'sincerely/shared/line_chart',
|
|
31
|
+
labels: @notifications_timeline[:labels],
|
|
32
|
+
series: @notifications_timeline[:series] %>
|
|
177
33
|
<% else %>
|
|
178
34
|
<div class="line-chart-empty">No notifications in this period</div>
|
|
179
35
|
<% end %>
|
|
@@ -185,111 +41,36 @@
|
|
|
185
41
|
<h2 class="card-title">Notifications by Status</h2>
|
|
186
42
|
</div>
|
|
187
43
|
<% if @notifications_by_state.any? %>
|
|
188
|
-
<%
|
|
189
|
-
state_colors = {
|
|
190
|
-
'draft' => '#9ca3af',
|
|
191
|
-
'accepted' => '#B9E0B8',
|
|
192
|
-
'delivered' => '#10b981',
|
|
193
|
-
'opened' => '#78c6cc',
|
|
194
|
-
'clicked' => '#BDD9DB',
|
|
195
|
-
'bounced' => '#D4183D',
|
|
196
|
-
'rejected' => '#D4183D',
|
|
197
|
-
'complained' => '#D4183D',
|
|
198
|
-
'delayed' => '#f97316'
|
|
199
|
-
}
|
|
200
|
-
total = @notifications_by_state.values.sum
|
|
201
|
-
radius = 68
|
|
202
|
-
circumference = 2 * Math::PI * radius
|
|
203
|
-
offset = 0
|
|
204
|
-
%>
|
|
44
|
+
<% segments = donut_segment_data(@notifications_by_state) %>
|
|
205
45
|
<div class="donut-chart-container" id="donutChart">
|
|
206
46
|
<div class="donut-chart-wrapper">
|
|
207
47
|
<svg class="donut-chart" viewBox="0 0 200 200">
|
|
208
|
-
<%
|
|
209
|
-
<%
|
|
210
|
-
percentage = count.to_f / total
|
|
211
|
-
dash_length = circumference * percentage
|
|
212
|
-
# Add small overlap to prevent visual gaps between segments
|
|
213
|
-
overlap = 0.5
|
|
214
|
-
visible_dash = dash_length + overlap
|
|
215
|
-
color = state_colors[state.to_s] || '#6b7280'
|
|
216
|
-
%>
|
|
48
|
+
<% segments.each do |seg| %>
|
|
217
49
|
<circle
|
|
218
50
|
class="donut-segment"
|
|
219
|
-
data-state="<%= state %>"
|
|
220
|
-
cx="100" cy="100" r="<%=
|
|
221
|
-
stroke="<%= color %>"
|
|
222
|
-
stroke-dasharray="<%= visible_dash %> <%=
|
|
223
|
-
stroke-dashoffset="<%=
|
|
51
|
+
data-state="<%= seg[:state] %>"
|
|
52
|
+
cx="100" cy="100" r="<%= Sincerely::ChartHelper::DONUT_RADIUS %>"
|
|
53
|
+
stroke="<%= seg[:color] %>"
|
|
54
|
+
stroke-dasharray="<%= seg[:visible_dash] %> <%= seg[:gap] %>"
|
|
55
|
+
stroke-dashoffset="<%= seg[:offset] %>"
|
|
224
56
|
/>
|
|
225
|
-
<% offset += dash_length %>
|
|
226
57
|
<% end %>
|
|
227
58
|
</svg>
|
|
228
59
|
<div class="donut-center">
|
|
229
|
-
<div class="donut-center-value"><%= number_with_delimiter(
|
|
60
|
+
<div class="donut-center-value"><%= number_with_delimiter(@notifications_by_state.values.sum) %></div>
|
|
230
61
|
<div class="donut-center-label">Total</div>
|
|
231
62
|
</div>
|
|
232
63
|
</div>
|
|
233
64
|
<div class="donut-legend">
|
|
234
|
-
<%
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<span class="donut-legend-
|
|
238
|
-
<span class="donut-legend-
|
|
239
|
-
<span class="donut-legend-count"><%= number_with_delimiter(count) %></span>
|
|
65
|
+
<% segments.each do |seg| %>
|
|
66
|
+
<div class="donut-legend-item" data-state="<%= seg[:state] %>">
|
|
67
|
+
<span class="donut-legend-color" style="background: <%= seg[:color] %>"></span>
|
|
68
|
+
<span class="donut-legend-text"><%= seg[:state].to_s.humanize %></span>
|
|
69
|
+
<span class="donut-legend-count"><%= number_with_delimiter(seg[:count]) %></span>
|
|
240
70
|
</div>
|
|
241
71
|
<% end %>
|
|
242
72
|
</div>
|
|
243
73
|
</div>
|
|
244
|
-
<script>
|
|
245
|
-
(function() {
|
|
246
|
-
const chart = document.getElementById('donutChart');
|
|
247
|
-
if (!chart) return;
|
|
248
|
-
|
|
249
|
-
const segments = chart.querySelectorAll('.donut-segment');
|
|
250
|
-
const legendItems = chart.querySelectorAll('.donut-legend-item');
|
|
251
|
-
|
|
252
|
-
function highlightState(state) {
|
|
253
|
-
segments.forEach(seg => {
|
|
254
|
-
if (seg.dataset.state === state) {
|
|
255
|
-
seg.classList.add('highlighted');
|
|
256
|
-
seg.classList.remove('dimmed');
|
|
257
|
-
} else {
|
|
258
|
-
seg.classList.add('dimmed');
|
|
259
|
-
seg.classList.remove('highlighted');
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
legendItems.forEach(item => {
|
|
263
|
-
if (item.dataset.state === state) {
|
|
264
|
-
item.classList.add('highlighted');
|
|
265
|
-
item.classList.remove('dimmed');
|
|
266
|
-
} else {
|
|
267
|
-
item.classList.add('dimmed');
|
|
268
|
-
item.classList.remove('highlighted');
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function resetHighlight() {
|
|
274
|
-
segments.forEach(seg => {
|
|
275
|
-
seg.classList.remove('highlighted', 'dimmed');
|
|
276
|
-
});
|
|
277
|
-
legendItems.forEach(item => {
|
|
278
|
-
item.classList.remove('highlighted', 'dimmed');
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
legendItems.forEach(item => {
|
|
283
|
-
item.addEventListener('mouseenter', () => highlightState(item.dataset.state));
|
|
284
|
-
item.addEventListener('mouseleave', resetHighlight);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
segments.forEach(seg => {
|
|
288
|
-
seg.addEventListener('mouseenter', () => highlightState(seg.dataset.state));
|
|
289
|
-
seg.addEventListener('mouseleave', resetHighlight);
|
|
290
|
-
});
|
|
291
|
-
})();
|
|
292
|
-
</script>
|
|
293
74
|
<% else %>
|
|
294
75
|
<div class="donut-empty">No notifications in this period</div>
|
|
295
76
|
<% end %>
|
|
@@ -298,7 +79,7 @@
|
|
|
298
79
|
<div class="card">
|
|
299
80
|
<div class="card-header">
|
|
300
81
|
<h2 class="card-title">Recent Notifications</h2>
|
|
301
|
-
<a href="<%=
|
|
82
|
+
<a href="<%= notifications_path %>" class="btn btn-secondary btn-sm">View All</a>
|
|
302
83
|
</div>
|
|
303
84
|
<div class="table-wrapper">
|
|
304
85
|
<table>
|
|
@@ -308,9 +89,9 @@
|
|
|
308
89
|
<tbody>
|
|
309
90
|
<% if @recent_notifications.any? %>
|
|
310
91
|
<% @recent_notifications.each do |notification| %>
|
|
311
|
-
<tr class="clickable-row"
|
|
92
|
+
<tr class="clickable-row" role="link" tabindex="0" data-href="<%= notification_path(notification) %>">
|
|
312
93
|
<td><%= truncate(notification.recipient, length: 30) %></td>
|
|
313
|
-
<td><span class="badge
|
|
94
|
+
<td><span class="badge <%= state_badge_class(notification.delivery_state) %>"><%= notification.delivery_state.humanize %></span></td>
|
|
314
95
|
<td><%= notification.created_at.strftime('%b %d, %H:%M') %></td>
|
|
315
96
|
</tr>
|
|
316
97
|
<% end %>
|
|
@@ -327,7 +108,7 @@
|
|
|
327
108
|
<div class="card">
|
|
328
109
|
<div class="card-header">
|
|
329
110
|
<h2 class="card-title">Recent Delivery Events</h2>
|
|
330
|
-
<a href="<%=
|
|
111
|
+
<a href="<%= delivery_events_path %>" class="btn btn-secondary btn-sm">View All</a>
|
|
331
112
|
</div>
|
|
332
113
|
<div class="table-wrapper">
|
|
333
114
|
<table>
|
|
@@ -336,14 +117,7 @@
|
|
|
336
117
|
</thead>
|
|
337
118
|
<tbody>
|
|
338
119
|
<% if @recent_delivery_events.any? %>
|
|
339
|
-
|
|
340
|
-
<% notification = notification_model.find_by(message_id: event.message_id) %>
|
|
341
|
-
<tr<%= " class=\"clickable-row\" onclick=\"window.location='#{sincerely.notification_path(notification)}'\"".html_safe if notification %>>
|
|
342
|
-
<td><span class="badge badge-<%= event.event_type %>"><%= event.event_type %></span></td>
|
|
343
|
-
<td><%= truncate(event.recipient, length: 25) %></td>
|
|
344
|
-
<td><%= (event.timestamp || event.created_at).strftime('%b %d, %H:%M') %></td>
|
|
345
|
-
</tr>
|
|
346
|
-
<% end %>
|
|
120
|
+
<%= render partial: 'sincerely/shared/event', collection: @recent_delivery_events, locals: { notifications_by_message_id: @notifications_by_message_id, detailed: false, recipient_length: 25 } %>
|
|
347
121
|
<% else %>
|
|
348
122
|
<tr><td colspan="3" class="empty-state">No delivery events</td></tr>
|
|
349
123
|
<% end %>
|
|
@@ -355,7 +129,7 @@
|
|
|
355
129
|
<div class="card">
|
|
356
130
|
<div class="card-header">
|
|
357
131
|
<h2 class="card-title">Recent Engagement Events</h2>
|
|
358
|
-
<a href="<%=
|
|
132
|
+
<a href="<%= engagement_events_path %>" class="btn btn-secondary btn-sm">View All</a>
|
|
359
133
|
</div>
|
|
360
134
|
<div class="table-wrapper">
|
|
361
135
|
<table>
|
|
@@ -364,14 +138,7 @@
|
|
|
364
138
|
</thead>
|
|
365
139
|
<tbody>
|
|
366
140
|
<% if @recent_engagement_events.any? %>
|
|
367
|
-
|
|
368
|
-
<% notification = notification_model.find_by(message_id: event.message_id) %>
|
|
369
|
-
<tr<%= " class=\"clickable-row\" onclick=\"window.location='#{sincerely.notification_path(notification)}'\"".html_safe if notification %>>
|
|
370
|
-
<td><span class="badge badge-<%= event.event_type %>"><%= event.event_type %></span></td>
|
|
371
|
-
<td><%= truncate(event.recipient, length: 25) %></td>
|
|
372
|
-
<td><%= (event.timestamp || event.created_at).strftime('%b %d, %H:%M') %></td>
|
|
373
|
-
</tr>
|
|
374
|
-
<% end %>
|
|
141
|
+
<%= render partial: 'sincerely/shared/event', collection: @recent_engagement_events, locals: { notifications_by_message_id: @notifications_by_message_id, detailed: false, recipient_length: 25 } %>
|
|
375
142
|
<% else %>
|
|
376
143
|
<tr><td colspan="3" class="empty-state">No engagement events</td></tr>
|
|
377
144
|
<% end %>
|
|
@@ -380,3 +147,5 @@
|
|
|
380
147
|
</div>
|
|
381
148
|
</div>
|
|
382
149
|
</div>
|
|
150
|
+
|
|
151
|
+
<%= javascript_include_tag 'sincerely/charts' %>
|
|
@@ -7,45 +7,31 @@
|
|
|
7
7
|
<div class="card-header">
|
|
8
8
|
<h2 class="card-title">Filters</h2>
|
|
9
9
|
</div>
|
|
10
|
-
<form method="get" action="<%=
|
|
10
|
+
<form method="get" action="<%= delivery_events_path %>" class="filters-form">
|
|
11
11
|
<div class="filters-grid">
|
|
12
12
|
<div class="filter-group">
|
|
13
13
|
<label for="event_type">Status</label>
|
|
14
14
|
<select name="event_type" id="event_type">
|
|
15
15
|
<option value="">All statuses</option>
|
|
16
|
-
|
|
17
|
-
<option value="<%= type %>" <%= 'selected' if params[:event_type] == type %>><%= type.to_s.humanize %></option>
|
|
18
|
-
<% end %>
|
|
16
|
+
<%= render partial: 'sincerely/shared/status_option', collection: @event_types, as: :status_value, locals: { param_name: :event_type } %>
|
|
19
17
|
</select>
|
|
20
18
|
</div>
|
|
21
19
|
<div class="filter-group">
|
|
22
20
|
<label for="template_id">Template</label>
|
|
23
21
|
<select name="template_id" id="template_id">
|
|
24
22
|
<option value="">All templates</option>
|
|
25
|
-
|
|
26
|
-
<option value="<%= template.id %>" <%= 'selected' if params[:template_id] == template.id.to_s %>>
|
|
27
|
-
<%= template.name %>
|
|
28
|
-
</option>
|
|
29
|
-
<% end %>
|
|
23
|
+
<%= render partial: 'sincerely/shared/template_option', collection: @templates, as: :template %>
|
|
30
24
|
</select>
|
|
31
25
|
</div>
|
|
32
26
|
<div class="filter-group">
|
|
33
27
|
<label for="recipient">Recipient</label>
|
|
34
28
|
<input type="text" name="recipient" id="recipient" value="<%= params[:recipient] %>" placeholder="Search by recipient...">
|
|
35
29
|
</div>
|
|
36
|
-
|
|
37
|
-
<label for="notification_type">Type</label>
|
|
38
|
-
<select name="notification_type" id="notification_type">
|
|
39
|
-
<option value="">All types</option>
|
|
40
|
-
<option value="email" <%= 'selected' if params[:notification_type] == 'email' %>>Email</option>
|
|
41
|
-
<option value="sms" <%= 'selected' if params[:notification_type] == 'sms' %>>SMS</option>
|
|
42
|
-
<option value="push" <%= 'selected' if params[:notification_type] == 'push' %>>Push</option>
|
|
43
|
-
</select>
|
|
44
|
-
</div>
|
|
30
|
+
<%= render 'sincerely/shared/notification_type_filter' %>
|
|
45
31
|
</div>
|
|
46
32
|
<div class="filter-actions">
|
|
47
33
|
<button type="submit" class="btn btn-primary">Apply Filters</button>
|
|
48
|
-
<a href="<%=
|
|
34
|
+
<a href="<%= delivery_events_path %>" class="btn btn-secondary">Clear</a>
|
|
49
35
|
</div>
|
|
50
36
|
</form>
|
|
51
37
|
</div>
|
|
@@ -66,25 +52,7 @@
|
|
|
66
52
|
</tr>
|
|
67
53
|
</thead>
|
|
68
54
|
<tbody>
|
|
69
|
-
|
|
70
|
-
<% notification = notification_model.find_by(message_id: event.message_id) %>
|
|
71
|
-
<tr<%= " class=\"clickable-row\" onclick=\"window.location='#{sincerely.notification_path(notification)}'\"".html_safe if notification %>>
|
|
72
|
-
<td><span class="badge badge-<%= event.event_type %>"><%= event.event_type %></span></td>
|
|
73
|
-
<td><%= truncate(event.recipient, length: 30) %></td>
|
|
74
|
-
<td><code><%= truncate(event.message_id, length: 20) %></code></td>
|
|
75
|
-
<td>
|
|
76
|
-
<% if event.options.present? %>
|
|
77
|
-
<details onclick="event.stopPropagation()">
|
|
78
|
-
<summary class="btn btn-secondary btn-sm">View</summary>
|
|
79
|
-
<pre class="event-details"><%= JSON.pretty_generate(event.options) rescue event.options %></pre>
|
|
80
|
-
</details>
|
|
81
|
-
<% else %>
|
|
82
|
-
-
|
|
83
|
-
<% end %>
|
|
84
|
-
</td>
|
|
85
|
-
<td><%= (event.timestamp || event.created_at).strftime('%b %d, %Y %H:%M:%S') %></td>
|
|
86
|
-
</tr>
|
|
87
|
-
<% end %>
|
|
55
|
+
<%= render partial: 'sincerely/shared/event', collection: @events, locals: { notifications_by_message_id: @notifications_by_message_id, detailed: true, recipient_length: 30 } %>
|
|
88
56
|
<% if @events.empty? %>
|
|
89
57
|
<tr>
|
|
90
58
|
<td colspan="5" class="empty-state">No delivery events found</td>
|
|
@@ -7,45 +7,31 @@
|
|
|
7
7
|
<div class="card-header">
|
|
8
8
|
<h2 class="card-title">Filters</h2>
|
|
9
9
|
</div>
|
|
10
|
-
<form method="get" action="<%=
|
|
10
|
+
<form method="get" action="<%= engagement_events_path %>" class="filters-form">
|
|
11
11
|
<div class="filters-grid">
|
|
12
12
|
<div class="filter-group">
|
|
13
13
|
<label for="event_type">Status</label>
|
|
14
14
|
<select name="event_type" id="event_type">
|
|
15
15
|
<option value="">All statuses</option>
|
|
16
|
-
|
|
17
|
-
<option value="<%= type %>" <%= 'selected' if params[:event_type] == type %>><%= type.to_s.humanize %></option>
|
|
18
|
-
<% end %>
|
|
16
|
+
<%= render partial: 'sincerely/shared/status_option', collection: @event_types, as: :status_value, locals: { param_name: :event_type } %>
|
|
19
17
|
</select>
|
|
20
18
|
</div>
|
|
21
19
|
<div class="filter-group">
|
|
22
20
|
<label for="template_id">Template</label>
|
|
23
21
|
<select name="template_id" id="template_id">
|
|
24
22
|
<option value="">All templates</option>
|
|
25
|
-
|
|
26
|
-
<option value="<%= template.id %>" <%= 'selected' if params[:template_id] == template.id.to_s %>>
|
|
27
|
-
<%= template.name %>
|
|
28
|
-
</option>
|
|
29
|
-
<% end %>
|
|
23
|
+
<%= render partial: 'sincerely/shared/template_option', collection: @templates, as: :template %>
|
|
30
24
|
</select>
|
|
31
25
|
</div>
|
|
32
26
|
<div class="filter-group">
|
|
33
27
|
<label for="recipient">Recipient</label>
|
|
34
28
|
<input type="text" name="recipient" id="recipient" value="<%= params[:recipient] %>" placeholder="Search by recipient...">
|
|
35
29
|
</div>
|
|
36
|
-
|
|
37
|
-
<label for="notification_type">Type</label>
|
|
38
|
-
<select name="notification_type" id="notification_type">
|
|
39
|
-
<option value="">All types</option>
|
|
40
|
-
<option value="email" <%= 'selected' if params[:notification_type] == 'email' %>>Email</option>
|
|
41
|
-
<option value="sms" <%= 'selected' if params[:notification_type] == 'sms' %>>SMS</option>
|
|
42
|
-
<option value="push" <%= 'selected' if params[:notification_type] == 'push' %>>Push</option>
|
|
43
|
-
</select>
|
|
44
|
-
</div>
|
|
30
|
+
<%= render 'sincerely/shared/notification_type_filter' %>
|
|
45
31
|
</div>
|
|
46
32
|
<div class="filter-actions">
|
|
47
33
|
<button type="submit" class="btn btn-primary">Apply Filters</button>
|
|
48
|
-
<a href="<%=
|
|
34
|
+
<a href="<%= engagement_events_path %>" class="btn btn-secondary">Clear</a>
|
|
49
35
|
</div>
|
|
50
36
|
</form>
|
|
51
37
|
</div>
|
|
@@ -66,25 +52,7 @@
|
|
|
66
52
|
</tr>
|
|
67
53
|
</thead>
|
|
68
54
|
<tbody>
|
|
69
|
-
|
|
70
|
-
<% notification = notification_model.find_by(message_id: event.message_id) %>
|
|
71
|
-
<tr<%= " class=\"clickable-row\" onclick=\"window.location='#{sincerely.notification_path(notification)}'\"".html_safe if notification %>>
|
|
72
|
-
<td><span class="badge badge-<%= event.event_type %>"><%= event.event_type %></span></td>
|
|
73
|
-
<td><%= truncate(event.recipient, length: 30) %></td>
|
|
74
|
-
<td><code><%= truncate(event.message_id, length: 20) %></code></td>
|
|
75
|
-
<td>
|
|
76
|
-
<% if event.options.present? %>
|
|
77
|
-
<details onclick="event.stopPropagation()">
|
|
78
|
-
<summary class="btn btn-secondary btn-sm">View</summary>
|
|
79
|
-
<pre class="event-details"><%= JSON.pretty_generate(event.options) rescue event.options %></pre>
|
|
80
|
-
</details>
|
|
81
|
-
<% else %>
|
|
82
|
-
-
|
|
83
|
-
<% end %>
|
|
84
|
-
</td>
|
|
85
|
-
<td><%= (event.timestamp || event.created_at).strftime('%b %d, %Y %H:%M:%S') %></td>
|
|
86
|
-
</tr>
|
|
87
|
-
<% end %>
|
|
55
|
+
<%= render partial: 'sincerely/shared/event', collection: @events, locals: { notifications_by_message_id: @notifications_by_message_id, detailed: true, recipient_length: 30 } %>
|
|
88
56
|
<% if @events.empty? %>
|
|
89
57
|
<tr>
|
|
90
58
|
<td colspan="5" class="empty-state">No engagement events found</td>
|
|
@@ -7,47 +7,31 @@
|
|
|
7
7
|
<div class="card-header">
|
|
8
8
|
<h2 class="card-title">Filter Notifications</h2>
|
|
9
9
|
</div>
|
|
10
|
-
<form method="get" action="<%=
|
|
10
|
+
<form method="get" action="<%= notifications_path %>" class="filters-form">
|
|
11
11
|
<div class="filters-grid">
|
|
12
12
|
<div class="filter-group">
|
|
13
13
|
<label for="status">Status</label>
|
|
14
14
|
<select name="status" id="status">
|
|
15
15
|
<option value="">All statuses</option>
|
|
16
|
-
|
|
17
|
-
<option value="<%= state %>" <%= 'selected' if params[:status] == state %>>
|
|
18
|
-
<%= state.to_s.humanize %>
|
|
19
|
-
</option>
|
|
20
|
-
<% end %>
|
|
16
|
+
<%= render partial: 'sincerely/shared/status_option', collection: @states, as: :status_value, locals: { param_name: :status } %>
|
|
21
17
|
</select>
|
|
22
18
|
</div>
|
|
23
19
|
<div class="filter-group">
|
|
24
20
|
<label for="template_id">Template</label>
|
|
25
21
|
<select name="template_id" id="template_id">
|
|
26
22
|
<option value="">All templates</option>
|
|
27
|
-
|
|
28
|
-
<option value="<%= template.id %>" <%= 'selected' if params[:template_id] == template.id.to_s %>>
|
|
29
|
-
<%= template.name %>
|
|
30
|
-
</option>
|
|
31
|
-
<% end %>
|
|
23
|
+
<%= render partial: 'sincerely/shared/template_option', collection: @templates, as: :template %>
|
|
32
24
|
</select>
|
|
33
25
|
</div>
|
|
34
26
|
<div class="filter-group">
|
|
35
27
|
<label for="recipient">Recipient</label>
|
|
36
28
|
<input type="text" name="recipient" id="recipient" value="<%= params[:recipient] %>" placeholder="Search by recipient...">
|
|
37
29
|
</div>
|
|
38
|
-
|
|
39
|
-
<label for="notification_type">Type</label>
|
|
40
|
-
<select name="notification_type" id="notification_type">
|
|
41
|
-
<option value="">All types</option>
|
|
42
|
-
<option value="email" <%= 'selected' if params[:notification_type] == 'email' %>>Email</option>
|
|
43
|
-
<option value="sms" <%= 'selected' if params[:notification_type] == 'sms' %>>SMS</option>
|
|
44
|
-
<option value="push" <%= 'selected' if params[:notification_type] == 'push' %>>Push</option>
|
|
45
|
-
</select>
|
|
46
|
-
</div>
|
|
30
|
+
<%= render 'sincerely/shared/notification_type_filter' %>
|
|
47
31
|
</div>
|
|
48
32
|
<div class="filter-actions">
|
|
49
33
|
<button type="submit" class="btn btn-primary">Apply Filters</button>
|
|
50
|
-
<a href="<%=
|
|
34
|
+
<a href="<%= notifications_path %>" class="btn btn-secondary">Clear</a>
|
|
51
35
|
</div>
|
|
52
36
|
</form>
|
|
53
37
|
</div>
|
|
@@ -70,12 +54,12 @@
|
|
|
70
54
|
</thead>
|
|
71
55
|
<tbody>
|
|
72
56
|
<% @notifications.each do |notification| %>
|
|
73
|
-
<tr class="clickable-row"
|
|
57
|
+
<tr class="clickable-row" role="link" tabindex="0" data-href="<%= notification_path(notification) %>">
|
|
74
58
|
<td><%= notification.id %></td>
|
|
75
59
|
<td><%= truncate(notification.recipient, length: 35) %></td>
|
|
76
60
|
<td><%= notification.template&.name || 'N/A' %></td>
|
|
77
61
|
<td><span class="badge"><%= notification.notification_type %></span></td>
|
|
78
|
-
<td><span class="badge
|
|
62
|
+
<td><span class="badge <%= state_badge_class(notification.delivery_state) %>"><%= notification.delivery_state.humanize %></span></td>
|
|
79
63
|
<td><%= notification.sent_at&.strftime('%b %d, %Y %H:%M') || '-' %></td>
|
|
80
64
|
</tr>
|
|
81
65
|
<% end %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="page-header page-header--visible">
|
|
2
2
|
<div class="page-header-top">
|
|
3
3
|
<button onclick="history.back()" class="btn-back" title="Back">
|
|
4
|
-
|
|
4
|
+
<%= inline_svg 'sincerely/chevron_left' %>
|
|
5
5
|
</button>
|
|
6
6
|
<h1 class="page-title">Notification #<%= @notification.id %></h1>
|
|
7
7
|
</div>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<span class="detail-label">Template</span>
|
|
33
33
|
<span class="detail-value">
|
|
34
34
|
<% if @notification.template %>
|
|
35
|
-
<a href="<%=
|
|
35
|
+
<a href="<%= template_path(@notification.template) %>"><%= @notification.template.name %></a>
|
|
36
36
|
<% else %>
|
|
37
37
|
N/A
|
|
38
38
|
<% end %>
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
<div class="timeline">
|
|
80
80
|
<% @timeline.each do |event| %>
|
|
81
81
|
<div class="timeline-item">
|
|
82
|
-
<div class="timeline-marker
|
|
82
|
+
<div class="timeline-marker <%= event_type_badge_class(event[:type]) %>"></div>
|
|
83
83
|
<div class="timeline-content">
|
|
84
84
|
<div class="timeline-header">
|
|
85
|
-
<span class="badge
|
|
85
|
+
<span class="badge <%= event_type_badge_class(event[:type]) %>"><%= event[:type] %></span>
|
|
86
86
|
<span class="timeline-time"><%= event[:timestamp].strftime('%b %d, %Y %H:%M:%S') %></span>
|
|
87
87
|
</div>
|
|
88
88
|
<% if event[:details].present? %>
|