package sqlite import ( "context" "database/sql" "encoding/json" "time" "switchboard-core/models" "switchboard-core/store" ) type TeamStore struct{} func NewTeamStore() *TeamStore { return &TeamStore{} } func (s *TeamStore) Create(ctx context.Context, t *models.Team) error { settingsJSON := ToJSON(t.Settings) t.ID = store.NewID() now := time.Now().UTC() t.CreatedAt = now t.UpdatedAt = now _, err := DB.ExecContext(ctx, ` INSERT INTO teams (id, name, description, created_by, is_active, settings, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, t.ID, t.Name, t.Description, t.CreatedBy, t.IsActive, settingsJSON, now.Format(timeFmt), now.Format(timeFmt), ) return err } 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 = ?`, id).Scan( &t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive, &settingsJSON, st(&t.CreatedAt), st(&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 = ?", 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, st(&t.CreatedAt), st(&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 = ? WHERE t.is_active = 1 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, st(&t.CreatedAt), st(&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 (id, team_id, user_id, role) VALUES (?, ?, ?, ?) ON CONFLICT (team_id, user_id) DO UPDATE SET role = excluded.role`, store.NewID(), 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 = ? AND user_id = ?", 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 = ? WHERE team_id = ? AND user_id = ?", 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, u.role as user_role FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ? 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, &m.UserRole) 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, u.role as user_role FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ? AND tm.user_id = ?`, teamID, userID).Scan( &m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt, &m.Email, &m.DisplayName, &m.Username, &m.UserRole) 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 = ?", 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 = ? AND user_id = ?", 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 = ? AND user_id = ?)", teamID, userID).Scan(&exists) return exists, err } // ── CS1 additions (v0.29.0) ───────────────────────────────────────────── func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) { var count int err := DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM teams WHERE id = ?`, teamID).Scan(&count) return count > 0, err } func (s *TeamStore) UpdateMemberRoleByID(ctx context.Context, memberID, teamID, role string) (int64, error) { res, err := DB.ExecContext(ctx, `UPDATE team_members SET role = ? WHERE id = ? AND team_id = ?`, 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 = ? AND team_id = ?`, 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 = ?) 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 = ? 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) { id := store.NewID() _, err := DB.ExecContext(ctx, ` INSERT INTO team_members (id, team_id, user_id, role) VALUES (?, ?, ?, ?)`, id, teamID, userID, role) return id, err } func (s *TeamStore) HasPrivateProviderRequirement(ctx context.Context, userID string) (bool, error) { var has bool err := DB.QueryRowContext(ctx, ` SELECT EXISTS( SELECT 1 FROM team_members tm JOIN teams t ON t.id = tm.team_id WHERE tm.user_id = ? AND t.is_active = 1 AND json_extract(t.settings, '$.require_private_providers') = 'true' )`, userID).Scan(&has) return has, err } func (s *TeamStore) MergeSettings(ctx context.Context, teamID, settingsJSON string) error { _, err := DB.ExecContext(ctx, `UPDATE teams SET settings = json_patch(COALESCE(settings, '{}'), ?) WHERE id = ?`, settingsJSON, teamID) return err }