feat: unify sharing UI, redesign lists header, fix team sharing, and add user autocomplete
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 18s

This commit is contained in:
sandrodach committed 2026-10-25 16:04:08 +01:00
1 parent 6d07352e80
commit 354129b2d4
10 files changed
+1049 -393

No files matched your search

+330 -40
View File
@@ -337,6 +337,14 @@ func (s *Store) migrate() error {
"CREATE TABLE IF NOT EXISTS task_list_shares (id TEXT PRIMARY KEY, task_list_id TEXT NOT NULL, target_type TEXT NOT NULL CHECK(target_type IN ('team', 'user')), target_id TEXT NOT NULL, created_at TEXT NOT NULL, created_by TEXT NOT NULL, UNIQUE(task_list_id, target_type, target_id));",
"CREATE INDEX IF NOT EXISTS idx_task_list_shares_target ON task_list_shares(target_type, target_id);",
"CREATE INDEX IF NOT EXISTS idx_task_list_shares_list ON task_list_shares(task_list_id);",
"CREATE TABLE IF NOT EXISTS doc_team_shares (id TEXT PRIMARY KEY, doc_id TEXT NOT NULL, team_id TEXT NOT NULL, permission TEXT NOT NULL CHECK(permission IN ('read', 'write')), created_at TEXT NOT NULL, created_by TEXT NOT NULL, UNIQUE(doc_id, team_id));",
"CREATE INDEX IF NOT EXISTS idx_doc_team_shares_team ON doc_team_shares(team_id);",
"CREATE INDEX IF NOT EXISTS idx_doc_team_shares_doc ON doc_team_shares(doc_id);",
"CREATE TABLE IF NOT EXISTS space_team_shares (id TEXT PRIMARY KEY, space_id TEXT NOT NULL, team_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'editor', created_at TEXT NOT NULL, created_by TEXT NOT NULL, UNIQUE(space_id, team_id));",
"CREATE INDEX IF NOT EXISTS idx_space_team_shares_team ON space_team_shares(team_id);",
"CREATE INDEX IF NOT EXISTS idx_space_team_shares_space ON space_team_shares(space_id);",
}
for _, q := range columnMigrations {
_, _ = s.db.Exec(q)
@@ -1331,19 +1339,26 @@ func (s *Store) GetDocuments(userID string) ([]domain.Document, error) {
}
_ = rows.Close()
// 2. Fetch documents shared with the user (with per-user is_pinned from doc_pins)
// 2. Fetch documents shared with the user directly or via teams (with per-user is_pinned from doc_pins)
sharedRows, err := s.db.Query(
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
d.is_archived, d.created_at, d.updated_at,
s.permission, u.display_name
COALESCE(s.permission, dts.permission, 'read') AS permission, u.display_name
FROM documents d
JOIN doc_shares s ON d.id = s.doc_id
JOIN users u ON d.user_id = u.id
LEFT JOIN doc_shares s ON d.id = s.doc_id AND s.user_id = ?
LEFT JOIN (
SELECT dts2.doc_id, dts2.permission
FROM doc_team_shares dts2
JOIN team_members tm ON tm.team_id = dts2.team_id
WHERE tm.user_id = ?
) dts ON d.id = dts.doc_id
LEFT JOIN doc_pins p ON p.doc_id = d.id AND p.user_id = ?
WHERE s.user_id = ? AND d.is_archived = 0
WHERE (s.user_id = ? OR dts.doc_id IS NOT NULL) AND d.is_archived = 0 AND d.user_id != ?
GROUP BY d.id
ORDER BY is_pinned DESC, d.updated_at DESC`,
userID, userID,
userID, userID, userID, userID, userID,
)
if err == nil {
for sharedRows.Next() {
@@ -1404,18 +1419,25 @@ func (s *Store) GetDocumentByID(userID, docID string) (*domain.Document, error)
return doc, nil
}
// Try shared document (with per-user is_pinned)
// Try shared document (direct or team share, with per-user is_pinned)
sharedRow := s.db.QueryRow(
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
d.is_archived, d.created_at, d.updated_at,
s.permission, u.display_name
COALESCE(s.permission, dts.permission, 'read') AS permission, u.display_name
FROM documents d
JOIN doc_shares s ON d.id = s.doc_id
JOIN users u ON d.user_id = u.id
LEFT JOIN doc_shares s ON d.id = s.doc_id AND s.user_id = ?
LEFT JOIN (
SELECT dts2.doc_id, dts2.permission
FROM doc_team_shares dts2
JOIN team_members tm ON tm.team_id = dts2.team_id
WHERE tm.user_id = ?
) dts ON d.id = dts.doc_id
LEFT JOIN doc_pins p ON p.doc_id = d.id AND p.user_id = ?
WHERE d.id = ? AND s.user_id = ?`,
userID, docID, userID,
WHERE d.id = ? AND (s.user_id = ? OR dts.doc_id IS NOT NULL)
LIMIT 1`,
userID, userID, userID, docID, userID,
)
d := &domain.Document{}
var blocksStr, tagsStr string
@@ -1630,11 +1652,17 @@ func (s *Store) GetWorkspaceTags(userID string) ([]domain.TagSummary, error) {
FROM doc_block_tags t
JOIN documents d ON t.doc_id = d.id
LEFT JOIN doc_shares s ON s.doc_id = d.id AND s.user_id = ?
WHERE (d.user_id = ? OR s.user_id = ?)
LEFT JOIN (
SELECT dts2.doc_id
FROM doc_team_shares dts2
JOIN team_members tm ON tm.team_id = dts2.team_id
WHERE tm.user_id = ?
) dts ON d.id = dts.doc_id
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL)
AND d.is_archived = 0
GROUP BY t.tag
ORDER BY count DESC, t.tag ASC`,
userID, userID, userID,
userID, userID, userID, userID,
)
if err != nil {
return nil, err
@@ -1686,16 +1714,22 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
rows, err := s.db.Query(
`SELECT t.block_id, d.id, d.title, d.icon, d.folder_path, d.blocks_json, d.updated_at, d.user_id,
COALESCE(u.display_name, '') as owner_name,
COALESCE(s.permission, '') as shared_permission
COALESCE(s.permission, dts.permission, '') as shared_permission
FROM doc_block_tags t
JOIN documents d ON t.doc_id = d.id
LEFT JOIN users u ON d.user_id = u.id
LEFT JOIN doc_shares s ON s.doc_id = d.id AND s.user_id = ?
WHERE (d.user_id = ? OR s.user_id = ?)
LEFT JOIN (
SELECT dts2.doc_id, dts2.permission
FROM doc_team_shares dts2
JOIN team_members tm ON tm.team_id = dts2.team_id
WHERE tm.user_id = ?
) dts ON d.id = dts.doc_id
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL)
AND t.tag = ?
AND d.is_archived = 0
ORDER BY d.updated_at DESC`,
userID, userID, userID, cleanTag,
userID, userID, userID, userID, cleanTag,
)
if err != nil {
return nil, err
@@ -1946,7 +1980,10 @@ func (s *Store) DeleteFolder(userID, folderPath string) ([]string, error) {
}
func (s *Store) GetDocShares(docID string) ([]domain.DocShare, error) {
rows, err := s.db.Query(
shares := make([]domain.DocShare, 0)
// 1. Direct user shares
uRows, err := s.db.Query(
`SELECT s.id, s.doc_id, s.user_id, u.email, u.display_name, s.permission, s.created_by, s.created_at
FROM doc_shares s
JOIN users u ON s.user_id = u.id
@@ -1954,19 +1991,89 @@ func (s *Store) GetDocShares(docID string) ([]domain.DocShare, error) {
ORDER BY s.created_at ASC`,
docID,
)
if err == nil {
for uRows.Next() {
var sh domain.DocShare
if err := uRows.Scan(&sh.ID, &sh.DocID, &sh.UserID, &sh.UserEmail, &sh.DisplayName, &sh.Permission, &sh.CreatedBy, &sh.CreatedAt); err == nil {
sh.TargetType = "user"
sh.TargetID = sh.UserID
sh.TargetName = sh.DisplayName
shares = append(shares, sh)
}
}
_ = uRows.Close()
}
// 2. Team shares
tRows, err := s.db.Query(
`SELECT s.id, s.doc_id, s.team_id, t.name, s.permission, s.created_by, s.created_at
FROM doc_team_shares s
JOIN teams t ON s.team_id = t.id
WHERE s.doc_id = ?
ORDER BY s.created_at ASC`,
docID,
)
if err == nil {
for tRows.Next() {
var sh domain.DocShare
var teamID, teamName string
if err := tRows.Scan(&sh.ID, &sh.DocID, &teamID, &teamName, &sh.Permission, &sh.CreatedBy, &sh.CreatedAt); err == nil {
sh.TargetType = "team"
sh.TargetID = teamID
sh.TargetName = teamName
sh.DisplayName = teamName
shares = append(shares, sh)
}
}
_ = tRows.Close()
}
return shares, nil
}
func (s *Store) CreateDocTeamShare(currentUserID, docID, teamID, permission string) (*domain.DocShare, error) {
doc, err := s.GetDocumentByID(currentUserID, docID)
if err != nil {
return nil, err
}
defer rows.Close()
shares := make([]domain.DocShare, 0)
for rows.Next() {
var sh domain.DocShare
if err := rows.Scan(&sh.ID, &sh.DocID, &sh.UserID, &sh.UserEmail, &sh.DisplayName, &sh.Permission, &sh.CreatedBy, &sh.CreatedAt); err == nil {
shares = append(shares, sh)
}
if !doc.IsOwner && doc.Permission != "write" {
return nil, fmt.Errorf("nur der Eigentümer oder Bearbeiter kann Freigaben verwalten")
}
return shares, nil
cleanTeamID := strings.TrimSpace(teamID)
var teamName string
err = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, cleanTeamID).Scan(&teamName)
if err != nil {
return nil, fmt.Errorf("team nicht gefunden")
}
if permission != "read" && permission != "write" {
permission = "read"
}
now := domain.NowISO()
shareID := domain.NewID("share")
_, err = s.db.Exec(
`INSERT INTO doc_team_shares (id, doc_id, team_id, permission, created_at, created_by)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(doc_id, team_id) DO UPDATE SET permission = excluded.permission`,
shareID, docID, cleanTeamID, permission, now, currentUserID,
)
if err != nil {
return nil, err
}
return &domain.DocShare{
ID: shareID,
DocID: docID,
TargetType: "team",
TargetID: cleanTeamID,
TargetName: teamName,
DisplayName: teamName,
Permission: permission,
CreatedBy: currentUserID,
CreatedAt: now,
}, nil
}
func (s *Store) CreateDocShare(currentUserID, docID, targetUserIdentifier, permission string) (*domain.DocShare, error) {
@@ -1978,13 +2085,19 @@ func (s *Store) CreateDocShare(currentUserID, docID, targetUserIdentifier, permi
return nil, fmt.Errorf("nur der Eigentümer oder Bearbeiter kann Freigaben verwalten")
}
cleanTarget := strings.TrimSpace(targetUserIdentifier)
// If identifier looks like a team or is prefixed with team_, route to CreateDocTeamShare
if strings.HasPrefix(cleanTarget, "team_") {
return s.CreateDocTeamShare(currentUserID, docID, cleanTarget, permission)
}
if permission != "read" && permission != "write" {
permission = "read"
}
// Resolve target user by ID, username, or Email
var targetUser *domain.User
cleanTarget := strings.TrimSpace(targetUserIdentifier)
if strings.HasPrefix(cleanTarget, "usr_") {
targetUser, _ = s.GetUserByID(cleanTarget)
}
@@ -1995,6 +2108,11 @@ func (s *Store) CreateDocShare(currentUserID, docID, targetUserIdentifier, permi
targetUser, err = s.GetUserByID(cleanTarget)
}
if err != nil || targetUser == nil {
// Check if it's a team ID that doesn't start with team_
var tName string
if s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, cleanTarget).Scan(&tName) == nil {
return s.CreateDocTeamShare(currentUserID, docID, cleanTarget, permission)
}
return nil, fmt.Errorf("benutzer nicht gefunden")
}
@@ -2017,6 +2135,9 @@ func (s *Store) CreateDocShare(currentUserID, docID, targetUserIdentifier, permi
return &domain.DocShare{
ID: shareID,
DocID: docID,
TargetType: "user",
TargetID: targetUser.ID,
TargetName: targetUser.DisplayName,
UserID: targetUser.ID,
UserEmail: targetUser.Email,
DisplayName: targetUser.DisplayName,
@@ -2026,16 +2147,26 @@ func (s *Store) CreateDocShare(currentUserID, docID, targetUserIdentifier, permi
}, nil
}
func (s *Store) DeleteDocShare(currentUserID, docID, targetUserID string) error {
func (s *Store) DeleteDocShare(currentUserID, docID, targetID string) error {
doc, err := s.GetDocumentByID(currentUserID, docID)
if err != nil {
return err
}
if !doc.IsOwner && doc.Permission != "write" && currentUserID != targetUserID {
if !doc.IsOwner && doc.Permission != "write" && currentUserID != targetID {
return fmt.Errorf("keine Berechtigung zum Entfernen der Freigabe")
}
_, err = s.db.Exec(`DELETE FROM doc_shares WHERE doc_id = ? AND user_id = ?`, docID, targetUserID)
cleanTarget := strings.TrimSpace(targetID)
// Try deleting team share first
res, err := s.db.Exec(`DELETE FROM doc_team_shares WHERE doc_id = ? AND (id = ? OR team_id = ?)`, docID, cleanTarget, cleanTarget)
if err == nil {
if rows, _ := res.RowsAffected(); rows > 0 {
return nil
}
}
// Fallback to user share
_, err = s.db.Exec(`DELETE FROM doc_shares WHERE doc_id = ? AND (id = ? OR user_id = ?)`, docID, cleanTarget, cleanTarget)
return err
}
@@ -2186,7 +2317,12 @@ func (s *Store) AddSpaceMember(currentUserID, spaceID, targetUserIdentifier, rol
return nil, errors.New("benutzer nicht gefunden")
}
if role != "owner" && role != "editor" && role != "viewer" {
// Protect existing owner from role downgrade!
var existingRole string
_ = s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, targetUser.ID).Scan(&existingRole)
if existingRole == "owner" {
role = "owner"
} else if role != "owner" && role != "editor" && role != "viewer" {
role = "editor"
}
@@ -2210,6 +2346,135 @@ func (s *Store) AddSpaceMember(currentUserID, spaceID, targetUserIdentifier, rol
}, nil
}
func (s *Store) CreateSpaceTeamShare(currentUserID, spaceID, teamID, role string) (*domain.SpaceShare, error) {
var myRole string
err := s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, currentUserID).Scan(&myRole)
if err != nil || (myRole != "owner" && myRole != "editor") {
return nil, errors.New("keine Berechtigung für diesen Bereich")
}
cleanTeamID := strings.TrimSpace(teamID)
var teamName string
err = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, cleanTeamID).Scan(&teamName)
if err != nil {
return nil, errors.New("team nicht gefunden")
}
if role != "owner" && role != "editor" && role != "viewer" {
role = "editor"
}
now := domain.NowISO()
shareID := domain.NewID("spshare")
_, err = s.db.Exec(
`INSERT INTO space_team_shares (id, space_id, team_id, role, created_at, created_by)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(space_id, team_id) DO UPDATE SET role = excluded.role`,
shareID, spaceID, cleanTeamID, role, now, currentUserID,
)
if err != nil {
return nil, err
}
// Add team members as space members, but preserve owner role!
members, _ := s.GetTeamMembers(cleanTeamID)
for _, tm := range members {
var curRole string
_ = s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, tm.UserID).Scan(&curRole)
if curRole == "owner" {
continue
}
_, _ = s.db.Exec(
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES (?, ?, ?, ?)
ON CONFLICT(space_id, user_id) DO UPDATE SET role = excluded.role`,
spaceID, tm.UserID, role, now,
)
}
return &domain.SpaceShare{
ID: shareID,
SpaceID: spaceID,
TargetType: "team",
TargetID: cleanTeamID,
TargetName: teamName,
Role: role,
CreatedBy: currentUserID,
CreatedAt: now,
}, nil
}
func (s *Store) GetSpaceShares(spaceID string) ([]domain.SpaceShare, error) {
shares := make([]domain.SpaceShare, 0)
// 1. Team shares
tRows, err := s.db.Query(
`SELECT s.id, s.space_id, s.team_id, t.name, s.role, s.created_by, s.created_at
FROM space_team_shares s
JOIN teams t ON s.team_id = t.id
WHERE s.space_id = ?
ORDER BY s.created_at ASC`,
spaceID,
)
if err == nil {
for tRows.Next() {
var sh domain.SpaceShare
if err := tRows.Scan(&sh.ID, &sh.SpaceID, &sh.TargetID, &sh.TargetName, &sh.Role, &sh.CreatedBy, &sh.CreatedAt); err == nil {
sh.TargetType = "team"
shares = append(shares, sh)
}
}
_ = tRows.Close()
}
// 2. Individual invited members (non-owner members)
var ownerID string
_ = s.db.QueryRow(`SELECT owner_id FROM spaces WHERE id = ?`, spaceID).Scan(&ownerID)
mRows, err := s.db.Query(
`SELECT sm.user_id, u.display_name, u.email, sm.role, sm.joined_at
FROM space_members sm
JOIN users u ON u.id = sm.user_id
WHERE sm.space_id = ? AND sm.user_id != ?
ORDER BY sm.joined_at ASC`,
spaceID, ownerID,
)
if err == nil {
for mRows.Next() {
var uid, dName, email, role, joinedAt string
if err := mRows.Scan(&uid, &dName, &email, &role, &joinedAt); err == nil {
name := dName
if name == "" {
name = email
}
shares = append(shares, domain.SpaceShare{
ID: "sm_" + uid,
SpaceID: spaceID,
TargetType: "user",
TargetID: uid,
TargetName: name,
Role: role,
CreatedAt: joinedAt,
})
}
}
_ = mRows.Close()
}
return shares, nil
}
func (s *Store) RemoveSpaceTeamShare(currentUserID, spaceID, targetID string) error {
var myRole string
err := s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, currentUserID).Scan(&myRole)
if err != nil || (myRole != "owner" && myRole != "editor") {
return errors.New("keine Berechtigung zum Entfernen der Freigabe")
}
cleanTarget := strings.TrimSpace(targetID)
_, err = s.db.Exec(`DELETE FROM space_team_shares WHERE space_id = ? AND (id = ? OR team_id = ?)`, spaceID, cleanTarget, cleanTarget)
return err
}
func (s *Store) RemoveSpaceMember(currentUserID, spaceID, targetUserID string) error {
var myRole string
err := s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, currentUserID).Scan(&myRole)
@@ -2217,12 +2482,17 @@ func (s *Store) RemoveSpaceMember(currentUserID, spaceID, targetUserID string) e
return errors.New("keine Berechtigung für diesen Bereich")
}
if currentUserID != targetUserID && myRole != "owner" {
cleanTarget := strings.TrimSpace(targetUserID)
if strings.HasPrefix(cleanTarget, "team_") || strings.HasPrefix(cleanTarget, "spshare_") {
return s.RemoveSpaceTeamShare(currentUserID, spaceID, cleanTarget)
}
if currentUserID != cleanTarget && myRole != "owner" {
return errors.New("nur der Eigentümer kann andere Mitglieder entfernen")
}
var targetRole string
_ = s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, targetUserID).Scan(&targetRole)
_ = s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, cleanTarget).Scan(&targetRole)
if targetRole == "owner" {
var ownerCount int
_ = s.db.QueryRow(`SELECT COUNT(*) FROM space_members WHERE space_id = ? AND role = 'owner'`, spaceID).Scan(&ownerCount)
@@ -2231,7 +2501,7 @@ func (s *Store) RemoveSpaceMember(currentUserID, spaceID, targetUserID string) e
}
}
_, err = s.db.Exec(`DELETE FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, targetUserID)
_, err = s.db.Exec(`DELETE FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, cleanTarget)
return err
}
@@ -2239,10 +2509,12 @@ func (s *Store) GetSpacesForUser(userID string) ([]domain.Space, error) {
rows, err := s.db.Query(
`SELECT s.id, s.name, s.icon, s.description, s.invite_code, s.is_personal, s.owner_id, s.created_at
FROM spaces s
JOIN space_members sm ON sm.space_id = s.id
WHERE sm.user_id = ?
LEFT JOIN space_members sm ON sm.space_id = s.id AND sm.user_id = ?
LEFT JOIN space_team_shares sts ON sts.space_id = s.id AND sts.team_id IN (SELECT team_id FROM team_members WHERE user_id = ?)
WHERE sm.user_id = ? OR sts.team_id IS NOT NULL
GROUP BY s.id
ORDER BY s.created_at ASC`,
userID,
userID, userID, userID,
)
if err != nil {
return nil, err
@@ -2261,6 +2533,7 @@ func (s *Store) GetSpacesForUser(userID string) ([]domain.Space, error) {
for i := range spaces {
spaces[i].Members, _ = s.GetSpaceMembers(spaces[i].ID)
spaces[i].Shares, _ = s.GetSpaceShares(spaces[i].ID)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM lists WHERE space_id = ?`, spaces[i].ID).Scan(&spaces[i].ListsCount)
}
return spaces, nil
@@ -2729,12 +3002,15 @@ func (s *Store) AddTaskListShare(taskListID, targetType, targetID, createdBy str
_, err := s.db.Exec(
`INSERT INTO task_list_shares (id, task_list_id, target_type, target_id, created_at, created_by)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(task_list_id, target_type, target_id) DO NOTHING`,
ON CONFLICT(task_list_id, target_type, target_id) DO UPDATE SET created_at = excluded.created_at`,
share.ID, share.TaskListID, share.TargetType, share.TargetID, share.CreatedAt, createdBy,
)
if err != nil {
return nil, err
}
_ = s.db.QueryRow(`SELECT id, created_at FROM task_list_shares WHERE task_list_id = ? AND target_type = ? AND target_id = ?`,
taskListID, targetType, targetID).Scan(&share.ID, &share.CreatedAt)
if targetType == "team" {
_, _ = s.db.Exec(`UPDATE task_lists SET team_id = ? WHERE id = ? AND (team_id = '' OR team_id IS NULL)`, targetID, taskListID)
_ = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, targetID).Scan(&share.TargetName)
@@ -3181,14 +3457,28 @@ func (s *Store) GetTeamsForOrg(orgID string) ([]domain.Team, error) {
return teams, nil
}
func (s *Store) GetTeamByID(teamID string) (*domain.Team, error) {
var t domain.Team
err := s.db.QueryRow(
`SELECT id, org_id, name, description, created_at FROM teams WHERE id = ?`,
teamID,
).Scan(&t.ID, &t.OrgID, &t.Name, &t.Description, &t.CreatedAt)
if err != nil {
return nil, err
}
t.Members, _ = s.GetTeamMembers(t.ID)
return &t, nil
}
func (s *Store) GetTeamsForUser(userID string) ([]domain.Team, error) {
rows, err := s.db.Query(
`SELECT DISTINCT t.id, t.org_id, t.name, t.description, t.created_at
FROM teams t
JOIN team_members tm ON tm.team_id = t.id
WHERE tm.user_id = ?
LEFT JOIN team_members tm ON tm.team_id = t.id
LEFT JOIN organizations o ON o.id = t.org_id
WHERE tm.user_id = ? OR o.owner_id = ?
ORDER BY t.created_at ASC`,
userID,
userID, userID,
)
if err != nil {
return []domain.Team{}, err