feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
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>
This commit is contained in:
97
frontend/components/layout/breadcrumb.tsx
Normal file
97
frontend/components/layout/breadcrumb.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 麵包屑導航組件
|
||||
*/
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
export function Breadcrumb() {
|
||||
const pathname = usePathname()
|
||||
|
||||
// 定義路徑對應的顯示名稱
|
||||
const pathMap: Record<string, string> = {
|
||||
dashboard: '系統首頁',
|
||||
employees: '員工管理',
|
||||
new: '新增員工',
|
||||
edit: '編輯員工',
|
||||
departments: '部門管理',
|
||||
organization: '組織架構',
|
||||
'system-functions': '系統功能管理',
|
||||
'company-info': '公司資料維護',
|
||||
}
|
||||
|
||||
// 解析路徑生成麵包屑
|
||||
const generateBreadcrumbs = (): BreadcrumbItem[] => {
|
||||
const paths = pathname.split('/').filter((path) => path)
|
||||
const breadcrumbs: BreadcrumbItem[] = []
|
||||
|
||||
paths.forEach((path, index) => {
|
||||
// 跳過數字 ID (例如 /employees/123)
|
||||
if (/^\d+$/.test(path)) {
|
||||
breadcrumbs.push({
|
||||
name: `#${path}`,
|
||||
href: paths.slice(0, index + 1).join('/'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const name = pathMap[path] || path
|
||||
const href = '/' + paths.slice(0, index + 1).join('/')
|
||||
|
||||
breadcrumbs.push({ name, href })
|
||||
})
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
const breadcrumbs = generateBreadcrumbs()
|
||||
|
||||
// 如果只有一層(例如只在 /dashboard),不顯示麵包屑
|
||||
if (breadcrumbs.length <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex mb-6" aria-label="Breadcrumb">
|
||||
<ol className="inline-flex items-center space-x-1 md:space-x-3">
|
||||
{breadcrumbs.map((crumb, index) => {
|
||||
const isLast = index === breadcrumbs.length - 1
|
||||
|
||||
return (
|
||||
<li key={crumb.href || crumb.name} className="inline-flex items-center">
|
||||
{index > 0 && (
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400 mx-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{isLast ? (
|
||||
<span className="text-sm font-medium text-gray-500">{crumb.name}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={crumb.href!}
|
||||
className="text-sm font-medium text-gray-700 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
{crumb.name}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user