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>
106 lines
3 KiB
TypeScript
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>
|
|
);
|
|
}
|