// fp-users.jsx — User management panel (admin only) // Exports: UsersPanel function UsersPanel({ onBack }) { const { profile } = useAuth(); const [users, setUsers] = useState([]); const [brands, setBrands] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(null); // Invite modal const [showInvite, setShowInvite] = useState(false); const [invEmail, setInvEmail] = useState(''); const [invName, setInvName] = useState(''); const [invRole, setInvRole] = useState('editor'); const [invBrand, setInvBrand] = useState(''); const [inviting, setInviting] = useState(false); const [invMsg, setInvMsg] = useState(''); useEffect(() => { Promise.all([ sbClient.from('profiles').select('*, brand_access(brand_id, role, brands(name))').order('created_at'), sbClient.from('brands').select('id, name').order('name'), ]).then(([{ data: u }, { data: b }]) => { setUsers(u || []); setBrands(b || []); setLoading(false); }); }, []); const updateRole = async (userId, role) => { setSaving(userId + '_role'); await sbClient.from('profiles').update({ role }).eq('id', userId); setUsers(us => us.map(u => u.id === userId ? { ...u, role } : u)); setSaving(null); }; const assignBrand = async (userId, brandId) => { if (!brandId) return; const { data: existing } = await sbClient.from('brand_access').select('id').eq('user_id', userId).eq('brand_id', brandId).single(); if (existing) { alert('Este usuario ya tiene acceso a esa marca.'); return; } const u = users.find(x => x.id === userId); const accessRole = u?.role === 'client' ? 'client' : 'editor'; setSaving(userId + '_brand'); const { data } = await sbClient.from('brand_access').insert({ user_id: userId, brand_id: brandId, role: accessRole }).select('brand_id, role, brands(name)').single(); if (data) setUsers(us => us.map(u => u.id === userId ? { ...u, brand_access: [...(u.brand_access||[]), data] } : u)); setSaving(null); }; const removeBrandAccess = async (userId, brandId) => { await sbClient.from('brand_access').delete().eq('user_id', userId).eq('brand_id', brandId); setUsers(us => us.map(u => u.id === userId ? { ...u, brand_access: u.brand_access.filter(a => a.brand_id !== brandId) } : u)); }; const deleteUser = async (userId) => { if (!confirm('¿Eliminar este usuario? (no se puede deshacer)')) return; await sbClient.from('profiles').delete().eq('id', userId); setUsers(us => us.filter(u => u.id !== userId)); }; // Invite: insert into profiles with a temp flag; user must sign up with same email const sendInvite = async e => { e.preventDefault(); setInviting(true); setInvMsg(''); // We use Supabase Auth admin invite if available, otherwise show instructions const { error } = await sbClient.auth.signInWithOtp({ email: invEmail, options: { shouldCreateUser: true, data: { name: invName, role: invRole } } }); if (error) { setInvMsg('Error: ' + error.message); } else { setInvMsg(`✓ Magic link enviado a ${invEmail}. Cuando inicie sesión, asígnale una marca.`); } setInviting(false); }; const ROLE_COLORS = { admin: '#7B5EA7', editor: '#C96A42', client: '#2E7D55' }; const ROLE_LABELS = { admin: 'Admin', editor: 'Editor', client: 'Cliente' }; return (
{/* Topbar */}
Feed Planner

Usuarios

{users.length} usuarios registrados

{loading ? (
) : (
{/* Header */}
Usuario
Rol
Marcas asignadas
{users.map(u => (
{/* User info */}
{u.name || '—'}
{u.email}
{u.id === profile?.id && }
{/* Role selector */}
{/* Brand access */}
{(u.brand_access || []).map(a => ( {a.brands?.name || a.brand_id} ))} {u.role !== 'admin' && ( )} {u.role === 'admin' && Acceso total}
{/* Delete */}
{u.id !== profile?.id && ( )}
))}
)}
{/* Invite modal */} {showInvite && (
{ setShowInvite(false); setInvMsg(''); }}>
e.stopPropagation()}>

Invitar usuario

Se enviará un magic link al email ingresado.

