ppt-tool/frontend/app/admin/components/TeamMemberDialog.tsx
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
Phase 1 (Foundation):
- Project restructure (presenton-main → backend/ + frontend/)
- Database schema (8 new models, Alembic config, seed script)
- Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware)
- RBAC (access_service, rbac_middleware, admin routers)
- Audit logging (fire-and-forget, AuditMiddleware, admin router)
- i18n (react-i18next with 5 namespace files)

Phase 2 (Admin Panel & Client Management):
- Admin panel shell (sidebar layout, role guard, 12 pages)
- Redux admin slice with 18 async thunks
- User management (role changes, deactivation)
- Client management (CRUD, brand config, team management)
- Brand config editor (colors, fonts, logos, voice rules)
- Master deck upload & parser (PPTX → HTML → React pipeline)
- Audit log viewer with filters and CSV/JSON export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:37:17 +00:00

106 lines
3 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from '@/store/store';
import { addTeamMember, fetchUsers } from '@/store/slices/adminSlice';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
interface TeamMemberDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
teamId: string;
existingMemberIds: string[];
}
export default function TeamMemberDialog({
open,
onOpenChange,
teamId,
existingMemberIds,
}: TeamMemberDialogProps) {
const dispatch = useDispatch<AppDispatch>();
const users = useSelector((state: RootState) => state.admin.users);
const [selectedUserId, setSelectedUserId] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
if (open && users.length === 0) {
dispatch(fetchUsers());
}
}, [open, users.length, dispatch]);
const availableUsers = users.filter(
(u) => u.is_active && !existingMemberIds.includes(u.id)
);
const handleSubmit = async () => {
if (!selectedUserId) return;
setLoading(true);
try {
await dispatch(addTeamMember({ teamId, userId: selectedUserId })).unwrap();
toast.success('Member added');
setSelectedUserId('');
onOpenChange(false);
} catch (err) {
toast.error(typeof err === 'string' ? err : 'Failed to add member');
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Team Member</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Select User</Label>
<Select value={selectedUserId} onValueChange={setSelectedUserId}>
<SelectTrigger>
<SelectValue placeholder="Choose a user..." />
</SelectTrigger>
<SelectContent>
{availableUsers.map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.display_name} ({u.email})
</SelectItem>
))}
</SelectContent>
</Select>
{availableUsers.length === 0 && (
<p className="text-sm text-gray-500">No available users to add.</p>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={loading || !selectedUserId}>
{loading ? 'Adding...' : 'Add Member'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}