more improve in sell report

This commit is contained in:
Hesabix 2025-08-21 21:49:49 +00:00
parent fa46e410fc
commit 2e4b0a68f2
3 changed files with 357 additions and 149 deletions

View file

@ -45,6 +45,15 @@ class SellReportController extends AbstractController
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
@ -52,6 +61,11 @@ class SellReportController extends AbstractController
$customerId = $request->query->get('customerId');
$status = $request->query->get('status');
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
// تنظیم تاریخ‌های پیش‌فرض اگر ارسال نشده باشند
if (!$startDate) {
$startDate = $jdate->jdate('Y/m/01', time()); // ابتدای ماه جاری
@ -107,6 +121,15 @@ class SellReportController extends AbstractController
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
@ -116,6 +139,11 @@ class SellReportController extends AbstractController
$page = max(1, (int) $request->query->get('page', 1));
$perPage = max(1, min(100, (int) $request->query->get('perPage', 20)));
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
try {
$invoices = $sellReportService->getSellInvoices(
$acc['bid'],
@ -165,6 +193,15 @@ class SellReportController extends AbstractController
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
@ -173,6 +210,11 @@ class SellReportController extends AbstractController
$customerId = $request->query->get('customerId');
$status = $request->query->get('status');
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
try {
$topProducts = $sellReportService->getTopProducts(
$acc['bid'],
@ -221,6 +263,15 @@ class SellReportController extends AbstractController
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
@ -228,6 +279,11 @@ class SellReportController extends AbstractController
$customerId = $request->query->get('customerId');
$status = $request->query->get('status');
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
try {
$topCustomers = $sellReportService->getTopCustomers(
$acc['bid'],

View file

@ -2,6 +2,14 @@
<v-card :loading="loading ? 'red' : null" :disabled="loading">
<!-- Toolbar -->
<v-toolbar color="toolbar" :title="$t('dialog.explore_accounts')" flat>
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn icon @click="loadNode('root', 'calc', true, 1)" :title="$t('button.back_to_root')">
<v-icon>mdi-home</v-icon>

View file

@ -2,132 +2,201 @@
<v-card :loading="loading ? 'red' : null" :disabled="loading">
<!-- Toolbar -->
<v-toolbar color="toolbar" title="گزارش فروش" flat>
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn icon @click="exportReport" :loading="exporting" :disabled="loading" title="دانلود گزارش">
<v-icon>mdi-download</v-icon>
</v-btn>
</v-toolbar>
<!-- Date Filter -->
<v-card-text class="pt-0">
<v-card variant="outlined" class="mb-4 date-filter-card">
<v-card-title class="text-subtitle-1 font-weight-medium pa-4 pb-2">
<v-icon icon="mdi-calendar-filter" class="me-2" color="primary"></v-icon>
فیلتر بر اساس تاریخ
<v-chip
v-if="isDateFilterActive"
color="success"
size="small"
class="ms-2"
prepend-icon="mdi-check-circle"
>
فعال
</v-chip>
</v-card-title>
<v-card-text class="pt-0">
<v-row>
<v-col cols="12" sm="6" md="4" class="date-picker-container">
<v-text-field
:model-value="formattedStartDate"
label="تاریخ شروع"
prepend-inner-icon="mdi-calendar"
readonly
@click="showStartDatePicker = true"
variant="outlined"
density="comfortable"
/>
<v-dialog v-model="showStartDatePicker" max-width="400">
<v-date-picker
v-model="gregorianStartDate"
:min="convertJalaliToGregorian(year.start)"
:max="convertJalaliToGregorian(year.end)"
locale="fa"
color="primary"
@update:model-value="(value) => {
dateFilter.startDate = convertGregorianToJalali(value);
gregorianStartDate = value;
showStartDatePicker = false;
}"
/>
</v-dialog>
</v-col>
<v-col cols="12" sm="6" md="4" class="date-picker-container">
<v-text-field
:model-value="formattedEndDate"
label="تاریخ پایان"
prepend-inner-icon="mdi-calendar"
readonly
@click="showEndDatePicker = true"
variant="outlined"
density="comfortable"
/>
<v-dialog v-model="showEndDatePicker" max-width="400">
<v-date-picker
v-model="gregorianEndDate"
:min="convertJalaliToGregorian(dateFilter.startDate || year.start)"
:max="convertJalaliToGregorian(year.end)"
locale="fa"
color="primary"
@update:model-value="(value) => {
dateFilter.endDate = convertGregorianToJalali(value);
gregorianEndDate = value;
showEndDatePicker = false;
}"
/>
</v-dialog>
</v-col>
<!-- Filters Section -->
<v-card-text class="pt-2">
<v-expansion-panels variant="accordion" class="mb-4">
<v-expansion-panel>
<v-expansion-panel-title class="filters-panel-title">
<div class="d-flex align-center">
<v-icon icon="mdi-filter-variant" class="me-2" color="primary"></v-icon>
<span class="font-weight-medium">فیلترهای گزارش</span>
<v-chip
v-if="isAnyFilterActive"
color="success"
size="small"
class="ms-2"
prepend-icon="mdi-check-circle"
>
فعال
</v-chip>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-card variant="outlined" class="filters-card">
<v-card-text class="pt-0">
<v-row>
<!-- Date Filters -->
<v-col cols="12">
<div class="text-caption text-medium-emphasis mb-3">
<v-icon size="small" class="me-1">mdi-calendar</v-icon>
فیلتر بر اساس تاریخ
</div>
<v-row>
<v-col cols="12" sm="6" md="3" class="date-picker-container">
<v-text-field
:model-value="formattedStartDate"
label="تاریخ شروع"
prepend-inner-icon="mdi-calendar-start"
readonly
@click="showStartDatePicker = true"
variant="outlined"
density="comfortable"
class="filter-field"
/>
<v-dialog v-model="showStartDatePicker" max-width="400">
<v-date-picker
v-model="gregorianStartDate"
:min="convertJalaliToGregorian(year.start)"
:max="convertJalaliToGregorian(year.end)"
locale="fa"
color="primary"
@update:model-value="(value) => {
dateFilter.startDate = convertGregorianToJalali(value);
gregorianStartDate = value;
showStartDatePicker = false;
}"
/>
</v-dialog>
</v-col>
<v-col cols="12" sm="6" md="3" class="date-picker-container">
<v-text-field
:model-value="formattedEndDate"
label="تاریخ پایان"
prepend-inner-icon="mdi-calendar-end"
readonly
@click="showEndDatePicker = true"
variant="outlined"
density="comfortable"
class="filter-field"
/>
<v-dialog v-model="showEndDatePicker" max-width="400">
<v-date-picker
v-model="gregorianEndDate"
:min="convertJalaliToGregorian(dateFilter.startDate || year.start)"
:max="convertJalaliToGregorian(year.end)"
locale="fa"
color="primary"
@update:model-value="(value) => {
dateFilter.endDate = convertGregorianToJalali(value);
gregorianEndDate = value;
showEndDatePicker = false;
}"
/>
</v-dialog>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-card-text>
<!-- Other Filters -->
<v-col cols="12">
<v-divider class="mb-3"></v-divider>
<div class="text-caption text-medium-emphasis mb-3">
<v-icon size="small" class="me-1">mdi-tune</v-icon>
فیلترهای اضافی
</div>
<v-row>
<v-col cols="12" sm="6" md="3">
<Hpersonsearch
v-model="filters.customerId"
label="مشتری"
:return-object="false"
:rules="[]"
class="filter-field"
/>
</v-col>
<v-col v-if="business.requireTwoStepApproval" cols="12" sm="6" md="3">
<v-select
v-model="filters.status"
:items="statusOptions"
item-title="text"
item-value="value"
label="وضعیت فاکتور"
prepend-inner-icon="mdi-check-circle"
variant="outlined"
density="comfortable"
clearable
class="filter-field"
></v-select>
</v-col>
<v-col :cols="business.requireTwoStepApproval ? 6 : 3" md="3">
<v-select
v-model="filters.groupBy"
:items="groupByOptions"
item-title="text"
item-value="value"
label="گروه‌بندی نمودار"
prepend-inner-icon="mdi-chart-line"
variant="outlined"
density="comfortable"
class="filter-field"
></v-select>
</v-col>
<v-col :cols="business.requireTwoStepApproval ? 6 : 3" md="3">
<v-select
v-model="chartType"
:items="chartTypeOptions"
item-title="text"
item-value="value"
label="نوع نمودار"
prepend-inner-icon="mdi-chart-bar"
variant="outlined"
density="comfortable"
class="filter-field"
></v-select>
</v-col>
</v-row>
</v-col>
<!-- Additional Filters -->
<v-card-text class="pt-0">
<v-row>
<v-col cols="12" md="3">
<Hpersonsearch
v-model="filters.customerId"
label="مشتری"
:return-object="false"
:rules="[]"
/>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="filters.status"
:items="statusOptions"
item-title="text"
item-value="value"
label="وضعیت"
outlined
dense
clearable
></v-select>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="filters.groupBy"
:items="groupByOptions"
item-title="text"
item-value="value"
label="گروه‌بندی نمودار"
outlined
dense
></v-select>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="chartType"
:items="chartTypeOptions"
item-title="text"
item-value="value"
label="نوع نمودار"
outlined
dense
></v-select>
</v-col>
</v-row>
<!-- Quick Actions -->
<v-col cols="12">
<v-divider class="mb-3"></v-divider>
<div class="d-flex justify-space-between align-center">
<div class="text-caption text-medium-emphasis">
<v-icon size="small" class="me-1">mdi-lightning-bolt</v-icon>
عملیات سریع
</div>
<div class="d-flex gap-2">
<v-btn
size="small"
variant="outlined"
color="primary"
prepend-icon="mdi-refresh"
@click="loadData"
:loading="loading"
>
بهروزرسانی
</v-btn>
<v-btn
size="small"
variant="outlined"
color="success"
prepend-icon="mdi-download"
@click="exportReport"
:loading="exporting"
:disabled="loading"
>
دانلود گزارش
</v-btn>
</div>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
<!-- Summary Cards -->
@ -272,10 +341,10 @@
</template>
<template #item.status="{ item }">
<v-chip
:color="item.isApproved ? 'success' : 'warning'"
:color="getApprovalStatusColor(item)"
size="small"
>
{{ item.isApproved ? 'تایید شده' : 'پیش‌نمایش' }}
{{ getApprovalStatusText(item) }}
</v-chip>
</template>
<template #item.actions="{ item }">
@ -349,6 +418,9 @@ export default {
errorDialog: false,
errorMessage: '',
// Business Info
business: { requireTwoStepApproval: false },
// Date Filter
dateFilter: {
startDate: '',
@ -464,31 +536,31 @@ export default {
],
productHeaders: [
{ text: 'نام کالا', value: 'name' },
{ text: 'کد', value: 'code' },
{ text: 'تعداد', value: 'totalCount' },
{ text: 'مبلغ کل', value: 'totalAmount' },
{ text: 'سود', value: 'profit' },
{ text: 'درصد سود', value: 'profitMargin' }
{ title: 'نام کالا', value: 'name', sortable: true, width: 200 },
{ title: 'کد', value: 'code', sortable: true, width: 120 },
{ title: 'تعداد', value: 'totalCount', sortable: true, width: 100 },
{ title: 'مبلغ کل', value: 'totalAmount', sortable: true, width: 150 },
{ title: 'سود', value: 'profit', sortable: true, width: 150 },
{ title: 'درصد سود', value: 'profitMargin', sortable: true, width: 120 }
],
customerHeaders: [
{ text: 'نام مشتری', value: 'name' },
{ text: 'کد', value: 'code' },
{ text: 'تعداد فاکتور', value: 'invoiceCount' },
{ text: 'مبلغ کل', value: 'totalAmount' },
{ text: 'میانگین فاکتور', value: 'averageAmount' }
{ title: 'نام مشتری', value: 'name', sortable: true, width: 200 },
{ title: 'کد', value: 'code', sortable: true, width: 120 },
{ title: 'تعداد فاکتور', value: 'invoiceCount', sortable: true, width: 120 },
{ title: 'مبلغ کل', value: 'totalAmount', sortable: true, width: 150 },
{ title: 'میانگین فاکتور', value: 'averageAmount', sortable: true, width: 150 }
],
invoiceHeaders: [
{ text: 'شماره فاکتور', value: 'code' },
{ text: 'تاریخ', value: 'date' },
{ text: 'مشتری', value: 'customer.name' },
{ text: 'مبلغ', value: 'amount' },
{ text: 'سود', value: 'profit' },
{ text: 'درصد سود', value: 'profitMargin' },
{ text: 'وضعیت', value: 'status' },
{ text: 'عملیات', value: 'actions', sortable: false }
{ title: 'شماره فاکتور', value: 'code', sortable: true, width: 120 },
{ title: 'تاریخ', value: 'date', sortable: true, width: 120 },
{ title: 'مشتری', value: 'customer.name', sortable: true, width: 150 },
{ title: 'مبلغ', value: 'amount', sortable: true, width: 150 },
{ title: 'سود', value: 'profit', sortable: true, width: 150 },
{ title: 'درصد سود', value: 'profitMargin', sortable: true, width: 120 },
{ title: 'وضعیت', value: 'status', sortable: true, width: 120 },
{ title: 'عملیات', value: 'actions', sortable: false, width: 100 }
]
};
},
@ -498,6 +570,13 @@ export default {
return this.dateFilter.startDate && this.dateFilter.endDate &&
(this.dateFilter.startDate !== this.year.start || this.dateFilter.endDate !== this.year.end);
},
isAnyFilterActive() {
return this.isDateFilterActive ||
this.filters.customerId ||
this.filters.status ||
this.filters.groupBy !== 'day' ||
this.chartType !== 'amount';
},
formattedStartDate() {
return this.dateFilter.startDate ? this.formatDateForDisplay(this.dateFilter.startDate) : '';
},
@ -507,9 +586,10 @@ export default {
totalInvoicePages() {
return Math.ceil(this.totalInvoices / 20);
},
},
// Watchers for automatic filter updates
},
// Watchers for automatic filter updates
watch: {
'filters.customerId'() {
this.loadData();
@ -567,6 +647,22 @@ export default {
},
methods: {
// متدهای مربوط به وضعیت تایید
getApprovalStatusText(item) {
if (!this.business?.requireTwoStepApproval) return 'تایید دو مرحله‌ای غیرفعال';
if (item.isPreview) return 'در انتظار تایید';
if (item.isApproved) return 'تایید شده';
return 'تایید شده';
},
getApprovalStatusColor(item) {
if (!this.business?.requireTwoStepApproval) return 'default';
if (item.isPreview) return 'warning';
if (item.isApproved) return 'success';
return 'success';
},
async loadInitialData() {
this.loading = true;
try {
@ -574,6 +670,10 @@ export default {
const yearResponse = await axios.get('/api/year/get');
this.year = yearResponse.data;
// دریافت اطلاعات کسب و کار
const businessResponse = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
this.business = businessResponse.data || { requireTwoStepApproval: false };
// تنظیم تاریخهای پیشفرض
this.dateFilter.startDate = this.year.start;
this.dateFilter.endDate = this.year.end;
@ -861,14 +961,30 @@ export default {
</style>
<style scoped>
.date-filter-card {
.filters-panel-title {
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.filters-panel-title:hover {
background: linear-gradient(135deg, #eff6ff, #f0f7ff);
border-color: #3b82f6;
}
.filters-card {
border-left: 4px solid #1976d2;
position: relative;
z-index: 100;
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
}
.filter-buttons {
gap: 8px;
.filter-field {
transition: all 0.3s ease;
}
.filter-field:hover {
transform: translateY(-1px);
}
.date-picker-container {
@ -897,8 +1013,36 @@ export default {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.gap-2 {
gap: 8px;
}
/* Expansion Panel Styles */
:deep(.v-expansion-panels) {
border-radius: 12px;
overflow: hidden;
}
:deep(.v-expansion-panel) {
border-radius: 12px;
margin-bottom: 0;
}
:deep(.v-expansion-panel-title) {
min-height: 56px;
padding: 16px 20px;
}
:deep(.v-expansion-panel-text__wrapper) {
padding: 0;
}
:deep(.v-expansion-panels__item) {
border-radius: 12px;
}
@media (max-width: 600px) {
.filter-buttons {
.gap-2 {
flex-direction: column;
gap: 12px;
}