diff --git a/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx b/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx new file mode 100644 index 0000000..e07aff1 --- /dev/null +++ b/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx @@ -0,0 +1,369 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +// import { Label } from "@/components/ui/label"; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/lib/api"; +import { useQuery } from "@tanstack/react-query"; +import { User, Role } from "@/types/user"; +import { useAuthStore } from "@/stores/auth"; +import { AxiosError } from "axios"; +import { useToast } from "@/hooks/use-toast"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +// Define the form schema with Zod +const formSchema = z.object({ + username: z.string().min(3, "아이디는 3자 이상이어야 합니다"), + password: z + .string() + .optional() + .refine((val) => !val || val.length >= 8, { + message: "비밀번호는 8자 이상이어야 합니다", + }), + name: z.string().min(2, "이름은 2자 이상이어야 합니다"), + email: z.string().email("올바른 이메일 형식이 아닙니다"), + phone: z.string().regex(/^[0-9-]+$/, "올바른 전화번호 형식이 아닙니다"), + role: z.enum(["company_admin", "branch_admin", "user"], { + required_error: "역할을 선택해주세요", + }), + roleId: z.string().uuid("올바른 권한 그룹을 선택해주세요"), + isActive: z.boolean(), + branchId: z.string().uuid("올바른 지점을 선택해주세요"), + departmentId: z.string().uuid("올바른 부서를 선택해주세요"), +}); + +type FormSchema = z.infer; + +interface UserFormProps { + initialData?: Partial; + onSubmit: (data: Partial) => void; + onCancel: () => void; +} + +export const UserForm: React.FC = ({ + initialData, + onSubmit, + onCancel, +}) => { + const { token, user } = useAuthStore(); + const { toast } = useToast(); + + // Initialize the form with react-hook-form and zod resolver + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + username: initialData?.username || "", + password: initialData?.password || "", + name: initialData?.name || "", + email: initialData?.email || "", + phone: initialData?.phone || "", + role: + (initialData?.role as "company_admin" | "branch_admin" | "user") || + "user", + roleId: initialData?.Roles?.[0]?.id || "", + isActive: initialData?.isActive || false, + branchId: initialData?.branchId || "", + departmentId: initialData?.departmentId || "", + }, + }); + + // Reset form when initialData changes + React.useEffect(() => { + if (initialData) { + form.reset({ + username: initialData.username || "", + password: initialData.password || "", + name: initialData.name || "", + email: initialData.email || "", + phone: initialData.phone || "", + role: + (initialData.role as "company_admin" | "branch_admin" | "user") || + "user", + roleId: initialData.Roles?.[0]?.id || "", + isActive: initialData.isActive || false, + branchId: initialData.branchId || "", + departmentId: initialData.departmentId || "", + }); + } + }, [initialData, form]); + + // Fetch available roles + const userRoles = [ + { value: "company_admin", label: "기업 관리자" }, + { value: "branch_admin", label: "지점 관리자" }, + { value: "user", label: "일반 유저" }, + ]; + + // Fetch branches + const { data: branches } = useQuery({ + queryKey: ["branches"], + queryFn: async () => { + const { data } = await api.get<{ id: string; name: string }[]>( + "/api/v1/admin/branches" + ); + return data; + }, + enabled: !!token, + }); + + // Fetch departments + const { data: departments } = useQuery({ + queryKey: ["departments", user?.companyId], + queryFn: async () => { + const { data } = await api.get<{ id: string; name: string }[]>( + `/api/v1/admin/departments/${user?.companyId}` + ); + return data; + }, + enabled: !!token && !!user?.companyId, + }); + + // Fetch roles + const { data: roles } = useQuery({ + queryKey: ["roles", user?.companyId], + queryFn: async () => { + const { data } = await api.get( + `/api/v1/admin/roles/${user?.companyId}` + ); + return data; + }, + enabled: !!token && !!user?.companyId, + }); + + const handleSubmit = async (data: FormSchema) => { + try { + await onSubmit(data); + } catch (error) { + const err = error as AxiosError; + toast({ + title: "에러", + description: + (err.response?.data as { message: string })?.message || + "에러가 발생했습니다.", + variant: "destructive", + }); + } + }; + + return ( +
+ + ( + + 아이디 + + + + + + )} + /> + + {!initialData && ( + ( + + 비밀번호 + + + + + + )} + /> + )} + + ( + + 이름 + + + + + + )} + /> + + ( + + 이메일 + + + + + + )} + /> + + ( + + 전화번호 + + + + + + )} + /> + + ( + + 역할 + + + + )} + /> + + ( + + 권한 그룹 + + + + )} + /> + + ( + + + + + 활성화 여부 + + + )} + /> + + ( + + 지점 + + + + )} + /> + + ( + + 부서 + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/plm-app/src/app/(admin)/common/oemmng/page.tsx b/plm-app/src/app/(admin)/common/oemmng/page.tsx new file mode 100644 index 0000000..b569e4e --- /dev/null +++ b/plm-app/src/app/(admin)/common/oemmng/page.tsx @@ -0,0 +1,332 @@ +// src/app/(admin)/users/accounts/page.tsx +"use client"; + +import React, { useState, useCallback } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { DataTable } from "@/components/ui/data-table"; +import { api } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; +import { Plus, Edit, Trash2 } from "lucide-react"; +import { ColumnDef } from "@tanstack/react-table"; +import { useAuthStore } from "@/stores/auth"; +import { AxiosError } from "axios"; +import { UserForm } from "./components/UserForm"; +import { Switch } from "@/components/ui/switch"; +import { User, PaginatedResponse } from "@/types/user"; + +const AccountsPage = () => { + const { token, user } = useAuthStore(); + const [isOpen, setIsOpen] = React.useState(false); + const [editingUser, setEditingUser] = React.useState(null); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // Fetch users with pagination + const { data, isLoading } = useQuery({ + queryKey: ["users", page, pageSize], + queryFn: async () => { + const { data } = await api.get("/api/v1/admin/users", { + params: { + page, + limit: pageSize, + }, + }); + return { + ...data, + users: data.users.map((user: User) => ({ + ...user, + Roles: user.Roles || [], + })), + }; + }, + enabled: !!token, + }); + + const handlePageSizeChange = useCallback((newPageSize: number) => { + setPageSize(newPageSize); + setPage(1); + }, []); + + // Create user mutation + const createMutation = useMutation({ + mutationFn: async (newUser: Partial) => { + // Include companyId in the user data + const userWithCompanyId = { + ...newUser, + companyId: user?.companyId, + }; + + const { data } = await api.post( + "/api/v1/admin/users", + userWithCompanyId + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + setIsOpen(false); + toast({ + title: "고객사 생성", + description: "새로운 고객사가 생성되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "고객사 생성 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Update user mutation + const updateMutation = useMutation({ + mutationFn: async (userData: Partial) => { + const { data } = await api.put( + `/api/v1/admin/users/${userData.id}`, + userData + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + setIsOpen(false); + setEditingUser(null); + toast({ + title: "고객사 수정", + description: "고객사 정보가 수정되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "고객사 수정 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Delete user mutation + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + await api.delete(`/api/v1/admin/users/${id}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + toast({ + title: "고객사 삭제", + description: "고객사가 삭제되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "고객사 삭제 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Table columns + const columns: ColumnDef[] = [ + { + id: "index", + header: "No", + cell: ({ row }) => row.original.index, + size: 60, + }, + // { + // accessorKey: "username", + // header: "아이디", + // }, + { + accessorKey: "name", + header: "업체명/고객사사", + }, + { + accessorKey: "Department.name", + header: "업체/고객사코드드", + cell: ({ row }) => row.original.Department?.name || "-", + }, + { + accessorKey: "email", + header: "이메일", + }, + // { + // accessorKey: "phone", + // header: "전화번호", + // }, + { + accessorKey: "role", + header: "역할", + cell: ({ row }) => { + const role = row.original.role; + const roleMap: Record = { + super_admin: "슈퍼 관리자", + company_admin: "기업 관리자", + branch_admin: "지점 관리자", + user: "일반 고객사", + }; + return roleMap[role] || role; + }, + }, + { + accessorKey: "roles", + header: "권한 그룹", + cell: ({ row }) => { + const roles = row.original.Roles; + return (roles ?? []).map((role) => role.name).join(", "); + }, + }, + // { + // accessorKey: "Company.name", + // header: "회사", + // cell: ({ row }) => row.original.Company?.name || "-", + // }, + { + accessorKey: "Branch.name", + header: "지점", + cell: ({ row }) => row.original.Branch?.name || "-", + }, + { + accessorKey: "isActive", + header: "활성화", + cell: ({ row }) => ( + { + updateMutation.mutate({ id: row.original.id, isActive: value }); + }} + /> + ), + }, + { + id: "actions", + header: "액션", + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]; + + if (isLoading) return
Loading...
; + + return ( +
+ {/* Header */} +
+
+

고객사 관리

+

+ 고객사를 관리하고 권한을 설정합니다. +

+
+ +
+ + {/* Users Table */} + + + 고객사 목록 + + + {data?.users && data.users.length > 0 ? ( + <> + setPage(newPage + 1), + onPageSizeChange: handlePageSizeChange, + }} + /> +
+ 총 {data.total}명의 고객사 +
+ + ) : ( +
+ 등록된 고객사가 없습니다. +
+ )} +
+
+ + {/* User Create/Edit Dialog */} + { + setIsOpen(open); + if (!open) setEditingUser(null); + }} + > + + + {editingUser ? "고객사 수정" : "새 고객사"} + + {editingUser + ? "기존 고객사 정보를 수정합니다." + : "새로운 고객사를 생성합니다."} + + + { + if (editingUser) { + updateMutation.mutate({ id: editingUser.id, ...data }); + } else { + createMutation.mutate(data); + } + }} + onCancel={() => { + setIsOpen(false); + setEditingUser(null); + }} + /> + + +
+ ); +}; + +export default AccountsPage; diff --git a/plm-app/src/types/common/oemmng/oemmng.ts b/plm-app/src/types/common/oemmng/oemmng.ts new file mode 100644 index 0000000..bcb0767 --- /dev/null +++ b/plm-app/src/types/common/oemmng/oemmng.ts @@ -0,0 +1,16 @@ +// src/types/menu.ts + +export interface oemMng { + id: string; + oem_code?: string | null; + oem_name?: string | null; + writer?: string | null; + regdate?: Date | null; + status?: string | null; + oem_no?: string | null; +} + +export interface oemMngResponse { + success: boolean; + data: oemMng[]; +} \ No newline at end of file