feat(discount-reports): enhance discount usage reporting with additional filters and improved query parameters
- Added new filter options for status, type, application level, and sorting in discount usage reports. - Updated API request parameters to align with new filter capabilities. - Refactored pagination logic to improve clarity and accuracy in displaying total items and pages. - Enhanced the sidebar layout for better organization and usability. These changes significantly improve the reporting functionality and user experience in the discount statistics section.
This commit is contained in:
parent
56a891e668
commit
785f97b26d
|
|
@ -355,10 +355,10 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||||
w-64 transform transition-transform duration-300 ease-in-out
|
w-64 transform transition-transform duration-300 ease-in-out
|
||||||
lg:translate-x-0 lg:block
|
lg:translate-x-0 lg:block
|
||||||
${isOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0'}
|
${isOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0'}
|
||||||
flex flex-col h-screen bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-lg lg:shadow-none
|
h-screen bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-lg lg:shadow-none
|
||||||
`}>
|
`}>
|
||||||
{/* Mobile close button */}
|
<div className="grid h-full grid-rows-[auto_auto_1fr_auto] overflow-hidden">
|
||||||
<div className="lg:hidden flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
<div className="lg:hidden flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<SectionTitle>
|
<SectionTitle>
|
||||||
پنل مدیریت
|
پنل مدیریت
|
||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
|
|
@ -370,20 +370,19 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Logo - desktop only */}
|
<div className="hidden lg:flex h-16 items-center justify-center border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="hidden lg:flex h-16 items-center justify-center border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
||||||
<SectionTitle>
|
<SectionTitle>
|
||||||
پنل مدیریت
|
پنل مدیریت
|
||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation - scrollable */}
|
<div className="min-h-0 overflow-y-auto overflow-x-hidden sidebar-nav">
|
||||||
<nav className="flex-1 space-y-1 px-4 py-6 overflow-y-auto overflow-x-hidden sidebar-nav min-h-0">
|
<nav className="space-y-1 px-4 py-6">
|
||||||
{menuItems.map(item => renderMenuItem(item))}
|
{menuItems.map(item => renderMenuItem(item))}
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* User Info - fixed at bottom */}
|
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-white dark:bg-gray-800">
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4 flex-shrink-0 bg-white dark:bg-gray-800">
|
|
||||||
<div className="flex items-center space-x-3 space-x-reverse">
|
<div className="flex items-center space-x-3 space-x-reverse">
|
||||||
<div className="h-8 w-8 rounded-full bg-primary-600 flex items-center justify-center">
|
<div className="h-8 w-8 rounded-full bg-primary-600 flex items-center justify-center">
|
||||||
<span className="text-sm font-medium text-white">
|
<span className="text-sm font-medium text-white">
|
||||||
|
|
@ -407,6 +406,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export interface DateRange {
|
export interface DateRange {
|
||||||
from?: string; // ISO 8601
|
from?: string;
|
||||||
to?: string; // ISO 8601
|
to?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscountUsageFilters {
|
export interface DiscountUsageFilters {
|
||||||
|
|
@ -8,6 +8,13 @@ export interface DiscountUsageFilters {
|
||||||
discount_code?: string;
|
discount_code?: string;
|
||||||
discount_id?: number;
|
discount_id?: number;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
|
status?: "active" | "inactive" | "expired";
|
||||||
|
type?: "percentage" | "fixed" | "fee_percentage";
|
||||||
|
application_level?: "invoice" | "category" | "product" | "shipping" | "product_fee";
|
||||||
|
min_usage_count?: number;
|
||||||
|
include_unused?: boolean;
|
||||||
|
sort_by?: "usage_count" | "amount" | "date" | "code" | "created_at";
|
||||||
|
sort_order?: "asc" | "desc";
|
||||||
group_by_code?: boolean;
|
group_by_code?: boolean;
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
|
|
@ -18,15 +25,15 @@ export interface DiscountUsage {
|
||||||
discount_code: string;
|
discount_code: string;
|
||||||
discount_name: string;
|
discount_name: string;
|
||||||
usage_count: number;
|
usage_count: number;
|
||||||
total_amount: number; // ریال
|
total_amount: number;
|
||||||
unique_users: number;
|
unique_users: number;
|
||||||
first_used_at: string; // ISO 8601
|
first_used_at: string;
|
||||||
last_used_at: string; // ISO 8601
|
last_used_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscountUsageSummary {
|
export interface DiscountUsageSummary {
|
||||||
total_usages: number;
|
total_usages: number;
|
||||||
total_discount_given: number; // ریال
|
total_discount_given: number;
|
||||||
unique_users: number;
|
unique_users: number;
|
||||||
unique_codes: number;
|
unique_codes: number;
|
||||||
most_used_code: string;
|
most_used_code: string;
|
||||||
|
|
@ -43,10 +50,14 @@ export interface DiscountUsageResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomerDiscountUsageFilters {
|
export interface CustomerDiscountUsageFilters {
|
||||||
user_id: number; // Required
|
user_id?: number;
|
||||||
|
phone_number?: string;
|
||||||
date_range?: DateRange;
|
date_range?: DateRange;
|
||||||
discount_code?: string;
|
discount_code?: string;
|
||||||
discount_id?: number;
|
discount_id?: number;
|
||||||
|
status?: "active" | "inactive" | "expired";
|
||||||
|
sort_by?: "date" | "amount" | "code";
|
||||||
|
sort_order?: "asc" | "desc";
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
}
|
}
|
||||||
|
|
@ -60,15 +71,15 @@ export interface CustomerDiscountUsage {
|
||||||
discount_name: string;
|
discount_name: string;
|
||||||
order_id: number;
|
order_id: number;
|
||||||
order_number: string;
|
order_number: string;
|
||||||
amount: number; // ریال
|
amount: number;
|
||||||
used_at: string; // ISO 8601
|
used_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomerDiscountUsageSummary {
|
export interface CustomerDiscountUsageSummary {
|
||||||
total_usages: number;
|
total_usages: number;
|
||||||
total_discount_amount: number; // ریال
|
total_discount_amount: number;
|
||||||
unique_codes: number;
|
unique_codes: number;
|
||||||
average_discount_per_order: number; // ریال
|
average_discount_per_order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomerDiscountUsageResponse {
|
export interface CustomerDiscountUsageResponse {
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,25 @@ export const getDiscountUsageReport = async (
|
||||||
): Promise<DiscountUsageResponse> => {
|
): Promise<DiscountUsageResponse> => {
|
||||||
const queryParams: Record<string, string | number | boolean> = {};
|
const queryParams: Record<string, string | number | boolean> = {};
|
||||||
|
|
||||||
if (filters.date_range?.from) queryParams.from = filters.date_range.from;
|
queryParams.view_mode = "simple";
|
||||||
if (filters.date_range?.to) queryParams.to = filters.date_range.to;
|
if (filters.date_range?.from) queryParams.from_date = filters.date_range.from;
|
||||||
|
if (filters.date_range?.to) queryParams.to_date = filters.date_range.to;
|
||||||
if (filters.discount_code) queryParams.discount_code = filters.discount_code;
|
if (filters.discount_code) queryParams.discount_code = filters.discount_code;
|
||||||
if (filters.discount_id !== undefined) queryParams.discount_id = filters.discount_id;
|
if (filters.discount_id !== undefined) queryParams.discount_id = filters.discount_id;
|
||||||
if (filters.user_id !== undefined) queryParams.user_id = filters.user_id;
|
if (filters.user_id !== undefined) queryParams.user_id = filters.user_id;
|
||||||
if (filters.group_by_code !== undefined) queryParams.group_by_code = filters.group_by_code;
|
if (filters.group_by_code !== undefined) queryParams.group_by_code = filters.group_by_code;
|
||||||
|
if (filters.status) queryParams.status = filters.status;
|
||||||
|
if (filters.type) queryParams.type = filters.type;
|
||||||
|
if (filters.application_level) queryParams.application_level = filters.application_level;
|
||||||
|
if (filters.min_usage_count !== undefined) queryParams.min_usage_count = filters.min_usage_count;
|
||||||
|
if (filters.include_unused !== undefined) queryParams.include_unused = filters.include_unused;
|
||||||
|
if (filters.sort_by) queryParams.sort_by = filters.sort_by;
|
||||||
|
if (filters.sort_order) queryParams.sort_order = filters.sort_order;
|
||||||
if (filters.limit !== undefined) queryParams.limit = filters.limit;
|
if (filters.limit !== undefined) queryParams.limit = filters.limit;
|
||||||
if (filters.offset !== undefined) queryParams.offset = filters.offset;
|
if (filters.offset !== undefined) queryParams.offset = filters.offset;
|
||||||
|
|
||||||
const response = await httpGetRequest<DiscountUsageResponse>(
|
const response = await httpGetRequest<DiscountUsageResponse>(
|
||||||
APIUrlGenerator(API_ROUTES.DISCOUNT_USAGE_REPORT, queryParams)
|
APIUrlGenerator(API_ROUTES.DISCOUNT_REPORTS, queryParams)
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
@ -32,11 +40,15 @@ export const getCustomerDiscountUsageReport = async (
|
||||||
): Promise<CustomerDiscountUsageResponse> => {
|
): Promise<CustomerDiscountUsageResponse> => {
|
||||||
const queryParams: Record<string, string | number> = {};
|
const queryParams: Record<string, string | number> = {};
|
||||||
|
|
||||||
queryParams.user_id = filters.user_id;
|
if (filters.user_id !== undefined) queryParams.user_id = filters.user_id;
|
||||||
if (filters.date_range?.from) queryParams.from = filters.date_range.from;
|
if (filters.phone_number) queryParams.phone_number = filters.phone_number;
|
||||||
if (filters.date_range?.to) queryParams.to = filters.date_range.to;
|
if (filters.date_range?.from) queryParams.from_date = filters.date_range.from;
|
||||||
|
if (filters.date_range?.to) queryParams.to_date = filters.date_range.to;
|
||||||
if (filters.discount_code) queryParams.discount_code = filters.discount_code;
|
if (filters.discount_code) queryParams.discount_code = filters.discount_code;
|
||||||
if (filters.discount_id !== undefined) queryParams.discount_id = filters.discount_id;
|
if (filters.discount_id !== undefined) queryParams.discount_id = filters.discount_id;
|
||||||
|
if (filters.status) queryParams.status = filters.status;
|
||||||
|
if (filters.sort_by) queryParams.sort_by = filters.sort_by;
|
||||||
|
if (filters.sort_order) queryParams.sort_order = filters.sort_order;
|
||||||
if (filters.limit !== undefined) queryParams.limit = filters.limit;
|
if (filters.limit !== undefined) queryParams.limit = filters.limit;
|
||||||
if (filters.offset !== undefined) queryParams.offset = filters.offset;
|
if (filters.offset !== undefined) queryParams.offset = filters.offset;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,13 @@ const DiscountUsageReportPage = () => {
|
||||||
last_used_at: formatDateTime(usage.last_used_at),
|
last_used_at: formatDateTime(usage.last_used_at),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const currentPage = Math.floor(filters.offset / filters.limit) + 1;
|
const limit = filters.limit;
|
||||||
const totalPages = data ? Math.ceil(data.total / filters.limit) : 1;
|
const offset = filters.offset;
|
||||||
|
const currentPage = Math.floor(offset / limit) + 1;
|
||||||
|
const totalItems = typeof data?.total === 'number'
|
||||||
|
? data.total
|
||||||
|
: offset + (data?.usages?.length || 0) + (data?.has_more ? 1 : 0);
|
||||||
|
const totalPages = Math.max(1, Math.ceil(totalItems / limit));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
|
|
@ -299,19 +304,19 @@ const DiscountUsageReportPage = () => {
|
||||||
<Table columns={columns} data={tableData} loading={isLoading} />
|
<Table columns={columns} data={tableData} loading={isLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data.total > 0 && (data.total > filters.limit || data.has_more) && (
|
{data && totalItems > limit && (
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="mt-4 flex justify-center">
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
itemsPerPage={filters.limit}
|
itemsPerPage={limit}
|
||||||
totalItems={data.total}
|
totalItems={totalItems}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data && data.total === 0 && (
|
{data && totalItems === 0 && (
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-8 text-center">
|
<div className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-8 text-center">
|
||||||
<p className="text-gray-600 dark:text-gray-400">دادهای یافت نشد</p>
|
<p className="text-gray-600 dark:text-gray-400">دادهای یافت نشد</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue