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: () =>
|
||||
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',
|
||||
name: 'costs_list',
|
||||
|
|
|
@ -358,7 +358,10 @@
|
|||
this.errorDialog = true;
|
||||
},
|
||||
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) {
|
||||
if (!dateString) return '';
|
||||
|
|
|
@ -95,6 +95,24 @@
|
|||
</v-list>
|
||||
</v-card>
|
||||
</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-container>
|
||||
</template>
|
||||
|
@ -122,6 +140,9 @@ export default {
|
|||
accountingReports: [
|
||||
{ text: 'ترازنامه', to: '/acc/reports/acc/balance_sheet' },
|
||||
{ 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