// Workspace page — the org-level landing reached by clicking "CRF2" in the rail. // A banner (the org identity) over a grid of Hub cards. Reuses the New-hub // NHTemplateCard: live hubs render in colour and open the hub; the rest are // "recommended" (shaded, not yet created). See app_evergreen.jsx (view==='workspace'). // Hub cards, grouped into three tiers: // active — built and in use; the card opens the hub (id is the hub key). // planned — identified from THIS org's chart (a department without a hub yet). // template — generic Proxa suggestions, shown as starting points. // Only `active` cards are live/clickable; planned & template are shaded. const WS_HUBS = [ // ── Active (built) ── { tier: 'active', id: 'Finance', name: 'Finance', live: true, desc: 'Actuals in, forecast recomputed, board pack out — with assumptions owned.', icon: '' }, { tier: 'active', id: 'Contracts', name: 'Customer', live: true, desc: 'Contracts, Salesforce and customer success in one representation.', icon: '' }, { tier: 'active', id: 'Diligence', name: 'Diligence', live: true, desc: 'A target modelled once — memo, model and checklist draw on the same state.', icon: '' }, { tier: 'active', id: 'Tenney North Park', name: 'Tenney North Park', live: true, desc: 'Pipeline, valuation and lease-up for a 125-unit multi-housing asset.', icon: '' }, // ── Planned (identified from the org chart) ── { tier: 'planned', id: 'operations', name: 'Operations', desc: 'Manufacturing and fulfillment across the Portland and Lebanon plants.', icon: '' }, { tier: 'planned', id: 'sales', name: 'Sales', desc: 'The reasoning behind each deal, not just the shape of the pipeline.', icon: '' }, { tier: 'planned', id: 'people', name: 'People', desc: 'Roles, responsibilities and compensation — planning you can compute.', icon: '' }, { tier: 'planned', id: 'rd', name: 'R&D', desc: 'Product and process development with dependencies made explicit.', icon: '' }, // ── Templates (generic suggestions) ── { tier: 'template', id: 'organization', name: 'Organization', desc: 'Every domain connected — a representation of the organization itself.', icon: '' }, { tier: 'template', id: 'product', name: 'Product', desc: 'Roadmap, decisions and metrics with their dependencies made explicit.', icon: '' }, { tier: 'template', id: 'asset', name: 'Asset', desc: 'Structure, cash flows, covenants and valuation — recomputed, not rebuilt.', icon: '' }, ]; // The three tiers, in display order, with a one-line rationale each. const WS_HUB_SECTIONS = [ { key: 'active', label: 'Active', sub: 'Built and in use.' }, { key: 'planned', label: 'Planned', sub: 'Identified from your structure — not built yet.' }, { key: 'template', label: 'Templates', sub: 'Generic starting points if you haven’t mapped your structure yet.' }, ]; // ── Team ────────────────────────────────────────────────────────────────── // Workspace-level membership: who can reach this workspace and at what level. // Distinct from a hub's Settings → Users, which scopes permission per hub. // People are the real Cascade Ridge leadership from hr/CascadeRidge-HR-Roster.xlsx // (EMP-1001…EMP-1016), so the artifact owners on the hub pages resolve here. const WS_ROLES = { owner: { label: 'Owner', tint: 'var(--accent-purple, #A374DB)' }, admin: { label: 'Admin', tint: 'var(--accent-blue, #4378E0)' }, editor: { label: 'Editor', tint: 'var(--accent-green, #52B26E)' }, viewer: { label: 'Viewer', tint: 'var(--fg-light-muted, #A09792)' }, }; // `hubs` holds hub display names; a member with all three sees the whole workspace. const WS_TEAM = [ { id: 'EMP-1001', name: 'Linda Petrov', title: 'CEO', role: 'owner', hubs: ['Finance', 'Customer', 'Tenney North Park'], seen: 'Active now', seenTs: 100 }, { id: 'EMP-1004', name: 'Trevor Garcia', title: 'Controller', role: 'admin', you: true, photo: 'assets/avatar.jpg', hubs: ['Finance', 'Customer', 'Tenney North Park'], seen: 'Active now', seenTs: 99 }, { id: 'EMP-1002', name: 'Jose Patel', title: 'CFO', role: 'admin', hubs: ['Finance', 'Customer', 'Tenney North Park'], seen: '2 hours ago', seenTs: 95 }, { id: 'EMP-1003', name: 'Monica Morales', title: 'COO', role: 'admin', hubs: ['Finance', 'Tenney North Park'], seen: 'Yesterday', seenTs: 90 }, { id: 'EMP-1015', name: 'Christina Costa', title: 'VP, Sales', role: 'editor', hubs: ['Finance', 'Customer'], seen: 'Yesterday', seenTs: 88 }, { id: 'EMP-1008', name: 'Nicole Novak', title: 'Supply Chain Manager', role: 'editor', hubs: ['Finance'], seen: '3 days ago', seenTs: 80 }, { id: 'EMP-1005', name: 'Christina Vargas', title: 'HR Manager', role: 'editor', hubs: ['Finance'], seen: '5 days ago', seenTs: 74 }, { id: 'EMP-1013', name: 'Christina Foster', title: 'VP, R&D', role: 'editor', hubs: ['Finance'], seen: '1 week ago', seenTs: 68 }, { id: 'EMP-1010', name: 'Olivia Hughes', title: 'Plant Manager · Lebanon, TN', role: 'viewer', hubs: ['Finance'], seen: '2 weeks ago', seenTs: 60 }, { id: 'EMP-1009', name: 'Christina Mitchell', title: 'Plant Manager · Portland, OR', role: 'viewer', hubs: ['Finance'], seen: '3 weeks ago', seenTs: 54 }, { id: 'EMP-1016', name: 'Trevor Sanchez', title: 'Director of Sales', role: 'editor', pending: true, hubs: ['Customer'], seen: null, seenTs: null }, ]; // first.last@ — Cascade Ridge's is from COMPANY.md §1. function wsEmail(name) { return name.toLowerCase().replace(/[^a-z\s]/g, '').trim().split(/\s+/).join('.') + '@' + wsEmailDomain(); } function wsInitials(name) { const p = name.trim().split(/\s+/); return (p.length === 1 ? p[0].slice(0, 2) : p[0][0] + p[p.length - 1][0]).toUpperCase(); } function WsTeamRow({ m }) { const role = WS_ROLES[m.role]; // Two chips fit the column; a third would wrap, so collapse to one chip + "+N". const shown = m.hubs.length > 2 ? m.hubs.slice(0, 1) : m.hubs; const rest = m.hubs.length - shown.length; return (
{m.photo ? : {wsInitials(m.name)}} {m.name} {m.you && You} {m.pending && Pending} {m.title}
{wsEmail(m.name)}
{role.label}
{shown.map((h) => {h})} {rest > 0 && +{rest}}
{m.pending ? Invite sent · awaiting : {m.seen}}
); } function WsTeam() { const [q, setQ] = React.useState(''); const [role, setRole] = React.useState('all'); const rows = React.useMemo(() => { const needle = q.trim().toLowerCase(); return wsTeam() .filter((m) => role === 'all' || m.role === role) .filter((m) => !needle || [m.name, m.title, wsEmail(m.name)] .some((f) => f.toLowerCase().includes(needle))) .slice() .sort((a, b) => (b.seenTs == null ? -Infinity : b.seenTs) - (a.seenTs == null ? -Infinity : a.seenTs)); }, [q, role]); // Role filter chips carry their own counts so the row doubles as a breakdown. const FILTERS = [{ key: 'all', label: 'All' }] .concat(Object.keys(WS_ROLES).map((k) => ({ key: k, label: WS_ROLES[k].label }))) .map((f) => ({ ...f, n: f.key === 'all' ? wsTeam().length : wsTeam().filter((m) => m.role === f.key).length })) .filter((f) => f.n > 0); return (
setQ(e.target.value)} />
{FILTERS.map((f) => ( ))}
Member Email Role Hubs Last active
{rows.length === 0 ?
No members match that search.
: rows.map((m) => )}
); } // ── Org ─────────────────────────────────────────────────────────────────── // The org chart is a DATA PRODUCT in the worksheet shell, exactly like the // Tools DPs (Calendar/Lists): the Data face is the department roster grid, the // Artifact face is the traditional org chart drawn from it, plus Control/Context. // One source array (WS_ORG) drives both faces — the chart is of DEPARTMENTS; // each department's head resolves back to a real person in WS_TEAM by id. const IconOrg = (p) => ( ); // parent is a department id (null at the top). head is an EMP id from WS_TEAM. // hub names the associated Proxa hub (if the department has one built) — this is // what the chart badge and the Data-face "Hub" column denote. const WS_ORG = [ { id: 'DEP-01', dept: 'Executive Office', parent: null, head: 'EMP-1001', focus: 'Leadership & strategy', hc: 4 }, { id: 'DEP-02', dept: 'Finance', parent: 'DEP-01', head: 'EMP-1002', focus: 'Accounting, FP&A, treasury', hc: 8, hub: 'Finance' }, { id: 'DEP-03', dept: 'Controllership', parent: 'DEP-02', head: 'EMP-1004', focus: 'Close, controls, reporting', hc: 5 }, { id: 'DEP-04', dept: 'Operations', parent: 'DEP-01', head: 'EMP-1003', focus: 'Manufacturing & fulfillment', hc: 210 }, { id: 'DEP-05', dept: 'Supply Chain', parent: 'DEP-04', head: 'EMP-1008', focus: 'Procurement & logistics', hc: 18 }, { id: 'DEP-06', dept: 'Portland Plant · OR', parent: 'DEP-04', head: 'EMP-1009', focus: 'Co-packing — 5 lines', hc: 95 }, { id: 'DEP-07', dept: 'Lebanon Plant · TN', parent: 'DEP-04', head: 'EMP-1010', focus: 'Co-packing — 4 lines', hc: 88 }, { id: 'DEP-08', dept: 'Sales', parent: 'DEP-01', head: 'EMP-1015', focus: 'Accounts & pipeline', hc: 12 }, { id: 'DEP-09', dept: 'Customer', parent: 'DEP-01', head: 'EMP-1016', focus: 'Contracts, CRM & success', hc: 7, hub: 'Customer' }, { id: 'DEP-10', dept: 'People', parent: 'DEP-01', head: 'EMP-1005', focus: 'Talent, payroll, safety', hc: 6 }, { id: 'DEP-11', dept: 'R&D', parent: 'DEP-01', head: 'EMP-1013', focus: 'Product & process development', hc: 9 }, ]; // ── Which workspace's content to render ─────────────────────────────────── // The arrays above are Cascade Ridge's. A domain pack with a `built` section // (see domains.jsx — the HLA lab) supplies its own in the same row shapes, so // every component below works unchanged. WorkspacePage sets this at the top of // its render; only one workspace is on screen at a time. let WS_DOMAIN = null; function wsBuilt() { const p = WS_DOMAIN && window.proxaDomain ? window.proxaDomain(WS_DOMAIN) : null; return (p && p.built) || null; } function wsHubs() { const b = wsBuilt(); return b ? b.hubs : WS_HUBS; } function wsTeam() { const b = wsBuilt(); return b ? b.team : WS_TEAM; } function wsOrg() { const b = wsBuilt(); return b ? b.org : WS_ORG; } function wsEmailDomain() { const b = wsBuilt(); return (b && b.emailDomain) || 'cascaderidgefoods.com'; } // Each workspace's own identity mark (domains.jsx). Falls back to Cascade // Ridge's so an un-domained workspace still renders something. function wsMark(key) { return (window.proxaMark ? window.proxaMark(key) : null) || 'assets/crf-mark.svg?v=3'; } const wsTeamById = (id) => wsTeam().find((m) => m.id === id) || {}; // Worksheet columns + rows (the Data face / data container behind the chart). const WS_ORG_COLS = ['Department', 'Parent', 'Head', 'Focus', 'Headcount', 'Hub']; function wsOrgRows() { const deptById = Object.fromEntries(wsOrg().map((d) => [d.id, d.dept])); return wsOrg().map((d) => [ d.dept, d.parent ? deptById[d.parent] : '—', wsTeamById(d.head).name || d.head, d.focus, String(d.hc), d.hub || '—', ]); } // Assemble the department tree from the flat WS_ORG list. function wsOrgTree() { const nodes = {}; wsOrg().forEach((d) => { const h = wsTeamById(d.head); nodes[d.id] = { ...d, head: h.name || d.head, headTitle: h.title || '', kids: [] }; }); let root = null; wsOrg().forEach((d) => { if (d.parent && nodes[d.parent]) nodes[d.parent].kids.push(nodes[d.id]); else if (!d.parent) root = nodes[d.id]; }); return root; } function WsOrgNode({ n }) { return (
  • {n.dept} {n.hub && ( )} {n.focus} {n.head}{n.headTitle ? ` · ${n.headTitle}` : ''}
    {n.kids.length > 0 && (
      {n.kids.map((k) => )}
    )}
  • ); } // The Artifact face: a traditional top-down org chart of departments. function WsOrgChart() { const root = React.useMemo(wsOrgTree, []); if (!root) return null; return (
    ); } // Same worksheet DP shell as the Tools DPs, but chart-first (Artifact face). function WsOrg() { return ( ); } // Badge label/class per tier (shared by the card and list views). function wsHubBadge(tier) { if (tier === 'planned') return { label: 'Planned', cls: 'planned' }; if (tier === 'template') return { label: 'Template', cls: 'template' }; return { label: 'Ready', cls: 'live' }; } // Hub columns for the shared OutputsList (reuses the Dashboard's .lib table). const WS_HUB_COLUMNS = [ { header: 'Hub', render: (h) => (
    {h.name}
    ) }, { header: 'Description', render: (h) => {h.desc} }, { header: 'Status', thClass: 'r', tdClass: 'r', render: (h) => { const b = wsHubBadge(h.tier); return {b.label}; } }, ]; // Card/List view toggle — reuses the hub Dashboard's .outputs-viewtoggle styling. function WsViewToggle({ value, onChange }) { const opts = [ ['card', 'Card', <>], ['list', 'List', <>], ]; return (
    {opts.map(([k, label, path]) => ( ))}
    ); } // Rich empty-state used for every workspace section: a hatched panel with an // icon, title, one-line prompt and an "Add …" CTA (opens the assistant). function WsSectionEmpty({ Icon, title, sub, ctaLabel, onCta, children }) { return (
    {title}

    {sub}

    {ctaLabel && ( )}
    {children}
    ); } // Fallback icon for a draft hub whose name doesn't match a template (a cube). const WS_DRAFT_ICON = ''; // First-time-user Hubs surface for a brand-new (empty) workspace: the shared // empty-state hero plus the generic Templates to start from. Clicking a // template BUILDS it — a draft hub lands in the rail and the template flow // opens (Model → Context). Built drafts render as live cards above Templates. // Step 0 of an empty workspace: what are you modelling? The answer swaps the // template set, the starting documents and the data products — the flow itself // is identical either way (see domains.jsx). function WsDomainPick({ onPick }) { const order = window.PROXA_DOMAIN_ORDER || ['company']; const packs = window.PROXA_DOMAINS || {}; return (
    What are you modelling? Claude needs to know the shape of the work before it can propose anything.
    {order.map((key) => { const p = packs[key]; if (!p) return null; return ( ); })}
    ); } function WsHubsEmpty({ draftHubs = [], onOpenHub, onBuildFromTemplate, onGuide, domain, onPickDomain }) { const Card = window.NHTemplateCard; // No domain yet → ask what we're modelling before showing any template. if (!domain && onPickDomain) return ; const pack = window.proxaDomain ? window.proxaDomain(domain) : null; // Templates come from the domain pack; WS_HUBS is the company fallback. const templates = pack ? pack.groups.flatMap((g) => g.items).map((t) => ({ ...t, live: false })) : wsHubs().filter((h) => h.tier === 'template'); const templateSection = (
    Templates {templates.length} Generic starting points — pick one and Claude builds it.
    {templates.map((h) => ( {}} onPropose={() => onBuildFromTemplate && onBuildFromTemplate(h.id, h.name)} /> ))}
    ); // Once the workspace has draft hubs, the hero yields to a Drafts section. if (draftHubs.length > 0) { return (
    Drafts {draftHubs.length} Being built — open one to continue.
    {draftHubs.map((name) => { const tpl = wsHubs().find((h) => h.tier === 'template' && name.indexOf(h.name) === 0); return ( onOpenHub && onOpenHub(name)} /> ); })}
    {templateSection}
    ); } return ( onGuide && onGuide('hub')}> {templateSection} ); } // Empty-state body for the not-yet-built Workspace tabs (Context). function WsEmpty({ Icon, title, sub }) { return (
    {title}
    {sub}
    ); } function WorkspacePage({ onOpenHub, onOpenSettings, onOpenAgentChat, onProposeHub, workspaces, activeWorkspaceId, onSwitchWorkspace, onCreateWorkspace, onRemoveWorkspace, draftHubs = [], onBuildFromTemplate, onCreateHub, onGuide, domain, onPickDomain, domainOf }) { const Card = window.NHTemplateCard; // Point the content resolvers at this workspace's domain before anything // below reads them (see WS_DOMAIN). WS_DOMAIN = domain || null; // Tabs behave like a hub's: a pill row switches the panel below. Graph/About // embed the existing full pages inline; their close/Esc returns to Hubs. const [tab, setTab] = React.useState('hubs'); const [hubsView, setHubsView] = React.useState('card'); // 'card' | 'list' // Workspace switcher — state lives in the app (so the rail reflects it too). const wsList = workspaces || [{ id: 'cascade', name: 'Cascade Ridge Foods' }]; const activeWs = activeWorkspaceId || 'cascade'; const [wsMenu, setWsMenu] = React.useState(false); const activeWorkspace = wsList.find((w) => w.id === activeWs) || wsList[0]; const isEmpty = !!activeWorkspace.empty; const PILLS = [ { key: 'hubs', label: 'Hubs', Icon: IconGrid, count: isEmpty ? draftHubs.length : wsHubs().filter((h) => h.tier === 'active').length }, { key: 'team', label: 'Team', Icon: IconUsers, count: isEmpty ? 0 : wsTeam().length }, { key: 'org', label: 'Org', Icon: IconOrg, count: isEmpty ? 0 : wsOrg().length }, // departments { key: 'graph', label: 'Graph', Icon: IconGraph }, { key: 'context', label: 'Context', Icon: IconInfo }, { key: 'about', label: 'About', Icon: IconChip }, ]; return (
    Workspace
    {wsMenu && (
    setWsMenu(false)} aria-hidden="true" />
    Workspaces
    {wsList.map((w) => (
    {!w.builtIn && onRemoveWorkspace && ( )}
    ))}
    )}
    {tab === 'hubs' && isEmpty && ( )} {tab === 'hubs' && !isEmpty && (
    {WS_HUB_SECTIONS.map((sec) => { const items = wsHubs().filter((h) => h.tier === sec.key); if (items.length === 0) return null; return (
    {sec.label} {items.length} {sec.sub}
    {hubsView === 'list' ? 'click'} onRowClick={(h) => (h.live ? onOpenHub(h.id) : onProposeHub && onProposeHub(h.name))} /> :
    {items.map((h) => ( onProposeHub && onProposeHub(h.name)} /> ))}
    }
    ); })}
    )} {tab === 'team' && (isEmpty ? onGuide && onGuide('team')} /> : )} {tab === 'org' && (isEmpty ? onGuide && onGuide('org')} /> :
    )} {tab === 'graph' && (isEmpty ? onGuide && onGuide('hub')} /> :
    setTab('hubs')} domain={domain} />
    )} {tab === 'context' && ( onGuide && onGuide('context')} /> )} {tab === 'about' && (isEmpty ? onGuide && onGuide('about')} /> :
    setTab('hubs')} domain={domain} />
    )}
    ); } window.WorkspacePage = WorkspacePage;