analytics service + inapp notification websocket fix
This commit is contained in:
parent
7d626d059f
commit
aa6194013c
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
// "context"
|
// "context"
|
||||||
_ "Yimaru-Backend/docs"
|
_ "Yimaru-Backend/docs"
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
customlogger "Yimaru-Backend/internal/logger"
|
customlogger "Yimaru-Backend/internal/logger"
|
||||||
|
|
@ -81,6 +82,7 @@ func main() {
|
||||||
zap.ReplaceGlobals(domain.MongoDBLogger)
|
zap.ReplaceGlobals(domain.MongoDBLogger)
|
||||||
|
|
||||||
store := repository.NewStore(db)
|
store := repository.NewStore(db)
|
||||||
|
analyticsDB := dbgen.New(db)
|
||||||
v := customvalidator.NewCustomValidator(validator.New())
|
v := customvalidator.NewCustomValidator(validator.New())
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
|
|
@ -426,6 +428,7 @@ func main() {
|
||||||
recommendationSvc,
|
recommendationSvc,
|
||||||
cfg,
|
cfg,
|
||||||
domain.MongoDBLogger,
|
domain.MongoDBLogger,
|
||||||
|
analyticsDB,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.Info("Starting server", "port", cfg.Port)
|
logger.Info("Starting server", "port", cfg.Port)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Remove the receiver_type column.
|
||||||
|
ALTER TABLE notifications
|
||||||
|
DROP COLUMN IF EXISTS receiver_type;
|
||||||
|
|
||||||
|
-- Re-add the foreign key constraint on user_id referencing users(id).
|
||||||
|
ALTER TABLE notifications
|
||||||
|
ADD CONSTRAINT notifications_user_id_fkey
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
9
db/migrations/000016_notifications_receiver_type.up.sql
Normal file
9
db/migrations/000016_notifications_receiver_type.up.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
-- Drop the foreign key constraint on user_id so notifications can reference
|
||||||
|
-- either users or team_members depending on receiver_type.
|
||||||
|
ALTER TABLE notifications
|
||||||
|
DROP CONSTRAINT IF EXISTS notifications_user_id_fkey;
|
||||||
|
|
||||||
|
-- Add a receiver_type column to distinguish between user and team_member recipients.
|
||||||
|
ALTER TABLE notifications
|
||||||
|
ADD COLUMN IF NOT EXISTS receiver_type TEXT NOT NULL DEFAULT 'user'
|
||||||
|
CHECK (receiver_type IN ('user', 'team_member'));
|
||||||
276
db/query/analytics.sql
Normal file
276
db/query/analytics.sql
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
-- =====================
|
||||||
|
-- Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- User Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsUsersSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||||
|
FROM users;
|
||||||
|
|
||||||
|
-- name: AnalyticsUsersByRole :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(role, 'unknown') AS role,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY role
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsUsersByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsUsersByAgeGroup :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(age_group, 'unknown') AS age_group,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY age_group
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsUsersByKnowledgeLevel :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(knowledge_level, 'unknown') AS knowledge_level,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY knowledge_level
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsUsersByRegion :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(region, 'unknown') AS region,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY region
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsUserRegistrationsLast30Days :many
|
||||||
|
SELECT
|
||||||
|
d.date,
|
||||||
|
COUNT(u.id)::bigint AS count
|
||||||
|
FROM generate_series(
|
||||||
|
CURRENT_DATE - INTERVAL '29 days',
|
||||||
|
CURRENT_DATE,
|
||||||
|
INTERVAL '1 day'
|
||||||
|
) AS d(date)
|
||||||
|
LEFT JOIN users u ON u.created_at::date = d.date
|
||||||
|
GROUP BY d.date
|
||||||
|
ORDER BY d.date;
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- Subscription Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsSubscriptionsSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'ACTIVE')::bigint AS active,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||||
|
FROM user_subscriptions;
|
||||||
|
|
||||||
|
-- name: AnalyticsSubscriptionsByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM user_subscriptions
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsRevenueByPlan :many
|
||||||
|
SELECT
|
||||||
|
sp.name AS plan_name,
|
||||||
|
sp.currency,
|
||||||
|
COUNT(p.id)::bigint AS total_payments,
|
||||||
|
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||||
|
FROM payments p
|
||||||
|
JOIN subscription_plans sp ON sp.id = p.plan_id
|
||||||
|
WHERE p.status = 'SUCCESS'
|
||||||
|
GROUP BY sp.name, sp.currency
|
||||||
|
ORDER BY total_revenue DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsNewSubscriptionsLast30Days :many
|
||||||
|
SELECT
|
||||||
|
d.date,
|
||||||
|
COUNT(us.id)::bigint AS count
|
||||||
|
FROM generate_series(
|
||||||
|
CURRENT_DATE - INTERVAL '29 days',
|
||||||
|
CURRENT_DATE,
|
||||||
|
INTERVAL '1 day'
|
||||||
|
) AS d(date)
|
||||||
|
LEFT JOIN user_subscriptions us ON us.created_at::date = d.date
|
||||||
|
GROUP BY d.date
|
||||||
|
ORDER BY d.date;
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- Payment Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsPaymentsSummary :one
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||||
|
COALESCE(AVG(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||||
|
COUNT(*)::bigint AS total_payments,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'SUCCESS')::bigint AS successful_payments
|
||||||
|
FROM payments;
|
||||||
|
|
||||||
|
-- name: AnalyticsPaymentsByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count,
|
||||||
|
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||||
|
FROM payments
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsPaymentsByMethod :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(payment_method, 'unknown') AS payment_method,
|
||||||
|
COUNT(*)::bigint AS count,
|
||||||
|
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||||
|
FROM payments
|
||||||
|
WHERE status = 'SUCCESS'
|
||||||
|
GROUP BY payment_method
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsRevenueLast30Days :many
|
||||||
|
SELECT
|
||||||
|
d.date,
|
||||||
|
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||||
|
FROM generate_series(
|
||||||
|
CURRENT_DATE - INTERVAL '29 days',
|
||||||
|
CURRENT_DATE,
|
||||||
|
INTERVAL '1 day'
|
||||||
|
) AS d(date)
|
||||||
|
LEFT JOIN payments p ON p.paid_at::date = d.date AND p.status = 'SUCCESS'
|
||||||
|
GROUP BY d.date
|
||||||
|
ORDER BY d.date;
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- Course Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsCourseCounts :one
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*)::bigint FROM course_categories) AS total_categories,
|
||||||
|
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
||||||
|
(SELECT COUNT(*)::bigint FROM sub_courses) AS total_sub_courses,
|
||||||
|
(SELECT COUNT(*)::bigint FROM sub_course_videos) AS total_videos;
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- Content Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsQuestionsCounts :one
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*)::bigint FROM questions) AS total_questions,
|
||||||
|
(SELECT COUNT(*)::bigint FROM question_sets) AS total_question_sets;
|
||||||
|
|
||||||
|
-- name: AnalyticsQuestionsByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(question_type, 'unknown') AS question_type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM questions
|
||||||
|
GROUP BY question_type
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsQuestionSetsByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(set_type, 'unknown') AS set_type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM question_sets
|
||||||
|
GROUP BY set_type
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- Notification Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsNotificationsSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE is_read = TRUE)::bigint AS read,
|
||||||
|
COUNT(*) FILTER (WHERE is_read = FALSE)::bigint AS unread
|
||||||
|
FROM notifications;
|
||||||
|
|
||||||
|
-- name: AnalyticsNotificationsByChannel :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(channel, 'unknown') AS channel,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM notifications
|
||||||
|
GROUP BY channel
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsNotificationsByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(type, 'unknown') AS type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM notifications
|
||||||
|
GROUP BY type
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- Issue Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsIssuesSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'resolved')::bigint AS resolved,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(*) > 0 THEN (COUNT(*) FILTER (WHERE status = 'resolved')::float8 / COUNT(*)::float8)
|
||||||
|
ELSE 0::float8
|
||||||
|
END AS resolution_rate
|
||||||
|
FROM reported_issues;
|
||||||
|
|
||||||
|
-- name: AnalyticsIssuesByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM reported_issues
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsIssuesByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(issue_type, 'unknown') AS issue_type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM reported_issues
|
||||||
|
GROUP BY issue_type
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- =====================
|
||||||
|
-- Team Analytics
|
||||||
|
-- =====================
|
||||||
|
|
||||||
|
-- name: AnalyticsTeamSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total_members
|
||||||
|
FROM team_members;
|
||||||
|
|
||||||
|
-- name: AnalyticsTeamByRole :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(team_role, 'unknown') AS team_role,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM team_members
|
||||||
|
GROUP BY team_role
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
||||||
|
-- name: AnalyticsTeamByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM team_members
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC;
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
-- name: CreateNotification :one
|
-- name: CreateNotification :one
|
||||||
INSERT INTO notifications (
|
INSERT INTO notifications (
|
||||||
user_id,
|
user_id,
|
||||||
|
receiver_type,
|
||||||
type,
|
type,
|
||||||
level,
|
level,
|
||||||
channel,
|
channel,
|
||||||
|
|
@ -15,7 +16,8 @@ VALUES (
|
||||||
$4,
|
$4,
|
||||||
$5,
|
$5,
|
||||||
$6,
|
$6,
|
||||||
$7
|
$7,
|
||||||
|
$8
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ services:
|
||||||
image: dpage/pgadmin4:latest
|
image: dpage/pgadmin4:latest
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "5050:80"
|
- "5030:80"
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: admin@local.dev
|
PGADMIN_DEFAULT_EMAIL: admin@local.dev
|
||||||
PGADMIN_DEFAULT_PASSWORD: admin
|
PGADMIN_DEFAULT_PASSWORD: admin
|
||||||
|
|
|
||||||
947
gen/db/analytics.sql.go
Normal file
947
gen/db/analytics.sql.go
Normal file
|
|
@ -0,0 +1,947 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: analytics.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*)::bigint FROM course_categories) AS total_categories,
|
||||||
|
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
||||||
|
(SELECT COUNT(*)::bigint FROM sub_courses) AS total_sub_courses,
|
||||||
|
(SELECT COUNT(*)::bigint FROM sub_course_videos) AS total_videos
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsCourseCountsRow struct {
|
||||||
|
TotalCategories int64 `json:"total_categories"`
|
||||||
|
TotalCourses int64 `json:"total_courses"`
|
||||||
|
TotalSubCourses int64 `json:"total_sub_courses"`
|
||||||
|
TotalVideos int64 `json:"total_videos"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Course Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsCourseCounts(ctx context.Context) (AnalyticsCourseCountsRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsCourseCounts)
|
||||||
|
var i AnalyticsCourseCountsRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TotalCategories,
|
||||||
|
&i.TotalCourses,
|
||||||
|
&i.TotalSubCourses,
|
||||||
|
&i.TotalVideos,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsIssuesByStatus = `-- name: AnalyticsIssuesByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM reported_issues
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsIssuesByStatusRow struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsIssuesByStatus(ctx context.Context) ([]AnalyticsIssuesByStatusRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsIssuesByStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsIssuesByStatusRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsIssuesByStatusRow
|
||||||
|
if err := rows.Scan(&i.Status, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsIssuesByType = `-- name: AnalyticsIssuesByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(issue_type, 'unknown') AS issue_type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM reported_issues
|
||||||
|
GROUP BY issue_type
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsIssuesByTypeRow struct {
|
||||||
|
IssueType string `json:"issue_type"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsIssuesByType(ctx context.Context) ([]AnalyticsIssuesByTypeRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsIssuesByType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsIssuesByTypeRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsIssuesByTypeRow
|
||||||
|
if err := rows.Scan(&i.IssueType, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsIssuesSummary = `-- name: AnalyticsIssuesSummary :one
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'resolved')::bigint AS resolved,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(*) > 0 THEN (COUNT(*) FILTER (WHERE status = 'resolved')::float8 / COUNT(*)::float8)
|
||||||
|
ELSE 0::float8
|
||||||
|
END AS resolution_rate
|
||||||
|
FROM reported_issues
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsIssuesSummaryRow struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Resolved int64 `json:"resolved"`
|
||||||
|
ResolutionRate float64 `json:"resolution_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Issue Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsIssuesSummary(ctx context.Context) (AnalyticsIssuesSummaryRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsIssuesSummary)
|
||||||
|
var i AnalyticsIssuesSummaryRow
|
||||||
|
err := row.Scan(&i.Total, &i.Resolved, &i.ResolutionRate)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsNewSubscriptionsLast30Days = `-- name: AnalyticsNewSubscriptionsLast30Days :many
|
||||||
|
SELECT
|
||||||
|
d.date,
|
||||||
|
COUNT(us.id)::bigint AS count
|
||||||
|
FROM generate_series(
|
||||||
|
CURRENT_DATE - INTERVAL '29 days',
|
||||||
|
CURRENT_DATE,
|
||||||
|
INTERVAL '1 day'
|
||||||
|
) AS d(date)
|
||||||
|
LEFT JOIN user_subscriptions us ON us.created_at::date = d.date
|
||||||
|
GROUP BY d.date
|
||||||
|
ORDER BY d.date
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsNewSubscriptionsLast30DaysRow struct {
|
||||||
|
Date interface{} `json:"date"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsNewSubscriptionsLast30Days(ctx context.Context) ([]AnalyticsNewSubscriptionsLast30DaysRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsNewSubscriptionsLast30Days)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsNewSubscriptionsLast30DaysRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsNewSubscriptionsLast30DaysRow
|
||||||
|
if err := rows.Scan(&i.Date, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsNotificationsByChannel = `-- name: AnalyticsNotificationsByChannel :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(channel, 'unknown') AS channel,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM notifications
|
||||||
|
GROUP BY channel
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsNotificationsByChannelRow struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsNotificationsByChannel(ctx context.Context) ([]AnalyticsNotificationsByChannelRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsNotificationsByChannel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsNotificationsByChannelRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsNotificationsByChannelRow
|
||||||
|
if err := rows.Scan(&i.Channel, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsNotificationsByType = `-- name: AnalyticsNotificationsByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(type, 'unknown') AS type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM notifications
|
||||||
|
GROUP BY type
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsNotificationsByTypeRow struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsNotificationsByType(ctx context.Context) ([]AnalyticsNotificationsByTypeRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsNotificationsByType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsNotificationsByTypeRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsNotificationsByTypeRow
|
||||||
|
if err := rows.Scan(&i.Type, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsNotificationsSummary = `-- name: AnalyticsNotificationsSummary :one
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE is_read = TRUE)::bigint AS read,
|
||||||
|
COUNT(*) FILTER (WHERE is_read = FALSE)::bigint AS unread
|
||||||
|
FROM notifications
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsNotificationsSummaryRow struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Read int64 `json:"read"`
|
||||||
|
Unread int64 `json:"unread"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Notification Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsNotificationsSummary(ctx context.Context) (AnalyticsNotificationsSummaryRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsNotificationsSummary)
|
||||||
|
var i AnalyticsNotificationsSummaryRow
|
||||||
|
err := row.Scan(&i.Total, &i.Read, &i.Unread)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsPaymentsByMethod = `-- name: AnalyticsPaymentsByMethod :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(payment_method, 'unknown') AS payment_method,
|
||||||
|
COUNT(*)::bigint AS count,
|
||||||
|
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||||
|
FROM payments
|
||||||
|
WHERE status = 'SUCCESS'
|
||||||
|
GROUP BY payment_method
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsPaymentsByMethodRow struct {
|
||||||
|
PaymentMethod string `json:"payment_method"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsPaymentsByMethod(ctx context.Context) ([]AnalyticsPaymentsByMethodRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsPaymentsByMethod)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsPaymentsByMethodRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsPaymentsByMethodRow
|
||||||
|
if err := rows.Scan(&i.PaymentMethod, &i.Count, &i.TotalAmount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsPaymentsByStatus = `-- name: AnalyticsPaymentsByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count,
|
||||||
|
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||||
|
FROM payments
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsPaymentsByStatusRow struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsPaymentsByStatus(ctx context.Context) ([]AnalyticsPaymentsByStatusRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsPaymentsByStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsPaymentsByStatusRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsPaymentsByStatusRow
|
||||||
|
if err := rows.Scan(&i.Status, &i.Count, &i.TotalAmount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsPaymentsSummary = `-- name: AnalyticsPaymentsSummary :one
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||||
|
COALESCE(AVG(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||||
|
COUNT(*)::bigint AS total_payments,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'SUCCESS')::bigint AS successful_payments
|
||||||
|
FROM payments
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsPaymentsSummaryRow struct {
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
AvgValue float64 `json:"avg_value"`
|
||||||
|
TotalPayments int64 `json:"total_payments"`
|
||||||
|
SuccessfulPayments int64 `json:"successful_payments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Payment Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsPaymentsSummary(ctx context.Context) (AnalyticsPaymentsSummaryRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsPaymentsSummary)
|
||||||
|
var i AnalyticsPaymentsSummaryRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TotalRevenue,
|
||||||
|
&i.AvgValue,
|
||||||
|
&i.TotalPayments,
|
||||||
|
&i.SuccessfulPayments,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsQuestionSetsByType = `-- name: AnalyticsQuestionSetsByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(set_type, 'unknown') AS set_type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM question_sets
|
||||||
|
GROUP BY set_type
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsQuestionSetsByTypeRow struct {
|
||||||
|
SetType string `json:"set_type"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsQuestionSetsByType(ctx context.Context) ([]AnalyticsQuestionSetsByTypeRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsQuestionSetsByType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsQuestionSetsByTypeRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsQuestionSetsByTypeRow
|
||||||
|
if err := rows.Scan(&i.SetType, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsQuestionsByType = `-- name: AnalyticsQuestionsByType :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(question_type, 'unknown') AS question_type,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM questions
|
||||||
|
GROUP BY question_type
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsQuestionsByTypeRow struct {
|
||||||
|
QuestionType string `json:"question_type"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsQuestionsByType(ctx context.Context) ([]AnalyticsQuestionsByTypeRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsQuestionsByType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsQuestionsByTypeRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsQuestionsByTypeRow
|
||||||
|
if err := rows.Scan(&i.QuestionType, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsQuestionsCounts = `-- name: AnalyticsQuestionsCounts :one
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*)::bigint FROM questions) AS total_questions,
|
||||||
|
(SELECT COUNT(*)::bigint FROM question_sets) AS total_question_sets
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsQuestionsCountsRow struct {
|
||||||
|
TotalQuestions int64 `json:"total_questions"`
|
||||||
|
TotalQuestionSets int64 `json:"total_question_sets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Content Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsQuestionsCounts(ctx context.Context) (AnalyticsQuestionsCountsRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsQuestionsCounts)
|
||||||
|
var i AnalyticsQuestionsCountsRow
|
||||||
|
err := row.Scan(&i.TotalQuestions, &i.TotalQuestionSets)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsRevenueByPlan = `-- name: AnalyticsRevenueByPlan :many
|
||||||
|
SELECT
|
||||||
|
sp.name AS plan_name,
|
||||||
|
sp.currency,
|
||||||
|
COUNT(p.id)::bigint AS total_payments,
|
||||||
|
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||||
|
FROM payments p
|
||||||
|
JOIN subscription_plans sp ON sp.id = p.plan_id
|
||||||
|
WHERE p.status = 'SUCCESS'
|
||||||
|
GROUP BY sp.name, sp.currency
|
||||||
|
ORDER BY total_revenue DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsRevenueByPlanRow struct {
|
||||||
|
PlanName string `json:"plan_name"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
TotalPayments int64 `json:"total_payments"`
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsRevenueByPlan(ctx context.Context) ([]AnalyticsRevenueByPlanRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsRevenueByPlan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsRevenueByPlanRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsRevenueByPlanRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.PlanName,
|
||||||
|
&i.Currency,
|
||||||
|
&i.TotalPayments,
|
||||||
|
&i.TotalRevenue,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsRevenueLast30Days = `-- name: AnalyticsRevenueLast30Days :many
|
||||||
|
SELECT
|
||||||
|
d.date,
|
||||||
|
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||||
|
FROM generate_series(
|
||||||
|
CURRENT_DATE - INTERVAL '29 days',
|
||||||
|
CURRENT_DATE,
|
||||||
|
INTERVAL '1 day'
|
||||||
|
) AS d(date)
|
||||||
|
LEFT JOIN payments p ON p.paid_at::date = d.date AND p.status = 'SUCCESS'
|
||||||
|
GROUP BY d.date
|
||||||
|
ORDER BY d.date
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsRevenueLast30DaysRow struct {
|
||||||
|
Date interface{} `json:"date"`
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsRevenueLast30Days(ctx context.Context) ([]AnalyticsRevenueLast30DaysRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsRevenueLast30Days)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsRevenueLast30DaysRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsRevenueLast30DaysRow
|
||||||
|
if err := rows.Scan(&i.Date, &i.TotalRevenue); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsSubscriptionsByStatus = `-- name: AnalyticsSubscriptionsByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM user_subscriptions
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsSubscriptionsByStatusRow struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsSubscriptionsByStatus(ctx context.Context) ([]AnalyticsSubscriptionsByStatusRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsSubscriptionsByStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsSubscriptionsByStatusRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsSubscriptionsByStatusRow
|
||||||
|
if err := rows.Scan(&i.Status, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsSubscriptionsSummary = `-- name: AnalyticsSubscriptionsSummary :one
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'ACTIVE')::bigint AS active,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||||
|
FROM user_subscriptions
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsSubscriptionsSummaryRow struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Active int64 `json:"active"`
|
||||||
|
NewToday int64 `json:"new_today"`
|
||||||
|
NewThisWeek int64 `json:"new_this_week"`
|
||||||
|
NewThisMonth int64 `json:"new_this_month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Subscription Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsSubscriptionsSummary(ctx context.Context) (AnalyticsSubscriptionsSummaryRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsSubscriptionsSummary)
|
||||||
|
var i AnalyticsSubscriptionsSummaryRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Total,
|
||||||
|
&i.Active,
|
||||||
|
&i.NewToday,
|
||||||
|
&i.NewThisWeek,
|
||||||
|
&i.NewThisMonth,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsTeamByRole = `-- name: AnalyticsTeamByRole :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(team_role, 'unknown') AS team_role,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM team_members
|
||||||
|
GROUP BY team_role
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsTeamByRoleRow struct {
|
||||||
|
TeamRole string `json:"team_role"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsTeamByRole(ctx context.Context) ([]AnalyticsTeamByRoleRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsTeamByRole)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsTeamByRoleRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsTeamByRoleRow
|
||||||
|
if err := rows.Scan(&i.TeamRole, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsTeamByStatus = `-- name: AnalyticsTeamByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM team_members
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsTeamByStatusRow struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsTeamByStatus(ctx context.Context) ([]AnalyticsTeamByStatusRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsTeamByStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsTeamByStatusRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsTeamByStatusRow
|
||||||
|
if err := rows.Scan(&i.Status, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsTeamSummary = `-- name: AnalyticsTeamSummary :one
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total_members
|
||||||
|
FROM team_members
|
||||||
|
`
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Team Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsTeamSummary(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsTeamSummary)
|
||||||
|
var total_members int64
|
||||||
|
err := row.Scan(&total_members)
|
||||||
|
return total_members, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsUserRegistrationsLast30Days = `-- name: AnalyticsUserRegistrationsLast30Days :many
|
||||||
|
SELECT
|
||||||
|
d.date,
|
||||||
|
COUNT(u.id)::bigint AS count
|
||||||
|
FROM generate_series(
|
||||||
|
CURRENT_DATE - INTERVAL '29 days',
|
||||||
|
CURRENT_DATE,
|
||||||
|
INTERVAL '1 day'
|
||||||
|
) AS d(date)
|
||||||
|
LEFT JOIN users u ON u.created_at::date = d.date
|
||||||
|
GROUP BY d.date
|
||||||
|
ORDER BY d.date
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsUserRegistrationsLast30DaysRow struct {
|
||||||
|
Date interface{} `json:"date"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsUserRegistrationsLast30Days(ctx context.Context) ([]AnalyticsUserRegistrationsLast30DaysRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsUserRegistrationsLast30Days)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsUserRegistrationsLast30DaysRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsUserRegistrationsLast30DaysRow
|
||||||
|
if err := rows.Scan(&i.Date, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsUsersByAgeGroup = `-- name: AnalyticsUsersByAgeGroup :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(age_group, 'unknown') AS age_group,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY age_group
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByAgeGroupRow struct {
|
||||||
|
AgeGroup string `json:"age_group"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context) ([]AnalyticsUsersByAgeGroupRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsUsersByAgeGroup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsUsersByAgeGroupRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsUsersByAgeGroupRow
|
||||||
|
if err := rows.Scan(&i.AgeGroup, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsUsersByKnowledgeLevel = `-- name: AnalyticsUsersByKnowledgeLevel :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(knowledge_level, 'unknown') AS knowledge_level,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY knowledge_level
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByKnowledgeLevelRow struct {
|
||||||
|
KnowledgeLevel string `json:"knowledge_level"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context) ([]AnalyticsUsersByKnowledgeLevelRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsUsersByKnowledgeLevel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsUsersByKnowledgeLevelRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsUsersByKnowledgeLevelRow
|
||||||
|
if err := rows.Scan(&i.KnowledgeLevel, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsUsersByRegion = `-- name: AnalyticsUsersByRegion :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(region, 'unknown') AS region,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY region
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByRegionRow struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsUsersByRegion(ctx context.Context) ([]AnalyticsUsersByRegionRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsUsersByRegion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsUsersByRegionRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsUsersByRegionRow
|
||||||
|
if err := rows.Scan(&i.Region, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsUsersByRole = `-- name: AnalyticsUsersByRole :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(role, 'unknown') AS role,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY role
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByRoleRow struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsUsersByRole(ctx context.Context) ([]AnalyticsUsersByRoleRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsUsersByRole)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsUsersByRoleRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsUsersByRoleRow
|
||||||
|
if err := rows.Scan(&i.Role, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsUsersByStatus = `-- name: AnalyticsUsersByStatus :many
|
||||||
|
SELECT
|
||||||
|
COALESCE(status, 'unknown') AS status,
|
||||||
|
COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByStatusRow struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AnalyticsUsersByStatus(ctx context.Context) ([]AnalyticsUsersByStatusRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, AnalyticsUsersByStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AnalyticsUsersByStatusRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AnalyticsUsersByStatusRow
|
||||||
|
if err := rows.Scan(&i.Status, &i.Count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalyticsUsersSummary = `-- name: AnalyticsUsersSummary :one
|
||||||
|
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::bigint AS total,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||||
|
FROM users
|
||||||
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersSummaryRow struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
NewToday int64 `json:"new_today"`
|
||||||
|
NewThisWeek int64 `json:"new_this_week"`
|
||||||
|
NewThisMonth int64 `json:"new_this_month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Analytics
|
||||||
|
// =====================
|
||||||
|
// =====================
|
||||||
|
// User Analytics
|
||||||
|
// =====================
|
||||||
|
func (q *Queries) AnalyticsUsersSummary(ctx context.Context) (AnalyticsUsersSummaryRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, AnalyticsUsersSummary)
|
||||||
|
var i AnalyticsUsersSummaryRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Total,
|
||||||
|
&i.NewToday,
|
||||||
|
&i.NewThisWeek,
|
||||||
|
&i.NewThisMonth,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
@ -66,17 +66,18 @@ type ModuleToSubCourse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
Channel pgtype.Text `json:"channel"`
|
Channel pgtype.Text `json:"channel"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Payload []byte `json:"payload"`
|
Payload []byte `json:"payload"`
|
||||||
IsRead bool `json:"is_read"`
|
IsRead bool `json:"is_read"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
ReadAt pgtype.Timestamptz `json:"read_at"`
|
ReadAt pgtype.Timestamptz `json:"read_at"`
|
||||||
|
ReceiverType string `json:"receiver_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Otp struct {
|
type Otp struct {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ func (q *Queries) CountUnreadNotifications(ctx context.Context, userID int64) (i
|
||||||
const CreateNotification = `-- name: CreateNotification :one
|
const CreateNotification = `-- name: CreateNotification :one
|
||||||
INSERT INTO notifications (
|
INSERT INTO notifications (
|
||||||
user_id,
|
user_id,
|
||||||
|
receiver_type,
|
||||||
type,
|
type,
|
||||||
level,
|
level,
|
||||||
channel,
|
channel,
|
||||||
|
|
@ -42,24 +43,27 @@ VALUES (
|
||||||
$4,
|
$4,
|
||||||
$5,
|
$5,
|
||||||
$6,
|
$6,
|
||||||
$7
|
$7,
|
||||||
|
$8
|
||||||
)
|
)
|
||||||
RETURNING id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at
|
RETURNING id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at, receiver_type
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateNotificationParams struct {
|
type CreateNotificationParams struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Type string `json:"type"`
|
ReceiverType string `json:"receiver_type"`
|
||||||
Level string `json:"level"`
|
Type string `json:"type"`
|
||||||
Channel pgtype.Text `json:"channel"`
|
Level string `json:"level"`
|
||||||
Title string `json:"title"`
|
Channel pgtype.Text `json:"channel"`
|
||||||
Message string `json:"message"`
|
Title string `json:"title"`
|
||||||
Payload []byte `json:"payload"`
|
Message string `json:"message"`
|
||||||
|
Payload []byte `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) {
|
func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) {
|
||||||
row := q.db.QueryRow(ctx, CreateNotification,
|
row := q.db.QueryRow(ctx, CreateNotification,
|
||||||
arg.UserID,
|
arg.UserID,
|
||||||
|
arg.ReceiverType,
|
||||||
arg.Type,
|
arg.Type,
|
||||||
arg.Level,
|
arg.Level,
|
||||||
arg.Channel,
|
arg.Channel,
|
||||||
|
|
@ -80,6 +84,7 @@ func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotification
|
||||||
&i.IsRead,
|
&i.IsRead,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.ReadAt,
|
&i.ReadAt,
|
||||||
|
&i.ReceiverType,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +100,7 @@ func (q *Queries) DeleteUserNotifications(ctx context.Context, userID int64) err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAllNotifications = `-- name: GetAllNotifications :many
|
const GetAllNotifications = `-- name: GetAllNotifications :many
|
||||||
SELECT id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at
|
SELECT id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at, receiver_type
|
||||||
FROM notifications
|
FROM notifications
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT $1 OFFSET $2
|
LIMIT $1 OFFSET $2
|
||||||
|
|
@ -127,6 +132,7 @@ func (q *Queries) GetAllNotifications(ctx context.Context, arg GetAllNotificatio
|
||||||
&i.IsRead,
|
&i.IsRead,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.ReadAt,
|
&i.ReadAt,
|
||||||
|
&i.ReceiverType,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +145,7 @@ func (q *Queries) GetAllNotifications(ctx context.Context, arg GetAllNotificatio
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetNotification = `-- name: GetNotification :one
|
const GetNotification = `-- name: GetNotification :one
|
||||||
SELECT id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at
|
SELECT id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at, receiver_type
|
||||||
FROM notifications
|
FROM notifications
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
|
|
@ -160,6 +166,7 @@ func (q *Queries) GetNotification(ctx context.Context, id int64) (Notification,
|
||||||
&i.IsRead,
|
&i.IsRead,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.ReadAt,
|
&i.ReadAt,
|
||||||
|
&i.ReceiverType,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +197,7 @@ func (q *Queries) GetUserNotificationCount(ctx context.Context, userID int64) (i
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserNotifications = `-- name: GetUserNotifications :many
|
const GetUserNotifications = `-- name: GetUserNotifications :many
|
||||||
SELECT id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at
|
SELECT id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at, receiver_type
|
||||||
FROM notifications
|
FROM notifications
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
|
|
@ -224,6 +231,7 @@ func (q *Queries) GetUserNotifications(ctx context.Context, arg GetUserNotificat
|
||||||
&i.IsRead,
|
&i.IsRead,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.ReadAt,
|
&i.ReadAt,
|
||||||
|
&i.ReceiverType,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -266,7 +274,7 @@ UPDATE notifications
|
||||||
SET is_read = TRUE,
|
SET is_read = TRUE,
|
||||||
read_at = NOW()
|
read_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at
|
RETURNING id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at, receiver_type
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) MarkNotificationAsRead(ctx context.Context, id int64) (Notification, error) {
|
func (q *Queries) MarkNotificationAsRead(ctx context.Context, id int64) (Notification, error) {
|
||||||
|
|
@ -284,6 +292,7 @@ func (q *Queries) MarkNotificationAsRead(ctx context.Context, id int64) (Notific
|
||||||
&i.IsRead,
|
&i.IsRead,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.ReadAt,
|
&i.ReadAt,
|
||||||
|
&i.ReceiverType,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +302,7 @@ UPDATE notifications
|
||||||
SET is_read = FALSE,
|
SET is_read = FALSE,
|
||||||
read_at = NULL
|
read_at = NULL
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at
|
RETURNING id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at, receiver_type
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) MarkNotificationAsUnread(ctx context.Context, id int64) (Notification, error) {
|
func (q *Queries) MarkNotificationAsUnread(ctx context.Context, id int64) (Notification, error) {
|
||||||
|
|
@ -311,6 +320,7 @@ func (q *Queries) MarkNotificationAsUnread(ctx context.Context, id int64) (Notif
|
||||||
&i.IsRead,
|
&i.IsRead,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.ReadAt,
|
&i.ReadAt,
|
||||||
|
&i.ReceiverType,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
121
internal/domain/analytics.go
Normal file
121
internal/domain/analytics.go
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type AnalyticsLabelCount struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsLabelAmount struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsRevenueByPlan struct {
|
||||||
|
PlanName string `json:"plan_name"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsTimePoint struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsRevenueTimePoint struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsUsersSection struct {
|
||||||
|
TotalUsers int64 `json:"total_users"`
|
||||||
|
NewToday int64 `json:"new_today"`
|
||||||
|
NewWeek int64 `json:"new_week"`
|
||||||
|
NewMonth int64 `json:"new_month"`
|
||||||
|
|
||||||
|
ByRole []AnalyticsLabelCount `json:"by_role"`
|
||||||
|
ByStatus []AnalyticsLabelCount `json:"by_status"`
|
||||||
|
ByAgeGroup []AnalyticsLabelCount `json:"by_age_group"`
|
||||||
|
ByKnowledgeLevel []AnalyticsLabelCount `json:"by_knowledge_level"`
|
||||||
|
ByRegion []AnalyticsLabelCount `json:"by_region"`
|
||||||
|
|
||||||
|
RegistrationsLast30Days []AnalyticsTimePoint `json:"registrations_last_30_days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsSubscriptionsSection struct {
|
||||||
|
TotalSubscriptions int64 `json:"total_subscriptions"`
|
||||||
|
ActiveSubscriptions int64 `json:"active_subscriptions"`
|
||||||
|
NewToday int64 `json:"new_today"`
|
||||||
|
NewWeek int64 `json:"new_week"`
|
||||||
|
NewMonth int64 `json:"new_month"`
|
||||||
|
|
||||||
|
ByStatus []AnalyticsLabelCount `json:"by_status"`
|
||||||
|
RevenueByPlan []AnalyticsRevenueByPlan `json:"revenue_by_plan"`
|
||||||
|
|
||||||
|
NewSubscriptionsLast30Days []AnalyticsTimePoint `json:"new_subscriptions_last_30_days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsPaymentsSection struct {
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
AvgTransactionValue float64 `json:"avg_transaction_value"`
|
||||||
|
TotalPayments int64 `json:"total_payments"`
|
||||||
|
SuccessfulPayments int64 `json:"successful_payments"`
|
||||||
|
|
||||||
|
ByStatus []AnalyticsLabelAmount `json:"by_status"`
|
||||||
|
ByMethod []AnalyticsLabelAmount `json:"by_method"`
|
||||||
|
|
||||||
|
RevenueLast30Days []AnalyticsRevenueTimePoint `json:"revenue_last_30_days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsCoursesSection struct {
|
||||||
|
TotalCategories int64 `json:"total_categories"`
|
||||||
|
TotalCourses int64 `json:"total_courses"`
|
||||||
|
TotalSubCourses int64 `json:"total_sub_courses"`
|
||||||
|
TotalVideos int64 `json:"total_videos"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsContentSection struct {
|
||||||
|
TotalQuestions int64 `json:"total_questions"`
|
||||||
|
TotalQuestionSets int64 `json:"total_question_sets"`
|
||||||
|
|
||||||
|
QuestionsByType []AnalyticsLabelCount `json:"questions_by_type"`
|
||||||
|
QuestionSetsByType []AnalyticsLabelCount `json:"question_sets_by_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsNotificationsSection struct {
|
||||||
|
TotalSent int64 `json:"total_sent"`
|
||||||
|
ReadCount int64 `json:"read_count"`
|
||||||
|
UnreadCount int64 `json:"unread_count"`
|
||||||
|
|
||||||
|
ByChannel []AnalyticsLabelCount `json:"by_channel"`
|
||||||
|
ByType []AnalyticsLabelCount `json:"by_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsIssuesSection struct {
|
||||||
|
TotalIssues int64 `json:"total_issues"`
|
||||||
|
ResolvedIssues int64 `json:"resolved_issues"`
|
||||||
|
ResolutionRate float64 `json:"resolution_rate"`
|
||||||
|
|
||||||
|
ByStatus []AnalyticsLabelCount `json:"by_status"`
|
||||||
|
ByType []AnalyticsLabelCount `json:"by_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsTeamSection struct {
|
||||||
|
TotalMembers int64 `json:"total_members"`
|
||||||
|
ByRole []AnalyticsLabelCount `json:"by_role"`
|
||||||
|
ByStatus []AnalyticsLabelCount `json:"by_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsDashboard struct {
|
||||||
|
GeneratedAt time.Time `json:"generated_at"`
|
||||||
|
Users AnalyticsUsersSection `json:"users"`
|
||||||
|
Subscriptions AnalyticsSubscriptionsSection `json:"subscriptions"`
|
||||||
|
Payments AnalyticsPaymentsSection `json:"payments"`
|
||||||
|
Courses AnalyticsCoursesSection `json:"courses"`
|
||||||
|
Content AnalyticsContentSection `json:"content"`
|
||||||
|
Notifications AnalyticsNotificationsSection `json:"notifications"`
|
||||||
|
Issues AnalyticsIssuesSection `json:"issues"`
|
||||||
|
Team AnalyticsTeamSection `json:"team"`
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,12 @@ type NotificationLevel string
|
||||||
type NotificationErrorSeverity string
|
type NotificationErrorSeverity string
|
||||||
type NotificationDeliveryStatus string
|
type NotificationDeliveryStatus string
|
||||||
type DeliveryChannel string
|
type DeliveryChannel string
|
||||||
|
type ReceiverType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReceiverTypeUser ReceiverType = "user"
|
||||||
|
ReceiverTypeTeamMember ReceiverType = "team_member"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE NotificationType = "knowledge_level_update"
|
NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE NotificationType = "knowledge_level_update"
|
||||||
|
|
@ -30,6 +36,7 @@ const (
|
||||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||||
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
|
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
|
||||||
NotificationRecieverSideBranchManager NotificationRecieverSide = "branch_manager"
|
NotificationRecieverSideBranchManager NotificationRecieverSide = "branch_manager"
|
||||||
|
NotificationRecieverSideTeamMember NotificationRecieverSide = "team_member"
|
||||||
|
|
||||||
NotificationDeliverySchemeBulk NotificationDeliveryScheme = "bulk"
|
NotificationDeliverySchemeBulk NotificationDeliveryScheme = "bulk"
|
||||||
NotificationDeliverySchemeSingle NotificationDeliveryScheme = "single"
|
NotificationDeliverySchemeSingle NotificationDeliveryScheme = "single"
|
||||||
|
|
@ -52,7 +59,7 @@ const (
|
||||||
DeliveryChannelEmail DeliveryChannel = "email"
|
DeliveryChannelEmail DeliveryChannel = "email"
|
||||||
DeliveryChannelSMS DeliveryChannel = "sms"
|
DeliveryChannelSMS DeliveryChannel = "sms"
|
||||||
DeliveryChannelPush DeliveryChannel = "push"
|
DeliveryChannelPush DeliveryChannel = "push"
|
||||||
DeliveryChannelInApp DeliveryChannel = "in-app"
|
DeliveryChannelInApp DeliveryChannel = "in_app"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationPayload struct {
|
type NotificationPayload struct {
|
||||||
|
|
@ -64,6 +71,7 @@ type NotificationPayload struct {
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
RecipientID int64 `json:"recipient_id"`
|
RecipientID int64 `json:"recipient_id"`
|
||||||
|
ReceiverType ReceiverType `json:"receiver_type"`
|
||||||
Type NotificationType `json:"type"`
|
Type NotificationType `json:"type"`
|
||||||
Level NotificationLevel `json:"level"`
|
Level NotificationLevel `json:"level"`
|
||||||
ErrorSeverity NotificationErrorSeverity `json:"error_severity"`
|
ErrorSeverity NotificationErrorSeverity `json:"error_severity"`
|
||||||
|
|
@ -81,6 +89,7 @@ type Notification struct {
|
||||||
}
|
}
|
||||||
type CreateNotification struct {
|
type CreateNotification struct {
|
||||||
RecipientID int64 `json:"recipient_id"`
|
RecipientID int64 `json:"recipient_id"`
|
||||||
|
ReceiverType ReceiverType `json:"receiver_type"`
|
||||||
Type NotificationType `json:"type"`
|
Type NotificationType `json:"type"`
|
||||||
Level NotificationLevel `json:"level"`
|
Level NotificationLevel `json:"level"`
|
||||||
ErrorSeverity *NotificationErrorSeverity `json:"error_severity"`
|
ErrorSeverity *NotificationErrorSeverity `json:"error_severity"`
|
||||||
|
|
@ -109,7 +118,6 @@ func FromJSON(data []byte) (*Notification, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReceiverFromRole(role Role) NotificationRecieverSide {
|
func ReceiverFromRole(role Role) NotificationRecieverSide {
|
||||||
|
|
||||||
switch role {
|
switch role {
|
||||||
case RoleAdmin:
|
case RoleAdmin:
|
||||||
return NotificationRecieverSideAdmin
|
return NotificationRecieverSideAdmin
|
||||||
|
|
@ -123,3 +131,12 @@ func ReceiverFromRole(role Role) NotificationRecieverSide {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReceiverTypeFromReciever(reciever NotificationRecieverSide) ReceiverType {
|
||||||
|
switch reciever {
|
||||||
|
case NotificationRecieverSideTeamMember:
|
||||||
|
return ReceiverTypeTeamMember
|
||||||
|
default:
|
||||||
|
return ReceiverTypeUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,20 @@ func (r *Store) CreateNotification(
|
||||||
n *domain.Notification,
|
n *domain.Notification,
|
||||||
) (*domain.Notification, error) {
|
) (*domain.Notification, error) {
|
||||||
|
|
||||||
|
receiverType := string(n.ReceiverType)
|
||||||
|
if receiverType == "" {
|
||||||
|
receiverType = string(domain.ReceiverTypeUser)
|
||||||
|
}
|
||||||
|
|
||||||
params := dbgen.CreateNotificationParams{
|
params := dbgen.CreateNotificationParams{
|
||||||
UserID: n.RecipientID,
|
UserID: n.RecipientID,
|
||||||
Type: string(n.Type),
|
ReceiverType: receiverType,
|
||||||
Level: string(n.Level),
|
Type: string(n.Type),
|
||||||
Channel: pgtype.Text{String: string(n.DeliveryChannel)},
|
Level: string(n.Level),
|
||||||
Title: n.Payload.Headline,
|
Channel: pgtype.Text{String: string(n.DeliveryChannel), Valid: true},
|
||||||
Message: n.Payload.Message,
|
Title: n.Payload.Headline,
|
||||||
Payload: marshalPayload(n.Payload),
|
Message: n.Payload.Message,
|
||||||
|
Payload: marshalPayload(n.Payload),
|
||||||
}
|
}
|
||||||
|
|
||||||
dbNotif, err := r.queries.CreateNotification(ctx, params)
|
dbNotif, err := r.queries.CreateNotification(ctx, params)
|
||||||
|
|
@ -178,6 +184,7 @@ func mapDBToDomain(db *dbgen.Notification) *domain.Notification {
|
||||||
return &domain.Notification{
|
return &domain.Notification{
|
||||||
ID: strconv.FormatInt(db.ID, 10),
|
ID: strconv.FormatInt(db.ID, 10),
|
||||||
RecipientID: db.UserID,
|
RecipientID: db.UserID,
|
||||||
|
ReceiverType: domain.ReceiverType(db.ReceiverType),
|
||||||
Type: domain.NotificationType(db.Type),
|
Type: domain.NotificationType(db.Type),
|
||||||
Level: domain.NotificationLevel(db.Level),
|
Level: domain.NotificationLevel(db.Level),
|
||||||
DeliveryChannel: channel,
|
DeliveryChannel: channel,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package httpserver
|
package httpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
|
|
@ -54,6 +55,7 @@ type App struct {
|
||||||
JwtConfig jwtutil.JwtConfig
|
JwtConfig jwtutil.JwtConfig
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
mongoLoggerSvc *zap.Logger
|
mongoLoggerSvc *zap.Logger
|
||||||
|
analyticsDB *dbgen.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(
|
func NewApp(
|
||||||
|
|
@ -77,6 +79,7 @@ func NewApp(
|
||||||
recommendationSvc recommendation.RecommendationService,
|
recommendationSvc recommendation.RecommendationService,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
mongoLoggerSvc *zap.Logger,
|
mongoLoggerSvc *zap.Logger,
|
||||||
|
analyticsDB *dbgen.Queries,
|
||||||
) *App {
|
) *App {
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
|
|
@ -119,6 +122,7 @@ func NewApp(
|
||||||
recommendationSvc: recommendationSvc,
|
recommendationSvc: recommendationSvc,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
mongoLoggerSvc: mongoLoggerSvc,
|
mongoLoggerSvc: mongoLoggerSvc,
|
||||||
|
analyticsDB: analyticsDB,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.initAppRoutes()
|
s.initAppRoutes()
|
||||||
|
|
|
||||||
359
internal/web_server/handlers/analytics_handler.go
Normal file
359
internal/web_server/handlers/analytics_handler.go
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toTime(v interface{}) time.Time {
|
||||||
|
if t, ok := v.(time.Time); ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
||||||
|
ctx := c.Context()
|
||||||
|
|
||||||
|
// ── Users ──
|
||||||
|
usersSummary, err := h.analyticsDB.AnalyticsUsersSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user analytics")
|
||||||
|
}
|
||||||
|
usersByRole, err := h.analyticsDB.AnalyticsUsersByRole(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by role")
|
||||||
|
}
|
||||||
|
usersByStatus, err := h.analyticsDB.AnalyticsUsersByStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by status")
|
||||||
|
}
|
||||||
|
usersByAge, err := h.analyticsDB.AnalyticsUsersByAgeGroup(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by age group")
|
||||||
|
}
|
||||||
|
usersByKnowledge, err := h.analyticsDB.AnalyticsUsersByKnowledgeLevel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by knowledge level")
|
||||||
|
}
|
||||||
|
usersByRegion, err := h.analyticsDB.AnalyticsUsersByRegion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by region")
|
||||||
|
}
|
||||||
|
userRegs, err := h.analyticsDB.AnalyticsUserRegistrationsLast30Days(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user registrations")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subscriptions ──
|
||||||
|
subsSummary, err := h.analyticsDB.AnalyticsSubscriptionsSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscription analytics")
|
||||||
|
}
|
||||||
|
subsByStatus, err := h.analyticsDB.AnalyticsSubscriptionsByStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscriptions by status")
|
||||||
|
}
|
||||||
|
revenueByPlan, err := h.analyticsDB.AnalyticsRevenueByPlan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue by plan")
|
||||||
|
}
|
||||||
|
newSubs30, err := h.analyticsDB.AnalyticsNewSubscriptionsLast30Days(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch new subscriptions last 30 days")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Payments ──
|
||||||
|
paymentsSummary, err := h.analyticsDB.AnalyticsPaymentsSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payment analytics")
|
||||||
|
}
|
||||||
|
paymentsByStatus, err := h.analyticsDB.AnalyticsPaymentsByStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by status")
|
||||||
|
}
|
||||||
|
paymentsByMethod, err := h.analyticsDB.AnalyticsPaymentsByMethod(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by method")
|
||||||
|
}
|
||||||
|
revenue30, err := h.analyticsDB.AnalyticsRevenueLast30Days(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue last 30 days")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Courses ──
|
||||||
|
courseCounts, err := h.analyticsDB.AnalyticsCourseCounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch course analytics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Content ──
|
||||||
|
questionsCounts, err := h.analyticsDB.AnalyticsQuestionsCounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch content analytics")
|
||||||
|
}
|
||||||
|
questionsByType, err := h.analyticsDB.AnalyticsQuestionsByType(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch questions by type")
|
||||||
|
}
|
||||||
|
questionSetsByType, err := h.analyticsDB.AnalyticsQuestionSetsByType(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch question sets by type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notifications ──
|
||||||
|
notifSummary, err := h.analyticsDB.AnalyticsNotificationsSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notification analytics")
|
||||||
|
}
|
||||||
|
notifByChannel, err := h.analyticsDB.AnalyticsNotificationsByChannel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by channel")
|
||||||
|
}
|
||||||
|
notifByType, err := h.analyticsDB.AnalyticsNotificationsByType(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Issues ──
|
||||||
|
issuesSummary, err := h.analyticsDB.AnalyticsIssuesSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issue analytics")
|
||||||
|
}
|
||||||
|
issuesByStatus, err := h.analyticsDB.AnalyticsIssuesByStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by status")
|
||||||
|
}
|
||||||
|
issuesByType, err := h.analyticsDB.AnalyticsIssuesByType(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Team ──
|
||||||
|
teamTotal, err := h.analyticsDB.AnalyticsTeamSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team analytics")
|
||||||
|
}
|
||||||
|
teamByRole, err := h.analyticsDB.AnalyticsTeamByRole(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by role")
|
||||||
|
}
|
||||||
|
teamByStatus, err := h.analyticsDB.AnalyticsTeamByStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Map to domain types ──
|
||||||
|
dashboard := domain.AnalyticsDashboard{
|
||||||
|
GeneratedAt: time.Now(),
|
||||||
|
Users: mapUsersSection(usersSummary, usersByRole, usersByStatus, usersByAge, usersByKnowledge, usersByRegion, userRegs),
|
||||||
|
Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30),
|
||||||
|
Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30),
|
||||||
|
Courses: domain.AnalyticsCoursesSection{
|
||||||
|
TotalCategories: courseCounts.TotalCategories,
|
||||||
|
TotalCourses: courseCounts.TotalCourses,
|
||||||
|
TotalSubCourses: courseCounts.TotalSubCourses,
|
||||||
|
TotalVideos: courseCounts.TotalVideos,
|
||||||
|
},
|
||||||
|
Content: mapContentSection(questionsCounts, questionsByType, questionSetsByType),
|
||||||
|
Notifications: mapNotificationsSection(notifSummary, notifByChannel, notifByType),
|
||||||
|
Issues: mapIssuesSection(issuesSummary, issuesByStatus, issuesByType),
|
||||||
|
Team: mapTeamSection(teamTotal, teamByRole, teamByStatus),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(dashboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapUsersSection(
|
||||||
|
summary dbgen.AnalyticsUsersSummaryRow,
|
||||||
|
byRole []dbgen.AnalyticsUsersByRoleRow,
|
||||||
|
byStatus []dbgen.AnalyticsUsersByStatusRow,
|
||||||
|
byAge []dbgen.AnalyticsUsersByAgeGroupRow,
|
||||||
|
byKnowledge []dbgen.AnalyticsUsersByKnowledgeLevelRow,
|
||||||
|
byRegion []dbgen.AnalyticsUsersByRegionRow,
|
||||||
|
regs []dbgen.AnalyticsUserRegistrationsLast30DaysRow,
|
||||||
|
) domain.AnalyticsUsersSection {
|
||||||
|
roles := make([]domain.AnalyticsLabelCount, len(byRole))
|
||||||
|
for i, r := range byRole {
|
||||||
|
roles[i] = domain.AnalyticsLabelCount{Label: r.Role, Count: r.Count}
|
||||||
|
}
|
||||||
|
statuses := make([]domain.AnalyticsLabelCount, len(byStatus))
|
||||||
|
for i, r := range byStatus {
|
||||||
|
statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count}
|
||||||
|
}
|
||||||
|
ages := make([]domain.AnalyticsLabelCount, len(byAge))
|
||||||
|
for i, r := range byAge {
|
||||||
|
ages[i] = domain.AnalyticsLabelCount{Label: r.AgeGroup, Count: r.Count}
|
||||||
|
}
|
||||||
|
knowledge := make([]domain.AnalyticsLabelCount, len(byKnowledge))
|
||||||
|
for i, r := range byKnowledge {
|
||||||
|
knowledge[i] = domain.AnalyticsLabelCount{Label: r.KnowledgeLevel, Count: r.Count}
|
||||||
|
}
|
||||||
|
regions := make([]domain.AnalyticsLabelCount, len(byRegion))
|
||||||
|
for i, r := range byRegion {
|
||||||
|
regions[i] = domain.AnalyticsLabelCount{Label: r.Region, Count: r.Count}
|
||||||
|
}
|
||||||
|
timePoints := make([]domain.AnalyticsTimePoint, len(regs))
|
||||||
|
for i, r := range regs {
|
||||||
|
timePoints[i] = domain.AnalyticsTimePoint{Date: toTime(r.Date), Count: r.Count}
|
||||||
|
}
|
||||||
|
return domain.AnalyticsUsersSection{
|
||||||
|
TotalUsers: summary.Total,
|
||||||
|
NewToday: summary.NewToday,
|
||||||
|
NewWeek: summary.NewThisWeek,
|
||||||
|
NewMonth: summary.NewThisMonth,
|
||||||
|
ByRole: roles,
|
||||||
|
ByStatus: statuses,
|
||||||
|
ByAgeGroup: ages,
|
||||||
|
ByKnowledgeLevel: knowledge,
|
||||||
|
ByRegion: regions,
|
||||||
|
RegistrationsLast30Days: timePoints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapSubscriptionsSection(
|
||||||
|
summary dbgen.AnalyticsSubscriptionsSummaryRow,
|
||||||
|
byStatus []dbgen.AnalyticsSubscriptionsByStatusRow,
|
||||||
|
byPlan []dbgen.AnalyticsRevenueByPlanRow,
|
||||||
|
newSubs []dbgen.AnalyticsNewSubscriptionsLast30DaysRow,
|
||||||
|
) domain.AnalyticsSubscriptionsSection {
|
||||||
|
statuses := make([]domain.AnalyticsLabelCount, len(byStatus))
|
||||||
|
for i, r := range byStatus {
|
||||||
|
statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count}
|
||||||
|
}
|
||||||
|
plans := make([]domain.AnalyticsRevenueByPlan, len(byPlan))
|
||||||
|
for i, r := range byPlan {
|
||||||
|
plans[i] = domain.AnalyticsRevenueByPlan{PlanName: r.PlanName, Currency: r.Currency, Revenue: r.TotalRevenue}
|
||||||
|
}
|
||||||
|
timePoints := make([]domain.AnalyticsTimePoint, len(newSubs))
|
||||||
|
for i, r := range newSubs {
|
||||||
|
timePoints[i] = domain.AnalyticsTimePoint{Date: toTime(r.Date), Count: r.Count}
|
||||||
|
}
|
||||||
|
return domain.AnalyticsSubscriptionsSection{
|
||||||
|
TotalSubscriptions: summary.Total,
|
||||||
|
ActiveSubscriptions: summary.Active,
|
||||||
|
NewToday: summary.NewToday,
|
||||||
|
NewWeek: summary.NewThisWeek,
|
||||||
|
NewMonth: summary.NewThisMonth,
|
||||||
|
ByStatus: statuses,
|
||||||
|
RevenueByPlan: plans,
|
||||||
|
NewSubscriptionsLast30Days: timePoints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapPaymentsSection(
|
||||||
|
summary dbgen.AnalyticsPaymentsSummaryRow,
|
||||||
|
byStatus []dbgen.AnalyticsPaymentsByStatusRow,
|
||||||
|
byMethod []dbgen.AnalyticsPaymentsByMethodRow,
|
||||||
|
revenue []dbgen.AnalyticsRevenueLast30DaysRow,
|
||||||
|
) domain.AnalyticsPaymentsSection {
|
||||||
|
statuses := make([]domain.AnalyticsLabelAmount, len(byStatus))
|
||||||
|
for i, r := range byStatus {
|
||||||
|
statuses[i] = domain.AnalyticsLabelAmount{Label: r.Status, Count: r.Count, Amount: r.TotalAmount}
|
||||||
|
}
|
||||||
|
methods := make([]domain.AnalyticsLabelAmount, len(byMethod))
|
||||||
|
for i, r := range byMethod {
|
||||||
|
methods[i] = domain.AnalyticsLabelAmount{Label: r.PaymentMethod, Count: r.Count, Amount: r.TotalAmount}
|
||||||
|
}
|
||||||
|
timePoints := make([]domain.AnalyticsRevenueTimePoint, len(revenue))
|
||||||
|
for i, r := range revenue {
|
||||||
|
timePoints[i] = domain.AnalyticsRevenueTimePoint{Date: toTime(r.Date), Revenue: r.TotalRevenue}
|
||||||
|
}
|
||||||
|
return domain.AnalyticsPaymentsSection{
|
||||||
|
TotalRevenue: summary.TotalRevenue,
|
||||||
|
AvgTransactionValue: summary.AvgValue,
|
||||||
|
TotalPayments: summary.TotalPayments,
|
||||||
|
SuccessfulPayments: summary.SuccessfulPayments,
|
||||||
|
ByStatus: statuses,
|
||||||
|
ByMethod: methods,
|
||||||
|
RevenueLast30Days: timePoints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapContentSection(
|
||||||
|
counts dbgen.AnalyticsQuestionsCountsRow,
|
||||||
|
byType []dbgen.AnalyticsQuestionsByTypeRow,
|
||||||
|
setsByType []dbgen.AnalyticsQuestionSetsByTypeRow,
|
||||||
|
) domain.AnalyticsContentSection {
|
||||||
|
qTypes := make([]domain.AnalyticsLabelCount, len(byType))
|
||||||
|
for i, r := range byType {
|
||||||
|
qTypes[i] = domain.AnalyticsLabelCount{Label: r.QuestionType, Count: r.Count}
|
||||||
|
}
|
||||||
|
sTypes := make([]domain.AnalyticsLabelCount, len(setsByType))
|
||||||
|
for i, r := range setsByType {
|
||||||
|
sTypes[i] = domain.AnalyticsLabelCount{Label: r.SetType, Count: r.Count}
|
||||||
|
}
|
||||||
|
return domain.AnalyticsContentSection{
|
||||||
|
TotalQuestions: counts.TotalQuestions,
|
||||||
|
TotalQuestionSets: counts.TotalQuestionSets,
|
||||||
|
QuestionsByType: qTypes,
|
||||||
|
QuestionSetsByType: sTypes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapNotificationsSection(
|
||||||
|
summary dbgen.AnalyticsNotificationsSummaryRow,
|
||||||
|
byChannel []dbgen.AnalyticsNotificationsByChannelRow,
|
||||||
|
byType []dbgen.AnalyticsNotificationsByTypeRow,
|
||||||
|
) domain.AnalyticsNotificationsSection {
|
||||||
|
channels := make([]domain.AnalyticsLabelCount, len(byChannel))
|
||||||
|
for i, r := range byChannel {
|
||||||
|
channels[i] = domain.AnalyticsLabelCount{Label: r.Channel, Count: r.Count}
|
||||||
|
}
|
||||||
|
types := make([]domain.AnalyticsLabelCount, len(byType))
|
||||||
|
for i, r := range byType {
|
||||||
|
types[i] = domain.AnalyticsLabelCount{Label: r.Type, Count: r.Count}
|
||||||
|
}
|
||||||
|
return domain.AnalyticsNotificationsSection{
|
||||||
|
TotalSent: summary.Total,
|
||||||
|
ReadCount: summary.Read,
|
||||||
|
UnreadCount: summary.Unread,
|
||||||
|
ByChannel: channels,
|
||||||
|
ByType: types,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapIssuesSection(
|
||||||
|
summary dbgen.AnalyticsIssuesSummaryRow,
|
||||||
|
byStatus []dbgen.AnalyticsIssuesByStatusRow,
|
||||||
|
byType []dbgen.AnalyticsIssuesByTypeRow,
|
||||||
|
) domain.AnalyticsIssuesSection {
|
||||||
|
statuses := make([]domain.AnalyticsLabelCount, len(byStatus))
|
||||||
|
for i, r := range byStatus {
|
||||||
|
statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count}
|
||||||
|
}
|
||||||
|
types := make([]domain.AnalyticsLabelCount, len(byType))
|
||||||
|
for i, r := range byType {
|
||||||
|
types[i] = domain.AnalyticsLabelCount{Label: r.IssueType, Count: r.Count}
|
||||||
|
}
|
||||||
|
return domain.AnalyticsIssuesSection{
|
||||||
|
TotalIssues: summary.Total,
|
||||||
|
ResolvedIssues: summary.Resolved,
|
||||||
|
ResolutionRate: summary.ResolutionRate,
|
||||||
|
ByStatus: statuses,
|
||||||
|
ByType: types,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapTeamSection(
|
||||||
|
totalMembers int64,
|
||||||
|
byRole []dbgen.AnalyticsTeamByRoleRow,
|
||||||
|
byStatus []dbgen.AnalyticsTeamByStatusRow,
|
||||||
|
) domain.AnalyticsTeamSection {
|
||||||
|
roles := make([]domain.AnalyticsLabelCount, len(byRole))
|
||||||
|
for i, r := range byRole {
|
||||||
|
roles[i] = domain.AnalyticsLabelCount{Label: r.TeamRole, Count: r.Count}
|
||||||
|
}
|
||||||
|
statuses := make([]domain.AnalyticsLabelCount, len(byStatus))
|
||||||
|
for i, r := range byStatus {
|
||||||
|
statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count}
|
||||||
|
}
|
||||||
|
return domain.AnalyticsTeamSection{
|
||||||
|
TotalMembers: totalMembers,
|
||||||
|
ByRole: roles,
|
||||||
|
ByStatus: statuses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,15 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
course_management "Yimaru-Backend/internal/services/course_management"
|
course_management "Yimaru-Backend/internal/services/course_management"
|
||||||
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
|
|
@ -52,6 +53,7 @@ type Handler struct {
|
||||||
validator *customvalidator.CustomValidator
|
validator *customvalidator.CustomValidator
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
mongoLoggerSvc *zap.Logger
|
mongoLoggerSvc *zap.Logger
|
||||||
|
analyticsDB *dbgen.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
|
|
@ -75,6 +77,7 @@ func New(
|
||||||
jwtConfig jwtutil.JwtConfig,
|
jwtConfig jwtutil.JwtConfig,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
mongoLoggerSvc *zap.Logger,
|
mongoLoggerSvc *zap.Logger,
|
||||||
|
analyticsDB *dbgen.Queries,
|
||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
|
|
@ -97,6 +100,7 @@ func New(
|
||||||
jwtConfig: jwtConfig,
|
jwtConfig: jwtConfig,
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
mongoLoggerSvc: mongoLoggerSvc,
|
mongoLoggerSvc: mongoLoggerSvc,
|
||||||
|
analyticsDB: analyticsDB,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,23 +28,21 @@ func (h *Handler) ConnectSocket(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
|
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert fasthttp request to net/http request for gorilla upgrader
|
||||||
|
stdReq := &http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
c.Context().Request.Header.VisitAll(func(key, value []byte) {
|
||||||
|
stdReq.Header.Set(string(key), string(value))
|
||||||
|
})
|
||||||
|
stdReq.Host = string(c.Context().Host())
|
||||||
|
stdReq.RequestURI = string(c.Context().RequestURI())
|
||||||
|
|
||||||
// Hijack the underlying net.Conn from fasthttp
|
// Hijack the underlying net.Conn from fasthttp
|
||||||
done := make(chan struct{})
|
// The hijack callback runs after the handler returns, so we must not block.
|
||||||
c.Context().HijackSetNoResponse(true)
|
c.Context().HijackSetNoResponse(true)
|
||||||
c.Context().Hijack(func(netConn net.Conn) {
|
c.Context().Hijack(func(netConn net.Conn) {
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
// Build a minimal http.Request for gorilla's upgrader
|
|
||||||
stdReq := &http.Request{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
Header: make(http.Header),
|
|
||||||
}
|
|
||||||
c.Context().Request.Header.VisitAll(func(key, value []byte) {
|
|
||||||
stdReq.Header.Set(string(key), string(value))
|
|
||||||
})
|
|
||||||
stdReq.Host = string(c.Context().Host())
|
|
||||||
stdReq.RequestURI = string(c.Context().RequestURI())
|
|
||||||
|
|
||||||
// Create a hijackable response writer around the raw connection
|
// Create a hijackable response writer around the raw connection
|
||||||
hjRW := &hijackResponseWriter{
|
hjRW := &hijackResponseWriter{
|
||||||
conn: netConn,
|
conn: netConn,
|
||||||
|
|
@ -99,7 +97,6 @@ func (h *Handler) ConnectSocket(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
<-done
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,6 +311,7 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
|
||||||
notification := &domain.Notification{
|
notification := &domain.Notification{
|
||||||
ID: "",
|
ID: "",
|
||||||
RecipientID: req.RecipientID,
|
RecipientID: req.RecipientID,
|
||||||
|
ReceiverType: domain.ReceiverTypeFromReciever(req.Reciever),
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
Level: req.Level,
|
Level: req.Level,
|
||||||
ErrorSeverity: errorSeverity,
|
ErrorSeverity: errorSeverity,
|
||||||
|
|
@ -368,6 +366,7 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
|
||||||
notification := &domain.Notification{
|
notification := &domain.Notification{
|
||||||
ID: "",
|
ID: "",
|
||||||
RecipientID: user.ID,
|
RecipientID: user.ID,
|
||||||
|
ReceiverType: domain.ReceiverTypeFromReciever(req.Reciever),
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
Level: req.Level,
|
Level: req.Level,
|
||||||
ErrorSeverity: errorSeverity,
|
ErrorSeverity: errorSeverity,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ func (a *App) initAppRoutes() {
|
||||||
a.JwtConfig,
|
a.JwtConfig,
|
||||||
a.cfg,
|
a.cfg,
|
||||||
a.mongoLoggerSvc,
|
a.mongoLoggerSvc,
|
||||||
|
a.analyticsDB,
|
||||||
)
|
)
|
||||||
|
|
||||||
a.fiber.Get("/", func(c *fiber.Ctx) error {
|
a.fiber.Get("/", func(c *fiber.Ctx) error {
|
||||||
|
|
@ -359,6 +360,9 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Get("/settings/:key", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingByKey)
|
groupV1.Get("/settings/:key", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingByKey)
|
||||||
groupV1.Put("/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList)
|
groupV1.Put("/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList)
|
||||||
|
|
||||||
|
// Analytics Routes
|
||||||
|
groupV1.Get("/analytics/dashboard", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAnalyticsDashboard)
|
||||||
|
|
||||||
// Vimeo Video Hosting Routes
|
// Vimeo Video Hosting Routes
|
||||||
vimeoGroup := groupV1.Group("/vimeo")
|
vimeoGroup := groupV1.Group("/vimeo")
|
||||||
vimeoGroup.Get("/videos/:video_id", a.authMiddleware, h.GetVimeoVideo)
|
vimeoGroup.Get("/videos/:video_id", a.authMiddleware, h.GetVimeoVideo)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user