add some widgets about cheque to dashboard

This commit is contained in:
Hesabix 2025-08-17 17:34:52 +00:00
parent 93bdf0fac4
commit ca043a913f
12 changed files with 1195 additions and 2 deletions

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

View file

@ -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
]);
}
} }

View file

@ -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();

View file

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

View file

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

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

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

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

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

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

View file

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

View file

@ -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,
}, },
}; };
}, },