package postgres import ( "context" "database/sql" "encoding/json" "switchboard-core/models" ) type TeamStore struct{} func NewTeamStore() *TeamStore { return &TeamStore{} } func (s *TeamStore) Create(ctx context.Context, t *models.Team) error { settingsJSON := ToJSON(t.Settings) return DB.QueryRowContext(ctx, ` INSERT INTO teams (name, description, created_by, is_active, settings) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at, updated_at`, t.Name, t.Description, t.CreatedBy, t.IsActive, settingsJSON, ).Scan(&t.ID, &t.CreatedAt, &t.UpdatedAt) } func (s *TeamStore) GetByID(ctx context.Context, id string) (*models.Team, error) { var t models.Team var settingsJSON []byte err := DB.QueryRowContext(ctx, ` SELECT id, name, description, created_by, is_active, settings, created_at, updated_at, (SELECT COUNT(*) FROM team_members WHERE team_id = teams.id) as member_count FROM teams WHERE id = $1`, id).Scan( &t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive, &settingsJSON, &t.CreatedAt, &t.UpdatedAt, &t.MemberCount, ) if err != nil { return nil, err } json.Unmarshal(settingsJSON, &t.Settings) return &t, nil } func (s *TeamStore) Update(ctx context.Context, id string, fields map[string]interface{}) error { b := NewUpdate("teams") for k, v := range fields { if k == "settings" { b.SetJSON(k, v) } else { b.Set(k, v) } } if !b.HasSets() { return nil } b.Where("id", id) _, err := b.Exec(DB) return err } func (s *TeamStore) Delete(ctx context.Context, id string) error { _, err := DB.ExecContext(ctx, "DELETE FROM teams WHERE id = $1", id) return err } func (s *TeamStore) List(ctx context.Context) ([]models.Team, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, name, description, created_by, is_active, settings, created_at, updated_at, (SELECT COUNT(*) FROM team_members WHERE team_id = teams.id) as member_count FROM teams ORDER BY name`) if err != nil { return nil, err } defer rows.Close() var result []models.Team for rows.Next() { var t models.Team var settingsJSON []byte err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive, &settingsJSON, &t.CreatedAt, &t.UpdatedAt, &t.MemberCount) if err != nil { return nil, err } json.Unmarshal(settingsJSON, &t.Settings) result = append(result, t) } return result, rows.Err() } func (s *TeamStore) ListForUser(ctx context.Context, userID string) ([]models.Team, error) { rows, err := DB.QueryContext(ctx, ` SELECT t.id, t.name, t.description, t.created_by, t.is_active, COALESCE(t.settings, '{}'), t.created_at, t.updated_at, tm.role AS my_role, (SELECT COUNT(*) FROM team_members WHERE team_id = t.id) AS member_count FROM teams t JOIN team_members tm ON tm.team_id = t.id AND tm.user_id = $1 WHERE t.is_active = true ORDER BY t.name ASC`, userID) if err != nil { return nil, err } defer rows.Close() var result []models.Team for rows.Next() { var t models.Team var settingsJSON []byte err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive, &settingsJSON, &t.CreatedAt, &t.UpdatedAt, &t.MyRole, &t.MemberCount) if err != nil { return nil, err } json.Unmarshal(settingsJSON, &t.Settings) result = append(result, t) } return result, rows.Err() } // ── Members ───────────────────────────────── func (s *TeamStore) AddMember(ctx context.Context, teamID, userID, role string) error { _, err := DB.ExecContext(ctx, ` INSERT INTO team_members (team_id, user_id, role) VALUES ($1, $2, $3) ON CONFLICT (team_id, user_id) DO UPDATE SET role = $3`, teamID, userID, role) return err } func (s *TeamStore) RemoveMember(ctx context.Context, teamID, userID string) error { _, err := DB.ExecContext(ctx, "DELETE FROM team_members WHERE team_id = $1 AND user_id = $2", teamID, userID) return err } func (s *TeamStore) UpdateMemberRole(ctx context.Context, teamID, userID, role string) error { _, err := DB.ExecContext(ctx, "UPDATE team_members SET role = $1 WHERE team_id = $2 AND user_id = $3", role, teamID, userID) return err } func (s *TeamStore) ListMembers(ctx context.Context, teamID string) ([]models.TeamMember, error) { rows, err := DB.QueryContext(ctx, ` SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at, u.email, COALESCE(u.display_name, ''), u.username FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = $1 ORDER BY tm.role DESC, u.username`, teamID) if err != nil { return nil, err } defer rows.Close() var result []models.TeamMember for rows.Next() { var m models.TeamMember err := rows.Scan(&m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt, &m.Email, &m.DisplayName, &m.Username) if err != nil { return nil, err } result = append(result, m) } return result, rows.Err() } func (s *TeamStore) GetMember(ctx context.Context, teamID, userID string) (*models.TeamMember, error) { var m models.TeamMember err := DB.QueryRowContext(ctx, ` SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at, u.email, COALESCE(u.display_name, ''), u.username FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = $1 AND tm.user_id = $2`, teamID, userID).Scan( &m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt, &m.Email, &m.DisplayName, &m.Username) if err != nil { return nil, err } return &m, nil } // GetUserTeamIDs returns all team IDs a user belongs to. func (s *TeamStore) GetUserTeamIDs(ctx context.Context, userID string) ([]string, error) { rows, err := DB.QueryContext(ctx, "SELECT team_id FROM team_members WHERE user_id = $1", userID) if err != nil { return nil, err } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } func (s *TeamStore) IsTeamAdmin(ctx context.Context, teamID, userID string) (bool, error) { var role string err := DB.QueryRowContext(ctx, "SELECT role FROM team_members WHERE team_id = $1 AND user_id = $2", teamID, userID).Scan(&role) if err == sql.ErrNoRows { return false, nil } if err != nil { return false, err } return role == models.TeamRoleAdmin, nil } func (s *TeamStore) IsMember(ctx context.Context, teamID, userID string) (bool, error) { var exists bool err := DB.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", teamID, userID).Scan(&exists) return exists, err } // ── CS1 additions (v0.29.0) ───────────────────────────────────────────── func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) { var exists bool err := DB.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM teams WHERE id = $1)`, teamID).Scan(&exists) return exists, err } func (s *TeamStore) UpdateMemberRoleByID(ctx context.Context, memberID, teamID, role string) (int64, error) { res, err := DB.ExecContext(ctx, `UPDATE team_members SET role = $1 WHERE id = $2 AND team_id = $3`, role, memberID, teamID) if err != nil { return 0, err } return res.RowsAffected() } func (s *TeamStore) DeleteMemberByID(ctx context.Context, memberID, teamID string) (int64, error) { res, err := DB.ExecContext(ctx, `DELETE FROM team_members WHERE id = $1 AND team_id = $2`, memberID, teamID) if err != nil { return 0, err } return res.RowsAffected() } func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([]string, error) { rows, err := DB.QueryContext(ctx, ` SELECT DISTINCT al.action FROM audit_log al WHERE al.actor_id IN (SELECT user_id FROM team_members WHERE team_id = $1) ORDER BY al.action `, teamID) if err != nil { return nil, err } defer rows.Close() var actions []string for rows.Next() { var a string if err := rows.Scan(&a); err != nil { return nil, err } actions = append(actions, a) } if actions == nil { actions = []string{} } return actions, rows.Err() } // ── CS5b additions (v0.29.0) ──────────────────────────────────────────── func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) { var teamID string err := DB.QueryRowContext(ctx, `SELECT team_id FROM team_members WHERE user_id = $1 LIMIT 1`, userID).Scan(&teamID) if err != nil { return "", nil } return teamID, nil } // ── CS6 additions (v0.29.0) ──────────────────────────────────────────── func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) { var id string err := DB.QueryRowContext(ctx, ` INSERT INTO team_members (team_id, user_id, role) VALUES ($1, $2, $3) RETURNING id`, teamID, userID, role).Scan(&id) return id, err } func (s *TeamStore) MergeSettings(ctx context.Context, teamID, settingsJSON string) error { _, err := DB.ExecContext(ctx, `UPDATE teams SET settings = COALESCE(settings, '{}'::jsonb) || $1::jsonb WHERE id = $2`, settingsJSON, teamID) return err }