Initial commit
This commit is contained in:
601
resources/js/@core/components/WidgetLibrary.vue
Normal file
601
resources/js/@core/components/WidgetLibrary.vue
Normal file
@@ -0,0 +1,601 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user