merge: 收口用户匹配与花名册同步修复
This commit is contained in:
commit
12d92a1b4b
@ -31,6 +31,7 @@
|
||||
|
||||
- `main` 是最终发布主线,由 `WuYongJie` 收口;日常开发默认同步 `origin/codex/xuxiake-mainline`。
|
||||
- 这份文档现在只固定 Git 协作方式、主线收口规则和共享接缝处理原则,不再把当前模块职责长期写死在这里。
|
||||
- 生产部署入口以 `docs/workflow/生产部署入口说明.md` 和 `scripts/deploy-yydd-prod-from-git.ps1` 为准;`192.168.58.20` 不是当前项目生产部署目标。
|
||||
|
||||
### 当前主线入口
|
||||
|
||||
|
||||
37
furniture-3d-system/docs/workflow/生产部署入口说明.md
Normal file
37
furniture-3d-system/docs/workflow/生产部署入口说明.md
Normal file
@ -0,0 +1,37 @@
|
||||
# 生产部署入口说明
|
||||
|
||||
适用范围:`furniture-3d-system` 当前生产部署入口核对。
|
||||
|
||||
这份文档只记录非敏感入口与核查口径,不保存密码、密钥、数据库连接串或对象存储密钥。
|
||||
|
||||
## 权威生产目标
|
||||
|
||||
- SSH 入口:`root@ltai5tdb2ogyj59fquyedh.yydd.ai`
|
||||
- SSH 端口:`60022`
|
||||
- 生产部署目录:`/opt/furniture-3d-system-prod`
|
||||
- PM2 进程名:`furniture-3d-system-prod`
|
||||
- 应用本机端口:`3001`
|
||||
- 默认部署来源:`origin/main`
|
||||
|
||||
## 本仓库部署脚本
|
||||
|
||||
- `scripts/deploy-yydd-prod-from-git.ps1`
|
||||
- `scripts/create-yydd-prod-deploy.ps1`
|
||||
|
||||
执行部署或生产核查前,优先读取上面两个脚本的默认参数,并在远端实时确认当前 PM2 进程、`current` 软链接和健康检查结果。
|
||||
|
||||
## 明确排除
|
||||
|
||||
- `192.168.58.20` 不是当前 `furniture-3d-system` 的生产部署目标。
|
||||
- 本机泛别名 `prod` 不能作为当前项目生产服务器判断依据。
|
||||
- 旧 `gongxiangp2 / youyidian-3d-material` 的生产或 FreeNAS 信息不能直接套用到当前项目。
|
||||
|
||||
## 最小核查口径
|
||||
|
||||
生产相关动作开始前,至少确认:
|
||||
|
||||
1. 目标主机是 `ltai5tdb2ogyj59fquyedh.yydd.ai:60022`。
|
||||
2. 远端存在 `/opt/furniture-3d-system-prod`。
|
||||
3. PM2 中存在 `furniture-3d-system-prod`。
|
||||
4. 应用监听端口是 `3001`。
|
||||
5. 不把密码、密钥或连接串写入 Git、文档或长期记忆。
|
||||
@ -25,6 +25,7 @@ import { DeleteConfirmDialog } from '@/components/ui/DeleteConfirmDialog'
|
||||
import { ActivateUserModal } from '@/components/users/ActivateUserModal'
|
||||
import { CreateUserModal } from '@/components/users/CreateUserModal'
|
||||
import { EditUserModal } from '@/components/users/EditUserModal'
|
||||
import { EmployeeMatchModal } from '@/components/users/EmployeeMatchModal'
|
||||
import { HandoverModal } from '@/components/users/HandoverModal'
|
||||
import { OrganizationManageModal } from '@/components/users/OrganizationManageModal'
|
||||
import { DepartmentAccessTree, type UserTreeItem } from '@/components/users/DepartmentAccessTree'
|
||||
@ -38,6 +39,7 @@ import {
|
||||
import { canAssignRoleByHierarchy, getAssignableRolesForRole } from '@/lib/role-scope'
|
||||
import { BUSINESS_ACCOUNT_TYPE_LABELS, BUSINESS_ACCOUNT_TYPE_OPTIONS } from '@/lib/business-account-types'
|
||||
import { useDebounce } from '@/hooks/usePerformance'
|
||||
import { getApiErrorMessage } from '@/lib/utils'
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ value: '', label: '所有账号权限', icon: Users },
|
||||
@ -96,6 +98,9 @@ function UsersPageContent() {
|
||||
const [editingUser, setEditingUser] = useState<UserTreeItem | null>(null)
|
||||
const [activationUser, setActivationUser] = useState<UserTreeItem | null>(null)
|
||||
const [handoverUser, setHandoverUser] = useState<UserTreeItem | null>(null)
|
||||
const [employeeMatchUser, setEmployeeMatchUser] = useState<UserTreeItem | null>(null)
|
||||
const [employeeMatchLoading, setEmployeeMatchLoading] = useState(false)
|
||||
const [employeeMatchError, setEmployeeMatchError] = useState<string | null>(null)
|
||||
const [deleteUserId, setDeleteUserId] = useState<string | null>(null)
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
|
||||
@ -156,6 +161,11 @@ function UsersPageContent() {
|
||||
const closeEditModal = useCallback(() => setEditingUser(null), [])
|
||||
const closeActivateModal = useCallback(() => setActivationUser(null), [])
|
||||
const closeHandoverModal = useCallback(() => setHandoverUser(null), [])
|
||||
const closeEmployeeMatchModal = useCallback(() => {
|
||||
if (employeeMatchLoading) return
|
||||
setEmployeeMatchUser(null)
|
||||
setEmployeeMatchError(null)
|
||||
}, [employeeMatchLoading])
|
||||
const closeDeleteDialog = useCallback(() => setDeleteUserId(null), [])
|
||||
|
||||
const handleCreateSuccess = useCallback(() => {
|
||||
@ -245,28 +255,47 @@ function UsersPageContent() {
|
||||
)
|
||||
|
||||
const handleEmployeeMatch = useCallback(
|
||||
async (user: UserTreeItem) => {
|
||||
const jobNumber = window.prompt(`请输入 ${user.name} 的工号`)
|
||||
if (!jobNumber?.trim()) return
|
||||
(user: UserTreeItem) => {
|
||||
setActionError(null)
|
||||
setEmployeeMatchError(null)
|
||||
setSelectedUser(user)
|
||||
setEmployeeMatchUser(user)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const confirmEmployeeMatch = useCallback(
|
||||
async (jobNumber: string) => {
|
||||
if (!employeeMatchUser) return
|
||||
|
||||
try {
|
||||
setEmployeeMatchLoading(true)
|
||||
setEmployeeMatchError(null)
|
||||
setActionError(null)
|
||||
const response = await fetch(`/api/users/${user.id}/employee-match`, {
|
||||
const response = await fetch(`/api/users/${employeeMatchUser.id}/employee-match`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ jobNumber: jobNumber.trim() }),
|
||||
body: JSON.stringify({ jobNumber }),
|
||||
})
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error?.message || '员工库匹配失败')
|
||||
throw new Error(getApiErrorMessage(payload.error, '员工库匹配失败'))
|
||||
}
|
||||
|
||||
const matchedUserId = employeeMatchUser.id
|
||||
setEmployeeMatchUser(null)
|
||||
triggerRefresh()
|
||||
if (selectedUser?.id === matchedUserId) {
|
||||
await refreshSelectedUserDetail(matchedUserId)
|
||||
}
|
||||
} catch (error) {
|
||||
setActionError(error instanceof Error ? error.message : '员工库匹配失败')
|
||||
setEmployeeMatchError(error instanceof Error ? error.message : '员工库匹配失败')
|
||||
} finally {
|
||||
setEmployeeMatchLoading(false)
|
||||
}
|
||||
},
|
||||
[triggerRefresh],
|
||||
[employeeMatchUser, refreshSelectedUserDetail, selectedUser?.id, triggerRefresh],
|
||||
)
|
||||
|
||||
const selectedUserAllowedRoles = useMemo<Role[]>(() => {
|
||||
@ -467,6 +496,14 @@ function UsersPageContent() {
|
||||
{handoverUser ? (
|
||||
<HandoverModal isOpen={!!handoverUser} user={handoverUser} onClose={closeHandoverModal} onSuccess={handleHandoverSuccess} />
|
||||
) : null}
|
||||
<EmployeeMatchModal
|
||||
isOpen={!!employeeMatchUser}
|
||||
user={employeeMatchUser}
|
||||
loading={employeeMatchLoading}
|
||||
error={employeeMatchError}
|
||||
onClose={closeEmployeeMatchModal}
|
||||
onConfirm={confirmEmployeeMatch}
|
||||
/>
|
||||
{editingUser ? (
|
||||
<EditUserModal
|
||||
isOpen={!!editingUser}
|
||||
|
||||
179
furniture-3d-system/src/components/users/EmployeeMatchModal.tsx
Normal file
179
furniture-3d-system/src/components/users/EmployeeMatchModal.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { AlertCircle, BadgeCheck, Link2 } from 'lucide-react';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
|
||||
interface EmployeeMatchUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
employeeDirectoryId?: string | null;
|
||||
employeeDirectory?: {
|
||||
jobNumber: string | null;
|
||||
name: string | null;
|
||||
title: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface EmployeeMatchModalProps {
|
||||
isOpen: boolean;
|
||||
user: EmployeeMatchUser | null;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
onClose: () => void;
|
||||
onConfirm: (jobNumber: string) => Promise<void> | void;
|
||||
}
|
||||
|
||||
const formFieldClass =
|
||||
'space-y-1.5 [&>label]:mb-0 [&>label]:text-[12px] [&>label]:font-semibold [&>label]:leading-5 [&>label]:tracking-normal [&>label]:text-[var(--obs-shell-muted)]';
|
||||
|
||||
const formControlClass =
|
||||
'h-10 rounded-[6px] border border-[var(--obs-shell-border)] bg-white px-3 py-0 text-[13px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-title)] placeholder:text-[var(--obs-shell-subtle)] focus:border focus:border-[var(--obs-shell-title)] focus:px-3 focus:py-0';
|
||||
|
||||
export function EmployeeMatchModal({
|
||||
isOpen,
|
||||
user,
|
||||
loading = false,
|
||||
error,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: EmployeeMatchModalProps) {
|
||||
const title = user?.employeeDirectoryId ? '重新匹配员工库' : '匹配员工库';
|
||||
const employeeDirectory = user?.employeeDirectory;
|
||||
const matchedSummaryParts = [
|
||||
employeeDirectory?.jobNumber,
|
||||
employeeDirectory?.name,
|
||||
employeeDirectory?.title,
|
||||
].filter(Boolean);
|
||||
const matchedSummary = matchedSummaryParts.length > 0 ? matchedSummaryParts.join(' / ') : null;
|
||||
const formKey = `${isOpen ? 'open' : 'closed'}-${user?.id ?? 'none'}-${employeeDirectory?.jobNumber ?? ''}`;
|
||||
|
||||
const footer = (
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="gray"
|
||||
size="regular"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
className="flex-1 rounded-md"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="employee-match-form"
|
||||
variant="filled"
|
||||
size="regular"
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
className="flex-1 rounded-md"
|
||||
>
|
||||
<Link2 className="h-4 w-4" />
|
||||
{user?.employeeDirectoryId ? '重新匹配' : '确认匹配'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={loading ? () => {} : onClose}
|
||||
title={title}
|
||||
size="small"
|
||||
showCloseButton={!loading}
|
||||
closeOnOverlayClick={!loading}
|
||||
footer={footer}
|
||||
>
|
||||
<EmployeeMatchForm
|
||||
key={formKey}
|
||||
user={user}
|
||||
loading={loading}
|
||||
error={error}
|
||||
matchedSummary={matchedSummary}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function EmployeeMatchForm({
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
matchedSummary,
|
||||
onConfirm,
|
||||
}: {
|
||||
user: EmployeeMatchUser | null;
|
||||
loading: boolean;
|
||||
error?: string | null;
|
||||
matchedSummary: string | null;
|
||||
onConfirm: (jobNumber: string) => Promise<void> | void;
|
||||
}) {
|
||||
const [jobNumber, setJobNumber] = useState(user?.employeeDirectory?.jobNumber ?? '');
|
||||
const [fieldError, setFieldError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const normalizedJobNumber = jobNumber.trim();
|
||||
if (!normalizedJobNumber) {
|
||||
setFieldError('请输入员工工号');
|
||||
return;
|
||||
}
|
||||
|
||||
setFieldError(null);
|
||||
await onConfirm(normalizedJobNumber);
|
||||
};
|
||||
|
||||
return (
|
||||
<form id="employee-match-form" onSubmit={handleSubmit} className="space-y-5 p-1">
|
||||
<section className="rounded-md border border-[var(--obs-shell-border)] bg-[var(--obs-shell-content-bg)] p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<BadgeCheck className="h-4 w-4 text-[var(--obs-shell-control-text)]" />
|
||||
<h3 className="text-[12px] font-black text-[var(--obs-shell-title)]">账号与员工库绑定</h3>
|
||||
</div>
|
||||
<p className="mt-3 text-[12px] font-medium leading-5 text-[var(--obs-shell-muted)]">
|
||||
将系统账号与 HR 员工库工号绑定后,组织、权限和钉钉资料会以员工库为核对依据。
|
||||
</p>
|
||||
{user ? (
|
||||
<div className="mt-3 rounded-[5px] bg-white px-3 py-2 text-[12px] font-semibold text-[var(--obs-shell-title)]">
|
||||
<span>{user.name}</span>
|
||||
<span className="mx-2 text-[var(--obs-shell-subtle)]">/</span>
|
||||
<span className="text-[var(--obs-shell-muted)]">{user.email}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{matchedSummary ? (
|
||||
<p className="mt-2 text-[11px] font-semibold text-[var(--obs-shell-success)]">
|
||||
当前匹配:{matchedSummary}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<Input
|
||||
label="员工工号"
|
||||
value={jobNumber}
|
||||
onChange={(event) => {
|
||||
setJobNumber(event.target.value);
|
||||
setFieldError(null);
|
||||
}}
|
||||
placeholder="请输入员工库工号"
|
||||
required
|
||||
autoComplete="off"
|
||||
disabled={loading}
|
||||
errorText={fieldError ?? undefined}
|
||||
containerClassName={formFieldClass}
|
||||
className={formControlClass}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<div className="flex items-start gap-3 rounded-md border border-[var(--obs-shell-danger-border)] bg-[var(--obs-shell-danger-bg)] px-4 py-3">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-[var(--obs-shell-danger)]" />
|
||||
<p className="text-[12px] font-semibold leading-5 text-[var(--obs-shell-danger)]">{error}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { EmployeeMatchModal } from '../EmployeeMatchModal';
|
||||
|
||||
const baseUser = {
|
||||
id: 'user-1',
|
||||
name: '吴用杰',
|
||||
email: 'wuyongjie@example.com',
|
||||
employeeDirectoryId: null,
|
||||
employeeDirectory: null,
|
||||
};
|
||||
|
||||
describe('EmployeeMatchModal', () => {
|
||||
it('使用统一 White Obsidian 弹窗外壳并提交工号', async () => {
|
||||
const actor = userEvent.setup();
|
||||
const handleConfirm = jest.fn();
|
||||
|
||||
render(
|
||||
<EmployeeMatchModal
|
||||
isOpen
|
||||
user={baseUser}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={handleConfirm}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: '匹配员工库' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('obsidian-modal-panel')).toBeInTheDocument();
|
||||
|
||||
const jobNumberInput = screen.getByLabelText(/员工工号/i);
|
||||
fireEvent.change(jobNumberInput, { target: { value: 'N05' } });
|
||||
await waitFor(() => expect(jobNumberInput).toHaveValue('N05'));
|
||||
await actor.click(screen.getByRole('button', { name: /确认匹配/i }));
|
||||
|
||||
expect(handleConfirm).toHaveBeenCalledWith('N05');
|
||||
});
|
||||
|
||||
it('重匹配时带出当前员工库工号', async () => {
|
||||
render(
|
||||
<EmployeeMatchModal
|
||||
isOpen
|
||||
user={{
|
||||
...baseUser,
|
||||
employeeDirectoryId: 'employee-1',
|
||||
employeeDirectory: {
|
||||
jobNumber: 'A1024',
|
||||
name: '吴用杰',
|
||||
title: '设计主管',
|
||||
},
|
||||
}}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: '重新匹配员工库' })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/员工工号/i)).toHaveValue('A1024');
|
||||
expect(screen.getByText('当前匹配:A1024 / 吴用杰 / 设计主管')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,107 @@
|
||||
jest.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
department: {
|
||||
findFirst: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
},
|
||||
employeeDirectory: {
|
||||
findFirst: jest.fn(),
|
||||
update: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { employeeDirectoryRepository } from '../employee-directory.repository'
|
||||
|
||||
const mockPrisma = prisma as unknown as {
|
||||
department: {
|
||||
findFirst: jest.Mock
|
||||
upsert: jest.Mock
|
||||
}
|
||||
employeeDirectory: {
|
||||
findFirst: jest.Mock
|
||||
update: jest.Mock
|
||||
create: jest.Mock
|
||||
}
|
||||
}
|
||||
|
||||
describe('employeeDirectoryRepository.upsertMany', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPrisma.employeeDirectory.findFirst.mockResolvedValue(null)
|
||||
mockPrisma.employeeDirectory.update.mockResolvedValue({})
|
||||
mockPrisma.employeeDirectory.create.mockResolvedValue({})
|
||||
})
|
||||
|
||||
it('同一员工同时出现在在职和离职数据时,以在职数据为准', async () => {
|
||||
await employeeDirectoryRepository.upsertMany({
|
||||
departments: [],
|
||||
employees: [
|
||||
{
|
||||
name: '希文',
|
||||
jobNumber: 'JS26030202',
|
||||
dingtalkUserId: '17724155651225136',
|
||||
title: '空间设计师',
|
||||
},
|
||||
],
|
||||
dimissionEntries: [
|
||||
{
|
||||
jobNumber: 'js26030202',
|
||||
rawSnapshot: {
|
||||
status: 2,
|
||||
pre_status: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(mockPrisma.employeeDirectory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
name: '希文',
|
||||
jobNumber: 'JS26030202',
|
||||
sourceStatus: 'EMPLOYED',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(mockPrisma.employeeDirectory.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
sourceStatus: 'DIMISSIONED',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('只存在离职数据时仍会标记匹配员工为离职', async () => {
|
||||
mockPrisma.employeeDirectory.findFirst.mockResolvedValueOnce({
|
||||
id: 'employee-1',
|
||||
user: null,
|
||||
})
|
||||
|
||||
await employeeDirectoryRepository.upsertMany({
|
||||
departments: [],
|
||||
employees: [],
|
||||
dimissionEntries: [
|
||||
{
|
||||
jobNumber: 'JS26030202',
|
||||
rawSnapshot: {
|
||||
status: 2,
|
||||
pre_status: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(mockPrisma.employeeDirectory.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'employee-1' },
|
||||
data: expect.objectContaining({
|
||||
sourceStatus: 'DIMISSIONED',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -36,6 +36,29 @@ export type EmployeeDirectoryWithUser = Prisma.EmployeeDirectoryGetPayload<{
|
||||
}
|
||||
}>
|
||||
|
||||
type EmployeeIdentityField = 'dingtalkUserId' | 'dingtalkUnionId' | 'mobile' | 'jobNumber'
|
||||
|
||||
function normalizeEmployeeIdentity(value: string | null | undefined): string | null {
|
||||
const text = String(value || '').trim()
|
||||
return text ? text.toLowerCase() : null
|
||||
}
|
||||
|
||||
function buildEmployeeIdentityKey(field: EmployeeIdentityField, value: string | null | undefined): string | null {
|
||||
const normalized = normalizeEmployeeIdentity(value)
|
||||
return normalized ? `${field}:${normalized}` : null
|
||||
}
|
||||
|
||||
function addEmployeeIdentityKey(
|
||||
keys: Set<string>,
|
||||
field: EmployeeIdentityField,
|
||||
value: string | null | undefined
|
||||
) {
|
||||
const key = buildEmployeeIdentityKey(field, value)
|
||||
if (key) {
|
||||
keys.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
class EmployeeDirectoryRepository {
|
||||
private async resolveUniqueDepartmentName(
|
||||
db: Pick<Prisma.TransactionClient, 'department'>,
|
||||
@ -217,6 +240,14 @@ class EmployeeDirectoryRepository {
|
||||
}) {
|
||||
const now = new Date()
|
||||
const departmentMap = new Map<string, { id: string; fullPath: string }>()
|
||||
const activeEmployeeIdentityKeys = new Set<string>()
|
||||
|
||||
for (const employee of input.employees) {
|
||||
addEmployeeIdentityKey(activeEmployeeIdentityKeys, 'dingtalkUserId', employee.dingtalkUserId)
|
||||
addEmployeeIdentityKey(activeEmployeeIdentityKeys, 'dingtalkUnionId', employee.dingtalkUnionId)
|
||||
addEmployeeIdentityKey(activeEmployeeIdentityKeys, 'mobile', employee.mobile)
|
||||
addEmployeeIdentityKey(activeEmployeeIdentityKeys, 'jobNumber', employee.jobNumber)
|
||||
}
|
||||
|
||||
const sortedDepartments = [...input.departments].sort((a, b) => {
|
||||
const aDepth = a.parentExternalId ? 1 : 0
|
||||
@ -337,6 +368,17 @@ class EmployeeDirectoryRepository {
|
||||
}
|
||||
|
||||
for (const dimission of input.dimissionEntries) {
|
||||
const activeIdentityMatched = [
|
||||
buildEmployeeIdentityKey('dingtalkUserId', dimission.dingtalkUserId),
|
||||
buildEmployeeIdentityKey('dingtalkUnionId', dimission.dingtalkUnionId),
|
||||
buildEmployeeIdentityKey('mobile', dimission.mobile),
|
||||
buildEmployeeIdentityKey('jobNumber', dimission.jobNumber),
|
||||
].some((key) => key !== null && activeEmployeeIdentityKeys.has(key))
|
||||
|
||||
if (activeIdentityMatched) {
|
||||
continue
|
||||
}
|
||||
|
||||
const dimissionConditions = [
|
||||
dimission.dingtalkUserId ? { dingtalkUserId: dimission.dingtalkUserId } : undefined,
|
||||
dimission.dingtalkUnionId ? { dingtalkUnionId: dimission.dingtalkUnionId } : undefined,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user