Major Features: - ✅ Multi-tenant architecture (tenant isolation) - ✅ Employee CRUD with lifecycle management (onboarding/offboarding) - ✅ Department tree structure with email domain management - ✅ Company info management (single-record editing) - ✅ System functions CRUD (permission management) - ✅ Email account management (multi-account per employee) - ✅ Keycloak SSO integration (auth.lab.taipei) - ✅ Redis session storage (10.1.0.254:6379) - Solves Cookie 4KB limitation - Cross-system session sharing - Sliding expiration (8 hours) - Automatic token refresh Technical Stack: Backend: - FastAPI + SQLAlchemy - PostgreSQL 16 (10.1.0.20:5433) - Keycloak Admin API integration - Docker Mailserver integration (SSH) - Alembic migrations Frontend: - Next.js 14 (App Router) - NextAuth 4 with Keycloak Provider - Redis session storage (ioredis) - Tailwind CSS Infrastructure: - Redis 7 (10.1.0.254:6379) - Session + Cache - Keycloak 26.1.0 (auth.lab.taipei) - Docker Mailserver (10.1.0.254) Architecture Highlights: - Session管理由 Keycloak + Redis 統一控制 - 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session - Token 自動刷新,異質服務整合 - 未來可無縫遷移到雲端 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
803 lines
36 KiB
TypeScript
803 lines
36 KiB
TypeScript
/**
|
||
* 完成初始化頁面
|
||
*
|
||
* 流程:
|
||
* 1. 填寫公司基本資訊
|
||
* 2. 設定郵件網域
|
||
* 3. 設定管理員帳號
|
||
* 4. 確認並執行初始化
|
||
*/
|
||
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
|
||
interface CompanyInfo {
|
||
company_name: string
|
||
company_name_en: string
|
||
tenant_code: string
|
||
tenant_prefix: string
|
||
tax_id: string
|
||
tel: string
|
||
add: string
|
||
}
|
||
|
||
interface MailDomainInfo {
|
||
domain_set: 1 | 2
|
||
domain: string
|
||
}
|
||
|
||
interface AdminInfo {
|
||
admin_legal_name: string
|
||
admin_english_name: string
|
||
admin_email: string
|
||
admin_phone: string
|
||
password_method: 'auto' | 'manual'
|
||
manual_password?: string
|
||
}
|
||
|
||
export default function CompleteInitialization() {
|
||
const router = useRouter()
|
||
const [step, setStep] = useState<'checking' | 'company' | 'maildomain' | 'admin' | 'confirm' | 'executing'>('checking')
|
||
const [sessionId, setSessionId] = useState<number | null>(null)
|
||
const [checkingDb, setCheckingDb] = useState(true)
|
||
const [dbReady, setDbReady] = useState(false)
|
||
|
||
const [companyInfo, setCompanyInfo] = useState<CompanyInfo>({
|
||
company_name: '',
|
||
company_name_en: '',
|
||
tenant_code: '',
|
||
tenant_prefix: '',
|
||
tax_id: '',
|
||
tel: '',
|
||
add: '',
|
||
})
|
||
|
||
const [mailDomainInfo, setMailDomainInfo] = useState<MailDomainInfo>({
|
||
domain_set: 2,
|
||
domain: '',
|
||
})
|
||
|
||
const [adminInfo, setAdminInfo] = useState<AdminInfo>({
|
||
admin_legal_name: '',
|
||
admin_english_name: '',
|
||
admin_email: '',
|
||
admin_phone: '',
|
||
password_method: 'auto',
|
||
})
|
||
|
||
const [generatedPassword, setGeneratedPassword] = useState<string>('')
|
||
const [executing, setExecuting] = useState(false)
|
||
|
||
// 載入 Keycloak Realm 名稱作為租戶代碼預設值
|
||
useEffect(() => {
|
||
const loadKeycloakRealm = async () => {
|
||
try {
|
||
const response = await fetch('http://10.1.0.245:10181/api/v1/installation/get-config/keycloak')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
if (data.configured && data.config && data.config.realm) {
|
||
setCompanyInfo(prev => ({
|
||
...prev,
|
||
tenant_code: data.config.realm.toLowerCase(), // ⚠️ 必須小寫,與 Keycloak Realm 一致
|
||
}))
|
||
}
|
||
}
|
||
setCheckingDb(false)
|
||
setDbReady(true)
|
||
setStep('company')
|
||
} catch (error) {
|
||
console.error('[Installation] Failed to load Keycloak config:', error)
|
||
setCheckingDb(false)
|
||
setDbReady(true)
|
||
setStep('company')
|
||
}
|
||
}
|
||
loadKeycloakRealm()
|
||
}, [])
|
||
|
||
// Step 1: 建立會話並儲存公司資訊
|
||
const handleSaveCompany = async () => {
|
||
try {
|
||
// 建立會話
|
||
if (!sessionId) {
|
||
const sessionResponse = await fetch('http://10.1.0.245:10181/api/v1/installation/sessions', {
|
||
method: 'POST',
|
||
})
|
||
|
||
if (!sessionResponse.ok) {
|
||
throw new Error('建立會話失敗')
|
||
}
|
||
|
||
const sessionData = await sessionResponse.json()
|
||
setSessionId(sessionData.session_id)
|
||
}
|
||
|
||
setStep('maildomain')
|
||
} catch (error) {
|
||
alert(`錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
|
||
}
|
||
}
|
||
|
||
// Step 2: 儲存郵件網域設定
|
||
const handleSaveMailDomain = async () => {
|
||
try {
|
||
if (!sessionId) {
|
||
throw new Error('會話 ID 不存在')
|
||
}
|
||
|
||
// 合併公司資訊 + 郵件網域資訊
|
||
const tenantData = {
|
||
...companyInfo,
|
||
...mailDomainInfo,
|
||
}
|
||
|
||
const response = await fetch(`http://10.1.0.245:10181/api/v1/installation/sessions/${sessionId}/tenant-info`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(tenantData),
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({ detail: `HTTP ${response.status}` }))
|
||
throw new Error(`儲存租戶資訊失敗: ${errorData.detail || response.statusText}`)
|
||
}
|
||
|
||
setStep('admin')
|
||
} catch (error) {
|
||
console.error('[MailDomain] Save failed:', error)
|
||
alert(`錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
|
||
}
|
||
}
|
||
|
||
// Step 3: 設定管理員帳號
|
||
const handleSaveAdmin = async () => {
|
||
try {
|
||
if (!sessionId) {
|
||
throw new Error('會話 ID 不存在')
|
||
}
|
||
|
||
const response = await fetch(`http://10.1.0.245:10181/api/v1/installation/sessions/${sessionId}/admin-setup`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(adminInfo),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.detail || '設定管理員失敗')
|
||
}
|
||
|
||
if (data.initial_password) {
|
||
setGeneratedPassword(data.initial_password)
|
||
}
|
||
|
||
setStep('confirm')
|
||
} catch (error) {
|
||
alert(`錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
|
||
}
|
||
}
|
||
|
||
// Step 4: 執行初始化
|
||
const handleExecute = async () => {
|
||
try {
|
||
if (!sessionId) {
|
||
throw new Error('會話 ID 不存在')
|
||
}
|
||
|
||
setStep('executing')
|
||
setExecuting(true)
|
||
|
||
const response = await fetch(`http://10.1.0.245:10181/api/v1/installation/sessions/${sessionId}/execute`, {
|
||
method: 'POST',
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.detail || '初始化失敗')
|
||
}
|
||
|
||
// 從回應中取得臨時密碼
|
||
const tempPassword = data.result?.credentials?.plain_password || '(密碼已清除,請聯絡管理員)'
|
||
const username = adminInfo.admin_english_name
|
||
|
||
// 顯示完成訊息和臨時密碼
|
||
alert(`初始化完成!
|
||
|
||
請使用以下資訊登入 SSO 系統:
|
||
|
||
帳號: ${username}
|
||
臨時密碼: ${tempPassword}
|
||
|
||
⚠️ 重要提醒:
|
||
1. 請立即記下或截圖這個密碼
|
||
2. 首次登入後系統會要求您變更密碼
|
||
3. 此密碼僅顯示一次,關閉後將無法再次查看
|
||
|
||
點擊「確定」後將跳轉至登入頁面。`)
|
||
|
||
// 跳轉到登入頁面
|
||
window.location.href = '/auth/signin'
|
||
} catch (error) {
|
||
alert(`錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
|
||
setExecuting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-950">
|
||
<div className="container mx-auto px-4 py-8">
|
||
<div className="max-w-3xl mx-auto">
|
||
{/* Progress Bar */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm font-medium text-gray-300">初始化進度</span>
|
||
<span className="text-sm font-medium text-gray-300">
|
||
{step === 'company' ? '1' : step === 'maildomain' ? '2' : step === 'admin' ? '3' : step === 'confirm' ? '4' : '5'} / 4
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||
<div
|
||
className="bg-gradient-to-r from-green-500 to-emerald-600 h-2 rounded-full transition-all"
|
||
style={{
|
||
width:
|
||
step === 'company' ? '25%' :
|
||
step === 'maildomain' ? '50%' :
|
||
step === 'admin' ? '75%' : '100%'
|
||
}}
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Card */}
|
||
<div className="bg-gray-800 rounded-xl shadow-xl p-8 border border-gray-700">
|
||
<div className="flex items-center mb-6">
|
||
<div className="w-12 h-12 bg-green-900/30 rounded-full flex items-center justify-center mr-4 border border-green-800">
|
||
<span className="text-2xl">✨</span>
|
||
</div>
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-100">完成初始化</h1>
|
||
<p className="text-gray-400">
|
||
{step === 'company' && '填寫公司基本資訊'}
|
||
{step === 'maildomain' && '設定郵件網域'}
|
||
{step === 'admin' && '設定系統管理員'}
|
||
{step === 'confirm' && '確認並執行'}
|
||
{step === 'executing' && '正在初始化系統...'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step 1: 公司資訊 */}
|
||
{step === 'company' && (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
公司名稱(中文) <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo.company_name}
|
||
onChange={(e) => setCompanyInfo({ ...companyInfo, company_name: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: 匠耘股份有限公司"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
公司名稱(英文)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo.company_name_en}
|
||
onChange={(e) => setCompanyInfo({ ...companyInfo, company_name_en: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: Porsche World Co., Ltd."
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
統一編號
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo.tax_id}
|
||
onChange={(e) => setCompanyInfo({ ...companyInfo, tax_id: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: 82871784"
|
||
maxLength={8}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
公司電話
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo.tel}
|
||
onChange={(e) => setCompanyInfo({ ...companyInfo, tel: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: 02-26262026"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
公司地址
|
||
</label>
|
||
<textarea
|
||
value={companyInfo.add}
|
||
onChange={(e) => setCompanyInfo({ ...companyInfo, add: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: 新北市淡水區北新路197號7樓"
|
||
rows={2}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
租戶代碼(Keycloak Realm) <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo.tenant_code}
|
||
readOnly
|
||
className="w-full px-4 py-2 bg-gray-600 border border-gray-500 rounded-lg text-gray-300 font-mono cursor-not-allowed"
|
||
placeholder="自動載入 Keycloak Realm..."
|
||
/>
|
||
<p className="text-xs text-gray-400 mt-1">🔒 已鎖定(與 Keycloak Realm 同步)</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
員工編號前綴 <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo.tenant_prefix}
|
||
onChange={(e) => setCompanyInfo({ ...companyInfo, tenant_prefix: e.target.value.toUpperCase() })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent font-mono"
|
||
placeholder="例如: PW"
|
||
maxLength={10}
|
||
/>
|
||
<p className="text-xs text-gray-400 mt-1">員工編號格式:{companyInfo.tenant_prefix || 'XX'}001</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-4">
|
||
<p className="text-sm text-blue-300">
|
||
<strong>💡 提示:</strong>
|
||
</p>
|
||
<ul className="text-sm text-blue-300 mt-2 space-y-1 ml-4 list-disc">
|
||
<li>租戶代碼(code)= Keycloak Realm 名稱,用於 SSO 識別</li>
|
||
<li>⚠️ 租戶代碼必須為<strong>小寫英文</strong>,與 Keycloak Realm 保持一致</li>
|
||
<li>員工編號前綴(prefix)用於產生員工工號,例如:PW → PW001</li>
|
||
<li>兩者可以相同或不同,視需求而定</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="flex gap-4 mt-8">
|
||
<button
|
||
onClick={() => router.back()}
|
||
className="px-6 py-2 border border-gray-600 rounded-lg text-gray-300 hover:bg-gray-700 transition-colors"
|
||
>
|
||
返回
|
||
</button>
|
||
<button
|
||
onClick={handleSaveCompany}
|
||
disabled={!companyInfo.company_name || !companyInfo.tenant_code || !companyInfo.tenant_prefix}
|
||
className="flex-1 bg-green-600 text-white py-2 px-6 rounded-lg hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors font-semibold"
|
||
>
|
||
下一步 →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 2: 郵件網域設定 */}
|
||
{step === 'maildomain' && (
|
||
<div className="space-y-4">
|
||
<div className="bg-yellow-900/20 border border-yellow-700 rounded-lg p-4 mb-6">
|
||
<p className="text-sm text-yellow-300">
|
||
<strong>⚠️ DNS 設定提醒:</strong>
|
||
</p>
|
||
<p className="text-sm text-yellow-300 mt-2">
|
||
本系統不提供 DNS 服務,請在您的網域服務商設定 MX 記錄後再繼續。
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
郵件網域條件 <span className="text-green-400">*</span>
|
||
</label>
|
||
<div className="space-y-2">
|
||
<label className="flex items-start p-4 bg-gray-700/50 border border-gray-600 rounded-lg cursor-pointer hover:bg-gray-700">
|
||
<input
|
||
type="radio"
|
||
name="domainSet"
|
||
checked={mailDomainInfo.domain_set === 1}
|
||
onChange={() => setMailDomainInfo({ ...mailDomainInfo, domain_set: 1 })}
|
||
className="mr-3 mt-1"
|
||
/>
|
||
<div>
|
||
<span className="text-gray-200 font-medium">組織網域</span>
|
||
<p className="text-sm text-gray-400 mt-1">
|
||
所有員工使用統一網域,例如:user@porscheworld.tw
|
||
</p>
|
||
</div>
|
||
</label>
|
||
<label className="flex items-start p-4 bg-gray-700/50 border border-gray-600 rounded-lg cursor-pointer hover:bg-gray-700">
|
||
<input
|
||
type="radio"
|
||
name="domainSet"
|
||
checked={mailDomainInfo.domain_set === 2}
|
||
onChange={() => setMailDomainInfo({ ...mailDomainInfo, domain_set: 2 })}
|
||
className="mr-3 mt-1"
|
||
/>
|
||
<div>
|
||
<span className="text-gray-200 font-medium">部門網域(推薦)</span>
|
||
<p className="text-sm text-gray-400 mt-1">
|
||
不同部門使用不同網域,例如:hr@ease.taipei, mis@lab.taipei
|
||
</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 組織網域模式:所有員工使用統一網域 */}
|
||
{mailDomainInfo.domain_set === 1 && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
組織網域名稱 <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={mailDomainInfo.domain}
|
||
onChange={(e) => setMailDomainInfo({ ...mailDomainInfo, domain: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: porscheworld.tw"
|
||
/>
|
||
<p className="text-xs text-gray-400 mt-2">
|
||
請先在 DNS 服務商設定:<br/>
|
||
MX 記錄: {mailDomainInfo.domain || 'yourdomain.tw'} → mail.{mailDomainInfo.domain || 'yourdomain.tw'} (優先級 10)
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 部門網域模式:需要輸入預設網域給系統管理員使用 */}
|
||
{mailDomainInfo.domain_set === 2 && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
預設網域(系統管理員使用) <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={mailDomainInfo.domain}
|
||
onChange={(e) => setMailDomainInfo({ ...mailDomainInfo, domain: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: porscheworld.tw"
|
||
/>
|
||
<p className="text-xs text-gray-400 mt-2">
|
||
此網域用於初始化部門與系統管理員郵箱,後續各部門可設定自己的網域。
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{mailDomainInfo.domain_set === 2 && (
|
||
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-4">
|
||
<p className="text-sm text-blue-300">
|
||
<strong>💡 部門網域說明:</strong>
|
||
</p>
|
||
<p className="text-sm text-blue-300 mt-2">
|
||
選擇部門網域後,每個部門可以設定獨立的郵件網域。<br/>
|
||
例如:ease.taipei (業務部)、lab.taipei (技術部)、porscheworld.tw (營運部)
|
||
</p>
|
||
<p className="text-sm text-blue-300 mt-2">
|
||
初始化完成後,人資可以在部門管理中為每個部門設定專屬網域。
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-4 mt-8">
|
||
<button
|
||
onClick={() => setStep('company')}
|
||
className="px-6 py-2 border border-gray-600 rounded-lg text-gray-300 hover:bg-gray-700 transition-colors"
|
||
>
|
||
上一步
|
||
</button>
|
||
<button
|
||
onClick={handleSaveMailDomain}
|
||
disabled={!mailDomainInfo.domain}
|
||
className="flex-1 bg-green-600 text-white py-2 px-6 rounded-lg hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors font-semibold"
|
||
>
|
||
下一步 →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 3: 管理員資訊 */}
|
||
{step === 'admin' && (
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
管理員姓名 <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={adminInfo.admin_legal_name}
|
||
onChange={(e) => setAdminInfo({ ...adminInfo, admin_legal_name: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: 陳柏旭"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
英文姓名
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={adminInfo.admin_english_name}
|
||
onChange={(e) => setAdminInfo({ ...adminInfo, admin_english_name: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: Porsche Chen"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Email <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={adminInfo.admin_email}
|
||
onChange={(e) => setAdminInfo({ ...adminInfo, admin_email: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: admin@example.com"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
電話
|
||
</label>
|
||
<input
|
||
type="tel"
|
||
value={adminInfo.admin_phone}
|
||
onChange={(e) => setAdminInfo({ ...adminInfo, admin_phone: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="例如: 0912345678"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
密碼設定方式
|
||
</label>
|
||
<div className="space-y-2">
|
||
<label className="flex items-center p-3 bg-gray-700/50 border border-gray-600 rounded-lg cursor-pointer hover:bg-gray-700">
|
||
<input
|
||
type="radio"
|
||
name="passwordMethod"
|
||
checked={adminInfo.password_method === 'auto'}
|
||
onChange={() => setAdminInfo({ ...adminInfo, password_method: 'auto', manual_password: undefined })}
|
||
className="mr-3"
|
||
/>
|
||
<span className="text-gray-200">自動產生密碼(推薦)</span>
|
||
</label>
|
||
<label className="flex items-center p-3 bg-gray-700/50 border border-gray-600 rounded-lg cursor-pointer hover:bg-gray-700">
|
||
<input
|
||
type="radio"
|
||
name="passwordMethod"
|
||
checked={adminInfo.password_method === 'manual'}
|
||
onChange={() => setAdminInfo({ ...adminInfo, password_method: 'manual' })}
|
||
className="mr-3"
|
||
/>
|
||
<span className="text-gray-200">手動輸入密碼</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{adminInfo.password_method === 'manual' && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
密碼 <span className="text-green-400">*</span>
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={adminInfo.manual_password || ''}
|
||
onChange={(e) => setAdminInfo({ ...adminInfo, manual_password: e.target.value })}
|
||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||
placeholder="請輸入密碼"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-4 mt-6">
|
||
<p className="text-sm text-blue-300">
|
||
<strong>💡 提示:</strong>
|
||
</p>
|
||
<ul className="text-sm text-blue-300 mt-2 space-y-1 ml-4 list-disc">
|
||
<li>系統管理員將自動歸屬於「初始化部門」</li>
|
||
<li>初始化完成後,可由人資建立正式組織架構</li>
|
||
<li>人資可將管理員調動至正式的 MIS 部門</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="flex gap-4 mt-8">
|
||
<button
|
||
onClick={() => setStep('maildomain')}
|
||
className="px-6 py-2 border border-gray-600 rounded-lg text-gray-300 hover:bg-gray-700 transition-colors"
|
||
>
|
||
上一步
|
||
</button>
|
||
<button
|
||
onClick={handleSaveAdmin}
|
||
disabled={!adminInfo.admin_legal_name || !adminInfo.admin_email || (adminInfo.password_method === 'manual' && !adminInfo.manual_password)}
|
||
className="flex-1 bg-green-600 text-white py-2 px-6 rounded-lg hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors font-semibold"
|
||
>
|
||
下一步 →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 4: 確認資訊 */}
|
||
{step === 'confirm' && (
|
||
<div className="space-y-6">
|
||
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-4">
|
||
<p className="text-sm text-blue-300">
|
||
<strong>⚠️ 注意:</strong>執行初始化後,系統將自動建立:
|
||
</p>
|
||
<ul className="text-sm text-blue-300 mt-2 space-y-1 ml-4 list-disc">
|
||
<li>租戶資料(公司)</li>
|
||
<li>初始化部門(INIT)</li>
|
||
<li>系統管理員帳號({companyInfo.tenant_prefix}001)</li>
|
||
<li>Keycloak SSO 帳號</li>
|
||
<li>郵件帳號 ({adminInfo.admin_email})</li>
|
||
</ul>
|
||
<p className="text-sm text-blue-300 mt-2">
|
||
此操作無法復原,請確認資料正確。
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="bg-gray-700/50 rounded-lg p-4">
|
||
<h3 className="text-lg font-semibold text-green-300 mb-3">公司資訊</h3>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">公司名稱:</span>
|
||
<span className="text-gray-200">{companyInfo.company_name}</span>
|
||
</div>
|
||
{companyInfo.company_name_en && (
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">英文名稱:</span>
|
||
<span className="text-gray-200">{companyInfo.company_name_en}</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">租戶代碼:</span>
|
||
<span className="text-gray-200 font-mono">{companyInfo.tenant_code}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">員工編號前綴:</span>
|
||
<span className="text-gray-200 font-mono">{companyInfo.tenant_prefix}</span>
|
||
</div>
|
||
{companyInfo.tax_id && (
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">統一編號:</span>
|
||
<span className="text-gray-200">{companyInfo.tax_id}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-gray-700/50 rounded-lg p-4">
|
||
<h3 className="text-lg font-semibold text-green-300 mb-3">郵件網域</h3>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">網域條件:</span>
|
||
<span className="text-gray-200">
|
||
{mailDomainInfo.domain_set === 1 ? '組織網域' : '部門網域'}
|
||
</span>
|
||
</div>
|
||
{mailDomainInfo.domain_set === 1 && mailDomainInfo.domain && (
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">組織網域:</span>
|
||
<span className="text-gray-200 font-mono">{mailDomainInfo.domain}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-gray-700/50 rounded-lg p-4">
|
||
<h3 className="text-lg font-semibold text-green-300 mb-3">管理員資訊</h3>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">姓名:</span>
|
||
<span className="text-gray-200">{adminInfo.admin_legal_name}</span>
|
||
</div>
|
||
{adminInfo.admin_english_name && (
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">英文姓名:</span>
|
||
<span className="text-gray-200">{adminInfo.admin_english_name}</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">Email:</span>
|
||
<span className="text-gray-200">{adminInfo.admin_email}</span>
|
||
</div>
|
||
{adminInfo.admin_phone && (
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">電話:</span>
|
||
<span className="text-gray-200">{adminInfo.admin_phone}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{generatedPassword && (
|
||
<div className="bg-yellow-900/20 border border-yellow-700 rounded-lg p-4">
|
||
<h3 className="text-lg font-semibold text-yellow-300 mb-3">⚠️ 初始密碼(請妥善保存)</h3>
|
||
<div className="bg-gray-900 rounded p-3 font-mono text-yellow-200 text-center text-lg">
|
||
{generatedPassword}
|
||
</div>
|
||
<p className="text-xs text-yellow-400 mt-2">
|
||
此密碼僅顯示一次,請立即記錄。首次登入後請更改密碼。
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="bg-yellow-900/20 border border-yellow-700 rounded-lg p-4">
|
||
<p className="text-sm text-yellow-300">
|
||
<strong>📋 初始化後續工作:</strong>
|
||
</p>
|
||
<ol className="text-sm text-yellow-300 mt-2 space-y-1 ml-4 list-decimal">
|
||
<li>使用管理員帳號登入系統</li>
|
||
<li>建立「人資部門」並新增第一位人資人員</li>
|
||
<li>由人資建立完整組織架構</li>
|
||
<li>由人資將管理員調動至正式部門(選填)</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<div className="flex gap-4 mt-8">
|
||
<button
|
||
onClick={() => setStep('admin')}
|
||
className="px-6 py-2 border border-gray-600 rounded-lg text-gray-300 hover:bg-gray-700 transition-colors"
|
||
>
|
||
上一步
|
||
</button>
|
||
<button
|
||
onClick={handleExecute}
|
||
className="flex-1 bg-gradient-to-r from-green-600 to-emerald-600 text-white py-3 px-6 rounded-lg font-semibold text-lg hover:from-green-700 hover:to-emerald-700 transition-all transform hover:scale-105 shadow-lg"
|
||
>
|
||
🚀 執行初始化
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 4: 執行中 */}
|
||
{step === 'executing' && (
|
||
<div className="text-center py-12">
|
||
<div className="w-20 h-20 border-4 border-green-500 border-t-transparent rounded-full animate-spin mx-auto mb-6"></div>
|
||
<h3 className="text-xl font-semibold text-green-300 mb-2">正在初始化系統</h3>
|
||
<p className="text-gray-400">請稍候,這可能需要幾秒鐘...</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|