diff --git a/gateway/html/dashboard.html b/gateway/html/dashboard.html index 9513429..1ca90d6 100644 --- a/gateway/html/dashboard.html +++ b/gateway/html/dashboard.html @@ -492,7 +492,7 @@ { id: 'factory', name: '공장관리', desc: '작업장 현황, TBM, 설비관리', icon: '\uD83C\uDFED', subdomain: 'tkfb', path: '/pages/dashboard.html', pageKey: 'dashboard', color: '#1a56db' }, { id: 'report_sys', name: '신고', desc: '사건·사고 신고 접수', icon: '\uD83D\uDEA8', subdomain: 'tkreport', accessKey: 'system2', color: '#dc2626' }, { id: 'quality', name: '부적합관리', desc: '부적합 이슈 추적·처리', icon: '\uD83D\uDCCA', subdomain: 'tkqc', accessKey: 'system3', color: '#059669' }, - { id: 'purchase', name: '소모품 관리', desc: '소모품 신청·구매, 분석', icon: '\uD83D\uDED2', subdomain: 'tkpurchase', color: '#d97706' }, + { id: 'purchase', name: '구매관리', desc: '자재 구매, 일용직 관리', icon: '\uD83D\uDED2', subdomain: 'tkpurchase', color: '#d97706' }, { id: 'safety', name: '안전관리', desc: '안전 점검, 방문 관리', icon: '\uD83E\uDDBA', subdomain: 'tksafety', color: '#7c3aed' }, { id: 'support', name: '행정지원', desc: '전사 행정 업무 지원', icon: '\uD83C\uDFE2', subdomain: 'tksupport', color: '#0284c7' }, { id: 'admin', name: '통합관리', desc: '사용자·권한 관리', icon: '\u2699\uFE0F', subdomain: 'tkuser', minRole: 'admin', color: '#0891b2' } diff --git a/tkeg/web/src/App.jsx b/tkeg/web/src/App.jsx new file mode 100644 index 0000000..e9ac65b --- /dev/null +++ b/tkeg/web/src/App.jsx @@ -0,0 +1,224 @@ +import React, { useState, useEffect } from 'react'; +import DashboardPage from './pages/dashboard/DashboardPage'; +import { UserMenu, ErrorBoundary } from './components/common'; +import NewMaterialsPage from './pages/NewMaterialsPage'; +import BOMManagementPage from './pages/BOMManagementPage'; +import UnifiedBOMPage from './pages/UnifiedBOMPage'; +import SystemSettingsPage from './pages/SystemSettingsPage'; +import PurchaseBatchPage from './pages/PurchaseBatchPage'; +import PurchaseRequestPage from './pages/PurchaseRequestPage'; +import SystemLogsPage from './pages/SystemLogsPage'; +import LogMonitoringPage from './pages/LogMonitoringPage'; +import InactiveProjectsPage from './pages/InactiveProjectsPage'; +import api from './api'; +import { config } from './config'; +import './App.css'; + +function getSSOToken() { + const match = document.cookie.match(/sso_token=([^;]*)/); + return match ? match[1] : null; +} + +function parseJwt(token) { + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + return JSON.parse(atob(base64)); + } catch { return null; } +} + +function App() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [user, setUser] = useState(null); + const [currentPage, setCurrentPage] = useState('dashboard'); + const [pageParams, setPageParams] = useState({}); + const [projects, setProjects] = useState([]); + const [editingProject, setEditingProject] = useState(null); + const [editedProjectName, setEditedProjectName] = useState(''); + const [showCreateProject, setShowCreateProject] = useState(false); + const [newProjectCode, setNewProjectCode] = useState(''); + const [newProjectName, setNewProjectName] = useState(''); + const [newClientName, setNewClientName] = useState(''); + const [inactiveProjects, setInactiveProjects] = useState(() => { + try { + const saved = localStorage.getItem('tkeg_inactiveProjects'); + return saved ? new Set(JSON.parse(saved)) : new Set(); + } catch { return new Set(); } + }); + + useEffect(() => { + try { + localStorage.setItem('tkeg_inactiveProjects', JSON.stringify(Array.from(inactiveProjects))); + } catch {} + }, [inactiveProjects]); + + const loadProjects = async () => { + try { + const response = await api.get('/jobs/'); + if (response.data?.jobs) { + setProjects(response.data.jobs); + } + } catch (error) { + console.error('프로젝트 목록 로드 실패:', error); + } + }; + + const handleActivateProject = (project) => { + const projectId = project.job_no || project.id; + setInactiveProjects(prev => { + const newSet = new Set(prev); + newSet.delete(projectId); + return newSet; + }); + }; + + // SSO 인증 확인 + useEffect(() => { + const token = getSSOToken(); + if (!token) { + window.location.href = config.ssoLoginUrl(window.location.href); + return; + } + const payload = parseJwt(token); + if (!payload) { + window.location.href = config.ssoLoginUrl(); + return; + } + setUser({ + user_id: payload.user_id, + username: payload.sub || payload.username, + name: payload.name || payload.username || payload.sub, + role: payload.role || 'user', + department: payload.department, + }); + setIsAuthenticated(true); + setIsLoading(false); + loadProjects(); + }, []); + + const navigateToPage = (page, params = {}) => { + setCurrentPage(page); + setPageParams(params); + }; + + const handleLogout = () => { + document.cookie = `sso_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${config.cookieDomain}`; + window.location.href = config.ssoLoginUrl(); + }; + + const renderCurrentPage = () => { + switch (currentPage) { + case 'dashboard': + return ( + {}} + updateProjectName={() => {}} + deleteProject={() => {}} + editingProject={editingProject} + setEditingProject={setEditingProject} + editedProjectName={editedProjectName} + setEditedProjectName={setEditedProjectName} + showCreateProject={showCreateProject} + setShowCreateProject={setShowCreateProject} + newProjectCode={newProjectCode} + setNewProjectCode={setNewProjectCode} + newProjectName={newProjectName} + setNewProjectName={setNewProjectName} + newClientName={newClientName} + setNewClientName={setNewClientName} + inactiveProjects={inactiveProjects} + setInactiveProjects={setInactiveProjects} + /> + ); + case 'unified-bom': + return ; + case 'materials': + return ( + + ); + case 'purchase-batch': + return ; + case 'purchase-request': + return ; + case 'system-settings': + return ; + case 'system-logs': + return ; + case 'log-monitoring': + return ; + case 'inactive-projects': + return ( + {}} + /> + ); + default: + return ( +
+
+
알 수 없는 페이지입니다.
+ +
+
+ ); + } + }; + + if (isLoading) { + return ( +
+
+
로딩 중...
+
+
+ ); + } + + if (!isAuthenticated) return null; + + return ( + +
+
+
+

+ TK-EG BOM Management System +

+

+ {user?.name || user?.username}님 환영합니다 +

+
+ +
+ {renderCurrentPage()} +
+
+ ); +} + +export default App; diff --git a/tkeg/web/src/main.jsx b/tkeg/web/src/main.jsx new file mode 100644 index 0000000..f6a12d7 --- /dev/null +++ b/tkeg/web/src/main.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { ThemeProvider, CssBaseline } from '@mui/material' +import theme from './theme' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + + , +) diff --git a/tkeg/web/src/pages/DashboardPage.old.jsx b/tkeg/web/src/pages/DashboardPage.old.jsx new file mode 100644 index 0000000..5ed50d3 --- /dev/null +++ b/tkeg/web/src/pages/DashboardPage.old.jsx @@ -0,0 +1,1119 @@ +import React, { useState, useEffect } from 'react'; + +const DashboardPage = ({ + user, + projects, + pendingSignupCount, + navigateToPage, + loadProjects, + createProject, + updateProjectName, + deleteProject, + editingProject, + setEditingProject, + editedProjectName, + setEditedProjectName, + showCreateProject, + setShowCreateProject, + newProjectCode, + setNewProjectCode, + newProjectName, + setNewProjectName, + newClientName, + setNewClientName, + inactiveProjects, + setInactiveProjects, +}) => { + const [selectedProject, setSelectedProject] = useState(null); + const [showProjectDropdown, setShowProjectDropdown] = useState(false); + + // 프로젝트 생성 모달 닫기 + const handleCloseCreateProject = () => { + setShowCreateProject(false); + setNewProjectCode(''); + setNewProjectName(''); + setNewClientName(''); + }; + + // 프로젝트 선택 처리 + const handleProjectSelect = (project) => { + setSelectedProject(project); + setShowProjectDropdown(false); + }; + + // 프로젝트 비활성화 + const handleDeactivateProject = (project) => { + const projectId = project.job_no || project.official_project_code || project.id; + const projectName = project.job_name || project.project_name || projectId; + + console.log('🔍 비활성화 요청:', { project, projectId, projectName }); + + if (window.confirm(`"${projectName}" 프로젝트를 비활성화하시겠습니까?`)) { + setInactiveProjects(prev => { + const newSet = new Set([...prev, projectId]); + console.log('📦 비활성화 프로젝트 업데이트:', { prev: Array.from(prev), new: Array.from(newSet) }); + return newSet; + }); + + const selectedProjectId = selectedProject?.job_no || selectedProject?.official_project_code || selectedProject?.id; + if (selectedProjectId === projectId) { + setSelectedProject(null); + } + setShowProjectDropdown(false); + } + }; + + // 프로젝트 활성화 + const handleActivateProject = (project) => { + setInactiveProjects(prev => { + const newSet = new Set(prev); + newSet.delete(project.job_no); + return newSet; + }); + }; + + // 프로젝트 삭제 (드롭다운용) + const handleDeleteProjectFromDropdown = (project, e) => { + e.stopPropagation(); + if (window.confirm(`"${project.job_name || project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?`)) { + deleteProject(project.job_no); + setShowProjectDropdown(false); + } + }; + + // 컴포넌트 마운트 시 프로젝트 로드 + useEffect(() => { + loadProjects(); + }, []); + + return ( +
+ {/* 대시보드 헤더 */} +
+

+ Dashboard +

+

+ TK-MP BOM Management System v2.0 - Project Selection Interface +

+
+ + {/* 프로젝트 선택 섹션 */} +
+
+
+

+ Project Selection +

+

+ Choose a project to access BOM and purchase management +

+
+
+ + + + + +
+
+ + {/* 프로젝트 드롭다운 */} +
+ + + {/* 드롭다운 메뉴 */} + {showProjectDropdown && ( +
+ {projects.length === 0 ? ( +
+ No projects available. Create a new one! +
+ ) : ( + projects + .filter(project => { + const projectId = project.job_no || project.official_project_code || project.id; + return !inactiveProjects.has(projectId); + }) + .map((project) => ( +
+
handleProjectSelect(project)} + style={{ + padding: '16px 20px', + cursor: 'pointer', + flex: 1 + }} + onMouseEnter={(e) => e.target.closest('div').style.background = '#f8fafc'} + onMouseLeave={(e) => e.target.closest('div').style.background = 'white'} + > +
+ {project.job_name || project.job_no} +
+
+ Code: {project.job_no} | Client: {project.client_name || 'N/A'} +
+
+ + {/* 프로젝트 관리 버튼들 */} +
+ + + +
+
+ )) + )} +
+ )} +
+
+ + {/* 프로젝트가 선택된 경우 - 프로젝트 관련 메뉴 */} + {selectedProject && ( +
+

+ Project Management +

+ +
+ {/* 통합 BOM 관리 */} +
navigateToPage('unified-bom', { selectedProject })} + style={{ + background: 'white', + borderRadius: '16px', + padding: '32px', + boxShadow: '0 8px 20px rgba(0, 0, 0, 0.08)', + border: '1px solid #e2e8f0', + cursor: 'pointer', + transition: 'all 0.3s ease', + textAlign: 'center' + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-5px)'; + e.currentTarget.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.15)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.08)'; + }} + > +
+ BOM +
+

