diff --git a/src/components/ui/VariantManager.tsx b/src/components/ui/VariantManager.tsx
index ffd11a0..6af85c6 100644
--- a/src/components/ui/VariantManager.tsx
+++ b/src/components/ui/VariantManager.tsx
@@ -101,6 +101,8 @@ const VariantForm: React.FC = ({ variant, onSave, onCancel, is
const [feePercentageDisplay, setFeePercentageDisplay] = useState(variant?.fee_percentage?.toString() || '');
const [profitPercentageDisplay, setProfitPercentageDisplay] = useState(variant?.profit_percentage?.toString() || '');
const [taxPercentageDisplay, setTaxPercentageDisplay] = useState(variant?.tax_percentage?.toString() || '');
+ const [goldPricePerGramDisplay, setGoldPricePerGramDisplay] = useState(variant?.gold_price_per_gram?.toString() || '');
+ const [factoryFeePercentageDisplay, setFactoryFeePercentageDisplay] = useState(variant?.factory_fee_percentage?.toString() || '');
const { mutateAsync: uploadFile } = useFileUpload();
const { mutate: deleteFile } = useFileDelete();
@@ -124,11 +126,17 @@ const VariantForm: React.FC = ({ variant, onSave, onCancel, is
if (variant?.tax_percentage !== undefined) {
setTaxPercentageDisplay(variant.tax_percentage.toString());
}
+ if (variant?.gold_price_per_gram !== undefined) {
+ setGoldPricePerGramDisplay(variant.gold_price_per_gram.toString());
+ }
+ if (variant?.factory_fee_percentage !== undefined) {
+ setFactoryFeePercentageDisplay(variant.factory_fee_percentage.toString());
+ }
// Load variant attribute value if exists
if (variantAttributeName && variant?.attributes && variant.attributes[variantAttributeName]) {
setVariantAttributeValue(variant.attributes[variantAttributeName].toString());
}
- }, [variant?.weight, variant?.fee_percentage, variant?.profit_percentage, variant?.tax_percentage, variant?.attributes, variantAttributeName]);
+ }, [variant?.weight, variant?.fee_percentage, variant?.profit_percentage, variant?.tax_percentage, variant?.gold_price_per_gram, variant?.factory_fee_percentage, variant?.attributes, variantAttributeName]);
const handleInputChange = (field: keyof ProductVariantFormData, value: any) => {
if (typeof value === 'string') {
@@ -309,6 +317,50 @@ const VariantForm: React.FC = ({ variant, onSave, onCancel, is
placeholder="مثال: ۱۲۰۰.۵"
/>
+
+
+
+ {
+ const converted = persianToEnglish(e.target.value);
+ setGoldPricePerGramDisplay(converted);
+ }}
+ onBlur={(e) => {
+ const converted = persianToEnglish(e.target.value);
+ const numValue = parseFloat(converted) || undefined;
+ handleInputChange('gold_price_per_gram', numValue);
+ }}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
+ placeholder="مثال: ۸۵۵۰۸۱۶"
+ />
+
+
+
+
+ {
+ const converted = persianToEnglish(e.target.value);
+ setFactoryFeePercentageDisplay(converted);
+ }}
+ onBlur={(e) => {
+ const converted = persianToEnglish(e.target.value);
+ const numValue = parseFloat(converted) || undefined;
+ handleInputChange('factory_fee_percentage', numValue);
+ }}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
+ placeholder="مثال: ۲"
+ />
+
diff --git a/src/constant/routes.ts b/src/constant/routes.ts
index 5efc491..335bde7 100644
--- a/src/constant/routes.ts
+++ b/src/constant/routes.ts
@@ -160,9 +160,27 @@ export const API_ROUTES = {
SALES_GROWTH_REPORT: "reports/sales/growth",
USER_REGISTRATION_GROWTH_REPORT: "reports/user-registration/growth",
SALES_BY_CATEGORY_REPORT: "reports/sales/by-category",
+ SALES_SUMMARY_REPORT: "reports/sales/summary",
+ PROFIT_LOSS_REPORT: "reports/profit-loss",
+ INVENTORY_VALUE_REPORT: "reports/inventory/value",
+ VARIANT_COMPARISON_REPORT: "reports/variants/comparison",
// Product Comments APIs
GET_PRODUCT_COMMENTS: "products/comments",
UPDATE_COMMENT_STATUS: (commentId: string) => `products/comments/${commentId}/status`,
DELETE_COMMENT: (commentId: string) => `products/comments/${commentId}`,
+
+ // Admin Notifications APIs
+ GET_ADMIN_NOTIFICATIONS: "notifications",
+ GET_ADMIN_NOTIFICATIONS_UNREAD: "notifications/unread",
+ GET_ADMIN_NOTIFICATIONS_COUNT: "notifications/count",
+ MARK_NOTIFICATION_READ: (id: string) => `notifications/${id}/read`,
+ MARK_ALL_NOTIFICATIONS_READ: "notifications/read-all",
+
+ // System Settings APIs
+ GET_AUTO_VERIFY_SETTING: "settings/auto-verify-new-users",
+ UPDATE_AUTO_VERIFY_SETTING: "settings/auto-verify-new-users",
+
+ // Wallet Credit APIs
+ WALLET_CREDIT: "wallet/credit",
};
diff --git a/src/index.css b/src/index.css
index a259ee3..958fa1e 100644
--- a/src/index.css
+++ b/src/index.css
@@ -145,4 +145,39 @@
min-height: 180px;
}
}
+
+ /* Sidebar scrollbar styles */
+ .sidebar-nav {
+ scrollbar-width: thin;
+ scrollbar-color: #d1d5db transparent;
+ }
+
+ .sidebar-nav::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .sidebar-nav::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ .sidebar-nav::-webkit-scrollbar-thumb {
+ background-color: #d1d5db;
+ border-radius: 3px;
+ }
+
+ .sidebar-nav::-webkit-scrollbar-thumb:hover {
+ background-color: #9ca3af;
+ }
+
+ .dark .sidebar-nav {
+ scrollbar-color: #4b5563 transparent;
+ }
+
+ .dark .sidebar-nav::-webkit-scrollbar-thumb {
+ background-color: #4b5563;
+ }
+
+ .dark .sidebar-nav::-webkit-scrollbar-thumb:hover {
+ background-color: #6b7280;
+ }
}
diff --git a/src/pages/Reports.tsx b/src/pages/Reports.tsx
index f0f9883..31c1586 100644
--- a/src/pages/Reports.tsx
+++ b/src/pages/Reports.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
-import { FileText, Download, TrendingUp, Users, ShoppingBag, DollarSign } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import { FileText, Download, TrendingUp, Users, ShoppingBag, DollarSign, Warehouse, TrendingDown, CreditCard, BarChart3 } from 'lucide-react';
import { Button } from '../components/ui/Button';
import { BarChart } from '../components/charts/BarChart';
import { lazy, Suspense } from 'react';
@@ -7,6 +8,7 @@ import { lazy, Suspense } from 'react';
const LineChart = lazy(() => import('../components/charts/LineChart').then(module => ({ default: module.LineChart })));
export const Reports = () => {
+ const navigate = useNavigate();
const [selectedPeriod, setSelectedPeriod] = useState('month');
const salesData = [
@@ -27,38 +29,70 @@ export const Reports = () => {
{ name: 'شهریور', value: 320 },
];
- const reports = [
+ const reportPages = [
{
id: 1,
- title: 'گزارش فروش ماهانه',
- description: 'گزارش کامل فروش محصولات در ماه گذشته',
- type: 'فروش',
- date: '۱۴۰۲/۰۸/۳۰',
- format: 'PDF'
+ title: 'گزارش خلاصه فروش',
+ description: 'گزارش جامع فروش محصولات با تفکیک و آمار کامل',
+ icon: DollarSign,
+ path: '/reports/sales-summary',
+ type: 'فروش'
},
{
id: 2,
- title: 'گزارش کاربران جدید',
- description: 'آمار کاربران جدید عضو شده در سیستم',
- type: 'کاربران',
- date: '۱۴۰۲/۰۸/۲۹',
- format: 'Excel'
+ title: 'گزارش ارزش موجودی',
+ description: 'ارزش کل موجودی به تومان بر اساس قیمت لحظهای',
+ icon: Warehouse,
+ path: '/reports/inventory-value',
+ type: 'موجودی'
},
{
id: 3,
- title: 'گزارش موجودی انبار',
- description: 'وضعیت موجودی محصولات در انبار',
- type: 'انبار',
- date: '۱۴۰۲/۰۸/۲۸',
- format: 'PDF'
+ title: 'گزارش روشهای پرداخت',
+ description: 'آمار و گزارش روشهای پرداخت استفاده شده',
+ icon: CreditCard,
+ path: '/reports/payment-methods',
+ type: 'پرداخت'
},
{
id: 4,
- title: 'گزارش درآمد روزانه',
- description: 'جزئیات درآمد حاصل از فروش در ۳۰ روز گذشته',
- type: 'مالی',
- date: '۱۴۰۲/۰۸/۲۷',
- format: 'Excel'
+ title: 'گزارش استفاده کاربر خاص از کدهای تخفیف',
+ description: 'گزارش استفاده یک کاربر خاص از کدهای تخفیف',
+ icon: Users,
+ path: '/reports/customer-discount-usage',
+ type: 'تخفیف'
+ },
+ {
+ id: 5,
+ title: 'گزارش مقایسه Variant',
+ description: 'مقایسه درصد اجرت کارخانه و درصد اجرت کاربر',
+ icon: TrendingUp,
+ path: '/reports/variant-comparison',
+ type: 'مقایسه'
+ },
+ {
+ id: 6,
+ title: 'گزارش سود و زیان',
+ description: 'گزارش سود یا زیان محقق شده',
+ icon: TrendingDown,
+ path: '/reports/profit-loss',
+ type: 'مالی'
+ },
+ {
+ id: 7,
+ title: 'گزارش کدهای تخفیف',
+ description: 'گزارش استفاده از کدهای تخفیف',
+ icon: BarChart3,
+ path: '/reports/discount-usage',
+ type: 'تخفیف'
+ },
+ {
+ id: 8,
+ title: 'گزارش ارسالها',
+ description: 'گزارش ارسالها بر اساس روش ارسال',
+ icon: ShoppingBag,
+ path: '/reports/shipments-by-method',
+ type: 'ارسال'
}
];
@@ -175,50 +209,36 @@ export const Reports = () => {
- گزارشهای اخیر
+ گزارشهای موجود
-
- {reports.map((report) => (
-
-
-
-
+
+ {reportPages.map((report) => {
+ const IconComponent = report.icon;
+ return (
+
navigate(report.path)}
+ className="flex items-start p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
+ >
+
+
-
-
+
+
{report.title}
{report.description}
-
-
- نوع: {report.type}
-
-
- تاریخ: {report.date}
-
-
- فرمت: {report.format}
-
-
+
+ {report.type}
+
-
-
- ))}
+ );
+ })}
diff --git a/src/pages/admin-notifications/core/_hooks.ts b/src/pages/admin-notifications/core/_hooks.ts
new file mode 100644
index 0000000..a4483d3
--- /dev/null
+++ b/src/pages/admin-notifications/core/_hooks.ts
@@ -0,0 +1,58 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { QUERY_KEYS } from "@/utils/query-key";
+import {
+ getAdminNotifications,
+ getAdminNotificationsUnread,
+ getAdminNotificationsCount,
+ markNotificationRead,
+ markAllNotificationsRead,
+} from "./_requests";
+import { AdminNotificationsFilters } from "./_models";
+
+export const useAdminNotifications = (filters?: AdminNotificationsFilters) => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS, filters],
+ queryFn: () => getAdminNotifications(filters),
+ });
+};
+
+export const useAdminNotificationsUnread = (limit?: number) => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS_UNREAD, limit],
+ queryFn: () => getAdminNotificationsUnread(limit),
+ });
+};
+
+export const useAdminNotificationsCount = () => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS_COUNT],
+ queryFn: () => getAdminNotificationsCount(),
+ refetchInterval: 30000,
+ });
+};
+
+export const useMarkNotificationRead = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (notificationId: number) => markNotificationRead(notificationId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS] });
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS_UNREAD] });
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS_COUNT] });
+ },
+ });
+};
+
+export const useMarkAllNotificationsRead = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: () => markAllNotificationsRead(),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS] });
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS_UNREAD] });
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_ADMIN_NOTIFICATIONS_COUNT] });
+ },
+ });
+};
diff --git a/src/pages/admin-notifications/core/_models.ts b/src/pages/admin-notifications/core/_models.ts
new file mode 100644
index 0000000..e81f9e2
--- /dev/null
+++ b/src/pages/admin-notifications/core/_models.ts
@@ -0,0 +1,44 @@
+export interface AdminNotification {
+ id: number;
+ admin_user_id: number;
+ title: string;
+ message: string;
+ metadata?: {
+ alert_type?: string;
+ error_msg?: string;
+ event_id?: string;
+ invoice_id?: string;
+ payment_id?: string;
+ retry_count?: string;
+ [key: string]: any;
+ };
+ is_read: boolean;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface AdminNotificationsResponse {
+ notifications: AdminNotification[];
+ unread_count: number;
+ offset: number;
+ limit: number;
+}
+
+export interface AdminNotificationsUnreadResponse {
+ notifications: AdminNotification[];
+ count: number;
+}
+
+export interface AdminNotificationsCountResponse {
+ unread_count: number;
+}
+
+export interface MarkNotificationReadResponse {
+ success: boolean;
+ message: string;
+}
+
+export interface AdminNotificationsFilters {
+ offset?: number;
+ limit?: number;
+}
diff --git a/src/pages/admin-notifications/core/_requests.ts b/src/pages/admin-notifications/core/_requests.ts
new file mode 100644
index 0000000..41d804e
--- /dev/null
+++ b/src/pages/admin-notifications/core/_requests.ts
@@ -0,0 +1,59 @@
+import { httpGetRequest, httpPutRequest, APIUrlGenerator } from "@/utils/baseHttpService";
+import { API_ROUTES } from "@/constant/routes";
+import {
+ AdminNotificationsResponse,
+ AdminNotificationsUnreadResponse,
+ AdminNotificationsCountResponse,
+ MarkNotificationReadResponse,
+ AdminNotificationsFilters,
+} from "./_models";
+
+export const getAdminNotifications = async (
+ filters?: AdminNotificationsFilters
+): Promise
=> {
+ const queryParams: Record = {};
+
+ if (filters?.offset !== undefined) queryParams.offset = filters.offset;
+ if (filters?.limit !== undefined) queryParams.limit = filters.limit;
+
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.GET_ADMIN_NOTIFICATIONS, queryParams)
+ );
+ return response.data;
+};
+
+export const getAdminNotificationsUnread = async (
+ limit?: number
+): Promise => {
+ const queryParams: Record = {};
+
+ if (limit !== undefined) queryParams.limit = limit;
+
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.GET_ADMIN_NOTIFICATIONS_UNREAD, queryParams)
+ );
+ return response.data;
+};
+
+export const getAdminNotificationsCount = async (): Promise => {
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.GET_ADMIN_NOTIFICATIONS_COUNT)
+ );
+ return response.data;
+};
+
+export const markNotificationRead = async (
+ notificationId: number
+): Promise => {
+ const response = await httpPutRequest(
+ APIUrlGenerator(API_ROUTES.MARK_NOTIFICATION_READ(notificationId.toString()))
+ );
+ return response.data;
+};
+
+export const markAllNotificationsRead = async (): Promise => {
+ const response = await httpPutRequest(
+ APIUrlGenerator(API_ROUTES.MARK_ALL_NOTIFICATIONS_READ)
+ );
+ return response.data;
+};
diff --git a/src/pages/admin-notifications/notifications-list/AdminNotificationsListPage.tsx b/src/pages/admin-notifications/notifications-list/AdminNotificationsListPage.tsx
new file mode 100644
index 0000000..ae3023f
--- /dev/null
+++ b/src/pages/admin-notifications/notifications-list/AdminNotificationsListPage.tsx
@@ -0,0 +1,251 @@
+import { useState } from 'react';
+import { Bell, BellOff, Check, CheckCheck, AlertCircle } from 'lucide-react';
+import { Button } from '@/components/ui/Button';
+import { Pagination } from '@/components/ui/Pagination';
+import { PageContainer, PageTitle } from '@/components/ui/Typography';
+import { useAdminNotifications, useMarkNotificationRead, useMarkAllNotificationsRead } from '../core/_hooks';
+import { AdminNotification } from '../core/_models';
+import { formatDateTime } from '@/utils/formatters';
+import { ReportSkeleton } from '@/components/common/ReportSkeleton';
+import { toast } from 'react-hot-toast';
+
+const AdminNotificationsListPage = () => {
+ const [filters, setFilters] = useState({ offset: 0, limit: 20 });
+ const [filterType, setFilterType] = useState<'all' | 'unread'>('all');
+
+ const { data, isLoading, error } = useAdminNotifications(filters);
+ const { mutate: markRead, isPending: isMarkingRead } = useMarkNotificationRead();
+ const { mutate: markAllRead, isPending: isMarkingAllRead } = useMarkAllNotificationsRead();
+
+ const notifications = data?.notifications || [];
+ const unreadCount = data?.unread_count || 0;
+ const totalPages = Math.ceil((data?.notifications?.length || 0) / filters.limit);
+
+ const filteredNotifications = filterType === 'unread'
+ ? notifications.filter(n => !n.is_read)
+ : notifications;
+
+ const handlePageChange = (page: number) => {
+ setFilters(prev => ({
+ ...prev,
+ offset: (page - 1) * prev.limit,
+ }));
+ };
+
+ const handleMarkAsRead = (notificationId: number) => {
+ markRead(notificationId, {
+ onSuccess: () => {
+ toast.success('نوتیفیکیشن به عنوان خوانده شده علامتگذاری شد');
+ },
+ onError: () => {
+ toast.error('خطا در علامتگذاری نوتیفیکیشن');
+ },
+ });
+ };
+
+ const handleMarkAllAsRead = () => {
+ markAllRead(undefined, {
+ onSuccess: () => {
+ toast.success('همه نوتیفیکیشنها به عنوان خوانده شده علامتگذاری شدند');
+ },
+ onError: () => {
+ toast.error('خطا در علامتگذاری نوتیفیکیشنها');
+ },
+ });
+ };
+
+ const getNotificationIcon = (notification: AdminNotification) => {
+ if (notification.metadata?.alert_type === 'failed_order_creation') {
+ return ;
+ }
+ return ;
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
خطا در بارگذاری نوتیفیکیشنها
+
+
+ );
+ }
+
+ return (
+
+
+
اعلانات ادمین
+
+ {unreadCount > 0 && (
+
+ )}
+
+
+
+
+
+
+
+
+
کل اعلانات
+
{notifications.length}
+
+
+
+
+
+
+
+
+
خوانده نشده
+
{unreadCount}
+
+
+
+
+
+
+
+
+
خوانده شده
+
+ {notifications.length - unreadCount}
+
+
+
+
+
+
+
+
+
+ لیست اعلانات
+
+
+
+
+
+
+ {filteredNotifications.length === 0 ? (
+
+
+
هیچ اعلانی یافت نشد
+
+ ) : (
+ filteredNotifications.map((notification) => (
+
+
+
+
+ {getNotificationIcon(notification)}
+
+
+
+
+
+ {notification.title}
+
+ {!notification.is_read && (
+
+ )}
+
+
+
+ {notification.message}
+
+
+ {notification.metadata && Object.keys(notification.metadata).length > 0 && (
+
+ {notification.metadata.invoice_id && (
+ شماره فاکتور: {notification.metadata.invoice_id}
+ )}
+ {notification.metadata.payment_id && (
+ شماره پرداخت: {notification.metadata.payment_id}
+ )}
+
+ )}
+
+
+ {formatDateTime(notification.created_at)}
+
+
+
+
+
+ {!notification.is_read && (
+
+ )}
+
+
+
+ ))
+ )}
+
+
+ {totalPages > 1 && (
+
+ )}
+
+
+
+ );
+};
+
+export default AdminNotificationsListPage;
diff --git a/src/pages/orders/core/_models.ts b/src/pages/orders/core/_models.ts
index f5e6077..6b98324 100644
--- a/src/pages/orders/core/_models.ts
+++ b/src/pages/orders/core/_models.ts
@@ -110,6 +110,7 @@ export interface Order {
national_code?: string;
verified: boolean;
avatar?: string;
+ is_deleted?: boolean;
};
payment_status?: PaymentStatus;
payments?: OrderPaymentRecord[];
diff --git a/src/pages/orders/orders-list/OrdersListPage.tsx b/src/pages/orders/orders-list/OrdersListPage.tsx
index c6d3010..c02b6ea 100644
--- a/src/pages/orders/orders-list/OrdersListPage.tsx
+++ b/src/pages/orders/orders-list/OrdersListPage.tsx
@@ -179,8 +179,13 @@ const OrdersListPage = () => {
align: 'right',
render: (_val, row: any) => (
-
+
{(row.user?.first_name || row.customer?.first_name || 'نامشخص')} {(row.user?.last_name || row.customer?.last_name || '')}
+ {row.user?.is_deleted && (
+
+ حذف شده
+
+ )}
{row.user?.phone_number ? englishToPersian(row.user.phone_number) : '-'}
diff --git a/src/pages/products/core/_models.ts b/src/pages/products/core/_models.ts
index 511b8c6..d175032 100644
--- a/src/pages/products/core/_models.ts
+++ b/src/pages/products/core/_models.ts
@@ -71,6 +71,7 @@ export interface ProductFormData {
explorer_file_ids?: ProductImage[];
is_delete_latest_explorer_files?: boolean;
product_cover_image_id?: string;
+ sku?: string;
}
export interface ProductVariantFormData {
@@ -86,6 +87,8 @@ export interface ProductVariantFormData {
attributes: Record
;
meta: Record;
file_ids: ProductImage[];
+ gold_price_per_gram?: number;
+ factory_fee_percentage?: number;
}
export interface ProductFilters {
@@ -111,6 +114,7 @@ export interface CreateProductRequest {
file_ids?: number[];
explorer_file_ids?: number[];
variants?: CreateVariantRequest[];
+ sku?: string;
}
export interface UpdateProductRequest {
@@ -128,6 +132,7 @@ export interface UpdateProductRequest {
explorer_file_ids?: number[];
is_delete_latest_explorer_files?: boolean;
variants?: UpdateVariantRequest[];
+ sku?: string;
}
export interface CreateVariantRequest {
@@ -143,6 +148,8 @@ export interface CreateVariantRequest {
attributes?: Record;
meta?: Record;
file_ids?: number[];
+ gold_price_per_gram?: number;
+ factory_fee_percentage?: number;
}
export interface UpdateVariantRequest {
@@ -158,6 +165,8 @@ export interface UpdateVariantRequest {
attributes?: Record;
meta?: Record;
file_ids?: number[];
+ gold_price_per_gram?: number;
+ factory_fee_percentage?: number;
}
export interface ProductsResponse {
diff --git a/src/pages/products/product-form/ProductFormPage.tsx b/src/pages/products/product-form/ProductFormPage.tsx
index 182a363..f34d1bb 100644
--- a/src/pages/products/product-form/ProductFormPage.tsx
+++ b/src/pages/products/product-form/ProductFormPage.tsx
@@ -39,6 +39,7 @@ const productSchema = yup.object({
explorer_file_ids: yup.array().of(yup.object()).default([]),
is_delete_latest_explorer_files: yup.boolean().optional(),
product_cover_image_id: yup.string().optional(),
+ sku: yup.string().optional(),
});
const toPublicUrl = (img: any): ProductImage => {
@@ -149,7 +150,8 @@ const ProductFormPage = () => {
variants: [],
explorer_file_ids: [],
is_delete_latest_explorer_files: false,
- product_cover_image_id: undefined
+ product_cover_image_id: undefined,
+ sku: undefined
}
});
@@ -187,7 +189,9 @@ const ProductFormPage = () => {
product_option_id: variant.product_option_id || undefined,
attributes: variant.attributes || {},
meta: variant.meta || {},
- file_ids: (variant.file_ids && variant.file_ids.length > 0 ? variant.file_ids : (variant as any).files || [])
+ file_ids: (variant.file_ids && variant.file_ids.length > 0 ? variant.file_ids : (variant as any).files || []),
+ gold_price_per_gram: variant.gold_price_per_gram || undefined,
+ factory_fee_percentage: variant.factory_fee_percentage || undefined
}));
console.log('✅ Successfully processed variants:', formVariants.length);
@@ -217,7 +221,8 @@ const ProductFormPage = () => {
product_option_id: product.product_option_id || undefined,
file_ids: (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []),
variants: formVariants,
- product_cover_image_id: (product as any).product_cover_image_id ? (product as any).product_cover_image_id.toString() : undefined
+ product_cover_image_id: (product as any).product_cover_image_id ? (product as any).product_cover_image_id.toString() : undefined,
+ sku: product.sku || undefined
});
const initialImages = (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []);
const normalizedImages: ProductImage[] = (initialImages || []).map(toPublicUrl);
@@ -335,6 +340,7 @@ const ProductFormPage = () => {
category_ids: convertedData.category_ids.length > 0 ? convertedData.category_ids : [],
product_option_id: convertedData.product_option_id || null,
file_ids: validImageIds,
+ sku: convertedData.sku || undefined,
};
const submitBaseData = {
@@ -349,7 +355,7 @@ const ProductFormPage = () => {
if (isEdit && id) {
// برای update، variants باید شامل ID باشه
const updateVariants = data.variants?.map((variant: any) => ({
- id: variant.id || 0, // اگر ID نداره، 0 بذار (برای variant جدید)
+ id: variant.id || 0,
enabled: variant.enabled,
fee_percentage: variant.fee_percentage,
profit_percentage: variant.profit_percentage,
@@ -360,7 +366,9 @@ const ProductFormPage = () => {
weight: variant.weight,
file_ids: Array.isArray(variant.file_ids) ? variant.file_ids.map((file: any) => Number(typeof file === 'object' ? file.id : file)).filter((id: number) => !isNaN(id)) : [],
attributes: variant.attributes && convertedData.variant_attribute_name && variant.attributes[convertedData.variant_attribute_name] !== undefined ? { [convertedData.variant_attribute_name]: variant.attributes[convertedData.variant_attribute_name] } : {},
- meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {}
+ meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {},
+ gold_price_per_gram: variant.gold_price_per_gram || undefined,
+ factory_fee_percentage: variant.factory_fee_percentage || undefined
})) || [];
const updatePayload = {
@@ -390,7 +398,9 @@ const ProductFormPage = () => {
weight: variant.weight,
file_ids: Array.isArray(variant.file_ids) ? variant.file_ids.map((file: any) => Number(typeof file === 'object' ? file.id : file)).filter((id: number) => !isNaN(id)) : [],
attributes: variant.attributes && convertedData.variant_attribute_name && variant.attributes[convertedData.variant_attribute_name] !== undefined ? { [convertedData.variant_attribute_name]: variant.attributes[convertedData.variant_attribute_name] } : {},
- meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {}
+ meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {},
+ gold_price_per_gram: variant.gold_price_per_gram || undefined,
+ factory_fee_percentage: variant.factory_fee_percentage || undefined
})) || [];
const createPayload: any = {
@@ -519,6 +529,13 @@ const ProductFormPage = () => {
placeholder="مثال: آبکاری، رنگ، سایز..."
/>
+
+
-
+
+
+
+
+
+
+
+
+ توجه: برای مشاهده گزارش، لطفاً شناسه کاربر را وارد کنید. این فیلد الزامی است.
+
@@ -135,7 +173,7 @@ const CustomerDiscountUsagePage = () => {
شناسه کاربر *
handleNumericFilterChange('user_id', e.target.value)}
placeholder="مثلاً 456"
numeric
@@ -148,8 +186,8 @@ const CustomerDiscountUsagePage = () => {
کد تخفیف
handleFilterChange('discount_code', e.target.value || undefined)}
+ value={tempFilters.discount_code || ''}
+ onChange={(e) => handleTempFilterChange('discount_code', e.target.value || undefined)}
placeholder="مثلاً SUMMER2025"
/>
@@ -159,7 +197,7 @@ const CustomerDiscountUsagePage = () => {
شناسه کد تخفیف
handleNumericFilterChange('discount_id', e.target.value)}
placeholder="مثلاً 123"
numeric
@@ -171,8 +209,8 @@ const CustomerDiscountUsagePage = () => {
تاریخ شروع
handleDateRangeChange(value, filters.date_range?.to)}
+ value={tempFilters.date_range?.from}
+ onChange={(value) => handleDateRangeChange(value, tempFilters.date_range?.to)}
placeholder="انتخاب تاریخ شروع"
/>
@@ -182,8 +220,8 @@ const CustomerDiscountUsagePage = () => {
تاریخ پایان
handleDateRangeChange(filters.date_range?.from, value)}
+ value={tempFilters.date_range?.to}
+ onChange={(value) => handleDateRangeChange(tempFilters.date_range?.from, value)}
placeholder="انتخاب تاریخ پایان"
/>
diff --git a/src/pages/reports/inventory-value/core/_hooks.ts b/src/pages/reports/inventory-value/core/_hooks.ts
new file mode 100644
index 0000000..bda48cb
--- /dev/null
+++ b/src/pages/reports/inventory-value/core/_hooks.ts
@@ -0,0 +1,10 @@
+import { useQuery } from "@tanstack/react-query";
+import { QUERY_KEYS } from "@/utils/query-key";
+import { getInventoryValueReport } from "./_requests";
+
+export const useInventoryValueReport = () => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.GET_INVENTORY_VALUE_REPORT],
+ queryFn: () => getInventoryValueReport(),
+ });
+};
diff --git a/src/pages/reports/inventory-value/core/_models.ts b/src/pages/reports/inventory-value/core/_models.ts
new file mode 100644
index 0000000..ef9de28
--- /dev/null
+++ b/src/pages/reports/inventory-value/core/_models.ts
@@ -0,0 +1,19 @@
+export interface FactoryFeeBreakdown {
+ factory_fee_percentage: number;
+ raw_weight: number;
+ weight_with_fee: number;
+ amount: number;
+ variants_count: number;
+ stock_quantity: number;
+}
+
+export interface InventoryValueResponse {
+ raw_inventory_amount: number;
+ raw_inventory_weight: number;
+ factory_fee_inventory_amount: number;
+ factory_fee_inventory_weight: number;
+ factory_fee_breakdown: FactoryFeeBreakdown[];
+ current_gold_price: number;
+ total_variants_count: number;
+ total_stock_quantity: number;
+}
diff --git a/src/pages/reports/inventory-value/core/_requests.ts b/src/pages/reports/inventory-value/core/_requests.ts
new file mode 100644
index 0000000..6569108
--- /dev/null
+++ b/src/pages/reports/inventory-value/core/_requests.ts
@@ -0,0 +1,10 @@
+import { httpGetRequest, APIUrlGenerator } from "@/utils/baseHttpService";
+import { API_ROUTES } from "@/constant/routes";
+import { InventoryValueResponse } from "./_models";
+
+export const getInventoryValueReport = async (): Promise => {
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.INVENTORY_VALUE_REPORT)
+ );
+ return response.data;
+};
diff --git a/src/pages/reports/inventory-value/inventory-value-report/InventoryValueReportPage.tsx b/src/pages/reports/inventory-value/inventory-value-report/InventoryValueReportPage.tsx
new file mode 100644
index 0000000..81ac027
--- /dev/null
+++ b/src/pages/reports/inventory-value/inventory-value-report/InventoryValueReportPage.tsx
@@ -0,0 +1,203 @@
+import React, { useState, useMemo } from 'react';
+import { useInventoryValueReport } from '../core/_hooks';
+import { Table } from '@/components/ui/Table';
+import { TableColumn } from '@/types';
+import { PageContainer, PageTitle } from '@/components/ui/Typography';
+import { Pagination } from '@/components/ui/Pagination';
+import { DollarSign, Package, TrendingUp, Hash } from 'lucide-react';
+import { formatWithThousands } from '@/utils/numberUtils';
+import { formatCurrency } from '@/utils/formatters';
+import { ReportSkeleton } from '@/components/common/ReportSkeleton';
+
+const InventoryValueReportPage = () => {
+ const { data, isLoading, error } = useInventoryValueReport();
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ const columns: TableColumn[] = useMemo(() => [
+ {
+ key: 'factory_fee_percentage',
+ label: 'درصد اجرت کارخانه',
+ align: 'right',
+ render: (val: number) => `${formatWithThousands(val, 1)}%`,
+ },
+ {
+ key: 'raw_weight',
+ label: 'وزن خام (گرم)',
+ align: 'right',
+ render: (val: number) => formatWithThousands(val, 2) + ' گرم',
+ },
+ {
+ key: 'weight_with_fee',
+ label: 'وزن با اجرت (گرم)',
+ align: 'right',
+ render: (val: number) => formatWithThousands(val, 2) + ' گرم',
+ },
+ {
+ key: 'amount',
+ label: 'ارزش (تومان)',
+ align: 'right',
+ render: (val: number) => formatCurrency(val),
+ },
+ {
+ key: 'variants_count',
+ label: 'تعداد Variant',
+ align: 'right',
+ render: (val: number) => formatWithThousands(val),
+ },
+ {
+ key: 'stock_quantity',
+ label: 'موجودی',
+ align: 'right',
+ render: (val: number) => formatWithThousands(val),
+ },
+ ], []);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
خطا در بارگذاری گزارش
+
+
+ );
+ }
+
+ return (
+
+ گزارش ارزش موجودی
+
+ {data && (
+ <>
+
+
+
+
+
+
+
+
ارزش موجودی خام
+
+ {formatCurrency(data.raw_inventory_amount)}
+
+
+
+
+
+
+
+
+
+
+
+
ارزش با اجرت کارخانه
+
+ {formatCurrency(data.factory_fee_inventory_amount)}
+
+
+
+
+
+
+
+
+
+
وزن خام
+
+ {formatWithThousands(data.raw_inventory_weight, 2)} گرم
+
+
+
+
+
+
+
+
+
+
+
+
قیمت لحظهای طلا
+
+ {formatCurrency(data.current_gold_price)}/گرم
+
+
+
+
+
+
+
+
+
+
+
+
+
+
تعداد کل Variant
+
+ {formatWithThousands(data.total_variants_count)}
+
+
+
+
+
+
+
+
+
+
موجودی کل
+
+ {formatWithThousands(data.total_stock_quantity)}
+
+
+
+
+
+
+
+
+ تفکیک بر اساس درصد اجرت کارخانه
+
+ {(() => {
+ const breakdownData = data.factory_fee_breakdown || [];
+ const totalPages = Math.ceil(breakdownData.length / itemsPerPage);
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ const paginatedData = breakdownData.slice(startIndex, endIndex);
+
+ return (
+ <>
+
+ {totalPages > 1 && (
+
+ )}
+ >
+ );
+ })()}
+
+ >
+ )}
+
+ );
+};
+
+export default InventoryValueReportPage;
diff --git a/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx b/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx
index 7db462b..e772bbd 100644
--- a/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx
+++ b/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx
@@ -36,31 +36,42 @@ const PaymentMethodsReportPage = () => {
group_by_user: false,
});
+ const [tempFilters, setTempFilters] = useState({
+ limit: 50,
+ offset: 0,
+ group_by_user: false,
+ });
+
const { data, isLoading, error } = usePaymentMethodsReport(filters);
- const handleFilterChange = (key: keyof PaymentMethodsFilters, value: any) => {
- setFilters(prev => ({
+ const handleTempFilterChange = (key: keyof PaymentMethodsFilters, value: any) => {
+ setTempFilters(prev => ({
...prev,
[key]: value,
- offset: 0,
}));
};
const handleDateRangeChange = (from: string | undefined, to: string | undefined) => {
- setFilters(prev => ({
+ setTempFilters(prev => ({
...prev,
date_range: {
from,
to,
},
- offset: 0,
}));
};
const handleNumericFilterChange = (key: 'user_id', raw: string) => {
const converted = persianToEnglish(raw).replace(/[^\d]/g, '');
const numeric = converted ? Number(converted) : undefined;
- handleFilterChange(key, numeric);
+ handleTempFilterChange(key, numeric);
+ };
+
+ const handleApplyFilters = () => {
+ setFilters({
+ ...tempFilters,
+ offset: 0,
+ });
};
const handlePageChange = (page: number) => {
@@ -71,11 +82,13 @@ const PaymentMethodsReportPage = () => {
};
const handleClearFilters = () => {
- setFilters({
+ const clearedFilters = {
limit: 50,
offset: 0,
group_by_user: false,
- });
+ };
+ setTempFilters(clearedFilters);
+ setFilters(clearedFilters);
};
const columns: TableColumn[] = [
@@ -158,15 +171,24 @@ const PaymentMethodsReportPage = () => {
فیلترها
-
+
+
+
+
@@ -175,7 +197,7 @@ const PaymentMethodsReportPage = () => {
شناسه کاربر
handleNumericFilterChange('user_id', e.target.value)}
placeholder="مثلاً 456"
numeric
@@ -187,8 +209,8 @@ const PaymentMethodsReportPage = () => {
نوع پرداخت
@@ -233,8 +255,8 @@ const PaymentMethodsReportPage = () => {
تاریخ پایان
handleDateRangeChange(filters.date_range?.from, value)}
+ value={tempFilters.date_range?.to}
+ onChange={(value) => handleDateRangeChange(tempFilters.date_range?.from, value)}
placeholder="انتخاب تاریخ پایان"
/>
@@ -243,8 +265,8 @@ const PaymentMethodsReportPage = () => {