From 68101a18112a9f2a6a4b343670549f62bab9a367 Mon Sep 17 00:00:00 2001 From: chpark <chpark@gdnsi.com> Date: Thu, 19 Dec 2024 18:33:03 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=A0=EA=B0=9D=EC=82=AC=20=ED=95=98?= =?UTF-8?q?=EB=8A=94=EC=A4=91=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/oemmng/components/UserForm.tsx | 369 ++++++++++++++++++ .../src/app/(admin)/common/oemmng/page.tsx | 332 ++++++++++++++++ plm-app/src/types/common/oemmng/oemmng.ts | 16 + 3 files changed, 717 insertions(+) create mode 100644 plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx create mode 100644 plm-app/src/app/(admin)/common/oemmng/page.tsx create mode 100644 plm-app/src/types/common/oemmng/oemmng.ts 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<typeof formSchema>; + +interface UserFormProps { + initialData?: Partial<User>; + onSubmit: (data: Partial<User>) => void; + onCancel: () => void; +} + +export const UserForm: React.FC<UserFormProps> = ({ + initialData, + onSubmit, + onCancel, +}) => { + const { token, user } = useAuthStore(); + const { toast } = useToast(); + + // Initialize the form with react-hook-form and zod resolver + const form = useForm<FormSchema>({ + 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<Role[]>({ + queryKey: ["roles", user?.companyId], + queryFn: async () => { + const { data } = await api.get<Role[]>( + `/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 ( + <Form {...form}> + <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="username" + render={({ field }) => ( + <FormItem> + <FormLabel>아이디</FormLabel> + <FormControl> + <Input {...field} value={field.value || ""} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {!initialData && ( + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>비밀번호</FormLabel> + <FormControl> + <Input type="password" {...field} value={field.value || ""} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>이름</FormLabel> + <FormControl> + <Input {...field} value={field.value || ""} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormLabel>이메일</FormLabel> + <FormControl> + <Input type="email" {...field} value={field.value || ""} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="phone" + render={({ field }) => ( + <FormItem> + <FormLabel>전화번호</FormLabel> + <FormControl> + <Input {...field} value={field.value || ""} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="role" + render={({ field }) => ( + <FormItem> + <FormLabel>역할</FormLabel> + <Select + onValueChange={field.onChange} + value={field.value || "user"} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="역할을 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {userRoles.map((role) => ( + <SelectItem key={role.value} value={role.value}> + {role.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="roleId" + render={({ field }) => ( + <FormItem> + <FormLabel>권한 그룹</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="권한 그룹을 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {roles?.map((role) => ( + <SelectItem key={role.id} value={role.id}> + {role.name} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="isActive" + render={({ field }) => ( + <FormItem className="flex items-center space-x-2"> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <FormLabel>활성화 여부</FormLabel> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="branchId" + render={({ field }) => ( + <FormItem> + <FormLabel>지점</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="지점을 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {branches?.map((branch) => ( + <SelectItem key={branch.id} value={branch.id}> + {branch.name} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="departmentId" + render={({ field }) => ( + <FormItem> + <FormLabel>부서</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="부서를 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {departments?.map((dept) => ( + <SelectItem key={dept.id} value={dept.id}> + {dept.name} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <div className="flex justify-end space-x-2"> + <Button type="button" variant="outline" onClick={onCancel}> + 취소 + </Button> + <Button type="submit">저장</Button> + </div> + </form> + </Form> + ); +}; 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<User | null>(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<PaginatedResponse>({ + 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<User>) => { + // Include companyId in the user data + const userWithCompanyId = { + ...newUser, + companyId: user?.companyId, + }; + + const { data } = await api.post<User>( + "/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<User>) => { + const { data } = await api.put<User>( + `/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<User>[] = [ + { + 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<string, string> = { + 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 }) => ( + <Switch + checked={row.original.isActive} + onCheckedChange={(value) => { + updateMutation.mutate({ id: row.original.id, isActive: value }); + }} + /> + ), + }, + { + id: "actions", + header: "액션", + cell: ({ row }) => ( + <div className="flex items-center gap-2"> + <Button + variant="ghost" + size="sm" + onClick={() => { + setEditingUser(row.original); + setIsOpen(true); + }} + > + <Edit className="h-4 w-4" /> + </Button> + <Button + variant="ghost" + size="sm" + onClick={() => { + if (confirm("정말 삭제하시겠습니까?")) { + deleteMutation.mutate(row.original.id); + } + }} + > + <Trash2 className="h-4 w-4" /> + </Button> + </div> + ), + }, + ]; + + if (isLoading) return <div>Loading...</div>; + + return ( + <div className="container mx-auto py-6"> + {/* Header */} + <div className="flex justify-between items-center mb-6"> + <div className="space-y-1"> + <h1 className="text-3xl font-bold">고객사 관리</h1> + <p className="text-muted-foreground"> + 고객사를 관리하고 권한을 설정합니다. + </p> + </div> + <Button onClick={() => setIsOpen(true)}> + <Plus className="mr-2 h-4 w-4" /> + 고객사 추가 + </Button> + </div> + + {/* Users Table */} + <Card> + <CardHeader> + <CardTitle>고객사 목록</CardTitle> + </CardHeader> + <CardContent> + {data?.users && data.users.length > 0 ? ( + <> + <DataTable + columns={columns} + data={data.users} + pagination={{ + pageIndex: page - 1, + pageSize, + pageCount: data.totalPages, + rowCount: data.total, + onPageChange: (newPage) => setPage(newPage + 1), + onPageSizeChange: handlePageSizeChange, + }} + /> + <div className="text-sm text-muted-foreground mt-2"> + 총 {data.total}명의 고객사 + </div> + </> + ) : ( + <div className="text-center py-12 text-muted-foreground"> + 등록된 고객사가 없습니다. + </div> + )} + </CardContent> + </Card> + + {/* User Create/Edit Dialog */} + <Dialog + open={isOpen} + onOpenChange={(open) => { + setIsOpen(open); + if (!open) setEditingUser(null); + }} + > + <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>{editingUser ? "고객사 수정" : "새 고객사"}</DialogTitle> + <DialogDescription> + {editingUser + ? "기존 고객사 정보를 수정합니다." + : "새로운 고객사를 생성합니다."} + </DialogDescription> + </DialogHeader> + <UserForm + initialData={editingUser || undefined} + onSubmit={(data) => { + if (editingUser) { + updateMutation.mutate({ id: editingUser.id, ...data }); + } else { + createMutation.mutate(data); + } + }} + onCancel={() => { + setIsOpen(false); + setEditingUser(null); + }} + /> + </DialogContent> + </Dialog> + </div> + ); +}; + +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