gongxiangp2/components/organization/OrganizationWorkspace.tsx

662 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 方向契约(云采石场 · seed 32cb33b5)
* THESIS: 组织架构页让"人"归位到真实组织树——部门即层叠的石料块,人员是块上的铭牌;
* 拒绝旧后台"平铺列表+堆卡片"的默认布局,用左右结构(左=组织采石剖面,右=人员明细)直接呈现层级与归属。
* OWN-WORLD: 浅色工业风——石材白/灰蓝地面、切割蓝(#5a66b8)唯一强调色、块状层叠卡片(细边框+轻投影)、
* 工程数据用等宽数字、状态用绿/琥珀信号色。
* STORY: 管理员打开页面即看到"公司这座山被切成一层层部门":点哪一层,右侧露出该层成员名单,
* 等级(M/P)、直属主管、在职状态像工程铭牌一样一目了然;未绑定/未确定人员单独成垛,不混入。
* FIRST VIEWPORT: 左 300px 组织树(层叠石料块,高亮当前层,节点带人数);右主区顶部=当前层路径+四格工程统计,
* 下方=人员明细表(花名/工号/职位/等级/主管/状态)。
* FORM: 云采石场方向(用户从概念骰子选定,替代脚本分配的深海潜水)。
* FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md
*/
'use client'
import { useEffect, useMemo, useState, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import BoardShell from '@/components/board/BoardShell'
import {
lastOrganizationPathSegment,
personBelongsToOrganizationPath,
} from '@/lib/organization-material-permissions'
// ---------- 类型 ----------
interface OrgNode {
id: string
name: string
path: string
level: number
parentId: string | null
count: number
children: OrgNode[]
}
interface EmployeeRow {
employeeProfileId: string
userId: string | null
username: string | null
realName: string | null
displayName: string | null
jobNumber: string | null
departmentPath: string | null
departmentPaths?: string[]
departmentName: string | null
positionTitle: string | null
positionLevel: string | null
levelType: string | null
managerName: string | null
isLeadership: boolean
employmentState: string | null
accountIsActive: boolean | null
}
interface UnboundUser {
userId: string
username: string
realName: string | null
oldDepartment: string | null
}
interface OrgResponse {
tree: OrgNode[]
employees: EmployeeRow[]
unclassified: EmployeeRow[]
unbound: UnboundUser[]
summary: {
totalEmployees: number
activeEmployees: number
inactiveEmployees: number
unboundCount: number
unclassifiedCount: number
visibleCount: number
scope: string
}
}
const LEVEL_TYPE_LABEL: Record<string, string> = {
M: '管理',
P: '专业',
S: '支持',
}
function isManagementLevel(employee: EmployeeRow) {
if (employee.levelType === 'M') return true
return /^M\d+/i.test(employee.positionLevel || '')
}
const STATE_LABEL: Record<string, string> = {
ACTIVE: '在职',
RESIGNED: '离职',
UNKNOWN: '未知',
}
type OrganizationWorkspaceProps = {
embedded?: boolean
scopeRootName?: string
heading?: string
description?: string
treeAsSecondarySidebar?: boolean
}
function findOrganizationNode(nodes: OrgNode[], name: string): OrgNode | null {
for (const node of nodes) {
if (node.name === name) return node
const match = findOrganizationNode(node.children, name)
if (match) return match
}
return null
}
// ---------- 组件 ----------
export default function OrganizationWorkspace({
embedded = false,
scopeRootName,
heading = '组织架构',
description = '品牌中心在职人员 · 数据源:HR 花名册(每日 02:00 自动同步)',
treeAsSecondarySidebar = false,
}: OrganizationWorkspaceProps) {
const { data: session, status } = useSession()
const router = useRouter()
const sessionUser = session?.user as
| { role?: string; isSuperAdmin?: boolean; managedDepartment?: string | null }
| undefined
const canAccess = ['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN'].includes(sessionUser?.role || '')
const isSuperAdmin = Boolean(sessionUser?.isSuperAdmin)
const [data, setData] = useState<OrgResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [includeInactive, setIncludeInactive] = useState(false)
const [expanded, setExpanded] = useState<Set<string>>(new Set())
const [selectedPath, setSelectedPath] = useState<string | null>(null)
const [selectedGroup, setSelectedGroup] = useState<'org' | 'unclassified' | 'unbound'>('org')
const [keyword, setKeyword] = useState('')
// 权限与重定向
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login')
return
}
if (status === 'authenticated' && !canAccess) {
router.push('/')
}
}, [status, canAccess, router])
// 加载数据
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(`/api/organization?includeInactive=${includeInactive}`)
if (!res.ok) {
const body = await res.json().catch(() => null)
throw new Error(body?.error || '加载失败')
}
const json = (await res.json()) as OrgResponse
setData(json)
const defaultNode = scopeRootName
? findOrganizationNode(json.tree, scopeRootName)
: json.tree.find((node) => node.count > 0) ?? null
setSelectedGroup('org')
setKeyword('')
if (defaultNode) {
setExpanded(new Set([defaultNode.id]))
setSelectedPath(defaultNode.path)
} else {
setExpanded(new Set())
setSelectedPath(null)
}
} catch (e) {
setError(e instanceof Error ? e.message : '加载失败')
} finally {
setLoading(false)
}
}, [includeInactive, scopeRootName])
useEffect(() => {
if (status === 'authenticated' && canAccess) {
void fetchData()
}
}, [status, canAccess, fetchData])
const scopeRootNode = useMemo(() => {
if (!data || !scopeRootName) return null
return findOrganizationNode(data.tree, scopeRootName)
}, [data, scopeRootName])
const scopedTree = useMemo(() => {
if (!data) return []
if (!scopeRootName) return data.tree
return scopeRootNode ? [scopeRootNode] : []
}, [data, scopeRootName, scopeRootNode])
const scopedEmployees = useMemo(() => {
if (!data) return []
if (!scopeRootName) return data.employees
if (!scopeRootNode) return []
return data.employees.filter((employee) =>
personBelongsToOrganizationPath(
employee.departmentPaths?.length ? employee.departmentPaths : [employee.departmentPath],
scopeRootNode.path,
),
)
}, [data, scopeRootName, scopeRootNode])
// 展平树为可渲染的行(带缩进深度)
const flatTree = useMemo(() => {
const rows: Array<{ node: OrgNode; depth: number }> = []
const walk = (nodes: OrgNode[], depth: number) => {
for (const node of nodes) {
rows.push({ node, depth })
if (expanded.has(node.id) && node.children.length > 0) {
walk(node.children, depth + 1)
}
}
}
walk(scopedTree, 0)
return rows
}, [scopedTree, expanded])
// 当前选中部门的人员
const selectedEmployees = useMemo(() => {
if (!data) return []
if (selectedGroup === 'unclassified') return data.unclassified
if (selectedGroup === 'unbound') return []
if (!selectedPath) return []
return data.employees.filter((e) =>
personBelongsToOrganizationPath(e.departmentPaths?.length ? e.departmentPaths : [e.departmentPath], selectedPath),
)
}, [data, selectedPath, selectedGroup])
const employeePaths = (e: EmployeeRow) =>
e.departmentPaths?.length ? e.departmentPaths : e.departmentPath ? [e.departmentPath] : []
const descendantCount = selectedEmployees.filter(
(e) => !employeePaths(e).some((path) => path === selectedPath),
).length
const matchingDepartmentLabel = (e: EmployeeRow) => {
const matching = selectedPath
? employeePaths(e).filter((path) => personBelongsToOrganizationPath([path], selectedPath))
: employeePaths(e)
const names = (matching.length > 0 ? matching : employeePaths(e)).map(
(path) => lastOrganizationPathSegment(path) || path,
)
return names.filter(Boolean).join('、') || '—'
}
// 部门负责人计数(当前选中列表)
const leaderCount = selectedEmployees.filter((e) => e.isLeadership).length
// 等级分布(当前选中列表)
const levelCounts = useMemo(() => {
const map = new Map<string, number>()
for (const e of selectedEmployees) {
if (e.positionLevel) map.set(e.positionLevel, (map.get(e.positionLevel) ?? 0) + 1)
}
return Array.from(map.entries()).sort((a, b) => b[1] - a[1])
}, [selectedEmployees])
// 搜索过滤
const filteredEmployees = useMemo(() => {
if (!keyword.trim()) return selectedEmployees
const k = keyword.trim().toLowerCase()
return selectedEmployees.filter(
(e) =>
(e.displayName ?? '').toLowerCase().includes(k) ||
(e.realName ?? '').toLowerCase().includes(k) ||
(e.jobNumber ?? '').toLowerCase().includes(k) ||
(e.positionTitle ?? '').toLowerCase().includes(k),
)
}, [selectedEmployees, keyword])
const toggleExpand = (id: string) => {
setExpanded((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const selectNode = (node: OrgNode) => {
setSelectedPath(node.path)
setSelectedGroup('org')
if (!expanded.has(node.id) && node.children.length > 0) {
setExpanded((prev) => new Set(prev).add(node.id))
}
}
if (status === 'loading' || (status === 'authenticated' && !canAccess)) {
return <div className="min-h-screen bg-[#fafafa]" />
}
const content = (
<div className={treeAsSecondarySidebar ? 'w-full' : 'mx-auto max-w-[1400px] px-5 py-6'}>
{/* direction: cloud-quarry (seed 32cb33b5) — THESIS: 部门即层叠石料块,人员是块上铭牌 */}
{/* 页头 */}
<div className={`${treeAsSecondarySidebar ? 'border-b border-[#e2e8f0] bg-white px-5 py-5 sm:px-7' : 'mb-5'} flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between`}>
<div>
<h1 className="text-[26px] font-bold text-[#1a1c1d] tracking-tight">{heading}</h1>
<p className="text-[13px] text-[#86898e] mt-1">{description}</p>
</div>
<div className="flex w-full items-center justify-between gap-3 sm:w-auto sm:justify-end">
{data && (
<span className="text-[12px] text-[#86898e] sm:text-right">
共 <b className="text-[#1a1c1d]">{scopedEmployees.length}</b> 人
{isSuperAdmin && (
<span className="ml-2">
(在职 {scopedEmployees.filter((employee) => employee.employmentState !== 'RESIGNED').length} / 离职{' '}
{scopedEmployees.filter((employee) => employee.employmentState === 'RESIGNED').length})
</span>
)}
</span>
)}
<button
type="button"
onClick={() => setIncludeInactive((v) => !v)}
className={`shrink-0 whitespace-nowrap rounded border px-3 py-1.5 text-[12px] transition-colors ${
includeInactive
? 'bg-[#5a66b8] text-white border-[#5a66b8]'
: 'bg-white text-[#5a66b8] border-[#5a66b8] hover:bg-[#eef0fa]'
}`}
>
含离职
</button>
</div>
</div>
{error && (
<div className={`${treeAsSecondarySidebar ? 'mx-5 mt-4 sm:mx-7' : 'mb-4'} rounded-lg border border-[#e8c98a] bg-[#fdf6e9] px-4 py-3 text-[13px] text-[#a0671c]`}>
{error}
</div>
)}
{loading && !data ? (
<div className={`${treeAsSecondarySidebar ? 'm-5 sm:m-7' : ''} rounded-lg border border-[#e7e8ea] bg-white p-10 text-center text-sm text-[#86898e] shadow-sm`}>
加载中…
</div>
) : data ? (
<div className={`grid grid-cols-1 items-start ${treeAsSecondarySidebar ? 'gap-0 lg:grid-cols-[232px_minmax(0,1fr)] lg:items-stretch' : 'gap-5 lg:grid-cols-[300px_1fr]'}`}>
{/* 左侧:组织树 */}
<aside className={treeAsSecondarySidebar
? 'overflow-hidden border-b border-[#e2e8f0] bg-[#f8fafc] lg:min-h-0 lg:border-b-0 lg:border-r'
: 'overflow-hidden rounded-lg border border-[#e7e8ea] bg-white shadow-sm'}
aria-label={treeAsSecondarySidebar ? '人员架构二级导航' : undefined}
>
<div className={`${treeAsSecondarySidebar ? 'border-b border-[#e2e8f0] px-4 py-4' : 'border-b border-[#eef0f3] px-4 py-3'} flex items-center justify-between`}>
<span className={`${treeAsSecondarySidebar ? 'text-sm font-bold text-[#101828]' : 'text-[12px] font-semibold text-[#1a1c1d]'}`}>
{scopeRootName ? `${scopeRootName}组织架构` : '组织架构'}
</span>
<span className={`${treeAsSecondarySidebar ? 'text-xs text-[#66758b]' : 'text-[11px] text-[#86898e]'}`}>{scopedTree.length} 顶层节点</span>
</div>
<nav className={`${treeAsSecondarySidebar ? 'max-h-[42vh] p-2 lg:max-h-[calc(100dvh-188px)] lg:py-4' : 'max-h-[calc(100vh-260px)] py-2'} overflow-y-auto`} aria-label="组织树">
{scopeRootName && scopedTree.length === 0 && (
<p className="px-4 py-8 text-center text-[13px] leading-6 text-[#86898e]">
未找到“{scopeRootName}”组织节点
</p>
)}
{flatTree.map(({ node, depth }) => {
const hasChildren = node.children.length > 0
const isExpanded = expanded.has(node.id)
const isSelected = selectedGroup === 'org' && selectedPath === node.path
return (
<div key={node.id}>
<button
type="button"
onClick={() => selectNode(node)}
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-colors ${
isSelected ? 'bg-[#eef0fa]' : 'hover:bg-[#f4f5f7]'
}`}
style={{ paddingLeft: `${12 + depth * 16}px` }}
>
{hasChildren ? (
<span
role="button"
tabIndex={-1}
aria-label={isExpanded ? '收起' : '展开'}
onClick={(e) => {
e.stopPropagation()
toggleExpand(node.id)
}}
className="shrink-0 cursor-pointer"
>
<svg
className={`w-3 h-3 text-[#86898e] transition-transform ${isExpanded ? 'rotate-90' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</span>
) : (
<span className="w-3 inline-block" aria-hidden="true" />
)}
<span
className={`text-[13px] truncate ${
isSelected ? 'text-[#1a1c1d] font-semibold' : 'text-[#1a1c1d]'
}`}
>
{node.name}
</span>
<span className="ml-auto text-[11px] font-medium text-[#5a66b8] tabular-nums">
{node.count}
</span>
</button>
</div>
)
})}
{/* 特殊组仅属于公司级全量视图。 */}
{!scopeRootName && <div className="border-t border-[#eef0f3] mt-2 pt-1">
<button
type="button"
onClick={() => {
setSelectedGroup('unclassified')
setSelectedPath(null)
}}
className={`w-full flex items-center justify-between px-3 py-2 text-left ${
selectedGroup === 'unclassified' ? 'bg-[#eef0fa]' : 'hover:bg-[#f4f5f7]'
}`}
>
<span className="text-[13px] text-[#a0671c]">未确定</span>
<span className="text-[11px] font-medium text-[#a0671c] tabular-nums">
{data.unclassified.length}
</span>
</button>
{isSuperAdmin && (
<button
type="button"
onClick={() => {
setSelectedGroup('unbound')
setSelectedPath(null)
}}
className={`w-full flex items-center justify-between px-3 py-2 text-left ${
selectedGroup === 'unbound' ? 'bg-[#eef0fa]' : 'hover:bg-[#f4f5f7]'
}`}
>
<span className="text-[13px] text-[#a0671c]">未绑定账号</span>
<span className="text-[11px] font-medium text-[#a0671c] tabular-nums">
{data.unbound.length}
</span>
</button>
)}
</div>}
</nav>
</aside>
{/* 右侧:人员明细 */}
<section className={treeAsSecondarySidebar
? 'min-w-0 overflow-hidden bg-white'
: 'overflow-hidden rounded-lg border border-[#e7e8ea] bg-white shadow-sm'}
>
<div className="px-5 py-4 border-b border-[#eef0f3]">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2 min-w-0">
{selectedGroup === 'org' && selectedPath ? (
<>
<span className="text-[11px] bg-[#5a66b8] text-white rounded px-2 py-0.5 font-semibold whitespace-nowrap">
L{selectedPath.split('/').length}
</span>
<span className="text-[14px] font-semibold text-[#1a1c1d] truncate">
{selectedPath.split('/').pop()}
</span>
</>
) : (
<span className="text-[14px] font-semibold text-[#1a1c1d]">
{selectedGroup === 'unclassified'
? '未确定部门'
: selectedGroup === 'unbound'
? '未绑定账号'
: scopeRootName ?? '请选择组织节点'}
</span>
)}
</div>
<div className="relative shrink-0">
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="搜索姓名 / 工号 / 职位"
className="w-40 md:w-52 text-[13px] px-3 py-1.5 rounded border border-[#d5dae0] bg-[#f4f5f7] focus:outline-none focus:border-[#5a66b8] focus:ring-2 focus:ring-[#eef0fa]"
/>
</div>
</div>
{selectedGroup === 'org' && selectedPath && (
<p className="text-[12px] text-[#86898e] mt-1.5 truncate">
{selectedPath}
{descendantCount > 0 ? ` · 含下级 ${descendantCount} 人` : ''}
</p>
)}
</div>
{/* 统计行 */}
{selectedGroup === 'org' && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 px-5 py-4 border-b border-[#eef0f3] bg-[#f4f5f7]">
<div>
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
{selectedEmployees.filter((e) => e.employmentState !== 'RESIGNED').length}
</div>
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">在职</div>
</div>
<div>
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
{leaderCount}
</div>
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">负责人</div>
</div>
<div>
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
{levelCounts[0]?.[0] ?? '—'}
</div>
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">最常见等级</div>
</div>
<div>
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
{selectedEmployees.filter((e) => e.managerName).length}
</div>
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">有主管记录</div>
</div>
</div>
)}
{/* 人员表 */}
{selectedGroup === 'unbound' ? (
<div className="p-5">
<p className="text-[13px] text-[#86898e] mb-3">
以下账号没有钉钉 ID,无法自动绑定权威人员档案,需人工核对后手动绑定。
</p>
<div className="grid gap-2">
{data.unbound.map((u) => (
<div
key={u.userId}
className="flex items-center justify-between px-4 py-2.5 rounded border border-[#eef0f3] bg-[#f4f5f7]"
>
<div>
<span className="text-[13px] text-[#1a1c1d] font-medium">
{u.realName ?? u.username}
</span>
<span className="text-[12px] text-[#86898e] ml-2">@{u.username}</span>
</div>
<span className="text-[12px] text-[#86898e]">{u.oldDepartment || '无部门'}</span>
</div>
))}
</div>
</div>
) : filteredEmployees.length === 0 ? (
<div className="p-10 text-center text-[#86898e] text-sm">
{scopeRootName && !scopeRootNode
? `未找到“${scopeRootName}”组织节点,请先完成公司组织架构同步。`
: '该部门暂无人员'}
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-[#e7e8ea] bg-[#f4f5f7]">
<th className="px-5 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">人员</th>
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">工号</th>
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">所属部门</th>
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">职位</th>
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">等级</th>
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">直属主管</th>
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">状态</th>
</tr>
</thead>
<tbody>
{filteredEmployees.map((e) => (
<tr key={e.employeeProfileId} className="border-b border-[#eef0f3] hover:bg-[#f4f5f7]">
<td className="px-5 py-3">
<div className="flex items-center gap-2.5">
<span
className={`w-7 h-7 rounded-full text-[12px] font-semibold flex items-center justify-center ${
isManagementLevel(e)
? 'bg-[#fdf6e9] text-[#8a6114]'
: 'bg-[#eef0fa] text-[#1a1c1d]'
}`}
>
{(e.displayName ?? e.realName ?? '?').slice(0, 1)}
</span>
<span
className={`text-[13px] font-medium ${
isManagementLevel(e) ? 'text-[#8a6114]' : 'text-[#1a1c1d]'
}`}
>
{e.displayName ?? e.realName ?? e.username}
</span>
{e.isLeadership && (
<span className="text-[10px] text-[#1c8a5a] bg-[#e8f5ee] rounded px-1.5 py-0.5">
负责人
</span>
)}
{employeePaths(e).length > 1 && (
<span className="text-[10px] text-[#5a66b8] bg-[#eef0fa] rounded px-1.5 py-0.5">
兼任
</span>
)}
</div>
</td>
<td className="px-4 py-3 text-[12px] text-[#86898e] tabular-nums">{e.jobNumber ?? '—'}</td>
<td className="px-4 py-3 text-[12px] text-[#86898e]" title={matchingDepartmentLabel(e)}>
{matchingDepartmentLabel(e)}
</td>
<td className="px-4 py-3 text-[13px] text-[#1a1c1d]">{e.positionTitle ?? '—'}</td>
<td className="px-4 py-3">
{e.positionLevel ? (
<span
className={`text-[11px] font-semibold rounded px-2 py-0.5 ${
e.levelType === 'M'
? 'bg-[#fdf6e9] text-[#a0671c]'
: 'bg-[#eef0fa] text-[#1a1c1d]'
}`}
>
{e.positionLevel}
{e.levelType ? ` · ${LEVEL_TYPE_LABEL[e.levelType] ?? e.levelType}` : ''}
</span>
) : (
<span className="text-[12px] text-[#b0b6be]">未记录</span>
)}
</td>
<td className="px-4 py-3 text-[13px] text-[#1a1c1d]">{e.managerName ?? '—'}</td>
<td className="px-4 py-3">
<span
className={`text-[11px] rounded-full px-2.5 py-0.5 ${
e.employmentState === 'RESIGNED'
? 'text-[#86898e] bg-[#eef0f3]'
: e.employmentState === 'UNKNOWN'
? 'text-[#a0671c] bg-[#fdf6e9]'
: 'text-[#1c8a5a] bg-[#e8f5ee]'
}`}
>
{STATE_LABEL[e.employmentState ?? 'UNKNOWN'] ?? '未知'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</div>
) : null}
</div>
)
if (embedded) {
return <main className="min-h-full bg-[#fafafa]">{content}</main>
}
return (
<BoardShell session={session} active="organization">
{content}
</BoardShell>
)
}