Skip to content

Commit 5800b92

Browse files
committed
feat: implement notification/support system
1 parent 60f03c3 commit 5800b92

7 files changed

Lines changed: 231 additions & 23 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
module V1
5+
class NotificationsController < Api::V1::BaseController
6+
before_action :set_notification, only: %i[show mark_as_read]
7+
8+
# GET /api/v1/notifications
9+
def index
10+
notifications = current_user.notifications.recent
11+
12+
notifications = notifications.unread if params[:unread] == 'true'
13+
notifications = notifications.by_type(params[:type]) if params[:type].present?
14+
15+
result = paginate(notifications)
16+
17+
render_success({
18+
notifications: NotificationSerializer.render_as_hash(result[:data]),
19+
total: result[:pagination][:total_count],
20+
page: result[:pagination][:current_page],
21+
per_page: result[:pagination][:per_page],
22+
total_pages: result[:pagination][:total_pages],
23+
unread_count: current_user.notifications.unread.count
24+
})
25+
end
26+
27+
# GET /api/v1/notifications/:id
28+
def show
29+
render_success({
30+
notification: NotificationSerializer.render_as_hash(@notification)
31+
})
32+
end
33+
34+
# PATCH /api/v1/notifications/:id/mark_as_read
35+
def mark_as_read
36+
@notification.mark_as_read!
37+
38+
render_success({
39+
notification: NotificationSerializer.render_as_hash(@notification)
40+
}, message: 'Notification marked as read')
41+
end
42+
43+
# PATCH /api/v1/notifications/mark_all_as_read
44+
def mark_all_as_read
45+
count = current_user.notifications.unread.count
46+
current_user.notifications.unread.update_all(is_read: true, read_at: Time.current)
47+
48+
render_success({
49+
marked_count: count
50+
}, message: "#{count} notifications marked as read")
51+
end
52+
53+
# GET /api/v1/notifications/unread_count
54+
def unread_count
55+
render_success({
56+
unread_count: current_user.notifications.unread.count
57+
})
58+
end
59+
60+
# DELETE /api/v1/notifications/:id
61+
def destroy
62+
@notification = current_user.notifications.find(params[:id])
63+
@notification.destroy!
64+
65+
render_deleted(message: 'Notification deleted successfully')
66+
rescue ActiveRecord::RecordNotFound
67+
render_not_found('Notification not found')
68+
end
69+
70+
private
71+
72+
def set_notification
73+
@notification = current_user.notifications.find(params[:id])
74+
rescue ActiveRecord::RecordNotFound
75+
render_not_found('Notification not found')
76+
end
77+
end
78+
end
79+
end

app/jobs/support/staff_notification_job.rb

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ def perform(ticket_id, notification_type, message_id = nil)
2323
private
2424

2525
def notify_staff_new_ticket(ticket)
26-
# Notify available support staff
2726
staff_users = User.where(role: %w[support_staff admin])
2827

2928
staff_users.each do |staff|
30-
Rails.logger.info("📧 Notifying staff #{staff.email} about new ticket")
31-
Rails.logger.info(" Ticket ##{ticket.ticket_number}: #{ticket.subject}")
32-
# StaffMailer.new_ticket(ticket, staff).deliver_later
29+
Notification.create!(
30+
user: staff,
31+
title: 'Novo Ticket de Suporte',
32+
message: "Ticket ##{ticket.ticket_number}: #{ticket.subject}",
33+
type: 'info',
34+
link_url: "/support/tickets/#{ticket.id}",
35+
link_type: 'support_ticket',
36+
link_id: ticket.id,
37+
channels: ['in_app']
38+
)
39+
40+
Rails.logger.info("Notification created for staff #{staff.email}")
41+
Rails.logger.info("Ticket ##{ticket.ticket_number}: #{ticket.subject}")
3342
end
3443
end
3544

@@ -38,19 +47,38 @@ def notify_assigned_staff(ticket, message_id)
3847

3948
message = SupportTicketMessage.find(message_id)
4049

