add sell report

This commit is contained in:
Hesabix 2025-08-21 21:09:26 +00:00
parent f609c4176f
commit fa46e410fc
8 changed files with 2578 additions and 1 deletions

View 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);
}
}
}

View 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);
}
}

View file

@ -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',

View file

@ -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 '';

View file

@ -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' }
] ]
} }
}, },

View 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>

View 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>

View 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>