+ BOM Management +

+

+ Upload, manage revisions, and classify materials in one unified workspace +

+ + {/* 기능 미리보기 */} +
+
+ 📤 Upload +
+
+ 📊 Revisions +
+
+ 📋 Materials +
+
+
+ + {/* 구매신청 관리 */} +
navigateToPage('purchase-request', { selectedProject })} + style={{ + background: 'white', + borderRadius: '16px', + padding: '32px', + boxShadow: '0 8px 20px rgba(0, 0, 0, 0.08)', + border: '1px solid #e2e8f0', + cursor: 'pointer', + transition: 'all 0.3s ease', + textAlign: 'center' + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-5px)'; + e.currentTarget.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.15)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.08)'; + }} + > +
+ REQ +
+

+ Purchase Request Management +

+

+ Manage purchase requests and export materials to Excel for procurement. +

+
+
+
+ )} + + {/* 관리자 메뉴 (Admin 이상만 표시) */} + {user?.role === 'admin' && ( +
+

+ System Administration +

+ +
+ {/* 사용자 관리 */} +
navigateToPage('user-management')} + style={{ + background: 'white', + borderRadius: '16px', + padding: '24px', + boxShadow: '0 8px 20px rgba(0, 0, 0, 0.08)', + border: '1px solid #e2e8f0', + cursor: 'pointer', + transition: 'all 0.3s ease', + textAlign: 'center' + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-3px)'; + e.currentTarget.style.boxShadow = '0 12px 25px rgba(0, 0, 0, 0.12)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.08)'; + }} + > +
+ USER +
+

+ User Management +

+

+ Manage user accounts and permissions +

+ {pendingSignupCount > 0 && ( +
+ {pendingSignupCount} pending +
+ )} +
+ + {/* 시스템 설정 */} +
navigateToPage('system-settings')} + style={{ + background: 'white', + borderRadius: '16px', + padding: '24px', + boxShadow: '0 8px 20px rgba(0, 0, 0, 0.08)', + border: '1px solid #e2e8f0', + cursor: 'pointer', + transition: 'all 0.3s ease', + textAlign: 'center' + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-3px)'; + e.currentTarget.style.boxShadow = '0 12px 25px rgba(0, 0, 0, 0.12)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.08)'; + }} + > +
+ SYS +
+

