diff --git a/src/langs/fa.json b/src/langs/fa.json index a631a7e..cb41e95 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -736,7 +736,21 @@ "address": "آدرس", "image_profile": "تصویر پروفایل", "upload_image": "آپلود تصویر", - "new_password": "رمز عبور جدید" + "new_password": "رمز عبور جدید", + "import_excel": "ورود از Excel", + "download_sample": "دانلود فایل نمونه", + "import_file": "فایل Excel", + "import_submit": "بارگذاری و ورود", + "import_description": "فایل Excel باید شامل ستون‌های phone، firstName، lastName و gender باشد. حداکثر ۵۰۰ ردیف.", + "import_file_required": "لطفاً یک فایل Excel انتخاب کنید", + "import_success": "مشتریان با موفقیت وارد شدند", + "import_failed": "خطا در ورود مشتریان", + "import_total": "تعداد کل: {{count}}", + "import_created": "کاربران جدید: {{count}}", + "import_linked": "متصل‌شده به رستوران: {{count}}", + "import_already_linked": "قبلاً متصل بودند: {{count}}", + "import_errors": "{{count}} خطا", + "import_error_row": "ردیف {{row}}{{phone}}: {{message}}" }, "priority": "الویت", "messages": { diff --git a/src/pages/customers/List.tsx b/src/pages/customers/List.tsx index 60d2889..e99fcc3 100644 --- a/src/pages/customers/List.tsx +++ b/src/pages/customers/List.tsx @@ -1,8 +1,13 @@ import Table from '@/components/Table' import Filters, { type FieldType } from '@/components/Filters' -import { type FC, useMemo } from 'react' +import Button from '@/components/Button' +import { type FC, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { DocumentDownload, DocumentUpload } from 'iconsax-react' import { useGetUsers } from './hooks/useUsersData' import { useCustomerFilters } from './hooks/useCustomerFilters' +import ImportCustomersModal from './components/ImportCustomersModal' +import { downloadCustomerImportSampleExcel } from './utils/downloadCustomerImportSampleExcel' import type { CustomerListRow, UserRestaurant } from '@/pages/customers/types/Types' import { formatOptionalDate, formatFaNumber } from '@/helpers/func' @@ -22,6 +27,9 @@ const mapUserRestaurantToRow = (userRestaurant: UserRestaurant): CustomerListRow } const CustomersList: FC = () => { + const { t } = useTranslation('global') + const [isImportModalOpen, setIsImportModalOpen] = useState(false) + const { filters, currentPage, @@ -92,8 +100,30 @@ const CustomersList: FC = () => { return (
-
-

لیست مشتریان

+
+

{t('customer.customer_list')}

+
+ + +
@@ -116,6 +146,11 @@ const CustomersList: FC = () => { onPageChange: handlePageChange, }} /> + + setIsImportModalOpen(false)} + />
) } diff --git a/src/pages/customers/components/ImportCustomersModal.tsx b/src/pages/customers/components/ImportCustomersModal.tsx new file mode 100644 index 0000000..f4fad78 --- /dev/null +++ b/src/pages/customers/components/ImportCustomersModal.tsx @@ -0,0 +1,128 @@ +import { type FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DocumentDownload } from 'iconsax-react'; +import { toast } from 'react-toastify'; +import DefaulModal from '@/components/DefaulModal'; +import Button from '@/components/Button'; +import UploadBox from '@/components/UploadBox'; +import { extractErrorMessage } from '@/config/func'; +import { useImportUsersFromExcel } from '../hooks/useUsersData'; +import { downloadCustomerImportSampleExcel } from '../utils/downloadCustomerImportSampleExcel'; +import type { ImportUsersResult } from '../types/Types'; + +type Props = { + open: boolean; + onClose: () => void; +}; + +const ImportCustomersModal: FC = ({ open, onClose }) => { + const { t } = useTranslation('global'); + const [selectedFile, setSelectedFile] = useState(null); + const [importResult, setImportResult] = useState(null); + const [isResetUpload, setIsResetUpload] = useState(false); + + const { mutate: importUsers, isPending } = useImportUsersFromExcel(); + + const handleClose = () => { + setSelectedFile(null); + setImportResult(null); + setIsResetUpload(prev => !prev); + onClose(); + }; + + const handleImport = () => { + if (!selectedFile) { + toast.error(t('customer.import_file_required')); + return; + } + + importUsers(selectedFile, { + onSuccess: (response) => { + setImportResult(response); + toast.success(response.message || t('customer.import_success')); + }, + onError: (error) => { + toast.error(extractErrorMessage(error, t('customer.import_failed'))); + }, + }); + }; + + return ( + +
+

+ {t('customer.import_description')} +

+ + + + setSelectedFile(files[0] ?? null)} + isReset={isResetUpload} + /> + + {importResult && ( +
+

{t('customer.import_total', { count: importResult.total })}

+

{t('customer.import_created', { count: importResult.usersCreated })}

+

{t('customer.import_linked', { count: importResult.usersLinked })}

+

{t('customer.import_already_linked', { count: importResult.usersAlreadyLinked })}

+ + {importResult.errors.length > 0 && ( +
+

+ {t('customer.import_errors', { count: importResult.errors.length })} +

+
    + {importResult.errors.map((error, index) => ( +
  • + {t('customer.import_error_row', { + row: error.row, + message: error.message, + phone: error.phone ? ` (${error.phone})` : '', + })} +
  • + ))} +
+
+ )} +
+ )} + +
+
+
+
+ ); +}; + +export default ImportCustomersModal; diff --git a/src/pages/customers/hooks/useUsersData.ts b/src/pages/customers/hooks/useUsersData.ts index 89294ed..b814bb5 100644 --- a/src/pages/customers/hooks/useUsersData.ts +++ b/src/pages/customers/hooks/useUsersData.ts @@ -1,5 +1,5 @@ import * as api from "../service/UsersService"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { GetUsersParams } from "../types/Types"; export const useGetUsers = (params?: GetUsersParams) => { @@ -8,3 +8,14 @@ export const useGetUsers = (params?: GetUsersParams) => { queryFn: () => api.getUsers(params), }); }; + +export const useImportUsersFromExcel = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: api.importUsersFromExcel, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + }, + }); +}; diff --git a/src/pages/customers/service/UsersService.ts b/src/pages/customers/service/UsersService.ts index c72788b..2028671 100644 --- a/src/pages/customers/service/UsersService.ts +++ b/src/pages/customers/service/UsersService.ts @@ -2,6 +2,7 @@ import axios from "@/config/axios"; import type { GetCustomersResponse, GetUsersParams, + ImportUsersResponse, } from "@/pages/customers/types/Types"; export const getUsers = async (params?: GetUsersParams) => { @@ -10,3 +11,20 @@ export const getUsers = async (params?: GetUsersParams) => { }); return data; }; + +export const importUsersFromExcel = async (file: File) => { + const formData = new FormData(); + formData.append("file", file); + + const { data } = await axios.post( + `/admin/users/import`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + }, + ); + + return data.data; +}; diff --git a/src/pages/customers/types/Types.ts b/src/pages/customers/types/Types.ts index e532531..fac29d2 100644 --- a/src/pages/customers/types/Types.ts +++ b/src/pages/customers/types/Types.ts @@ -55,3 +55,20 @@ export type CustomerListRow = RowDataType & { totalOrderAmount: string; status: string; }; + +export type ImportUserRowError = { + row: number; + phone?: string; + message: string; +}; + +export type ImportUsersResult = { + message?: string; + total: number; + usersCreated: number; + usersLinked: number; + usersAlreadyLinked: number; + errors: ImportUserRowError[]; +}; + +export type ImportUsersResponse = IResponse; diff --git a/src/pages/customers/utils/downloadCustomerImportSampleExcel.ts b/src/pages/customers/utils/downloadCustomerImportSampleExcel.ts new file mode 100644 index 0000000..ab2ae68 --- /dev/null +++ b/src/pages/customers/utils/downloadCustomerImportSampleExcel.ts @@ -0,0 +1,24 @@ +import * as XLSX from 'xlsx'; + +const SAMPLE_ROWS = [ + { + phone: '9123456789', + firstName: 'علی', + lastName: 'محمدی', + gender: 'مرد', + }, + { + phone: '9129876543', + firstName: 'سارا', + lastName: 'احمدی', + gender: 'زن', + }, +]; + +export const downloadCustomerImportSampleExcel = () => { + const worksheet = XLSX.utils.json_to_sheet(SAMPLE_ROWS); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, 'customers'); + + XLSX.writeFile(workbook, 'customer-import-sample.xlsx'); +};