Merge pull request #1040 from frappe/mergify/bp/main-hotfix/pr-1033
This commit is contained in:
commit
09b4e25500
File diff suppressed because it is too large
Load Diff
0
crm/fcrm/doctype/crm_dashboard/__init__.py
Normal file
0
crm/fcrm/doctype/crm_dashboard/__init__.py
Normal file
8
crm/fcrm/doctype/crm_dashboard/crm_dashboard.js
Normal file
8
crm/fcrm/doctype/crm_dashboard/crm_dashboard.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("CRM Dashboard", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
105
crm/fcrm/doctype/crm_dashboard/crm_dashboard.json
Normal file
105
crm/fcrm/doctype/crm_dashboard/crm_dashboard.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2025-07-14 12:19:49.725022",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"private",
|
||||
"column_break_exbw",
|
||||
"user",
|
||||
"section_break_hfza",
|
||||
"layout"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_exbw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_hfza",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "[]",
|
||||
"fieldname": "layout",
|
||||
"fieldtype": "Code",
|
||||
"label": "Layout",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Name",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "private",
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"mandatory_depends_on": "private",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "private",
|
||||
"fieldtype": "Check",
|
||||
"label": "Private"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-14 12:36:10.831351",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Dashboard",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
33
crm/fcrm/doctype/crm_dashboard/crm_dashboard.py
Normal file
33
crm/fcrm/doctype/crm_dashboard/crm_dashboard.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CRMDashboard(Document):
|
||||
pass
|
||||
|
||||
|
||||
def default_manager_dashboard_layout():
|
||||
"""
|
||||
Returns the default layout for the CRM Manager Dashboard.
|
||||
"""
|
||||
return '[{"name":"total_leads","type":"number_chart","tooltip":"Total number of leads","layout":{"x":0,"y":0,"w":4,"h":3,"i":"total_leads"}},{"name":"ongoing_deals","type":"number_chart","tooltip":"Total number of ongoing deals","layout":{"x":8,"y":0,"w":4,"h":3,"i":"ongoing_deals"}},{"name":"won_deals","type":"number_chart","tooltip":"Total number of won deals","layout":{"x":12,"y":0,"w":4,"h":3,"i":"won_deals"}},{"name":"average_won_deal_value","type":"number_chart","tooltip":"Average value of won deals","layout":{"x":16,"y":0,"w":4,"h":3,"i":"average_won_deal_value"}},{"name":"average_deal_value","type":"number_chart","tooltip":"Average deal value of ongoing and won deals","layout":{"x":0,"y":2,"w":4,"h":3,"i":"average_deal_value"}},{"name":"average_time_to_close_a_lead","type":"number_chart","tooltip":"Average time taken to close a lead","layout":{"x":4,"y":0,"w":4,"h":3,"i":"average_time_to_close_a_lead"}},{"name":"average_time_to_close_a_deal","type":"number_chart","layout":{"x":4,"y":2,"w":4,"h":3,"i":"average_time_to_close_a_deal"}},{"name":"spacer","type":"spacer","layout":{"x":8,"y":2,"w":12,"h":3,"i":"spacer"}},{"name":"sales_trend","type":"axis_chart","layout":{"x":0,"y":4,"w":10,"h":9,"i":"sales_trend"}},{"name":"forecasted_revenue","type":"axis_chart","layout":{"x":10,"y":4,"w":10,"h":9,"i":"forecasted_revenue"}},{"name":"funnel_conversion","type":"axis_chart","layout":{"x":0,"y":11,"w":10,"h":9,"i":"funnel_conversion"}},{"name":"deals_by_stage_donut","type":"donut_chart","layout":{"x":10,"y":11,"w":10,"h":9,"i":"deals_by_stage_donut"}},{"name":"lost_deal_reasons","type":"axis_chart","layout":{"x":0,"y":32,"w":20,"h":9,"i":"lost_deal_reasons"}},{"name":"leads_by_source","type":"donut_chart","layout":{"x":0,"y":18,"w":10,"h":9,"i":"leads_by_source"}},{"name":"deals_by_source","type":"donut_chart","layout":{"x":10,"y":18,"w":10,"h":9,"i":"deals_by_source"}},{"name":"deals_by_territory","type":"axis_chart","layout":{"x":0,"y":25,"w":10,"h":9,"i":"deals_by_territory"}},{"name":"deals_by_salesperson","type":"axis_chart","layout":{"x":10,"y":25,"w":10,"h":9,"i":"deals_by_salesperson"}}]'
|
||||
|
||||
|
||||
def create_default_manager_dashboard(force=False):
|
||||
"""
|
||||
Creates the default CRM Manager Dashboard if it does not exist.
|
||||
"""
|
||||
if not frappe.db.exists("CRM Dashboard", "Manager Dashboard"):
|
||||
doc = frappe.new_doc("CRM Dashboard")
|
||||
doc.title = "Manager Dashboard"
|
||||
doc.layout = default_manager_dashboard_layout()
|
||||
doc.insert(ignore_permissions=True)
|
||||
elif force:
|
||||
doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
|
||||
doc.layout = default_manager_dashboard_layout()
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.layout
|
||||
30
crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py
Normal file
30
crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestCRMDashboard(UnitTestCase):
|
||||
"""
|
||||
Unit tests for CRMDashboard.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestCRMDashboard(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for CRMDashboard.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@ -4,6 +4,7 @@ import click
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
|
||||
from crm.fcrm.doctype.crm_products.crm_products import create_product_details_script
|
||||
|
||||
|
||||
@ -23,6 +24,7 @@ def after_install(force=False):
|
||||
add_default_lost_reasons()
|
||||
add_standard_dropdown_items()
|
||||
add_default_scripts()
|
||||
create_default_manager_dashboard(force)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ def execute():
|
||||
]
|
||||
|
||||
for status in deal_statuses:
|
||||
if status.type is None or status.type == "":
|
||||
if not status.type or status.type is None or status.type == "Open":
|
||||
if status.deal_status in openStatuses:
|
||||
type = "Open"
|
||||
elif status.deal_status in ongoingStatuses:
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit 424288f77af4779dd3bb71dc3d278fc627f95179
|
||||
Subproject commit b295b54aaa3a2a22f455df819d87433dc6b9ff6a
|
||||
14
frontend/components.d.ts
vendored
14
frontend/components.d.ts
vendored
@ -12,6 +12,7 @@ declare module 'vue' {
|
||||
Activities: typeof import('./src/components/Activities/Activities.vue')['default']
|
||||
ActivityHeader: typeof import('./src/components/Activities/ActivityHeader.vue')['default']
|
||||
ActivityIcon: typeof import('./src/components/Icons/ActivityIcon.vue')['default']
|
||||
AddChartModal: typeof import('./src/components/Dashboard/AddChartModal.vue')['default']
|
||||
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
|
||||
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
|
||||
AddressModal: typeof import('./src/components/Modals/AddressModal.vue')['default']
|
||||
@ -62,7 +63,9 @@ declare module 'vue' {
|
||||
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
||||
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
|
||||
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
||||
DashboardItem: typeof import('./src/components/Dashboard/DashboardItem.vue')['default']
|
||||
DataFields: typeof import('./src/components/Activities/DataFields.vue')['default']
|
||||
DataFieldsModal: typeof import('./src/components/Modals/DataFieldsModal.vue')['default']
|
||||
DealModal: typeof import('./src/components/Modals/DealModal.vue')['default']
|
||||
@ -99,11 +102,9 @@ declare module 'vue' {
|
||||
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
|
||||
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
|
||||
EmailTemplateIcon: typeof import('./src/components/Icons/EmailTemplateIcon.vue')['default']
|
||||
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
|
||||
EmailTemplatePage: typeof import('./src/components/Settings/EmailTemplate/EmailTemplatePage.vue')['default']
|
||||
EmailTemplates: typeof import('./src/components/Settings/EmailTemplate/EmailTemplates.vue')['default']
|
||||
EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.vue')['default']
|
||||
EmailTemplatesListView: typeof import('./src/components/ListViews/EmailTemplatesListView.vue')['default']
|
||||
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
|
||||
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
|
||||
ErrorPage: typeof import('./src/components/ErrorPage.vue')['default']
|
||||
@ -167,11 +168,9 @@ declare module 'vue' {
|
||||
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
||||
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
|
||||
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
||||
LucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
||||
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
||||
LucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
LucideX: typeof import('~icons/lucide/x')['default']
|
||||
LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
LucidePenLine: typeof import('~icons/lucide/pen-line')['default']
|
||||
LucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
|
||||
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
|
||||
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
||||
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
|
||||
@ -205,7 +204,6 @@ declare module 'vue' {
|
||||
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
|
||||
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
|
||||
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
|
||||
ProfileImageEditor: typeof import('./src/components/Settings/ProfileImageEditor.vue')['default']
|
||||
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
|
||||
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
||||
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"@tiptap/extension-paragraph": "^2.12.0",
|
||||
"@twilio/voice-sdk": "^2.10.2",
|
||||
"@vueuse/integrations": "^10.3.0",
|
||||
"frappe-ui": "^0.1.166",
|
||||
"frappe-ui": "^0.1.171",
|
||||
"gemoji": "^8.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.1",
|
||||
|
||||
165
frontend/src/components/Dashboard/AddChartModal.vue
Normal file
165
frontend/src/components/Dashboard/AddChartModal.vue
Normal file
@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{ title: __('Add chart') }"
|
||||
@close="show = false"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<FormControl
|
||||
v-model="chartType"
|
||||
type="select"
|
||||
:label="__('Chart Type')"
|
||||
:options="chartTypes"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="chartType === 'number_chart'"
|
||||
v-model="numberChart"
|
||||
type="select"
|
||||
:label="__('Number chart')"
|
||||
:options="numberCharts"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="chartType === 'axis_chart'"
|
||||
v-model="axisChart"
|
||||
type="select"
|
||||
:label="__('Axis chart')"
|
||||
:options="axisCharts"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="chartType === 'donut_chart'"
|
||||
v-model="donutChart"
|
||||
type="select"
|
||||
:label="__('Donut chart')"
|
||||
:options="donutCharts"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button variant="outline" :label="__('Cancel')" @click="show = false" />
|
||||
<Button variant="solid" :label="__('Add')" @click="addChart" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getRandom } from '@/utils'
|
||||
import { createResource, Dialog, FormControl } from 'frappe-ui'
|
||||
import { ref, reactive, inject } from 'vue'
|
||||
|
||||
const show = defineModel({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
|
||||
const items = defineModel('items', {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
})
|
||||
|
||||
const fromDate = inject('fromDate', ref(''))
|
||||
const toDate = inject('toDate', ref(''))
|
||||
const filters = inject('filters', reactive({ period: '', user: '' }))
|
||||
|
||||
const chartType = ref('spacer')
|
||||
const chartTypes = [
|
||||
{ label: __('Spacer'), value: 'spacer' },
|
||||
{ label: __('Number chart'), value: 'number_chart' },
|
||||
{ label: __('Axis chart'), value: 'axis_chart' },
|
||||
{ label: __('Donut chart'), value: 'donut_chart' },
|
||||
]
|
||||
|
||||
const numberChart = ref('')
|
||||
const numberCharts = [
|
||||
{ label: __('Total leads'), value: 'total_leads' },
|
||||
{ label: __('Ongoing deals'), value: 'ongoing_deals' },
|
||||
{ label: __('Avg ongoing deal value'), value: 'average_ongoing_deal_value' },
|
||||
{ label: __('Won deals'), value: 'won_deals' },
|
||||
{ label: __('Avg won deal value'), value: 'average_won_deal_value' },
|
||||
{ label: __('Avg deal value'), value: 'average_deal_value' },
|
||||
{
|
||||
label: __('Avg time to close a lead'),
|
||||
value: 'average_time_to_close_a_lead',
|
||||
},
|
||||
{
|
||||
label: __('Avg time to close a deal'),
|
||||
value: 'average_time_to_close_a_deal',
|
||||
},
|
||||
]
|
||||
|
||||
const axisChart = ref('sales_trend')
|
||||
const axisCharts = [
|
||||
{ label: __('Sales trend'), value: 'sales_trend' },
|
||||
{ label: __('Forecasted revenue'), value: 'forecasted_revenue' },
|
||||
{ label: __('Funnel conversion'), value: 'funnel_conversion' },
|
||||
{ label: __('Deals by ongoing & won stage'), value: 'deals_by_stage_axis' },
|
||||
{ label: __('Lost deal reasons'), value: 'lost_deal_reasons' },
|
||||
{ label: __('Deals by territory'), value: 'deals_by_territory' },
|
||||
{ label: __('Deals by salesperson'), value: 'deals_by_salesperson' },
|
||||
]
|
||||
|
||||
const donutChart = ref('deals_by_stage_donut')
|
||||
const donutCharts = [
|
||||
{ label: __('Deals by stage'), value: 'deals_by_stage_donut' },
|
||||
{ label: __('Leads by source'), value: 'leads_by_source' },
|
||||
{ label: __('Deals by source'), value: 'deals_by_source' },
|
||||
]
|
||||
|
||||
async function addChart() {
|
||||
show.value = false
|
||||
if (chartType.value == 'spacer') {
|
||||
items.value.push({
|
||||
name: 'spacer',
|
||||
type: 'spacer',
|
||||
layout: { x: 0, y: 0, w: 4, h: 2, i: 'spacer_' + getRandom(4) },
|
||||
})
|
||||
} else {
|
||||
await getChart(chartType.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function getChart(type: string) {
|
||||
let name =
|
||||
type == 'number_chart'
|
||||
? numberChart.value
|
||||
: type == 'axis_chart'
|
||||
? axisChart.value
|
||||
: donutChart.value
|
||||
|
||||
await createResource({
|
||||
url: 'crm.api.dashboard.get_chart',
|
||||
params: {
|
||||
name,
|
||||
type,
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
},
|
||||
auto: true,
|
||||
onSuccess: (data = {}) => {
|
||||
let width = 4
|
||||
let height = 2
|
||||
|
||||
if (['axis_chart', 'donut_chart'].includes(type)) {
|
||||
width = 10
|
||||
height = 7
|
||||
}
|
||||
|
||||
items.value.push({
|
||||
name,
|
||||
type,
|
||||
layout: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: width,
|
||||
h: height,
|
||||
i: name + '_' + getRandom(4),
|
||||
},
|
||||
data: data,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
62
frontend/src/components/Dashboard/DashboardGrid.vue
Normal file
62
frontend/src/components/Dashboard/DashboardGrid.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-y-auto p-3">
|
||||
<GridLayout
|
||||
v-if="items.length > 0"
|
||||
class="h-fit w-full"
|
||||
:class="[editing ? 'mb-[20rem] !select-none' : '']"
|
||||
:cols="20"
|
||||
:rowHeight="42"
|
||||
:disabled="!editing"
|
||||
:modelValue="items.map((item) => item.layout)"
|
||||
@update:modelValue="
|
||||
(newLayout) => {
|
||||
items.forEach((item, idx) => {
|
||||
item.layout = newLayout[idx]
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #item="{ index }">
|
||||
<div class="group relative flex h-full w-full p-2 text-ink-gray-8">
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center"
|
||||
:class="
|
||||
editing
|
||||
? 'pointer-events-none [&>div:first-child]:rounded [&>div:first-child]:group-hover:ring-2 [&>div:first-child]:group-hover:ring-outline-gray-2'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<DashboardItem
|
||||
:index="index"
|
||||
:item="items[index]"
|
||||
:editing="editing"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="editing"
|
||||
class="flex absolute right-0 top-0 bg-surface-gray-6 rounded cursor-pointer opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<div
|
||||
class="rounded p-1 hover:bg-surface-gray-5"
|
||||
@click="items.splice(index, 1)"
|
||||
>
|
||||
<FeatherIcon name="trash-2" class="size-3 text-ink-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</GridLayout>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { GridLayout } from 'frappe-ui'
|
||||
|
||||
const props = defineProps({
|
||||
editing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const items = defineModel()
|
||||
</script>
|
||||
49
frontend/src/components/Dashboard/DashboardItem.vue
Normal file
49
frontend/src/components/Dashboard/DashboardItem.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="h-full w-full">
|
||||
<div
|
||||
v-if="item.type == 'number_chart'"
|
||||
class="flex h-full w-full rounded shadow overflow-hidden cursor-pointer"
|
||||
>
|
||||
<Tooltip :text="__(item.data.tooltip)">
|
||||
<NumberChart v-if="item.data" :key="index" :config="item.data" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.type == 'spacer'"
|
||||
class="rounded bg-surface-white h-full overflow-hidden text-ink-gray-5 flex items-center justify-center"
|
||||
:class="editing ? 'border border-dashed border-outline-gray-2' : ''"
|
||||
>
|
||||
{{ editing ? __('Spacer') : '' }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.type == 'axis_chart'"
|
||||
class="h-full w-full rounded-md bg-surface-white shadow"
|
||||
>
|
||||
<AxisChart v-if="item.data" :config="item.data" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.type == 'donut_chart'"
|
||||
class="h-full w-full rounded-md bg-surface-white shadow overflow-hidden"
|
||||
>
|
||||
<DonutChart v-if="item.data" :config="item.data" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { AxisChart, DonutChart, NumberChart, Tooltip } from 'frappe-ui'
|
||||
|
||||
const props = defineProps({
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
editing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -4,9 +4,44 @@
|
||||
<template #left-header>
|
||||
<ViewBreadcrumbs routeName="Dashboard" />
|
||||
</template>
|
||||
<template #right-header>
|
||||
<Button
|
||||
v-if="!editing"
|
||||
:label="__('Refresh')"
|
||||
@click="dashboardItems.reload"
|
||||
>
|
||||
<template #prefix>
|
||||
<LucideRefreshCcw class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!editing && (isManager() || isAdmin())"
|
||||
:label="__('Edit')"
|
||||
@click="enableEditing"
|
||||
>
|
||||
<template #prefix>
|
||||
<LucidePenLine class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="editing"
|
||||
:label="__('Chart')"
|
||||
icon-left="plus"
|
||||
@click="showAddChartModal = true"
|
||||
/>
|
||||
<Button v-if="editing" :label="__('Cancel')" @click="cancel" />
|
||||
<Button
|
||||
v-if="editing"
|
||||
variant="solid"
|
||||
:label="__('Save')"
|
||||
:disabled="!dirty"
|
||||
:loading="saveDashboard.loading"
|
||||
@click="save"
|
||||
/>
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
|
||||
<div class="p-5 pb-3 flex items-center gap-4">
|
||||
<div class="p-5 pb-2 flex items-center gap-4">
|
||||
<Dropdown
|
||||
v-if="!showDatePicker"
|
||||
:options="options"
|
||||
@ -83,81 +118,51 @@
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="p-5 pt-2 w-full overflow-y-scroll">
|
||||
<div class="transition-all animate-fade-in duration-300">
|
||||
<div
|
||||
v-if="!numberCards.loading"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4"
|
||||
>
|
||||
<Tooltip
|
||||
v-for="(config, index) in numberCards.data"
|
||||
:text="config.tooltip"
|
||||
>
|
||||
<NumberChart
|
||||
:key="index"
|
||||
class="border rounded-md"
|
||||
:config="config"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
|
||||
<div v-if="salesTrend.data" class="border rounded-md min-h-80">
|
||||
<AxisChart :config="salesTrend.data" />
|
||||
</div>
|
||||
<div v-if="forecastedRevenue.data" class="border rounded-md min-h-80">
|
||||
<AxisChart :config="forecastedRevenue.data" />
|
||||
</div>
|
||||
<div v-if="funnelConversion.data" class="border rounded-md min-h-80">
|
||||
<AxisChart :config="funnelConversion.data" />
|
||||
</div>
|
||||
<div v-if="dealsByStage.data" class="border rounded-md">
|
||||
<AxisChart :config="dealsByStage.data.bar" />
|
||||
</div>
|
||||
<div v-if="dealsByStage.data" class="border rounded-md">
|
||||
<DonutChart :config="dealsByStage.data.donut" />
|
||||
</div>
|
||||
<div v-if="leadsBySource.data" class="border rounded-md">
|
||||
<DonutChart :config="leadsBySource.data" />
|
||||
</div>
|
||||
<div v-if="dealsByTerritory.data" class="border rounded-md">
|
||||
<AxisChart :config="dealsByTerritory.data" />
|
||||
</div>
|
||||
<div v-if="dealsBySalesperson.data" class="border rounded-md">
|
||||
<AxisChart :config="dealsBySalesperson.data" />
|
||||
</div>
|
||||
<div v-if="lostDealReasons.data" class="border rounded-md">
|
||||
<AxisChart :config="lostDealReasons.data" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-y-scroll">
|
||||
<DashboardGrid
|
||||
class="pt-1"
|
||||
v-if="!dashboardItems.loading && dashboardItems.data"
|
||||
v-model="dashboardItems.data"
|
||||
:editing="editing"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AddChartModal
|
||||
v-if="showAddChartModal"
|
||||
v-model="showAddChartModal"
|
||||
v-model:items="dashboardItems.data"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AddChartModal from '@/components/Dashboard/AddChartModal.vue'
|
||||
import LucideRefreshCcw from '~icons/lucide/refresh-ccw'
|
||||
import LucidePenLine from '~icons/lucide/pen-line'
|
||||
import DashboardGrid from '@/components/Dashboard/DashboardGrid.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { copy } from '@/utils'
|
||||
import { getLastXDays, formatter, formatRange } from '@/utils/dashboard'
|
||||
import {
|
||||
AxisChart,
|
||||
DonutChart,
|
||||
NumberChart,
|
||||
usePageMeta,
|
||||
createResource,
|
||||
DateRangePicker,
|
||||
Dropdown,
|
||||
Tooltip,
|
||||
} from 'frappe-ui'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, provide } from 'vue'
|
||||
|
||||
const { users, getUser, isManager, isAdmin } = usersStore()
|
||||
|
||||
const editing = ref(false)
|
||||
|
||||
const showDatePicker = ref(false)
|
||||
const datePickerRef = ref(null)
|
||||
const preset = ref('Last 30 Days')
|
||||
const showAddChartModal = ref(false)
|
||||
|
||||
const filters = reactive({
|
||||
period: getLastXDays(),
|
||||
@ -177,19 +182,7 @@ const toDate = computed(() => {
|
||||
function updateFilter(key: string, value: any, callback?: () => void) {
|
||||
filters[key] = value
|
||||
callback?.()
|
||||
reload()
|
||||
}
|
||||
|
||||
function reload() {
|
||||
numberCards.reload()
|
||||
salesTrend.reload()
|
||||
funnelConversion.reload()
|
||||
dealsBySalesperson.reload()
|
||||
dealsByTerritory.reload()
|
||||
lostDealReasons.reload()
|
||||
forecastedRevenue.reload()
|
||||
dealsByStage.reload()
|
||||
leadsBySource.reload()
|
||||
dashboardItems.reload()
|
||||
}
|
||||
|
||||
const options = computed(() => [
|
||||
@ -202,7 +195,7 @@ const options = computed(() => [
|
||||
onClick: () => {
|
||||
preset.value = 'Last 7 Days'
|
||||
filters.period = getLastXDays(7)
|
||||
reload()
|
||||
dashboardItems.reload()
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -210,7 +203,7 @@ const options = computed(() => [
|
||||
onClick: () => {
|
||||
preset.value = 'Last 30 Days'
|
||||
filters.period = getLastXDays(30)
|
||||
reload()
|
||||
dashboardItems.reload()
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -218,7 +211,7 @@ const options = computed(() => [
|
||||
onClick: () => {
|
||||
preset.value = 'Last 60 Days'
|
||||
filters.period = getLastXDays(60)
|
||||
reload()
|
||||
dashboardItems.reload()
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -226,7 +219,7 @@ const options = computed(() => [
|
||||
onClick: () => {
|
||||
preset.value = 'Last 90 Days'
|
||||
filters.period = getLastXDays(90)
|
||||
reload()
|
||||
dashboardItems.reload()
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -242,9 +235,9 @@ const options = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const numberCards = createResource({
|
||||
url: 'crm.api.dashboard.get_number_card_data',
|
||||
cache: ['Analytics', 'NumberCards'],
|
||||
const dashboardItems = createResource({
|
||||
url: 'crm.api.dashboard.get_dashboard',
|
||||
cache: ['Analytics', 'ManagerDashboard'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
@ -255,275 +248,50 @@ const numberCards = createResource({
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const salesTrend = createResource({
|
||||
url: 'crm.api.dashboard.get_sales_trend_data',
|
||||
cache: ['Analytics', 'SalesTrend'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Sales trend'),
|
||||
subtitle: __('Daily performance of leads, deals, and wins'),
|
||||
xAxis: {
|
||||
title: __('Date'),
|
||||
key: 'date',
|
||||
type: 'time' as const,
|
||||
timeGrain: 'day' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Count'),
|
||||
},
|
||||
series: [
|
||||
{ name: 'leads', type: 'line' as const, showDataPoints: true },
|
||||
{ name: 'deals', type: 'line' as const, showDataPoints: true },
|
||||
{ name: 'won_deals', type: 'line' as const, showDataPoints: true },
|
||||
],
|
||||
}
|
||||
const dirty = computed(() => {
|
||||
if (!editing.value) return false
|
||||
return JSON.stringify(dashboardItems.data) !== JSON.stringify(oldItems.value)
|
||||
})
|
||||
|
||||
const oldItems = ref([])
|
||||
|
||||
provide('fromDate', fromDate)
|
||||
provide('toDate', toDate)
|
||||
provide('filters', filters)
|
||||
|
||||
function enableEditing() {
|
||||
editing.value = true
|
||||
oldItems.value = copy(dashboardItems.data)
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
editing.value = false
|
||||
dashboardItems.data = copy(oldItems.value)
|
||||
}
|
||||
|
||||
const saveDashboard = createResource({
|
||||
url: 'frappe.client.set_value',
|
||||
method: 'POST',
|
||||
onSuccess: () => {
|
||||
dashboardItems.reload()
|
||||
editing.value = false
|
||||
},
|
||||
})
|
||||
|
||||
const funnelConversion = createResource({
|
||||
url: 'crm.api.dashboard.get_funnel_conversion_data',
|
||||
cache: ['Analytics', 'FunnelConversion'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Funnel conversion'),
|
||||
subtitle: __('Lead to deal conversion pipeline'),
|
||||
xAxis: {
|
||||
title: __('Stage'),
|
||||
key: 'stage',
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Count'),
|
||||
},
|
||||
swapXY: true,
|
||||
series: [
|
||||
{
|
||||
name: 'count',
|
||||
type: 'bar' as const,
|
||||
echartOptions: {
|
||||
colorBy: 'data',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
function save() {
|
||||
const dashboardItemsCopy = copy(dashboardItems.data)
|
||||
|
||||
const dealsBySalesperson = createResource({
|
||||
url: 'crm.api.dashboard.get_deals_by_salesperson',
|
||||
cache: ['Analytics', 'DealsBySalesperson'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
transform(r = { data: [], currency_symbol: '$' }) {
|
||||
return {
|
||||
data: r.data || [],
|
||||
title: __('Deals by salesperson'),
|
||||
subtitle: __('Number of deals and total value per salesperson'),
|
||||
xAxis: {
|
||||
title: __('Salesperson'),
|
||||
key: 'salesperson',
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Number of deals'),
|
||||
},
|
||||
y2Axis: {
|
||||
title: __('Deal value') + ` (${r.currency_symbol})`,
|
||||
},
|
||||
series: [
|
||||
{ name: 'deals', type: 'bar' as const },
|
||||
{
|
||||
name: 'value',
|
||||
type: 'line' as const,
|
||||
showDataPoints: true,
|
||||
axis: 'y2' as const,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
dashboardItemsCopy.forEach((item: any) => {
|
||||
delete item.data
|
||||
})
|
||||
|
||||
const dealsByTerritory = createResource({
|
||||
url: 'crm.api.dashboard.get_deals_by_territory',
|
||||
cache: ['Analytics', 'DealsByTerritory'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
transform(r = { data: [], currency_symbol: '$' }) {
|
||||
return {
|
||||
data: r.data || [],
|
||||
title: __('Deals by territory'),
|
||||
subtitle: __('Geographic distribution of deals and revenue'),
|
||||
xAxis: {
|
||||
title: __('Territory'),
|
||||
key: 'territory',
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Number of deals'),
|
||||
},
|
||||
y2Axis: {
|
||||
title: __('Deal value') + ` (${r.currency_symbol})`,
|
||||
},
|
||||
series: [
|
||||
{ name: 'deals', type: 'bar' as const },
|
||||
{
|
||||
name: 'value',
|
||||
type: 'line' as const,
|
||||
showDataPoints: true,
|
||||
axis: 'y2' as const,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const lostDealReasons = createResource({
|
||||
url: 'crm.api.dashboard.get_lost_deal_reasons',
|
||||
cache: ['Analytics', 'LostDealReasons'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Lost deal reasons'),
|
||||
subtitle: __('Common reasons for losing deals'),
|
||||
xAxis: {
|
||||
title: __('Reason'),
|
||||
key: 'reason',
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Count'),
|
||||
},
|
||||
swapXY: true,
|
||||
series: [{ name: 'count', type: 'bar' as const }],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const forecastedRevenue = createResource({
|
||||
url: 'crm.api.dashboard.get_forecasted_revenue',
|
||||
cache: ['Analytics', 'ForecastedRevenue'],
|
||||
makeParams() {
|
||||
return { user: filters.user }
|
||||
},
|
||||
auto: true,
|
||||
transform(r = { data: [], currency_symbol: '$' }) {
|
||||
return {
|
||||
data: r.data || [],
|
||||
title: __('Revenue forecast'),
|
||||
subtitle: __('Projected vs actual revenue based on deal probability'),
|
||||
xAxis: {
|
||||
title: __('Month'),
|
||||
key: 'month',
|
||||
type: 'time' as const,
|
||||
timeGrain: 'month' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Revenue') + ` (${r.currency_symbol})`,
|
||||
},
|
||||
series: [
|
||||
{ name: 'forecasted', type: 'line' as const, showDataPoints: true },
|
||||
{ name: 'actual', type: 'line' as const, showDataPoints: true },
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const dealsByStage = createResource({
|
||||
url: 'crm.api.dashboard.get_deals_by_stage',
|
||||
cache: ['Analytics', 'DealsByStage'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
transform(data = []) {
|
||||
return {
|
||||
donut: {
|
||||
data: data,
|
||||
title: __('Deals by stage'),
|
||||
subtitle: __('Current pipeline distribution'),
|
||||
categoryColumn: 'stage',
|
||||
valueColumn: 'count',
|
||||
},
|
||||
bar: {
|
||||
data: data.filter((d) => d.status_type != 'Lost'),
|
||||
title: __('Deals by ongoing & won stage'),
|
||||
xAxis: {
|
||||
title: __('Stage'),
|
||||
key: 'stage',
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Count'),
|
||||
},
|
||||
series: [{ name: 'count', type: 'bar' as const }],
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const leadsBySource = createResource({
|
||||
url: 'crm.api.dashboard.get_leads_by_source',
|
||||
cache: ['Analytics', 'LeadsBySource'],
|
||||
makeParams() {
|
||||
return {
|
||||
from_date: fromDate.value,
|
||||
to_date: toDate.value,
|
||||
user: filters.user,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Leads by source'),
|
||||
subtitle: __('Lead generation channel analysis'),
|
||||
categoryColumn: 'source',
|
||||
valueColumn: 'count',
|
||||
}
|
||||
},
|
||||
})
|
||||
saveDashboard.submit({
|
||||
doctype: 'CRM Dashboard',
|
||||
name: 'Manager Dashboard',
|
||||
fieldname: 'layout',
|
||||
value: JSON.stringify(dashboardItemsCopy),
|
||||
})
|
||||
}
|
||||
|
||||
usePageMeta(() => {
|
||||
return { title: __('CRM Dashboard') }
|
||||
|
||||
@ -531,3 +531,8 @@ export function TemplateOption({ active, option, theme, icon, onClick }) {
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
export function copy(obj) {
|
||||
if (!obj) return obj
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
69
yarn.lock
69
yarn.lock
@ -952,6 +952,13 @@
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.8"
|
||||
|
||||
"@floating-ui/core@^1.7.2":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.2.tgz#3d1c35263950b314b6d5a72c8bfb9e3c1551aefd"
|
||||
integrity sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.6.7":
|
||||
version "1.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.12.tgz#6333dcb5a8ead3b2bf82f33d6bc410e95f54e556"
|
||||
@ -968,6 +975,19 @@
|
||||
"@floating-ui/core" "^1.6.0"
|
||||
"@floating-ui/utils" "^0.2.9"
|
||||
|
||||
"@floating-ui/dom@^1.7.0":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.2.tgz#3540b051cf5ce0d4f4db5fb2507a76e8ea5b4a45"
|
||||
integrity sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.7.2"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/utils@^0.2.10":
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||
|
||||
"@floating-ui/utils@^0.2.8":
|
||||
version "0.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
|
||||
@ -1022,6 +1042,11 @@
|
||||
local-pkg "^1.0.0"
|
||||
mlly "^1.7.4"
|
||||
|
||||
"@interactjs/types@1.10.27":
|
||||
version "1.10.27"
|
||||
resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.10.27.tgz#10afd71cef2498e2b5192cf0d46f937d8ceb767f"
|
||||
integrity sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==
|
||||
|
||||
"@internationalized/date@^3.5.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.7.0.tgz#23a4956308ee108e308517a7137c69ab8f5f2ad9"
|
||||
@ -1095,6 +1120,11 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@juggle/resize-observer@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@ -1582,6 +1612,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
|
||||
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
|
||||
|
||||
"@vexip-ui/hooks@^2.8.0":
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@vexip-ui/hooks/-/hooks-2.9.2.tgz#3c6ba9670f1a4ac4211b05279e18657a3c1921ba"
|
||||
integrity sha512-zdwcTZUHYD/5aqndmUulyia4tPMI3FB09PUn674hZiQlkslO1KiH56WAI8R75wbvzPSmmhl5IA3VcbBZeaFEcw==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.7.0"
|
||||
"@juggle/resize-observer" "^3.4.0"
|
||||
"@vexip-ui/utils" "2.16.4"
|
||||
|
||||
"@vexip-ui/utils@2.16.4", "@vexip-ui/utils@^2.16.1":
|
||||
version "2.16.4"
|
||||
resolved "https://registry.yarnpkg.com/@vexip-ui/utils/-/utils-2.16.4.tgz#3429376a8f9e88040e969c21f14e70fe25d36127"
|
||||
integrity sha512-KX+Q4EsuwDp6ZlRJ7OAkiYxu52D5CVM8zpqQz/FXYV+JUtzl9T3dvxgtA8gQ0wm5Sh/xT6jp8Wo4X7tLAzRh/A==
|
||||
|
||||
"@vitejs/plugin-vue-jsx@^3.0.1":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz#9953fd9456539e1f0f253bf0fcd1289e66c67cd1"
|
||||
@ -2572,10 +2616,10 @@ fraction.js@^4.3.7:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||
|
||||
frappe-ui@^0.1.166:
|
||||
version "0.1.166"
|
||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.166.tgz#2664d9373b4751a39144c283be67f219c5eb99e3"
|
||||
integrity sha512-VSv2OE/JHa4ReOW0/9SafRzvQ6Dkxa1Bz6u58UU8FvagqpJVorQJlm2854LXuCk1IDV+uulPCr7uxiC8kwcjFw==
|
||||
frappe-ui@^0.1.171:
|
||||
version "0.1.171"
|
||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.171.tgz#10c582ea62292461ff37bb0b3ac2269409a373e9"
|
||||
integrity sha512-hIwban7j7qa+n/F6bZ+B78jYyGGj1gnibR/k0Kdx1SYPCfMdYr2TfZA8ySpbIvqWpeYxCus6nS4MD+wf0DpUOw==
|
||||
dependencies:
|
||||
"@floating-ui/vue" "^1.1.6"
|
||||
"@headlessui/vue" "^1.7.14"
|
||||
@ -2608,6 +2652,7 @@ frappe-ui@^0.1.166:
|
||||
dompurify "^3.2.6"
|
||||
echarts "^5.6.0"
|
||||
feather-icons "^4.28.0"
|
||||
grid-layout-plus "^1.1.0"
|
||||
highlight.js "^11.11.1"
|
||||
idb-keyval "^6.2.0"
|
||||
lowlight "^3.3.0"
|
||||
@ -2773,6 +2818,15 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
grid-layout-plus@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/grid-layout-plus/-/grid-layout-plus-1.1.0.tgz#4c6610ff3aa39ddea2953861c224d1914bf5a33d"
|
||||
integrity sha512-Q5uj0U5nx6xfHg8G1CDRJAEg+/40RVJl5jjRImcRwC78BxoJrEkTneT1pyxYMlbZ8fpGPT6QdHJQkD4+W6gt5A==
|
||||
dependencies:
|
||||
"@vexip-ui/hooks" "^2.8.0"
|
||||
"@vexip-ui/utils" "^2.16.1"
|
||||
interactjs "^1.10.27"
|
||||
|
||||
has-bigints@^1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe"
|
||||
@ -2854,6 +2908,13 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
interactjs@^1.10.27:
|
||||
version "1.10.27"
|
||||
resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.27.tgz#16499aba4987a5ccfdaddca7d1ba7bb1118e14d0"
|
||||
integrity sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==
|
||||
dependencies:
|
||||
"@interactjs/types" "1.10.27"
|
||||
|
||||
internal-slot@^1.0.7, internal-slot@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user