346 lines
7.7 KiB
Vue
346 lines
7.7 KiB
Vue
|
|
<template>
|
||
|
|
<VCard
|
||
|
|
class="overflow-visible"
|
||
|
|
:style="cardBackgroundStyle"
|
||
|
|
>
|
||
|
|
<VCardText class="d-flex flex-column align-center pa-4">
|
||
|
|
<!-- Header Section - Responsive -->
|
||
|
|
<div class="d-flex flex-column flex-sm-row justify-space-between align-start align-sm-center w-100 mb-4 gap-3">
|
||
|
|
<div class="text-center text-sm-start">
|
||
|
|
<h5 class="text-h5 text-wrap mb-2">
|
||
|
|
Cost Overview
|
||
|
|
</h5>
|
||
|
|
<div class="text-body-1 mb-2 mb-sm-4 text-medium-emphasis">
|
||
|
|
Monthly Breakdown
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="text-center text-sm-end">
|
||
|
|
<h3 class="text-h4 text-sm-h3 mb-1">
|
||
|
|
{{ totalCostFormatted }}
|
||
|
|
</h3>
|
||
|
|
<div class="d-flex align-center justify-center justify-sm-end">
|
||
|
|
<VIcon
|
||
|
|
icon="tabler-trending-up"
|
||
|
|
color="success"
|
||
|
|
size="20"
|
||
|
|
class="me-1"
|
||
|
|
/>
|
||
|
|
<span class="text-success font-weight-medium text-sm">
|
||
|
|
+12.5%
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Chart Container - Responsive -->
|
||
|
|
<div
|
||
|
|
class="d-flex justify-center position-relative chart-container"
|
||
|
|
>
|
||
|
|
<VueApexCharts
|
||
|
|
:options="chartOptions"
|
||
|
|
:series="series"
|
||
|
|
type="donut"
|
||
|
|
width="100%"
|
||
|
|
height="100%"
|
||
|
|
class="chart-responsive"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<!-- Center Content -->
|
||
|
|
<div class="chart-center-content">
|
||
|
|
<div class="text-center">
|
||
|
|
<h4 class="text-h5 text-sm-h4 mb-1">{{ totalCostFormatted }}</h4>
|
||
|
|
<span class="text-body-2 text-medium-emphasis">Total Cost</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Stats Cards - Responsive Grid -->
|
||
|
|
<div class="stats-grid w-100 mt-4">
|
||
|
|
<div
|
||
|
|
v-for="(item, index) in costBreakdown"
|
||
|
|
:key="index"
|
||
|
|
class="stat-item pa-3 rounded-lg"
|
||
|
|
:style="getItemCardStyle(index)"
|
||
|
|
>
|
||
|
|
<div class="d-flex align-center justify-center mb-2">
|
||
|
|
<div
|
||
|
|
class="dot me-2"
|
||
|
|
:style="{ backgroundColor: chartColors[index] }"
|
||
|
|
></div>
|
||
|
|
<span class="text-body-2 text-medium-emphasis text-center">{{ item.label }}</span>
|
||
|
|
</div>
|
||
|
|
<h6 class="text-h6 text-center">{{ formatCurrency(item.value) }}</h6>
|
||
|
|
<div class="text-caption text-center">{{ item.percentage }}%</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</VCardText>
|
||
|
|
</VCard>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, computed } from 'vue'
|
||
|
|
import { useTheme } from 'vuetify'
|
||
|
|
import { hexToRgb } from '@layouts/utils'
|
||
|
|
import VueApexCharts from 'vue3-apexcharts'
|
||
|
|
|
||
|
|
const vuetifyTheme = useTheme()
|
||
|
|
const currentTheme = computed(() => vuetifyTheme.current.value.colors)
|
||
|
|
|
||
|
|
const labels = ['Human Resources', 'Materials', 'Equipment']
|
||
|
|
const series = ref([3000, 5000, 2000])
|
||
|
|
|
||
|
|
const totalCost = computed(() =>
|
||
|
|
series.value.reduce((a, b) => a + b, 0)
|
||
|
|
)
|
||
|
|
|
||
|
|
const totalCostFormatted = computed(() =>
|
||
|
|
new Intl.NumberFormat('en-US', {
|
||
|
|
style: 'currency',
|
||
|
|
currency: 'USD',
|
||
|
|
minimumFractionDigits: 0
|
||
|
|
}).format(totalCost.value)
|
||
|
|
)
|
||
|
|
|
||
|
|
const formatCurrency = (value) => {
|
||
|
|
return new Intl.NumberFormat('en-US', {
|
||
|
|
style: 'currency',
|
||
|
|
currency: 'USD',
|
||
|
|
minimumFractionDigits: 0
|
||
|
|
}).format(value)
|
||
|
|
}
|
||
|
|
|
||
|
|
const costBreakdown = computed(() => {
|
||
|
|
return labels.map((label, index) => ({
|
||
|
|
label,
|
||
|
|
value: series.value[index],
|
||
|
|
percentage: Math.round((series.value[index] / totalCost.value) * 100)
|
||
|
|
}))
|
||
|
|
})
|
||
|
|
|
||
|
|
const chartColors = computed(() => [
|
||
|
|
`rgba(${hexToRgb(currentTheme.value.primary)}, 1)`,
|
||
|
|
`rgba(${hexToRgb(currentTheme.value.success)}, 1)`,
|
||
|
|
`rgba(${hexToRgb(currentTheme.value.warning)}, 1)`
|
||
|
|
])
|
||
|
|
|
||
|
|
// Card Background Style
|
||
|
|
const cardBackgroundStyle = computed(() => {
|
||
|
|
const primaryColor = currentTheme.value.primary
|
||
|
|
|
||
|
|
const createGradientColor = (color, opacity = 0.08) => {
|
||
|
|
if (color.startsWith('#')) {
|
||
|
|
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||
|
|
}
|
||
|
|
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||
|
|
}
|
||
|
|
|
||
|
|
const gradientColor1 = createGradientColor(primaryColor, 0.1)
|
||
|
|
const gradientColor2 = createGradientColor(primaryColor, 0.03)
|
||
|
|
|
||
|
|
return {
|
||
|
|
background: `linear-gradient(135deg,
|
||
|
|
${gradientColor1} 0%,
|
||
|
|
${gradientColor2} 50%,
|
||
|
|
${gradientColor1} 100%)`
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
const getItemCardStyle = (index) => {
|
||
|
|
const color = chartColors.value[index]
|
||
|
|
const bgColor = color.replace('1)', '0.1)')
|
||
|
|
|
||
|
|
return {
|
||
|
|
backgroundColor: bgColor,
|
||
|
|
border: `1px solid ${color.replace('1)', '0.2)')}`
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const chartOptions = computed(() => {
|
||
|
|
const labelColor = `rgba(${hexToRgb(currentTheme.value['on-surface'])}, ${vuetifyTheme.current.value.variables['disabled-opacity']})`
|
||
|
|
|
||
|
|
return {
|
||
|
|
labels,
|
||
|
|
colors: chartColors.value,
|
||
|
|
chart: {
|
||
|
|
type: 'donut',
|
||
|
|
toolbar: { show: false },
|
||
|
|
sparkline: { enabled: false }
|
||
|
|
},
|
||
|
|
stroke: {
|
||
|
|
colors: ['transparent'],
|
||
|
|
width: 0
|
||
|
|
},
|
||
|
|
legend: {
|
||
|
|
show: false
|
||
|
|
},
|
||
|
|
tooltip: {
|
||
|
|
theme: vuetifyTheme.current.value.dark ? 'dark' : 'light',
|
||
|
|
style: { fontFamily: 'inherit' },
|
||
|
|
y: {
|
||
|
|
formatter: (val) => formatCurrency(val)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
dataLabels: {
|
||
|
|
enabled: true,
|
||
|
|
formatter: (val) => Math.round(val) + '%',
|
||
|
|
style: {
|
||
|
|
fontSize: '12px',
|
||
|
|
fontWeight: 'bold',
|
||
|
|
colors: ['#fff']
|
||
|
|
}
|
||
|
|
},
|
||
|
|
plotOptions: {
|
||
|
|
pie: {
|
||
|
|
donut: {
|
||
|
|
size: '75%',
|
||
|
|
labels: {
|
||
|
|
show: false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
responsive: [
|
||
|
|
{
|
||
|
|
breakpoint: 600,
|
||
|
|
options: {
|
||
|
|
chart: {
|
||
|
|
width: 280,
|
||
|
|
height: 280
|
||
|
|
},
|
||
|
|
dataLabels: {
|
||
|
|
style: {
|
||
|
|
fontSize: '10px'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
breakpoint: 480,
|
||
|
|
options: {
|
||
|
|
chart: {
|
||
|
|
width: 250,
|
||
|
|
height: 250
|
||
|
|
},
|
||
|
|
dataLabels: {
|
||
|
|
style: {
|
||
|
|
fontSize: '9px'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
/* Chart Container - Responsive */
|
||
|
|
.chart-container {
|
||
|
|
width: 100%;
|
||
|
|
max-width: 330px;
|
||
|
|
height: 330px;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 599px) {
|
||
|
|
.chart-container {
|
||
|
|
max-width: 280px;
|
||
|
|
height: 280px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 479px) {
|
||
|
|
.chart-container {
|
||
|
|
max-width: 250px;
|
||
|
|
height: 250px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.chart-responsive {
|
||
|
|
width: 100% !important;
|
||
|
|
height: 100% !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chart-center-content {
|
||
|
|
position: absolute;
|
||
|
|
top: 50%;
|
||
|
|
left: 50%;
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dot {
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
border-radius: 50%;
|
||
|
|
display: inline-block;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Stats Grid - Responsive */
|
||
|
|
.stats-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(3, 1fr);
|
||
|
|
gap: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 767px) {
|
||
|
|
.stats-grid {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
gap: 0.75rem;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 599px) and (min-width: 480px) {
|
||
|
|
.stats-grid {
|
||
|
|
grid-template-columns: repeat(2, 1fr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-item {
|
||
|
|
min-height: fit-content;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Responsive Typography */
|
||
|
|
@media (max-width: 599px) {
|
||
|
|
.text-h5 {
|
||
|
|
font-size: 1.25rem !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
.text-h4 {
|
||
|
|
font-size: 1.5rem !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
.text-h6 {
|
||
|
|
font-size: 1rem !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 479px) {
|
||
|
|
.text-h5 {
|
||
|
|
font-size: 1.125rem !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
.text-h4 {
|
||
|
|
font-size: 1.25rem !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chart-center-content h4 {
|
||
|
|
font-size: 1.125rem !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Card padding adjustments for mobile */
|
||
|
|
@media (max-width: 599px) {
|
||
|
|
.pa-4 {
|
||
|
|
padding: 1rem !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 479px) {
|
||
|
|
.pa-4 {
|
||
|
|
padding: 0.75rem !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|