// Workspace.jsx — Workspaces, Sub-Teams, Roles, Invites, Guest Access
(() => {
const { Card, Badge, Avatar, Switch } = window.TechyFuelOSDesignSystem_be0222;
// Roles MUST match the backend validation (owner/admin/manager/member/guest).
// A value the API rejects makes the PATCH 422 and the dropdown snap back to
// its old value — which is exactly what "manager" did before it was added.
const ROLES = [
{ value: 'owner', label: 'Owner', desc: 'Full control — billing, delete workspace, all settings', color: '#7c3aed', icon: 'crown' },
{ value: 'admin', label: 'Admin', desc: 'Manage members, teams, projects. No billing access', color: '#2563eb', icon: 'shield' },
{ value: 'manager', label: 'Project Manager', desc: 'Runs projects, milestones and tasks. No payroll or invoices', color: '#0891b2', icon: 'briefcase' },
{ value: 'team_lead', label: 'Team Lead', desc: 'Creates and assigns tasks for their team. No financials', color: '#d97706', icon: 'users' },
{ value: 'member', label: 'Team Member', desc: 'Works on assigned projects and tasks only', color: '#0d9488', icon: 'user' },
{ value: 'finance', label: 'Finance', desc: 'Invoices, costs and revenue. Limited project editing', color: '#be123c', icon: 'dollar-sign' },
{ value: 'client', label: 'Client', desc: 'Sees only their own projects, via the client portal', color: '#65a30d', icon: 'eye' },
{ value: 'guest', label: 'Guest', desc: 'Client-facing view only — limited read access', color: '#65a30d', icon: 'eye' },
];
const TEAM_COLORS = ['#3b82f6','#8b5cf6','#f59e0b','#10b981','#ef4444','#f97316','#06b6d4','#ec4899'];
const TEAM_ICONS = [
'users','code','pen-tool','megaphone','crown','briefcase','bar-chart','globe',
// Video editing / production
'video','film','clapperboard','scissors','camera','mic',
// Sales / growth
'trending-up','dollar-sign','handshake','target','shopping-cart','badge-percent',
// General
'palette','rocket','shield','wrench','headphones','phone','mail','heart','star','zap','package','pen-line',
];
const PERM_MATRIX = [
{ feature: 'View projects & tasks', owner: true, admin: true, manager: true, member: true, guest: true },
{ feature: 'Create & edit tasks', owner: true, admin: true, manager: true, member: true, guest: false },
{ feature: 'Create projects', owner: true, admin: true, manager: true, member: true, guest: false },
{ feature: 'Access Team Chat', owner: true, admin: true, manager: true, member: true, guest: false },
{ feature: 'View Finance & Invoices', owner: true, admin: true, manager: true, member: false, guest: false },
{ feature: 'Manage team members', owner: true, admin: true, manager: false, member: false, guest: false },
{ feature: 'Manage integrations', owner: true, admin: true, manager: false, member: false, guest: false },
{ feature: 'View Meta Ads & Reports', owner: true, admin: true, manager: true, member: true, guest: false },
{ feature: 'Approve / reject deliverables',owner: true, admin: true, manager: true, member: false, guest: true },
{ feature: 'Upload feedback files', owner: true, admin: true, manager: true, member: false, guest: true },
{ feature: 'Billing & workspace settings', owner: true, admin: false, manager: false, member: false, guest: false },
{ feature: 'Delete workspace', owner: true, admin: false, manager: false, member: false, guest: false },
];
function uid() { return Math.random().toString(36).slice(2); }
// ── Role Badge ─────────────────────────────────────────────────────────────────
function RoleBadge({ role }) {
const r = ROLES.find(x => x.value === role) || ROLES.find(x => x.value === 'member') || ROLES[0];
return (
{r.label}
);
}
// ── Invite Modal ───────────────────────────────────────────────────────────────
function InviteModal({ workspaceId, onClose, onInvited }) {
const [tab, setTab] = React.useState('email'); // 'email' | 'link'
const [email, setEmail] = React.useState('');
const [inviteName, setInviteName] = React.useState('');
const [role, setRole] = React.useState('member');
const [link, setLink] = React.useState('');
const [copied, setCopied] = React.useState(false);
const [sending, setSending] = React.useState(false);
const [sent, setSent] = React.useState(false);
const [emailSent, setEmailSent] = React.useState(false);
const [notice, setNotice] = React.useState('');
async function generateLink() {
// The backend now returns the real join link (and sends the email if a
// mailbox is connected). Use it instead of a made-up /join?token= URL.
if (window.API?.createWorkspaceInvite) {
try {
const res = await window.API.createWorkspaceInvite({ workspace_id: workspaceId, role });
if (res?.invite_link) { setLink(res.invite_link); return; }
if (res?.data?.token) { setLink(`${window.location.origin}/?invite=${res.data.token}`); return; }
} catch {}
}
setLink(`${window.location.origin}/?invite=${uid() + uid()}`);
}
async function sendInvite() {
if (!email.trim()) return;
setSending(true);
setNotice('');
try {
// Create the team_members row with the chosen role *before* they sign
// up, so the auth trigger links their account to this row (and this
// role) instead of defaulting a "no invite found" signup to owner.
if (window.API?.addTeamMember) {
try {
await window.API.addTeamMember({ name: (inviteName.trim() || email.trim().split('@')[0]), email: email.trim(), role, status: 'active' });
} catch (e) {
// Already invited/exists with this email — fine, they're already set up.
}
}
// Single call: the backend creates the invite, emails it if a mailbox is
// connected, and always returns the join link so we can show it here.
let res = null;
if (window.API?.createWorkspaceInvite) {
res = await window.API.createWorkspaceInvite({ workspace_id: workspaceId, email: email.trim(), role });
}
if (res?.invite_link) setLink(res.invite_link);
setEmailSent(!!res?.email_sent);
if (!res?.email_sent) {
// No mailbox connected (or send failed): fall back to the copyable link.
setNotice(res?.email_error || 'Could not send the email — copy the link below and share it.');
}
setSent(true);
onInvited?.();
} catch (e) {
setNotice('Something went wrong creating the invite. Please try again.');
}
setSending(false);
}
function copyLink() {
navigator.clipboard.writeText(link).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
e.stopPropagation()}>
Invite to workspace
Members get access based on their role
{/* Tabs */}
{[['email','mail','Email invite'],['link','link','Invite link']].map(([t, icon, label]) => (
setTab(t)} style={{ padding: '12px 16px', border: 'none', background: 'none', cursor: 'pointer', fontSize: 14, fontWeight: 600, fontFamily: 'var(--font-sans)', color: tab === t ? 'var(--blue-600)' : 'var(--text-muted)', borderBottom: tab === t ? '2px solid var(--blue-600)' : '2px solid transparent', display: 'flex', alignItems: 'center', gap: 6 }}>
{label}
))}
{/* Role selector */}
Role
{ROLES.map(r => (
setRole(r.value)}
style={{ padding: '10px 12px', border: `2px solid ${role === r.value ? r.color : 'var(--slate-200)'}`, borderRadius: 10, background: role === r.value ? r.color + '10' : 'white', cursor: 'pointer', textAlign: 'left' }}>
{r.label}
{r.desc}
))}
{tab === 'email' ? (
sent ? (
{emailSent ? '✅' : '🔗'}
{emailSent ? 'Invite sent!' : 'Invite created'}
{emailSent
?
{email} will receive an email invite
:
{notice || 'Share this link with them to join:'}
}
{!emailSent && link && (
{copied ? 'Copied!' : 'Copy'}
)}
{ setSent(false); setEmail(''); setInviteName(''); setLink(''); setNotice(''); }} style={{ marginTop: 12, padding: '8px 20px', background: 'var(--blue-600)', color: 'white', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 600, fontFamily: 'var(--font-sans)' }}>Invite another
) : (
Full name
setInviteName(e.target.value)} placeholder="e.g. Ahsan Ather"
style={{ width: '100%', height: 40, padding: '0 14px', border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none', boxSizing: 'border-box', marginBottom: 14 }} />
Email address
setEmail(e.target.value)} placeholder="colleague@company.com" type="email"
onKeyDown={e => e.key === 'Enter' && sendInvite()}
style={{ width: '100%', height: 40, padding: '0 14px', border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none', boxSizing: 'border-box', marginBottom: 14 }} />
{sending ? 'Sending...' : 'Send invite'}
)
) : (
{!link ? (
Generate invite link
) : (
)}
)}
);
}
// ── Team Card ──────────────────────────────────────────────────────────────────
function TeamCard({ team, allMembers, onAddMember, onRemoveMember, onDelete }) {
const members = (team.team_memberships || []).map(m => m.team_members).filter(Boolean);
const [hover, setHover] = React.useState(false);
const [showAdd, setShowAdd] = React.useState(false);
const [addId, setAddId] = React.useState('');
const available = allMembers.filter(m => !members.some(tm => tm?.id === m.id));
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{ border: '1px solid var(--slate-200)', borderRadius: 14, overflow: 'hidden', background: 'white', transition: 'box-shadow 0.15s', boxShadow: hover ? 'var(--shadow-md)' : 'none' }}>
{team.name}
{members.length} member{members.length !== 1 ? 's' : ''}
onDelete(team.id)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: 4, opacity: hover ? 1 : 0, transition: 'opacity 0.15s' }}>
{members.map(m => m && (
{m.name?.split(' ')[0]}
onRemoveMember(team.id, m.id)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: 0, lineHeight: 1, fontSize: 14, display: 'flex', alignItems: 'center' }}>×
))}
{members.length === 0 &&
No members yet
}
{showAdd ? (
setAddId(e.target.value)} style={{ flex: 1, height: 32, padding: '0 8px', border: '1px solid var(--slate-200)', borderRadius: 7, fontSize: 13, fontFamily: 'var(--font-sans)', outline: 'none' }}>
Pick member...
{available.map(m => {m.name} )}
{ if (addId) { onAddMember(team.id, addId); setAddId(''); setShowAdd(false); } }} style={{ height: 32, padding: '0 12px', background: 'var(--blue-600)', color: 'white', border: 'none', borderRadius: 7, cursor: 'pointer', fontSize: 13, fontWeight: 600 }}>Add
setShowAdd(false)} style={{ height: 32, padding: '0 10px', background: 'none', border: '1px solid var(--slate-200)', borderRadius: 7, cursor: 'pointer', fontSize: 13, color: 'var(--text-muted)' }}>✕
) : (
setShowAdd(true)} style={{ marginTop: 10, fontSize: 12, color: 'var(--blue-600)', background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', gap: 4, fontWeight: 600 }}>
Add member
)}
);
}
// ── Permission Matrix ──────────────────────────────────────────────────────────
function PermissionMatrix() {
return (
Feature
{ROLES.map(r => (
{r.label}
))}
{PERM_MATRIX.map((row, i) => (
{row.feature}
{ROLES.map(r => (
{row[r.value]
?
:
}
))}
))}
);
}
// ── Main Workspace Screen ──────────────────────────────────────────────────────
function Workspace() {
useLucide();
const [tab, setTab] = React.useState('members'); // 'members' | 'teams' | 'invites' | 'permissions'
const [workspaces, setWorkspaces] = React.useState([]);
const [activeWs, setActiveWs] = React.useState(null);
const [members, setMembers] = React.useState([]);
const [teams, setTeams] = React.useState([]);
const [invites, setInvites] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [showInviteModal, setShowInviteModal] = React.useState(false);
const [showNewTeam, setShowNewTeam] = React.useState(false);
const [showNewWs, setShowNewWs] = React.useState(false);
const [newTeam, setNewTeam] = React.useState({ name: '', color: TEAM_COLORS[0], icon: 'users' });
const [newWs, setNewWs] = React.useState({ name: '', description: '' });
const [search, setSearch] = React.useState('');
const [roleFilter, setRoleFilter] = React.useState('all');
const [saving, setSaving] = React.useState(false);
React.useEffect(() => {
if (!window.API) { setLoading(false); return; }
(async () => {
try {
const { data: ws } = await window.API.getWorkspaces();
const wsArr = ws || [];
setWorkspaces(wsArr);
const activeId = await window.API.getActiveWorkspaceId().catch(() => null);
setActiveWs(wsArr.find(w => w.id === activeId) || wsArr[0] || null);
} catch {}
try { const { data } = await window.API.getTeam(); setMembers(data || []); } catch {}
setLoading(false);
})();
}, []);
React.useEffect(() => {
if (!activeWs || !window.API) return;
(async () => {
try { const { data } = await window.API.getTeams(activeWs.id); setTeams(data || []); } catch {}
try { const { data } = await window.API.getWorkspaceInvites(activeWs.id); setInvites(data || []); } catch {}
})();
}, [activeWs?.id]);
async function createWorkspace() {
if (!newWs.name.trim() || !window.API) return;
setSaving(true);
try {
const { data, error } = await window.API.createWorkspace({ name: newWs.name.trim(), description: newWs.description });
if (error) { alert('Could not create workspace:\n\n' + (error.message || JSON.stringify(error))); return; }
// Creating a workspace makes it the new active one server-side —
// reload so every screen refetches data scoped to it.
window.location.reload();
} catch (err) {
alert('Error: ' + (err.message || JSON.stringify(err)));
} finally { setSaving(false); }
}
async function switchWorkspace(workspaceId) {
if (!workspaceId || workspaceId === activeWs?.id || !window.API) return;
try {
const { error } = await window.API.switchWorkspace(workspaceId);
if (error) { alert('Could not switch workspace:\n\n' + (error.message || JSON.stringify(error))); return; }
window.location.reload();
} catch (err) {
alert('Error: ' + (err.message || JSON.stringify(err)));
}
}
// Delete workspace — owner or super-admin only, with type-to-confirm.
const authUser = (() => { try { return JSON.parse(localStorage.getItem('tf_auth_user') || '{}'); } catch { return {}; } })();
const isSuperAdmin = !!authUser.is_super_admin;
const canDeleteWs = isSuperAdmin || window.TFMyRole === 'owner';
const [showDeleteWs, setShowDeleteWs] = React.useState(false);
const [deleteConfirm, setDeleteConfirm] = React.useState('');
const [deleting, setDeleting] = React.useState(false);
async function deleteWorkspace() {
if (!activeWs || !window.API || !window.API.deleteWorkspace) return;
if (deleteConfirm.trim() !== activeWs.name) return;
setDeleting(true);
try {
const { error } = await window.API.deleteWorkspace(activeWs.id);
if (error) { alert('Could not delete workspace:\n\n' + (error.message || JSON.stringify(error))); setDeleting(false); return; }
window.location.reload();
} catch (err) {
alert('Error: ' + (err.message || JSON.stringify(err)));
setDeleting(false);
}
}
async function createTeam() {
if (!newTeam.name.trim() || !activeWs || !window.API) return;
setSaving(true);
try {
const { data } = await window.API.createTeam({ ...newTeam, workspace_id: activeWs.id });
if (data) { setTeams(prev => [...prev, { ...data, team_memberships: [] }]); setNewTeam({ name: '', color: TEAM_COLORS[0], icon: 'users' }); setShowNewTeam(false); }
} catch {}
setSaving(false);
}
async function deleteTeam(id) {
if (!window.API) return;
try { await window.API.deleteTeam(id); setTeams(prev => prev.filter(t => t.id !== id)); } catch {}
}
async function addTeamMember(teamId, memberId) {
if (!window.API) return;
try {
await window.API.addTeamMembership({ team_id: teamId, member_id: memberId });
const member = members.find(m => m.id === memberId);
setTeams(prev => prev.map(t => t.id === teamId ? { ...t, team_memberships: [...(t.team_memberships || []), { member_id: memberId, team_members: member }] } : t));
} catch {}
}
async function removeMember(teamId, memberId) {
if (!window.API) return;
try {
await window.API.removeTeamMembership(teamId, memberId);
setTeams(prev => prev.map(t => t.id === teamId ? { ...t, team_memberships: (t.team_memberships || []).filter(m => m.member_id !== memberId) } : t));
} catch {}
}
async function updateMemberRole(memberId, newRole) {
if (!window.API) return;
try {
await window.API.updateTeamMember(memberId, { role: newRole });
setMembers(prev => prev.map(m => m.id === memberId ? { ...m, role: newRole } : m));
} catch {}
}
// Rename a member — invited-by-email members default to the email prefix as
// their name, which reads badly; let an owner/admin set a proper name.
async function renameMember(m) {
if (!window.API) return;
const next = window.prompt('Edit member name:', m.name || '');
if (next === null) return;
const name = next.trim();
if (!name || name === m.name) return;
try {
await window.API.updateTeamMember(m.id, { name });
setMembers(prev => prev.map(x => x.id === m.id ? { ...x, name } : x));
} catch { alert('Could not update the name. Please try again.'); }
}
async function revokeInvite(id) {
if (!window.API) return;
try { await window.API.revokeInvite(id); setInvites(prev => prev.filter(i => i.id !== id)); } catch {}
}
const filteredMembers = members.filter(m => {
const matchSearch = !search || m.name?.toLowerCase().includes(search.toLowerCase()) || m.email?.toLowerCase().includes(search.toLowerCase());
const matchRole = roleFilter === 'all' || m.role === roleFilter || m.access_level === roleFilter;
return matchSearch && matchRole;
});
const TABS = [
{ id: 'members', label: 'Members', icon: 'users', count: members.length },
{ id: 'teams', label: 'Sub-Teams', icon: 'layout-grid', count: teams.length },
{ id: 'invites', label: 'Invites', icon: 'mail', count: invites.filter(i => !i.accepted_at).length },
{ id: 'permissions', label: 'Permissions', icon: 'shield' },
];
if (loading) return Loading workspace...
;
return (
{/* Header */}
Workspace
Manage members, teams, and access for your workspace
{/* Workspace switcher */}
{activeWs?.name || 'No workspace'}
{activeWs?.plan || 'free'} plan
{workspaces.length > 1 && (
switchWorkspace(e.target.value)}
style={{ height: 38, padding: '0 10px', border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 13, fontFamily: 'var(--font-sans)', background: 'white', cursor: 'pointer', outline: 'none' }}>
{workspaces.map(w => {w.name} )}
)}
setShowNewWs(true)} style={{ height: 38, padding: '0 14px', background: 'var(--slate-50)', border: '1px solid var(--slate-200)', borderRadius: 9, cursor: 'pointer', fontSize: 13, fontFamily: 'var(--font-sans)', color: 'var(--text-body)', display: 'flex', alignItems: 'center', gap: 6 }}>
New workspace
setShowInviteModal(true)} style={{ height: 38, padding: '0 18px', background: 'var(--blue-600)', color: 'white', border: 'none', borderRadius: 9, cursor: 'pointer', fontSize: 14, fontWeight: 700, fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', gap: 6 }}>
Invite member
{canDeleteWs && activeWs && (
{ setDeleteConfirm(''); setShowDeleteWs(true); }} title="Delete this workspace" style={{ height: 38, padding: '0 14px', background: 'white', color: 'var(--red-600)', border: '1px solid var(--red-200)', borderRadius: 9, cursor: 'pointer', fontSize: 13, fontWeight: 600, fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', gap: 6 }}>
Delete
)}
{/* Delete workspace — owner / super-admin only, type-to-confirm */}
{showDeleteWs && (
!deleting && setShowDeleteWs(false)} style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,0.55)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}>
e.stopPropagation()} style={{ background: 'white', borderRadius: 14, padding: 22, width: '100%', maxWidth: 440, boxShadow: 'var(--shadow-xl)' }}>
This permanently deletes {activeWs?.name} and removes all its members. This cannot be undone. Type the workspace name to confirm.
setDeleteConfirm(e.target.value)} placeholder={activeWs?.name}
style={{ width: '100%', height: 40, padding: '0 12px', border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none', boxSizing: 'border-box', marginBottom: 16 }} />
setShowDeleteWs(false)} style={{ height: 38, padding: '0 16px', background: 'var(--slate-50)', border: '1px solid var(--slate-200)', borderRadius: 9, cursor: 'pointer', fontSize: 13, fontWeight: 600, fontFamily: 'var(--font-sans)', color: 'var(--text-body)' }}>Cancel
{deleting ? 'Deleting…' : 'Delete workspace'}
)}
{/* Stats row */}
{[
{ label: 'Total members', value: members.length, icon: 'users', color: 'var(--blue-600)' },
{ label: 'Sub-teams', value: teams.length, icon: 'layout-grid', color: 'var(--purple-600)' },
{ label: 'Pending invites', value: invites.filter(i => !i.accepted_at).length, icon: 'mail', color: 'var(--amber-600)' },
{ label: 'Guest accounts', value: members.filter(m => m.role === 'guest' || m.access_level === 'guest').length, icon: 'eye', color: 'var(--green-600)' },
].map(s => (
))}
{/* Tabs */}
{TABS.map(t => (
setTab(t.id)}
style={{ display: 'flex', flexShrink: 0, whiteSpace: 'nowrap', alignItems: 'center', gap: 6, padding: '7px 16px', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: 14, fontWeight: 600, fontFamily: 'var(--font-sans)', background: tab === t.id ? 'white' : 'transparent', color: tab === t.id ? 'var(--text-body)' : 'var(--text-muted)', boxShadow: tab === t.id ? '0 1px 4px rgba(0,0,0,0.08)' : 'none' }}>
{t.label}
{t.count !== undefined && t.count > 0 && {t.count} }
))}
{/* ── Members tab ── */}
{tab === 'members' && (
setSearch(e.target.value)} placeholder="Search members..."
style={{ width: '100%', paddingLeft: 32, height: 38, border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none', boxSizing: 'border-box' }} />
setRoleFilter(e.target.value)} style={{ height: 38, padding: '0 12px', border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 14, fontFamily: 'var(--font-sans)', background: 'white', cursor: 'pointer', outline: 'none' }}>
All roles
{ROLES.map(r => {r.label} )}
{/* Header */}
Member Department Role Access
{filteredMembers.map((m, i) => (
{m.name}
renameMember(m)} title="Edit name" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: 2, display: 'inline-flex', alignItems: 'center', lineHeight: 1 }}>
{m.email || '—'}
{m.department || '—'}
updateMemberRole(m.id, e.target.value)}
style={{ height: 30, padding: '0 8px', border: '1px solid var(--slate-200)', borderRadius: 7, fontSize: 13, fontFamily: 'var(--font-sans)', background: 'white', cursor: 'pointer', outline: 'none' }}>
{ROLES.map(r => {r.label} )}
))}
{filteredMembers.length === 0 && (
No members found
)}
)}
{/* ── Sub-Teams tab ── */}
{tab === 'teams' && (
setShowNewTeam(t => !t)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 18px', background: 'var(--blue-600)', color: 'white', border: 'none', borderRadius: 9, cursor: 'pointer', fontSize: 14, fontWeight: 700, fontFamily: 'var(--font-sans)' }}>
New team
{/* New team form */}
{showNewTeam && (
Create sub-team
setNewTeam(t => ({ ...t, name: e.target.value }))} placeholder="Team name (e.g. Design, Dev...)"
style={{ flex: 1, minWidth: 200, height: 38, padding: '0 12px', border: '1px solid var(--slate-200)', borderRadius: 8, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none' }} />
{TEAM_COLORS.map(c => (
setNewTeam(t => ({ ...t, color: c }))} style={{ width: 28, height: 28, borderRadius: '50%', background: c, border: newTeam.color === c ? '3px solid var(--slate-800)' : '2px solid transparent', cursor: 'pointer', padding: 0 }} />
))}
setNewTeam(t => ({ ...t, icon: e.target.value }))} style={{ height: 38, padding: '0 10px', border: '1px solid var(--slate-200)', borderRadius: 8, fontSize: 13, fontFamily: 'var(--font-sans)', outline: 'none', cursor: 'pointer' }}>
{TEAM_ICONS.map(ic => {ic} )}
{/* Live preview of the chosen icon in the chosen colour. */}
{saving ? 'Creating...' : 'Create'}
setShowNewTeam(false)} style={{ height: 38, padding: '0 14px', background: 'none', border: '1px solid var(--slate-200)', borderRadius: 8, cursor: 'pointer', fontSize: 14, color: 'var(--text-muted)', fontFamily: 'var(--font-sans)' }}>Cancel
)}
{teams.map(team => (
))}
{teams.length === 0 && (
No sub-teams yet
Create teams like Design, Dev, Marketing to organize your members
)}
)}
{/* ── Invites tab ── */}
{tab === 'invites' && (
setShowInviteModal(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 18px', background: 'var(--blue-600)', color: 'white', border: 'none', borderRadius: 9, cursor: 'pointer', fontSize: 14, fontWeight: 700, fontFamily: 'var(--font-sans)' }}>
Invite member
{invites.length === 0 ? (
No pending invites
Invite your team via email or share an invite link
) : invites.map((inv, i) => (
{inv.email || 'Link invite'}
Invited by {inv.team_members?.name || 'Unknown'} · {new Date(inv.created_at).toLocaleDateString()}
{inv.expires_at && ` · expires ${new Date(inv.expires_at).toLocaleDateString()}`}
{inv.accepted_at ? 'Accepted' : 'Pending'}
{!inv.accepted_at && (
revokeInvite(inv.id)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--red-500)', fontSize: 12, fontWeight: 600, fontFamily: 'var(--font-sans)', padding: '4px 8px' }}>Revoke
)}
))}
)}
{/* ── Permissions tab ── */}
{tab === 'permissions' && (
Role Permissions
What each role can do in this workspace. Guest = client-facing limited access.
Guest (Client) Access
Clients invited as Guest see only the Client Portal — project progress, invoices, and deliverables pending their approval. They cannot access Team Chat, Finance details, or any internal workspace data.
)}
{/* New Workspace Modal */}
{showNewWs && (
setShowNewWs(false)}>
e.stopPropagation()}>
New workspace
Workspace name
setNewWs(w => ({ ...w, name: e.target.value }))} placeholder="e.g. Brand Studio, Client X..."
style={{ width: '100%', height: 40, padding: '0 14px', border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none', boxSizing: 'border-box', marginBottom: 12 }} />
Description (optional)
setNewWs(w => ({ ...w, description: e.target.value }))} placeholder="What this workspace is for..."
style={{ width: '100%', height: 38, padding: '0 14px', border: '1px solid var(--slate-200)', borderRadius: 9, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none', boxSizing: 'border-box', marginBottom: 20 }} />
{saving ? 'Creating...' : 'Create workspace'}
setShowNewWs(false)} style={{ padding: '10px 18px', background: 'none', border: '1px solid var(--slate-200)', borderRadius: 9, cursor: 'pointer', fontSize: 14, color: 'var(--text-muted)', fontFamily: 'var(--font-sans)' }}>Cancel
)}
{/* Invite Modal */}
{showInviteModal && (
setShowInviteModal(false)} onInvited={() => { /* refresh invites */ }} />
)}
);
}
Object.assign(window, { Workspace });
})();