Dev #3

Merged
sandrodach merged 30 commits from dev into main 2026-10-25 18:00:10 +01:00
4 changed files with 251 additions and 253 deletions
Showing only changes of commit cfc0477b5c - Show all commits

No files matched your search

@@ -31,7 +31,7 @@ interface AdminSettingsViewProps {
onBack: () => void; onBack: () => void;
} }
type AdminSection = 'users' | 'orgs' | 'server' | 'smtp'; type AdminSection = 'users' | 'teams' | 'server' | 'smtp';
export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
currentUser, currentUser,
@@ -56,11 +56,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
const [resetPwdUserId, setResetPwdUserId] = useState<string | null>(null); const [resetPwdUserId, setResetPwdUserId] = useState<string | null>(null);
const [resetPwdVal, setResetPwdVal] = useState(''); const [resetPwdVal, setResetPwdVal] = useState('');
// --- Organizations & Teams State --- // --- Teams & Groups State ---
const [orgs, setOrgs] = useState<Organization[]>([]);
const [allTeams, setAllTeams] = useState<Team[]>([]); const [allTeams, setAllTeams] = useState<Team[]>([]);
const [newOrgName, setNewOrgName] = useState('');
const [selectedOrgId, setSelectedOrgId] = useState('');
const [newTeamName, setNewTeamName] = useState(''); const [newTeamName, setNewTeamName] = useState('');
const [newTeamDesc, setNewTeamDesc] = useState(''); const [newTeamDesc, setNewTeamDesc] = useState('');
const [memberSelectByTeam, setMemberSelectByTeam] = useState<Record<string, string>>({}); const [memberSelectByTeam, setMemberSelectByTeam] = useState<Record<string, string>>({});
@@ -86,19 +83,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
const loadAll = async () => { const loadAll = async () => {
try { try {
const [uList, invList, orgList, teamList] = await Promise.all([ const [uList, invList, teamList] = await Promise.all([
api.adminGetUsers(), api.adminGetUsers(),
api.adminGetInvites(), api.adminGetInvites(),
api.getOrganizations(),
api.getUserTeams(), api.getUserTeams(),
]); ]);
setUsers(uList); setUsers(uList);
setInvites(invList); setInvites(invList);
setOrgs(orgList);
setAllTeams(teamList); setAllTeams(teamList);
if (orgList.length > 0 && !selectedOrgId) {
setSelectedOrgId(orgList[0].id);
}
if (isServerAdmin) { if (isServerAdmin) {
const sys = await api.adminGetSettings(); const sys = await api.adminGetSettings();
@@ -240,62 +232,33 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
}; };
// ============================================================================ // ============================================================================
// 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) => { const handleCreateTeam = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!selectedOrgId || !newTeamName.trim()) return; if (!newTeamName.trim()) return;
setStatusMsg(null); setStatusMsg(null);
try { try {
const created = await api.createOrgTeam( const created = await api.createTeam(
selectedOrgId,
newTeamName.trim(), newTeamName.trim(),
newTeamDesc.trim() newTeamDesc.trim()
); );
setNewTeamName(''); setNewTeamName('');
setNewTeamDesc(''); setNewTeamDesc('');
await loadAll(); await loadAll();
setStatusMsg({ type: 'ok', text: `Team "${created.name}" wurde erstellt.` }); setStatusMsg({ type: 'ok', text: `Gruppe / Team "${created.name}" wurde erstellt.` });
} catch (err: any) { } catch (err: any) {
setStatusMsg({ type: 'err', text: err.message }); setStatusMsg({ type: 'err', text: err.message });
} }
}; };
const handleDeleteTeam = async (team: Team) => { 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); setStatusMsg(null);
try { try {
await api.deleteTeam(team.id); await api.deleteTeam(team.id);
await loadAll(); await loadAll();
setStatusMsg({ type: 'ok', text: `Team "${team.name}" gelöscht.` }); setStatusMsg({ type: 'ok', text: `Gruppe / Team "${team.name}" gelöscht.` });
} catch (err: any) { } catch (err: any) {
setStatusMsg({ type: 'err', text: err.message }); setStatusMsg({ type: 'err', text: err.message });
} }
@@ -308,8 +271,9 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
setStatusMsg(null); setStatusMsg(null);
try { try {
await api.addTeamMember(teamId, { userId: selectedUserId, role }); await api.addTeamMember(teamId, { userId: selectedUserId, role });
setMemberSelectByTeam((prev) => ({ ...prev, [teamId]: '' }));
await loadAll(); await loadAll();
setStatusMsg({ type: 'ok', text: 'Teammitglied hinzugefügt.' }); setStatusMsg({ type: 'ok', text: 'Mitglied zur Gruppe hinzugefügt.' });
} catch (err: any) { } catch (err: any) {
setStatusMsg({ type: 'err', text: err.message }); setStatusMsg({ type: 'err', text: err.message });
} }
@@ -320,6 +284,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
try { try {
await api.removeTeamMember(teamId, userId); await api.removeTeamMember(teamId, userId);
await loadAll(); await loadAll();
setStatusMsg({ type: 'ok', text: 'Mitglied aus Gruppe entfernt.' });
} catch (err: any) { } catch (err: any) {
setStatusMsg({ type: 'err', text: err.message }); setStatusMsg({ type: 'err', text: err.message });
} }
@@ -362,11 +327,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
} }
}; };
// Collect all teams across orgs for invite dropdown // All teams for invite dropdown
const flattenedTeams: Team[] = const flattenedTeams: Team[] = allTeams;
orgs.flatMap((o) => o.teams || []).length > 0
? orgs.flatMap((o) => o.teams || [])
: allTeams;
return ( return (
<div className="w-full space-y-4"> <div className="w-full space-y-4">
@@ -440,17 +402,17 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</button> </button>
<button <button
onClick={() => setSection('orgs')} onClick={() => setSection('teams')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'orgs' section === 'teams'
? 'bg-panel-elevated text-main border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-canvas/50' : 'text-muted hover:text-main hover:bg-canvas/50'
}`} }`}
> >
<Building2 className="w-4 h-4 text-accent flex-shrink-0" /> <Users className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">Organisationen & Teams</p> <p className="font-semibold">Teams & Gruppen</p>
<p className="text-[10px] text-faint">Teams & Mitglieder zuweisen</p> <p className="text-[10px] text-faint">Familie, Arbeit, Freunde zum Teilen</p>
</div> </div>
</button> </button>
@@ -797,230 +759,195 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
)} )}
{/* ================================================================= */} {/* ================================================================= */}
{/* SECTION 2: ORGANISATIONEN & TEAMS */} {/* SECTION 2: TEAMS & GRUPPEN */}
{/* ================================================================= */} {/* ================================================================= */}
{section === 'orgs' && ( {section === 'teams' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-border pb-3"> <div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">Organisationen & Teams verwalten</h2> <h2 className="text-sm font-bold text-main">Teams & Gruppen verwalten</h2>
<p className="text-muted mt-0.5"> <p className="text-muted mt-0.5">
Erstelle Organisationen und unterteile sie in Teams. Weise Benutzern Teams zu, damit Erstelle Gruppen wie Familie, Arbeit oder Freunde. So kannst du Notizen und Listen
sie im Modus &quot;Teams isoliert&quot; gemeinsam an Listen und Aufgaben arbeiten können. direkt für eine gesamte Gruppe freigeben, ohne jeden Benutzer einzeln auswählen zu müssen.
</p> </p>
</div> </div>
{/* Create Organization & Team */} {/* Create Team/Group Card */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<div className="p-4 bg-canvas border border-border rounded-card space-y-3"> <h3 className="font-bold text-main flex items-center gap-2">
<h3 className="font-bold text-main">1. Neue Organisation gründen</h3> <Plus className="w-4 h-4 text-accent" />
<form onSubmit={handleCreateOrg} className="flex gap-2"> <span>Neue Gruppe / Neues Team anlegen</span>
<input </h3>
type="text" <form onSubmit={handleCreateTeam} className="space-y-3">
value={newOrgName} <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
onChange={(e) => setNewOrgName(e.target.value)} <div>
placeholder="z.B. Homelab, Familie, Firma…" <label className="block text-muted text-[11px] mb-1 font-medium">
required Name der Gruppe / des Teams *
className="flex-1 min-w-0 h-8 px-2.5 input-field" </label>
/> <input
<button type="submit" className="btn-primary h-8 px-3 text-xs flex-shrink-0"> type="text"
Anlegen value={newTeamName}
onChange={(e) => setNewTeamName(e.target.value)}
placeholder="z. B. Familie, Arbeit, Freunde…"
required
className="w-full h-8 px-2.5 input-field text-xs"
/>
</div>
<div>
<label className="block text-muted text-[11px] mb-1 font-medium">
Beschreibung (optional)
</label>
<input
type="text"
value={newTeamDesc}
onChange={(e) => setNewTeamDesc(e.target.value)}
placeholder="z. B. Gemeinsame Einkaufslisten & Haushaltsnotizen…"
className="w-full h-8 px-2.5 input-field text-xs"
/>
</div>
</div>
<div className="flex justify-end">
<button type="submit" className="btn-primary h-8 px-4 text-xs flex items-center gap-1.5">
<Plus className="w-3.5 h-3.5" />
<span>Gruppe / Team anlegen</span>
</button> </button>
</form> </div>
</div> </form>
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<h3 className="font-bold text-main">2. Neues Team in Organisation anlegen</h3>
{orgs.length === 0 ? (
<p className="text-faint">
Bitte lege zuerst links eine Organisation an.
</p>
) : (
<form onSubmit={handleCreateTeam} className="space-y-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<select
value={selectedOrgId}
onChange={(e) => setSelectedOrgId(e.target.value)}
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
>
{orgs.map((o) => (
<option key={o.id} value={o.id}>
Org: {o.name}
</option>
))}
</select>
<input
type="text"
value={newTeamName}
onChange={(e) => setNewTeamName(e.target.value)}
placeholder="Team-Name (z.B. Infrastruktur)"
required
className="w-full min-w-0 h-8 px-2.5 input-field"
/>
</div>
<div className="flex gap-2">
<input
type="text"
value={newTeamDesc}
onChange={(e) => setNewTeamDesc(e.target.value)}
placeholder="Kurzbeschreibung (optional)…"
className="flex-1 min-w-0 h-8 px-2.5 input-field"
/>
<button type="submit" className="btn-primary h-8 px-3 text-xs flex-shrink-0">
Team erstellen
</button>
</div>
</form>
)}
</div>
</div> </div>
{/* Organizations & Nested Teams Display */} {/* Teams & Groups List */}
<div className="space-y-4"> <div className="space-y-4">
{orgs.length === 0 ? ( <div className="flex items-center justify-between">
<div className="p-6 bg-canvas border border-border rounded-card text-center text-faint"> <h3 className="font-bold text-main text-xs uppercase tracking-wider text-muted">
Noch keine Organisationen vorhanden. Bestehende Gruppen & Teams ({allTeams.length})
</h3>
</div>
{allTeams.length === 0 ? (
<div className="p-8 bg-canvas border border-border rounded-card text-center space-y-2">
<Users className="w-8 h-8 text-faint mx-auto opacity-50" />
<p className="text-muted text-xs font-medium">Noch keine Teams oder Gruppen vorhanden.</p>
<p className="text-faint text-[11px]">
Lege oben deine erste Gruppe an (z. B. &quot;Familie&quot; oder &quot;Arbeit&quot;), um Notizen und Listen gemeinsam zu nutzen.
</p>
</div> </div>
) : ( ) : (
orgs.map((org) => { allTeams.map((team) => {
const orgTeams = Array.isArray(org.teams) ? org.teams : []; const members = Array.isArray(team.members) ? team.members : [];
const availableUsers = users.filter(
(u) => !members.some((m) => m.userId === u.id)
);
return ( return (
<div key={org.id} className="bg-canvas border border-border rounded-card overflow-hidden"> <div
<div className="p-3 bg-panel-elevated border-b border-border flex items-center justify-between"> key={team.id}
<div className="flex items-center gap-2"> className="p-4 bg-canvas border border-border rounded-card space-y-4"
<Building2 className="w-4 h-4 text-accent" /> >
<span className="font-bold text-sm text-main">{org.name}</span> <div className="flex items-start justify-between gap-3 border-b border-border pb-3">
<span className="text-[11px] text-muted"> <div className="flex items-start gap-2.5">
({orgTeams.length} {orgTeams.length === 1 ? 'Team' : 'Teams'}) <div className="w-8 h-8 rounded-btn bg-accent/10 text-accent flex items-center justify-center flex-shrink-0 mt-0.5">
</span> <Users className="w-4 h-4" />
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-bold text-sm text-main">{team.name}</span>
<span className="text-[11px] px-1.5 py-0.5 rounded-full bg-panel-elevated text-muted border border-border">
{members.length} {members.length === 1 ? 'Mitglied' : 'Mitglieder'}
</span>
</div>
{team.description && (
<p className="text-xs text-muted mt-1">
{team.description}
</p>
)}
</div>
</div> </div>
<button <button
onClick={() => handleDeleteOrg(org)} onClick={() => handleDeleteTeam(team)}
className="btn-icon btn-icon-danger" className="btn-icon btn-icon-danger"
title="Organisation löschen" title="Gruppe / Team löschen"
> >
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
<div className="p-3 space-y-3"> {/* Members Chips */}
{orgTeams.length === 0 ? ( <div className="space-y-2">
<p className="text-faint text-xs"> <p className="text-[11px] font-semibold text-muted">
Noch keine Teams in dieser Organisation. Mitglieder ({members.length}):
</p>
{members.length === 0 ? (
<p className="text-[11px] text-faint">
Noch keine Mitglieder in dieser Gruppe.
</p> </p>
) : ( ) : (
orgTeams.map((team) => { <div className="flex flex-wrap gap-2">
const members = Array.isArray(team.members) ? team.members : []; {members.map((m) => (
return (
<div <div
key={team.id} key={m.userId}
className="p-3 bg-panel border border-border rounded-card space-y-3" className="px-2.5 py-1 bg-panel border border-border rounded-btn flex items-center gap-2 text-xs"
> >
<div className="flex items-center justify-between border-b border-border pb-2"> <span className="text-main font-medium">
<div> {m.displayName}
<span className="font-bold text-main flex items-center gap-1.5"> </span>
<Users className="w-3.5 h-3.5 text-accent" /> <span className="text-[10px] text-muted">
<span>{team.name}</span> ({m.role === 'team_admin' ? 'Gruppen-Admin' : 'Mitglied'})
</span> </span>
{team.description && ( <button
<p className="text-[11px] text-muted mt-0.5"> onClick={() => handleRemoveMemberFromTeam(team.id, m.userId)}
{team.description} className="text-faint hover:text-status-err font-bold ml-0.5"
</p> title="Aus Gruppe entfernen"
)} >
</div> ×
<button </button>
onClick={() => handleDeleteTeam(team)}
className="btn-icon btn-icon-danger"
title="Team löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
{/* Team Members */}
<div className="space-y-1.5">
<p className="text-[11px] font-semibold text-muted">
Mitglieder ({members.length}):
</p>
{members.length === 0 ? (
<p className="text-[11px] text-faint">
Noch keine Mitglieder zugewiesen.
</p>
) : (
<div className="flex flex-wrap gap-1.5">
{members.map((m) => (
<div
key={m.userId}
className="px-2 py-1 bg-canvas border border-border rounded-btn flex items-center gap-2"
>
<span className="text-main font-medium">
{m.displayName}
</span>
<span className="text-[10px] text-muted">
({m.role})
</span>
<button
onClick={() =>
handleRemoveMemberFromTeam(team.id, m.userId)
}
className="text-faint hover:text-status-err"
title="Aus Team entfernen"
>
×
</button>
</div>
))}
</div>
)}
</div>
{/* Add User to Team */}
<div className="pt-2 border-t border-border flex flex-wrap items-center gap-2">
<select
value={memberSelectByTeam[team.id] || ''}
onChange={(e) =>
setMemberSelectByTeam((prev) => ({
...prev,
[team.id]: e.target.value,
}))
}
className="flex-1 min-w-[160px] h-7 px-2 input-field text-[11px] bg-canvas"
>
<option value="">Benutzer auswählen…</option>
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.displayName} ({u.email})
</option>
))}
</select>
<select
value={memberRoleByTeam[team.id] || 'member'}
onChange={(e) =>
setMemberRoleByTeam((prev) => ({
...prev,
[team.id]: e.target.value,
}))
}
className="h-7 px-2 input-field text-[11px] bg-canvas"
>
<option value="member">Mitglied</option>
<option value="team_admin">Team-Admin</option>
</select>
<button
type="button"
onClick={() => handleAddMemberToTeam(team.id)}
disabled={!memberSelectByTeam[team.id]}
className="btn-action h-7 px-2.5 text-[11px] disabled:opacity-40"
>
+ Zum Team hinzufügen
</button>
</div>
</div> </div>
); ))}
}) </div>
)} )}
</div> </div>
{/* Add User to Team Row */}
<div className="pt-3 border-t border-border flex flex-wrap items-center gap-2">
<select
value={memberSelectByTeam[team.id] || ''}
onChange={(e) =>
setMemberSelectByTeam((prev) => ({
...prev,
[team.id]: e.target.value,
}))
}
className="flex-1 min-w-[200px] h-8 px-2.5 input-field text-xs bg-panel"
>
<option value="">Benutzer auswählen zum Hinzufügen…</option>
{availableUsers.map((u) => (
<option key={u.id} value={u.id}>
{u.displayName} ({u.email})
</option>
))}
</select>
<select
value={memberRoleByTeam[team.id] || 'member'}
onChange={(e) =>
setMemberRoleByTeam((prev) => ({
...prev,
[team.id]: e.target.value,
}))
}
className="h-8 px-2.5 input-field text-xs bg-panel"
>
<option value="member">Mitglied</option>
<option value="team_admin">Gruppen-Admin</option>
</select>
<button
type="button"
onClick={() => handleAddMemberToTeam(team.id)}
disabled={!memberSelectByTeam[team.id]}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 disabled:opacity-40"
>
<UserPlus className="w-3.5 h-3.5" />
<span>Hinzufügen</span>
</button>
</div>
</div> </div>
); );
}) })
+7
View File
@@ -664,6 +664,13 @@ export const api = {
}); });
}, },
async createTeam(name: string, description = ''): Promise<Team> {
return request<Team>('/api/v1/teams', {
method: 'POST',
body: JSON.stringify({ name, description }),
});
},
async deleteTeam(teamId: string): Promise<void> { async deleteTeam(teamId: string): Promise<void> {
await request(`/api/v1/teams/${teamId}`, { method: 'DELETE' }); await request(`/api/v1/teams/${teamId}`, { method: 'DELETE' });
}, },
+40
View File
@@ -144,6 +144,7 @@ func (s *Server) Router() http.Handler {
r.Get("/api/v1/organizations/{id}/teams", s.handleGetOrgTeams) r.Get("/api/v1/organizations/{id}/teams", s.handleGetOrgTeams)
r.Post("/api/v1/organizations/{id}/teams", s.handleCreateOrgTeam) r.Post("/api/v1/organizations/{id}/teams", s.handleCreateOrgTeam)
r.Get("/api/v1/teams", s.handleGetUserTeams) r.Get("/api/v1/teams", s.handleGetUserTeams)
r.Post("/api/v1/teams", s.handleCreateTeamDirect)
r.Delete("/api/v1/teams/{id}", s.handleDeleteTeam) r.Delete("/api/v1/teams/{id}", s.handleDeleteTeam)
r.Post("/api/v1/teams/{id}/members", s.handleAddTeamMember) r.Post("/api/v1/teams/{id}/members", s.handleAddTeamMember)
r.Delete("/api/v1/teams/{id}/members/{userId}", s.handleRemoveTeamMember) 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}) 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) { func (s *Server) handleGetUserTeams(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context()) 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) teams, err := s.store.GetTeamsForUser(userID)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, err.Error()) writeError(w, http.StatusInternalServerError, err.Error())
+24
View File
@@ -2752,6 +2752,30 @@ func (s *Store) DeleteTeam(teamID string) error {
return err 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) { func (s *Store) GetTeamsForOrg(orgID string) ([]domain.Team, error) {
rows, err := s.db.Query( rows, err := s.db.Query(
`SELECT id, org_id, name, description, created_at FROM teams WHERE org_id = ? ORDER BY created_at ASC`, `SELECT id, org_id, name, description, created_at FROM teams WHERE org_id = ? ORDER BY created_at ASC`,