add some widgets about cheque to dashboard
This commit is contained in:
parent
93bdf0fac4
commit
ca043a913f
37
hesabixCore/migrations/Version20250101000000.php
Normal file
37
hesabixCore/migrations/Version20250101000000.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250101000000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add cheque dashboard settings fields';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings ADD cheques TINYINT(1) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_due_today TINYINT(1) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_status_chart TINYINT(1) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_monthly_chart TINYINT(1) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_due_soon TINYINT(1) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings DROP cheques');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_due_today');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_status_chart');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_monthly_chart');
|
||||||
|
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_due_soon');
|
||||||
|
}
|
||||||
|
}
|
|
@ -892,4 +892,135 @@ class ChequeController extends AbstractController
|
||||||
'result' => 'ok'
|
'result' => 'ok'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/cheque/dashboard/stats', name: 'app_cheque_dashboard_stats')]
|
||||||
|
public function app_cheque_dashboard_stats(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, Jdate $jdate): JsonResponse
|
||||||
|
{
|
||||||
|
$acc = $access->hasRole('cheque');
|
||||||
|
if (!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
|
$money = $acc['money'];
|
||||||
|
$defaultMoey = $acc['bid']->getMoney();
|
||||||
|
$defMoney = false;
|
||||||
|
if ($defaultMoey->getId() == $money->getId()) {
|
||||||
|
$defMoney = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// آمار کلی چکها
|
||||||
|
$qb = $entityManager->createQueryBuilder();
|
||||||
|
$totalInputCheques = $qb->select('COUNT(c.id)')
|
||||||
|
->from(Cheque::class, 'c')
|
||||||
|
->where('c.bid = :bid')
|
||||||
|
->andWhere('c.type = :type')
|
||||||
|
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->setParameter('type', 'input')
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
|
||||||
|
$qb = $entityManager->createQueryBuilder();
|
||||||
|
$totalOutputCheques = $qb->select('COUNT(c.id)')
|
||||||
|
->from(Cheque::class, 'c')
|
||||||
|
->where('c.bid = :bid')
|
||||||
|
->andWhere('c.type = :type')
|
||||||
|
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->setParameter('type', 'output')
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
|
||||||
|
// چکهای سررسید امروز
|
||||||
|
$today = $jdate->jdate('Y/m/d', time());
|
||||||
|
$qb = $entityManager->createQueryBuilder();
|
||||||
|
$todayDueCheques = $qb->select('c')
|
||||||
|
->from(Cheque::class, 'c')
|
||||||
|
->where('c.bid = :bid')
|
||||||
|
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
|
||||||
|
->andWhere('c.date = :today')
|
||||||
|
->andWhere('c.status != :rejected')
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('today', $today)
|
||||||
|
->setParameter('rejected', 'برگشت خورده')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
// چکهای نزدیک به سررسید (7 روز آینده)
|
||||||
|
$endDate = $jdate->jdate('Y/m/d', strtotime('+7 days'));
|
||||||
|
$qb = $entityManager->createQueryBuilder();
|
||||||
|
$soonDueCheques = $qb->select('c')
|
||||||
|
->from(Cheque::class, 'c')
|
||||||
|
->where('c.bid = :bid')
|
||||||
|
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
|
||||||
|
->andWhere('c.date >= :start')
|
||||||
|
->andWhere('c.date <= :end')
|
||||||
|
->andWhere('c.status != :rejected')
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('start', $today)
|
||||||
|
->setParameter('end', $endDate)
|
||||||
|
->setParameter('rejected', 'برگشت خورده')
|
||||||
|
->orderBy('c.date', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
// آمار وضعیت چکها
|
||||||
|
$qb = $entityManager->createQueryBuilder();
|
||||||
|
$statusStats = $qb->select('c.status, COUNT(c.id) as count, SUM(c.amount) as total_amount')
|
||||||
|
->from(Cheque::class, 'c')
|
||||||
|
->where('c.bid = :bid')
|
||||||
|
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->groupBy('c.status')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
// آمار ماهانه چکها (6 ماه اخیر) - سادهسازی شده
|
||||||
|
$sixMonthsAgo = $jdate->jdate('Y/m', strtotime('-6 months')) . '/01';
|
||||||
|
$qb = $entityManager->createQueryBuilder();
|
||||||
|
$allCheques = $qb->select('c.date, c.type, c.amount')
|
||||||
|
->from(Cheque::class, 'c')
|
||||||
|
->where('c.bid = :bid')
|
||||||
|
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
|
||||||
|
->andWhere('c.date >= :sixMonthsAgo')
|
||||||
|
->setParameter('bid', $acc['bid'])
|
||||||
|
->setParameter('money', $money)
|
||||||
|
->setParameter('sixMonthsAgo', $sixMonthsAgo)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
// پردازش دادههای ماهانه
|
||||||
|
$processedMonthlyStats = [];
|
||||||
|
foreach ($allCheques as $cheque) {
|
||||||
|
$month = substr($cheque['date'], 0, 7); // YYYY/MM
|
||||||
|
$key = $month . '_' . $cheque['type'];
|
||||||
|
|
||||||
|
if (!isset($processedMonthlyStats[$key])) {
|
||||||
|
$processedMonthlyStats[$key] = [
|
||||||
|
'month' => $month,
|
||||||
|
'type' => $cheque['type'],
|
||||||
|
'count' => 0,
|
||||||
|
'total_amount' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$processedMonthlyStats[$key]['count']++;
|
||||||
|
$processedMonthlyStats[$key]['total_amount'] += (float)($cheque['amount'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$monthlyStats = array_values($processedMonthlyStats);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'totalInputCheques' => $totalInputCheques,
|
||||||
|
'totalOutputCheques' => $totalOutputCheques,
|
||||||
|
'todayDueCheques' => Explore::SerializeCheques($todayDueCheques),
|
||||||
|
'soonDueCheques' => Explore::SerializeCheques($soonDueCheques),
|
||||||
|
'statusStats' => $statusStats,
|
||||||
|
'monthlyStats' => $monthlyStats
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,11 @@ class DashboardController extends AbstractController
|
||||||
if(array_key_exists('topCostCenters',$params)) $setting->setTopCostCenters($params['topCostCenters']);
|
if(array_key_exists('topCostCenters',$params)) $setting->setTopCostCenters($params['topCostCenters']);
|
||||||
if(array_key_exists('incomes',$params)) $setting->setIncomes($params['incomes']);
|
if(array_key_exists('incomes',$params)) $setting->setIncomes($params['incomes']);
|
||||||
if(array_key_exists('topIncomeCenters',$params)) $setting->setTopIncomesChart($params['topIncomeCenters']);
|
if(array_key_exists('topIncomeCenters',$params)) $setting->setTopIncomesChart($params['topIncomeCenters']);
|
||||||
|
if(array_key_exists('cheques',$params)) $setting->setCheques($params['cheques']);
|
||||||
|
if(array_key_exists('chequesDueToday',$params)) $setting->setChequesDueToday($params['chequesDueToday']);
|
||||||
|
if(array_key_exists('chequesStatusChart',$params)) $setting->setChequesStatusChart($params['chequesStatusChart']);
|
||||||
|
if(array_key_exists('chequesMonthlyChart',$params)) $setting->setChequesMonthlyChart($params['chequesMonthlyChart']);
|
||||||
|
if(array_key_exists('chequesDueSoon',$params)) $setting->setChequesDueSoon($params['chequesDueSoon']);
|
||||||
|
|
||||||
$entityManagerInterface->persist($setting);
|
$entityManagerInterface->persist($setting);
|
||||||
$entityManagerInterface->flush();
|
$entityManagerInterface->flush();
|
||||||
|
|
|
@ -66,6 +66,21 @@ class DashboardSettings
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $topIncomesChart = null;
|
private ?bool $topIncomesChart = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $cheques = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $chequesDueToday = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $chequesStatusChart = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $chequesMonthlyChart = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $chequesDueSoon = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
|
@ -274,4 +289,64 @@ class DashboardSettings
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isCheques(): ?bool
|
||||||
|
{
|
||||||
|
return $this->cheques;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCheques(?bool $cheques): static
|
||||||
|
{
|
||||||
|
$this->cheques = $cheques;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isChequesDueToday(): ?bool
|
||||||
|
{
|
||||||
|
return $this->chequesDueToday;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setChequesDueToday(?bool $chequesDueToday): static
|
||||||
|
{
|
||||||
|
$this->chequesDueToday = $chequesDueToday;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isChequesStatusChart(): ?bool
|
||||||
|
{
|
||||||
|
return $this->chequesStatusChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setChequesStatusChart(?bool $chequesStatusChart): static
|
||||||
|
{
|
||||||
|
$this->chequesStatusChart = $chequesStatusChart;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isChequesMonthlyChart(): ?bool
|
||||||
|
{
|
||||||
|
return $this->chequesMonthlyChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setChequesMonthlyChart(?bool $chequesMonthlyChart): static
|
||||||
|
{
|
||||||
|
$this->chequesMonthlyChart = $chequesMonthlyChart;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isChequesDueSoon(): ?bool
|
||||||
|
{
|
||||||
|
return $this->chequesDueSoon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setChequesDueSoon(?bool $chequesDueSoon): static
|
||||||
|
{
|
||||||
|
$this->chequesDueSoon = $chequesDueSoon;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,7 @@ class Explore
|
||||||
'id' => $person->getId(),
|
'id' => $person->getId(),
|
||||||
'code' => $person->getCode(),
|
'code' => $person->getCode(),
|
||||||
'nikename' => $person->getNikename(),
|
'nikename' => $person->getNikename(),
|
||||||
'name' => $person->getName(),
|
'name' => $person->getName() ?: $person->getNikename(),
|
||||||
'tel' => $person->getTel(),
|
'tel' => $person->getTel(),
|
||||||
'mobile' => $person->getmobile(),
|
'mobile' => $person->getmobile(),
|
||||||
'mobile2' => $person->getMobile2(),
|
'mobile2' => $person->getMobile2(),
|
||||||
|
@ -633,6 +633,11 @@ class Explore
|
||||||
'topCostCenters' => $item->isTopCostCenters(),
|
'topCostCenters' => $item->isTopCostCenters(),
|
||||||
'incomes' => $item->isIncomes(),
|
'incomes' => $item->isIncomes(),
|
||||||
'topIncomeCenters' => $item->isTopIncomesChart(),
|
'topIncomeCenters' => $item->isTopIncomesChart(),
|
||||||
|
'cheques' => $item->isCheques(),
|
||||||
|
'chequesDueToday' => $item->isChequesDueToday(),
|
||||||
|
'chequesStatusChart' => $item->isChequesStatusChart(),
|
||||||
|
'chequesMonthlyChart' => $item->isChequesMonthlyChart(),
|
||||||
|
'chequesDueSoon' => $item->isChequesDueSoon(),
|
||||||
];
|
];
|
||||||
if ($result['topCommodities'] === null)
|
if ($result['topCommodities'] === null)
|
||||||
$result['topCommodities'] = true;
|
$result['topCommodities'] = true;
|
||||||
|
@ -664,6 +669,16 @@ class Explore
|
||||||
$result['incomes'] = true;
|
$result['incomes'] = true;
|
||||||
if ($result['topIncomeCenters'] === null)
|
if ($result['topIncomeCenters'] === null)
|
||||||
$result['topIncomeCenters'] = true;
|
$result['topIncomeCenters'] = true;
|
||||||
|
if ($result['cheques'] === null)
|
||||||
|
$result['cheques'] = true;
|
||||||
|
if ($result['chequesDueToday'] === null)
|
||||||
|
$result['chequesDueToday'] = true;
|
||||||
|
if ($result['chequesStatusChart'] === null)
|
||||||
|
$result['chequesStatusChart'] = true;
|
||||||
|
if ($result['chequesMonthlyChart'] === null)
|
||||||
|
$result['chequesMonthlyChart'] = true;
|
||||||
|
if ($result['chequesDueSoon'] === null)
|
||||||
|
$result['chequesDueSoon'] = true;
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
173
webUI/src/components/widgets/ChequesDueSoonWidget.vue
Normal file
173
webUI/src/components/widgets/ChequesDueSoonWidget.vue
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
<template>
|
||||||
|
<v-card class="cheques-due-soon-widget" elevation="0" variant="outlined">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
|
<span class="text-h6">
|
||||||
|
<v-icon left color="orange">mdi-clock-alert</v-icon>
|
||||||
|
چکهای نزدیک به سررسید
|
||||||
|
</span>
|
||||||
|
<v-chip :color="soonDueCheques.length > 0 ? 'orange' : 'success'" size="small">
|
||||||
|
{{ soonDueCheques.length }} چک
|
||||||
|
</v-chip>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div v-if="loading" class="text-center py-4">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="soonDueCheques.length === 0" class="text-center py-4">
|
||||||
|
<v-icon size="48" color="success">mdi-check-circle</v-icon>
|
||||||
|
<div class="text-body-1 mt-2">هیچ چکی در 7 روز آینده سررسید ندارد</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="cheque in soonDueCheques.slice(0, 5)"
|
||||||
|
:key="cheque.id"
|
||||||
|
:class="getDueClass(cheque.date)"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon
|
||||||
|
:color="cheque.type === 'input' ? 'success' : 'error'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ cheque.type === 'input' ? 'mdi-arrow-down' : 'mdi-arrow-up' }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title class="text-body-2 text-truncate">
|
||||||
|
{{ cheque.number }} - {{ cheque.bankOncheque }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="text-caption text-truncate">
|
||||||
|
{{ cheque.person?.name || 'نامشخص' }} - {{ $filters.formatNumber(cheque.amount) }} {{ $t('currency.irr.short') }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-chip
|
||||||
|
:color="getDueChipColor(cheque.date)"
|
||||||
|
size="x-small"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{{ getDaysUntilDue(cheque.date) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<div v-if="soonDueCheques.length > 5" class="text-center mt-2">
|
||||||
|
<v-btn variant="text" size="small" color="primary">
|
||||||
|
مشاهده {{ soonDueCheques.length - 5 }} چک دیگر
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import moment from 'moment-jalaali';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChequesDueSoonWidget',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
soonDueCheques: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/cheque/dashboard/stats');
|
||||||
|
this.soonDueCheques = response.data.soonDueCheques || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching soon due cheques:', error);
|
||||||
|
this.soonDueCheques = [];
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
convertShamsiToMiladi(shamsiDate) {
|
||||||
|
if (!shamsiDate) return new Date();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// استفاده از moment-jalaali برای تبدیل دقیق
|
||||||
|
const jMoment = moment(shamsiDate, 'jYYYY/jM/jD');
|
||||||
|
return jMoment.toDate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting date:', error);
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDaysUntilDue(date) {
|
||||||
|
const today = new Date();
|
||||||
|
const dueDate = this.convertShamsiToMiladi(date);
|
||||||
|
const diffTime = dueDate - today;
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays === 0) return 'امروز';
|
||||||
|
if (diffDays === 1) return 'فردا';
|
||||||
|
if (diffDays < 0) return `${Math.abs(diffDays)} روز گذشته`;
|
||||||
|
return `${diffDays} روز`;
|
||||||
|
},
|
||||||
|
getDueChipColor(date) {
|
||||||
|
const today = new Date();
|
||||||
|
const dueDate = this.convertShamsiToMiladi(date);
|
||||||
|
const diffTime = dueDate - today;
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays <= 0) return 'error';
|
||||||
|
if (diffDays <= 3) return 'warning';
|
||||||
|
return 'info';
|
||||||
|
},
|
||||||
|
getDueClass(date) {
|
||||||
|
const today = new Date();
|
||||||
|
const dueDate = this.convertShamsiToMiladi(date);
|
||||||
|
const diffTime = dueDate - today;
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays <= 0) return 'border-left-error';
|
||||||
|
if (diffDays <= 3) return 'border-left-warning';
|
||||||
|
return 'border-left-info';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cheques-due-soon-widget {
|
||||||
|
min-height: 300px;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheques-due-soon-widget .v-card-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left-error {
|
||||||
|
border-left: 3px solid #F44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left-warning {
|
||||||
|
border-left: 3px solid #FF9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left-info {
|
||||||
|
border-left: 3px solid #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
116
webUI/src/components/widgets/ChequesDueTodayWidget.vue
Normal file
116
webUI/src/components/widgets/ChequesDueTodayWidget.vue
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<template>
|
||||||
|
<v-card class="cheques-due-today-widget" elevation="0" variant="outlined">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
|
<span class="text-h6">
|
||||||
|
<v-icon left color="warning">mdi-calendar-alert</v-icon>
|
||||||
|
چکهای سررسید امروز
|
||||||
|
</span>
|
||||||
|
<v-chip :color="todayDueCheques.length > 0 ? 'warning' : 'success'" size="small">
|
||||||
|
{{ todayDueCheques.length }} چک
|
||||||
|
</v-chip>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div v-if="loading" class="text-center py-4">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="todayDueCheques.length === 0" class="text-center py-4">
|
||||||
|
<v-icon size="48" color="success">mdi-check-circle</v-icon>
|
||||||
|
<div class="text-body-1 mt-2">هیچ چکی امروز سررسید ندارد</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="cheque in todayDueCheques.slice(0, 5)"
|
||||||
|
:key="cheque.id"
|
||||||
|
:class="cheque.type === 'input' ? 'border-left-success' : 'border-left-error'"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon
|
||||||
|
:color="cheque.type === 'input' ? 'success' : 'error'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ cheque.type === 'input' ? 'mdi-arrow-down' : 'mdi-arrow-up' }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title class="text-body-2 text-truncate">
|
||||||
|
{{ cheque.number }} - {{ cheque.bankOncheque }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="text-caption text-truncate">
|
||||||
|
{{ cheque.person?.name || 'نامشخص' }} - {{ $filters.formatNumber(cheque.amount) }} {{ $t('currency.irr.short') }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<div v-if="todayDueCheques.length > 5" class="text-center mt-2">
|
||||||
|
<v-btn variant="text" size="small" color="primary">
|
||||||
|
مشاهده {{ todayDueCheques.length - 5 }} چک دیگر
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChequesDueTodayWidget',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
todayDueCheques: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/cheque/dashboard/stats');
|
||||||
|
this.todayDueCheques = response.data.todayDueCheques || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching today due cheques:', error);
|
||||||
|
this.todayDueCheques = [];
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cheques-due-today-widget {
|
||||||
|
min-height: 300px;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheques-due-today-widget .v-card-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left-success {
|
||||||
|
border-left: 3px solid #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left-error {
|
||||||
|
border-left: 3px solid #F44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
259
webUI/src/components/widgets/ChequesMonthlyChart.vue
Normal file
259
webUI/src/components/widgets/ChequesMonthlyChart.vue
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
<template>
|
||||||
|
<v-card class="cheques-monthly-chart" elevation="0" variant="outlined">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
|
<span class="text-h6">
|
||||||
|
<v-icon left color="primary">mdi-chart-bar</v-icon>
|
||||||
|
نمودار ماهانه چکها
|
||||||
|
</span>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn-toggle
|
||||||
|
v-model="chartType"
|
||||||
|
mandatory
|
||||||
|
density="compact"
|
||||||
|
color="primary"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<v-btn value="count" size="small">تعداد</v-btn>
|
||||||
|
<v-btn value="amount" size="small">مبلغ</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
<v-btn icon @click="refreshData" :loading="loading">
|
||||||
|
<v-icon>mdi-refresh</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div v-if="loading" class="text-center py-4">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<apexchart
|
||||||
|
v-if="!loading && series[0].data.length > 0"
|
||||||
|
ref="barChart"
|
||||||
|
type="bar"
|
||||||
|
height="300"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="series"
|
||||||
|
></apexchart>
|
||||||
|
<div v-else-if="!loading && series[0].data.length === 0" class="text-center py-4">
|
||||||
|
<v-icon size="48" color="grey">mdi-chart-bar</v-icon>
|
||||||
|
<div class="text-body-1 mt-2">دادهای برای نمایش وجود ندارد</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-h6 font-weight-bold text-success">
|
||||||
|
{{ chartType === 'count' ? totalInputCount : $filters.formatNumber(totalInputAmount) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
{{ chartType === 'count' ? 'کل تعداد دریافتی' : 'کل مبلغ دریافتی' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-h6 font-weight-bold text-error">
|
||||||
|
{{ chartType === 'count' ? totalOutputCount : $filters.formatNumber(totalOutputAmount) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
{{ chartType === 'count' ? 'کل تعداد پرداختی' : 'کل مبلغ پرداختی' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChequesMonthlyChart',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
monthlyData: [],
|
||||||
|
chartType: 'count',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'چکهای دریافتی',
|
||||||
|
data: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'چکهای پرداختی',
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
chartCategories: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chartOptions() {
|
||||||
|
return {
|
||||||
|
chart: {
|
||||||
|
type: 'bar',
|
||||||
|
stacked: false,
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
bar: {
|
||||||
|
horizontal: false,
|
||||||
|
columnWidth: '55%',
|
||||||
|
endingShape: 'rounded'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
show: true,
|
||||||
|
width: 2,
|
||||||
|
colors: ['transparent']
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: this.chartCategories,
|
||||||
|
labels: {
|
||||||
|
rotate: -45,
|
||||||
|
rotateAlways: false,
|
||||||
|
style: {
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: {
|
||||||
|
text: this.chartType === 'count' ? 'تعداد چک' : 'مبلغ (ریال)',
|
||||||
|
style: {
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
style: {
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
opacity: 1
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
style: {
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
formatter: (val) => {
|
||||||
|
if (this.chartType === 'count') {
|
||||||
|
return val + " چک";
|
||||||
|
} else {
|
||||||
|
return this.$filters.formatNumber(val) + " " + this.$t('currency.irr.short');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors: ['#4CAF50', '#F44336'],
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
totalInputAmount() {
|
||||||
|
return this.monthlyData
|
||||||
|
.filter(item => item.type === 'input')
|
||||||
|
.reduce((sum, item) => sum + parseFloat(item.total_amount || 0), 0);
|
||||||
|
},
|
||||||
|
totalOutputAmount() {
|
||||||
|
return this.monthlyData
|
||||||
|
.filter(item => item.type === 'output')
|
||||||
|
.reduce((sum, item) => sum + parseFloat(item.total_amount || 0), 0);
|
||||||
|
},
|
||||||
|
totalInputCount() {
|
||||||
|
return this.monthlyData
|
||||||
|
.filter(item => item.type === 'input')
|
||||||
|
.reduce((sum, item) => sum + parseInt(item.count || 0), 0);
|
||||||
|
},
|
||||||
|
totalOutputCount() {
|
||||||
|
return this.monthlyData
|
||||||
|
.filter(item => item.type === 'output')
|
||||||
|
.reduce((sum, item) => sum + parseInt(item.count || 0), 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
chartType() {
|
||||||
|
this.updateChart();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/cheque/dashboard/stats');
|
||||||
|
this.monthlyData = response.data.monthlyStats || [];
|
||||||
|
this.updateChart();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching monthly cheque stats:', error);
|
||||||
|
this.monthlyData = [];
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateChart() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const months = [...new Set(this.monthlyData.map(item => item.month))].sort();
|
||||||
|
|
||||||
|
const inputData = months.map(month => {
|
||||||
|
const item = this.monthlyData.find(d => d.month === month && d.type === 'input');
|
||||||
|
if (this.chartType === 'count') {
|
||||||
|
return item ? parseInt(item.count) : 0;
|
||||||
|
} else {
|
||||||
|
return item ? parseFloat(item.total_amount || 0) : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputData = months.map(month => {
|
||||||
|
const item = this.monthlyData.find(d => d.month === month && d.type === 'output');
|
||||||
|
if (this.chartType === 'count') {
|
||||||
|
return item ? parseInt(item.count) : 0;
|
||||||
|
} else {
|
||||||
|
return item ? parseFloat(item.total_amount || 0) : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.series[0].data = inputData;
|
||||||
|
this.series[1].data = outputData;
|
||||||
|
this.chartCategories = months.map(month => {
|
||||||
|
const [year, monthNum] = month.split('/');
|
||||||
|
return `${monthNum}/${year}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
refreshData() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cheques-monthly-chart {
|
||||||
|
min-height: 450px;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheques-monthly-chart .v-card-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
237
webUI/src/components/widgets/ChequesStatusChart.vue
Normal file
237
webUI/src/components/widgets/ChequesStatusChart.vue
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
<template>
|
||||||
|
<v-card class="cheques-status-chart" elevation="0" variant="outlined">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
|
<span class="text-h6">
|
||||||
|
<v-icon left color="info">mdi-chart-pie</v-icon>
|
||||||
|
وضعیت چکها
|
||||||
|
</span>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn-toggle
|
||||||
|
v-model="chartType"
|
||||||
|
mandatory
|
||||||
|
density="compact"
|
||||||
|
color="primary"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<v-btn value="count" size="small">تعداد</v-btn>
|
||||||
|
<v-btn value="amount" size="small">مبلغ</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
<v-btn icon @click="refreshData" :loading="loading">
|
||||||
|
<v-icon>mdi-refresh</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div v-if="loading" class="text-center py-4">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<apexchart
|
||||||
|
v-if="!loading && series.length > 0"
|
||||||
|
ref="pieChart"
|
||||||
|
type="pie"
|
||||||
|
height="250"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="series"
|
||||||
|
></apexchart>
|
||||||
|
<div v-else-if="!loading && series.length === 0" class="text-center py-4">
|
||||||
|
<v-icon size="48" color="grey">mdi-chart-pie</v-icon>
|
||||||
|
<div class="text-body-1 mt-2">دادهای برای نمایش وجود ندارد</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<div class="status-legend">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in statusData"
|
||||||
|
:key="index"
|
||||||
|
class="d-flex justify-space-between align-center mb-3"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center flex-1 min-width-0">
|
||||||
|
<div
|
||||||
|
class="status-color mr-2 flex-shrink-0"
|
||||||
|
:style="{ backgroundColor: getStatusColor(item.status) }"
|
||||||
|
></div>
|
||||||
|
<span class="text-body-2 text-truncate">{{ item.status }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-right flex-shrink-0 ml-2">
|
||||||
|
<div class="text-body-2 font-weight-bold">
|
||||||
|
{{ chartType === 'count' ? item.count : $filters.formatNumber(item.total_amount || 0) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
{{ chartType === 'count' ? 'چک' : $t('currency.irr.short') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChequesStatusChart',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
statusData: [],
|
||||||
|
series: [],
|
||||||
|
chartLabels: [],
|
||||||
|
chartType: 'count'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chartOptions() {
|
||||||
|
return {
|
||||||
|
chart: {
|
||||||
|
type: 'pie',
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
},
|
||||||
|
labels: this.chartLabels,
|
||||||
|
colors: ['#4CAF50', '#FF9800', '#F44336', '#2196F3', '#9C27B0'],
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
style: {
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
formatter: (val) => {
|
||||||
|
if (this.chartType === 'count') {
|
||||||
|
return val + ' چک';
|
||||||
|
} else {
|
||||||
|
return this.$filters.formatNumber(val) + ' ' + this.$t('currency.irr.short');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responsive: [{
|
||||||
|
breakpoint: 480,
|
||||||
|
options: {
|
||||||
|
chart: {
|
||||||
|
width: 200,
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
chartType() {
|
||||||
|
this.updateChart();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/cheque/dashboard/stats');
|
||||||
|
this.statusData = response.data.statusStats || [];
|
||||||
|
this.updateChart();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cheque status stats:', error);
|
||||||
|
this.statusData = [];
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateChart() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.chartType === 'count') {
|
||||||
|
this.series = this.statusData.map(item => item.count);
|
||||||
|
} else {
|
||||||
|
this.series = this.statusData.map(item => parseFloat(item.total_amount || 0));
|
||||||
|
}
|
||||||
|
this.chartLabels = this.statusData.map(item => item.status);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getStatusColor(status) {
|
||||||
|
const colorMap = {
|
||||||
|
'وصول': '#4CAF50',
|
||||||
|
'وصول نشده': '#FF9800',
|
||||||
|
'برگشت خورده': '#F44336',
|
||||||
|
'پاس شده': '#2196F3',
|
||||||
|
'پاس نشده': '#9C27B0'
|
||||||
|
};
|
||||||
|
return colorMap[status] || '#757575';
|
||||||
|
},
|
||||||
|
refreshData() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cheques-status-chart {
|
||||||
|
min-height: 400px;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheques-status-chart .v-card-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-legend {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-legend::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-legend::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-legend::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-legend::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-width-0 {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
99
webUI/src/components/widgets/ChequesSummaryWidget.vue
Normal file
99
webUI/src/components/widgets/ChequesSummaryWidget.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<v-card class="cheques-summary-widget" elevation="0" variant="outlined">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
|
<span class="text-h6">
|
||||||
|
<v-icon left color="primary">mdi-credit-card</v-icon>
|
||||||
|
خلاصه چکها
|
||||||
|
</span>
|
||||||
|
<v-btn icon @click="refreshData" :loading="loading">
|
||||||
|
<v-icon>mdi-refresh</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold text-success">
|
||||||
|
{{ $filters.formatNumber(chequeStats.totalInputCheques) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
چکهای دریافتی
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold text-error">
|
||||||
|
{{ $filters.formatNumber(chequeStats.totalOutputCheques) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
چکهای پرداختی
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<span class="text-body-2">کل چکها:</span>
|
||||||
|
<span class="text-h6 font-weight-bold">
|
||||||
|
{{ $filters.formatNumber(chequeStats.totalInputCheques + chequeStats.totalOutputCheques) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChequesSummaryWidget',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
chequeStats: {
|
||||||
|
totalInputCheques: 0,
|
||||||
|
totalOutputCheques: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/cheque/dashboard/stats');
|
||||||
|
this.chequeStats = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cheque stats:', error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshData() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cheques-summary-widget {
|
||||||
|
min-height: 200px;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheques-summary-widget .v-card-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,7 +16,7 @@
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab value="output" block>
|
<v-tab value="output" block>
|
||||||
<v-icon start>mdi-file-import</v-icon>
|
<v-icon start>mdi-file-import</v-icon>
|
||||||
چکهای واگذار شده
|
چکهای پرداختی
|
||||||
</v-tab>
|
</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -55,6 +55,27 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
|
<!-- ویجتهای چک -->
|
||||||
|
<v-col cols="12" sm="6" md="4" v-show="permissions.cheque && dashboard.cheques">
|
||||||
|
<cheques-summary-widget class="animate__animated animate__zoomIn" />
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6" md="4" v-show="permissions.cheque && dashboard.chequesDueToday">
|
||||||
|
<cheques-due-today-widget class="animate__animated animate__zoomIn" />
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6" md="4" v-show="permissions.cheque && dashboard.chequesDueSoon">
|
||||||
|
<cheques-due-soon-widget class="animate__animated animate__zoomIn" />
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="12" md="6" v-show="permissions.cheque && dashboard.chequesStatusChart">
|
||||||
|
<cheques-status-chart class="animate__animated animate__zoomIn" />
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="12" md="6" v-show="permissions.cheque && dashboard.chequesMonthlyChart">
|
||||||
|
<cheques-monthly-chart class="animate__animated animate__zoomIn" />
|
||||||
|
</v-col>
|
||||||
<!-- کارتهای قبلی (بدون تغییر) -->
|
<!-- کارتهای قبلی (بدون تغییر) -->
|
||||||
<v-col cols="12" sm="6" md="4" v-show="permissions.wallet && dashboard.wallet">
|
<v-col cols="12" sm="6" md="4" v-show="permissions.wallet && dashboard.wallet">
|
||||||
<v-card class="animate__animated animate__zoomIn card-equal-height" color="success-lighten-4" variant="elevated"
|
<v-card class="animate__animated animate__zoomIn card-equal-height" color="success-lighten-4" variant="elevated"
|
||||||
|
@ -212,6 +233,12 @@
|
||||||
inset class="text-caption" />
|
inset class="text-caption" />
|
||||||
<v-switch color="primary" :label="$t('dashboard.incomes.centers')" v-model="dashboard.topIncomeCenters"
|
<v-switch color="primary" :label="$t('dashboard.incomes.centers')" v-model="dashboard.topIncomeCenters"
|
||||||
hide-details inset class="text-caption" />
|
hide-details inset class="text-caption" />
|
||||||
|
<v-switch color="primary" label="خلاصه چکها" v-model="dashboard.cheques" hide-details inset
|
||||||
|
class="text-caption" />
|
||||||
|
<v-switch color="primary" label="چکهای سررسید امروز" v-model="dashboard.chequesDueToday" hide-details inset
|
||||||
|
class="text-caption" />
|
||||||
|
<v-switch color="primary" label="چکهای نزدیک به سررسید" v-model="dashboard.chequesDueSoon" hide-details inset
|
||||||
|
class="text-caption" />
|
||||||
|
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="6" md="4">
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
@ -235,6 +262,10 @@
|
||||||
hide-details inset class="text-caption" />
|
hide-details inset class="text-caption" />
|
||||||
<v-switch color="primary" :label="$t('drawer.notif')" v-model="dashboard.notif"
|
<v-switch color="primary" :label="$t('drawer.notif')" v-model="dashboard.notif"
|
||||||
:hint="$t('dialog.notif_msg')" persistent-hint inset class="text-caption" />
|
:hint="$t('dialog.notif_msg')" persistent-hint inset class="text-caption" />
|
||||||
|
<v-switch color="primary" label="نمودار وضعیت چکها" v-model="dashboard.chequesStatusChart" hide-details inset
|
||||||
|
class="text-caption" />
|
||||||
|
<v-switch color="primary" label="نمودار ماهانه چکها" v-model="dashboard.chequesMonthlyChart" hide-details inset
|
||||||
|
class="text-caption" />
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
@ -257,6 +288,11 @@ import SaleChart from "./component/widgets/saleChart.vue";
|
||||||
import TopCommoditiesChart from '@/components/widgets/TopCommoditiesChart.vue';
|
import TopCommoditiesChart from '@/components/widgets/TopCommoditiesChart.vue';
|
||||||
import TopCostCentersChart from '@/components/widgets/TopCostCentersChart.vue';
|
import TopCostCentersChart from '@/components/widgets/TopCostCentersChart.vue';
|
||||||
import TopIncomeCentersChart from '@/components/widgets/TopIncomeCentersChart.vue';
|
import TopIncomeCentersChart from '@/components/widgets/TopIncomeCentersChart.vue';
|
||||||
|
import ChequesSummaryWidget from '@/components/widgets/ChequesSummaryWidget.vue';
|
||||||
|
import ChequesDueTodayWidget from '@/components/widgets/ChequesDueTodayWidget.vue';
|
||||||
|
import ChequesStatusChart from '@/components/widgets/ChequesStatusChart.vue';
|
||||||
|
import ChequesMonthlyChart from '@/components/widgets/ChequesMonthlyChart.vue';
|
||||||
|
import ChequesDueSoonWidget from '@/components/widgets/ChequesDueSoonWidget.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "dashboard",
|
name: "dashboard",
|
||||||
|
@ -265,6 +301,11 @@ export default {
|
||||||
TopCommoditiesChart,
|
TopCommoditiesChart,
|
||||||
TopCostCentersChart,
|
TopCostCentersChart,
|
||||||
TopIncomeCentersChart,
|
TopIncomeCentersChart,
|
||||||
|
ChequesSummaryWidget,
|
||||||
|
ChequesDueTodayWidget,
|
||||||
|
ChequesStatusChart,
|
||||||
|
ChequesMonthlyChart,
|
||||||
|
ChequesDueSoonWidget,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
@ -294,6 +335,11 @@ export default {
|
||||||
topCostCenters: false,
|
topCostCenters: false,
|
||||||
incomes: false,
|
incomes: false,
|
||||||
topIncomeCenters: false,
|
topIncomeCenters: false,
|
||||||
|
cheques: false,
|
||||||
|
chequesDueToday: false,
|
||||||
|
chequesStatusChart: false,
|
||||||
|
chequesMonthlyChart: false,
|
||||||
|
chequesDueSoon: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue