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

View file

@ -2,6 +2,14 @@
<v-card :loading="loading ? 'red' : null" :disabled="loading"> <v-card :loading="loading ? 'red' : null" :disabled="loading">
<!-- Toolbar --> <!-- Toolbar -->
<v-toolbar color="toolbar" :title="$t('dialog.explore_accounts')" flat> <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-spacer></v-spacer>
<v-btn icon @click="loadNode('root', 'calc', true, 1)" :title="$t('button.back_to_root')"> <v-btn icon @click="loadNode('root', 'calc', true, 1)" :title="$t('button.back_to_root')">
<v-icon>mdi-home</v-icon> <v-icon>mdi-home</v-icon>

View file

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