Files
panel/resources/js/@core/components/WidgetLibrary.vue

601 lines
16 KiB
Vue
Raw Permalink Normal View History

2025-08-04 16:33:07 +03:30
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
isCrmRoute: Boolean,
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue'])
const selectedWidgets = ref([])
const isLoading = ref(false)
const searchQuery = ref('')
const availableWidgets = [
{
id: 'leads1',
name: 'Generated Leads 1',
icon: 'tabler-chart-pie',
category: 'Analytics',
description: 'Lead generation progress'
},
{
id: 'leads2',
name: 'Generated Leads 2',
icon: 'tabler-chart-donut',
category: 'Analytics',
description: 'Primary leads overview'
},
{
id: 'leads3',
name: 'Generated Leads 3',
icon: 'tabler-chart-donut-2',
category: 'Analytics',
description: 'Warning leads tracking'
},
{
id: 'project-activity',
name: 'Project Activity',
icon: 'tabler-chart-bar',
category: 'Projects',
description: 'Activity bar chart'
},
{
id: 'analysis1',
name: 'Analysis Card 1',
icon: 'tabler-chart-line',
category: 'Analytics',
description: 'Active projects progress'
},
{
id: 'analysis2',
name: 'Analysis Card 2',
icon: 'tabler-chart-area',
category: 'Analytics',
description: 'Cost overview analysis'
},
{
id: 'cost-overview',
name: 'Cost Overview',
icon: 'tabler-cash',
category: 'Finance',
description: 'Financial cost tracking'
},
{
id: 'earning-reports',
name: 'Earning Reports',
icon: 'tabler-report-money',
category: 'Finance',
description: 'Yearly overview reports'
},
{
id: 'analytics-sales',
name: 'Analytics Sales',
icon: 'tabler-trending-up',
category: 'Sales',
description: 'Sales analytics data'
},
{
id: 'sales-countries',
name: 'Sales by Countries',
icon: 'tabler-world',
category: 'Sales',
description: 'Geographic sales data'
},
{
id: 'project-status',
name: 'Project Status',
icon: 'tabler-calendar-stats',
category: 'Projects',
description: 'Project status overview'
},
{
id: 'active-project',
name: 'Active Project',
icon: 'tabler-folder-open',
category: 'Projects',
description: 'Current active projects'
},
{
id: 'recent-transactions',
name: 'Recent Transactions',
icon: 'tabler-receipt',
category: 'Finance',
description: 'Latest transaction history'
},
{
id: 'activity-timeline',
name: 'Activity Timeline',
icon: 'tabler-timeline',
category: 'Activity',
description: 'Timeline of activities'
},
{
id: 'ecommerce-congratulations',
name: 'Congratulations John',
icon: 'tabler-trophy',
category: 'Ecommerce',
description: 'Congratulations card for achievements'
},
{
id: 'ecommerce-earning-reports',
name: 'Ecommerce Earning Reports',
icon: 'tabler-report-analytics',
category: 'Ecommerce',
description: 'Detailed earning reports and analytics'
},
{
id: 'ecommerce-expenses',
name: 'Expenses Radial Chart',
icon: 'tabler-chart-donut-3',
category: 'Ecommerce',
description: 'Expense breakdown in radial chart'
},
{
id: 'ecommerce-generated-leads',
name: 'Ecommerce Generated Leads',
icon: 'tabler-users-plus',
category: 'Ecommerce',
description: 'Lead generation statistics'
},
{
id: 'ecommerce-invoice-table',
name: 'Invoice Table',
icon: 'tabler-file-invoice',
category: 'Ecommerce',
description: 'Comprehensive invoice management table'
},
{
id: 'ecommerce-order',
name: 'Order Management',
icon: 'tabler-shopping-cart',
category: 'Ecommerce',
description: 'Order tracking and management'
},
{
id: 'ecommerce-popular-products',
name: 'Popular Products',
icon: 'tabler-star',
category: 'Ecommerce',
description: 'Most popular products showcase'
},
{
id: 'ecommerce-revenue-report',
name: 'Revenue Report',
icon: 'tabler-chart-line',
category: 'Ecommerce',
description: 'Revenue analysis and trends'
},
{
id: 'ecommerce-statistics',
name: 'Ecommerce Statistics',
icon: 'tabler-chart-infographic',
category: 'Ecommerce',
description: 'Comprehensive ecommerce statistics'
},
{
id: 'ecommerce-total-profit',
name: 'Total Profit Line Chart',
icon: 'tabler-trending-up-2',
category: 'Ecommerce',
description: 'Total profit visualization'
},
{
id: 'ecommerce-transactions',
name: 'Ecommerce Transactions',
icon: 'tabler-credit-card',
category: 'Ecommerce',
description: 'Transaction history and details'
}
]
const filteredWidgets = computed(() => {
const base = availableWidgets.filter(widget =>
!(defaultWidgetIds.includes(widget.id) && selectedWidgets.value.includes(widget.id))
)
if (!searchQuery.value.trim()) {
return base
}
return base.filter(widget =>
widget.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
widget.description.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
widget.category.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const handleDragStart = (e, widget) => {
console.log('Drag started for widget:', widget.id);
e.dataTransfer.effectAllowed = "copy";
e.dataTransfer.setData("text/plain", widget.id);
e.dataTransfer.setData("application/json", JSON.stringify(widget));
e.target.classList.add('dragging');
};
const handleDragEnd = (e) => {
e.target.classList.remove('dragging');
};
onMounted(() => {
syncWidgetState();
window.addEventListener('dashboard-widgets-state', (event) => {
if (event.detail && Array.isArray(event.detail.widgets)) {
selectedWidgets.value = event.detail.widgets;
}
});
window.addEventListener('widget-added-to-dashboard', (event) => {
if (event.detail && event.detail.widgetId) {
const widgetId = event.detail.widgetId;
if (!selectedWidgets.value.includes(widgetId)) {
selectedWidgets.value.push(widgetId);
}
}
});
});
const defaultWidgetIds = [
"leads1",
"leads2",
"leads3",
"project-activity",
"analysis1",
"analysis2",
"cost-overview",
"earning-reports",
"analytics-sales",
"sales-countries",
"project-status",
"active-project",
"recent-transactions",
"activity-timeline"
]
const widgetCategories = computed(() => {
const categories = {}
filteredWidgets.value.forEach(widget => {
if (!categories[widget.category]) {
categories[widget.category] = []
}
categories[widget.category].push(widget)
})
Object.keys(categories).forEach(category => {
const list = categories[category]
const normal = list.filter(w =>
!selectedWidgets.value.includes(w.id) &&
!defaultWidgetIds.includes(w.id)
)
const userAdded = list.filter(w =>
selectedWidgets.value.includes(w.id) &&
!defaultWidgetIds.includes(w.id)
)
const defaultRemoved = list.filter(w =>
defaultWidgetIds.includes(w.id) &&
!selectedWidgets.value.includes(w.id)
)
categories[category] = [
...normal,
...userAdded,
...defaultRemoved
]
})
return categories
})
const addWidgetToCard = async (widgetId) => {
if (isLoading.value || selectedWidgets.value.includes(widgetId)) return
try {
isLoading.value = true
const widget = availableWidgets.find(w => w.id === widgetId)
if (!widget) {
console.error('Widget not found:', widgetId)
return
}
const event = new CustomEvent('add-widget-to-dashboard', {
detail: { widgetId, widget }
})
window.dispatchEvent(event)
} catch (error) {
console.error('Error adding widget:', error)
} finally {
isLoading.value = false
}
}
const removeWidgetFromCard = async (widgetId) => {
if (isLoading.value) return
try {
isLoading.value = true
const widget = availableWidgets.find(w => w.id === widgetId)
if (!widget) {
console.error('Widget not found:', widgetId)
return
}
const event = new CustomEvent('remove-widget-from-dashboard', {
detail: { widgetId, widget }
})
window.dispatchEvent(event)
const index = selectedWidgets.value.indexOf(widgetId)
if (index > -1) {
selectedWidgets.value.splice(index, 1)
}
} catch (error) {
console.error('Error removing widget:', error)
} finally {
isLoading.value = false
}
}
const clearSearch = () => {
searchQuery.value = ''
}
const syncWidgetState = () => {
const event = new CustomEvent('get-dashboard-widgets')
window.dispatchEvent(event)
}
onMounted(() => {
syncWidgetState()
window.addEventListener('dashboard-widgets-state', (event) => {
if (event.detail && Array.isArray(event.detail.widgets)) {
selectedWidgets.value = event.detail.widgets
}
})
})
onUnmounted(() => {
window.removeEventListener('dashboard-widgets-state', () => { })
})
</script>
<template>
<VNavigationDrawer :model-value="modelValue" v-if="isCrmRoute" data-allow-mismatch temporary touchless border="none"
location="end" width="450" elevation="10" :scrim="false" class="widget-sidebar"
@update:model-value="val => emit('update:modelValue', val)">
<div class="widget-sidebar-header">
<div class="header-content">
<div class="header-icon">
<VIcon icon="tabler-layout-grid" size="24" />
</div>
<div class="header-text">
<h6 class="text-h6 mb-1">Widget Library</h6>
<p class="text-body-2 mb-0 text-medium-emphasis">
Add widgets to your dashboard ({{ selectedWidgets.length }} selected)
</p>
</div>
</div>
<VBtn icon variant="text" color="medium-emphasis" size="small" @click="emit('update:modelValue', false)">
<VIcon icon="tabler-x" color="high-emphasis" size="24" />
</VBtn>
</div>
<VDivider />
<div class="pa-4 pb-2">
<VTextField v-model="searchQuery" placeholder="Search widgets..." prepend-inner-icon="tabler-search"
variant="outlined" density="compact" clearable @click:clear="clearSearch" />
</div>
<PerfectScrollbar :options="{ wheelPropagation: false, suppressScrollX: true }" class="widget-sidebar-content">
<div class="pa-4 pt-2" style="min-height: 100%;">
<div v-if="Object.keys(widgetCategories).length === 0" class="empty-state">
<VIcon icon="tabler-search-off" size="48" color="medium-emphasis" class="mb-3" />
<h6 class="text-h6 text-medium-emphasis mb-2">No widgets found</h6>
<p class="text-body-2 text-medium-emphasis">
Try adjusting your search terms
</p>
</div>
<template v-else>
<template v-for="(widgets, category) in widgetCategories" :key="category">
<div class="widget-category mb-6">
<div class="category-header mb-3">
<h6 class="text-subtitle-1 text-high-emphasis font-weight-semibold">
{{ category }} ({{ widgets.length }})
</h6>
<VDivider class="mt-2" />
</div>
<div class="widgets-grid">
<div v-for="widget in widgets" :key="widget.id" class="widget-card" :class="{
'widget-selected': selectedWidgets.includes(widget.id),
'widget-loading': isLoading
}" draggable="true" @dragstart="handleDragStart($event, widget)" @dragend="handleDragEnd">
<div class="widget-preview">
<div class="widget-icon" :class="{ 'selected': selectedWidgets.includes(widget.id) }">
<VIcon :icon="widget.icon" size="20" />
</div>
<div class="widget-info">
<h6 class="widget-name">{{ widget.name }}</h6>
<p class="widget-description">{{ widget.description }}</p>
</div>
</div>
<div class="widget-actions">
<VBtn v-if="!selectedWidgets.includes(widget.id)" size="small" color="primary" variant="flat"
:loading="isLoading" :disabled="isLoading" @click="addWidgetToCard(widget.id)">
<VIcon icon="tabler-plus" size="16" class="me-1" />
Add
</VBtn>
<VBtn v-else size="small" color="error" variant="flat" :loading="isLoading" :disabled="isLoading"
@click="removeWidgetFromCard(widget.id)">
<VIcon icon="tabler-minus" size="16" class="me-1" />
Remove
</VBtn>
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</PerfectScrollbar>
</VNavigationDrawer>
</template>
<style lang="scss">
@use "@layouts/styles/mixins" as layoutMixins;
.widget-sidebar {
&.v-navigation-drawer--temporary:not(.v-navigation-drawer--active) {
transform: translateX(110%) !important;
@include layoutMixins.rtl {
transform: translateX(-110%) !important;
}
}
.widget-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
.header-content {
display: flex;
align-items: center;
gap: 0.75rem;
.header-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 0.5rem;
background-color: rgba(var(--v-theme-primary), 0.12);
color: rgb(var(--v-theme-primary));
}
.header-text {
h6 {
color: rgb(var(--v-theme-on-surface));
font-weight: 600;
}
}
}
}
.widget-sidebar-content {
flex: 1;
max-height: calc(100vh - 180px);
overflow-y: auto;
}
.widget-category {
.category-header {
h6 {
color: rgb(var(--v-theme-on-surface));
margin-bottom: 0.5rem;
}
}
.widgets-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
}
.widget-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 0.5rem;
background-color: rgb(var(--v-theme-surface));
transition: all 0.2s ease;
cursor: pointer;
&[draggable="true"] {
cursor: grab;
}
&:active {
cursor: grabbing;
}
&.dragging {
opacity: 0.5;
transform: scale(0.95);
}
&:hover {
border-color: rgba(var(--v-theme-primary), 0.3);
box-shadow: 0 2px 8px rgba(var(--v-theme-primary), 0.1);
}
&.widget-selected {
border-color: rgb(var(--v-theme-success));
background-color: rgba(var(--v-theme-success), 0.08);
}
.widget-preview {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
.widget-icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 0.375rem;
background-color: rgba(var(--v-theme-primary), 0.12);
color: rgb(var(--v-theme-primary));
transition: all 0.2s ease;
&.selected {
background-color: rgba(var(--v-theme-success), 0.12);
color: rgb(var(--v-theme-success));
}
}
.widget-info {
flex: 1;
.widget-name {
font-size: 0.875rem;
font-weight: 600;
color: rgb(var(--v-theme-on-surface));
margin-bottom: 0.25rem;
line-height: 1.2;
}
.widget-description {
font-size: 0.75rem;
color: rgba(var(--v-theme-on-surface), 0.7);
margin: 0;
line-height: 1.2;
}
}
}
.widget-actions {
margin-left: 0.75rem;
}
&.widget-loading {
pointer-events: none;
opacity: 0.7;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
text-align: center;
}
.v-text-field {
.v-field__outline {
border-radius: 0.5rem;
}
}
}
</style>