259 lines
7.4 KiB
Vue
259 lines
7.4 KiB
Vue
|
<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>
|