Files
panel/resources/js/components/CostOverview.vue

346 lines
7.7 KiB
Vue
Raw Permalink Normal View History

2025-08-04 16:33:07 +03:30
<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>