'use client' import { useEffect, useState } from 'react' import { useSession } from 'next-auth/react' import { useRouter, useParams } from 'next/navigation' import EmailAccountsTab from '@/components/employees/email-accounts-tab' import PermissionsTab from '@/components/employees/permissions-tab' interface DepartmentMembership { id: number department_id: number department_name: string department_code: string department_depth: number effective_email_domain?: string position?: string membership_type: string is_active: boolean joined_at: string ended_at?: string } interface Employee { id: number employee_id: string username_base: string legal_name: string english_name: string | null phone: string | null mobile: string | null hire_date: string termination_date: string | null status: string has_network_drive: boolean created_at: string updated_at: string } type TabType = 'basic' | 'departments' | 'email' | 'permissions' interface OffboardResult { disabled?: boolean handled?: boolean created?: boolean error?: string | null message?: string method?: string } export default function EmployeeDetailPage() { const { data: session, status } = useSession() const router = useRouter() const params = useParams() const employeeId = params.id as string const [employee, setEmployee] = useState(null) const [memberships, setMemberships] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [activeTab, setActiveTab] = useState('basic') // 離職處理 Dialog 狀態 const [showOffboardDialog, setShowOffboardDialog] = useState(false) const [offboardConfirmText, setOffboardConfirmText] = useState('') const [offboardLoading, setOffboardLoading] = useState(false) const [offboardEmailHandling, setOffboardEmailHandling] = useState<'forward' | 'disable'>('forward') const [offboardResults, setOffboardResults] = useState<{ keycloak: OffboardResult email: OffboardResult drive: OffboardResult } | null>(null) useEffect(() => { if (status === 'unauthenticated') { router.push('/auth/signin') } }, [status, router]) useEffect(() => { if (status === 'authenticated' && employeeId) { fetchEmployee() fetchMemberships() } }, [status, employeeId]) const fetchEmployee = async () => { try { setLoading(true) setError(null) const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/employees/${employeeId}` ) if (!response.ok) { throw new Error('無法載入員工資料') } const data = await response.json() setEmployee(data) } catch (err) { setError(err instanceof Error ? err.message : '載入失敗') } finally { setLoading(false) } } const fetchMemberships = async () => { try { const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/department-members/?employee_id=${employeeId}` ) if (response.ok) { const data = await response.json() setMemberships(Array.isArray(data) ? data : data.items || []) } } catch (err) { console.error('載入部門成員資料失敗:', err) } } // 執行離職流程 const handleOffboard = async () => { if (!employee) return if (offboardConfirmText !== employee.employee_id) return setOffboardLoading(true) try { const searchParams = new URLSearchParams({ disable_keycloak: 'true', email_handling: offboardEmailHandling, disable_drive: 'true', }) const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/employees/${employeeId}/offboard?${searchParams.toString()}`, { method: 'POST' } ) if (!response.ok) { const data = await response.json() throw new Error(data.detail || '離職流程執行失敗') } const data = await response.json() setOffboardResults(data.results) setEmployee((prev) => prev ? { ...prev, status: 'terminated' } : null) } catch (err) { alert(err instanceof Error ? err.message : '操作失敗') } finally { setOffboardLoading(false) } } if (status === 'loading' || loading) { return (
載入中...
) } if (!session) return null if (error) { return (
{error}
) } if (!employee) return null const tabs: { id: TabType; label: string; icon: string }[] = [ { id: 'basic', label: '基本資料', icon: '👤' }, { id: 'departments', label: '部門成員', icon: '🏢' }, { id: 'email', label: '郵件帳號', icon: '📧' }, { id: 'permissions', label: '系統權限', icon: '🔐' }, ] // 找到主要郵件網域 (取第一個啟用中的成員紀錄的有效網域) const primaryMembership = memberships.find((m) => m.is_active) const primaryEmailDomain = primaryMembership?.effective_email_domain const membershipTypeLabel = (type: string) => { switch (type) { case 'permanent': return '正式' case 'temporary': return '臨時' case 'project': return '專案' default: return type } } return (
{/* 頁首 */}