41-
Rails.logger.info("📧 Notifying assigned staff #{ticket.assigned_to.email}")
42-
Rails.logger.info(" Ticket ##{ticket.ticket_number}: New message from #{message.user.full_name}")
50+
Notification.create!(
51+
user: ticket.assigned_to,
52+
title: 'Nova Mensagem de Usuario',
53+
message: "Ticket ##{ticket.ticket_number}: Nova mensagem de #{message.user.full_name}",
54+
type: 'info',
55+
link_url: "/support/tickets/#{ticket.id}",
56+
link_type: 'support_ticket',
57+
link_id: ticket.id,
58+
channels: ['in_app']
59+
)
4360

44-
# StaffMailer.new_user_message(ticket, ticket.assigned_to, message).deliver_later
61+
Rails.logger.info("Notification created for assigned staff #{ticket.assigned_to.email}")
62+
Rails.logger.info("Ticket ##{ticket.ticket_number}: New message from #{message.user.full_name}")
4563
end
4664

4765
def notify_all_staff_urgent(ticket)
4866
staff_users = User.where(role: %w[support_staff admin])
4967

5068
staff_users.each do |staff|
51-
Rails.logger.warn("⚠️ URGENT: Notifying #{staff.email}")
52-
Rails.logger.warn(" Ticket ##{ticket.ticket_number}: #{ticket.subject}")
53-
# StaffMailer.urgent_ticket(ticket, staff).deliver_later
69+
Notification.create!(
70+
user: staff,
71+
title: 'URGENTE: Ticket Prioritario',
72+
message: "Ticket ##{ticket.ticket_number}: #{ticket.subject}",
73+
type: 'error',
74+
link_url: "/support/tickets/#{ticket.id}",
75+
link_type: 'support_ticket',
76+
link_id: ticket.id,
77+
channels: ['in_app']
78+
)
79+
80+
Rails.logger.warn("URGENT: Notification created for #{staff.email}")
81+
Rails.logger.warn("Ticket ##{ticket.ticket_number}: #{ticket.subject}")
5482
end
5583
end
5684
end

app/jobs/support/ticket_notification_job.rb

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,69 @@ def perform(ticket_id, notification_type, message_id = nil)
2626
private
2727

2828
def send_ticket_created_email(ticket, user)
29-
# TODO: Replace with actual mailer
30-
Rails.logger.info("📧 Sending ticket created email to #{user.email}")
31-
Rails.logger.info(" Ticket ##{ticket.ticket_number}: #{ticket.subject}")
29+
Notification.create!(
30+
user: user,
31+
title: 'Ticket Criado',
32+
message: "Seu ticket ##{ticket.ticket_number} foi criado com sucesso: #{ticket.subject}",
33+
type: 'info',
34+
link_url: "/support/tickets/#{ticket.id}",
35+
link_type: 'support_ticket',
36+
link_id: ticket.id,
37+
channels: ['in_app']
38+
)
3239

33-
# SupportMailer.ticket_created(ticket, user).deliver_later
40+
Rails.logger.info("Notification created for #{user.email}")
41+
Rails.logger.info("Ticket ##{ticket.ticket_number}: #{ticket.subject}")
3442
end
3543

3644
def send_new_message_email(ticket, user, message_id)
3745
message = SupportTicketMessage.find(message_id)
3846

39-
Rails.logger.info("📧 Sending new message notification to #{user.email}")
40-
Rails.logger.info(" Ticket ##{ticket.ticket_number}: New response from #{message.user.full_name}")
47+
Notification.create!(
48+
user: user,
49+
title: 'Nova Mensagem no Ticket',
50+
message: "Ticket ##{ticket.ticket_number}: Nova resposta de #{message.user.full_name}",
51+
type: 'info',
52+
link_url: "/support/tickets/#{ticket.id}",
53+
link_type: 'support_ticket',
54+
link_id: ticket.id,
55+
channels: ['in_app']
56+
)
4157

42-
# SupportMailer.new_message(ticket, user, message).deliver_later
58+
Rails.logger.info("Notification created for #{user.email}")
59+
Rails.logger.info("Ticket ##{ticket.ticket_number}: New response from #{message.user.full_name}")
4360
end
4461

4562
def send_status_changed_email(ticket, user)
46-
Rails.logger.info("📧 Sending status change notification to #{user.email}")
47-
Rails.logger.info(" Ticket ##{ticket.ticket_number}: Status changed to #{ticket.status}")
63+
Notification.create!(
64+
user: user,
65+
title: 'Status do Ticket Alterado',
66+
message: "Ticket ##{ticket.ticket_number}: Status alterado para #{ticket.status}",
67+
type: 'info',
68+
link_url: "/support/tickets/#{ticket.id}",
69+
link_type: 'support_ticket',
70+
link_id: ticket.id,
71+
channels: ['in_app']
72+
)
4873

