Create and update customer

This commit is contained in:
hamid zarghami
2026-02-25 16:14:39 +03:30
parent a7abf5db26
commit d5a6342158
8 changed files with 426 additions and 4 deletions
+2
View File
@@ -23,6 +23,8 @@ export const Paths = {
},
users: {
list: '/users/list',
create: '/users/create',
update: '/users/update/',
},
admin: {
list: '/admin/list',
+142
View File
@@ -0,0 +1,142 @@
import { useFormik } from "formik";
import { type FC } from "react";
import * as Yup from "yup";
import Button from "@/components/Button";
import { TickCircle } from "iconsax-react";
import Input from "@/components/Input";
import Textarea from "@/components/Textarea";
import SwitchComponent from "@/components/Switch";
import { useCreateUser } from "./hooks/useUserData";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { extractErrorMessage } from "@/config/func";
import { Paths } from "@/config/Paths";
import type { CreateUserType } from "./types/Types";
const initialValues: CreateUserType = {
phone: "",
firstName: "",
lastName: "",
address: "",
gender: true,
maxCredit: 0,
};
const validationSchema = Yup.object({
phone: Yup.string()
.required("شماره موبایل اجباری است")
.matches(/^09[0-9]{9}$/, "شماره موبایل معتبر نیست"),
firstName: Yup.string().required("نام اجباری است"),
lastName: Yup.string().required("نام خانوادگی اجباری است"),
address: Yup.string(),
maxCredit: Yup.number().min(0, "اعتبار نمی‌تواند منفی باشد"),
});
const CreateUser: FC = () => {
const navigate = useNavigate();
const { mutate: createUser, isPending } = useCreateUser();
const formik = useFormik<CreateUserType>({
initialValues,
validationSchema,
onSubmit: (values) => {
createUser(values, {
onSuccess: () => {
toast.success("مشتری با موفقیت ایجاد شد");
navigate(Paths.users.list);
},
onError: (error) => toast.error(extractErrorMessage(error)),
});
},
});
return (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">ساخت مشتری</h1>
<Button
className="w-fit px-6"
onClick={() => formik.handleSubmit()}
isLoading={isPending}
>
<div className="flex gap-1.5">
<TickCircle size={18} color="black" />
<div className="text-[13px] font-light">ذخیره</div>
</div>
</Button>
</div>
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
<div className="rowTwoInput">
<Input
label="موبایل"
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
{...formik.getFieldProps("phone")}
error_text={
formik.touched.phone && formik.errors.phone
? formik.errors.phone
: ""
}
/>
<Input
label="نام"
placeholder="نام"
{...formik.getFieldProps("firstName")}
error_text={
formik.touched.firstName && formik.errors.firstName
? formik.errors.firstName
: ""
}
/>
</div>
<div className="mt-6">
<Input
label="نام خانوادگی"
placeholder="نام خانوادگی"
{...formik.getFieldProps("lastName")}
error_text={
formik.touched.lastName && formik.errors.lastName
? formik.errors.lastName
: ""
}
/>
</div>
<div className="mt-6">
<Textarea
label="آدرس"
placeholder="آدرس"
{...formik.getFieldProps("address")}
/>
</div>
<div className="mt-6">
<label className="block text-xs text-description mb-1">جنسیت</label>
<SwitchComponent
active={formik.values.gender}
onChange={(value) => formik.setFieldValue("gender", value)}
label={formik.values.gender ? "مرد" : "زن"}
/>
</div>
<div className="mt-6">
<Input
label="حداکثر اعتبار (ریال)"
type="number"
min={0}
placeholder="۰"
{...formik.getFieldProps("maxCredit")}
error_text={
formik.touched.maxCredit && formik.errors.maxCredit
? formik.errors.maxCredit
: ""
}
/>
</div>
</div>
</div>
);
};
export default CreateUser;
+25 -2
View File
@@ -1,10 +1,13 @@
import { useState, type FC } from 'react'
import { Link } from 'react-router-dom'
import { useGetUsers } from './hooks/useUserData'
import Table from '@/components/Table'
import moment from 'moment-jalaali'
import Button from '@/components/Button'
import { AddSquare, Edit } from 'iconsax-react'
import { Paths } from '@/config/Paths'
const UsersList: FC = () => {
const [page, setPage] = useState<number>(1)
const { data } = useGetUsers(page)
@@ -12,6 +15,14 @@ const UsersList: FC = () => {
<div className='mt-5'>
<div className='flex justify-between items-center'>
<h1 className='text-lg font-light'>کاربران</h1>
<Link to={Paths.users.create}>
<Button className='w-fit px-6'>
<div className='flex gap-1.5'>
<AddSquare size={18} color='black' />
<div className='text-[13px] font-light'>ساخت مشتری</div>
</div>
</Button>
</Link>
</div>
<Table
@@ -42,6 +53,19 @@ const UsersList: FC = () => {
)
}
},
{
key: 'actions',
title: 'عملیات',
render: (item) => (
<Link
to={`${Paths.users.update}${item.id}`}
className="inline-block p-1 hover:opacity-70"
aria-label="ویرایش"
>
<Edit size={20} color="#0037FF" />
</Link>
),
},
]}
data={data?.data}
pagination={{
@@ -50,7 +74,6 @@ const UsersList: FC = () => {
totalPages: data?.meta?.totalPages as number
}}
/>
</div>
)
}
+189
View File
@@ -0,0 +1,189 @@
import { useFormik } from "formik";
import { type FC, useEffect } from "react";
import * as Yup from "yup";
import Button from "@/components/Button";
import { TickCircle } from "iconsax-react";
import Input from "@/components/Input";
import Textarea from "@/components/Textarea";
import SwitchComponent from "@/components/Switch";
import {
useGetUserById,
useUpdateUser,
} from "./hooks/useUserData";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { extractErrorMessage } from "@/config/func";
import { Paths } from "@/config/Paths";
import type { CreateUserType } from "./types/Types";
const initialValues: CreateUserType = {
phone: "",
firstName: "",
lastName: "",
address: "",
gender: true,
maxCredit: 0,
};
const validationSchema = Yup.object({
phone: Yup.string()
.required("شماره موبایل اجباری است")
.matches(/^09[0-9]{9}$/, "شماره موبایل معتبر نیست"),
firstName: Yup.string().required("نام اجباری است"),
lastName: Yup.string().required("نام خانوادگی اجباری است"),
address: Yup.string(),
maxCredit: Yup.number().min(0, "اعتبار نمی‌تواند منفی باشد"),
});
const UpdateUser: FC = () => {
const navigate = useNavigate();
const { id: userId } = useParams<{ id: string }>();
const { data: userData, isLoading: isLoadingUser } = useGetUserById(
userId ?? null,
!!userId
);
const { mutate: updateUser, isPending } = useUpdateUser();
const formik = useFormik<CreateUserType>({
initialValues,
validationSchema,
onSubmit: (values) => {
if (!userId) return;
updateUser(
{ userId, params: values },
{
onSuccess: () => {
toast.success("مشتری با موفقیت ویرایش شد");
navigate(Paths.users.list);
},
onError: (error) => toast.error(extractErrorMessage(error)),
}
);
},
});
useEffect(() => {
if (userData?.data) {
const u = userData.data;
formik.setValues({
phone: u.phone ?? "",
firstName: u.firstName ?? "",
lastName: u.lastName ?? "",
address: u.addresse ?? "",
gender: u.gender ?? true,
maxCredit: u.maxCredit ?? 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userData?.data]);
if (isLoadingUser) {
return (
<div className="mt-5">
<div className="flex justify-center items-center h-64">
<div className="text-lg">در حال بارگذاری...</div>
</div>
</div>
);
}
if (!userId || (!userData?.data && !isLoadingUser)) {
return (
<div className="mt-5">
<div className="flex justify-center items-center h-64">
<div className="text-lg text-red-500">مشتری یافت نشد</div>
</div>
</div>
);
}
return (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">ویرایش مشتری</h1>
<Button
className="w-fit px-6"
onClick={() => formik.handleSubmit()}
isLoading={isPending}
>
<div className="flex gap-1.5">
<TickCircle size={18} color="black" />
<div className="text-[13px] font-light">ذخیره</div>
</div>
</Button>
</div>
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
<div className="rowTwoInput">
<Input
label="موبایل"
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
{...formik.getFieldProps("phone")}
error_text={
formik.touched.phone && formik.errors.phone
? formik.errors.phone
: ""
}
/>
<Input
label="نام"
placeholder="نام"
{...formik.getFieldProps("firstName")}
error_text={
formik.touched.firstName && formik.errors.firstName
? formik.errors.firstName
: ""
}
/>
</div>
<div className="mt-6">
<Input
label="نام خانوادگی"
placeholder="نام خانوادگی"
{...formik.getFieldProps("lastName")}
error_text={
formik.touched.lastName && formik.errors.lastName
? formik.errors.lastName
: ""
}
/>
</div>
<div className="mt-6">
<Textarea
label="آدرس"
placeholder="آدرس"
{...formik.getFieldProps("address")}
/>
</div>
<div className="mt-6">
<label className="block text-xs text-description mb-1">جنسیت</label>
<SwitchComponent
active={formik.values.gender}
onChange={(value) => formik.setFieldValue("gender", value)}
label={formik.values.gender ? "مرد" : "زن"}
/>
</div>
<div className="mt-6">
<Input
label="حداکثر اعتبار (ریال)"
type="number"
min={0}
placeholder="۰"
{...formik.getFieldProps("maxCredit")}
error_text={
formik.touched.maxCredit && formik.errors.maxCredit
? formik.errors.maxCredit
: ""
}
/>
</div>
</div>
</div>
);
};
export default UpdateUser;
+31 -1
View File
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/UserService";
import type { CreateUserType } from "../types/Types";
export const useGetUsers = (page: number = 1) => {
return useQuery({
@@ -7,3 +8,32 @@ export const useGetUsers = (page: number = 1) => {
queryFn: () => api.getUsers(page),
});
};
export const useGetUserById = (userId: string | null, enabled: boolean) => {
return useQuery({
queryKey: ["users", userId],
queryFn: () => api.getUserById(userId!),
enabled: !!userId && enabled,
});
};
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
};
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, params }: { userId: string; params: CreateUserType }) =>
api.updateUser(userId, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
};
+22 -1
View File
@@ -1,5 +1,9 @@
import axios from "@/config/axios";
import type { UsersResponseType } from "../types/Types";
import type {
UsersResponseType,
UserDetailResponseType,
CreateUserType,
} from "../types/Types";
export const getUsers = async (page: number) => {
const { data } = await axios.get<UsersResponseType>(
@@ -15,3 +19,20 @@ export const searchUsers = async (search: string) => {
);
return data;
};
export const createUser = async (params: CreateUserType) => {
const { data } = await axios.post("/admin/users", params);
return data;
};
export const getUserById = async (userId: string) => {
const { data } = await axios.get<UserDetailResponseType>(
`/admin/users/${userId}`
);
return data;
};
export const updateUser = async (userId: string, params: CreateUserType) => {
const { data } = await axios.patch(`/admin/users/${userId}`, params);
return data;
};
+11
View File
@@ -15,3 +15,14 @@ export interface UserType extends RowDataType {
}
export type UsersResponseType = BaseResponse<UserType[]>;
export interface CreateUserType {
phone: string;
firstName: string;
lastName: string;
address: string;
gender: boolean;
maxCredit: number;
}
export type UserDetailResponseType = BaseResponse<UserType>;
+4
View File
@@ -43,6 +43,8 @@ import CreateSection from "@/pages/print/Create";
import UpdateSection from "@/pages/print/Update";
import PrintForm from "@/pages/print/Form";
import UsersList from "@/pages/user/List";
import CreateUser from "@/pages/user/Create";
import UpdateUser from "@/pages/user/Update";
import AdminList from "@/pages/admin/List";
import CreateAdmin from "@/pages/admin/Create";
import EditAdmin from "@/pages/admin/Update";
@@ -103,6 +105,8 @@ const MainRouter: FC = () => {
<Route path={Paths.print.form + ":itemId/:orderId"} element={<PrintForm />} />
<Route path={Paths.users.list} element={<UsersList />} />
<Route path={Paths.users.create} element={<CreateUser />} />
<Route path={Paths.users.update + ":id"} element={<UpdateUser />} />
<Route path={Paths.admin.list} element={<AdminList />} />
<Route path={Paths.admin.create} element={<CreateAdmin />} />