241 lines
7.5 KiB
TypeScript
Executable File
241 lines
7.5 KiB
TypeScript
Executable File
'use client';
|
||
|
||
import * as React from 'react';
|
||
import { useState } from 'react';
|
||
import { Button } from '@nice/ui/components/button';
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@nice/ui/components/dialog';
|
||
import { Input } from '@nice/ui/components/input';
|
||
import { Label } from '@nice/ui/components/label';
|
||
import { Textarea } from '@nice/ui/components/textarea';
|
||
import { DeptSelect, SingleDeptSelector, TermSelect } from '@/components/selector';
|
||
import { useTRPC } from '@fenghuo/client';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { TaxonomySlug } from '@fenghuo/common';
|
||
import type { OrganizationDialogState } from './types.js';
|
||
|
||
interface OrganizationDialogProps {
|
||
dialog: OrganizationDialogState;
|
||
onClose: () => void;
|
||
onSave: (data: {
|
||
name: string;
|
||
slug: string;
|
||
description: string;
|
||
parentId?: string;
|
||
organizationTypeId?: string;
|
||
professionIds?: string[];
|
||
}) => void;
|
||
}
|
||
|
||
export function OrganizationDialog({ dialog, onClose, onSave }: OrganizationDialogProps) {
|
||
// 修改:统一使用 organization 字段而不是 department
|
||
const [name, setName] = useState(dialog.organization?.name || '');
|
||
const [slug, setSlug] = useState(dialog.organization?.slug || '');
|
||
const [description, setDescription] = useState(dialog.organization?.description || '');
|
||
const [parentId, setParentId] = useState(dialog.parentId || dialog.organization?.parentId || '');
|
||
const [organizationTypeId, setOrganizationTypeId] = useState('');
|
||
const [professionIds, setProfessionIds] = useState<string[]>([]);
|
||
|
||
const trpc = useTRPC();
|
||
|
||
// 获取组织树数据用于显示父部门名称
|
||
const { data: organizationTree = [] } = useQuery({
|
||
...trpc.organization.getTree.queryOptions({
|
||
includeInactive: false,
|
||
}),
|
||
});
|
||
|
||
// 重置表单数据
|
||
React.useEffect(() => {
|
||
if (dialog.open) {
|
||
// 修改:统一使用 organization 字段
|
||
setName(dialog.organization?.name || '');
|
||
setSlug(dialog.organization?.slug || '');
|
||
setDescription(dialog.organization?.description || '');
|
||
setParentId(dialog.parentId || dialog.organization?.parentId || '');
|
||
|
||
// 从部门的关联术语中提取数据
|
||
if (dialog.organization?.terms) {
|
||
// 过滤组织类型术语
|
||
const orgTypeTerms = dialog.organization.terms.filter(
|
||
(term) => term.taxonomy?.slug === TaxonomySlug.ORGANIZATION_TYPE,
|
||
);
|
||
setOrganizationTypeId(orgTypeTerms[0]?.id || '');
|
||
|
||
// 过滤专业术语
|
||
const professionTerms = dialog.organization.terms.filter(
|
||
(term) => term.taxonomy?.slug === TaxonomySlug.PROFESSION,
|
||
);
|
||
setProfessionIds(professionTerms.map((term) => term.id));
|
||
} else {
|
||
// 重置术语选择器
|
||
setOrganizationTypeId('');
|
||
setProfessionIds([]);
|
||
}
|
||
}
|
||
}, [dialog]);
|
||
|
||
const handleSave = () => {
|
||
if (!name.trim()) return;
|
||
|
||
onSave({
|
||
name: name.trim(),
|
||
slug: slug.trim(),
|
||
description: description.trim(),
|
||
parentId: parentId || undefined,
|
||
organizationTypeId: organizationTypeId || undefined,
|
||
professionIds: professionIds.length > 0 ? professionIds : undefined,
|
||
});
|
||
onClose();
|
||
};
|
||
|
||
const getDialogTitle = () => {
|
||
switch (dialog.mode) {
|
||
case 'edit':
|
||
return '编辑部门';
|
||
case 'addChild':
|
||
return '添加下级部门';
|
||
case 'add':
|
||
default:
|
||
return '新增部门';
|
||
}
|
||
};
|
||
|
||
// 获取需要排除的部门ID(编辑模式下排除自身及其子部门)
|
||
const getExcludeIds = React.useMemo(() => {
|
||
if (dialog.mode === 'edit' && dialog.organization) {
|
||
// 递归收集所有子部门ID
|
||
const collectChildIds = (orgs: any[], parentId: string): string[] => {
|
||
const childIds: string[] = [];
|
||
orgs.forEach((org) => {
|
||
if (org.parentId === parentId) {
|
||
childIds.push(org.id);
|
||
// 递归收集子部门的子部门
|
||
if (org.children) {
|
||
childIds.push(...collectChildIds(org.children, org.id));
|
||
} else {
|
||
// 如果没有children,从扁平化结构中查找
|
||
childIds.push(...collectChildIds(orgs, org.id));
|
||
}
|
||
}
|
||
});
|
||
return childIds;
|
||
};
|
||
|
||
// 扁平化组织树
|
||
const flattenOrgs = (orgs: any[]): any[] => {
|
||
return orgs.reduce((acc, org) => {
|
||
acc.push(org);
|
||
if (org.children) {
|
||
acc.push(...flattenOrgs(org.children));
|
||
}
|
||
return acc;
|
||
}, []);
|
||
};
|
||
|
||
const flatOrgs = flattenOrgs(organizationTree);
|
||
const excludeIds = [dialog.organization.id, ...collectChildIds(flatOrgs, dialog.organization.id)];
|
||
return excludeIds;
|
||
}
|
||
return [];
|
||
}, [dialog.mode, dialog.organization, organizationTree]);
|
||
|
||
return (
|
||
<Dialog open={dialog.open} onOpenChange={(open) => !open && onClose()}>
|
||
<DialogContent className="sm:max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle>{getDialogTitle()}</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="grid gap-4 py-4">
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dept-name" className="text-sm font-medium">
|
||
部门名称 *
|
||
</Label>
|
||
<Input
|
||
id="dept-name"
|
||
placeholder="请输入部门名称"
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
required
|
||
className="focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dept-slug" className="text-sm font-medium">
|
||
别名
|
||
</Label>
|
||
<Input
|
||
id="dept-slug"
|
||
placeholder="输入URL友好的别名"
|
||
value={slug}
|
||
onChange={(e) => setSlug(e.target.value)}
|
||
className="focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dept-description" className="text-sm font-medium">
|
||
部门描述
|
||
</Label>
|
||
<Textarea
|
||
id="dept-description"
|
||
placeholder="请输入部门描述(可选)"
|
||
value={description}
|
||
onChange={(e) => setDescription(e.target.value)}
|
||
rows={3}
|
||
className="focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dept-organization-type" className="text-sm font-medium">
|
||
组织类型
|
||
</Label>
|
||
<TermSelect
|
||
taxonomySlug={TaxonomySlug.ORGANIZATION_TYPE}
|
||
value={organizationTypeId}
|
||
onValueChange={(value) => setOrganizationTypeId(typeof value === 'string' ? value : '')}
|
||
placeholder="选择组织类型(可选)"
|
||
className="w-full border border-[#EBEFF5]"
|
||
allowClear={true}
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dept-profession" className="text-sm font-medium">
|
||
专业
|
||
</Label>
|
||
<TermSelect
|
||
taxonomySlug={TaxonomySlug.PROFESSION}
|
||
value={professionIds}
|
||
onValueChange={(value) => setProfessionIds(Array.isArray(value) ? value : [])}
|
||
placeholder="选择专业(可选)"
|
||
className="w-full border border-[#EBEFF5]"
|
||
allowClear={true}
|
||
multiple={true}
|
||
maxSelections={5}
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dept-parent" className="text-sm font-medium">
|
||
父部门
|
||
</Label>
|
||
<SingleDeptSelector
|
||
value={parentId}
|
||
onValueChange={setParentId}
|
||
placeholder="选择父部门(可选)"
|
||
className="w-full border border-[#EBEFF5]"
|
||
allowClear={true}
|
||
excludeIds={getExcludeIds}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-end gap-3 pt-4">
|
||
<Button variant="outline" onClick={onClose}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleSave} disabled={!name.trim()} className="min-w-[80px]">
|
||
保存
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|