49-
# SupportMailer.status_changed(ticket, user).deliver_later
74+
Rails.logger.info("Notification created for #{user.email}")
75+
Rails.logger.info("Ticket ##{ticket.ticket_number}: Status changed to #{ticket.status}")
5076
end
5177

5278
def send_ticket_resolved_email(ticket, user)
53-
Rails.logger.info("📧 Sending resolution notification to #{user.email}")
54-
Rails.logger.info(" Ticket ##{ticket.ticket_number}: Your ticket has been resolved")
79+
Notification.create!(
80+
user: user,
81+
title: 'Ticket Resolvido',
82+
message: "Ticket ##{ticket.ticket_number}: Seu ticket foi resolvido",
83+
type: 'success',
84+
link_url: "/support/tickets/#{ticket.id}",
85+
link_type: 'support_ticket',
86+
link_id: ticket.id,
87+
channels: ['in_app']
88+
)
5589

56-
# SupportMailer.ticket_resolved(ticket, user).deliver_later
90+
Rails.logger.info("Notification created for #{user.email}")
91+
Rails.logger.info("Ticket ##{ticket.ticket_number}: Your ticket has been resolved")
5792
end
5893
end
5994
end

app/models/notification.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
# @example Mark notification as read
2525
# notification.mark_as_read!
2626
class Notification < ApplicationRecord
27+
self.inheritance_column = :_type_disabled
28+
2729
# Associations
2830
belongs_to :user
2931

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
class NotificationSerializer < Blueprinter::Base
4+
identifier :id
5+
6+
fields :title, :message, :type
7+
fields :link_url, :link_type, :link_id
8+
fields :is_read, :read_at
9+
fields :channels, :email_sent, :discord_sent
10+
fields :metadata
11+
fields :created_at, :updated_at
12+
13+
field :time_ago do |notification|
14+
time_diff = Time.current - notification.created_at
15+
16+
case time_diff
17+
when 0..59
18+
"#{time_diff.to_i} seconds ago"
19+
when 60..3599
20+
"#{(time_diff / 60).to_i} minutes ago"
21+
when 3600..86_399
22+
"#{(time_diff / 3600).to_i} hours ago"
23+
when 86_400..604_799
24+
"#{(time_diff / 86_400).to_i} days ago"
25+
else
26+
notification.created_at.strftime('%d/%m/%Y')
27+
end
28+
end
29+
30+
association :user, blueprint: UserSerializer
31+
end

config/routes.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
get 'me', to: 'auth#me'
3232
end
3333

34+
# Notifications
35+
resources :notifications, only: %i[index show destroy] do
36+
member do
37+
patch :mark_as_read, to: 'notifications#mark_as_read'
38+
end
39+
collection do
40+
patch :mark_all_as_read, to: 'notifications#mark_all_as_read'
41+
get :unread_count, to: 'notifications#unread_count'
42+
end
43+
end
44+
3445
# Dashboard
3546
resources :dashboard, only: [:index] do
3647
collection do

db/schema.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,22 @@
1212

1313
ActiveRecord::Schema[7.2].define(version: 2026_02_12_000000) do
1414
create_schema "auth"
15+
create_schema "extensions"
16+
create_schema "graphql"
17+
create_schema "graphql_public"
18+
create_schema "pgbouncer"
19+
create_schema "realtime"
20+
create_schema "storage"
21+
create_schema "supabase_migrations"
22+
create_schema "vault"
1523

1624
# These are extensions that must be enabled in order to support this database
25+
enable_extension "pg_graphql"
26+
enable_extension "pg_stat_statements"
1727
enable_extension "pgcrypto"
1828
enable_extension "plpgsql"
29+
enable_extension "supabase_vault"
30+
enable_extension "uuid-ossp"
1931

