Initial commit
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type {
|
||||
OrgInfoPayload,
|
||||
OrgCreateMemberInput,
|
||||
OrgSavedCreatePayload,
|
||||
SavedOrgSummary,
|
||||
TalentTemplate,
|
||||
EmployeeDetailPayload,
|
||||
ReorgProposalInfo,
|
||||
ArchitecturePreset,
|
||||
ArchitecturePresetDetail,
|
||||
HireTalentHandler,
|
||||
} from '../types/visual'
|
||||
import { TeamView } from './TeamView'
|
||||
import { DelegationStrategyPanel } from './DelegationStrategyPanel'
|
||||
import { ArchitectureMarketplace } from './ArchitectureMarketplace'
|
||||
import { EmployeesMarketplace } from './EmployeesMarketplace'
|
||||
import { ConfigImportExportPanel } from './ConfigImportExportPanel'
|
||||
import { OrgCreateModal } from './OrgCreateModal'
|
||||
import { getRuntimeOrgView } from '../lib/runtimeOrg'
|
||||
import './org.css'
|
||||
import './team.css'
|
||||
import './marketplace.css'
|
||||
import './config.css'
|
||||
import './structure.css'
|
||||
|
||||
/* ── Inline SVG icon data-URIs (no external CDN) ────────────────── */
|
||||
const TAB_ICON = {
|
||||
team: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/%3E%3C/svg%3E",
|
||||
runtime: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M13 2.05v2.02c3.95.49 7 3.85 7 7.93 0 3.21-1.81 6-4.72 7.72L13 17v5h5l-1.22-1.22C19.91 19.07 22 15.76 22 12c0-5.18-3.95-9.45-9-9.95zM11 2.05C5.95 2.55 2 6.82 2 12c0 3.76 2.09 7.07 5.22 8.78L6 22h5V2.05z'/%3E%3C/svg%3E",
|
||||
architecture: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z'/%3E%3C/svg%3E",
|
||||
employees: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E",
|
||||
}
|
||||
const STAT_ICON = {
|
||||
agents: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M20 9V7c0-1.1-.9-2-2-2h-3c0-1.66-1.34-3-3-3S9 3.34 9 5H6c-1.1 0-2 .9-2 2v2c-1.66 0-3 1.34-3 3s1.34 3 3 3v4c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-4c1.66 0 3-1.34 3-3s-1.34-3-3-3z'/%3E%3C/svg%3E",
|
||||
active: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2322c55e' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z'/%3E%3C/svg%3E",
|
||||
}
|
||||
const SECTION_ICON = {
|
||||
packages: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM12 17.5L6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z'/%3E%3C/svg%3E",
|
||||
channels: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z'/%3E%3C/svg%3E",
|
||||
reorg: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z'/%3E%3C/svg%3E",
|
||||
importPkg: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z'/%3E%3C/svg%3E",
|
||||
}
|
||||
|
||||
type SubTab = 'team' | 'runtime' | 'architecture' | 'employees'
|
||||
|
||||
function humanizeOrgName(value?: string | null): string {
|
||||
const normalized = String(value ?? '').trim()
|
||||
if (!normalized) return ''
|
||||
return normalized
|
||||
.replace(/^org[_-]/i, '')
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\b\w/g, char => char.toUpperCase())
|
||||
}
|
||||
|
||||
interface OrgTabProps {
|
||||
data: OrgInfoPayload | null
|
||||
/** role_id -> recruited names for the selected session (canvas display only). */
|
||||
sessionRecruitmentByRole?: Record<string, string[]> | null
|
||||
talents: TalentTemplate[]
|
||||
employeeDetail: EmployeeDetailPayload | null
|
||||
reorgProposals: ReorgProposalInfo[]
|
||||
isCustomMode?: boolean
|
||||
onRequestData: () => void
|
||||
onRequestTalents: () => void
|
||||
onRequestEmployeeDetail: (employeeId: string) => void
|
||||
onHireTalent: HireTalentHandler
|
||||
hiringTemplateId?: string | null
|
||||
onImportEmployee?: (employeeId: string) => void
|
||||
onRequestReorgList: () => void
|
||||
onReorgDecide: (proposalId: string, approved: boolean, notes?: string) => void
|
||||
// Market
|
||||
onMarketExport?: (data: { package_id: string; name: string; description: string; version: string }) => void
|
||||
onMarketInstall?: (path: string, strategy: string) => void
|
||||
onMarketUninstall?: (packageId: string) => void
|
||||
// Architecture gallery
|
||||
marketPresets?: ArchitecturePreset[]
|
||||
marketPreviewData?: ArchitecturePresetDetail | null
|
||||
onMarketBrowse?: () => void
|
||||
onMarketPreview?: (presetId: string) => void
|
||||
onMarketApplyPreset?: (presetId: string, strategy: string) => void
|
||||
onMarketClearPreview?: () => void
|
||||
// Config import/export
|
||||
onConfigExport?: () => void
|
||||
onConfigImport?: (yaml: string, dryRun: boolean) => void
|
||||
configExportYaml?: string | null
|
||||
configImportPreview?: { roles_added: number; roles_removed: number; employees_changed: number } | null
|
||||
configImportError?: string | null
|
||||
// Saved org architectures (named snapshots) — rendered in the Team tab toolbar
|
||||
onSavedOrgsList?: () => void
|
||||
onSavedOrgSaveAs?: (name: string, overwrite: boolean) => void
|
||||
onSavedOrgCreate?: (organizationName: string, members: OrgCreateMemberInput[]) => void
|
||||
onSavedOrgLoad?: (name: string) => void
|
||||
onSavedOrgDelete?: (name: string) => void
|
||||
savedOrgsList?: SavedOrgSummary[] | null
|
||||
activeSavedOrg?: string | null
|
||||
activeSavedOrgVersionAtLoad?: number | null
|
||||
orgCreatePending?: boolean
|
||||
orgCreateResult?: (OrgSavedCreatePayload & { nonce: number }) | null
|
||||
onSelectCorporate?: () => void
|
||||
// Org editing
|
||||
onAddRole?: (roleId: string, name: string, responsibility: string, reportsTo: string, icon?: string | null) => void
|
||||
onBulkAddRoles?: (roles: Array<{ role_id: string; name: string; responsibility: string; reports_to: string }>) => void
|
||||
onUpdateRole?: (roleId: string, updates: { name?: string; responsibility?: string; reports_to?: string; can_spawn?: string[]; icon?: string | null; execution_strategy?: string; preferred_external_agent?: string | null; prompt_refs?: string[] }) => void
|
||||
onDeleteRole?: (roleId: string) => void
|
||||
onUpdateOrgStrategy?: (data: { final_decider_role_id?: string | null }) => void
|
||||
onUpdateRuntimePolicy?: (policy: Record<string, any>) => void
|
||||
onResetArchitecture?: () => void
|
||||
}
|
||||
|
||||
export function OrgTab({
|
||||
data, sessionRecruitmentByRole, talents, employeeDetail, reorgProposals, isCustomMode,
|
||||
onRequestData, onRequestTalents, onRequestEmployeeDetail,
|
||||
onHireTalent, hiringTemplateId, onImportEmployee, onRequestReorgList, onReorgDecide,
|
||||
onMarketExport, onMarketInstall, onMarketUninstall,
|
||||
marketPresets, marketPreviewData, onMarketBrowse, onMarketPreview, onMarketApplyPreset, onMarketClearPreview,
|
||||
onAddRole, onBulkAddRoles, onUpdateRole, onDeleteRole, onUpdateOrgStrategy,
|
||||
onUpdateRuntimePolicy, onResetArchitecture,
|
||||
onConfigExport, onConfigImport, configExportYaml, configImportPreview, configImportError,
|
||||
onSavedOrgsList, onSavedOrgSaveAs, onSavedOrgCreate, onSavedOrgLoad, onSavedOrgDelete, savedOrgsList,
|
||||
activeSavedOrg, activeSavedOrgVersionAtLoad, orgCreatePending, orgCreateResult, onSelectCorporate,
|
||||
}: OrgTabProps) {
|
||||
const [activeTab, setActiveTab] = useState<SubTab>('team')
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
|
||||
const switchTab = useCallback((tab: SubTab) => {
|
||||
setActiveTab(tab)
|
||||
}, [])
|
||||
const [applyingPresetId, setApplyingPresetId] = useState<string | null>(null)
|
||||
const versionAtApply = useRef<number>(-1) // track org_version when apply starts
|
||||
const [toast, setToast] = useState<{ msg: string; type: 'info' | 'warn' } | null>(null)
|
||||
const toastTimer = useRef<ReturnType<typeof setTimeout>>(null)
|
||||
|
||||
const showToast = useCallback((msg: string, type: 'info' | 'warn' = 'info') => {
|
||||
setToast({ msg, type })
|
||||
if (toastTimer.current) clearTimeout(toastTimer.current)
|
||||
toastTimer.current = setTimeout(() => setToast(null), 3000)
|
||||
}, [])
|
||||
|
||||
useEffect(() => () => {
|
||||
if (toastTimer.current) clearTimeout(toastTimer.current)
|
||||
}, [])
|
||||
|
||||
const onRequestDataRef = useRef(onRequestData)
|
||||
const onRequestTalentsRef = useRef(onRequestTalents)
|
||||
const onRequestReorgListRef = useRef(onRequestReorgList)
|
||||
const onMarketBrowseRef = useRef(onMarketBrowse)
|
||||
const onSavedOrgsListRef = useRef(onSavedOrgsList)
|
||||
onRequestDataRef.current = onRequestData
|
||||
onRequestTalentsRef.current = onRequestTalents
|
||||
onRequestReorgListRef.current = onRequestReorgList
|
||||
onMarketBrowseRef.current = onMarketBrowse
|
||||
onSavedOrgsListRef.current = onSavedOrgsList
|
||||
|
||||
useEffect(() => {
|
||||
onRequestDataRef.current()
|
||||
onRequestTalentsRef.current()
|
||||
onRequestReorgListRef.current()
|
||||
onMarketBrowseRef.current?.()
|
||||
onSavedOrgsListRef.current?.()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!orgCreateResult || !orgCreateResult.ok) return
|
||||
setCreateOpen(false)
|
||||
setActiveTab('team')
|
||||
showToast(`Created ${orgCreateResult.organization_name || orgCreateResult.name} and saved automatically`)
|
||||
}, [orgCreateResult, showToast])
|
||||
|
||||
// In org mode: show only user-owned roles. In company mode: show all roles read-only.
|
||||
const allRoles = data?.roles ?? []
|
||||
const displayRoles = useMemo(() => isCustomMode ? allRoles.filter(r => !r.is_builtin) : allRoles, [allRoles, isCustomMode])
|
||||
const displayEmployees = useMemo(() => data?.employees ?? [], [data?.employees])
|
||||
const runtimeView = useMemo(() => getRuntimeOrgView(data), [data])
|
||||
const activeAgents = useMemo(() => displayEmployees.filter(e => e.linked_agent_id), [displayEmployees])
|
||||
const configuredOrgName = data?.organization_name?.trim()
|
||||
const activeOrgLabel = configuredOrgName || humanizeOrgName(activeSavedOrg) || (isCustomMode ? 'Custom org' : 'Corporate company')
|
||||
const activeOrgId = (isCustomMode ? (data?.organization_id || activeSavedOrg) : 'corporate') || ''
|
||||
const architectureKindLabel = isCustomMode ? 'Saved org' : 'Corporate'
|
||||
const architectureStateLabel = isCustomMode
|
||||
? activeSavedOrg ? 'Editable saved architecture' : 'Editable draft architecture'
|
||||
: 'Built-in read-only architecture'
|
||||
const runtimeStateLabel = runtimeView.frontier.status || runtimeView.projectRun?.status || runtimeView.projectRun?.lifecycle_status || 'ready'
|
||||
|
||||
// Roles that already have at least one non-placeholder employee.
|
||||
const filledRoleIds = useMemo(
|
||||
() => {
|
||||
const ids = new Set<string>()
|
||||
for (const employee of displayEmployees) {
|
||||
if (employee.is_default_employee) continue
|
||||
const roleIds = employee.role_ids?.length ? employee.role_ids : [employee.role_id]
|
||||
for (const roleId of roleIds) {
|
||||
if (roleId) ids.add(roleId)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
},
|
||||
[displayEmployees],
|
||||
)
|
||||
const vacantRoles = useMemo(() => displayRoles.filter(r => !filledRoleIds.has(r.role_id)), [displayRoles, filledRoleIds])
|
||||
|
||||
const installedIds = useMemo(() => new Set((data?.installed_packages ?? []).map(p => p.package_id)), [data?.installed_packages])
|
||||
|
||||
// When applying a preset, wait for org_version to change (every config.save() increments it)
|
||||
const orgVersion = data?.org_version ?? 0
|
||||
useEffect(() => {
|
||||
if (applyingPresetId && versionAtApply.current >= 0 && orgVersion !== versionAtApply.current) {
|
||||
setApplyingPresetId(null)
|
||||
versionAtApply.current = -1
|
||||
setActiveTab('team')
|
||||
showToast('Architecture applied successfully')
|
||||
}
|
||||
}, [orgVersion, applyingPresetId, showToast])
|
||||
|
||||
const handleApplyPreset = (presetId: string, strategy: string) => {
|
||||
versionAtApply.current = orgVersion // snapshot current version
|
||||
setApplyingPresetId(presetId)
|
||||
onMarketApplyPreset?.(presetId, strategy)
|
||||
}
|
||||
|
||||
const handleOrgSelection = (value: string) => {
|
||||
if (value === 'corporate') {
|
||||
onSelectCorporate?.()
|
||||
return
|
||||
}
|
||||
if (value.startsWith('org:')) {
|
||||
const orgName = value.slice(4)
|
||||
if (orgName) onSavedOrgLoad?.(orgName)
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="org-tab"><div className="org-loading">Loading organization data...</div></div>
|
||||
}
|
||||
|
||||
const installedPackages = data.installed_packages ?? []
|
||||
const savedOrgOptions = savedOrgsList ?? []
|
||||
const selectedOrgValue = isCustomMode && activeSavedOrg ? `org:${activeSavedOrg}` : 'corporate'
|
||||
|
||||
return (
|
||||
<div className="org-tab">
|
||||
<div className={`org-header${isCustomMode ? ' org-header--custom' : ' org-header--corporate'}`}>
|
||||
<div className="org-header-main">
|
||||
<div className="org-eyebrow">
|
||||
<span>Company</span>
|
||||
<span className="org-eyebrow-separator">/</span>
|
||||
<span>{architectureKindLabel}</span>
|
||||
</div>
|
||||
<div className="org-title-row">
|
||||
<h2 className="org-title">{activeOrgLabel}</h2>
|
||||
<span className="org-version-badge">v{data.org_version}</span>
|
||||
<span className={`org-state-badge${isCustomMode ? ' org-state-badge--editable' : ' org-state-badge--readonly'}`}>
|
||||
{architectureStateLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="org-header-meta">
|
||||
<span className="org-meta-pill">{data.company_profile || (isCustomMode ? 'custom' : 'corporate')}</span>
|
||||
{activeOrgId && <code className="org-meta-code">{activeOrgId}</code>}
|
||||
<span className="org-meta-pill org-meta-pill--runtime">{runtimeStateLabel}</span>
|
||||
<span className="org-meta-pill org-meta-pill--saved">Auto-saved</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="org-control-panel">
|
||||
<label className="org-switcher">
|
||||
<span className="org-switcher-label">Organization</span>
|
||||
<span className="org-switcher-select-wrap">
|
||||
<select
|
||||
className="org-switcher-select"
|
||||
value={selectedOrgValue}
|
||||
onChange={e => handleOrgSelection(e.target.value)}
|
||||
onFocus={() => onSavedOrgsList?.()}
|
||||
onPointerDown={() => onSavedOrgsList?.()}
|
||||
aria-label="Organization"
|
||||
>
|
||||
<option value="corporate">Corporate</option>
|
||||
{savedOrgOptions.map(org => (
|
||||
<option key={org.name} value={`org:${org.name}`}>
|
||||
{(org.organization_name || org.name).trim() || org.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</label>
|
||||
<button type="button" className="org-create-trigger" onClick={() => setCreateOpen(true)}>
|
||||
<span className="org-create-trigger-icon" aria-hidden>+</span>
|
||||
New organization
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="org-stats-strip">
|
||||
<span className="org-stat">
|
||||
<img src={STAT_ICON.agents} alt="" className="org-stat-icon" />
|
||||
<b>{displayRoles.length}</b> roles
|
||||
</span>
|
||||
<span className="org-stat">
|
||||
<img src={TAB_ICON.employees} alt="" className="org-stat-icon" />
|
||||
<b>{displayEmployees.length}</b> employees
|
||||
</span>
|
||||
<span className="org-stat">
|
||||
<img src={TAB_ICON.runtime} alt="" className="org-stat-icon" />
|
||||
<b>{runtimeView.runtimeTeams.length}</b> runtime teams
|
||||
</span>
|
||||
<span className="org-stat">
|
||||
<img src={STAT_ICON.active} alt="" className="org-stat-icon" />
|
||||
<b>{activeAgents.length}</b> active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="org-subtabs">
|
||||
{([
|
||||
{ id: 'team' as SubTab, icon: TAB_ICON.team, label: 'Team', count: displayRoles.length },
|
||||
{ id: 'runtime' as SubTab, icon: TAB_ICON.runtime, label: 'Runtime', count: runtimeView.runtimeTeams.length },
|
||||
{ id: 'architecture' as SubTab, icon: TAB_ICON.architecture, label: 'Architecture', count: marketPresets?.length ?? 0 },
|
||||
{ id: 'employees' as SubTab, icon: TAB_ICON.employees, label: 'Employees', count: talents.length },
|
||||
]).map(tab => (
|
||||
<button key={tab.id}
|
||||
className={`org-subtab${activeTab === tab.id ? ' org-subtab--active' : ''}`}
|
||||
onClick={() => switchTab(tab.id)}>
|
||||
<img src={tab.icon} alt="" className="org-subtab-icon" />
|
||||
<span className="org-subtab-label">{tab.label}</span>
|
||||
<span className="org-subtab-count">{tab.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── Tab content ─────────────────────────────────── */}
|
||||
{/* Toast notification */}
|
||||
{toast && (
|
||||
<div className={`org-toast org-toast--${toast.type}`} onClick={() => setToast(null)}>
|
||||
{toast.msg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="org-tab-content">
|
||||
|
||||
{/* Team tab */}
|
||||
{activeTab === 'team' && (
|
||||
<TeamView
|
||||
roles={displayRoles}
|
||||
employees={displayEmployees}
|
||||
sessionRecruitmentByRole={sessionRecruitmentByRole}
|
||||
isCustomMode={isCustomMode}
|
||||
onAddRole={onAddRole ?? (() => {})}
|
||||
onBulkAddRoles={onBulkAddRoles}
|
||||
onUpdateRole={onUpdateRole ?? (() => {})}
|
||||
onDeleteRole={onDeleteRole ?? (() => {})}
|
||||
onExport={onMarketExport ?? (() => {})}
|
||||
onImportEmployee={onImportEmployee}
|
||||
onResetArchitecture={onResetArchitecture}
|
||||
onSwitchToTab={(target) => setActiveTab(target)}
|
||||
savedOrgsList={savedOrgsList}
|
||||
activeSavedOrg={activeSavedOrg ?? null}
|
||||
currentOrgVersion={data?.org_version ?? 0}
|
||||
versionAtLoad={activeSavedOrgVersionAtLoad ?? null}
|
||||
onSavedOrgsList={onSavedOrgsList}
|
||||
onSavedOrgSaveAs={onSavedOrgSaveAs}
|
||||
onSavedOrgLoad={onSavedOrgLoad}
|
||||
onSavedOrgDelete={onSavedOrgDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Runtime tab */}
|
||||
{activeTab === 'runtime' && (
|
||||
<DelegationStrategyPanel
|
||||
roles={displayRoles}
|
||||
runtimeTeams={runtimeView.runtimeTeams}
|
||||
runtimeSeats={runtimeView.runtimeSeats}
|
||||
workItems={runtimeView.workItems}
|
||||
frontier={runtimeView.frontier}
|
||||
companyProfile={data.company_profile}
|
||||
runtimePolicy={data.runtime_policy}
|
||||
finalDeciderRoleId={data.final_decider_role_id}
|
||||
topLevelRoleIds={data.top_level_role_ids}
|
||||
readOnly={!isCustomMode}
|
||||
onUpdateOrgStrategy={onUpdateOrgStrategy}
|
||||
onUpdateRuntimePolicy={onUpdateRuntimePolicy}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Architecture tab */}
|
||||
{activeTab === 'architecture' && (
|
||||
<>
|
||||
<ArchitectureMarketplace
|
||||
presets={marketPresets ?? []}
|
||||
installedIds={installedIds}
|
||||
previewData={marketPreviewData ?? null}
|
||||
applyingPresetId={applyingPresetId}
|
||||
readOnly={!isCustomMode}
|
||||
onPreview={onMarketPreview ?? (() => {})}
|
||||
onApplyPreset={handleApplyPreset}
|
||||
onClearPreview={onMarketClearPreview ?? (() => {})}
|
||||
installedPackages={installedPackages}
|
||||
channels={data.channels}
|
||||
reorgProposals={reorgProposals}
|
||||
isCustomMode={!!isCustomMode}
|
||||
onReorgDecide={onReorgDecide}
|
||||
onMarketInstall={(p, s) => onMarketInstall?.(p, s)}
|
||||
onMarketUninstall={(id) => onMarketUninstall?.(id)}
|
||||
/>
|
||||
{isCustomMode && (
|
||||
<ConfigImportExportPanel
|
||||
onExport={onConfigExport ?? (() => {})}
|
||||
onImport={onConfigImport ?? (() => {})}
|
||||
configExportYaml={configExportYaml ?? null}
|
||||
importPreview={configImportPreview ?? null}
|
||||
importError={configImportError ?? null}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Employees tab */}
|
||||
{activeTab === 'employees' && (
|
||||
<EmployeesMarketplace
|
||||
templates={talents}
|
||||
vacantRoles={vacantRoles}
|
||||
hiringTemplateId={hiringTemplateId ?? null}
|
||||
readOnly={!isCustomMode}
|
||||
onHireTalent={onHireTalent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<OrgCreateModal
|
||||
open={createOpen}
|
||||
pending={orgCreatePending}
|
||||
result={orgCreateResult ?? null}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
onCreate={(organizationName, members) => onSavedOrgCreate?.(organizationName, members)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user