feat: manager/admin can view/edit any employee's portfolio
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Building2, BadgeCheck, Calendar, Wrench, Ticket, FileText, Edit3, Save, X, Loader2 } from 'lucide-react';
|
||||
import { ArrowLeft, Building2, BadgeCheck, Calendar, Ticket, FileText, Edit3, Save, X, Loader2, Search, User } from 'lucide-react';
|
||||
import { useAuthStore } from '@/stores';
|
||||
import employeeService, { type PortfolioEntry } from '@/services/employeeService';
|
||||
import type { EmployeeSearchResult } from '@/types';
|
||||
|
||||
const initialColor = (name: string) => {
|
||||
const colors = ['from-emerald-400 to-emerald-500', 'from-orange-400 to-orange-500', 'from-blue-400 to-blue-500', 'from-pink-400 to-pink-500', 'from-purple-400 to-purple-500'];
|
||||
@@ -16,6 +17,13 @@ const initialColor = (name: string) => {
|
||||
export default function PortfolioPage() {
|
||||
const router = useRouter();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const isMgmt = user?.roleCode === 'MANAGER' || user?.roleCode === 'ADMIN';
|
||||
const [targetEmpId, setTargetEmpId] = useState<number | null>(null);
|
||||
const [targetName, setTargetName] = useState('');
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<EmployeeSearchResult[]>([]);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
const [portfolio, setPortfolio] = useState<PortfolioEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
@@ -23,16 +31,45 @@ export default function PortfolioPage() {
|
||||
const [editText, setEditText] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const loadPortfolio = (empId: number) => {
|
||||
setLoading(true);
|
||||
employeeService.getPortfolio(user.empId)
|
||||
setError('');
|
||||
employeeService.getPortfolio(empId)
|
||||
.then((res) => {
|
||||
setPortfolio(Array.isArray(res.data) ? res.data : []);
|
||||
})
|
||||
.catch(() => setError('포트폴리오를 불러오는데 실패했습니다.'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [user]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const id = targetEmpId || user.empId;
|
||||
setTargetName(targetEmpId ? targetName : user.name);
|
||||
loadPortfolio(id);
|
||||
}, [user, targetEmpId]);
|
||||
|
||||
// Employee search (MANAGER/ADMIN only)
|
||||
const handleSearch = (keyword: string) => {
|
||||
setSearchKeyword(keyword);
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
if (!keyword.trim()) { setSearchResults([]); return; }
|
||||
const t = setTimeout(async () => {
|
||||
try {
|
||||
const res = await employeeService.search(keyword);
|
||||
if (res.success) setSearchResults(res.data.slice(0, 10));
|
||||
} catch { setSearchResults([]); }
|
||||
}, 300);
|
||||
setSearchTimeout(t);
|
||||
};
|
||||
|
||||
const selectEmployee = (emp: EmployeeSearchResult) => {
|
||||
setTargetEmpId(emp.empId);
|
||||
setTargetName(emp.name);
|
||||
setShowSearch(false);
|
||||
setSearchKeyword('');
|
||||
setSearchResults([]);
|
||||
};
|
||||
|
||||
const handleEditAchievements = (entry: PortfolioEntry) => {
|
||||
setEditingId(entry.dispatchId);
|
||||
@@ -40,10 +77,11 @@ export default function PortfolioPage() {
|
||||
};
|
||||
|
||||
const handleSaveAchievements = async () => {
|
||||
if (!user || !editingId) return;
|
||||
const empId = targetEmpId || user?.empId;
|
||||
if (!empId || !editingId) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await employeeService.updateAchievements(user.empId, editingId, editText);
|
||||
await employeeService.updateAchievements(empId, editingId, editText);
|
||||
setPortfolio((prev) => prev.map((p) => p.dispatchId === editingId ? { ...p, achievements: editText } : p));
|
||||
setEditingId(null);
|
||||
} catch {
|
||||
@@ -65,14 +103,49 @@ export default function PortfolioPage() {
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<button onClick={() => router.back()} className="p-2 hover:bg-surface-container rounded-xl transition-colors">
|
||||
<ArrowLeft className="w-5 h-5 text-text-muted" />
|
||||
</button>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<h1 className="font-outfit text-2xl font-semibold text-text-main tracking-tight">경력 기술서</h1>
|
||||
<p className="text-text-muted text-sm mt-0.5">{user.name}님의 포트폴리오</p>
|
||||
<p className="text-text-muted text-sm mt-0.5">{targetName || user.name}님의 포트폴리오</p>
|
||||
</div>
|
||||
{isMgmt && (
|
||||
<div className="relative">
|
||||
<button onClick={() => setShowSearch(!showSearch)}
|
||||
className="flex items-center gap-2 bg-surface border border-border px-4 py-2.5 rounded-xl text-sm font-medium text-text-main hover:bg-surface-container transition-colors">
|
||||
<Search className="w-4 h-4" /> 직원 검색
|
||||
</button>
|
||||
{showSearch && (
|
||||
<div className="absolute right-0 top-full mt-2 w-72 bg-surface rounded-2xl border border-border shadow-xl z-50 overflow-hidden">
|
||||
<div className="p-3">
|
||||
<input value={searchKeyword} onChange={(e) => handleSearch(e.target.value)}
|
||||
placeholder="이름 또는 ID를 입력하세요..."
|
||||
className="w-full px-3 py-2 border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
autoFocus />
|
||||
</div>
|
||||
{searchResults.length > 0 && (
|
||||
<div className="pb-2">
|
||||
{searchResults.map((emp) => (
|
||||
<button key={emp.empId} onClick={() => selectEmployee(emp)}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-container-low transition-colors text-left">
|
||||
<User className="w-4 h-4 text-text-muted" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-main">{emp.name}</p>
|
||||
<p className="text-xs text-text-muted">{emp.roleCode} · {emp.email}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{searchKeyword && searchResults.length === 0 && (
|
||||
<p className="text-xs text-text-muted text-center pb-3">검색 결과가 없습니다.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
|
||||
Reference in New Issue
Block a user