add sell report
This commit is contained in:
parent
f609c4176f
commit
fa46e410fc
519
hesabixCore/src/Controller/SellReportController.php
Normal file
519
hesabixCore/src/Controller/SellReportController.php
Normal file
|
@ -0,0 +1,519 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Business;
|
||||||
|
use App\Entity\HesabdariDoc;
|
||||||
|
use App\Entity\HesabdariRow;
|
||||||
|
use App\Entity\Person;
|
||||||
|
use App\Entity\Commodity;
|
||||||
|
use App\Service\Access;
|
||||||
|
use App\Service\Log;
|
||||||
|
use App\Service\Jdate;
|
||||||
|
use App\Service\SellReportService;
|
||||||
|
use App\Service\PluginService;
|
||||||
|
use App\Service\Provider;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
||||||
|
class SellReportController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/api/sell/report/summary', name: 'app_sell_report_summary', methods: ['GET'])]
|
||||||
|
public function getSellSummary(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService,
|
||||||
|
Jdate $jdate
|
||||||
|
): JsonResponse {
|
||||||
|
// بررسی دسترسی
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی فعال بودن پلاگین accpro
|
||||||
|
if (!$pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'پلاگین accpro فعال نیست'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت پارامترها
|
||||||
|
$startDate = $request->query->get('startDate');
|
||||||
|
$endDate = $request->query->get('endDate');
|
||||||
|
$groupBy = $request->query->get('groupBy', 'day');
|
||||||
|
$customerId = $request->query->get('customerId');
|
||||||
|
$status = $request->query->get('status');
|
||||||
|
|
||||||
|
// تنظیم تاریخهای پیشفرض اگر ارسال نشده باشند
|
||||||
|
if (!$startDate) {
|
||||||
|
$startDate = $jdate->jdate('Y/m/01', time()); // ابتدای ماه جاری
|
||||||
|
}
|
||||||
|
if (!$endDate) {
|
||||||
|
$endDate = $jdate->jdate('Y/m/d', time()); // امروز
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$summary = $sellReportService->getSellSummary(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
$groupBy,
|
||||||
|
$customerId,
|
||||||
|
$status
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'result' => 1,
|
||||||
|
'data' => $summary
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/report/invoices', name: 'app_sell_report_invoices', methods: ['GET'])]
|
||||||
|
public function getSellInvoices(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService
|
||||||
|
): JsonResponse {
|
||||||
|
// بررسی دسترسی
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی فعال بودن پلاگین accpro
|
||||||
|
if (!$pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'پلاگین accpro فعال نیست'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت پارامترها
|
||||||
|
$startDate = $request->query->get('startDate');
|
||||||
|
$endDate = $request->query->get('endDate');
|
||||||
|
$customerId = $request->query->get('customerId');
|
||||||
|
$commodityId = $request->query->get('commodityId');
|
||||||
|
$status = $request->query->get('status');
|
||||||
|
$page = max(1, (int) $request->query->get('page', 1));
|
||||||
|
$perPage = max(1, min(100, (int) $request->query->get('perPage', 20)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$invoices = $sellReportService->getSellInvoices(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
$customerId,
|
||||||
|
$commodityId,
|
||||||
|
$status,
|
||||||
|
$page,
|
||||||
|
$perPage
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'result' => 1,
|
||||||
|
'data' => $invoices
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/report/top-products', name: 'app_sell_report_top_products', methods: ['GET'])]
|
||||||
|
public function getTopProducts(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService
|
||||||
|
): JsonResponse {
|
||||||
|
// بررسی دسترسی
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی فعال بودن پلاگین accpro
|
||||||
|
if (!$pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'پلاگین accpro فعال نیست'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت پارامترها
|
||||||
|
$startDate = $request->query->get('startDate');
|
||||||
|
$endDate = $request->query->get('endDate');
|
||||||
|
$limit = max(1, min(50, (int) $request->query->get('limit', 10)));
|
||||||
|
$sortBy = $request->query->get('sortBy', 'amount');
|
||||||
|
$customerId = $request->query->get('customerId');
|
||||||
|
$status = $request->query->get('status');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$topProducts = $sellReportService->getTopProducts(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
$limit,
|
||||||
|
$sortBy,
|
||||||
|
$customerId,
|
||||||
|
$status
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'result' => 1,
|
||||||
|
'data' => $topProducts
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/report/top-customers', name: 'app_sell_report_top_customers', methods: ['GET'])]
|
||||||
|
public function getTopCustomers(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService
|
||||||
|
): JsonResponse {
|
||||||
|
// بررسی دسترسی
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی فعال بودن پلاگین accpro
|
||||||
|
if (!$pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'پلاگین accpro فعال نیست'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت پارامترها
|
||||||
|
$startDate = $request->query->get('startDate');
|
||||||
|
$endDate = $request->query->get('endDate');
|
||||||
|
$limit = max(1, min(50, (int) $request->query->get('limit', 10)));
|
||||||
|
$customerId = $request->query->get('customerId');
|
||||||
|
$status = $request->query->get('status');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$topCustomers = $sellReportService->getTopCustomers(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
$limit,
|
||||||
|
$customerId,
|
||||||
|
$status
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'result' => 1,
|
||||||
|
'data' => $topCustomers
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/report/chart', name: 'app_sell_report_chart', methods: ['GET'])]
|
||||||
|
public function getSellChart(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService
|
||||||
|
): JsonResponse {
|
||||||
|
// بررسی دسترسی
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی فعال بودن پلاگین accpro
|
||||||
|
if (!$pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'پلاگین accpro فعال نیست'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت پارامترها
|
||||||
|
$startDate = $request->query->get('startDate');
|
||||||
|
$endDate = $request->query->get('endDate');
|
||||||
|
$groupBy = $request->query->get('groupBy', 'day');
|
||||||
|
$type = $request->query->get('type', 'amount');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$chartData = $sellReportService->getSellChart(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
$groupBy,
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'result' => 1,
|
||||||
|
'data' => $chartData
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/report/customer-analysis', name: 'app_sell_report_customer_analysis', methods: ['GET'])]
|
||||||
|
public function getCustomerAnalysis(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService
|
||||||
|
): JsonResponse {
|
||||||
|
// بررسی دسترسی
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی فعال بودن پلاگین accpro
|
||||||
|
if (!$pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'پلاگین accpro فعال نیست'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت پارامترها
|
||||||
|
$startDate = $request->query->get('startDate');
|
||||||
|
$endDate = $request->query->get('endDate');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$customerAnalysis = $sellReportService->getCustomerAnalysis(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'result' => 1,
|
||||||
|
'data' => $customerAnalysis
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/report/export', name: 'app_sell_report_export', methods: ['POST'])]
|
||||||
|
public function exportReport(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService,
|
||||||
|
Provider $provider,
|
||||||
|
Jdate $jdate
|
||||||
|
): BinaryFileResponse|JsonResponse {
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی فعال بودن پلاگین accpro
|
||||||
|
if (!$pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'پلاگین accpro فعال نیست'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = json_decode($request->getContent(), true) ?? [];
|
||||||
|
$startDate = $params['startDate'] ?? null;
|
||||||
|
$endDate = $params['endDate'] ?? null;
|
||||||
|
$customerId = $params['customerId'] ?? null;
|
||||||
|
$status = $params['status'] ?? null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// دریافت تمام دادههای گزارش
|
||||||
|
$summary = $sellReportService->getSellSummary(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
'day',
|
||||||
|
$customerId,
|
||||||
|
$status
|
||||||
|
);
|
||||||
|
|
||||||
|
$topProducts = $sellReportService->getTopProducts(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
100, // تعداد بیشتر برای export
|
||||||
|
'amount',
|
||||||
|
$customerId,
|
||||||
|
$status
|
||||||
|
);
|
||||||
|
|
||||||
|
$topCustomers = $sellReportService->getTopCustomers(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
100, // تعداد بیشتر برای export
|
||||||
|
$customerId,
|
||||||
|
$status
|
||||||
|
);
|
||||||
|
|
||||||
|
$invoices = $sellReportService->getSellInvoices(
|
||||||
|
$acc['bid'],
|
||||||
|
$acc['year'],
|
||||||
|
$acc['money'],
|
||||||
|
$startDate,
|
||||||
|
$endDate,
|
||||||
|
$customerId,
|
||||||
|
null,
|
||||||
|
$status,
|
||||||
|
1,
|
||||||
|
1000 // تعداد بیشتر برای export
|
||||||
|
);
|
||||||
|
|
||||||
|
// آمادهسازی دادهها برای Excel
|
||||||
|
$excelData = [];
|
||||||
|
|
||||||
|
// 1. خلاصه آمار
|
||||||
|
$excelData[] = ['خلاصه آمار فروش'];
|
||||||
|
$excelData[] = ['', '']; // خط خالی
|
||||||
|
$excelData[] = ['کل فروش', number_format($summary['totalAmount']) . ' ریال'];
|
||||||
|
$excelData[] = ['تعداد فاکتور', number_format($summary['totalCount'])];
|
||||||
|
$excelData[] = ['میانگین فاکتور', number_format($summary['averageAmount']) . ' ریال'];
|
||||||
|
$excelData[] = ['کل سود', number_format($summary['totalProfit']) . ' ریال'];
|
||||||
|
$excelData[] = ['درصد سود', $summary['profitMargin'] . '%'];
|
||||||
|
$excelData[] = ['بیشترین مبلغ', number_format($summary['maxAmount']) . ' ریال'];
|
||||||
|
$excelData[] = ['کمترین مبلغ', number_format($summary['minAmount']) . ' ریال'];
|
||||||
|
$excelData[] = ['', '']; // خط خالی
|
||||||
|
|
||||||
|
// 2. محصولات برتر
|
||||||
|
$excelData[] = ['محصولات پرفروش'];
|
||||||
|
$excelData[] = ['', '']; // خط خالی
|
||||||
|
$excelData[] = ['نام کالا', 'کد', 'تعداد', 'مبلغ کل', 'سود', 'درصد سود'];
|
||||||
|
|
||||||
|
foreach ($topProducts as $product) {
|
||||||
|
$excelData[] = [
|
||||||
|
$product['name'],
|
||||||
|
$product['code'],
|
||||||
|
number_format($product['totalCount']),
|
||||||
|
number_format($product['totalAmount']) . ' ریال',
|
||||||
|
number_format($product['profit']) . ' ریال',
|
||||||
|
$product['profitMargin'] . '%'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$excelData[] = ['', '']; // خط خالی
|
||||||
|
|
||||||
|
// 3. مشتریان برتر
|
||||||
|
$excelData[] = ['مشتریان پرفروش'];
|
||||||
|
$excelData[] = ['', '']; // خط خالی
|
||||||
|
$excelData[] = ['نام مشتری', 'کد', 'تعداد فاکتور', 'مبلغ کل', 'میانگین فاکتور'];
|
||||||
|
|
||||||
|
foreach ($topCustomers as $customer) {
|
||||||
|
$excelData[] = [
|
||||||
|
$customer['name'],
|
||||||
|
$customer['code'],
|
||||||
|
number_format($customer['invoiceCount']),
|
||||||
|
number_format($customer['totalAmount']) . ' ریال',
|
||||||
|
number_format($customer['averageAmount']) . ' ریال'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$excelData[] = ['', '']; // خط خالی
|
||||||
|
|
||||||
|
// 4. لیست فاکتورها
|
||||||
|
$excelData[] = ['لیست فاکتورها'];
|
||||||
|
$excelData[] = ['', '']; // خط خالی
|
||||||
|
$excelData[] = ['شماره فاکتور', 'تاریخ', 'مشتری', 'مبلغ', 'سود', 'درصد سود', 'وضعیت'];
|
||||||
|
|
||||||
|
foreach ($invoices['invoices'] as $invoice) {
|
||||||
|
$excelData[] = [
|
||||||
|
$invoice['code'],
|
||||||
|
$invoice['date'],
|
||||||
|
$invoice['customer']['name'] ?? '',
|
||||||
|
number_format($invoice['amount']) . ' ریال',
|
||||||
|
number_format($invoice['profit']) . ' ریال',
|
||||||
|
$invoice['profitMargin'] . '%',
|
||||||
|
$invoice['isApproved'] ? 'تایید شده' : 'پیشنمایش'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// هدرهای Excel
|
||||||
|
$headers = [
|
||||||
|
'گزارش فروش - ' . $acc['bid']->getName(),
|
||||||
|
'تاریخ شروع: ' . ($startDate ?: 'همه'),
|
||||||
|
'تاریخ پایان: ' . ($endDate ?: 'همه'),
|
||||||
|
'تاریخ ایجاد: ' . $jdate->jdate('Y/m/d H:i:s', time())
|
||||||
|
];
|
||||||
|
|
||||||
|
// ایجاد فایل Excel
|
||||||
|
$filePath = $provider->createExcellFromArray($excelData, $headers);
|
||||||
|
|
||||||
|
return new BinaryFileResponse($filePath);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'result' => 0,
|
||||||
|
'message' => 'خطا در ایجاد گزارش: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
903
hesabixCore/src/Service/SellReportService.php
Normal file
903
hesabixCore/src/Service/SellReportService.php
Normal file
|
@ -0,0 +1,903 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Business;
|
||||||
|
use App\Entity\HesabdariDoc;
|
||||||
|
use App\Entity\HesabdariRow;
|
||||||
|
use App\Entity\Person;
|
||||||
|
use App\Entity\Commodity;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
|
||||||
|
class SellReportService
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
private Connection $connection;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->connection = $entityManager->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت خلاصه آمار فروش
|
||||||
|
*/
|
||||||
|
public function getSellSummary(
|
||||||
|
Business $business,
|
||||||
|
$year,
|
||||||
|
$money,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null,
|
||||||
|
string $groupBy = 'day',
|
||||||
|
?int $customerId = null,
|
||||||
|
?string $status = null
|
||||||
|
): array {
|
||||||
|
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('d')
|
||||||
|
->from(HesabdariDoc::class, 'd')
|
||||||
|
->where('d.bid = :bid')
|
||||||
|
->andWhere('d.year = :year')
|
||||||
|
->andWhere('d.money = :money')
|
||||||
|
->andWhere('d.type = :type')
|
||||||
|
->setParameter('bid', $business)
|
||||||
|
->setParameter('year', $year)
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('type', 'sell');
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$queryBuilder->andWhere('d.date >= :startDate')
|
||||||
|
->setParameter('startDate', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$queryBuilder->andWhere('d.date <= :endDate')
|
||||||
|
->setParameter('endDate', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس وضعیت
|
||||||
|
if ($status) {
|
||||||
|
if ($status === 'approved') {
|
||||||
|
$queryBuilder->andWhere('d.isApproved = :isApproved')
|
||||||
|
->setParameter('isApproved', true);
|
||||||
|
} elseif ($status === 'preview') {
|
||||||
|
$queryBuilder->andWhere('d.isPreview = :isPreview')
|
||||||
|
->setParameter('isPreview', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$docs = $queryBuilder->getQuery()->getResult();
|
||||||
|
|
||||||
|
$totalAmount = 0;
|
||||||
|
$totalCount = 0;
|
||||||
|
$maxAmount = 0;
|
||||||
|
$minAmount = PHP_FLOAT_MAX;
|
||||||
|
$maxDoc = null;
|
||||||
|
$minDoc = null;
|
||||||
|
$totalProfit = 0;
|
||||||
|
|
||||||
|
foreach ($docs as $doc) {
|
||||||
|
// فیلتر بر اساس مشتری
|
||||||
|
if ($customerId) {
|
||||||
|
$hasCustomer = false;
|
||||||
|
foreach ($doc->getHesabdariRows() as $row) {
|
||||||
|
if ($row->getPerson() && $row->getPerson()->getId() == $customerId) {
|
||||||
|
$hasCustomer = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$hasCustomer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$amount = (float) $doc->getAmount();
|
||||||
|
$totalAmount += $amount;
|
||||||
|
$totalCount++;
|
||||||
|
|
||||||
|
if ($amount > $maxAmount) {
|
||||||
|
$maxAmount = $amount;
|
||||||
|
$maxDoc = $doc;
|
||||||
|
}
|
||||||
|
if ($amount < $minAmount && $amount > 0) {
|
||||||
|
$minAmount = $amount;
|
||||||
|
$minDoc = $doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// محاسبه سود
|
||||||
|
$totalProfit += $this->calculateDocProfit($doc, $business);
|
||||||
|
}
|
||||||
|
|
||||||
|
$avgAmount = $totalCount > 0 ? $totalAmount / $totalCount : 0;
|
||||||
|
|
||||||
|
// محاسبه آمار گروهبندی شده
|
||||||
|
$groupedStats = $this->getGroupedStats($docs, $groupBy);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'totalAmount' => round($totalAmount),
|
||||||
|
'totalCount' => $totalCount,
|
||||||
|
'averageAmount' => round($avgAmount),
|
||||||
|
'maxAmount' => round($maxAmount),
|
||||||
|
'minAmount' => $minAmount === PHP_FLOAT_MAX ? 0 : round($minAmount),
|
||||||
|
'maxDoc' => $maxDoc ? [
|
||||||
|
'code' => $maxDoc->getCode(),
|
||||||
|
'date' => $maxDoc->getDate(),
|
||||||
|
'amount' => round($maxDoc->getAmount())
|
||||||
|
] : null,
|
||||||
|
'minDoc' => $minDoc ? [
|
||||||
|
'code' => $minDoc->getCode(),
|
||||||
|
'date' => $minDoc->getDate(),
|
||||||
|
'amount' => round($minDoc->getAmount())
|
||||||
|
] : null,
|
||||||
|
'totalProfit' => round($totalProfit),
|
||||||
|
'profitMargin' => $totalAmount > 0 ? round(($totalProfit / $totalAmount) * 100) : 0,
|
||||||
|
'groupedStats' => $groupedStats
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت لیست فاکتورهای فروش
|
||||||
|
*/
|
||||||
|
public function getSellInvoices(
|
||||||
|
Business $business,
|
||||||
|
$year,
|
||||||
|
$money,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null,
|
||||||
|
?int $customerId = null,
|
||||||
|
?int $commodityId = null,
|
||||||
|
?string $status = null,
|
||||||
|
int $page = 1,
|
||||||
|
int $perPage = 20
|
||||||
|
): array {
|
||||||
|
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('d')
|
||||||
|
->from(HesabdariDoc::class, 'd')
|
||||||
|
->leftJoin('d.submitter', 'u')
|
||||||
|
->where('d.bid = :bid')
|
||||||
|
->andWhere('d.year = :year')
|
||||||
|
->andWhere('d.money = :money')
|
||||||
|
->andWhere('d.type = :type')
|
||||||
|
->setParameter('bid', $business)
|
||||||
|
->setParameter('year', $year)
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('type', 'sell');
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$queryBuilder->andWhere('d.date >= :startDate')
|
||||||
|
->setParameter('startDate', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$queryBuilder->andWhere('d.date <= :endDate')
|
||||||
|
->setParameter('endDate', $endDate);
|
||||||
|
}
|
||||||
|
// فیلتر مشتری بعداً در PHP اعمال میشود
|
||||||
|
if ($status) {
|
||||||
|
if ($status === 'approved') {
|
||||||
|
$queryBuilder->andWhere('d.isApproved = :isApproved')
|
||||||
|
->setParameter('isApproved', true);
|
||||||
|
} elseif ($status === 'preview') {
|
||||||
|
$queryBuilder->andWhere('d.isPreview = :isPreview')
|
||||||
|
->setParameter('isPreview', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// شمارش کل رکوردها (بدون فیلتر مشتری)
|
||||||
|
$countQuery = clone $queryBuilder;
|
||||||
|
$totalCount = $countQuery->select('COUNT(DISTINCT d.id)')->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
// دریافت رکوردها با pagination
|
||||||
|
$queryBuilder->setFirstResult(($page - 1) * $perPage)
|
||||||
|
->setMaxResults($perPage)
|
||||||
|
->orderBy('d.id', 'DESC');
|
||||||
|
|
||||||
|
$docs = $queryBuilder->getQuery()->getResult();
|
||||||
|
|
||||||
|
$invoices = [];
|
||||||
|
foreach ($docs as $doc) {
|
||||||
|
$customer = null;
|
||||||
|
$profit = 0;
|
||||||
|
|
||||||
|
// پیدا کردن مشتری
|
||||||
|
foreach ($doc->getHesabdariRows() as $row) {
|
||||||
|
if ($row->getPerson()) {
|
||||||
|
$customer = $row->getPerson();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس مشتری
|
||||||
|
if ($customerId && (!$customer || $customer->getId() != $customerId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// محاسبه سود
|
||||||
|
$profit = $this->calculateDocProfit($doc, $business);
|
||||||
|
|
||||||
|
// فیلتر بر اساس کالا
|
||||||
|
if ($commodityId) {
|
||||||
|
$hasCommodity = false;
|
||||||
|
foreach ($doc->getHesabdariRows() as $row) {
|
||||||
|
if ($row->getCommodity() && $row->getCommodity()->getId() == $commodityId) {
|
||||||
|
$hasCommodity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$hasCommodity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoices[] = [
|
||||||
|
'id' => $doc->getId(),
|
||||||
|
'code' => $doc->getCode(),
|
||||||
|
'date' => $doc->getDate(),
|
||||||
|
'amount' => round($doc->getAmount()),
|
||||||
|
'description' => $doc->getDes(),
|
||||||
|
'customer' => $customer ? [
|
||||||
|
'id' => $customer->getId(),
|
||||||
|
'name' => $customer->getNikename(),
|
||||||
|
'code' => $customer->getCode()
|
||||||
|
] : null,
|
||||||
|
'submitter' => $doc->getSubmitter() ? [
|
||||||
|
'id' => $doc->getSubmitter()->getId(),
|
||||||
|
'name' => $doc->getSubmitter()->getFullName()
|
||||||
|
] : null,
|
||||||
|
'isApproved' => $doc->isApproved(),
|
||||||
|
'isPreview' => $doc->isPreview(),
|
||||||
|
'profit' => round($profit),
|
||||||
|
'profitMargin' => $doc->getAmount() > 0 ? round(($profit / $doc->getAmount()) * 100) : 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'invoices' => $invoices,
|
||||||
|
'total' => $totalCount,
|
||||||
|
'page' => $page,
|
||||||
|
'perPage' => $perPage,
|
||||||
|
'totalPages' => ceil($totalCount / $perPage)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت کالاهای پرفروش
|
||||||
|
*/
|
||||||
|
public function getTopProducts(
|
||||||
|
Business $business,
|
||||||
|
$year,
|
||||||
|
$money,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null,
|
||||||
|
int $limit = 10,
|
||||||
|
string $sortBy = 'amount',
|
||||||
|
?int $customerId = null,
|
||||||
|
?string $status = null
|
||||||
|
): array {
|
||||||
|
// استفاده از Query Builder به جای SQL خام
|
||||||
|
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('c.id, c.name, c.code')
|
||||||
|
->addSelect('SUM(hr.bs) as total_amount')
|
||||||
|
->addSelect('SUM(hr.commdityCount) as total_count')
|
||||||
|
->addSelect('COUNT(DISTINCT hd.id) as invoice_count')
|
||||||
|
->from(HesabdariDoc::class, 'hd')
|
||||||
|
->join('hd.hesabdariRows', 'hr')
|
||||||
|
->join('hr.commodity', 'c')
|
||||||
|
->where('hd.bid = :bid')
|
||||||
|
->andWhere('hd.year = :year')
|
||||||
|
->andWhere('hd.money = :money')
|
||||||
|
->andWhere('hd.type = :type')
|
||||||
|
->andWhere('hr.commodity IS NOT NULL')
|
||||||
|
->setParameter('bid', $business)
|
||||||
|
->setParameter('year', $year)
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('type', 'sell');
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date >= :startDate')
|
||||||
|
->setParameter('startDate', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date <= :endDate')
|
||||||
|
->setParameter('endDate', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس مشتری
|
||||||
|
if ($customerId) {
|
||||||
|
$queryBuilder->andWhere('hr.person = :customerId')
|
||||||
|
->setParameter('customerId', $customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس وضعیت
|
||||||
|
if ($status) {
|
||||||
|
if ($status === 'approved') {
|
||||||
|
$queryBuilder->andWhere('hd.isApproved = :isApproved')
|
||||||
|
->setParameter('isApproved', true);
|
||||||
|
} elseif ($status === 'preview') {
|
||||||
|
$queryBuilder->andWhere('hd.isPreview = :isPreview')
|
||||||
|
->setParameter('isPreview', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryBuilder->groupBy('c.id, c.name, c.code');
|
||||||
|
|
||||||
|
// مرتبسازی
|
||||||
|
switch ($sortBy) {
|
||||||
|
case 'count':
|
||||||
|
$queryBuilder->orderBy('total_count', 'DESC');
|
||||||
|
break;
|
||||||
|
case 'amount':
|
||||||
|
default:
|
||||||
|
$queryBuilder->orderBy('total_amount', 'DESC');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryBuilder->setMaxResults($limit);
|
||||||
|
|
||||||
|
$results = $queryBuilder->getQuery()->getArrayResult();
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
foreach ($results as $result) {
|
||||||
|
// محاسبه سود برای هر کالا
|
||||||
|
$profit = $this->calculateProductProfit($result['id'], $business, $startDate, $endDate);
|
||||||
|
|
||||||
|
$products[] = [
|
||||||
|
'id' => $result['id'],
|
||||||
|
'name' => $result['name'],
|
||||||
|
'code' => $result['code'],
|
||||||
|
'totalAmount' => round((float) $result['total_amount']),
|
||||||
|
'totalCount' => round((float) $result['total_count']),
|
||||||
|
'invoiceCount' => (int) $result['invoice_count'],
|
||||||
|
'profit' => round($profit),
|
||||||
|
'profitMargin' => $result['total_amount'] > 0 ? round(($profit / $result['total_amount']) * 100) : 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت مشتریان پرفروش
|
||||||
|
*/
|
||||||
|
public function getTopCustomers(
|
||||||
|
Business $business,
|
||||||
|
$year,
|
||||||
|
$money,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null,
|
||||||
|
int $limit = 10,
|
||||||
|
?int $customerId = null,
|
||||||
|
?string $status = null
|
||||||
|
): array {
|
||||||
|
// استفاده از Query Builder به جای SQL خام
|
||||||
|
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('p.id, p.nikename, p.code, p.mobile')
|
||||||
|
->addSelect('COUNT(DISTINCT hd.id) as invoice_count')
|
||||||
|
->addSelect('SUM(hd.amount) as total_amount')
|
||||||
|
->from(HesabdariDoc::class, 'hd')
|
||||||
|
->join('hd.hesabdariRows', 'hr')
|
||||||
|
->join('hr.person', 'p')
|
||||||
|
->where('hd.bid = :bid')
|
||||||
|
->andWhere('hd.year = :year')
|
||||||
|
->andWhere('hd.money = :money')
|
||||||
|
->andWhere('hd.type = :type')
|
||||||
|
->andWhere('hr.person IS NOT NULL')
|
||||||
|
->setParameter('bid', $business)
|
||||||
|
->setParameter('year', $year)
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('type', 'sell');
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date >= :startDate')
|
||||||
|
->setParameter('startDate', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date <= :endDate')
|
||||||
|
->setParameter('endDate', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس مشتری (برای getTopCustomers منطقی نیست، اما برای سازگاری)
|
||||||
|
if ($customerId) {
|
||||||
|
$queryBuilder->andWhere('p.id = :customerId')
|
||||||
|
->setParameter('customerId', $customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس وضعیت
|
||||||
|
if ($status) {
|
||||||
|
if ($status === 'approved') {
|
||||||
|
$queryBuilder->andWhere('hd.isApproved = :isApproved')
|
||||||
|
->setParameter('isApproved', true);
|
||||||
|
} elseif ($status === 'preview') {
|
||||||
|
$queryBuilder->andWhere('hd.isPreview = :isPreview')
|
||||||
|
->setParameter('isPreview', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryBuilder->groupBy('p.id, p.nikename, p.code, p.mobile')
|
||||||
|
->orderBy('total_amount', 'DESC')
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
$results = $queryBuilder->getQuery()->getArrayResult();
|
||||||
|
|
||||||
|
$customers = [];
|
||||||
|
foreach ($results as $result) {
|
||||||
|
$customers[] = [
|
||||||
|
'id' => $result['id'],
|
||||||
|
'name' => $result['nikename'],
|
||||||
|
'code' => $result['code'],
|
||||||
|
'mobile' => $result['mobile'],
|
||||||
|
'invoiceCount' => (int) $result['invoice_count'],
|
||||||
|
'totalAmount' => round((float) $result['total_amount']),
|
||||||
|
'averageAmount' => $result['invoice_count'] > 0 ? round($result['total_amount'] / $result['invoice_count']) : 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $customers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت دادههای نمودار
|
||||||
|
*/
|
||||||
|
public function getSellChart(
|
||||||
|
Business $business,
|
||||||
|
$year,
|
||||||
|
$money,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null,
|
||||||
|
string $groupBy = 'day',
|
||||||
|
string $type = 'amount'
|
||||||
|
): array {
|
||||||
|
// استفاده از Query Builder ساده
|
||||||
|
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('hd.date')
|
||||||
|
->addSelect('COUNT(DISTINCT hd.id) as invoice_count')
|
||||||
|
->addSelect('SUM(hd.amount) as total_amount')
|
||||||
|
->from(HesabdariDoc::class, 'hd')
|
||||||
|
->where('hd.bid = :bid')
|
||||||
|
->andWhere('hd.year = :year')
|
||||||
|
->andWhere('hd.money = :money')
|
||||||
|
->andWhere('hd.type = :type')
|
||||||
|
->setParameter('bid', $business)
|
||||||
|
->setParameter('year', $year)
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('type', 'sell');
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date >= :startDate')
|
||||||
|
->setParameter('startDate', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date <= :endDate')
|
||||||
|
->setParameter('endDate', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryBuilder->groupBy('hd.date')
|
||||||
|
->orderBy('hd.date', 'ASC');
|
||||||
|
|
||||||
|
$results = $queryBuilder->getQuery()->getArrayResult();
|
||||||
|
|
||||||
|
$labels = [];
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
// گروهبندی در PHP
|
||||||
|
$groupedData = [];
|
||||||
|
foreach ($results as $result) {
|
||||||
|
$date = $result['date'];
|
||||||
|
$groupKey = $this->getGroupKey($date, $groupBy);
|
||||||
|
|
||||||
|
if (!isset($groupedData[$groupKey])) {
|
||||||
|
$groupedData[$groupKey] = [
|
||||||
|
'invoice_count' => 0,
|
||||||
|
'total_amount' => 0,
|
||||||
|
'label' => $this->formatGroupLabel($date, $groupBy)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$groupedData[$groupKey]['invoice_count'] += (int) $result['invoice_count'];
|
||||||
|
$groupedData[$groupKey]['total_amount'] += (float) $result['total_amount'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// مرتبسازی بر اساس کلید
|
||||||
|
ksort($groupedData);
|
||||||
|
|
||||||
|
foreach ($groupedData as $group) {
|
||||||
|
$labels[] = $group['label'];
|
||||||
|
if ($type === 'count') {
|
||||||
|
$data[] = $group['invoice_count'];
|
||||||
|
} else {
|
||||||
|
$data[] = round($group['total_amount']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'labels' => $labels,
|
||||||
|
'data' => $data,
|
||||||
|
'type' => $type,
|
||||||
|
'groupBy' => $groupBy
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تولید کلید گروهبندی
|
||||||
|
*/
|
||||||
|
private function getGroupKey(string $date, string $groupBy): string
|
||||||
|
{
|
||||||
|
$parts = explode('/', $date);
|
||||||
|
if (count($parts) !== 3) {
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = $parts[0];
|
||||||
|
$month = $parts[1];
|
||||||
|
$day = $parts[2];
|
||||||
|
|
||||||
|
switch ($groupBy) {
|
||||||
|
case 'week':
|
||||||
|
// محاسبه هفته (ساده)
|
||||||
|
$dayOfYear = $this->getDayOfYear($year, $month, $day);
|
||||||
|
$week = ceil($dayOfYear / 7);
|
||||||
|
return $year . '-' . str_pad($week, 2, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
case 'month':
|
||||||
|
return $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
case 'day':
|
||||||
|
default:
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فرمت کردن برچسب گروه
|
||||||
|
*/
|
||||||
|
private function formatGroupLabel(string $date, string $groupBy): string
|
||||||
|
{
|
||||||
|
$parts = explode('/', $date);
|
||||||
|
if (count($parts) !== 3) {
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = $parts[0];
|
||||||
|
$month = $parts[1];
|
||||||
|
$day = $parts[2];
|
||||||
|
|
||||||
|
switch ($groupBy) {
|
||||||
|
case 'week':
|
||||||
|
$dayOfYear = $this->getDayOfYear($year, $month, $day);
|
||||||
|
$week = ceil($dayOfYear / 7);
|
||||||
|
return 'هفته ' . $week;
|
||||||
|
|
||||||
|
case 'month':
|
||||||
|
$monthNames = [
|
||||||
|
'01' => 'فروردین',
|
||||||
|
'02' => 'اردیبهشت',
|
||||||
|
'03' => 'خرداد',
|
||||||
|
'04' => 'تیر',
|
||||||
|
'05' => 'مرداد',
|
||||||
|
'06' => 'شهریور',
|
||||||
|
'07' => 'مهر',
|
||||||
|
'08' => 'آبان',
|
||||||
|
'09' => 'آذر',
|
||||||
|
'10' => 'دی',
|
||||||
|
'11' => 'بهمن',
|
||||||
|
'12' => 'اسفند'
|
||||||
|
];
|
||||||
|
$monthName = $monthNames[$month] ?? $month;
|
||||||
|
return $monthName . ' ' . $year;
|
||||||
|
|
||||||
|
case 'day':
|
||||||
|
default:
|
||||||
|
return $month . '/' . $day;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* محاسبه روز سال
|
||||||
|
*/
|
||||||
|
private function getDayOfYear(string $year, string $month, string $day): int
|
||||||
|
{
|
||||||
|
$monthDays = [
|
||||||
|
'01' => 31, '02' => 31, '03' => 31, '04' => 31, '05' => 31, '06' => 31,
|
||||||
|
'07' => 30, '08' => 30, '09' => 30, '10' => 30, '11' => 30, '12' => 29
|
||||||
|
];
|
||||||
|
|
||||||
|
$dayOfYear = (int) $day;
|
||||||
|
for ($m = 1; $m < (int) $month; $m++) {
|
||||||
|
$monthKey = str_pad($m, 2, '0', STR_PAD_LEFT);
|
||||||
|
$dayOfYear += $monthDays[$monthKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dayOfYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فرمت کردن برچسب تاریخ
|
||||||
|
*/
|
||||||
|
private function formatDateLabel(string $date): string
|
||||||
|
{
|
||||||
|
// تبدیل تاریخ شمسی به فرمت مناسب
|
||||||
|
$parts = explode('/', $date);
|
||||||
|
if (count($parts) === 3) {
|
||||||
|
return $parts[1] . '/' . $parts[2]; // ماه/روز
|
||||||
|
}
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فرمت کردن برچسب ماه
|
||||||
|
*/
|
||||||
|
private function formatMonthLabel(string $yearMonth): string
|
||||||
|
{
|
||||||
|
$parts = explode('-', $yearMonth);
|
||||||
|
if (count($parts) === 2) {
|
||||||
|
$year = $parts[0];
|
||||||
|
$month = $parts[1];
|
||||||
|
|
||||||
|
$monthNames = [
|
||||||
|
'01' => 'فروردین',
|
||||||
|
'02' => 'اردیبهشت',
|
||||||
|
'03' => 'خرداد',
|
||||||
|
'04' => 'تیر',
|
||||||
|
'05' => 'مرداد',
|
||||||
|
'06' => 'شهریور',
|
||||||
|
'07' => 'مهر',
|
||||||
|
'08' => 'آبان',
|
||||||
|
'09' => 'آذر',
|
||||||
|
'10' => 'دی',
|
||||||
|
'11' => 'بهمن',
|
||||||
|
'12' => 'اسفند'
|
||||||
|
];
|
||||||
|
|
||||||
|
$monthName = $monthNames[$month] ?? $month;
|
||||||
|
return $monthName . ' ' . $year;
|
||||||
|
}
|
||||||
|
return $yearMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تحلیل مشتریان
|
||||||
|
*/
|
||||||
|
public function getCustomerAnalysis(
|
||||||
|
Business $business,
|
||||||
|
$year,
|
||||||
|
$money,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null
|
||||||
|
): array {
|
||||||
|
// مشتریان جدید
|
||||||
|
$newCustomersSql = "
|
||||||
|
SELECT COUNT(DISTINCT p.id) as new_customers
|
||||||
|
FROM person p
|
||||||
|
JOIN hesabdari_row hr ON hr.person_id = p.id
|
||||||
|
JOIN hesabdari_doc hd ON hd.id = hr.doc_id
|
||||||
|
WHERE hd.bid_id = :bid_id
|
||||||
|
AND hd.year_id = :year_id
|
||||||
|
AND hd.money_id = :money_id
|
||||||
|
AND hd.type = 'sell'
|
||||||
|
AND hd.date >= :start_date
|
||||||
|
AND hd.date <= :end_date
|
||||||
|
AND p.id NOT IN (
|
||||||
|
SELECT DISTINCT hr2.person_id
|
||||||
|
FROM hesabdari_row hr2
|
||||||
|
JOIN hesabdari_doc hd2 ON hd2.id = hr2.doc_id
|
||||||
|
WHERE hd2.bid_id = :bid_id
|
||||||
|
AND hd2.year_id = :year_id
|
||||||
|
AND hd2.money_id = :money_id
|
||||||
|
AND hd2.type = 'sell'
|
||||||
|
AND hd2.date < :start_date
|
||||||
|
)
|
||||||
|
";
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'bid_id' => $business->getId(),
|
||||||
|
'year_id' => $year->getId(),
|
||||||
|
'money_id' => $money->getId(),
|
||||||
|
'start_date' => $startDate ?? '1400/01/01',
|
||||||
|
'end_date' => $endDate ?? '1500/12/29'
|
||||||
|
];
|
||||||
|
|
||||||
|
$stmt = $this->connection->prepare($newCustomersSql);
|
||||||
|
$stmt->executeQuery($params);
|
||||||
|
$newCustomers = $stmt->fetchAssociative();
|
||||||
|
|
||||||
|
// کل مشتریان
|
||||||
|
$totalCustomersSql = "
|
||||||
|
SELECT COUNT(DISTINCT p.id) as total_customers
|
||||||
|
FROM person p
|
||||||
|
JOIN hesabdari_row hr ON hr.person_id = p.id
|
||||||
|
JOIN hesabdari_doc hd ON hd.id = hr.doc_id
|
||||||
|
WHERE hd.bid_id = :bid_id
|
||||||
|
AND hd.year_id = :year_id
|
||||||
|
AND hd.money_id = :money_id
|
||||||
|
AND hd.type = 'sell'
|
||||||
|
AND hd.date >= :start_date
|
||||||
|
AND hd.date <= :end_date
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $this->connection->prepare($totalCustomersSql);
|
||||||
|
$stmt->executeQuery($params);
|
||||||
|
$totalCustomers = $stmt->fetchAssociative();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'newCustomers' => (int) $newCustomers['new_customers'],
|
||||||
|
'totalCustomers' => (int) $totalCustomers['total_customers'],
|
||||||
|
'repeatCustomers' => (int) $totalCustomers['total_customers'] - (int) $newCustomers['new_customers']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* محاسبه سود یک فاکتور
|
||||||
|
*/
|
||||||
|
private function calculateDocProfit(HesabdariDoc $doc, Business $business): float
|
||||||
|
{
|
||||||
|
$profit = 0;
|
||||||
|
$profitCalcType = $business->getProfitCalctype() ?? 'simple';
|
||||||
|
|
||||||
|
foreach ($doc->getHesabdariRows() as $row) {
|
||||||
|
if ($row->getCommodity() && $row->getCommdityCount()) {
|
||||||
|
$commodity = $row->getCommodity();
|
||||||
|
$count = $row->getCommdityCount();
|
||||||
|
$sellPrice = $row->getBs() / $count;
|
||||||
|
|
||||||
|
if ($profitCalcType === 'simple') {
|
||||||
|
$buyPrice = $commodity->getPriceBuy() ? (float) $commodity->getPriceBuy() : 0;
|
||||||
|
$profit += ($sellPrice - $buyPrice) * $count;
|
||||||
|
} elseif ($profitCalcType === 'lis') {
|
||||||
|
// LIFO - آخرین ورودی، اولین خروجی
|
||||||
|
$lastBuyRow = $this->entityManager->getRepository(HesabdariRow::class)
|
||||||
|
->findOneBy([
|
||||||
|
'commodity' => $commodity,
|
||||||
|
'bs' => 0
|
||||||
|
], ['id' => 'DESC']);
|
||||||
|
|
||||||
|
if ($lastBuyRow && $lastBuyRow->getCommdityCount() > 0) {
|
||||||
|
$buyPrice = $lastBuyRow->getBd() / $lastBuyRow->getCommdityCount();
|
||||||
|
$profit += ($sellPrice - $buyPrice) * $count;
|
||||||
|
} else {
|
||||||
|
$profit += $row->getBs();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Average - میانگین قیمت خرید
|
||||||
|
$buyRows = $this->entityManager->getRepository(HesabdariRow::class)
|
||||||
|
->findBy([
|
||||||
|
'commodity' => $commodity,
|
||||||
|
'bs' => 0
|
||||||
|
], ['id' => 'DESC']);
|
||||||
|
|
||||||
|
$totalBuyAmount = 0;
|
||||||
|
$totalBuyCount = 0;
|
||||||
|
foreach ($buyRows as $buyRow) {
|
||||||
|
$totalBuyAmount += $buyRow->getBd();
|
||||||
|
$totalBuyCount += $buyRow->getCommdityCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalBuyCount > 0) {
|
||||||
|
$avgBuyPrice = $totalBuyAmount / $totalBuyCount;
|
||||||
|
$profit += ($sellPrice - $avgBuyPrice) * $count;
|
||||||
|
} else {
|
||||||
|
$profit += $row->getBs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $profit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* محاسبه سود یک کالا
|
||||||
|
*/
|
||||||
|
private function calculateProductProfit(int $commodityId, Business $business, ?string $startDate, ?string $endDate): float
|
||||||
|
{
|
||||||
|
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('hr')
|
||||||
|
->from(HesabdariRow::class, 'hr')
|
||||||
|
->join('hr.doc', 'hd')
|
||||||
|
->where('hr.commodity = :commodityId')
|
||||||
|
->andWhere('hd.type = :type')
|
||||||
|
->andWhere('hd.bid = :bid')
|
||||||
|
->setParameter('commodityId', $commodityId)
|
||||||
|
->setParameter('type', 'sell')
|
||||||
|
->setParameter('bid', $business);
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date >= :startDate')
|
||||||
|
->setParameter('startDate', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$queryBuilder->andWhere('hd.date <= :endDate')
|
||||||
|
->setParameter('endDate', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $queryBuilder->getQuery()->getResult();
|
||||||
|
|
||||||
|
$profit = 0;
|
||||||
|
$profitCalcType = $business->getProfitCalctype() ?? 'simple';
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if ($row->getCommdityCount()) {
|
||||||
|
$commodity = $row->getCommodity();
|
||||||
|
$count = $row->getCommdityCount();
|
||||||
|
$sellPrice = $row->getBs() / $count;
|
||||||
|
|
||||||
|
if ($profitCalcType === 'simple') {
|
||||||
|
$buyPrice = $commodity->getPriceBuy() ? (float) $commodity->getPriceBuy() : 0;
|
||||||
|
$profit += ($sellPrice - $buyPrice) * $count;
|
||||||
|
} elseif ($profitCalcType === 'lis') {
|
||||||
|
$lastBuyRow = $this->entityManager->getRepository(HesabdariRow::class)
|
||||||
|
->findOneBy([
|
||||||
|
'commodity' => $commodity,
|
||||||
|
'bs' => 0
|
||||||
|
], ['id' => 'DESC']);
|
||||||
|
|
||||||
|
if ($lastBuyRow && $lastBuyRow->getCommdityCount() > 0) {
|
||||||
|
$buyPrice = $lastBuyRow->getBd() / $lastBuyRow->getCommdityCount();
|
||||||
|
$profit += ($sellPrice - $buyPrice) * $count;
|
||||||
|
} else {
|
||||||
|
$profit += $row->getBs();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$buyRows = $this->entityManager->getRepository(HesabdariRow::class)
|
||||||
|
->findBy([
|
||||||
|
'commodity' => $commodity,
|
||||||
|
'bs' => 0
|
||||||
|
], ['id' => 'DESC']);
|
||||||
|
|
||||||
|
$totalBuyAmount = 0;
|
||||||
|
$totalBuyCount = 0;
|
||||||
|
foreach ($buyRows as $buyRow) {
|
||||||
|
$totalBuyAmount += $buyRow->getBd();
|
||||||
|
$totalBuyCount += $buyRow->getCommdityCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalBuyCount > 0) {
|
||||||
|
$avgBuyPrice = $totalBuyAmount / $totalBuyCount;
|
||||||
|
$profit += ($sellPrice - $avgBuyPrice) * $count;
|
||||||
|
} else {
|
||||||
|
$profit += $row->getBs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $profit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت آمار گروهبندی شده
|
||||||
|
*/
|
||||||
|
private function getGroupedStats(array $docs, string $groupBy): array
|
||||||
|
{
|
||||||
|
$grouped = [];
|
||||||
|
|
||||||
|
foreach ($docs as $doc) {
|
||||||
|
$date = $doc->getDate();
|
||||||
|
$amount = (float) $doc->getAmount();
|
||||||
|
|
||||||
|
if ($groupBy === 'day') {
|
||||||
|
$key = $date;
|
||||||
|
} elseif ($groupBy === 'month') {
|
||||||
|
// استخراج ماه از تاریخ شمسی
|
||||||
|
$parts = explode('/', $date);
|
||||||
|
$key = $parts[0] . '/' . $parts[1];
|
||||||
|
} else {
|
||||||
|
$key = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($grouped[$key])) {
|
||||||
|
$grouped[$key] = [
|
||||||
|
'date' => $key,
|
||||||
|
'amount' => 0,
|
||||||
|
'count' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$grouped[$key]['amount'] += $amount;
|
||||||
|
$grouped[$key]['count']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// مرتبسازی بر اساس تاریخ
|
||||||
|
ksort($grouped);
|
||||||
|
|
||||||
|
return array_values($grouped);
|
||||||
|
}
|
||||||
|
}
|
|
@ -320,6 +320,16 @@ const router = createRouter({
|
||||||
component: () =>
|
component: () =>
|
||||||
import('../views/acc/reports/persons/withdet.vue'),
|
import('../views/acc/reports/persons/withdet.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'reports/sell',
|
||||||
|
name: 'sell_report',
|
||||||
|
component: () =>
|
||||||
|
import('../views/acc/reports/sell/SellReport.vue'),
|
||||||
|
meta: {
|
||||||
|
'title': 'گزارش فروش',
|
||||||
|
'login': true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'costs/list',
|
path: 'costs/list',
|
||||||
name: 'costs_list',
|
name: 'costs_list',
|
||||||
|
|
|
@ -358,7 +358,10 @@
|
||||||
this.errorDialog = true;
|
this.errorDialog = true;
|
||||||
},
|
},
|
||||||
formatNumber(value) {
|
formatNumber(value) {
|
||||||
return value ? Number(value).toLocaleString('fa-IR') : '0';
|
if (!value) return '0';
|
||||||
|
// گرد کردن عدد به نزدیکترین عدد صحیح
|
||||||
|
const roundedValue = Math.round(Number(value));
|
||||||
|
return roundedValue.toLocaleString('fa-IR');
|
||||||
},
|
},
|
||||||
formatDateForDisplay(dateString) {
|
formatDateForDisplay(dateString) {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
|
|
|
@ -95,6 +95,24 @@
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
|
<!-- فروش (conditional) -->
|
||||||
|
<v-col v-if="isPluginActive('accpro')" cols="12" md="6" lg="4">
|
||||||
|
<v-card outlined class="report-card">
|
||||||
|
<v-card-subtitle class="card-title">
|
||||||
|
<v-icon class="mr-2">mdi-cart</v-icon>
|
||||||
|
فروش
|
||||||
|
</v-card-subtitle>
|
||||||
|
<v-list dense class="report-list">
|
||||||
|
<v-list-item v-for="item in salesReports" :key="item.to" :to="item.to" class="list-item">
|
||||||
|
<template v-slot:default>
|
||||||
|
<v-icon small class="mr-2">mdi-chevron-left</v-icon>
|
||||||
|
{{ item.text }}
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
@ -122,6 +140,9 @@ export default {
|
||||||
accountingReports: [
|
accountingReports: [
|
||||||
{ text: 'ترازنامه', to: '/acc/reports/acc/balance_sheet' },
|
{ text: 'ترازنامه', to: '/acc/reports/acc/balance_sheet' },
|
||||||
{ text: this.$t('dialog.explore_accounts'), to: '/acc/reports/acc/explore_accounts' }
|
{ text: this.$t('dialog.explore_accounts'), to: '/acc/reports/acc/explore_accounts' }
|
||||||
|
],
|
||||||
|
salesReports: [
|
||||||
|
{ text: 'گزارش فروش', to: '/acc/reports/sell' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
906
webUI/src/views/acc/reports/sell/SellReport.vue
Normal file
906
webUI/src/views/acc/reports/sell/SellReport.vue
Normal file
|
@ -0,0 +1,906 @@
|
||||||
|
<template>
|
||||||
|
<v-card :loading="loading ? 'red' : null" :disabled="loading">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<v-toolbar color="toolbar" title="گزارش فروش" flat>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Summary Cards -->
|
||||||
|
<v-card-text class="pt-0">
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 primary--text">{{ formatNumber(chartType === 'count' ? summary.totalCount : summary.totalAmount, chartType === 'count' ? true : false) }}</div>
|
||||||
|
<div class="text-subtitle-1">{{ chartType === 'count' ? 'تعداد فاکتور' : 'کل فروش' }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 success--text">{{ formatNumber(summary.totalCount, true) }}</div>
|
||||||
|
<div class="text-subtitle-1">تعداد فاکتور</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 warning--text">{{ formatNumber(summary.totalProfit, false) }}</div>
|
||||||
|
<div class="text-subtitle-1">کل سود</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 info--text">{{ formatNumber(summary.averageAmount, false) }}</div>
|
||||||
|
<div class="text-subtitle-1">میانگین فاکتور</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Chart -->
|
||||||
|
<v-card-text class="pt-0">
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<v-card-title>نمودار فروش</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<apexchart
|
||||||
|
type="line"
|
||||||
|
height="300"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="chartSeries"
|
||||||
|
></apexchart>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Top Products and Customers -->
|
||||||
|
<v-card-text class="pt-0">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
کالاهای پرفروش
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-select
|
||||||
|
v-model="productSortBy"
|
||||||
|
:items="productSortOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
label="مرتبسازی"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
style="max-width: 150px;"
|
||||||
|
@change="loadTopProducts"
|
||||||
|
></v-select>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-data-table
|
||||||
|
:headers="productHeaders"
|
||||||
|
:items="topProducts"
|
||||||
|
:loading="loadingProducts"
|
||||||
|
hide-default-footer
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<template #item.totalAmount="{ item }">
|
||||||
|
{{ formatNumber(item.totalAmount, false) }}
|
||||||
|
</template>
|
||||||
|
<template #item.profit="{ item }">
|
||||||
|
{{ formatNumber(item.profit, false) }}
|
||||||
|
</template>
|
||||||
|
<template #item.profitMargin="{ item }">
|
||||||
|
{{ item.profitMargin }}%
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>مشتریان پرفروش</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-data-table
|
||||||
|
:headers="customerHeaders"
|
||||||
|
:items="topCustomers"
|
||||||
|
:loading="loadingCustomers"
|
||||||
|
hide-default-footer
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<template #item.totalAmount="{ item }">
|
||||||
|
{{ formatNumber(item.totalAmount, false) }}
|
||||||
|
</template>
|
||||||
|
<template #item.averageAmount="{ item }">
|
||||||
|
{{ formatNumber(item.averageAmount, false) }}
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Invoices Table -->
|
||||||
|
<v-card-text class="pt-0">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>لیست فاکتورها</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-data-table
|
||||||
|
:headers="invoiceHeaders"
|
||||||
|
:items="invoices"
|
||||||
|
:loading="loadingInvoices"
|
||||||
|
:items-per-page="20"
|
||||||
|
hide-default-footer
|
||||||
|
>
|
||||||
|
<template #item.amount="{ item }">
|
||||||
|
{{ formatNumber(item.amount, false) }}
|
||||||
|
</template>
|
||||||
|
<template #item.profit="{ item }">
|
||||||
|
{{ formatNumber(item.profit, false) }}
|
||||||
|
</template>
|
||||||
|
<template #item.profitMargin="{ item }">
|
||||||
|
{{ item.profitMargin }}%
|
||||||
|
</template>
|
||||||
|
<template #item.status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="item.isApproved ? 'success' : 'warning'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ item.isApproved ? 'تایید شده' : 'پیشنمایش' }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="primary"
|
||||||
|
@click="viewInvoice(item)"
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<v-row class="mt-2" justify="center" align="center">
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
icon
|
||||||
|
:disabled="invoicePage === 1"
|
||||||
|
@click="changeInvoicePage(invoicePage - 1)"
|
||||||
|
>
|
||||||
|
<v-icon small>mdi-chevron-right</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<span class="mx-2 text-caption">
|
||||||
|
صفحه {{ invoicePage }} از {{ totalInvoicePages }}
|
||||||
|
</span>
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
icon
|
||||||
|
:disabled="invoicePage === totalInvoicePages"
|
||||||
|
@click="changeInvoicePage(invoicePage + 1)"
|
||||||
|
>
|
||||||
|
<v-icon small>mdi-chevron-left</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Error Dialog -->
|
||||||
|
<v-dialog v-model="errorDialog" max-width="500">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-error">خطا</v-card-title>
|
||||||
|
<v-card-text>{{ errorMessage }}</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" @click="errorDialog = false">بستن</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
import { format } from 'date-fns-jalali';
|
||||||
|
import moment from 'jalali-moment';
|
||||||
|
import Hpersonsearch from '@/components/forms/Hpersonsearch.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SellReport',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
Hpersonsearch
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
exporting: false,
|
||||||
|
errorDialog: false,
|
||||||
|
errorMessage: '',
|
||||||
|
|
||||||
|
// Date Filter
|
||||||
|
dateFilter: {
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
},
|
||||||
|
year: {
|
||||||
|
start: '',
|
||||||
|
end: '',
|
||||||
|
},
|
||||||
|
gregorianStartDate: '',
|
||||||
|
gregorianEndDate: '',
|
||||||
|
showStartDatePicker: false,
|
||||||
|
showEndDatePicker: false,
|
||||||
|
|
||||||
|
// Additional Filters
|
||||||
|
filters: {
|
||||||
|
customerId: null,
|
||||||
|
status: null,
|
||||||
|
groupBy: 'day',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Data
|
||||||
|
summary: {
|
||||||
|
totalAmount: 0,
|
||||||
|
totalCount: 0,
|
||||||
|
totalProfit: 0,
|
||||||
|
averageAmount: 0,
|
||||||
|
maxAmount: 0,
|
||||||
|
minAmount: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart
|
||||||
|
chartType: 'amount',
|
||||||
|
chartOptions: {
|
||||||
|
chart: {
|
||||||
|
type: 'line',
|
||||||
|
fontFamily: 'Vazir, sans-serif',
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: [],
|
||||||
|
labels: {
|
||||||
|
style: {
|
||||||
|
fontFamily: 'Vazir, sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: (value) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value);
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
fontFamily: 'Vazir, sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
y: {
|
||||||
|
formatter: (value) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value) + ' ریال';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chartSeries: [
|
||||||
|
{
|
||||||
|
name: 'مبلغ فروش',
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Top Products
|
||||||
|
topProducts: [],
|
||||||
|
loadingProducts: false,
|
||||||
|
productSortBy: 'amount',
|
||||||
|
|
||||||
|
// Top Customers
|
||||||
|
topCustomers: [],
|
||||||
|
loadingCustomers: false,
|
||||||
|
|
||||||
|
// Invoices
|
||||||
|
invoices: [],
|
||||||
|
loadingInvoices: false,
|
||||||
|
invoicePage: 1,
|
||||||
|
totalInvoices: 0,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Options
|
||||||
|
statusOptions: [
|
||||||
|
{ text: 'همه', value: null },
|
||||||
|
{ text: 'تایید شده', value: 'approved' },
|
||||||
|
{ text: 'پیشنمایش', value: 'preview' },
|
||||||
|
],
|
||||||
|
|
||||||
|
groupByOptions: [
|
||||||
|
{ text: 'روزانه', value: 'day' },
|
||||||
|
{ text: 'هفتگی', value: 'week' },
|
||||||
|
{ text: 'ماهانه', value: 'month' },
|
||||||
|
],
|
||||||
|
|
||||||
|
chartTypeOptions: [
|
||||||
|
{ text: 'مبلغ فروش', value: 'amount' },
|
||||||
|
{ text: 'تعداد فاکتور', value: 'count' },
|
||||||
|
],
|
||||||
|
|
||||||
|
productSortOptions: [
|
||||||
|
{ text: 'بر اساس مبلغ', value: 'amount' },
|
||||||
|
{ text: 'بر اساس تعداد', value: 'count' },
|
||||||
|
],
|
||||||
|
|
||||||
|
productHeaders: [
|
||||||
|
{ text: 'نام کالا', value: 'name' },
|
||||||
|
{ text: 'کد', value: 'code' },
|
||||||
|
{ text: 'تعداد', value: 'totalCount' },
|
||||||
|
{ text: 'مبلغ کل', value: 'totalAmount' },
|
||||||
|
{ text: 'سود', value: 'profit' },
|
||||||
|
{ text: 'درصد سود', value: 'profitMargin' }
|
||||||
|
],
|
||||||
|
|
||||||
|
customerHeaders: [
|
||||||
|
{ text: 'نام مشتری', value: 'name' },
|
||||||
|
{ text: 'کد', value: 'code' },
|
||||||
|
{ text: 'تعداد فاکتور', value: 'invoiceCount' },
|
||||||
|
{ text: 'مبلغ کل', value: 'totalAmount' },
|
||||||
|
{ text: 'میانگین فاکتور', value: 'averageAmount' }
|
||||||
|
],
|
||||||
|
|
||||||
|
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 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isDateFilterActive() {
|
||||||
|
return this.dateFilter.startDate && this.dateFilter.endDate &&
|
||||||
|
(this.dateFilter.startDate !== this.year.start || this.dateFilter.endDate !== this.year.end);
|
||||||
|
},
|
||||||
|
formattedStartDate() {
|
||||||
|
return this.dateFilter.startDate ? this.formatDateForDisplay(this.dateFilter.startDate) : '';
|
||||||
|
},
|
||||||
|
formattedEndDate() {
|
||||||
|
return this.dateFilter.endDate ? this.formatDateForDisplay(this.dateFilter.endDate) : '';
|
||||||
|
},
|
||||||
|
totalInvoicePages() {
|
||||||
|
return Math.ceil(this.totalInvoices / 20);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Watchers for automatic filter updates
|
||||||
|
watch: {
|
||||||
|
'filters.customerId'() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
'filters.status'() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
'filters.groupBy'() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
'chartType'() {
|
||||||
|
this.loadChartData();
|
||||||
|
},
|
||||||
|
'productSortBy'() {
|
||||||
|
this.loadTopProducts();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.loadInitialData();
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'dateFilter.startDate'() {
|
||||||
|
if (this.dateFilter.startDate && this.dateFilter.endDate) {
|
||||||
|
this.gregorianStartDate = this.convertJalaliToGregorian(this.dateFilter.startDate);
|
||||||
|
this.gregorianEndDate = this.convertJalaliToGregorian(this.dateFilter.endDate);
|
||||||
|
this.invoicePage = 1;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'dateFilter.endDate'() {
|
||||||
|
if (this.dateFilter.startDate && this.dateFilter.endDate) {
|
||||||
|
this.gregorianStartDate = this.convertJalaliToGregorian(this.dateFilter.startDate);
|
||||||
|
this.gregorianEndDate = this.convertJalaliToGregorian(this.dateFilter.endDate);
|
||||||
|
this.invoicePage = 1;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'filters.customerId'() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
'filters.status'() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
'filters.groupBy'() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
'chartType'() {
|
||||||
|
this.loadChartData();
|
||||||
|
},
|
||||||
|
'productSortBy'() {
|
||||||
|
this.loadTopProducts();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadInitialData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
// دریافت اطلاعات سال مالی
|
||||||
|
const yearResponse = await axios.get('/api/year/get');
|
||||||
|
this.year = yearResponse.data;
|
||||||
|
|
||||||
|
// تنظیم تاریخهای پیشفرض
|
||||||
|
this.dateFilter.startDate = this.year.start;
|
||||||
|
this.dateFilter.endDate = this.year.end;
|
||||||
|
this.gregorianStartDate = this.convertJalaliToGregorian(this.dateFilter.startDate);
|
||||||
|
this.gregorianEndDate = this.convertJalaliToGregorian(this.dateFilter.endDate);
|
||||||
|
|
||||||
|
await this.loadData();
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('خطا در بارگذاری اطلاعات اولیه: ' + (error.response?.data?.message || error.message));
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadSummary(),
|
||||||
|
this.loadTopProducts(),
|
||||||
|
this.loadTopCustomers(),
|
||||||
|
this.loadInvoices(),
|
||||||
|
this.loadChartData()
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('خطا در بارگذاری اطلاعات: ' + (error.response?.data?.message || error.message));
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSummary() {
|
||||||
|
const params = {
|
||||||
|
startDate: this.dateFilter.startDate,
|
||||||
|
endDate: this.dateFilter.endDate,
|
||||||
|
groupBy: this.filters.groupBy,
|
||||||
|
customerId: this.filters.customerId,
|
||||||
|
status: this.filters.status
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get('/api/sell/report/summary', { params });
|
||||||
|
if (response.data.result === 1) {
|
||||||
|
this.summary = response.data.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTopProducts() {
|
||||||
|
this.loadingProducts = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
startDate: this.dateFilter.startDate,
|
||||||
|
endDate: this.dateFilter.endDate,
|
||||||
|
limit: 10,
|
||||||
|
sortBy: this.productSortBy,
|
||||||
|
customerId: this.filters.customerId,
|
||||||
|
status: this.filters.status
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get('/api/sell/report/top-products', { params });
|
||||||
|
if (response.data.result === 1) {
|
||||||
|
this.topProducts = response.data.data;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loadingProducts = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTopCustomers() {
|
||||||
|
this.loadingCustomers = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
startDate: this.dateFilter.startDate,
|
||||||
|
endDate: this.dateFilter.endDate,
|
||||||
|
limit: 10,
|
||||||
|
customerId: this.filters.customerId,
|
||||||
|
status: this.filters.status
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get('/api/sell/report/top-customers', { params });
|
||||||
|
if (response.data.result === 1) {
|
||||||
|
this.topCustomers = response.data.data;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loadingCustomers = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadInvoices() {
|
||||||
|
this.loadingInvoices = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
startDate: this.dateFilter.startDate,
|
||||||
|
endDate: this.dateFilter.endDate,
|
||||||
|
customerId: this.filters.customerId,
|
||||||
|
status: this.filters.status,
|
||||||
|
page: this.invoicePage,
|
||||||
|
perPage: 20
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get('/api/sell/report/invoices', { params });
|
||||||
|
if (response.data.result === 1) {
|
||||||
|
this.invoices = response.data.data.invoices;
|
||||||
|
this.totalInvoices = response.data.data.total;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loadingInvoices = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async loadChartData() {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
startDate: this.dateFilter.startDate,
|
||||||
|
endDate: this.dateFilter.endDate,
|
||||||
|
groupBy: this.filters.groupBy,
|
||||||
|
type: this.chartType
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get('/api/sell/report/chart', { params });
|
||||||
|
if (response.data.result === 1) {
|
||||||
|
this.updateChart(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading chart data:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateChart(chartData) {
|
||||||
|
this.chartOptions.xaxis.categories = chartData.labels || [];
|
||||||
|
this.chartSeries[0].data = chartData.data || [];
|
||||||
|
this.chartSeries[0].name = this.chartType === 'amount' ? 'مبلغ فروش' : 'تعداد فاکتور';
|
||||||
|
|
||||||
|
// بهروزرسانی tooltip بر اساس نوع نمودار
|
||||||
|
this.chartOptions.tooltip.y.formatter = (value) => {
|
||||||
|
if (this.chartType === 'amount') {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value) + ' ریال';
|
||||||
|
} else {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value) + ' عدد';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
changeInvoicePage(page) {
|
||||||
|
this.invoicePage = page;
|
||||||
|
this.loadInvoices();
|
||||||
|
},
|
||||||
|
|
||||||
|
viewInvoice(invoice) {
|
||||||
|
this.$router.push(`/acc/sell/view/${invoice.code}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async exportReport() {
|
||||||
|
this.exporting = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
startDate: this.dateFilter.startDate,
|
||||||
|
endDate: this.dateFilter.endDate,
|
||||||
|
customerId: this.filters.customerId,
|
||||||
|
status: this.filters.status
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post('/api/sell/report/export', params, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
|
||||||
|
// دانلود فایل
|
||||||
|
const url = window.URL.createObjectURL(response.data);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `sell_report_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.xlsx`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
this.showError('گزارش با موفقیت دانلود شد');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting report:', error);
|
||||||
|
this.showError('خطا در دانلود گزارش: ' + (error.response?.data?.message || error.message));
|
||||||
|
} finally {
|
||||||
|
this.exporting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
this.errorMessage = message;
|
||||||
|
this.errorDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatNumber(value, isCount = false) {
|
||||||
|
if (!value) return '0';
|
||||||
|
// گرد کردن عدد به نزدیکترین عدد صحیح
|
||||||
|
const roundedValue = Math.round(Number(value));
|
||||||
|
const formattedNumber = roundedValue.toLocaleString('fa-IR');
|
||||||
|
|
||||||
|
// اگر تعداد فاکتور است، واحد "عدد" اضافه نکن
|
||||||
|
if (isCount) {
|
||||||
|
return formattedNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// برای مبالغ، واحد "ریال" اضافه کن
|
||||||
|
return formattedNumber + ' ریال';
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDateForDisplay(dateString) {
|
||||||
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// اگر تاریخ شمسی است (فرمت Y/m/d)، آن را به میلادی تبدیل کن
|
||||||
|
if (typeof dateString === 'string' && dateString.includes('/')) {
|
||||||
|
const parts = dateString.split('/');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
// استفاده از jalali-moment برای تبدیل دقیق
|
||||||
|
const gregorianDate = moment(dateString, 'jYYYY/jMM/jDD').toDate();
|
||||||
|
return format(gregorianDate, 'yyyy/MM/dd');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر تاریخ میلادی است
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return format(date, 'yyyy/MM/dd');
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
convertJalaliToGregorian(jalaliDate) {
|
||||||
|
if (!jalaliDate) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// اگر تاریخ شمسی است (فرمت Y/m/d)، آن را به میلادی تبدیل کن
|
||||||
|
if (typeof jalaliDate === 'string' && jalaliDate.includes('/')) {
|
||||||
|
const parts = jalaliDate.split('/');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const year = parseInt(parts[0]);
|
||||||
|
const month = parseInt(parts[1]);
|
||||||
|
const day = parseInt(parts[2]);
|
||||||
|
|
||||||
|
// استفاده از jalali-moment برای تبدیل دقیق
|
||||||
|
const gregorianDate = moment(`${year}/${month}/${day}`, 'jYYYY/jMM/jDD').format('YYYY-MM-DD');
|
||||||
|
return gregorianDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jalaliDate;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting Jalali to Gregorian:', error);
|
||||||
|
return jalaliDate;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
convertGregorianToJalali(gregorianDate) {
|
||||||
|
if (!gregorianDate) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// استفاده از jalali-moment برای تبدیل دقیق
|
||||||
|
const jalaliDate = moment(gregorianDate, 'YYYY-MM-DD').format('jYYYY/jMM/jDD');
|
||||||
|
return jalaliDate;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting Gregorian to Jalali:', error);
|
||||||
|
return gregorianDate;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Global styles for Vuetify date picker z-index */
|
||||||
|
.v-date-picker {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-dialog {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-overlay {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.date-filter-card {
|
||||||
|
border-left: 4px solid #1976d2;
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.filter-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
147
webUI/src/views/acc/reports/sell/components/SellChart.vue
Normal file
147
webUI/src/views/acc/reports/sell/components/SellChart.vue
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
نمودار فروش
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-select
|
||||||
|
v-model="chartType"
|
||||||
|
:items="chartTypeOptions"
|
||||||
|
label="نوع نمودار"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
style="max-width: 200px"
|
||||||
|
@change="updateChart"
|
||||||
|
></v-select>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<apexchart
|
||||||
|
type="line"
|
||||||
|
height="300"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="chartSeries"
|
||||||
|
></apexchart>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SellChart',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
chartData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
labels: [],
|
||||||
|
data: [],
|
||||||
|
type: 'amount',
|
||||||
|
groupBy: 'day'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chartType: 'amount',
|
||||||
|
chartTypeOptions: [
|
||||||
|
{ text: 'مبلغ', value: 'amount' },
|
||||||
|
{ text: 'تعداد', value: 'count' }
|
||||||
|
],
|
||||||
|
chartOptions: {
|
||||||
|
chart: {
|
||||||
|
id: 'sell-chart-component',
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
toolbar: { show: false }
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: [],
|
||||||
|
labels: {
|
||||||
|
style: {
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: function(value) {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors: ['#1976d2'],
|
||||||
|
stroke: {
|
||||||
|
curve: 'smooth',
|
||||||
|
width: 3
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
gradient: {
|
||||||
|
shadeIntensity: 1,
|
||||||
|
opacityFrom: 0.7,
|
||||||
|
opacityTo: 0.1,
|
||||||
|
stops: [0, 90, 100]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
y: {
|
||||||
|
formatter: function(value) {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value) + ' ریال';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chartSeries: [{
|
||||||
|
name: 'فروش',
|
||||||
|
data: []
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateChartData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateChart() {
|
||||||
|
this.$emit('chart-type-changed', this.chartType);
|
||||||
|
},
|
||||||
|
updateChartData() {
|
||||||
|
this.chartOptions.xaxis.categories = this.chartData.labels || [];
|
||||||
|
this.chartSeries[0].data = this.chartData.data || [];
|
||||||
|
this.chartSeries[0].name = this.chartType === 'amount' ? 'مبلغ فروش' : 'تعداد فاکتور';
|
||||||
|
|
||||||
|
// بهروزرسانی tooltip بر اساس نوع نمودار
|
||||||
|
this.chartOptions.tooltip.y.formatter = (value) => {
|
||||||
|
if (this.chartType === 'amount') {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value) + ' ریال';
|
||||||
|
} else {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value) + ' عدد';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
chartData: {
|
||||||
|
handler() {
|
||||||
|
this.updateChartData();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
chartType() {
|
||||||
|
this.updateChartData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
68
webUI/src/views/acc/reports/sell/components/SellSummary.vue
Normal file
68
webUI/src/views/acc/reports/sell/components/SellSummary.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 primary--text">{{ formatNumber(summary.totalAmount) }}</div>
|
||||||
|
<div class="text-subtitle-1">کل فروش</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 success--text">{{ formatNumber(summary.totalCount) }}</div>
|
||||||
|
<div class="text-subtitle-1">تعداد فاکتور</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 info--text">{{ formatNumber(summary.averageAmount) }}</div>
|
||||||
|
<div class="text-subtitle-1">میانگین فاکتور</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="summary-card">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h4 warning--text">{{ formatNumber(summary.totalProfit) }}</div>
|
||||||
|
<div class="text-subtitle-1">سود کل</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SellSummary',
|
||||||
|
props: {
|
||||||
|
summary: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
totalAmount: 0,
|
||||||
|
totalCount: 0,
|
||||||
|
averageAmount: 0,
|
||||||
|
totalProfit: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatNumber(number) {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.summary-card {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue