고객사 하는중중

This commit is contained in:
chpark 2024-12-19 18:33:03 +09:00
parent 7b90ebd0d1
commit 68101a1811
3 changed files with 717 additions and 0 deletions

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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[];
}