241 lines
7.5 KiB
TypeScript
241 lines
7.5 KiB
TypeScript
|
|
'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>
|
|||
|
|
);
|
|||
|
|
}
|