+ System Settings +

+

+ Configure system preferences and settings +

+
+ + {/* 시스템 로그 */} +
navigateToPage('system-logs')} + style={{ + background: 'white', + borderRadius: '16px', + padding: '24px', + boxShadow: '0 8px 20px rgba(0, 0, 0, 0.08)', + border: '1px solid #e2e8f0', + cursor: 'pointer', + transition: 'all 0.3s ease', + textAlign: 'center' + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-3px)'; + e.currentTarget.style.boxShadow = '0 12px 25px rgba(0, 0, 0, 0.12)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.08)'; + }} + > +
+ LOG +
+

+ System Logs +

+

+ View system activity and error logs +

+
+ + {/* 로그 모니터링 */} +
navigateToPage('log-monitoring')} + style={{ + background: 'white', + borderRadius: '16px', + padding: '24px', + boxShadow: '0 8px 20px rgba(0, 0, 0, 0.08)', + border: '1px solid #e2e8f0', + cursor: 'pointer', + transition: 'all 0.3s ease', + textAlign: 'center' + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-3px)'; + e.currentTarget.style.boxShadow = '0 12px 25px rgba(0, 0, 0, 0.12)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.08)'; + }} + > +
+ MON +
+

+ Log Monitoring +

+

+ Real-time system monitoring and alerts +

