casualroom/apps/fenghuo/web/components/organization/organization-dialog.tsx

241 lines
7.5 KiB
TypeScript
Raw Normal View History

2025-07-28 07:50:50 +08:00
'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>
);
}