248 lines
8.2 KiB
TypeScript
248 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Plus, X, Star } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import type { Department, DepartmentMember } from "@/types/department";
|
|
import * as departmentAPI from "@/lib/api/department";
|
|
|
|
interface DepartmentMembersProps {
|
|
companyCode: string;
|
|
selectedDepartment: Department | null;
|
|
}
|
|
|
|
/**
|
|
* 부서 인원 관리 컴포넌트
|
|
*/
|
|
export function DepartmentMembers({ companyCode, selectedDepartment }: DepartmentMembersProps) {
|
|
const [members, setMembers] = useState<DepartmentMember[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
const [searchUserId, setSearchUserId] = useState("");
|
|
|
|
// 부서원 목록 로드
|
|
useEffect(() => {
|
|
if (selectedDepartment) {
|
|
loadMembers();
|
|
}
|
|
}, [selectedDepartment]);
|
|
|
|
const loadMembers = async () => {
|
|
if (!selectedDepartment) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await departmentAPI.getDepartmentMembers(selectedDepartment.dept_code);
|
|
if (response.success && response.data) {
|
|
setMembers(response.data);
|
|
} else {
|
|
console.error("부서원 목록 로드 실패:", response.error);
|
|
setMembers([]);
|
|
}
|
|
} catch (error) {
|
|
console.error("부서원 목록 로드 실패:", error);
|
|
setMembers([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// 부서원 추가
|
|
const handleAddMember = async () => {
|
|
if (!searchUserId.trim() || !selectedDepartment) return;
|
|
|
|
try {
|
|
const response = await departmentAPI.addDepartmentMember(
|
|
selectedDepartment.dept_code,
|
|
searchUserId
|
|
);
|
|
|
|
if (response.success) {
|
|
setIsAddModalOpen(false);
|
|
setSearchUserId("");
|
|
loadMembers();
|
|
} else {
|
|
alert(response.error || "부서원 추가에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("부서원 추가 실패:", error);
|
|
alert("부서원 추가 중 오류가 발생했습니다.");
|
|
}
|
|
};
|
|
|
|
// 부서원 제거
|
|
const handleRemoveMember = async (userId: string) => {
|
|
if (!selectedDepartment) return;
|
|
if (!confirm("이 부서에서 사용자를 제외하시겠습니까?")) return;
|
|
|
|
try {
|
|
const response = await departmentAPI.removeDepartmentMember(
|
|
selectedDepartment.dept_code,
|
|
userId
|
|
);
|
|
|
|
if (response.success) {
|
|
loadMembers();
|
|
} else {
|
|
alert(response.error || "부서원 제거에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("부서원 제거 실패:", error);
|
|
alert("부서원 제거 중 오류가 발생했습니다.");
|
|
}
|
|
};
|
|
|
|
// 주 부서 설정
|
|
const handleSetPrimaryDepartment = async (userId: string) => {
|
|
if (!selectedDepartment) return;
|
|
|
|
try {
|
|
const response = await departmentAPI.setPrimaryDepartment(
|
|
selectedDepartment.dept_code,
|
|
userId
|
|
);
|
|
|
|
if (response.success) {
|
|
loadMembers();
|
|
} else {
|
|
alert(response.error || "주 부서 설정에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("주 부서 설정 실패:", error);
|
|
alert("주 부서 설정 중 오류가 발생했습니다.");
|
|
}
|
|
};
|
|
|
|
if (!selectedDepartment) {
|
|
return (
|
|
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card p-8 shadow-sm">
|
|
<p className="text-sm text-muted-foreground">좌측에서 부서를 선택하세요</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-semibold">{selectedDepartment.dept_name}</h3>
|
|
<p className="text-sm text-muted-foreground">부서원 {members.length}명</p>
|
|
</div>
|
|
<Button size="sm" className="h-9 gap-2 text-sm" onClick={() => setIsAddModalOpen(true)}>
|
|
<Plus className="h-4 w-4" />
|
|
부서원 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 부서원 목록 */}
|
|
<div className="space-y-2 rounded-lg border bg-card p-4 shadow-sm">
|
|
{isLoading ? (
|
|
<div className="py-8 text-center text-sm text-muted-foreground">로딩 중...</div>
|
|
) : members.length === 0 ? (
|
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
부서원이 없습니다. 부서원을 추가해주세요.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{members.map((member) => (
|
|
<div
|
|
key={member.user_id}
|
|
className="flex items-center justify-between rounded-lg border bg-background p-3 transition-colors hover:bg-muted/50"
|
|
>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">{member.user_name}</span>
|
|
<span className="text-sm text-muted-foreground">({member.user_id})</span>
|
|
{member.is_primary && (
|
|
<Badge variant="default" className="h-5 gap-1 text-xs">
|
|
<Star className="h-3 w-3" />
|
|
주 부서
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="mt-1 flex gap-4 text-xs text-muted-foreground">
|
|
{member.position_name && <span>직책: {member.position_name}</span>}
|
|
{member.email && <span>이메일: {member.email}</span>}
|
|
{member.phone && <span>전화: {member.phone}</span>}
|
|
{member.cell_phone && <span>휴대폰: {member.cell_phone}</span>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
{!member.is_primary && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-8 gap-1 text-xs"
|
|
onClick={() => handleSetPrimaryDepartment(member.user_id)}
|
|
>
|
|
<Star className="h-3 w-3" />
|
|
주 부서 설정
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive"
|
|
onClick={() => handleRemoveMember(member.user_id)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 부서원 추가 모달 */}
|
|
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
|
<DialogContent className="sm:max-w-[425px]">
|
|
<DialogHeader>
|
|
<DialogTitle>부서원 추가</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="user_id">
|
|
사용자 ID <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="user_id"
|
|
value={searchUserId}
|
|
onChange={(e) => setSearchUserId(e.target.value)}
|
|
placeholder="사용자 ID를 입력하세요"
|
|
autoFocus
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
겸직이 가능합니다. 한 사용자를 여러 부서에 추가할 수 있습니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setIsAddModalOpen(false)}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleAddMember} disabled={!searchUserId.trim()}>
|
|
추가
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|