Files
2026-06-22 14:31:01 +05:00

221 lines
6.9 KiB
Go

package mysql
import (
"context"
"database/sql"
"fmt"
"strings"
"secunda-test/internal/domain"
"secunda-test/internal/service"
)
type TaskRepository struct{ db *sql.DB }
func NewTaskRepository(db *sql.DB) *TaskRepository { return &TaskRepository{db: db} }
func (r *TaskRepository) Create(ctx context.Context, task domain.Task) (domain.Task, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO tasks(team_id,title,description,status,assignee_id,created_by)
VALUES(?,?,?,?,?,?)`,
task.TeamID, task.Title, task.Description, valueOrDefault(string(task.Status), string(domain.StatusTodo)), task.AssigneeID, task.CreatedBy)
if err != nil {
return domain.Task{}, err
}
id, _ := res.LastInsertId()
return r.Get(ctx, id)
}
func (r *TaskRepository) Get(ctx context.Context, id int64) (domain.Task, error) {
var t domain.Task
err := r.db.QueryRowContext(ctx, `
SELECT id,team_id,title,description,status,assignee_id,created_by,created_at,updated_at
FROM tasks WHERE id=?`, id).
Scan(&t.ID, &t.TeamID, &t.Title, &t.Description, &t.Status, &t.AssigneeID, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt)
if err == sql.ErrNoRows {
return domain.Task{}, service.ErrNotFound
}
return t, err
}
func (r *TaskRepository) Update(ctx context.Context, task domain.Task, changedBy int64) (domain.Task, error) {
old, err := r.Get(ctx, task.ID)
if err != nil {
return domain.Task{}, err
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return domain.Task{}, err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, `
UPDATE tasks SET title=?,description=?,status=?,assignee_id=? WHERE id=?`,
task.Title, task.Description, task.Status, task.AssigneeID, task.ID)
if err != nil {
return domain.Task{}, err
}
addHistory := func(field, oldValue, newValue string) error {
if oldValue == newValue {
return nil
}
_, err := tx.ExecContext(ctx, `INSERT INTO task_history(task_id,changed_by,field_name,old_value,new_value) VALUES(?,?,?,?,?)`,
task.ID, changedBy, field, oldValue, newValue)
return err
}
if err := addHistory("title", old.Title, task.Title); err != nil {
return domain.Task{}, err
}
if err := addHistory("description", old.Description, task.Description); err != nil {
return domain.Task{}, err
}
if err := addHistory("status", string(old.Status), string(task.Status)); err != nil {
return domain.Task{}, err
}
if err := addHistory("assignee_id", ptrString(old.AssigneeID), ptrString(task.AssigneeID)); err != nil {
return domain.Task{}, err
}
if err := tx.Commit(); err != nil {
return domain.Task{}, err
}
return r.Get(ctx, task.ID)
}
func (r *TaskRepository) List(ctx context.Context, filter domain.TaskFilter) ([]domain.Task, error) {
args := []any{filter.TeamID}
where := []string{"team_id=?"}
if filter.Status != "" {
where = append(where, "status=?")
args = append(args, filter.Status)
}
if filter.AssigneeID != nil {
where = append(where, "assignee_id=?")
args = append(args, *filter.AssigneeID)
}
limit, offset := filter.PageSize, (filter.Page-1)*filter.PageSize
args = append(args, limit, offset)
query := fmt.Sprintf(`
SELECT id,team_id,title,description,status,assignee_id,created_by,created_at,updated_at
FROM tasks WHERE %s ORDER BY created_at DESC LIMIT ? OFFSET ?`, strings.Join(where, " AND "))
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []domain.Task
for rows.Next() {
var t domain.Task
if err := rows.Scan(&t.ID, &t.TeamID, &t.Title, &t.Description, &t.Status, &t.AssigneeID, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (r *TaskRepository) History(ctx context.Context, taskID int64) ([]domain.TaskHistory, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id,task_id,changed_by,field_name,old_value,new_value,created_at
FROM task_history WHERE task_id=? ORDER BY created_at DESC`, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []domain.TaskHistory
for rows.Next() {
var h domain.TaskHistory
if err := rows.Scan(&h.ID, &h.TaskID, &h.ChangedBy, &h.FieldName, &h.OldValue, &h.NewValue, &h.CreatedAt); err != nil {
return nil, err
}
out = append(out, h)
}
return out, rows.Err()
}
func (r *TaskRepository) TeamSummary(ctx context.Context) ([]domain.TeamSummary, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT t.id, t.name, COUNT(DISTINCT tm.user_id) AS members_count,
COUNT(DISTINCT CASE WHEN ta.status='done' AND ta.updated_at >= NOW() - INTERVAL 7 DAY THEN ta.id END) AS done_last_7_days
FROM teams t
LEFT JOIN team_members tm ON tm.team_id=t.id
LEFT JOIN tasks ta ON ta.team_id=t.id
GROUP BY t.id, t.name
ORDER BY t.name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []domain.TeamSummary
for rows.Next() {
var s domain.TeamSummary
if err := rows.Scan(&s.TeamID, &s.TeamName, &s.MembersCount, &s.DoneLast7Days); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
func (r *TaskRepository) TopCreators(ctx context.Context) ([]domain.TopCreator, error) {
rows, err := r.db.QueryContext(ctx, `
WITH ranked AS (
SELECT t.team_id, te.name AS team_name, t.created_by AS user_id, u.name AS user_name,
COUNT(*) AS tasks_created,
DENSE_RANK() OVER (PARTITION BY t.team_id ORDER BY COUNT(*) DESC) AS user_rank
FROM tasks t
JOIN teams te ON te.id=t.team_id
JOIN users u ON u.id=t.created_by
WHERE t.created_at >= NOW() - INTERVAL 1 MONTH
GROUP BY t.team_id, te.name, t.created_by, u.name
)
SELECT team_id,team_name,user_id,user_name,tasks_created,user_rank
FROM ranked WHERE user_rank <= 3 ORDER BY team_id,user_rank`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []domain.TopCreator
for rows.Next() {
var c domain.TopCreator
if err := rows.Scan(&c.TeamID, &c.TeamName, &c.UserID, &c.UserName, &c.TasksCreated, &c.Rank); err != nil {
return nil, err
}
out = append(out, c)
}
return out, rows.Err()
}
func (r *TaskRepository) InvalidAssignees(ctx context.Context) ([]domain.Task, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT ta.id,ta.team_id,ta.title,ta.description,ta.status,ta.assignee_id,ta.created_by,ta.created_at,ta.updated_at
FROM tasks ta
LEFT JOIN team_members tm ON tm.team_id=ta.team_id AND tm.user_id=ta.assignee_id
WHERE ta.assignee_id IS NOT NULL AND tm.user_id IS NULL`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []domain.Task
for rows.Next() {
var t domain.Task
if err := rows.Scan(&t.ID, &t.TeamID, &t.Title, &t.Description, &t.Status, &t.AssigneeID, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func valueOrDefault(v, fallback string) string {
if v == "" {
return fallback
}
return v
}
func ptrString(v *int64) string {
if v == nil {
return ""
}
return fmt.Sprint(*v)
}