diff --git a/frontend/src/features/settings/AdminSettingsView.tsx b/frontend/src/features/settings/AdminSettingsView.tsx index a344b11..24b00fd 100644 --- a/frontend/src/features/settings/AdminSettingsView.tsx +++ b/frontend/src/features/settings/AdminSettingsView.tsx @@ -31,7 +31,7 @@ interface AdminSettingsViewProps { onBack: () => void; } -type AdminSection = 'users' | 'orgs' | 'server' | 'smtp'; +type AdminSection = 'users' | 'teams' | 'server' | 'smtp'; export const AdminSettingsView: React.FC = ({ currentUser, @@ -56,11 +56,8 @@ export const AdminSettingsView: React.FC = ({ const [resetPwdUserId, setResetPwdUserId] = useState(null); const [resetPwdVal, setResetPwdVal] = useState(''); - // --- Organizations & Teams State --- - const [orgs, setOrgs] = useState([]); + // --- Teams & Groups State --- const [allTeams, setAllTeams] = useState([]); - const [newOrgName, setNewOrgName] = useState(''); - const [selectedOrgId, setSelectedOrgId] = useState(''); const [newTeamName, setNewTeamName] = useState(''); const [newTeamDesc, setNewTeamDesc] = useState(''); const [memberSelectByTeam, setMemberSelectByTeam] = useState>({}); @@ -86,19 +83,14 @@ export const AdminSettingsView: React.FC = ({ const loadAll = async () => { try { - const [uList, invList, orgList, teamList] = await Promise.all([ + const [uList, invList, teamList] = await Promise.all([ api.adminGetUsers(), api.adminGetInvites(), - api.getOrganizations(), api.getUserTeams(), ]); setUsers(uList); setInvites(invList); - setOrgs(orgList); setAllTeams(teamList); - if (orgList.length > 0 && !selectedOrgId) { - setSelectedOrgId(orgList[0].id); - } if (isServerAdmin) { const sys = await api.adminGetSettings(); @@ -240,62 +232,33 @@ export const AdminSettingsView: React.FC = ({ }; // ============================================================================ - // HANDLERS: ORGANIZATIONS & TEAMS + // HANDLERS: TEAMS & GRUPPEN // ============================================================================ - const handleCreateOrg = async (e: React.FormEvent) => { - e.preventDefault(); - if (!newOrgName.trim()) return; - setStatusMsg(null); - try { - const created = await api.createOrganization(newOrgName.trim()); - setNewOrgName(''); - setSelectedOrgId(created.id); - await loadAll(); - setStatusMsg({ type: 'ok', text: `Organisation "${created.name}" angelegt.` }); - } catch (err: any) { - setStatusMsg({ type: 'err', text: err.message }); - } - }; - - const handleDeleteOrg = async (org: Organization) => { - if (!window.confirm(`Organisation "${org.name}" inklusive aller zugehörigen Teams löschen?`)) - return; - setStatusMsg(null); - try { - await api.deleteOrganization(org.id); - await loadAll(); - setStatusMsg({ type: 'ok', text: `Organisation "${org.name}" gelöscht.` }); - } catch (err: any) { - setStatusMsg({ type: 'err', text: err.message }); - } - }; - const handleCreateTeam = async (e: React.FormEvent) => { e.preventDefault(); - if (!selectedOrgId || !newTeamName.trim()) return; + if (!newTeamName.trim()) return; setStatusMsg(null); try { - const created = await api.createOrgTeam( - selectedOrgId, + const created = await api.createTeam( newTeamName.trim(), newTeamDesc.trim() ); setNewTeamName(''); setNewTeamDesc(''); await loadAll(); - setStatusMsg({ type: 'ok', text: `Team "${created.name}" wurde erstellt.` }); + setStatusMsg({ type: 'ok', text: `Gruppe / Team "${created.name}" wurde erstellt.` }); } catch (err: any) { setStatusMsg({ type: 'err', text: err.message }); } }; const handleDeleteTeam = async (team: Team) => { - if (!window.confirm(`Team "${team.name}" wirklich löschen?`)) return; + if (!window.confirm(`Gruppe / Team "${team.name}" wirklich löschen?`)) return; setStatusMsg(null); try { await api.deleteTeam(team.id); await loadAll(); - setStatusMsg({ type: 'ok', text: `Team "${team.name}" gelöscht.` }); + setStatusMsg({ type: 'ok', text: `Gruppe / Team "${team.name}" gelöscht.` }); } catch (err: any) { setStatusMsg({ type: 'err', text: err.message }); } @@ -308,8 +271,9 @@ export const AdminSettingsView: React.FC = ({ setStatusMsg(null); try { await api.addTeamMember(teamId, { userId: selectedUserId, role }); + setMemberSelectByTeam((prev) => ({ ...prev, [teamId]: '' })); await loadAll(); - setStatusMsg({ type: 'ok', text: 'Teammitglied hinzugefügt.' }); + setStatusMsg({ type: 'ok', text: 'Mitglied zur Gruppe hinzugefügt.' }); } catch (err: any) { setStatusMsg({ type: 'err', text: err.message }); } @@ -320,6 +284,7 @@ export const AdminSettingsView: React.FC = ({ try { await api.removeTeamMember(teamId, userId); await loadAll(); + setStatusMsg({ type: 'ok', text: 'Mitglied aus Gruppe entfernt.' }); } catch (err: any) { setStatusMsg({ type: 'err', text: err.message }); } @@ -362,11 +327,8 @@ export const AdminSettingsView: React.FC = ({ } }; - // Collect all teams across orgs for invite dropdown - const flattenedTeams: Team[] = - orgs.flatMap((o) => o.teams || []).length > 0 - ? orgs.flatMap((o) => o.teams || []) - : allTeams; + // All teams for invite dropdown + const flattenedTeams: Team[] = allTeams; return (
@@ -440,17 +402,17 @@ export const AdminSettingsView: React.FC = ({ @@ -797,230 +759,195 @@ export const AdminSettingsView: React.FC = ({ )} {/* ================================================================= */} - {/* SECTION 2: ORGANISATIONEN & TEAMS */} + {/* SECTION 2: TEAMS & GRUPPEN */} {/* ================================================================= */} - {section === 'orgs' && ( + {section === 'teams' && (
-

Organisationen & Teams verwalten

+

Teams & Gruppen verwalten

- Erstelle Organisationen und unterteile sie in Teams. Weise Benutzern Teams zu, damit - sie im Modus "Teams isoliert" gemeinsam an Listen und Aufgaben arbeiten können. + Erstelle Gruppen wie Familie, Arbeit oder Freunde. So kannst du Notizen und Listen + direkt für eine gesamte Gruppe freigeben, ohne jeden Benutzer einzeln auswählen zu müssen.

- {/* Create Organization & Team */} -
-
-

1. Neue Organisation gründen

-
- setNewOrgName(e.target.value)} - placeholder="z.B. Homelab, Familie, Firma…" - required - className="flex-1 min-w-0 h-8 px-2.5 input-field" - /> - -
-
- -
-

2. Neues Team in Organisation anlegen

- {orgs.length === 0 ? ( -

- Bitte lege zuerst links eine Organisation an. -

- ) : ( -
-
- - setNewTeamName(e.target.value)} - placeholder="Team-Name (z.B. Infrastruktur)" - required - className="w-full min-w-0 h-8 px-2.5 input-field" - /> -
-
- setNewTeamDesc(e.target.value)} - placeholder="Kurzbeschreibung (optional)…" - className="flex-1 min-w-0 h-8 px-2.5 input-field" - /> - -
-
- )} -
+
+
- {/* Organizations & Nested Teams Display */} + {/* Teams & Groups List */}
- {orgs.length === 0 ? ( -
- Noch keine Organisationen vorhanden. +
+

+ Bestehende Gruppen & Teams ({allTeams.length}) +

+
+ + {allTeams.length === 0 ? ( +
+ +

Noch keine Teams oder Gruppen vorhanden.

+

+ Lege oben deine erste Gruppe an (z. B. "Familie" oder "Arbeit"), um Notizen und Listen gemeinsam zu nutzen. +

) : ( - orgs.map((org) => { - const orgTeams = Array.isArray(org.teams) ? org.teams : []; + allTeams.map((team) => { + const members = Array.isArray(team.members) ? team.members : []; + const availableUsers = users.filter( + (u) => !members.some((m) => m.userId === u.id) + ); return ( -
-
-
- - {org.name} - - ({orgTeams.length} {orgTeams.length === 1 ? 'Team' : 'Teams'}) - +
+
+
+
+ +
+
+
+ {team.name} + + {members.length} {members.length === 1 ? 'Mitglied' : 'Mitglieder'} + +
+ {team.description && ( +

+ {team.description} +

+ )} +
-
- {orgTeams.length === 0 ? ( -

- Noch keine Teams in dieser Organisation. + {/* Members Chips */} +

+

+ Mitglieder ({members.length}): +

+ {members.length === 0 ? ( +

+ Noch keine Mitglieder in dieser Gruppe.

) : ( - orgTeams.map((team) => { - const members = Array.isArray(team.members) ? team.members : []; - return ( +
+ {members.map((m) => (
-
-
- - - {team.name} - - {team.description && ( -

- {team.description} -

- )} -
- -
- - {/* Team Members */} -
-

- Mitglieder ({members.length}): -

- {members.length === 0 ? ( -

- Noch keine Mitglieder zugewiesen. -

- ) : ( -
- {members.map((m) => ( -
- - {m.displayName} - - - ({m.role}) - - -
- ))} -
- )} -
- - {/* Add User to Team */} -
- - - - - -
+ + {m.displayName} + + + ({m.role === 'team_admin' ? 'Gruppen-Admin' : 'Mitglied'}) + +
- ); - }) + ))} +
)}
+ + {/* Add User to Team Row */} +
+ + + + + +
); }) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index cc7fbfe..82df2e3 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -664,6 +664,13 @@ export const api = { }); }, + async createTeam(name: string, description = ''): Promise { + return request('/api/v1/teams', { + method: 'POST', + body: JSON.stringify({ name, description }), + }); + }, + async deleteTeam(teamId: string): Promise { await request(`/api/v1/teams/${teamId}`, { method: 'DELETE' }); }, diff --git a/internal/api/server.go b/internal/api/server.go index 5afebda..26702ee 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -144,6 +144,7 @@ func (s *Server) Router() http.Handler { r.Get("/api/v1/organizations/{id}/teams", s.handleGetOrgTeams) r.Post("/api/v1/organizations/{id}/teams", s.handleCreateOrgTeam) r.Get("/api/v1/teams", s.handleGetUserTeams) + r.Post("/api/v1/teams", s.handleCreateTeamDirect) r.Delete("/api/v1/teams/{id}", s.handleDeleteTeam) r.Post("/api/v1/teams/{id}/members", s.handleAddTeamMember) r.Delete("/api/v1/teams/{id}/members/{userId}", s.handleRemoveTeamMember) @@ -1691,8 +1692,47 @@ func (s *Server) handleDeleteTeam(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } +func (s *Server) handleCreateTeamDirect(w http.ResponseWriter, r *http.Request) { + userID := auth.GetUserID(r.Context()) + var req struct { + Name string `json:"name"` + Description string `json:"description"` + OrgID string `json:"orgId"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Ungültiges JSON") + return + } + name := strings.TrimSpace(req.Name) + if name == "" { + writeError(w, http.StatusBadRequest, "Name für das Team / die Gruppe ist erforderlich") + return + } + orgID := strings.TrimSpace(req.OrgID) + if orgID == "" { + orgID = "default" + } + team, err := s.store.CreateTeam(orgID, name, req.Description) + if err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + // Automatically add creator to team as team_admin + _ = s.store.AddTeamMember(team.ID, userID, "team_admin") + team.Members, _ = s.store.GetTeamMembers(team.ID) + writeJSON(w, http.StatusCreated, team) +} + func (s *Server) handleGetUserTeams(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) + user, _ := s.store.GetUserByID(userID) + if (r.URL.Query().Get("all") == "true") || (user != nil && (user.Role == "admin" || user.Role == "team_admin")) { + teams, err := s.store.GetAllTeams() + if err == nil { + writeJSON(w, http.StatusOK, teams) + return + } + } teams, err := s.store.GetTeamsForUser(userID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index d3f0389..87f77b7 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -2752,6 +2752,30 @@ func (s *Store) DeleteTeam(teamID string) error { return err } +func (s *Store) GetAllTeams() ([]domain.Team, error) { + rows, err := s.db.Query( + `SELECT id, org_id, name, description, created_at FROM teams ORDER BY name COLLATE NOCASE ASC`, + ) + if err != nil { + return []domain.Team{}, err + } + defer rows.Close() + + teams := make([]domain.Team, 0) + for rows.Next() { + var t domain.Team + if err := rows.Scan(&t.ID, &t.OrgID, &t.Name, &t.Description, &t.CreatedAt); err == nil { + t.Members = []domain.TeamMember{} + teams = append(teams, t) + } + } + for i := range teams { + members, _ := s.GetTeamMembers(teams[i].ID) + teams[i].Members = members + } + return teams, nil +} + func (s *Store) GetTeamsForOrg(orgID string) ([]domain.Team, error) { rows, err := s.db.Query( `SELECT id, org_id, name, description, created_at FROM teams WHERE org_id = ? ORDER BY created_at ASC`,