diff --git a/cypress/e2e/smoke.cy.ts b/cypress/e2e/smoke.cy.ts index 71c84ba..e9b7de5 100644 --- a/cypress/e2e/smoke.cy.ts +++ b/cypress/e2e/smoke.cy.ts @@ -22,6 +22,9 @@ describe("Smoke Tests", () => { cy.visit("/orders"); cy.url().should("include", "/orders"); + cy.visit("/users-admin"); + cy.url().should("include", "/users-admin"); + cy.visit("/admin-users"); cy.url().should("include", "/admin-users"); diff --git a/cypress/e2e/users-admin.cy.ts b/cypress/e2e/users-admin.cy.ts new file mode 100644 index 0000000..383bfa1 --- /dev/null +++ b/cypress/e2e/users-admin.cy.ts @@ -0,0 +1,349 @@ +/// + +describe("Users Admin Management", () => { + beforeEach(() => { + cy.login(); + cy.visit("/users-admin"); + cy.waitForLoading(); + }); + + it("should display users admin list page", () => { + cy.contains("مدیریت کاربران").should("be.visible"); + cy.getByTestId("create-user-button").should("be.visible"); + }); + + it("should navigate to create user page", () => { + cy.getByTestId("create-user-button").click(); + cy.url().should("include", "/users-admin/create"); + cy.contains("ایجاد کاربر جدید").should("be.visible"); + }); + + it("should create a new user", () => { + cy.getByTestId("create-user-button").click(); + + // Fill basic information + cy.getByTestId("first-name-input").type("محمد"); + cy.getByTestId("last-name-input").type("احمدی"); + cy.getByTestId("phone-number-input").type("09123456789"); + cy.getByTestId("email-input").type("mohammad.ahmadi@example.com"); + cy.getByTestId("national-code-input").type("1234567890"); + cy.getByTestId("password-input").type("password123"); + + // Set verification status + cy.getByTestId("verified-true-radio").check(); + + // Submit form + cy.getByTestId("submit-button").click(); + + // Verify creation + cy.url().should("include", "/users-admin/"); + cy.url().should("not.include", "/create"); + }); + + it("should validate required fields", () => { + cy.getByTestId("create-user-button").click(); + + // Submit button should be disabled initially + cy.getByTestId("submit-button").should("be.disabled"); + + // Fill only first name + cy.getByTestId("first-name-input").type("محمد"); + cy.getByTestId("submit-button").should("be.disabled"); + + // Fill all required fields + cy.getByTestId("last-name-input").type("احمدی"); + cy.getByTestId("phone-number-input").type("09123456789"); + + // Now submit button should be enabled + cy.getByTestId("submit-button").should("not.be.disabled"); + }); + + it("should validate phone number format", () => { + cy.getByTestId("create-user-button").click(); + + // Test invalid phone number + cy.getByTestId("phone-number-input").type("123456"); + cy.getByTestId("first-name-input").type("محمد"); + cy.getByTestId("last-name-input").type("احمدی"); + + cy.get(".text-red-600").should("contain", "شماره تلفن معتبر نیست"); + + // Fix phone number + cy.getByTestId("phone-number-input").clear().type("09123456789"); + cy.get(".text-red-600").should("not.contain", "شماره تلفن معتبر نیست"); + }); + + it("should validate email format", () => { + cy.getByTestId("create-user-button").click(); + + // Test invalid email + cy.getByTestId("email-input").type("invalid-email"); + cy.getByTestId("first-name-input").type("محمد"); + + cy.get(".text-red-600").should("contain", "ایمیل معتبر نیست"); + + // Fix email + cy.getByTestId("email-input").clear().type("valid@example.com"); + cy.get(".text-red-600").should("not.contain", "ایمیل معتبر نیست"); + }); + + it("should search users", () => { + // Search by text + cy.getByTestId("search-users-input").type("محمد"); + cy.getByTestId("search-button").click(); + cy.wait(500); + + // Clear search + cy.getByTestId("clear-filters-button").click(); + cy.getByTestId("search-users-input").should("have.value", ""); + }); + + it("should filter users by status", () => { + // Filter by verified status + cy.getByTestId("status-filter-select").select("verified"); + cy.getByTestId("search-button").click(); + cy.wait(500); + + // Filter by unverified status + cy.getByTestId("status-filter-select").select("unverified"); + cy.getByTestId("search-button").click(); + cy.wait(500); + + // Reset filter + cy.getByTestId("status-filter-select").select("all"); + cy.getByTestId("search-button").click(); + }); + + it("should handle user verification toggle", () => { + // Mock API response for users list + cy.intercept("GET", "**/users**", { + statusCode: 200, + body: { + users: [ + { + id: 1, + phone_number: "+989123456789", + first_name: "محمد", + last_name: "احمدی", + email: "mohammad@example.com", + verified: false, + }, + ], + total: 1, + limit: 20, + offset: 0, + }, + }).as("getUsers"); + + // Mock verify API + cy.intercept("POST", "**/users/1/verify", { + statusCode: 200, + body: { message: "User verified successfully" }, + }).as("verifyUser"); + + cy.visit("/users-admin"); + cy.wait("@getUsers"); + + // Click verify button + cy.getByTestId("verify-user-1").click(); + cy.wait("@verifyUser"); + + // Check for success message + cy.contains("کاربر با موفقیت تأیید شد").should("be.visible"); + }); + + it("should view user details", () => { + // Mock API response + cy.intercept("GET", "**/users**", { + statusCode: 200, + body: { + users: [ + { + id: 1, + phone_number: "+989123456789", + first_name: "محمد", + last_name: "احمدی", + email: "mohammad@example.com", + verified: true, + }, + ], + }, + }).as("getUsers"); + + cy.intercept("GET", "**/users/1", { + statusCode: 200, + body: { + id: 1, + phone_number: "+989123456789", + first_name: "محمد", + last_name: "احمدی", + email: "mohammad@example.com", + verified: true, + }, + }).as("getUser"); + + cy.visit("/users-admin"); + cy.wait("@getUsers"); + + // Click view button + cy.getByTestId("view-user-1").click(); + cy.wait("@getUser"); + + cy.url().should("include", "/users-admin/1"); + cy.contains("جزئیات کاربر").should("be.visible"); + cy.contains("محمد احمدی").should("be.visible"); + }); + + it("should edit user", () => { + // Mock get user API + cy.intercept("GET", "**/users/1", { + statusCode: 200, + body: { + id: 1, + phone_number: "+989123456789", + first_name: "محمد", + last_name: "احمدی", + email: "mohammad@example.com", + verified: true, + }, + }).as("getUser"); + + // Mock update user API + cy.intercept("PUT", "**/users/1", { + statusCode: 200, + body: { + id: 1, + phone_number: "+989123456789", + first_name: "محمد", + last_name: "احمدی ویرایش شده", + email: "mohammad.updated@example.com", + verified: true, + }, + }).as("updateUser"); + + cy.visit("/users-admin/1/edit"); + cy.wait("@getUser"); + + // Edit user information + cy.getByTestId("last-name-input").clear().type("احمدی ویرایش شده"); + cy.getByTestId("email-input").clear().type("mohammad.updated@example.com"); + + // Submit form + cy.getByTestId("submit-button").click(); + cy.wait("@updateUser"); + + // Check for success message + cy.contains("کاربر با موفقیت به‌روزرسانی شد").should("be.visible"); + }); + + it("should delete user with confirmation", () => { + // Mock API responses + cy.intercept("GET", "**/users**", { + statusCode: 200, + body: { + users: [ + { + id: 1, + phone_number: "+989123456789", + first_name: "محمد", + last_name: "احمدی", + email: "mohammad@example.com", + verified: true, + }, + ], + }, + }).as("getUsers"); + + cy.intercept("DELETE", "**/users/1", { + statusCode: 200, + body: { message: "User deleted successfully" }, + }).as("deleteUser"); + + cy.visit("/users-admin"); + cy.wait("@getUsers"); + + // Click delete button + cy.getByTestId("delete-user-1").click(); + + // Confirm deletion in modal + cy.contains("آیا از حذف کاربر").should("be.visible"); + cy.contains("button", "حذف").click(); + cy.wait("@deleteUser"); + + // Check for success message + cy.contains("کاربر با موفقیت حذف شد").should("be.visible"); + }); + + it("should handle form cancellation", () => { + cy.getByTestId("create-user-button").click(); + + // Fill some data + cy.getByTestId("first-name-input").type("محمد"); + cy.getByTestId("last-name-input").type("احمدی"); + + // Click cancel + cy.getByTestId("cancel-button").click(); + + // Should return to list page + cy.url().should("include", "/users-admin"); + cy.url().should("not.include", "/create"); + }); + + it("should show empty state when no users found", () => { + // Mock empty users response + cy.intercept("GET", "**/users**", { + statusCode: 200, + body: { + users: [], + total: 0, + limit: 20, + offset: 0, + }, + }).as("getEmptyUsers"); + + cy.visit("/users-admin"); + cy.wait("@getEmptyUsers"); + + cy.contains("هیچ کاربری یافت نشد").should("be.visible"); + cy.contains("برای شروع یک کاربر ایجاد کنید").should("be.visible"); + }); + + it("should work on mobile viewport", () => { + cy.viewport("iphone-6"); + + cy.getByTestId("create-user-button").should("be.visible"); + cy.getByTestId("create-user-button").click(); + + cy.contains("ایجاد کاربر جدید").should("be.visible"); + + // Form should be usable on mobile + cy.getByTestId("first-name-input").type("محمد"); + cy.getByTestId("last-name-input").type("احمدی"); + cy.getByTestId("phone-number-input").type("09123456789"); + + cy.getByTestId("submit-button").should("be.visible"); + }); + + it("should be accessible", () => { + cy.getByTestId("create-user-button").click(); + + // Check for proper labels + cy.get("label").should("have.length.greaterThan", 5); + + // Check for required field indicators + cy.getByTestId("first-name-input").should( + "have.attr", + "aria-required", + "true" + ); + cy.getByTestId("last-name-input").should( + "have.attr", + "aria-required", + "true" + ); + + // Check for proper form structure + cy.get("form").should("exist"); + cy.get(".bg-gradient-to-r").should("have.length.greaterThan", 1); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index d3c52e4..7ea2a78 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,11 @@ const DiscountCodeFormPage = lazy(() => import('./pages/discount-codes/discount- const OrdersListPage = lazy(() => import('./pages/orders/orders-list/OrdersListPage')); const OrderDetailPage = lazy(() => import('./pages/orders/order-detail/OrderDetailPage')); +// Users Admin Pages +const UsersAdminListPage = lazy(() => import('./pages/users-admin/users-admin-list/UsersAdminListPage')); +const UserAdminDetailPage = lazy(() => import('./pages/users-admin/user-admin-detail/UserAdminDetailPage')); +const UserAdminFormPage = lazy(() => import('./pages/users-admin/user-admin-form/UserAdminFormPage')); + // Products Pages const ProductsListPage = lazy(() => import('./pages/products/products-list/ProductsListPage')); const ProductFormPage = lazy(() => import('./pages/products/product-form/ProductFormPage')); @@ -124,6 +129,12 @@ const AppRoutes = () => { } /> } /> + {/* Users Admin Routes */} + } /> + } /> + } /> + } /> + {/* Landing Hero Route */} } /> diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index f31ce86..e90ef5e 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -14,6 +14,7 @@ import { Sliders, BadgePercent, ShoppingCart, + Users, X } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; @@ -39,6 +40,16 @@ const menuItems: MenuItem[] = [ icon: ShoppingCart, path: '/orders', }, + { + title: 'مدیریت کاربران', + icon: Users, + path: '/users-admin', + }, + { + title: 'کدهای تخفیف', + icon: BadgePercent, + path: '/discount-codes', + }, { title: 'مدیریت محصولات', icon: Package, @@ -58,11 +69,6 @@ const menuItems: MenuItem[] = [ icon: Sliders, path: '/product-options', }, - { - title: 'کدهای تخفیف', - icon: BadgePercent, - path: '/discount-codes', - }, ] }, { diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 102de5e..66c874d 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { clsx } from 'clsx'; import { Label } from './Typography'; -import { persianToEnglish } from '../../utils/numberUtils'; +import { persianToEnglish, formatWithThousands } from '../../utils/numberUtils'; interface InputProps extends Omit, 'size'> { label?: string; @@ -10,10 +10,11 @@ interface InputProps extends Omit, ' inputSize?: 'sm' | 'md' | 'lg'; icon?: React.ComponentType<{ className?: string }>; numeric?: boolean; + thousandSeparator?: boolean; } export const Input = React.forwardRef( - ({ label, error, helperText, inputSize = 'md', className, id, onChange, type, numeric, ...props }, ref) => { + ({ label, error, helperText, inputSize = 'md', className, id, onChange, type, numeric, thousandSeparator, ...props }, ref) => { const sizeClasses = { sm: 'px-3 py-2 text-sm', md: 'px-3 py-3 text-base', @@ -32,9 +33,24 @@ export const Input = React.forwardRef( ); const handleChange = (e: React.ChangeEvent) => { - if ((type === 'number' || numeric) && e.target.value) { - const convertedValue = persianToEnglish(e.target.value); - e.target.value = convertedValue; + let value = e.target.value; + if ((type === 'number' || numeric) && value) { + value = persianToEnglish(value); + } + if (thousandSeparator) { + const caret = e.target.selectionStart || 0; + const prevLength = e.target.value.length; + value = formatWithThousands(value); + e.target.value = value; + const newLength = value.length; + const delta = newLength - prevLength; + requestAnimationFrame(() => { + try { + e.target.setSelectionRange(caret + delta, caret + delta); + } catch { } + }); + } else { + e.target.value = value; } onChange?.(e); }; @@ -49,7 +65,7 @@ export const Input = React.forwardRef( const inputProps = { ref, id, - type: numeric ? 'text' : type, + type: numeric || thousandSeparator ? 'text' : type, inputMode: getInputMode(), className: inputClasses, onChange: handleChange, diff --git a/src/constant/routes.ts b/src/constant/routes.ts index cf3a0d8..309158a 100644 --- a/src/constant/routes.ts +++ b/src/constant/routes.ts @@ -92,4 +92,16 @@ export const API_ROUTES = { GET_ORDERS: "checkout/orders", GET_ORDER: (id: string) => `checkout/orders/${id}`, UPDATE_ORDER_STATUS: (id: string) => `checkout/orders/${id}/status`, + + // User Admin APIs + GET_USERS: "api/v1/admin/users", + GET_USER: (id: string) => `api/v1/admin/users/${id}`, + SEARCH_USERS: "api/v1/admin/users/search", + CREATE_USER: "api/v1/admin/users", + UPDATE_USER: (id: string) => `api/v1/admin/users/${id}`, + UPDATE_USER_PROFILE: (id: string) => `api/v1/admin/users/${id}/profile`, + UPDATE_USER_AVATAR: (id: string) => `api/v1/admin/users/${id}/avatar`, + DELETE_USER: (id: string) => `api/v1/admin/users/${id}`, + VERIFY_USER: (id: string) => `api/v1/admin/users/${id}/verify`, + UNVERIFY_USER: (id: string) => `api/v1/admin/users/${id}/unverify`, }; diff --git a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx index 8479543..2814cc5 100644 --- a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx +++ b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; +import { parseFormattedNumber } from '@/utils/numberUtils'; import { useDiscountCode, useCreateDiscountCode, useUpdateDiscountCode } from '../core/_hooks'; import { CreateDiscountCodeRequest } from '../core/_models'; import { Button } from "@/components/ui/Button"; @@ -19,8 +20,16 @@ const schema = yup.object({ value: yup.number().typeError('مقدار نامعتبر است').required('مقدار الزامی است').min(0.01, 'مقدار باید بیشتر از صفر باشد'), status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'), application_level: yup.mixed<'invoice' | 'category' | 'product' | 'shipping' | 'product_fee'>().oneOf(['invoice', 'category', 'product', 'shipping', 'product_fee']).required('سطح اعمال الزامی است'), - min_purchase_amount: yup.number().transform((v, o) => o === '' ? undefined : v).min(0.01, 'مبلغ باید بیشتر از صفر باشد').nullable(), - max_discount_amount: yup.number().transform((v, o) => o === '' ? undefined : v).min(0.01, 'مبلغ باید بیشتر از صفر باشد').nullable(), + min_purchase_amount: yup + .number() + .transform((val, original) => parseFormattedNumber(original) as any) + .min(0.01, 'مبلغ باید بیشتر از صفر باشد') + .nullable(), + max_discount_amount: yup + .number() + .transform((val, original) => parseFormattedNumber(original) as any) + .min(0.01, 'مبلغ باید بیشتر از صفر باشد') + .nullable(), usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').nullable(), user_usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').nullable(), single_use: yup.boolean().required('این فیلد الزامی است'), @@ -227,6 +236,8 @@ const DiscountCodeFormPage = () => { step="0.01" placeholder="100000" error={errors.min_purchase_amount?.message as string} + thousandSeparator + numeric {...register('min_purchase_amount')} /> { step="0.01" placeholder="50000" error={errors.max_discount_amount?.message as string} + thousandSeparator + numeric {...register('max_discount_amount')} /> diff --git a/src/pages/discount-codes/discount-codes-list/DiscountCodesListPage.tsx b/src/pages/discount-codes/discount-codes-list/DiscountCodesListPage.tsx index 14516aa..92ba913 100644 --- a/src/pages/discount-codes/discount-codes-list/DiscountCodesListPage.tsx +++ b/src/pages/discount-codes/discount-codes-list/DiscountCodesListPage.tsx @@ -1,43 +1,13 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDiscountCodes, useDeleteDiscountCode } from '../core/_hooks'; import { DiscountCode } from '../core/_models'; import { Button } from "@/components/ui/Button"; import { Modal } from "@/components/ui/Modal"; +import { Table } from "@/components/ui/Table"; +import { TableColumn } from "@/types"; import { Percent, BadgePercent, Trash2, Edit3, Plus, Ticket } from 'lucide-react'; -const ListSkeleton = () => ( -
-
-
- - - - - - - - - - - - - {[...Array(5)].map((_, i) => ( - - {Array.from({ length: 6 }).map((__, j) => ( - - ))} - - ))} - -
کدنامنوعوضعیتبازه زمانیعملیات
-
-
-
-
-
-); - const DiscountCodesListPage = () => { const navigate = useNavigate(); const [deleteId, setDeleteId] = useState(null); @@ -55,6 +25,61 @@ const DiscountCodesListPage = () => { } }; + const columns: TableColumn[] = useMemo(() => [ + { key: 'code', label: 'کد', sortable: true }, + { key: 'name', label: 'نام', sortable: true }, + { + key: 'value', + label: 'مقدار', + render: (_val, row: any) => row.type === 'percentage' ? `${row.value}%` : `${row.value} تومان` + }, + { + key: 'application_level', + label: 'سطح', + render: (_val, row: any) => row.application_level === 'invoice' ? 'کل سبد' : row.application_level === 'category' ? 'دسته‌بندی' : row.application_level === 'product' ? 'محصول' : 'ارسال' + }, + { + key: 'status', + label: 'وضعیت', + render: (val: string) => ( + + {val === 'active' ? 'فعال' : 'غیرفعال'} + + ) + }, + { + key: 'period', + label: 'بازه زمانی', + render: (_val, row: any) => ( + + {row.valid_from ? new Date(row.valid_from).toLocaleDateString('fa-IR') : '-'} تا {row.valid_to ? new Date(row.valid_to).toLocaleDateString('fa-IR') : '-'} + + ) + }, + { + key: 'actions', + label: 'عملیات', + render: (_val, row: any) => ( +
+ + +
+ ) + } + ], [navigate]); + if (error) { return (
@@ -101,7 +126,7 @@ const DiscountCodesListPage = () => {
{isLoading ? ( - + ) : !discountCodes || discountCodes.length === 0 ? (
@@ -115,69 +140,7 @@ const DiscountCodesListPage = () => {
) : ( -
-
-
-
- - - - - - - - - - - - - {(discountCodes || []).map((dc: DiscountCode) => ( - - - - - - - - - - ))} - -
کدناممقدارسطحوضعیتبازه زمانیعملیات
{dc.code}{dc.name} - {dc.type === 'percentage' ? `${dc.value}%` : `${dc.value} تومان`} - - {dc.application_level === 'invoice' ? 'کل سبد' : - dc.application_level === 'category' ? 'دسته‌بندی' : - dc.application_level === 'product' ? 'محصول' : 'ارسال'} - - - {dc.status === 'active' ? 'فعال' : 'غیرفعال'} - - - {dc.valid_from ? new Date(dc.valid_from).toLocaleDateString('fa-IR') : '-'} - {' '}تا{' '} - {dc.valid_to ? new Date(dc.valid_to).toLocaleDateString('fa-IR') : '-'} - -
- - -
-
- - - + )} setDeleteId(null)} title="حذف کد تخفیف"> diff --git a/src/pages/orders/orders-list/OrdersListPage.tsx b/src/pages/orders/orders-list/OrdersListPage.tsx index 6c996d1..c792f61 100644 --- a/src/pages/orders/orders-list/OrdersListPage.tsx +++ b/src/pages/orders/orders-list/OrdersListPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks'; import { Order, OrderFilters, OrderStatus } from '../core/_models'; @@ -7,6 +7,8 @@ import { Input } from "@/components/ui/Input"; import { Modal } from "@/components/ui/Modal"; import { Pagination } from "@/components/ui/Pagination"; import { PageContainer, PageTitle, SectionTitle } from "@/components/ui/Typography"; +import { Table } from "@/components/ui/Table"; +import { TableColumn } from "@/types"; import { ShoppingCart, Package, @@ -53,23 +55,7 @@ const formatDate = (dateString: string) => { }; const ListSkeleton = () => ( -
-
-
- - {[...Array(5)].map((_, i) => ( - - {Array.from({ length: 7 }).map((__, j) => ( - - ))} - - ))} - -
-
-
- - + ); const OrdersListPage = () => { @@ -87,6 +73,45 @@ const OrdersListPage = () => { const { data: stats, isLoading: statsLoading } = useOrderStats(); const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus(); + const columns: TableColumn[] = useMemo(() => [ + { key: 'order_number', label: 'شماره سفارش', sortable: true, render: (v: string) => `#${v}` }, + { + key: 'customer', + label: 'مشتری', + render: (_val, row: any) => ( +
+
{row.customer.first_name} {row.customer.last_name}
+
{row.customer.email}
+
+ ) + }, + { key: 'total_amount', label: 'مبلغ', sortable: true, render: (v: number) => formatCurrency(v) }, + { key: 'status', label: 'وضعیت', render: (v: OrderStatus) => ({getStatusText(v)}) }, + { key: 'created_at', label: 'تاریخ', sortable: true, render: (v: string) => formatDate(v) }, + { + key: 'actions', + label: 'عملیات', + render: (_val, row: any) => ( +
+ + +
+ ) + }, + ], []); + const handleStatusUpdate = () => { if (statusUpdateId) { updateStatus( @@ -246,68 +271,7 @@ const OrdersListPage = () => { ) : ( <> -
-
-
- - - - - - - - - - - - {ordersData.orders.map((order: Order) => ( - - - - - - - - - ))} - -
شماره سفارشمشتریمبلغوضعیتتاریخعملیات
- #{order.order_number} - -
-
{order.customer.first_name} {order.customer.last_name}
-
{order.customer.email}
-
-
- {formatCurrency(order.total_amount)} - - - {getStatusText(order.status)} - - - {formatDate(order.created_at)} - -
- - -
-
- - - - {/* صفحه‌بندی */} + { + return useQuery({ + queryKey: [QUERY_KEY.GET_USERS, filters], + queryFn: () => getUsers(filters), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +}; + +// Get user by ID +export const useUser = (id: string) => { + return useQuery({ + queryKey: [QUERY_KEY.GET_USER, id], + queryFn: () => getUser(id), + enabled: !!id, + staleTime: 5 * 60 * 1000, + }); +}; + +// Search users with filters +export const useSearchUsers = (filters: UserFilters) => { + return useQuery({ + queryKey: [QUERY_KEY.SEARCH_USERS, filters], + queryFn: () => searchUsers(filters), + enabled: Object.keys(filters).length > 0, + staleTime: 2 * 60 * 1000, // 2 minutes for search results + }); +}; + +// Get user statistics +export const useUserStats = () => { + return useQuery({ + queryKey: [QUERY_KEY.USER_STATS], + queryFn: getUserStats, + staleTime: 10 * 60 * 1000, // 10 minutes + }); +}; + +// Create user mutation +export const useCreateUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (userData: CreateUserRequest) => createUser(userData), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_USERS] }); + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_STATS] }); + toast.success("کاربر با موفقیت ایجاد شد"); + }, + onError: (error: any) => { + const message = error?.response?.data?.message || "خطا در ایجاد کاربر"; + toast.error(message); + }, + }); +}; + +// Update user mutation +export const useUpdateUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + id, + userData, + }: { + id: string; + userData: UpdateUserRequest; + }) => updateUser(id, userData), + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_USERS] }); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.GET_USER, variables.id], + }); + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_STATS] }); + toast.success("کاربر با موفقیت به‌روزرسانی شد"); + }, + onError: (error: any) => { + const message = + error?.response?.data?.message || "خطا در به‌روزرسانی کاربر"; + toast.error(message); + }, + }); +}; + +// Update user profile mutation +export const useUpdateUserProfile = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + id, + userData, + }: { + id: string; + userData: UpdateUserProfileRequest; + }) => updateUserProfile(id, userData), + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_USERS] }); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.GET_USER, variables.id], + }); + toast.success("پروفایل کاربر با موفقیت به‌روزرسانی شد"); + }, + onError: (error: any) => { + const message = + error?.response?.data?.message || "خطا در به‌روزرسانی پروفایل"; + toast.error(message); + }, + }); +}; + +// Update user avatar mutation +export const useUpdateUserAvatar = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + id, + avatarData, + }: { + id: string; + avatarData: UpdateUserAvatarRequest; + }) => updateUserAvatar(id, avatarData), + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_USERS] }); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.GET_USER, variables.id], + }); + toast.success("تصویر کاربر با موفقیت به‌روزرسانی شد"); + }, + onError: (error: any) => { + const message = + error?.response?.data?.message || "خطا در به‌روزرسانی تصویر"; + toast.error(message); + }, + }); +}; + +// Delete user mutation +export const useDeleteUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => deleteUser(id), + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_USERS] }); + queryClient.removeQueries({ queryKey: [QUERY_KEY.GET_USER, variables] }); + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_STATS] }); + toast.success("کاربر با موفقیت حذف شد"); + }, + onError: (error: any) => { + const message = error?.response?.data?.message || "خطا در حذف کاربر"; + toast.error(message); + }, + }); +}; + +// Verify user mutation +export const useVerifyUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => verifyUser(id), + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_USERS] }); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.GET_USER, variables], + }); + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_STATS] }); + toast.success("کاربر با موفقیت تأیید شد"); + }, + onError: (error: any) => { + const message = error?.response?.data?.message || "خطا در تأیید کاربر"; + toast.error(message); + }, + }); +}; + +// Unverify user mutation +export const useUnverifyUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => unverifyUser(id), + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.GET_USERS] }); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.GET_USER, variables], + }); + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_STATS] }); + toast.success("تأیید کاربر لغو شد"); + }, + onError: (error: any) => { + const message = + error?.response?.data?.message || "خطا در لغو تأیید کاربر"; + toast.error(message); + }, + }); +}; diff --git a/src/pages/users-admin/core/_requests.ts b/src/pages/users-admin/core/_requests.ts new file mode 100644 index 0000000..c970598 --- /dev/null +++ b/src/pages/users-admin/core/_requests.ts @@ -0,0 +1,155 @@ +// User Admin API requests + +import { + httpGetRequest, + httpPostRequest, + httpPutRequest, + httpDeleteRequest, +} from "../../../utils/baseHttpService"; +import { APIUrlGenerator } from "../../../utils/baseHttpService"; +import { API_ROUTES } from "../../../constant/routes"; +import { + User, + PaginatedUsersResponse, + UserFilters, + CreateUserRequest, + UpdateUserRequest, + UpdateUserProfileRequest, + UpdateUserAvatarRequest, + UserActionResponse, + UserStats, +} from "./_models"; + +// Get all users with pagination +export const getUsers = async (filters?: UserFilters): Promise => { + const queryParams: Record = {}; + + if (filters?.limit) queryParams.limit = filters.limit; + if (filters?.offset) queryParams.offset = filters.offset; + + const response = await httpGetRequest( + APIUrlGenerator(API_ROUTES.GET_USERS, queryParams) + ); + + return response.data.users || []; +}; + +// Get user by ID +export const getUser = async (id: string): Promise => { + const response = await httpGetRequest(API_ROUTES.GET_USER(id)); + return response.data; +}; + +// Search users with filters +export const searchUsers = async ( + filters: UserFilters +): Promise => { + const queryParams: Record = {}; + + if (filters.verified !== undefined) queryParams.verified = filters.verified; + if (filters.search_text) queryParams.search_text = filters.search_text; + if (filters.phone_number) queryParams.phone_number = filters.phone_number; + if (filters.email) queryParams.email = filters.email; + if (filters.national_code) queryParams.national_code = filters.national_code; + if (filters.limit) queryParams.limit = filters.limit; + if (filters.offset) queryParams.offset = filters.offset; + + const response = await httpGetRequest( + APIUrlGenerator(API_ROUTES.SEARCH_USERS, queryParams) + ); + + return response.data; +}; + +// Create new user +export const createUser = async ( + userData: CreateUserRequest +): Promise => { + const response = await httpPostRequest( + API_ROUTES.CREATE_USER, + userData + ); + return response.data; +}; + +// Update user +export const updateUser = async ( + id: string, + userData: UpdateUserRequest +): Promise => { + const response = await httpPutRequest( + API_ROUTES.UPDATE_USER(id), + userData + ); + return response.data; +}; + +// Update user profile +export const updateUserProfile = async ( + id: string, + userData: UpdateUserProfileRequest +): Promise => { + const response = await httpPutRequest( + API_ROUTES.UPDATE_USER_PROFILE(id), + userData + ); + return response.data; +}; + +// Update user avatar +export const updateUserAvatar = async ( + id: string, + avatarData: UpdateUserAvatarRequest +): Promise => { + const response = await httpPutRequest( + API_ROUTES.UPDATE_USER_AVATAR(id), + avatarData + ); + return response.data; +}; + +// Delete user +export const deleteUser = async (id: string): Promise => { + const response = await httpDeleteRequest( + API_ROUTES.DELETE_USER(id) + ); + return response.data; +}; + +// Verify user +export const verifyUser = async (id: string): Promise => { + const response = await httpPostRequest( + API_ROUTES.VERIFY_USER(id), + {} + ); + return response.data; +}; + +// Unverify user +export const unverifyUser = async (id: string): Promise => { + const response = await httpPostRequest( + API_ROUTES.UNVERIFY_USER(id), + {} + ); + return response.data; +}; + +// Get user statistics +export const getUserStats = async (): Promise => { + const allUsers = await getUsers({ limit: 1000 }); + + const stats: UserStats = { + total_users: allUsers.length, + verified_users: allUsers.filter((user) => user.verified).length, + unverified_users: allUsers.filter((user) => !user.verified).length, + recent_registrations: allUsers.filter((user) => { + if (!user.created_at) return false; + const createdDate = new Date(user.created_at); + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + return createdDate > thirtyDaysAgo; + }).length, + }; + + return stats; +}; diff --git a/src/pages/users-admin/user-admin-detail/UserAdminDetailPage.tsx b/src/pages/users-admin/user-admin-detail/UserAdminDetailPage.tsx new file mode 100644 index 0000000..ce21420 --- /dev/null +++ b/src/pages/users-admin/user-admin-detail/UserAdminDetailPage.tsx @@ -0,0 +1,342 @@ +import React, { useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { User, Edit, UserCheck, UserX, Trash2, ArrowLeft, Phone, Mail, CreditCard, Calendar } from 'lucide-react'; +import { useUser, useVerifyUser, useUnverifyUser, useDeleteUser } from '../core/_hooks'; +import { PageContainer } from '../../../components/ui/Typography'; +import { Button } from '../../../components/ui/Button'; +import { Modal } from '../../../components/ui/Modal'; +import { LoadingSpinner } from '../../../components/ui/LoadingSpinner'; + +const UserAdminDetailPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const [deleteModal, setDeleteModal] = useState(false); + + // Hooks + const { data: user, isLoading, error } = useUser(id!); + const verifyUserMutation = useVerifyUser(); + const unverifyUserMutation = useUnverifyUser(); + const deleteUserMutation = useDeleteUser(); + + // Handlers + const handleEdit = () => { + navigate(`/users-admin/${id}/edit`); + }; + + const handleVerifyToggle = () => { + if (!user) return; + + if (user.verified) { + unverifyUserMutation.mutate(user.id.toString()); + } else { + verifyUserMutation.mutate(user.id.toString()); + } + }; + + const handleDeleteClick = () => { + setDeleteModal(true); + }; + + const handleDeleteConfirm = () => { + if (user) { + deleteUserMutation.mutate(user.id.toString(), { + onSuccess: () => { + navigate('/users-admin'); + } + }); + } + }; + + const handleBack = () => { + navigate('/users-admin'); + }; + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (error || !user) { + return ( + +
+

خطا در بارگذاری اطلاعات کاربر

+ +
+
+ ); + } + + return ( + +
+ {/* Header */} +
+
+ +
+

+ جزئیات کاربر +

+

+ مشاهده و مدیریت اطلاعات کاربر +

+
+
+
+ + + +
+
+ +
+ {/* User Profile Card */} +
+
+
+
+ {user.avatar ? ( + {`${user.first_name} + ) : ( +
+ + {user.first_name.charAt(0)}{user.last_name.charAt(0)} + +
+ )} +
+

+ {user.first_name} {user.last_name} +

+
+ + {user.verified ? 'تأیید شده' : 'تأیید نشده'} + +
+

+ شناسه کاربر: {user.id} +

+
+
+
+ + {/* User Details */} +
+
+
+

+ + اطلاعات شخصی +

+
+
+
+
+
+ +
+

شماره تلفن

+

{user.phone_number}

+
+
+ + {user.email && ( +
+ +
+

ایمیل

+

{user.email}

+
+
+ )} + + {user.national_code && ( +
+ +
+

کد ملی

+

{user.national_code}

+
+
+ )} +
+ +
+ {user.created_at && ( +
+ +
+

تاریخ ثبت‌نام

+

+ {new Date(user.created_at).toLocaleDateString('fa-IR')} +

+
+
+ )} + + {user.updated_at && ( +
+ +
+

آخرین به‌روزرسانی

+

+ {new Date(user.updated_at).toLocaleDateString('fa-IR')} +

+
+
+ )} + +
+ +
+

وضعیت حساب

+

+ {user.verified ? 'حساب تأیید شده است' : 'حساب تأیید نشده است'} +

+
+
+
+
+
+
+
+
+ + {/* Actions Section */} +
+

+ عملیات سریع +

+
+ + +
+
+ + {/* Delete Confirmation Modal */} + setDeleteModal(false)} + title="حذف کاربر" + > +
+

+ آیا از حذف کاربر "{user.first_name} {user.last_name}" اطمینان دارید؟ +

+
+

+ هشدار: این عمل غیرقابل بازگشت است و تمام اطلاعات کاربر به طور کامل حذف خواهد شد. +

+
+
+ + +
+
+
+
+
+ ); +}; + +export default UserAdminDetailPage; diff --git a/src/pages/users-admin/user-admin-form/UserAdminFormPage.tsx b/src/pages/users-admin/user-admin-form/UserAdminFormPage.tsx new file mode 100644 index 0000000..c360038 --- /dev/null +++ b/src/pages/users-admin/user-admin-form/UserAdminFormPage.tsx @@ -0,0 +1,322 @@ +import React, { useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import { User, ArrowLeft, Save, UserPlus } from 'lucide-react'; +import { useUser, useCreateUser, useUpdateUser } from '../core/_hooks'; +import { CreateUserRequest, UpdateUserRequest } from '../core/_models'; +import { PageContainer } from '../../../components/ui/Typography'; +import { Button } from '../../../components/ui/Button'; +import { Input } from '../../../components/ui/Input'; +import { LoadingSpinner } from '../../../components/ui/LoadingSpinner'; + +// Validation schema +const createUserSchema = yup.object({ + phone_number: yup + .string() + .required('شماره تلفن الزامی است') + .matches(/^(\+98|0)?9\d{9}$/, 'شماره تلفن معتبر نیست'), + first_name: yup + .string() + .required('نام الزامی است') + .min(1, 'نام باید حداقل ۱ کاراکتر باشد') + .max(100, 'نام نباید بیشتر از ۱۰۰ کاراکتر باشد'), + last_name: yup + .string() + .required('نام خانوادگی الزامی است') + .min(1, 'نام خانوادگی باید حداقل ۱ کاراکتر باشد') + .max(100, 'نام خانوادگی نباید بیشتر از ۱۰۰ کاراکتر باشد'), + email: yup + .string() + .email('ایمیل معتبر نیست') + .max(255, 'ایمیل نباید بیشتر از ۲۵۵ کاراکتر باشد') + .nullable(), + national_code: yup + .string() + .max(20, 'کد ملی نباید بیشتر از ۲۰ کاراکتر باشد') + .nullable(), + verified: yup.boolean().required('وضعیت تأیید الزامی است'), + password: yup + .string() + .min(6, 'رمز عبور باید حداقل ۶ کاراکتر باشد') + .max(100, 'رمز عبور نباید بیشتر از ۱۰۰ کاراکتر باشد') + .nullable(), +}); + +const updateUserSchema = yup.object({ + first_name: yup + .string() + .required('نام الزامی است') + .min(1, 'نام باید حداقل ۱ کاراکتر باشد') + .max(100, 'نام نباید بیشتر از ۱۰۰ کاراکتر باشد'), + last_name: yup + .string() + .required('نام خانوادگی الزامی است') + .min(1, 'نام خانوادگی باید حداقل ۱ کاراکتر باشد') + .max(100, 'نام خانوادگی نباید بیشتر از ۱۰۰ کاراکتر باشد'), + email: yup + .string() + .email('ایمیل معتبر نیست') + .max(255, 'ایمیل نباید بیشتر از ۲۵۵ کاراکتر باشد') + .nullable(), + national_code: yup + .string() + .max(20, 'کد ملی نباید بیشتر از ۲۰ کاراکتر باشد') + .nullable(), + verified: yup.boolean().required('وضعیت تأیید الزامی است'), +}); + +type FormData = CreateUserRequest | UpdateUserRequest; + +const UserAdminFormPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const isEdit = !!id; + + // Hooks + const { data: user, isLoading: userLoading } = useUser(id!, { enabled: isEdit }); + const createUserMutation = useCreateUser(); + const updateUserMutation = useUpdateUser(); + + const { + register, + handleSubmit, + formState: { errors, isValid }, + reset, + setValue, + } = useForm({ + resolver: yupResolver(isEdit ? updateUserSchema : createUserSchema), + mode: 'onChange', + defaultValues: { + verified: false, + }, + }); + + // Populate form in edit mode + useEffect(() => { + if (isEdit && user) { + setValue('first_name', user.first_name); + setValue('last_name', user.last_name); + setValue('email', user.email || ''); + setValue('national_code', user.national_code || ''); + setValue('verified', user.verified); + } + }, [isEdit, user, setValue]); + + // Handlers + const onSubmit = (data: FormData) => { + if (isEdit && id) { + updateUserMutation.mutate( + { id, userData: data as UpdateUserRequest }, + { + onSuccess: () => { + navigate(`/users-admin/${id}`); + }, + } + ); + } else { + createUserMutation.mutate(data as CreateUserRequest, { + onSuccess: (newUser) => { + navigate(`/users-admin/${newUser.id}`); + }, + }); + } + }; + + const handleBack = () => { + if (isEdit) { + navigate(`/users-admin/${id}`); + } else { + navigate('/users-admin'); + } + }; + + const isLoading = createUserMutation.isPending || updateUserMutation.isPending; + + if (isEdit && userLoading) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+ {/* Header */} +
+ +
+

+ {isEdit ? : } + {isEdit ? 'ویرایش کاربر' : 'ایجاد کاربر جدید'} +

+

+ {isEdit ? 'ویرایش اطلاعات کاربر' : 'افزودن کاربر جدید به سیستم'} +

+
+
+ +
+ {/* اطلاعات اصلی */} +
+
+

+ + اطلاعات اصلی کاربر +

+
+
+
+ + + {!isEdit && ( + + )} + + + {!isEdit && ( + + )} +
+
+
+ + {/* تنظیمات حساب */} +
+
+

+ + تنظیمات حساب +

+
+
+
+
+ +
+ + +
+ {errors.verified && ( +

+ {errors.verified.message} +

+ )} +
+
+
+
+ + {/* دکمه‌های اکشن */} +
+
+ + +
+
+ +
+
+ ); +}; + +export default UserAdminFormPage; diff --git a/src/pages/users-admin/users-admin-list/UsersAdminListPage.tsx b/src/pages/users-admin/users-admin-list/UsersAdminListPage.tsx new file mode 100644 index 0000000..c07730b --- /dev/null +++ b/src/pages/users-admin/users-admin-list/UsersAdminListPage.tsx @@ -0,0 +1,358 @@ +import React, { useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Users, Plus, Search, Filter, UserCheck, UserX, Edit, Trash2, Eye } from 'lucide-react'; +import { useUsers, useUserStats, useVerifyUser, useUnverifyUser, useDeleteUser } from '../core/_hooks'; +import { User, UserFilters } from '../core/_models'; +import { PageContainer } from '../../../components/ui/Typography'; +import { Button } from '../../../components/ui/Button'; +import { Input } from '../../../components/ui/Input'; +import { Modal } from '../../../components/ui/Modal'; +import { Pagination } from '../../../components/ui/Pagination'; +import { StatsCard } from '../../../components/dashboard/StatsCard'; +import { Table } from '../../../components/ui/Table'; +import { TableColumn } from '../../../types'; + +const UsersAdminListPage: React.FC = () => { + const navigate = useNavigate(); + + // State for filters and pagination + const [filters, setFilters] = useState({ + limit: 20, + offset: 0 + }); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedStatus, setSelectedStatus] = useState<'all' | 'verified' | 'unverified'>('all'); + const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; user: User | null }>({ + isOpen: false, + user: null + }); + + // Hooks + const { data: users = [], isLoading, error } = useUsers(filters); + const { data: stats } = useUserStats(); + const verifyUserMutation = useVerifyUser(); + const unverifyUserMutation = useUnverifyUser(); + const deleteUserMutation = useDeleteUser(); + + // Handlers + const handleCreate = () => { + navigate('/users-admin/create'); + }; + + const handleEdit = (user: User) => { + navigate(`/users-admin/${user.id}/edit`); + }; + + const handleView = (user: User) => { + navigate(`/users-admin/${user.id}`); + }; + + const handleSearch = () => { + const newFilters: UserFilters = { + ...filters, + offset: 0, + }; + + if (searchTerm.trim()) { + newFilters.search_text = searchTerm.trim(); + } else { + delete newFilters.search_text; + } + + if (selectedStatus !== 'all') { + newFilters.verified = selectedStatus === 'verified'; + } else { + delete newFilters.verified; + } + + setFilters(newFilters); + }; + + const handleClearFilters = () => { + setSearchTerm(''); + setSelectedStatus('all'); + setFilters({ + limit: 20, + offset: 0 + }); + }; + + const handleVerifyToggle = (user: User) => { + if (user.verified) { + unverifyUserMutation.mutate(user.id.toString()); + } else { + verifyUserMutation.mutate(user.id.toString()); + } + }; + + const handleDeleteClick = (user: User) => { + setDeleteModal({ isOpen: true, user }); + }; + + const handleDeleteConfirm = () => { + if (deleteModal.user) { + deleteUserMutation.mutate(deleteModal.user.id.toString()); + setDeleteModal({ isOpen: false, user: null }); + } + }; + + const handlePageChange = (newOffset: number) => { + setFilters(prev => ({ ...prev, offset: newOffset })); + }; + + const columns: TableColumn[] = useMemo(() => [ + { + key: 'name', + label: 'کاربر', + render: (_val, row: any) => ( +
+
+ {row.avatar ? ( + {`${row.first_name} + ) : ( +
+ + {row.first_name?.charAt(0)}{row.last_name?.charAt(0)} + +
+ )} +
+
+
+ {row.first_name} {row.last_name} +
+ {row.national_code && ( +
کد ملی: {row.national_code}
+ )} +
+
+ ) + }, + { key: 'phone_number', label: 'شماره تلفن' }, + { key: 'email', label: 'ایمیل', render: (v: string) => v || '-' }, + { + key: 'verified', + label: 'وضعیت', + render: (v: boolean) => ( + + {v ? 'تأیید شده' : 'تأیید نشده'} + + ) + }, + { + key: 'actions', + label: 'عملیات', + render: (_val, row: any) => ( +
+ + + + +
+ ) + } + ], [handleEdit, handleView, handleVerifyToggle]); + + if (error) { + return ( + +
+

خطا در بارگذاری کاربران

+
+
+ ); + } + + return ( + +
+ {/* Header */} +
+
+

+ + مدیریت کاربران +

+

مشاهده و مدیریت کاربران سیستم

+
+ +
+ + {/* Stats Cards */} + {stats && ( +
+ + + + +
+ )} + + {/* Filters */} +
+
+ setSearchTerm(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSearch()} + data-testid="search-users-input" + /> + +
+ + +
+
+
+ + {/* Users Table */} +
+ {isLoading ? ( +
+ ) : users.length === 0 ? ( +
+ +

+ هیچ کاربری یافت نشد +

+

+ برای شروع یک کاربر ایجاد کنید +

+ +
+ ) : ( +
+ )} + + + {/* Pagination */} + {users.length > 0 && ( + handlePageChange((page - 1) * (filters.limit || 20))} + /> + )} + + {/* Delete Confirmation Modal */} + setDeleteModal({ isOpen: false, user: null })} + title="حذف کاربر" + > +
+

+ آیا از حذف کاربر "{deleteModal.user?.first_name} {deleteModal.user?.last_name}" اطمینان دارید؟ +

+

+ این عمل غیرقابل بازگشت است. +

+
+ + +
+
+
+ + + ); +}; + +export default UsersAdminListPage; diff --git a/src/utils/numberUtils.ts b/src/utils/numberUtils.ts index 292497a..1d34a05 100644 --- a/src/utils/numberUtils.ts +++ b/src/utils/numberUtils.ts @@ -83,3 +83,22 @@ export const createOptionalNumberTransform = () => { return isNaN(num) ? null : num; }; }; + +export const formatWithThousands = (value: string | number): string => { + if (value === null || value === undefined) return ""; + const str = persianToEnglish(value.toString()); + if (str === "") return ""; + const parts = str.replace(/[^\d.]/g, "").split("."); + const integerPart = parts[0]; + const decimalPart = parts.length > 1 ? parts[1] : ""; + const withCommas = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return decimalPart ? `${withCommas}.${decimalPart}` : withCommas; +}; + +export const parseFormattedNumber = (value: any): number | undefined => { + if (value === null || value === undefined) return undefined; + const cleaned = persianToEnglish(String(value)).replace(/,/g, "").trim(); + if (cleaned === "") return undefined; + const num = Number(cleaned); + return isNaN(num) ? undefined : num; +}; diff --git a/src/utils/query-key.ts b/src/utils/query-key.ts index df4b742..007e904 100644 --- a/src/utils/query-key.ts +++ b/src/utils/query-key.ts @@ -83,4 +83,17 @@ export const QUERY_KEYS = { GET_ORDERS: "get_orders", GET_ORDER: "get_order", UPDATE_ORDER_STATUS: "update_order_status", + + // User Admin + GET_USERS: "get_users", + GET_USER: "get_user", + SEARCH_USERS: "search_users", + CREATE_USER: "create_user", + UPDATE_USER: "update_user", + UPDATE_USER_PROFILE: "update_user_profile", + UPDATE_USER_AVATAR: "update_user_avatar", + DELETE_USER: "delete_user", + VERIFY_USER: "verify_user", + UNVERIFY_USER: "unverify_user", + USER_STATS: "user_stats", };