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

241 lines
7.5 KiB
TypeScript
Executable File
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.

'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>
);
}