+
+
+
+ )} + + {/* 시스템 현황 섹션 */} +
+

+ System Overview +

+
+ {/* 등록된 프로젝트 */} +
+
+ {projects.length || 0} +
+
Registered Projects
+
+ {/* 선택된 프로젝트 */} +
+
+ {selectedProject ? '1' : '0'} +
+
Selected Project
+
+ {/* 현재 권한 */} +
+
+ {user?.role === 'admin' ? 'Admin' : 'User'} +
+
Current Role
+
+ {/* 시스템 상태 */} +
+
+ Active +
+
System Status
+
+
+
+ + {/* 프로젝트 생성 모달 */} + {showCreateProject && ( +
+
+

+ Create New Project +

+
+ + setNewProjectCode(e.target.value)} + style={{ + width: '100%', + padding: '12px', + borderRadius: '8px', + border: '1px solid #cbd5e1', + fontSize: '16px', + boxSizing: 'border-box' + }} + placeholder="e.g., J24-001" + /> +
+
+ + setNewProjectName(e.target.value)} + style={{ + width: '100%', + padding: '12px', + borderRadius: '8px', + border: '1px solid #cbd5e1', + fontSize: '16px', + boxSizing: 'border-box' + }} + placeholder="e.g., Ulsan SK Energy Expansion" + /> +
+
+ + setNewClientName(e.target.value)} + style={{ + width: '100%', + padding: '12px', + borderRadius: '8px', + border: '1px solid #cbd5e1', + fontSize: '16px', + boxSizing: 'border-box' + }} + placeholder="e.g., Samsung Engineering" + /> +
+
+ + +
+
+
+ )} +
+ ); +}; + +export default DashboardPage; \ No newline at end of file diff --git a/tkeg/web/src/pages/dashboard/AdminSection.jsx b/tkeg/web/src/pages/dashboard/AdminSection.jsx new file mode 100644 index 0000000..3af42d7 --- /dev/null +++ b/tkeg/web/src/pages/dashboard/AdminSection.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardActionArea from '@mui/material/CardActionArea'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import PeopleIcon from '@mui/icons-material/People'; +import SettingsIcon from '@mui/icons-material/Settings'; +import ArticleIcon from '@mui/icons-material/Article'; +import MonitorHeartIcon from '@mui/icons-material/MonitorHeart'; +import ArchiveIcon from '@mui/icons-material/Archive'; + +const ADMIN_ITEMS = [ + { key: 'users', page: 'user-management', label: '사용자 관리', icon: PeopleIcon }, + { key: 'settings', page: 'system-settings', label: '시스템 설정', icon: SettingsIcon }, + { key: 'logs', page: 'system-logs', label: '시스템 로그', icon: ArticleIcon }, + { key: 'monitor', page: 'log-monitoring', label: '모니터링', icon: MonitorHeartIcon }, + { key: 'inactive', page: 'inactive-projects', label: '비활성 프로젝트', icon: ArchiveIcon }, +]; + +export default function AdminSection({ user, navigateToPage }) { + if (user?.role !== 'admin') return null; + + return ( + <> + + + + 관리자 전용 + + + {ADMIN_ITEMS.map(({ key, page, label, icon: Icon }) => ( + + navigateToPage(page)} + sx={{ p: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.75 }} + > + + + {label} + + + + ))} + + + + ); +} diff --git a/tkeg/web/src/pages/dashboard/CreateProjectDialog.jsx b/tkeg/web/src/pages/dashboard/CreateProjectDialog.jsx new file mode 100644 index 0000000..bc12f82 --- /dev/null +++ b/tkeg/web/src/pages/dashboard/CreateProjectDialog.jsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import api from '../../api'; + +export default function CreateProjectDialog({ open, onClose, onCreated }) { + const [code, setCode] = useState(''); + const [name, setName] = useState(''); + const [client, setClient] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async () => { + if (!code.trim() || !name.trim()) return; + setSubmitting(true); + try { + await api.post('/dashboard/projects', null, { + params: { + official_project_code: code.trim(), + project_name: name.trim(), + client_name: client.trim() || undefined, + }, + }); + setCode(''); + setName(''); + setClient(''); + onCreated?.(); + onClose(); + } catch (err) { + alert(err.response?.data?.detail || '프로젝트 생성 실패'); + } finally { + setSubmitting(false); + } + }; + + return ( + + 프로젝트 생성 + + + setCode(e.target.value)} + size="small" + required + fullWidth + /> + setName(e.target.value)} + size="small" + required + fullWidth + /> + setClient(e.target.value)} + size="small" + fullWidth + /> + + + + + + + + ); +} diff --git a/tkeg/web/src/pages/dashboard/DashboardPage.jsx b/tkeg/web/src/pages/dashboard/DashboardPage.jsx new file mode 100644 index 0000000..37304db --- /dev/null +++ b/tkeg/web/src/pages/dashboard/DashboardPage.jsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import CircularProgress from '@mui/material/CircularProgress'; +import ProjectSelectorBar from './ProjectSelectorBar'; +import MetricCards from './MetricCards'; +import QuickActionCards from './QuickActionCards'; +import AdminSection from './AdminSection'; +import CreateProjectDialog from './CreateProjectDialog'; +import useDashboardData from './useDashboardData'; + +export default function DashboardPage({ + user, + projects, + navigateToPage, + loadProjects, + inactiveProjects, + setInactiveProjects, + // legacy props (kept for App.jsx compat) + showCreateProject, + setShowCreateProject, + ...rest +}) { + const [selectedProject, setSelectedProject] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const { loading, getMetrics } = useDashboardData(user, projects, selectedProject); + + const roleLabelMap = { + admin: '시스템 관리자', + manager: '프로젝트 매니저', + designer: '설계 담당자', + purchaser: '구매 담당자', + }; + const roleLabel = roleLabelMap[user?.role] || '사용자'; + + return ( + + {/* Header */} + + Dashboard + + {user?.name || user?.username}님 환영합니다 — {roleLabel} + + + + {/* Project selector */} + + setDialogOpen(true)} + inactiveProjects={inactiveProjects} + /> + + + {/* Metric cards */} + {loading ? ( + + + + ) : ( + + + + )} + + {/* Quick action cards */} + + + + + {/* Admin section */} + + + {/* Create project dialog */} + setDialogOpen(false)} + onCreated={loadProjects} + /> + + ); +} diff --git a/tkeg/web/src/pages/dashboard/MetricCards.jsx b/tkeg/web/src/pages/dashboard/MetricCards.jsx new file mode 100644 index 0000000..5981e1b --- /dev/null +++ b/tkeg/web/src/pages/dashboard/MetricCards.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Typography from '@mui/material/Typography'; +import FolderIcon from '@mui/icons-material/Folder'; +import DescriptionIcon from '@mui/icons-material/Description'; +import Inventory2Icon from '@mui/icons-material/Inventory2'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; + +const ICONS = { + projects: FolderIcon, + bom: DescriptionIcon, + materials: Inventory2Icon, + purchase: ShoppingCartIcon, +}; + +export default function MetricCards({ metrics }) { + const entries = Object.entries(metrics); + + return ( + + {entries.map(([key, { label, value, color }]) => { + const Icon = ICONS[key] || FolderIcon; + return ( + + + + + + + {value} + + + {label} + + + + ); + })} + + ); +} diff --git a/tkeg/web/src/pages/dashboard/ProjectSelectorBar.jsx b/tkeg/web/src/pages/dashboard/ProjectSelectorBar.jsx new file mode 100644 index 0000000..ce4e937 --- /dev/null +++ b/tkeg/web/src/pages/dashboard/ProjectSelectorBar.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Autocomplete from '@mui/material/Autocomplete'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import AddIcon from '@mui/icons-material/Add'; + +export default function ProjectSelectorBar({ + projects, + selectedProject, + onSelectProject, + onCreateProject, + inactiveProjects, +}) { + const activeProjects = projects.filter(p => { + const id = p.job_no || p.official_project_code || p.id; + return !inactiveProjects.has(id); + }); + + return ( + + onSelectProject(v)} + getOptionLabel={(opt) => { + const name = opt.job_name || opt.project_name || ''; + const code = opt.job_no || opt.official_project_code || ''; + return name ? `${name} (${code})` : code; + }} + isOptionEqualToValue={(opt, val) => + (opt.job_no || opt.id) === (val.job_no || val.id) + } + renderInput={(params) => ( + + )} + noOptionsText="프로젝트 없음" + /> + + + ); +} diff --git a/tkeg/web/src/pages/dashboard/QuickActionCards.jsx b/tkeg/web/src/pages/dashboard/QuickActionCards.jsx new file mode 100644 index 0000000..7c5ca82 --- /dev/null +++ b/tkeg/web/src/pages/dashboard/QuickActionCards.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardActionArea from '@mui/material/CardActionArea'; +import Typography from '@mui/material/Typography'; +import Chip from '@mui/material/Chip'; +import DescriptionIcon from '@mui/icons-material/Description'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; + +const ACTIONS = [ + { + key: 'bom', + page: 'unified-bom', + title: 'BOM 관리', + desc: 'BOM 파일 업로드, 리비전 관리, 자재 분류를 통합 관리합니다.', + icon: DescriptionIcon, + color: '#2563eb', + tags: ['Upload', 'Revision', 'Materials'], + }, + { + key: 'purchase', + page: 'purchase-request', + title: '구매요청 관리', + desc: '구매요청을 관리하고 Excel로 내보내기합니다.', + icon: ShoppingCartIcon, + color: '#16a34a', + tags: ['Request', 'Excel Export'], + }, +]; + +export default function QuickActionCards({ selectedProject, navigateToPage }) { + if (!selectedProject) return null; + + return ( + + {ACTIONS.map(({ key, page, title, desc, icon: Icon, color, tags }) => ( + + navigateToPage(page, { selectedProject })} + sx={{ p: 3, display: 'flex', flexDirection: 'row', alignItems: 'flex-start', gap: 2 }} + > + + + + + {title} + + {desc} + + + {tags.map(t => ( + + ))} + + + + + ))} + + ); +} diff --git a/tkeg/web/src/pages/dashboard/useDashboardData.js b/tkeg/web/src/pages/dashboard/useDashboardData.js new file mode 100644 index 0000000..3c7d314 --- /dev/null +++ b/tkeg/web/src/pages/dashboard/useDashboardData.js @@ -0,0 +1,84 @@ +import { useState, useEffect, useCallback } from 'react'; +import api, { fetchMaterialsSummary } from '../../api'; + +export default function useDashboardData(user, projects, selectedProject) { + const [stats, setStats] = useState(null); + const [projectStats, setProjectStats] = useState(null); + const [loading, setLoading] = useState(true); + + // Always fetch system-wide stats + useEffect(() => { + let cancelled = false; + setLoading(true); + + api.get('/dashboard/stats') + .then(res => { + if (!cancelled && res.data?.success) { + setStats(res.data.stats); + } + }) + .catch(err => console.error('Dashboard stats error:', err)) + .finally(() => { if (!cancelled) setLoading(false); }); + + return () => { cancelled = true; }; + }, [user]); + + // Fetch project-specific data when a project is selected + useEffect(() => { + if (!selectedProject) { + setProjectStats(null); + return; + } + + let cancelled = false; + const jobNo = selectedProject.job_no || selectedProject.official_project_code; + + Promise.all([ + fetchMaterialsSummary({ project_id: jobNo }).catch(() => ({ data: {} })), + api.get('/purchase-request/list', { params: { job_no: jobNo } }).catch(() => ({ data: {} })), + api.get('/files', { params: { job_no: jobNo } }).catch(() => ({ data: {} })), + ]).then(([materialsRes, purchaseRes, filesRes]) => { + if (cancelled) return; + + const materials = materialsRes.data; + const purchases = purchaseRes.data; + const files = filesRes.data; + + setProjectStats({ + bomCount: files.files?.length || files.total || 0, + totalMaterials: materials.total_count || materials.total || 0, + purchaseRequests: purchases.total || (Array.isArray(purchases.requests) ? purchases.requests.length : 0), + }); + }); + + return () => { cancelled = true; }; + }, [selectedProject]); + + // Build metric values + const getMetrics = useCallback(() => { + if (selectedProject && projectStats) { + return { + projects: { label: '프로젝트', value: 1, color: '#2563eb' }, + bom: { label: 'BOM 파일', value: projectStats.bomCount, color: '#16a34a' }, + materials: { label: '총 자재', value: projectStats.totalMaterials, color: '#9333ea' }, + purchase: { label: '구매요청', value: projectStats.purchaseRequests, color: '#d97706' }, + }; + } + + // System-wide fallback + const metrics = stats?.metrics || []; + const findMetric = (label) => { + const m = metrics.find(m => m.label.includes(label)); + return m ? m.value : 0; + }; + + return { + projects: { label: '전체 프로젝트', value: findMetric('프로젝트') || projects.length, color: '#2563eb' }, + bom: { label: 'BOM 파일', value: findMetric('업로드') || '-', color: '#16a34a' }, + materials: { label: '전체 자재', value: findMetric('자재') || '-', color: '#9333ea' }, + purchase: { label: '구매요청', value: findMetric('구매') || '-', color: '#d97706' }, + }; + }, [stats, projectStats, selectedProject, projects]); + + return { stats, projectStats, loading, getMetrics }; +} diff --git a/tkeg/web/src/theme.js b/tkeg/web/src/theme.js new file mode 100644 index 0000000..ef414c6 --- /dev/null +++ b/tkeg/web/src/theme.js @@ -0,0 +1,42 @@ +import { createTheme } from '@mui/material/styles'; + +const theme = createTheme({ + palette: { + primary: { main: '#2563eb' }, + success: { main: '#16a34a' }, + warning: { main: '#d97706' }, + background: { + default: '#f9fafb', + paper: '#ffffff', + }, + divider: '#e5e7eb', + text: { + primary: '#111827', + secondary: '#6b7280', + }, + }, + shape: { borderRadius: 8 }, + typography: { + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + h4: { fontWeight: 700 }, + h5: { fontWeight: 700 }, + h6: { fontWeight: 600 }, + }, + components: { + MuiCard: { + styleOverrides: { + root: { + boxShadow: '0 1px 3px rgba(0,0,0,0.08)', + border: '1px solid #e5e7eb', + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { textTransform: 'none', fontWeight: 600 }, + }, + }, + }, +}); + +export default theme;