refactor: demo page

This commit is contained in:
2025-09-24 12:12:33 +03:30
parent 3f7af72848
commit 617bddefa1
6 changed files with 479 additions and 122 deletions

33
config/cors.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Laravel CORS Configuration
|--------------------------------------------------------------------------
|
| این تنظیمات برای کنترل درخواست‌های Cross-Origin استفاده میشن
|
*/
'paths' => ['api/*', 'dev/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [
'http://localhost:8000',
'http://megategra.com:8001'
],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];

View File

@@ -40,7 +40,7 @@ const vuetifyTheme = useTheme()
const route = useRoute() const route = useRoute()
const isCrmRoute = computed(() => { const isCrmRoute = computed(() => {
return route.path === '/dashboards/crm' return route.path === '/dashboards/crm' || route.path === '/dashboards/demo'
}) })
const colors = [ const colors = [

View File

@@ -10,8 +10,8 @@ import TasksTable from "./task.vue";
import ResourcesTable from "./resource.vue"; import ResourcesTable from "./resource.vue";
import GanttChart from './gantt.vue' import GanttChart from './gantt.vue'
const isEditMode = ref(false) const isEditMode = ref(false)
const autoCompact = ref(true)
let grid = null let grid = null
const handleEditModeChange = (event) => { const handleEditModeChange = (event) => {
@@ -33,6 +33,9 @@ const handleDeleteCard = (cardId) => {
const element = document.querySelector(`[data-card-id="${cardId}"]`) const element = document.querySelector(`[data-card-id="${cardId}"]`)
if (element && grid) { if (element && grid) {
grid.removeWidget(element) grid.removeWidget(element)
if (autoCompact.value) {
grid.compact()
}
} }
} }
@@ -45,6 +48,12 @@ onMounted(async () => {
resizable: true, resizable: true,
disableOneColumnMode: true, disableOneColumnMode: true,
animate: true, animate: true,
columnOpts: {
breakpoints: [
{ w: 768, c: 1 },
{ w: 992, c: 12 }
]
}
}) })
grid.enableMove(false) grid.enableMove(false)
@@ -60,22 +69,6 @@ onUnmounted(() => {
if (grid) { if (grid) {
grid.destroy(false) grid.destroy(false)
} }
setTimeout(() => {
if (gridContainer.value) {
grid = GridStack.init({
column: 12,
cellHeight: 50,
float: true,
draggable: {
handle: '.grid-stack-item-content'
},
resizable: {
handles: 'e, se, s, sw, w'
}
}, gridContainer.value)
}
}, 100)
}) })
</script> </script>
@@ -101,6 +94,16 @@ onUnmounted(() => {
</p> </p>
</div> </div>
</div> </div>
<div class="banner-controls">
<VSwitch
v-model="autoCompact"
label="Auto Compact"
class="auto-compact-switch"
hide-details
inset
/>
</div>
</div> </div>
<div class="banner-decorations"> <div class="banner-decorations">
@@ -203,13 +206,13 @@ onUnmounted(() => {
</div> </div>
<div class="grid-stack-item" :class="{ 'edit-mode': isEditMode }" gs-w="4" gs-h="7" gs-max-h="7" <div class="grid-stack-item" :class="{ 'edit-mode': isEditMode }" gs-w="4" gs-h="7" gs-max-h="7"
data-card-id="analysis"> data-card-id="analysis2">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<div v-if="isEditMode" class="drag-handle" title="Move analysis"> <div v-if="isEditMode" class="drag-handle" title="Move analysis2">
<VIcon size="18">mdi-drag-horizontal-variant</VIcon> <VIcon size="18">mdi-drag-horizontal-variant</VIcon>
</div> </div>
<VBtn v-if="isEditMode" variant="text" size="small" color="error" class="delete-btn-chart" <VBtn v-if="isEditMode" variant="text" size="small" color="error" class="delete-btn-chart"
title="Delete analysis" @click.stop="handleDeleteCard('analysis')"> title="Delete analysis2" @click.stop="handleDeleteCard('analysis2')">
<VIcon icon="tabler-trash-x" size="16" class="me-1" /> <VIcon icon="tabler-trash-x" size="16" class="me-1" />
Delete Delete
</VBtn> </VBtn>
@@ -247,7 +250,7 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<div class="grid-stack-item" :class="{ 'edit-mode': isEditMode }" gs-w="12" gs-h="6" gs-max-h="6" <div class="grid-stack-item" :class="{ 'edit-mode': isEditMode }" gs-w="12" gs-h="6" gs-max-h="6"
data-card-id="gantt-chart"> data-card-id="gantt-chart">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<div v-if="isEditMode" class="drag-handle" title="Move gantt-chart"> <div v-if="isEditMode" class="drag-handle" title="Move gantt-chart">

View File

@@ -2,6 +2,22 @@
import HC from "highcharts" import HC from "highcharts"
import { ref, onMounted } from "vue" import { ref, onMounted } from "vue"
// Props for component reusability
const props = defineProps({
title: {
type: String,
default: "The Germanic Language Tree"
},
backgroundColor: {
type: String,
default: ""
},
showBackground: {
type: Boolean,
default: true
}
})
const cssRGB = name => getComputedStyle(document.documentElement).getPropertyValue(name).trim() const cssRGB = name => getComputedStyle(document.documentElement).getPropertyValue(name).trim()
const toHex = rgb => { const toHex = rgb => {
const [r,g,b] = rgb.split(",").map(v => parseInt(v,10)) const [r,g,b] = rgb.split(",").map(v => parseInt(v,10))
@@ -62,7 +78,7 @@ onMounted(() => {
const chartOptions = ref({ const chartOptions = ref({
chart: { inverted: true, height: 1200, backgroundColor: 'transparent' }, chart: { inverted: true, height: 1200, backgroundColor: 'transparent' },
title: { text: "The Germanic Language Tree" }, title: { text: props.title },
accessibility: { point: { descriptionFormat: "{add index 1}. {toNode.id} comes from {fromNode.id}" } }, accessibility: { point: { descriptionFormat: "{add index 1}. {toNode.id} comes from {fromNode.id}" } },
tooltip: { outside: true }, tooltip: { outside: true },
series: [{ series: [{
@@ -133,7 +149,12 @@ const chartOptions = ref({
</script> </script>
<template> <template>
<div class="chart-container"> <div
class="chart-container"
:style="{
backgroundColor: showBackground ? backgroundColor : 'transparent'
}"
>
<highcharts :options="chartOptions" /> <highcharts :options="chartOptions" />
</div> </div>
</template> </template>
@@ -141,7 +162,7 @@ const chartOptions = ref({
<style scoped> <style scoped>
.chart-container { .chart-container {
padding: 16px; padding: 16px;
background-color: transparent;
min-height: 100vh; min-height: 100vh;
border-radius: 8px;
} }
</style> </style>

View File

@@ -1,45 +1,276 @@
<script setup> <script setup>
import { useDisplay, useTheme } from 'vuetify' import { useDisplay, useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils' import { hexToRgb } from '@layouts/utils'
import { computed, onMounted, onUnmounted, ref, nextTick } from 'vue' import { computed, onMounted, onUnmounted, ref, nextTick, watch } from 'vue'
const props = defineProps({ const props = defineProps({
apiUrl: {
type: String,
required: true
},
apiHeaders: {
type: Object,
default: () => ({})
},
dataKey: {
type: String,
default: 'data'
},
sumFields: {
type: Array,
default: () => ['cost']
},
sumField: {
type: String,
default: 'cost'
},
nameField: {
type: String,
default: 'name'
},
title: {
type: String,
default: 'Generated Leads'
},
subtitle: {
type: String,
default: 'Total Summary'
},
currency: {
type: String,
default: '$'
},
showPercentage: {
type: Boolean,
default: true
},
previousValue: {
type: Number,
default: null
},
displayMode: {
type: String,
default: 'single',
validator: (value) => ['single', 'multiple'].includes(value)
},
donutColors: { donutColors: {
type: Array, type: Array,
default: () => [] default: () => ['success']
}, },
progress: { progress: {
type: Number, type: Number,
default: 75 default: null
},
target: {
type: Number,
default: null
},
refreshInterval: {
type: Number,
default: 0
},
roundNumbers: {
type: Boolean,
default: false
} }
}) })
const emit = defineEmits(['dataLoaded', 'error', 'loading'])
const vuetifyTheme = useTheme() const vuetifyTheme = useTheme()
const display = useDisplay() const display = useDisplay()
const cardRef = ref(null) const cardRef = ref(null)
const cardWidth = ref(300) const cardWidth = ref(300)
const isLoading = ref(false)
const error = ref(null)
const apiData = ref([])
const totalValue = ref(0)
const multipleValues = ref({})
const previousTotal = ref(0)
const refreshTimer = ref(null)
const effectiveSumFields = computed(() => {
if (props.displayMode === 'multiple' && props.sumFields.length > 0) {
return props.sumFields
}
return [props.sumField]
})
const fetchData = async () => {
try {
isLoading.value = true
error.value = null
emit('loading', true)
const response = await fetch(props.apiUrl, {
headers: {
'Content-Type': 'application/json',
...props.apiHeaders
}
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const responseData = await response.json()
const dataArray = responseData[props.dataKey] || responseData
if (!Array.isArray(dataArray)) {
throw new Error('Response data is not an array')
}
if (totalValue.value > 0) {
previousTotal.value = totalValue.value
}
apiData.value = dataArray
const newMultipleValues = {}
let newTotalValue = 0
effectiveSumFields.value.forEach(field => {
const fieldTotal = dataArray.reduce((sum, item) => {
const value = parseFloat(item[field]) || 0
return sum + value
}, 0)
newMultipleValues[field] = props.roundNumbers ? Math.round(fieldTotal) : fieldTotal
if (field === effectiveSumFields.value[0]) {
newTotalValue = newMultipleValues[field]
}
})
if (props.displayMode === 'multiple') {
newTotalValue = Object.values(newMultipleValues).reduce((sum, val) => sum + val, 0)
}
multipleValues.value = newMultipleValues
totalValue.value = newTotalValue
emit('dataLoaded', {
data: dataArray,
total: newTotalValue,
breakdown: newMultipleValues,
count: dataArray.length
})
} catch (err) {
error.value = err.message
emit('error', err.message)
} finally {
isLoading.value = false
emit('loading', false)
}
}
const setupRefresh = () => {
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
}
if (props.refreshInterval > 0) {
refreshTimer.value = setInterval(fetchData, props.refreshInterval)
}
}
const formatCurrency = (value) => {
if (typeof value !== 'number') return `${props.currency}0`
const roundedValue = Math.round(value)
return `${props.currency}${roundedValue.toLocaleString('en-US')}`
}
const formatFieldName = (field) => {
return field.charAt(0).toUpperCase() + field.slice(1).replace(/([A-Z])/g, ' $1')
}
const formattedTotal = computed(() => {
if (isLoading.value) return '...'
if (error.value) return 'Error'
return formatCurrency(totalValue.value)
})
const formattedBreakdown = computed(() => {
if (isLoading.value || error.value || props.displayMode === 'single') return []
return effectiveSumFields.value.map(field => ({
field,
label: formatFieldName(field),
value: multipleValues.value[field] || 0,
formatted: formatCurrency(multipleValues.value[field] || 0)
}))
})
const percentageChange = computed(() => {
if (!props.showPercentage || isLoading.value || error.value) return null
let baseValue = props.previousValue || previousTotal.value
if (!baseValue || baseValue === 0) return null
const change = ((totalValue.value - baseValue) / baseValue) * 100
return {
value: Math.abs(change).toFixed(1),
isPositive: change >= 0
}
})
const calculatedProgress = computed(() => {
if (props.progress !== null) return props.progress
if (!props.target || props.target === 0) return 75
return Math.min(Math.max((totalValue.value / props.target) * 100, 0), 100)
})
const updateCardWidth = () => { const updateCardWidth = () => {
if (cardRef.value) { if (cardRef.value && typeof cardRef.value.getBoundingClientRect === 'function') {
const rect = cardRef.value.getBoundingClientRect() try {
const newWidth = rect.width const rect = cardRef.value.getBoundingClientRect()
if (newWidth > 0 && Math.abs(newWidth - cardWidth.value) > 5) { const newWidth = rect.width
cardWidth.value = newWidth if (newWidth > 0 && Math.abs(newWidth - cardWidth.value) > 5) {
cardWidth.value = newWidth
}
} catch (error) {
console.warn('Error getting card dimensions:', error)
} }
} }
} }
onMounted(async () => { onMounted(async () => {
await nextTick() await nextTick()
updateCardWidth()
setTimeout(() => {
updateCardWidth()
}, 100)
window.addEventListener('resize', updateCardWidth) window.addEventListener('resize', updateCardWidth)
await fetchData()
setupRefresh()
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', updateCardWidth) window.removeEventListener('resize', updateCardWidth)
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
}
}) })
const chartSeries = computed(() => [props.progress, 100 - props.progress]) watch(() => props.apiUrl, () => {
fetchData()
})
watch(() => props.refreshInterval, () => {
setupRefresh()
})
const chartSeries = computed(() => [calculatedProgress.value, 100 - calculatedProgress.value])
const processColors = (colors) => { const processColors = (colors) => {
const currentTheme = vuetifyTheme.current.value.colors const currentTheme = vuetifyTheme.current.value.colors
@@ -58,9 +289,9 @@ const chartSize = computed(() => {
sm: display.sm.value, sm: display.sm.value,
md: display.md.value md: display.md.value
} }
let size = 120 let size = 120
if (breakpoints.xs) { if (breakpoints.xs) {
size = Math.min(width * 0.35, 120) size = Math.min(width * 0.35, 120)
} else if (breakpoints.sm) { } else if (breakpoints.sm) {
@@ -72,42 +303,46 @@ const chartSize = computed(() => {
} else { } else {
size = Math.min(width * 0.3, 240) size = Math.min(width * 0.3, 240)
} }
size = Math.max(size, 100) size = Math.max(size, 100)
return { width: size, height: size } return { width: size, height: size }
}) })
const textSizes = computed(() => { const textSizes = computed(() => {
const width = cardWidth.value const width = cardWidth.value
if (display.xs.value || width < 300) { if (display.xs.value || width < 300) {
return { return {
title: 'text-body-2', title: 'text-body-2',
subtitle: 'text-caption', subtitle: 'text-caption',
mainNumber: 'text-h6', mainNumber: 'text-h6',
percentage: 'text-caption' percentage: 'text-caption',
breakdown: 'text-caption'
} }
} else if (display.sm.value || width < 450) { } else if (display.sm.value || width < 450) {
return { return {
title: 'text-body-1', title: 'text-body-1',
subtitle: 'text-body-2', subtitle: 'text-body-2',
mainNumber: 'text-h5', mainNumber: 'text-h5',
percentage: 'text-body-2' percentage: 'text-body-2',
breakdown: 'text-body-2'
} }
} else if (width < 600) { } else if (width < 600) {
return { return {
title: 'text-h6', title: 'text-h6',
subtitle: 'text-body-2', subtitle: 'text-body-2',
mainNumber: 'text-h4', mainNumber: 'text-h4',
percentage: 'text-body-1' percentage: 'text-body-1',
breakdown: 'text-body-2'
} }
} else { } else {
return { return {
title: 'text-h5', title: 'text-h5',
subtitle: 'text-body-1', subtitle: 'text-body-1',
mainNumber: 'text-h3', mainNumber: 'text-h3',
percentage: 'text-body-1' percentage: 'text-body-1',
breakdown: 'text-body-1'
} }
} }
}) })
@@ -118,7 +353,7 @@ const chartOptions = computed(() => {
const width = cardWidth.value const width = cardWidth.value
const chartSizeValue = chartSize.value.width const chartSizeValue = chartSize.value.width
const defaultDonutColors = [currentTheme.success] const defaultDonutColors = [currentTheme.primary]
const usedDonutColors = props.donutColors.length const usedDonutColors = props.donutColors.length
? processColors(props.donutColors) ? processColors(props.donutColors)
: defaultDonutColors : defaultDonutColors
@@ -127,15 +362,15 @@ const chartOptions = computed(() => {
const backgroundCircleColor = `rgba(${hexToRgb(usedDonutColors[0])}, 0.15)` const backgroundCircleColor = `rgba(${hexToRgb(usedDonutColors[0])}, 0.15)`
const valueFontSize = chartSizeValue < 120 ? '0.9rem' : const valueFontSize = chartSizeValue < 120 ? '0.9rem' :
chartSizeValue < 160 ? '1.2rem' : chartSizeValue < 160 ? '1.2rem' :
chartSizeValue < 200 ? '1.4rem' : '1.6rem' chartSizeValue < 200 ? '1.4rem' : '1.6rem'
const labelFontSize = chartSizeValue < 120 ? '0.7rem' : const labelFontSize = chartSizeValue < 120 ? '0.7rem' :
chartSizeValue < 160 ? '0.8rem' : chartSizeValue < 160 ? '0.8rem' :
chartSizeValue < 200 ? '0.9rem' : '1rem' chartSizeValue < 200 ? '0.9rem' : '1rem'
const donutSize = chartSizeValue < 130 ? '60%' : const donutSize = chartSizeValue < 130 ? '60%' :
chartSizeValue < 180 ? '65%' : '70%' chartSizeValue < 180 ? '65%' : '70%'
return { return {
chart: { chart: {
@@ -202,7 +437,7 @@ const chartOptions = computed(() => {
fontFamily: 'Public Sans', fontFamily: 'Public Sans',
fontWeight: 500, fontWeight: 500,
formatter() { formatter() {
return `${props.progress}%` return `${Math.round(calculatedProgress.value)}%`
}, },
}, },
}, },
@@ -214,7 +449,7 @@ const chartOptions = computed(() => {
}) })
const cardBackgroundStyle = computed(() => { const cardBackgroundStyle = computed(() => {
const defaultDonutColors = [vuetifyTheme.current.value.colors.success] const defaultDonutColors = [vuetifyTheme.current.value.colors.primary]
const colors = props.donutColors.length ? processColors(props.donutColors) : defaultDonutColors const colors = props.donutColors.length ? processColors(props.donutColors) : defaultDonutColors
const createGradientColor = (color, opacity = 0.08) => { const createGradientColor = (color, opacity = 0.08) => {
@@ -244,90 +479,115 @@ const cardBackgroundStyle = computed(() => {
const layoutClasses = computed(() => { const layoutClasses = computed(() => {
const width = cardWidth.value const width = cardWidth.value
return { return {
cardPadding: display.xs.value ? 'pa-3' : cardPadding: display.xs.value ? 'pa-3' :
display.sm.value ? 'pa-4' : display.sm.value ? 'pa-4' :
width < 400 ? 'pa-4' : 'pa-5', width < 400 ? 'pa-4' : 'pa-5',
textSpacing: display.xs.value ? 'me-2' : textSpacing: display.xs.value ? 'me-2' :
display.sm.value ? 'me-3' : display.sm.value ? 'me-3' :
width < 400 ? 'me-3' : 'me-4', width < 400 ? 'me-3' : 'me-4',
iconSize: display.xs.value ? 14 : iconSize: display.xs.value ? 14 :
display.sm.value ? 16 : display.sm.value ? 16 :
width < 400 ? 16 : 18, width < 400 ? 16 : 18,
itemSpacing: display.xs.value ? 'mb-2' : itemSpacing: display.xs.value ? 'mb-2' :
width < 400 ? 'mb-3' : 'mb-3' width < 400 ? 'mb-3' : 'mb-3'
} }
}) })
defineExpose({
refresh: fetchData,
getData: () => apiData.value,
getTotal: () => totalValue.value,
getBreakdown: () => multipleValues.value
})
</script> </script>
<template> <template>
<VCard <VCard ref="cardRef" class="overflow-visible" :style="cardBackgroundStyle">
ref="cardRef" <VCardText class="d-flex align-center justify-space-between" :class="layoutClasses.cardPadding">
class="overflow-visible" <div class="d-flex flex-column flex-grow-1 flex-shrink-1" :class="layoutClasses.textSpacing"
:style="cardBackgroundStyle" style="min-width: 0;">
>
<VCardText
class="d-flex align-center justify-space-between"
:class="layoutClasses.cardPadding"
>
<div
class="d-flex flex-column flex-grow-1 flex-shrink-1"
:class="layoutClasses.textSpacing"
style="min-width: 0;"
>
<div :class="layoutClasses.itemSpacing"> <div :class="layoutClasses.itemSpacing">
<h5 <h5 class="text-no-wrap font-weight-medium text-truncate" :class="textSizes.title" style="line-height: 1.2;">
class="text-no-wrap font-weight-medium text-truncate" {{ title }}
:class="textSizes.title"
style="line-height: 1.2;"
>
Generated Leads
</h5> </h5>
<div <div class="text-medium-emphasis text-truncate" :class="textSizes.subtitle" style="line-height: 1.1;">
class="text-medium-emphasis text-truncate" {{ subtitle }}
:class="textSizes.subtitle"
style="line-height: 1.1;"
>
Monthly Report
</div> </div>
</div> </div>
<div> <div>
<h3 <template v-if="isLoading">
class="font-weight-bold" <VSkeletonLoader type="text" :class="[textSizes.mainNumber, layoutClasses.itemSpacing]" width="120" />
:class="[textSizes.mainNumber, layoutClasses.itemSpacing]" <VSkeletonLoader type="text" :class="textSizes.percentage" width="60" />
style="line-height: 1.1;" </template>
>
4,350 <template v-else-if="error">
</h3> <h3 class="font-weight-bold text-error" :class="[textSizes.mainNumber, layoutClasses.itemSpacing]"
<div class="d-flex align-center"> style="line-height: 1.1;">
<VIcon Error
icon="tabler-chevron-up" </h3>
:color="(props.donutColors.length ? processColors(props.donutColors) : [vuetifyTheme.current.value.colors.success])[0]" <div class="d-flex align-center">
:size="layoutClasses.iconSize" <VIcon icon="tabler-alert-circle" color="error" :size="layoutClasses.iconSize"
class="me-1 flex-shrink-0" class="me-1 flex-shrink-0" />
/> <span class="font-weight-medium text-no-wrap text-error" :class="textSizes.percentage">
<span Failed to load
class="font-weight-medium text-no-wrap" </span>
:class="textSizes.percentage" </div>
:style="{ color: (props.donutColors.length ? processColors(props.donutColors) : [vuetifyTheme.current.value.colors.success])[0] }" </template>
>
15.8% <template v-else>
</span> <h3 class="font-weight-bold" :class="[textSizes.mainNumber, layoutClasses.itemSpacing]"
</div> style="line-height: 1.1;">
{{ formattedTotal }}
</h3>
<div v-if="displayMode === 'multiple' && formattedBreakdown.length > 1" class="mb-2">
<div v-for="item in formattedBreakdown" :key="item.field"
class="d-flex justify-space-between align-center mb-1">
<span class="text-medium-emphasis" :class="textSizes.breakdown">
{{ item.label }}:
</span>
<span class="font-weight-medium" :class="textSizes.breakdown">
{{ item.formatted }}
</span>
</div>
<VDivider class="my-2 opacity-60" />
</div>
<div v-if="percentageChange" class="d-flex align-center">
<VIcon :icon="percentageChange.isPositive ? 'tabler-chevron-up' : 'tabler-chevron-down'" :color="percentageChange.isPositive ?
(props.donutColors.length ? processColors(props.donutColors)[0] : vuetifyTheme.current.value.colors.primary) :
vuetifyTheme.current.value.colors.error" :size="layoutClasses.iconSize" class="me-1 flex-shrink-0" />
<span class="font-weight-medium text-no-wrap" :class="textSizes.percentage" :style="{
color: percentageChange.isPositive ?
(props.donutColors.length ? processColors(props.donutColors)[0] : vuetifyTheme.current.value.colors.primary) :
vuetifyTheme.current.value.colors.error
}">
{{ percentageChange.value }}%
</span>
</div>
<div v-else-if="showPercentage" class="d-flex align-center">
<VIcon icon="tabler-list"
:color="props.donutColors.length ? processColors(props.donutColors)[0] : vuetifyTheme.current.value.colors.primary"
:size="layoutClasses.iconSize" class="me-1 flex-shrink-0" />
<span class="font-weight-medium text-no-wrap text-medium-emphasis" :class="textSizes.percentage">
{{ apiData.length }} leads
</span>
</div>
</template>
</div> </div>
</div> </div>
<div <div class="d-flex align-center justify-center flex-shrink-0">
class="d-flex align-center justify-center flex-shrink-0" <VueApexCharts v-if="!isLoading && !error" :options="chartOptions" :series="chartSeries"
> :height="chartSize.height" :width="chartSize.width" />
<VueApexCharts <VSkeletonLoader v-else-if="isLoading" type="avatar" :width="chartSize.width" :height="chartSize.height" />
:options="chartOptions" <div v-else class="d-flex align-center justify-center"
:series="chartSeries" :style="{ width: chartSize.width + 'px', height: chartSize.height + 'px' }">
:height="chartSize.height" <VIcon icon="tabler-alert-circle" color="error" :size="chartSize.width * 0.3" />
:width="chartSize.width" </div>
/>
</div> </div>
</VCardText> </VCardText>
</VCard> </VCard>

View File

@@ -1,6 +1,5 @@
.grid-stack-container { .grid-stack-container {
position: relative; position: relative;
transition: all 0.3s ease;
} }
.grid-stack-container.edit-mode-active { .grid-stack-container.edit-mode-active {
@@ -11,7 +10,6 @@
.grid-stack-item { .grid-stack-item {
position: relative; position: relative;
transition: all 0.3s ease;
} }
.grid-stack-item.edit-mode { .grid-stack-item.edit-mode {
@@ -260,6 +258,32 @@
opacity: 1; opacity: 1;
} }
.custom-toggle {
.v-selection-control__input {
.v-switch__track {
background-color: rgba(255, 255, 255, 0.3) !important;
opacity: 1 !important;
}
}
&.v-switch--on .v-selection-control__input {
.v-switch__track {
background-color: rgba(255, 255, 255, 0.8) !important;
}
.v-switch__thumb {
background-color: rgba(var(--v-theme-primary), 1) !important;
}
}
.v-label {
color: rgba(255, 255, 255, 0.95) !important;
font-weight: 500;
font-size: 0.9rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.banner-content { .banner-content {
flex-direction: column; flex-direction: column;
@@ -291,4 +315,20 @@
padding: 3px 6px !important; padding: 3px 6px !important;
height: 24px !important; height: 24px !important;
} }
.auto-compact-switch {
.v-switch__track {
width: 44px !important;
height: 22px !important;
}
.v-switch__thumb {
width: 18px !important;
height: 18px !important;
}
.v-label {
font-size: 0.85rem;
}
}
} }