{invMsg && (
{invMsg}
)}
{[['Email','email','email','tu@cliente.com'],['Nombre','text','invName','Nombre del usuario']].map(([label, type, id, ph]) => (
id==='email'?setInvEmail(e.target.value):setInvName(e.target.value)} required={id==='email'} style={usStyles.input}/>
))}
)}
); } const usStyles = { page: { minHeight:'100vh', background:'var(--cream)', display:'flex', flexDirection:'column' }, topbar: { background:'#fff', borderBottom:'1px solid var(--border)', padding:'14px 32px', display:'flex', alignItems:'center', justifyContent:'space-between' }, logo: { fontFamily:"'Playfair Display',serif", fontSize:20, fontWeight:600, display:'flex', alignItems:'center', gap:8 }, dot: { width:8, height:8, borderRadius:'50%', background:'var(--accent)', display:'inline-block' }, backBtn: { background:'none', border:'none', cursor:'pointer', color:'var(--ink-soft)', display:'flex', alignItems:'center', gap:6, fontSize:13, padding:0 }, content: { maxWidth:960, margin:'0 auto', width:'100%', padding:'36px 24px' }, pageHeader: { marginBottom:24 }, title: { fontFamily:"'Playfair Display',serif", fontSize:28, fontWeight:600, letterSpacing:'-0.5px' }, sub: { fontSize:13, color:'var(--ink-muted)', marginTop:4 }, table: { background:'#fff', borderRadius:16, overflow:'hidden', boxShadow:'0 1px 4px rgba(28,25,23,0.07)' }, tableHead: { display:'flex', alignItems:'center', gap:16, padding:'12px 20px', background:'var(--cream)', fontSize:11, fontWeight:700, color:'var(--ink-muted)', letterSpacing:'0.06em', textTransform:'uppercase', borderBottom:'1px solid var(--border)' }, row: { display:'flex', alignItems:'center', gap:16, padding:'14px 20px', borderBottom:'1px solid var(--border)' }, select: { border:'1px solid', borderRadius:8, padding:'5px 10px', fontFamily:"'DM Sans',sans-serif", fontSize:12.5, outline:'none', cursor:'pointer', appearance:'none' }, assignSelect: { border:'1px dashed var(--cream-dark)', borderRadius:8, padding:'4px 10px', fontFamily:"'DM Sans',sans-serif", fontSize:12, color:'var(--ink-soft)', background:'var(--cream)', outline:'none', cursor:'pointer' }, brandTag: { display:'inline-flex', alignItems:'center', gap:5, background:'var(--accent-light)', color:'var(--accent)', border:'1px solid var(--accent-mid)', borderRadius:20, padding:'3px 8px 3px 10px', fontSize:12, fontWeight:500 }, tagDel: { background:'none', border:'none', cursor:'pointer', color:'var(--accent)', fontSize:11, padding:0, lineHeight:1, opacity:0.7, display:'flex', alignItems:'center' }, delBtn: { background:'none', border:'none', cursor:'pointer', color:'var(--ink-muted)', padding:6, borderRadius:8, display:'flex', alignItems:'center', transition:'color 0.15s' }, btnPrimary: { background:'var(--accent)', color:'#fff', border:'none', borderRadius:9, padding:'9px 16px', fontFamily:"'DM Sans',sans-serif", fontSize:13, fontWeight:600, cursor:'pointer', display:'flex', alignItems:'center', gap:6 }, btnOutline: { background:'#fff', color:'var(--ink-soft)', border:'1px solid var(--border)', borderRadius:9, padding:'9px 14px', fontFamily:"'DM Sans',sans-serif", fontSize:13, fontWeight:500, cursor:'pointer', display:'flex', alignItems:'center', gap:6 }, modalBackdrop: { position:'fixed', inset:0, background:'rgba(28,25,23,0.45)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:100, backdropFilter:'blur(4px)' }, modalCard: { background:'#fff', borderRadius:18, padding:'28px', width:400, boxShadow:'0 12px 40px rgba(28,25,23,0.16)' }, fieldLabel: { fontSize:11, fontWeight:600, color:'var(--ink-soft)', letterSpacing:'0.06em', textTransform:'uppercase' }, input: { background:'var(--cream)', border:'1px solid var(--cream-dark)', borderRadius:8, padding:'9px 12px', fontFamily:"'DM Sans',sans-serif", fontSize:13.5, color:'var(--ink)', outline:'none', width:'100%' }, }; Object.assign(window, { UsersPanel });