{employee.legal_name}

員工編號: {employee.employee_id}

{employee.status === 'active' && ( )}
{/* Tab 切換 */}
{/* 基本資料 Tab */} {activeTab === 'basic' && (

基本資料

{employee.employee_id}

{employee.status === 'active' ? '在職' : employee.status === 'inactive' ? '停用' : '離職'}

{employee.legal_name}

{employee.english_name || '-'}

{employee.username_base}

{new Date(employee.hire_date).toLocaleDateString('zh-TW')}

{employee.termination_date && (

{new Date(employee.termination_date).toLocaleDateString('zh-TW')}

)}
{/* 聯絡資訊 */}

聯絡資訊

{employee.phone || '-'}

{employee.mobile || '-'}

{/* 系統帳號資訊 */}

系統帳號資訊

所屬部門數量

{memberships.filter((m) => m.is_active).length}

{memberships.filter((m) => m.is_active).length > 1 ? '多部門任職' : '單一部門'}

NAS 帳號

{employee.has_network_drive ? '已建立' : '未建立'}

{employee.has_network_drive && ( )}
{/* 系統資訊 */}
建立時間: {new Date(employee.created_at).toLocaleString('zh-TW')}
更新時間: {new Date(employee.updated_at).toLocaleString('zh-TW')}
)} {/* 部門成員 Tab */} {activeTab === 'departments' && (

部門成員紀錄

員工可同時隸屬多個部門

{memberships.length === 0 ? (

尚未加入任何部門

請至「組織架構」頁面將員工加入部門

) : (
{memberships.map((membership) => (
{membership.department_name} {membership.department_code} {membership.department_depth === 0 && ( 第一層 )} {membership.is_active ? '啟用中' : '已結束'}
{membership.position && (
職位: {membership.position}
)}
類型: {membershipTypeLabel(membership.membership_type)}
{membership.effective_email_domain && (
郵件網域: @{membership.effective_email_domain}
)}
加入: {new Date(membership.joined_at).toLocaleDateString('zh-TW')}
{membership.ended_at && (
結束: {new Date(membership.ended_at).toLocaleDateString('zh-TW')}
)}
))}
)}

部門成員管理說明

  • • 員工可同時隸屬多個部門 (主要職務 + 兼任)
  • • 郵件網域由所屬第一層部門決定
  • • 員工編號不因部門轉換而改變
  • • 若需調整部門成員,請至「組織架構」頁面操作
)} {/* 郵件帳號 Tab */} {activeTab === 'email' && ( )} {/* 系統權限 Tab */} {activeTab === 'permissions' && } {/* 離職處理 Dialog */} {showOffboardDialog && (
{offboardResults ? (

離職流程執行完成

{offboardResults.keycloak?.disabled ? '✓' : '⚠'}

Keycloak SSO 帳號

{offboardResults.keycloak?.message}

{offboardResults.keycloak?.error && (

{offboardResults.keycloak.error}

)}
{offboardResults.email?.handled ? '✓' : '⚠'}

郵件帳號 ({offboardResults.email?.method === 'forward' ? '轉發' : '停用'})

{offboardResults.email?.message}

{offboardResults.drive?.disabled ? '✓' : '⚠'}

雲端硬碟帳號

{offboardResults.drive?.message}

) : (

員工離職處理

此操作將自動執行以下操作:

  • • 停用 Keycloak SSO 帳號 (帳號保留以維持審計記錄)
  • • 處理郵件帳號 (轉發或停用)
  • • 停用雲端硬碟帳號
  • • 將員工狀態設為「離職」
setOffboardConfirmText(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-red-500 focus:border-red-500 font-mono" placeholder={employee.employee_id} />
)}
)}
) }