2032
create_table "audit_logs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
2133
t.uuid "organization_id", null: false
@@ -166,6 +178,8 @@
166178
t.index ["match_type"], name: "index_matches_on_match_type"
167179
t.index ["organization_id", "created_at"], name: "idx_matches_org_created"
168180
t.index ["organization_id", "game_start", "victory"], name: "idx_matches_org_game_start_victory", comment: "Otimiza queries de winrate por período"
181+
t.index ["organization_id", "game_start"], name: "idx_matches_org_game_start"
182+
t.index ["organization_id", "victory"], name: "idx_matches_org_victory"
169183
t.index ["organization_id"], name: "index_matches_on_organization_id"
170184
t.index ["riot_match_id"], name: "index_matches_on_riot_match_id", unique: true
171185
t.index ["victory"], name: "index_matches_on_victory"
@@ -302,6 +316,7 @@
302316
t.datetime "updated_at", null: false
303317
t.index ["champion"], name: "index_player_match_stats_on_champion"
304318
t.index ["match_id", "player_id"], name: "idx_player_stats_match_player_agg", comment: "Otimiza agregações de estatísticas (SUM kills/deaths/assists)"
319+
t.index ["match_id"], name: "idx_player_stats_match"
305320
t.index ["player_id", "match_id"], name: "index_player_match_stats_on_player_id_and_match_id", unique: true
306321
t.index ["player_id"], name: "index_player_match_stats_on_player_id"
307322
end
@@ -365,6 +380,7 @@
365380
t.index ["organization_id", "deleted_at"], name: "idx_players_org_deleted_active", where: "(deleted_at IS NULL)", comment: "Índice parcial para COUNT de players ativos"
366381
t.index ["organization_id", "last_sync_at"], name: "idx_players_org_last_sync"
367382
t.index ["organization_id", "role"], name: "index_players_on_org_and_role"
383+
t.index ["organization_id", "status"], name: "idx_players_org_status"
368384
t.index ["organization_id", "sync_status"], name: "idx_players_org_sync_status"
369385
t.index ["organization_id"], name: "index_players_on_organization_id"
370386
t.index ["player_access_enabled"], name: "index_players_on_player_access_enabled", comment: "Quick lookup for players with access enabled"
@@ -403,12 +419,15 @@
403419
t.jsonb "metadata", default: {}
404420
t.datetime "created_at", null: false
405421
t.datetime "updated_at", null: false
422+
t.uuid "scrim_id"
406423
t.index ["created_by_id"], name: "index_schedules_on_created_by_id"
407424
t.index ["event_type"], name: "index_schedules_on_event_type"
408425
t.index ["match_id"], name: "index_schedules_on_match_id"
409426
t.index ["organization_id", "event_type"], name: "idx_schedules_org_event_type"
410427
t.index ["organization_id", "start_time", "event_type"], name: "idx_schedules_org_time_type", comment: "Otimiza queries de próximos eventos"
428+
t.index ["organization_id", "start_time"], name: "idx_schedules_org_time"
411429
t.index ["organization_id"], name: "index_schedules_on_organization_id"
430+
t.index ["scrim_id"], name: "index_schedules_on_scrim_id"
412431
t.index ["start_time"], name: "index_schedules_on_start_time"
413432
t.index ["status"], name: "index_schedules_on_status"
414433
end
@@ -634,9 +653,11 @@
634653
t.datetime "last_login_at", precision: nil
635654
t.datetime "created_at", null: false
636655
t.datetime "updated_at", null: false
656+
t.string "supabase_uid"
637657
t.index ["email"], name: "index_users_on_email", unique: true
638658
t.index ["organization_id"], name: "index_users_on_organization_id"
639659
t.index ["role"], name: "index_users_on_role"
660+
t.index ["supabase_uid"], name: "index_users_on_supabase_uid"
640661
end
641662

642663
create_table "vod_reviews", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
@@ -704,6 +725,7 @@
704725
add_foreign_key "players", "organizations", column: "previous_organization_id", on_delete: :nullify
705726
add_foreign_key "schedules", "matches"
706727
add_foreign_key "schedules", "organizations"
728+
add_foreign_key "schedules", "scrims", on_delete: :cascade
707729
add_foreign_key "schedules", "users", column: "created_by_id"
708730
add_foreign_key "scouting_watchlists", "organizations"
709731
add_foreign_key "scouting_watchlists", "scouting_targets"

0 commit comments

Comments
 (0)