fix: 组织架构根部门自动创建为公司名
1. 注册时自动创建根部门(公司名,level=0),作为组织架构顶层节点 2. 部门列表查询时兼容历史组织:若无任何部门,自动补建根部门 3. 创建部门时若未指定父部门,默认挂到根部门下 4. 前端 OrgChart: - 根部门显示"公司"标签,不可编辑/删除 - 新增部门时默认父部门为根部门 - 上级部门下拉移除"无(根部门)"选项,改为列出所有可选部门 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,24 @@ const updateDeptSchema = createDeptSchema.partial()
|
|||||||
/** 获取部门树 */
|
/** 获取部门树 */
|
||||||
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
// 兼容历史组织:若无任何部门,自动创建根部门(公司名)
|
||||||
|
const existing = await prisma.department.findFirst({ where: { orgId: req.user!.orgId! } })
|
||||||
|
if (!existing) {
|
||||||
|
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId! } })
|
||||||
|
if (org) {
|
||||||
|
await prisma.department.create({
|
||||||
|
data: {
|
||||||
|
orgId: org.id,
|
||||||
|
name: org.name,
|
||||||
|
parentId: null,
|
||||||
|
level: 0,
|
||||||
|
sortOrder: 0,
|
||||||
|
description: '组织根节点',
|
||||||
|
createdBy: req.user!.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
const departments = await prisma.department.findMany({
|
const departments = await prisma.department.findMany({
|
||||||
where: { orgId: req.user!.orgId! },
|
where: { orgId: req.user!.orgId! },
|
||||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||||
@@ -37,7 +55,12 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const data = createDeptSchema.parse(req.body)
|
const data = createDeptSchema.parse(req.body)
|
||||||
// 空字符串视为无父部门,统一转为 null
|
// 空字符串视为无父部门,统一转为 null
|
||||||
const parentId = data.parentId && data.parentId.trim() !== '' ? data.parentId : null
|
let parentId = data.parentId && data.parentId.trim() !== '' ? data.parentId : null
|
||||||
|
// 若未指定父部门,默认挂到根部门(level=0)下
|
||||||
|
if (!parentId) {
|
||||||
|
const root = await prisma.department.findFirst({ where: { orgId: req.user!.orgId!, parentId: null }, orderBy: { sortOrder: 'asc' } })
|
||||||
|
if (root) parentId = root.id
|
||||||
|
}
|
||||||
let level = 0
|
let level = 0
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
const parent = await prisma.department.findFirst({ where: { id: parentId, orgId: req.user!.orgId! } })
|
const parent = await prisma.department.findFirst({ where: { id: parentId, orgId: req.user!.orgId! } })
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ export async function register(orgName: string, phone: string, password: string)
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 自动创建根部门(公司名),作为组织架构的顶层节点
|
||||||
|
await prisma.department.create({
|
||||||
|
data: {
|
||||||
|
orgId: org.id,
|
||||||
|
name: orgName,
|
||||||
|
parentId: null,
|
||||||
|
level: 0,
|
||||||
|
sortOrder: 0,
|
||||||
|
description: '组织根节点',
|
||||||
|
createdBy: user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { lastLoginAt: new Date() },
|
data: { lastLoginAt: new Date() },
|
||||||
|
|||||||
@@ -61,10 +61,13 @@ export default function OrgChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderTree = (items: any[], parentId: string | null, level: number = 0) => {
|
const renderTree = (items: any[], parentId: string | null, level: number = 0) => {
|
||||||
return items.filter(d => d.parentId === parentId).map(d => (
|
return items.filter(d => d.parentId === parentId).map(d => {
|
||||||
|
// 根部门(level=0 且无父部门)为公司节点,不可编辑/删除
|
||||||
|
const isRoot = d.level === 0 && !d.parentId
|
||||||
|
return (
|
||||||
<div key={d.id}>
|
<div key={d.id}>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 py-2 px-3 hover:bg-gray-50 rounded-md"
|
className={`flex items-center gap-2 py-2 px-3 rounded-md ${isRoot ? 'bg-primary/5 font-medium' : 'hover:bg-gray-50'}`}
|
||||||
style={{ marginLeft: level * 24 }}
|
style={{ marginLeft: level * 24 }}
|
||||||
>
|
>
|
||||||
{items.some(c => c.parentId === d.id) ? (
|
{items.some(c => c.parentId === d.id) ? (
|
||||||
@@ -74,29 +77,35 @@ export default function OrgChart() {
|
|||||||
) : (
|
) : (
|
||||||
<span className="w-4" />
|
<span className="w-4" />
|
||||||
)}
|
)}
|
||||||
<Building2 className="w-4 h-4 text-primary" />
|
<Building2 className={`w-4 h-4 ${isRoot ? 'text-primary' : 'text-gray-400'}`} />
|
||||||
<span className="flex-1 text-sm">{d.name}</span>
|
<span className="flex-1 text-sm">{d.name}</span>
|
||||||
|
{isRoot && <span className="text-xs text-primary">公司</span>}
|
||||||
<span className="text-xs text-gray-400">{d._count?.employees || 0}人</span>
|
<span className="text-xs text-gray-400">{d._count?.employees || 0}人</span>
|
||||||
<button
|
{!isRoot && (
|
||||||
onClick={() => {
|
<>
|
||||||
setEditing(d)
|
<button
|
||||||
setForm({ name: d.name, parentId: d.parentId || '', sortOrder: d.sortOrder, description: d.description || '' })
|
onClick={() => {
|
||||||
setShowModal(true)
|
setEditing(d)
|
||||||
}}
|
setForm({ name: d.name, parentId: d.parentId || '', sortOrder: d.sortOrder, description: d.description || '' })
|
||||||
className="text-gray-400 hover:text-primary"
|
setShowModal(true)
|
||||||
>
|
}}
|
||||||
<Edit className="w-3.5 h-3.5" />
|
className="text-gray-400 hover:text-primary"
|
||||||
</button>
|
>
|
||||||
<button
|
<Edit className="w-3.5 h-3.5" />
|
||||||
onClick={() => { if (confirm(`确认删除部门"${d.name}"?`)) deleteMutation.mutate(d.id) }}
|
</button>
|
||||||
className="text-gray-400 hover:text-danger"
|
<button
|
||||||
>
|
onClick={() => { if (confirm(`确认删除部门"${d.name}"?`)) deleteMutation.mutate(d.id) }}
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
className="text-gray-400 hover:text-danger"
|
||||||
</button>
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{expanded.has(d.id) && renderTree(items, d.id, level + 1)}
|
{expanded.has(d.id) && renderTree(items, d.id, level + 1)}
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,7 +139,9 @@ export default function OrgChart() {
|
|||||||
<span className="text-sm font-medium">部门列表</span>
|
<span className="text-sm font-medium">部门列表</span>
|
||||||
<Button size="sm" onClick={() => {
|
<Button size="sm" onClick={() => {
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
setForm({ name: '', parentId: '', sortOrder: 0, description: '' })
|
// 默认父部门为根部门(level=0 且 parentId=null)
|
||||||
|
const root = departments.find((d: any) => d.level === 0 && !d.parentId)
|
||||||
|
setForm({ name: '', parentId: root?.id || '', sortOrder: 0, description: '' })
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
}}>
|
}}>
|
||||||
<Plus className="w-4 h-4 mr-1" />新增部门
|
<Plus className="w-4 h-4 mr-1" />新增部门
|
||||||
@@ -158,9 +169,10 @@ export default function OrgChart() {
|
|||||||
<div>
|
<div>
|
||||||
<Label>上级部门</Label>
|
<Label>上级部门</Label>
|
||||||
<Select value={form.parentId} onChange={(e) => setForm({ ...form, parentId: e.target.value })}>
|
<Select value={form.parentId} onChange={(e) => setForm({ ...form, parentId: e.target.value })}>
|
||||||
<option value="">无(根部门)</option>
|
|
||||||
{departments.filter((d: any) => d.id !== editing?.id).map((d: any) => (
|
{departments.filter((d: any) => d.id !== editing?.id).map((d: any) => (
|
||||||
<option key={d.id} value={d.id}>{d.name}</option>
|
<option key={d.id} value={d.id}>
|
||||||
|
{d.level === 0 && !d.parentId ? `${d.name}(公司)` : d.name}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user