2025-08-04 16:33:07 +03:30
|
|
|
<script setup>
|
|
|
|
|
import { useDisplay, useTheme } from 'vuetify'
|
|
|
|
|
import { hexToRgb } from '@layouts/utils'
|
2025-09-15 15:46:14 +03:30
|
|
|
import { computed, onMounted, onUnmounted, ref, nextTick } from 'vue'
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
donutColors: {
|
|
|
|
|
type: Array,
|
|
|
|
|
default: () => []
|
|
|
|
|
},
|
|
|
|
|
progress: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 75
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const vuetifyTheme = useTheme()
|
|
|
|
|
const display = useDisplay()
|
|
|
|
|
const cardRef = ref(null)
|
2025-09-15 15:46:14 +03:30
|
|
|
const cardWidth = ref(300)
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
const updateCardWidth = () => {
|
|
|
|
|
if (cardRef.value) {
|
2025-09-15 15:46:14 +03:30
|
|
|
const rect = cardRef.value.getBoundingClientRect()
|
|
|
|
|
const newWidth = rect.width
|
|
|
|
|
if (newWidth > 0 && Math.abs(newWidth - cardWidth.value) > 5) {
|
|
|
|
|
cardWidth.value = newWidth
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
onMounted(async () => {
|
|
|
|
|
await nextTick()
|
2025-08-04 16:33:07 +03:30
|
|
|
updateCardWidth()
|
|
|
|
|
window.addEventListener('resize', updateCardWidth)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', updateCardWidth)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const chartSeries = computed(() => [props.progress, 100 - props.progress])
|
|
|
|
|
|
|
|
|
|
const processColors = (colors) => {
|
|
|
|
|
const currentTheme = vuetifyTheme.current.value.colors
|
|
|
|
|
return colors.map(color => {
|
|
|
|
|
if (typeof color === 'string' && currentTheme[color]) {
|
|
|
|
|
return currentTheme[color]
|
|
|
|
|
}
|
|
|
|
|
return color
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chartSize = computed(() => {
|
2025-09-15 15:46:14 +03:30
|
|
|
const width = cardWidth.value
|
|
|
|
|
const breakpoints = {
|
|
|
|
|
xs: display.xs.value,
|
|
|
|
|
sm: display.sm.value,
|
|
|
|
|
md: display.md.value
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
let size = 120
|
|
|
|
|
|
|
|
|
|
if (breakpoints.xs) {
|
|
|
|
|
size = Math.min(width * 0.35, 120)
|
|
|
|
|
} else if (breakpoints.sm) {
|
|
|
|
|
size = Math.min(width * 0.4, 150)
|
|
|
|
|
} else if (width <= 400) {
|
|
|
|
|
size = Math.min(width * 0.4, 160)
|
|
|
|
|
} else if (width <= 600) {
|
|
|
|
|
size = Math.min(width * 0.35, 200)
|
2025-08-04 16:33:07 +03:30
|
|
|
} else {
|
2025-09-15 15:46:14 +03:30
|
|
|
size = Math.min(width * 0.3, 240)
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
size = Math.max(size, 100)
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
return { width: size, height: size }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const textSizes = computed(() => {
|
2025-09-15 15:46:14 +03:30
|
|
|
const width = cardWidth.value
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
if (display.xs.value || width < 300) {
|
2025-08-04 16:33:07 +03:30
|
|
|
return {
|
|
|
|
|
title: 'text-body-2',
|
|
|
|
|
subtitle: 'text-caption',
|
|
|
|
|
mainNumber: 'text-h6',
|
2025-09-15 15:46:14 +03:30
|
|
|
percentage: 'text-caption'
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
2025-09-15 15:46:14 +03:30
|
|
|
} else if (display.sm.value || width < 450) {
|
2025-08-04 16:33:07 +03:30
|
|
|
return {
|
|
|
|
|
title: 'text-body-1',
|
|
|
|
|
subtitle: 'text-body-2',
|
|
|
|
|
mainNumber: 'text-h5',
|
|
|
|
|
percentage: 'text-body-2'
|
|
|
|
|
}
|
2025-09-15 15:46:14 +03:30
|
|
|
} else if (width < 600) {
|
2025-08-04 16:33:07 +03:30
|
|
|
return {
|
|
|
|
|
title: 'text-h6',
|
|
|
|
|
subtitle: 'text-body-2',
|
|
|
|
|
mainNumber: 'text-h4',
|
|
|
|
|
percentage: 'text-body-1'
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {
|
2025-09-15 15:46:14 +03:30
|
|
|
title: 'text-h5',
|
2025-08-04 16:33:07 +03:30
|
|
|
subtitle: 'text-body-1',
|
2025-09-15 15:46:14 +03:30
|
|
|
mainNumber: 'text-h3',
|
2025-08-04 16:33:07 +03:30
|
|
|
percentage: 'text-body-1'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const chartOptions = computed(() => {
|
|
|
|
|
const currentTheme = vuetifyTheme.current.value.colors
|
|
|
|
|
const variableTheme = vuetifyTheme.current.value.variables
|
2025-09-15 15:46:14 +03:30
|
|
|
const width = cardWidth.value
|
|
|
|
|
const chartSizeValue = chartSize.value.width
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
const defaultDonutColors = [currentTheme.success]
|
2025-08-04 16:33:07 +03:30
|
|
|
const usedDonutColors = props.donutColors.length
|
|
|
|
|
? processColors(props.donutColors)
|
|
|
|
|
: defaultDonutColors
|
|
|
|
|
|
|
|
|
|
const headingColor = `rgba(${hexToRgb(currentTheme['on-background'])},${variableTheme['high-emphasis-opacity']})`
|
|
|
|
|
const backgroundCircleColor = `rgba(${hexToRgb(usedDonutColors[0])}, 0.15)`
|
|
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
const valueFontSize = chartSizeValue < 120 ? '0.9rem' :
|
|
|
|
|
chartSizeValue < 160 ? '1.2rem' :
|
|
|
|
|
chartSizeValue < 200 ? '1.4rem' : '1.6rem'
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
const labelFontSize = chartSizeValue < 120 ? '0.7rem' :
|
|
|
|
|
chartSizeValue < 160 ? '0.8rem' :
|
|
|
|
|
chartSizeValue < 200 ? '0.9rem' : '1rem'
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-15 15:46:14 +03:30
|
|
|
const donutSize = chartSizeValue < 130 ? '60%' :
|
|
|
|
|
chartSizeValue < 180 ? '65%' : '70%'
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
chart: {
|
|
|
|
|
parentHeightOffset: 0,
|
|
|
|
|
type: 'donut',
|
2025-09-15 15:46:14 +03:30
|
|
|
sparkline: { enabled: false },
|
|
|
|
|
animations: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
easing: 'easeinout',
|
|
|
|
|
speed: 800,
|
|
|
|
|
dynamicAnimation: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
speed: 350
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
},
|
|
|
|
|
labels: ['Progress', 'Remaining'],
|
|
|
|
|
colors: [...usedDonutColors, backgroundCircleColor],
|
2025-09-15 15:46:14 +03:30
|
|
|
stroke: { width: 0 },
|
|
|
|
|
dataLabels: { enabled: false },
|
|
|
|
|
legend: { show: false },
|
2025-08-04 16:33:07 +03:30
|
|
|
tooltip: {
|
|
|
|
|
enabled: true,
|
2025-09-15 15:46:14 +03:30
|
|
|
theme: vuetifyTheme.current.value.dark ? 'dark' : 'light',
|
2025-08-04 16:33:07 +03:30
|
|
|
style: {
|
2025-09-15 15:46:14 +03:30
|
|
|
fontSize: chartSizeValue < 140 ? '11px' : '13px'
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
grid: {
|
2025-09-15 15:46:14 +03:30
|
|
|
padding: { top: 0, bottom: 0, right: 0, left: 0 }
|
2025-08-04 16:33:07 +03:30
|
|
|
},
|
|
|
|
|
states: {
|
|
|
|
|
hover: {
|
2025-09-15 15:46:14 +03:30
|
|
|
filter: { type: 'none' }
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
plotOptions: {
|
|
|
|
|
pie: {
|
|
|
|
|
startAngle: -90,
|
|
|
|
|
endAngle: 270,
|
|
|
|
|
donut: {
|
|
|
|
|
size: donutSize,
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
labels: {
|
|
|
|
|
show: true,
|
|
|
|
|
value: {
|
|
|
|
|
show: true,
|
|
|
|
|
fontSize: valueFontSize,
|
|
|
|
|
fontFamily: 'Public Sans',
|
|
|
|
|
color: headingColor,
|
|
|
|
|
fontWeight: 600,
|
2025-09-15 15:46:14 +03:30
|
|
|
offsetY: chartSizeValue < 130 ? -1 : 0,
|
2025-08-04 16:33:07 +03:30
|
|
|
formatter(val) {
|
|
|
|
|
return `${Math.round(val)}%`
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-09-15 15:46:14 +03:30
|
|
|
name: { show: false },
|
2025-08-04 16:33:07 +03:30
|
|
|
total: {
|
|
|
|
|
show: true,
|
|
|
|
|
showAlways: true,
|
|
|
|
|
color: usedDonutColors[0],
|
|
|
|
|
fontSize: labelFontSize,
|
|
|
|
|
label: 'Progress',
|
|
|
|
|
fontFamily: 'Public Sans',
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
formatter() {
|
|
|
|
|
return `${props.progress}%`
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-09-15 15:46:14 +03:30
|
|
|
responsive: []
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const cardBackgroundStyle = computed(() => {
|
2025-09-15 15:46:14 +03:30
|
|
|
const defaultDonutColors = [vuetifyTheme.current.value.colors.success]
|
2025-08-04 16:33:07 +03:30
|
|
|
const colors = props.donutColors.length ? processColors(props.donutColors) : defaultDonutColors
|
|
|
|
|
|
|
|
|
|
const createGradientColor = (color, opacity = 0.08) => {
|
|
|
|
|
if (color.includes('rgba')) {
|
|
|
|
|
return color.replace(/[\d\.]+\)$/g, `${opacity})`)
|
|
|
|
|
}
|
|
|
|
|
if (color.startsWith('#')) {
|
|
|
|
|
return `rgba(${hexToRgb(color)}, ${opacity})`
|
|
|
|
|
}
|
|
|
|
|
if (color.includes('rgb')) {
|
|
|
|
|
return color.replace('rgb', 'rgba').replace(')', `, ${opacity})`)
|
|
|
|
|
}
|
|
|
|
|
return `rgba(${hexToRgb(color)}, ${opacity})`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const gradientColors = colors.map((color, index) =>
|
2025-09-15 15:46:14 +03:30
|
|
|
createGradientColor(color, index === 0 ? 0.1 : 0.03)
|
2025-08-04 16:33:07 +03:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
background: `linear-gradient(135deg,
|
|
|
|
|
${gradientColors[0]} 0%,
|
2025-09-15 15:46:14 +03:30
|
|
|
${gradientColors[1] || createGradientColor(colors[0], 0.05)} 50%,
|
|
|
|
|
${gradientColors[2] || createGradientColor(colors[0], 0.02)} 100%)`
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const layoutClasses = computed(() => {
|
2025-09-15 15:46:14 +03:30
|
|
|
const width = cardWidth.value
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
return {
|
2025-09-15 15:46:14 +03:30
|
|
|
cardPadding: display.xs.value ? 'pa-3' :
|
|
|
|
|
display.sm.value ? 'pa-4' :
|
|
|
|
|
width < 400 ? 'pa-4' : 'pa-5',
|
|
|
|
|
textSpacing: display.xs.value ? 'me-2' :
|
|
|
|
|
display.sm.value ? 'me-3' :
|
|
|
|
|
width < 400 ? 'me-3' : 'me-4',
|
|
|
|
|
iconSize: display.xs.value ? 14 :
|
|
|
|
|
display.sm.value ? 16 :
|
|
|
|
|
width < 400 ? 16 : 18,
|
|
|
|
|
itemSpacing: display.xs.value ? 'mb-2' :
|
|
|
|
|
width < 400 ? 'mb-3' : 'mb-3'
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<VCard
|
|
|
|
|
ref="cardRef"
|
|
|
|
|
class="overflow-visible"
|
|
|
|
|
:style="cardBackgroundStyle"
|
|
|
|
|
>
|
|
|
|
|
<VCardText
|
|
|
|
|
class="d-flex align-center justify-space-between"
|
2025-09-15 15:46:14 +03:30
|
|
|
:class="layoutClasses.cardPadding"
|
2025-08-04 16:33:07 +03:30
|
|
|
>
|
|
|
|
|
<div
|
2025-09-15 15:46:14 +03:30
|
|
|
class="d-flex flex-column flex-grow-1 flex-shrink-1"
|
|
|
|
|
:class="layoutClasses.textSpacing"
|
|
|
|
|
style="min-width: 0;"
|
2025-08-04 16:33:07 +03:30
|
|
|
>
|
2025-09-15 15:46:14 +03:30
|
|
|
<div :class="layoutClasses.itemSpacing">
|
2025-08-04 16:33:07 +03:30
|
|
|
<h5
|
2025-09-15 15:46:14 +03:30
|
|
|
class="text-no-wrap font-weight-medium text-truncate"
|
2025-08-04 16:33:07 +03:30
|
|
|
:class="textSizes.title"
|
|
|
|
|
style="line-height: 1.2;"
|
|
|
|
|
>
|
|
|
|
|
Generated Leads
|
|
|
|
|
</h5>
|
|
|
|
|
<div
|
2025-09-15 15:46:14 +03:30
|
|
|
class="text-medium-emphasis text-truncate"
|
2025-08-04 16:33:07 +03:30
|
|
|
:class="textSizes.subtitle"
|
|
|
|
|
style="line-height: 1.1;"
|
|
|
|
|
>
|
|
|
|
|
Monthly Report
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h3
|
|
|
|
|
class="font-weight-bold"
|
2025-09-15 15:46:14 +03:30
|
|
|
:class="[textSizes.mainNumber, layoutClasses.itemSpacing]"
|
2025-08-04 16:33:07 +03:30
|
|
|
style="line-height: 1.1;"
|
|
|
|
|
>
|
|
|
|
|
4,350
|
|
|
|
|
</h3>
|
|
|
|
|
<div class="d-flex align-center">
|
|
|
|
|
<VIcon
|
|
|
|
|
icon="tabler-chevron-up"
|
|
|
|
|
:color="(props.donutColors.length ? processColors(props.donutColors) : [vuetifyTheme.current.value.colors.success])[0]"
|
|
|
|
|
:size="layoutClasses.iconSize"
|
2025-09-15 15:46:14 +03:30
|
|
|
class="me-1 flex-shrink-0"
|
2025-08-04 16:33:07 +03:30
|
|
|
/>
|
|
|
|
|
<span
|
2025-09-15 15:46:14 +03:30
|
|
|
class="font-weight-medium text-no-wrap"
|
2025-08-04 16:33:07 +03:30
|
|
|
:class="textSizes.percentage"
|
|
|
|
|
:style="{ color: (props.donutColors.length ? processColors(props.donutColors) : [vuetifyTheme.current.value.colors.success])[0] }"
|
|
|
|
|
>
|
|
|
|
|
15.8%
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
2025-09-15 15:46:14 +03:30
|
|
|
class="d-flex align-center justify-center flex-shrink-0"
|
2025-08-04 16:33:07 +03:30
|
|
|
>
|
|
|
|
|
<VueApexCharts
|
|
|
|
|
:options="chartOptions"
|
|
|
|
|
:series="chartSeries"
|
|
|
|
|
:height="chartSize.height"
|
|
|
|
|
:width="chartSize.width"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</VCardText>
|
|
|
|
|
</VCard>
|
2025-09-15 15:46:14 +03:30
|
|
|
</template>
|