diff options
Diffstat (limited to 'lib/approval-template/table/category-management-dialog.tsx')
| -rw-r--r-- | lib/approval-template/table/category-management-dialog.tsx | 515 |
1 files changed, 515 insertions, 0 deletions
diff --git a/lib/approval-template/table/category-management-dialog.tsx b/lib/approval-template/table/category-management-dialog.tsx new file mode 100644 index 00000000..7f8b202d --- /dev/null +++ b/lib/approval-template/table/category-management-dialog.tsx @@ -0,0 +1,515 @@ +"use client" + +import * as React from "react" +import { Loader, Plus, Settings, Trash, Edit, Eye, EyeOff } from "lucide-react" +import { toast } from "sonner" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { Badge } from "@/components/ui/badge" + +import { type ApprovalTemplateCategory } from "../category-service" +import { + createApprovalTemplateCategory, + updateApprovalTemplateCategory, + deleteApprovalTemplateCategory, + getApprovalTemplateCategoryList, + getActiveApprovalTemplateCategories, +} from "../category-service" +import { + createApprovalTemplateCategorySchema, + updateApprovalTemplateCategorySchema, + type CreateApprovalTemplateCategorySchema, + type UpdateApprovalTemplateCategorySchema, +} from "../category-validations" + +interface CategoryManagementDialogProps extends React.ComponentPropsWithRef<typeof Dialog> { + showTrigger?: boolean + onSuccess?: () => void +} + +export function CategoryManagementDialog({ + showTrigger = true, + onSuccess, + ...props +}: CategoryManagementDialogProps) { + const [categories, setCategories] = React.useState<ApprovalTemplateCategory[]>([]) + const [isLoading, setIsLoading] = React.useState(false) + const [editingCategory, setEditingCategory] = React.useState<ApprovalTemplateCategory | null>(null) + const [deletingCategory, setDeletingCategory] = React.useState<ApprovalTemplateCategory | null>(null) + const [showCreateForm, setShowCreateForm] = React.useState(false) + + // 폼 상태 + const createForm = useForm<CreateApprovalTemplateCategorySchema>({ + resolver: zodResolver(createApprovalTemplateCategorySchema), + defaultValues: { + name: "", + description: "", + sortOrder: 0, + }, + }) + + const updateForm = useForm<UpdateApprovalTemplateCategorySchema>({ + resolver: zodResolver(updateApprovalTemplateCategorySchema), + defaultValues: { + name: "", + description: "", + isActive: true, + sortOrder: 0, + }, + }) + + // 카테고리 목록 로드 + const loadCategories = React.useCallback(async () => { + setIsLoading(true) + try { + const result = await getApprovalTemplateCategoryList({ + page: 1, + perPage: 100, // 충분히 큰 수로 모든 카테고리 로드 + sort: [ + { id: 'sortOrder', desc: false }, + { id: 'name', desc: false } + ], + }) + setCategories(result.data) + } catch (error) { + toast.error("카테고리 목록을 불러오는데 실패했습니다.") + } finally { + setIsLoading(false) + } + }, []) + + React.useEffect(() => { + if (props.open) { + loadCategories() + } + }, [props.open, loadCategories]) + + // 생성 핸들러 + const handleCreate = async (data: CreateApprovalTemplateCategorySchema) => { + try { + // 임시 사용자 ID (실제로는 세션에서 가져와야 함) + const userId = 1 // TODO: 실제 사용자 ID로 변경 + + await createApprovalTemplateCategory({ + ...data, + createdBy: userId, + }) + + toast.success("카테고리가 생성되었습니다.") + createForm.reset() + setShowCreateForm(false) + loadCategories() + onSuccess?.() + } catch (error) { + toast.error(error instanceof Error ? error.message : "카테고리 생성에 실패했습니다.") + } + } + + // 수정 핸들러 + const handleUpdate = async (data: UpdateApprovalTemplateCategorySchema) => { + if (!editingCategory) return + + try { + // 임시 사용자 ID (실제로는 세션에서 가져와야 함) + const userId = 1 // TODO: 실제 사용자 ID로 변경 + + await updateApprovalTemplateCategory(editingCategory.id, { + ...data, + updatedBy: userId, + }) + + toast.success("카테고리가 수정되었습니다.") + setEditingCategory(null) + updateForm.reset() + loadCategories() + onSuccess?.() + } catch (error) { + toast.error(error instanceof Error ? error.message : "카테고리 수정에 실패했습니다.") + } + } + + // 삭제 핸들러 + const handleDelete = async () => { + if (!deletingCategory) return + + try { + // 임시 사용자 ID (실제로는 세션에서 가져와야 함) + const userId = 1 // TODO: 실제 사용자 ID로 변경 + + const result = await deleteApprovalTemplateCategory(deletingCategory.id, userId) + if (result.error) { + toast.error(result.error) + } else { + toast.success("카테고리가 삭제되었습니다.") + loadCategories() + onSuccess?.() + } + } catch (error) { + toast.error("카테고리 삭제에 실패했습니다.") + } finally { + setDeletingCategory(null) + } + } + + // 수정 폼 열기 + const openEditForm = (category: ApprovalTemplateCategory) => { + setEditingCategory(category) + updateForm.reset({ + name: category.name, + description: category.description || "", + isActive: category.isActive, + sortOrder: category.sortOrder, + }) + } + + const trigger = showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm"> + <Settings className="mr-2 size-4" aria-hidden="true" /> + 카테고리 관리 + </Button> + </DialogTrigger> + ) : null + + return ( + <> + {trigger} + <Dialog {...props}> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>결재 템플릿 카테고리 관리</DialogTitle> + <DialogDescription> + 결재 템플릿의 카테고리를 관리합니다. 카테고리는 부서별로 분류하여 사용하세요. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 생성 폼 */} + {showCreateForm && ( + <div className="border rounded-lg p-4 space-y-4"> + <h3 className="text-lg font-semibold">새 카테고리 추가</h3> + <Form {...createForm}> + <form onSubmit={createForm.handleSubmit(handleCreate)} className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={createForm.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>카테고리 이름 *</FormLabel> + <FormControl> + <Input placeholder="카테고리 이름을 입력하세요" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={createForm.control} + name="sortOrder" + render={({ field }) => ( + <FormItem> + <FormLabel>정렬 순서</FormLabel> + <FormControl> + <Input + type="number" + placeholder="0" + {...field} + onChange={(e) => field.onChange(parseInt(e.target.value) || 0)} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + <FormField + control={createForm.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>설명</FormLabel> + <FormControl> + <Textarea + placeholder="카테고리에 대한 설명을 입력하세요" + className="min-h-[80px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <div className="flex gap-2"> + <Button type="submit" size="sm"> + <Plus className="mr-2 size-4" /> + 추가 + </Button> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => { + setShowCreateForm(false) + createForm.reset() + }} + > + 취소 + </Button> + </div> + </form> + </Form> + </div> + )} + + {/* 카테고리 목록 */} + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h3 className="text-lg font-semibold">카테고리 목록</h3> + <Button + variant="outline" + size="sm" + onClick={() => setShowCreateForm(!showCreateForm)} + > + <Plus className="mr-2 size-4" /> + {showCreateForm ? "취소" : "새 카테고리"} + </Button> + </div> + + {isLoading ? ( + <div className="flex items-center justify-center py-8"> + <Loader className="size-4 animate-spin" /> + <span className="ml-2">로딩 중...</span> + </div> + ) : ( + <Table> + <TableHeader> + <TableRow> + <TableHead>이름</TableHead> + <TableHead>설명</TableHead> + <TableHead>정렬순서</TableHead> + <TableHead>상태</TableHead> + <TableHead className="w-[120px]">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {categories.length === 0 ? ( + <TableRow> + <TableCell colSpan={5} className="text-center py-8 text-muted-foreground"> + 등록된 카테고리가 없습니다. + </TableCell> + </TableRow> + ) : ( + categories.map((category) => ( + <TableRow key={category.id}> + <TableCell className="font-medium">{category.name}</TableCell> + <TableCell className="max-w-[200px] truncate"> + {category.description || "-"} + </TableCell> + <TableCell>{category.sortOrder}</TableCell> + <TableCell> + <Badge variant={category.isActive ? "default" : "secondary"}> + {category.isActive ? ( + <> + <Eye className="mr-1 size-3" /> + 활성 + </> + ) : ( + <> + <EyeOff className="mr-1 size-3" /> + 비활성 + </> + )} + </Badge> + </TableCell> + <TableCell> + <div className="flex gap-1"> + <Button + variant="ghost" + size="sm" + onClick={() => openEditForm(category)} + > + <Edit className="size-4" /> + </Button> + <Button + variant="ghost" + size="sm" + onClick={() => setDeletingCategory(category)} + > + <Trash className="size-4" /> + </Button> + </div> + </TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + )} + </div> + + {/* 수정 폼 */} + {editingCategory && ( + <div className="border rounded-lg p-4 space-y-4"> + <h3 className="text-lg font-semibold">카테고리 수정</h3> + <Form {...updateForm}> + <form onSubmit={updateForm.handleSubmit(handleUpdate)} className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={updateForm.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>카테고리 이름 *</FormLabel> + <FormControl> + <Input placeholder="카테고리 이름을 입력하세요" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={updateForm.control} + name="sortOrder" + render={({ field }) => ( + <FormItem> + <FormLabel>정렬 순서</FormLabel> + <FormControl> + <Input + type="number" + placeholder="0" + {...field} + onChange={(e) => field.onChange(parseInt(e.target.value) || 0)} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + <FormField + control={updateForm.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>설명</FormLabel> + <FormControl> + <Textarea + placeholder="카테고리에 대한 설명을 입력하세요" + className="min-h-[80px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={updateForm.control} + name="isActive" + render={({ field }) => ( + <FormItem className="flex items-center space-x-2"> + <FormControl> + <input + type="checkbox" + checked={field.value} + onChange={field.onChange} + className="rounded" + /> + </FormControl> + <FormLabel>활성 상태</FormLabel> + <FormMessage /> + </FormItem> + )} + /> + <div className="flex gap-2"> + <Button type="submit" size="sm"> + <Edit className="mr-2 size-4" /> + 수정 + </Button> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => { + setEditingCategory(null) + updateForm.reset() + }} + > + 취소 + </Button> + </div> + </form> + </Form> + </div> + )} + </div> + + <DialogFooter> + <Button variant="outline" onClick={() => props.onOpenChange?.(false)}> + 닫기 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 삭제 확인 다이얼로그 */} + <AlertDialog open={!!deletingCategory} onOpenChange={() => setDeletingCategory(null)}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>카테고리 삭제</AlertDialogTitle> + <AlertDialogDescription> + "{deletingCategory?.name}" 카테고리를 삭제하시겠습니까? + 이 작업은 되돌릴 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"> + 삭제 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> + ) +} |
