75 lines
3.8 KiB
TypeScript
75 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useLayoutEffect, useRef } from 'react';
|
|
import {
|
|
Avatar, AvatarFallback, AvatarImage, Button, Dialog, DialogBody, DialogClose,
|
|
DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
|
} from '@hm-design/ui';
|
|
import type { OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js';
|
|
|
|
const statusNames = { active: '재직', dispatched: '파견', onLeave: '휴직' } as const;
|
|
|
|
/** Explicit React-only adapter. The Core and default entry stay UI-independent. */
|
|
export default function HMEmployeeDialog({ selection, onClose }: OrgChartEmployeePopupProps) {
|
|
const returnTarget = useRef<OrgChartEmployeeSelection | null>(null);
|
|
const returnRoot = useRef<ShadowRoot | null>(null);
|
|
const returnContainer = useRef<HTMLElement | null>(null);
|
|
useLayoutEffect(() => {
|
|
if (!selection) return;
|
|
returnTarget.current = selection;
|
|
const root = selection.context.trigger.getRootNode();
|
|
const Shadow = selection.context.trigger.ownerDocument.defaultView?.ShadowRoot;
|
|
returnRoot.current = Shadow && root instanceof Shadow ? root : null;
|
|
returnContainer.current = returnRoot.current?.host.parentElement ?? null;
|
|
}, [selection]);
|
|
|
|
function restoreFocus(event: Event) {
|
|
event.preventDefault();
|
|
const previous = returnTarget.current;
|
|
returnTarget.current = null;
|
|
if (!previous) return;
|
|
const { trigger } = previous.context;
|
|
// A mode/selection render can replace the card while the Portal is open.
|
|
// Retain its ShadowRoot before replacement and resolve the same employee.
|
|
const shadow = returnRoot.current;
|
|
const currentRoot = shadow?.host.isConnected ? shadow : returnContainer.current?.firstElementChild?.shadowRoot;
|
|
const replacement = [...currentRoot?.querySelectorAll<HTMLElement>('.hm-orgchart-employee') ?? []]
|
|
.find(card => card.dataset.employeeId === previous.employee.id);
|
|
const target = trigger.isConnected ? trigger : replacement
|
|
?? currentRoot?.querySelector<HTMLElement>('.hm-orgchart-shell');
|
|
target?.focus({ preventScroll: true });
|
|
}
|
|
|
|
const displayed = selection ?? returnTarget.current;
|
|
const employee = displayed?.employee;
|
|
const context = displayed?.context;
|
|
const image = employee?.profileImage
|
|
?? context?.trigger.querySelector<HTMLImageElement>('.hm-orgchart-avatar img')?.src;
|
|
const fields: [string, string | null | undefined][] = employee ? [
|
|
['이름', employee.name], ['직위', employee.position],
|
|
['소속 회사', context?.affiliation?.name], ['소속 조직', context?.organization?.name],
|
|
['직책', employee.jobTitle], ['근무 상태', statusNames[employee.workStatus]],
|
|
] : [];
|
|
|
|
return <Dialog open={selection !== null} onOpenChange={open => { if (!open) onClose(); }}>
|
|
<DialogContent onCloseAutoFocus={restoreFocus} data-component="HMOrgChartEmployeeDialog" data-employee-id={employee?.id}>
|
|
<DialogHeader>
|
|
<DialogTitle>직원 정보</DialogTitle>
|
|
<DialogDescription>조직도에서 선택한 직원의 정보를 확인합니다.</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogBody>
|
|
<Avatar size="lg">
|
|
{image ? <AvatarImage src={image} alt="" referrerPolicy="no-referrer" /> : null}
|
|
<AvatarFallback>{employee?.name.slice(0, 1)}</AvatarFallback>
|
|
</Avatar>
|
|
<dl style={{ display: 'grid', gap: '0.75rem', margin: '1rem 0 0' }}>{fields.filter(([, value]) => Boolean(value)).map(([label, value]) => <div key={label} style={{ display: 'grid', gridTemplateColumns: '5rem minmax(0, 1fr)', gap: '0.75rem' }}>
|
|
<dt style={{ fontWeight: 500 }}>{label}</dt><dd style={{ margin: 0, minWidth: 0, overflowWrap: 'anywhere' }}>{value}</dd>
|
|
</div>)}</dl>
|
|
</DialogBody>
|
|
<DialogFooter>
|
|
<DialogClose asChild><Button variant="cancel" appearance="solid">닫기</Button></DialogClose>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>;
|
|
}
|