Dev #3
No files matched your search
@@ -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<AdminSettingsViewProps> = ({
|
||||
currentUser,
|
||||
@@ -56,11 +56,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
const [resetPwdUserId, setResetPwdUserId] = useState<string | null>(null);
|
||||
const [resetPwdVal, setResetPwdVal] = useState('');
|
||||
|
||||
// --- Organizations & Teams State ---
|
||||
const [orgs, setOrgs] = useState<Organization[]>([]);
|
||||
// --- Teams & Groups State ---
|
||||
const [allTeams, setAllTeams] = useState<Team[]>([]);
|
||||
const [newOrgName, setNewOrgName] = useState('');
|
||||
const [selectedOrgId, setSelectedOrgId] = useState('');
|
||||
const [newTeamName, setNewTeamName] = useState('');
|
||||
const [newTeamDesc, setNewTeamDesc] = useState('');
|
||||
const [memberSelectByTeam, setMemberSelectByTeam] = useState<Record<string, string>>({});
|
||||
@@ -86,19 +83,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
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<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) => {
|
||||
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<AdminSettingsViewProps> = ({
|
||||
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<AdminSettingsViewProps> = ({
|
||||
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<AdminSettingsViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="w-full space-y-4">
|
||||
@@ -440,17 +402,17 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</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 ${
|
||||
section === 'orgs'
|
||||
section === 'teams'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: '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>
|
||||
<p className="font-semibold">Organisationen & Teams</p>
|
||||
<p className="text-[10px] text-faint">Teams & Mitglieder zuweisen</p>
|
||||
<p className="font-semibold">Teams & Gruppen</p>
|
||||
<p className="text-[10px] text-faint">Familie, Arbeit, Freunde zum Teilen</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -797,173 +759,142 @@ 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="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">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Create Organization & Team */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Create Team/Group Card */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
|
||||
<h3 className="font-bold text-main">1. Neue Organisation gründen</h3>
|
||||
<form onSubmit={handleCreateOrg} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newOrgName}
|
||||
onChange={(e) => setNewOrgName(e.target.value)}
|
||||
placeholder="z.B. Homelab, Familie, Firma…"
|
||||
required
|
||||
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">
|
||||
Anlegen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<h3 className="font-bold text-main flex items-center gap-2">
|
||||
<Plus className="w-4 h-4 text-accent" />
|
||||
<span>Neue Gruppe / Neues Team anlegen</span>
|
||||
</h3>
|
||||
<form onSubmit={handleCreateTeam} className="space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-muted text-[11px] mb-1 font-medium">
|
||||
Name der Gruppe / des Teams *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTeamName}
|
||||
onChange={(e) => setNewTeamName(e.target.value)}
|
||||
placeholder="Team-Name (z.B. Infrastruktur)"
|
||||
placeholder="z. B. Familie, Arbeit, Freunde…"
|
||||
required
|
||||
className="w-full min-w-0 h-8 px-2.5 input-field"
|
||||
className="w-full h-8 px-2.5 input-field text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<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="Kurzbeschreibung (optional)…"
|
||||
className="flex-1 min-w-0 h-8 px-2.5 input-field"
|
||||
placeholder="z. B. Gemeinsame Einkaufslisten & Haushaltsnotizen…"
|
||||
className="w-full h-8 px-2.5 input-field text-xs"
|
||||
/>
|
||||
<button type="submit" className="btn-primary h-8 px-3 text-xs flex-shrink-0">
|
||||
Team erstellen
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Organizations & Nested Teams Display */}
|
||||
{/* Teams & Groups List */}
|
||||
<div className="space-y-4">
|
||||
{orgs.length === 0 ? (
|
||||
<div className="p-6 bg-canvas border border-border rounded-card text-center text-faint">
|
||||
Noch keine Organisationen vorhanden.
|
||||
</div>
|
||||
) : (
|
||||
orgs.map((org) => {
|
||||
const orgTeams = Array.isArray(org.teams) ? org.teams : [];
|
||||
return (
|
||||
<div key={org.id} className="bg-canvas border border-border rounded-card overflow-hidden">
|
||||
<div className="p-3 bg-panel-elevated border-b border-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="w-4 h-4 text-accent" />
|
||||
<span className="font-bold text-sm text-main">{org.name}</span>
|
||||
<span className="text-[11px] text-muted">
|
||||
({orgTeams.length} {orgTeams.length === 1 ? 'Team' : 'Teams'})
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteOrg(org)}
|
||||
className="btn-icon btn-icon-danger"
|
||||
title="Organisation löschen"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-main text-xs uppercase tracking-wider text-muted">
|
||||
Bestehende Gruppen & Teams ({allTeams.length})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{orgTeams.length === 0 ? (
|
||||
<p className="text-faint text-xs">
|
||||
Noch keine Teams in dieser Organisation.
|
||||
{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. "Familie" oder "Arbeit"), um Notizen und Listen gemeinsam zu nutzen.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
orgTeams.map((team) => {
|
||||
allTeams.map((team) => {
|
||||
const members = Array.isArray(team.members) ? team.members : [];
|
||||
const availableUsers = users.filter(
|
||||
(u) => !members.some((m) => m.userId === u.id)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={team.id}
|
||||
className="p-3 bg-panel border border-border rounded-card space-y-3"
|
||||
className="p-4 bg-canvas border border-border rounded-card space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-border pb-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<div className="w-8 h-8 rounded-btn bg-accent/10 text-accent flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Users className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-main flex items-center gap-1.5">
|
||||
<Users className="w-3.5 h-3.5 text-accent" />
|
||||
<span>{team.name}</span>
|
||||
<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-[11px] text-muted mt-0.5">
|
||||
<p className="text-xs text-muted mt-1">
|
||||
{team.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteTeam(team)}
|
||||
className="btn-icon btn-icon-danger"
|
||||
title="Team löschen"
|
||||
title="Gruppe / Team löschen"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Team Members */}
|
||||
<div className="space-y-1.5">
|
||||
{/* Members Chips */}
|
||||
<div className="space-y-2">
|
||||
<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.
|
||||
Noch keine Mitglieder in dieser Gruppe.
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{members.map((m) => (
|
||||
<div
|
||||
key={m.userId}
|
||||
className="px-2 py-1 bg-canvas border border-border rounded-btn flex items-center gap-2"
|
||||
className="px-2.5 py-1 bg-panel border border-border rounded-btn flex items-center gap-2 text-xs"
|
||||
>
|
||||
<span className="text-main font-medium">
|
||||
{m.displayName}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted">
|
||||
({m.role})
|
||||
({m.role === 'team_admin' ? 'Gruppen-Admin' : 'Mitglied'})
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleRemoveMemberFromTeam(team.id, m.userId)
|
||||
}
|
||||
className="text-faint hover:text-status-err"
|
||||
title="Aus Team entfernen"
|
||||
onClick={() => handleRemoveMemberFromTeam(team.id, m.userId)}
|
||||
className="text-faint hover:text-status-err font-bold ml-0.5"
|
||||
title="Aus Gruppe entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -973,8 +904,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add User to Team */}
|
||||
<div className="pt-2 border-t border-border flex flex-wrap items-center gap-2">
|
||||
{/* 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) =>
|
||||
@@ -983,10 +914,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
[team.id]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="flex-1 min-w-[160px] h-7 px-2 input-field text-[11px] bg-canvas"
|
||||
className="flex-1 min-w-[200px] h-8 px-2.5 input-field text-xs bg-panel"
|
||||
>
|
||||
<option value="">Benutzer auswählen…</option>
|
||||
{users.map((u) => (
|
||||
<option value="">Benutzer auswählen zum Hinzufügen…</option>
|
||||
{availableUsers.map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.displayName} ({u.email})
|
||||
</option>
|
||||
@@ -1001,19 +932,20 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
[team.id]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-7 px-2 input-field text-[11px] bg-canvas"
|
||||
className="h-8 px-2.5 input-field text-xs bg-panel"
|
||||
>
|
||||
<option value="member">Mitglied</option>
|
||||
<option value="team_admin">Team-Admin</option>
|
||||
<option value="team_admin">Gruppen-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"
|
||||
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 disabled:opacity-40"
|
||||
>
|
||||
+ Zum Team hinzufügen
|
||||
<UserPlus className="w-3.5 h-3.5" />
|
||||
<span>Hinzufügen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1022,11 +954,6 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
|
||||
@@ -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> {
|
||||
await request(`/api/v1/teams/${teamId}`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in new issue
Block a user