feat: overhaul groups management, remove global team_admin and isolate group administration
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
This commit is contained in:
1 parent
60c302e23e
commit
e37302f73c
11 files changed
+936
-388
No files matched your search
@@ -10,16 +10,13 @@ import {
|
||||
Copy,
|
||||
Check,
|
||||
ArrowLeft,
|
||||
Share2,
|
||||
Send,
|
||||
AlertCircle,
|
||||
Building2,
|
||||
UserPlus,
|
||||
Share2,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Organization,
|
||||
SystemSettings,
|
||||
Team,
|
||||
User,
|
||||
UserInvite,
|
||||
UserRole,
|
||||
@@ -31,7 +28,7 @@ interface AdminSettingsViewProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
type AdminSection = 'users' | 'teams' | 'server' | 'smtp';
|
||||
type AdminSection = 'users' | 'server' | 'smtp';
|
||||
|
||||
export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
currentUser,
|
||||
@@ -50,19 +47,11 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
const [inviteEmail, setInviteEmail] = useState('');
|
||||
const [inviteRole, setInviteRole] = useState<UserRole>('member');
|
||||
const [inviteTeamId, setInviteTeamId] = useState('');
|
||||
const [copiedInviteId, setCopiedInviteId] = useState<string | null>(null);
|
||||
|
||||
const [resetPwdUserId, setResetPwdUserId] = useState<string | null>(null);
|
||||
const [resetPwdVal, setResetPwdVal] = useState('');
|
||||
|
||||
// --- Teams & Groups State ---
|
||||
const [allTeams, setAllTeams] = useState<Team[]>([]);
|
||||
const [newTeamName, setNewTeamName] = useState('');
|
||||
const [newTeamDesc, setNewTeamDesc] = useState('');
|
||||
const [memberSelectByTeam, setMemberSelectByTeam] = useState<Record<string, string>>({});
|
||||
const [memberRoleByTeam, setMemberRoleByTeam] = useState<Record<string, string>>({});
|
||||
|
||||
// --- Server & SMTP Settings State ---
|
||||
const [settings, setSettings] = useState<SystemSettings>({
|
||||
sharingMode: 'teams_only',
|
||||
@@ -83,14 +72,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
const [uList, invList, teamList] = await Promise.all([
|
||||
const [uList, invList] = await Promise.all([
|
||||
api.adminGetUsers(),
|
||||
api.adminGetInvites(),
|
||||
api.getUserTeams(),
|
||||
]);
|
||||
setUsers(uList);
|
||||
setInvites(invList);
|
||||
setAllTeams(teamList);
|
||||
|
||||
if (isServerAdmin) {
|
||||
const sys = await api.adminGetSettings();
|
||||
@@ -149,7 +136,6 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
const inv = await api.adminCreateInvite({
|
||||
email: inviteEmail.trim(),
|
||||
role: inviteRole,
|
||||
teamId: inviteTeamId,
|
||||
});
|
||||
setInvites((prev) => [inv, ...prev]);
|
||||
setInviteEmail('');
|
||||
@@ -231,64 +217,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HANDLERS: TEAMS & GRUPPEN
|
||||
// ============================================================================
|
||||
const handleCreateTeam = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newTeamName.trim()) return;
|
||||
setStatusMsg(null);
|
||||
try {
|
||||
const created = await api.createTeam(
|
||||
newTeamName.trim(),
|
||||
newTeamDesc.trim()
|
||||
);
|
||||
setNewTeamName('');
|
||||
setNewTeamDesc('');
|
||||
await loadAll();
|
||||
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(`Gruppe / Team "${team.name}" wirklich löschen?`)) return;
|
||||
setStatusMsg(null);
|
||||
try {
|
||||
await api.deleteTeam(team.id);
|
||||
await loadAll();
|
||||
setStatusMsg({ type: 'ok', text: `Gruppe / Team "${team.name}" gelöscht.` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'err', text: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddMemberToTeam = async (teamId: string) => {
|
||||
const selectedUserId = memberSelectByTeam[teamId];
|
||||
if (!selectedUserId) return;
|
||||
const role = memberRoleByTeam[teamId] || 'member';
|
||||
setStatusMsg(null);
|
||||
try {
|
||||
await api.addTeamMember(teamId, { userId: selectedUserId, role });
|
||||
setMemberSelectByTeam((prev) => ({ ...prev, [teamId]: '' }));
|
||||
await loadAll();
|
||||
setStatusMsg({ type: 'ok', text: 'Mitglied zur Gruppe hinzugefügt.' });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'err', text: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveMemberFromTeam = async (teamId: string, userId: string) => {
|
||||
setStatusMsg(null);
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HANDLERS: SERVER & SMTP SETTINGS
|
||||
@@ -327,9 +256,6 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// All teams for invite dropdown
|
||||
const flattenedTeams: Team[] = allTeams;
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
{/* Top Page Header */}
|
||||
@@ -346,17 +272,15 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<div>
|
||||
<h1 className="text-base font-bold text-main tracking-tight flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-accent" />
|
||||
<span>Administration & Team-Steuerung</span>
|
||||
<span>Server-Administration</span>
|
||||
</h1>
|
||||
<p className="text-xs text-muted">
|
||||
Benutzerverwaltung, Einladungscodes, Organisationen, Teams, Isolationsmodus & Mail-Server
|
||||
Benutzerverwaltung, Einladungscodes, Isolationsmodus & Mail-Server
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="badge-accent">
|
||||
{isServerAdmin ? 'Server-Administrator' : 'Team-Administrator'}
|
||||
</span>
|
||||
<span className="badge-accent">Server-Administrator</span>
|
||||
</div>
|
||||
|
||||
{statusMsg && (
|
||||
@@ -401,21 +325,6 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
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 === 'teams'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-canvas/50'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-semibold">Teams & Gruppen</p>
|
||||
<p className="text-[10px] text-faint">Familie, Arbeit, Freunde zum Teilen</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isServerAdmin && (
|
||||
<>
|
||||
<button
|
||||
@@ -520,8 +429,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
|
||||
>
|
||||
<option value="member">Mitglied (Standard)</option>
|
||||
<option value="team_admin">Team-Admin (Darf einladen)</option>
|
||||
{isServerAdmin && <option value="admin">Server-Admin (Vollzugriff)</option>}
|
||||
<option value="admin">Server-Admin (Vollzugriff)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -546,7 +454,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleCreateInvite} className="space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="min-w-0">
|
||||
<label className="block text-muted mb-1">
|
||||
Gebundene E-Mail (optional)
|
||||
@@ -561,33 +469,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<label className="block text-muted mb-1">Zugewiesene Rolle</label>
|
||||
<label className="block text-muted mb-1">Zugewiesene Server-Rolle</label>
|
||||
<select
|
||||
value={inviteRole}
|
||||
onChange={(e) => setInviteRole(e.target.value as UserRole)}
|
||||
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
|
||||
>
|
||||
<option value="member">Mitglied</option>
|
||||
<option value="team_admin">Team-Admin</option>
|
||||
{isServerAdmin && <option value="admin">Server-Admin</option>}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<label className="block text-muted mb-1">
|
||||
Automatisch Team zuweisen (optional)
|
||||
</label>
|
||||
<select
|
||||
value={inviteTeamId}
|
||||
onChange={(e) => setInviteTeamId(e.target.value)}
|
||||
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
|
||||
>
|
||||
<option value="">Kein direktes Team</option>
|
||||
{flattenedTeams.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
</option>
|
||||
))}
|
||||
<option value="admin">Server-Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -690,8 +579,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
title="Benutzerrolle ändern"
|
||||
>
|
||||
<option value="member">Mitglied</option>
|
||||
<option value="team_admin">Team-Admin</option>
|
||||
{isServerAdmin && <option value="admin">Server-Admin</option>}
|
||||
<option value="admin">Server-Admin</option>
|
||||
</select>
|
||||
|
||||
{isServerAdmin && (
|
||||
@@ -758,203 +646,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* SECTION 2: TEAMS & GRUPPEN */}
|
||||
{/* ================================================================= */}
|
||||
{section === 'teams' && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-main">Teams & Gruppen verwalten</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
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 Team/Group Card */}
|
||||
<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">
|
||||
<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="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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Teams & Groups List */}
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
|
||||
{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>
|
||||
) : (
|
||||
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-4 bg-canvas border border-border rounded-card space-y-4"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
onClick={() => handleDeleteTeam(team)}
|
||||
className="btn-icon btn-icon-danger"
|
||||
title="Gruppe / Team löschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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 in dieser Gruppe.
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{members.map((m) => (
|
||||
<div
|
||||
key={m.userId}
|
||||
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 === 'team_admin' ? 'Gruppen-Admin' : 'Mitglied'})
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleRemoveMemberFromTeam(team.id, m.userId)}
|
||||
className="text-faint hover:text-status-err font-bold ml-0.5"
|
||||
title="Aus Gruppe entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* SECTION 3: SERVER & SHARING-ISOLATION (SERVER-ADMIN ONLY) */}
|
||||
|
||||
Reference in new issue
Block a user