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
|
import frappe
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
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
|
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_default_lost_reasons()
|
||||||
add_standard_dropdown_items()
|
add_standard_dropdown_items()
|
||||||
add_default_scripts()
|
add_default_scripts()
|
||||||
|
create_default_manager_dashboard(force)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ def execute():
|
|||||||
]
|
]
|
||||||
|
|
||||||
for status in deal_statuses:
|
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:
|
if status.deal_status in openStatuses:
|
||||||
type = "Open"
|
type = "Open"
|
||||||
elif status.deal_status in ongoingStatuses:
|
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']
|
Activities: typeof import('./src/components/Activities/Activities.vue')['default']
|
||||||
ActivityHeader: typeof import('./src/components/Activities/ActivityHeader.vue')['default']
|
ActivityHeader: typeof import('./src/components/Activities/ActivityHeader.vue')['default']
|
||||||
ActivityIcon: typeof import('./src/components/Icons/ActivityIcon.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']
|
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
|
||||||
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
|
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
|
||||||
AddressModal: typeof import('./src/components/Modals/AddressModal.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']
|
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||||
CustomActions: typeof import('./src/components/CustomActions.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']
|
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']
|
DataFields: typeof import('./src/components/Activities/DataFields.vue')['default']
|
||||||
DataFieldsModal: typeof import('./src/components/Modals/DataFieldsModal.vue')['default']
|
DataFieldsModal: typeof import('./src/components/Modals/DataFieldsModal.vue')['default']
|
||||||
DealModal: typeof import('./src/components/Modals/DealModal.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']
|
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
|
||||||
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
|
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
|
||||||
EmailTemplateIcon: typeof import('./src/components/Icons/EmailTemplateIcon.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']
|
EmailTemplatePage: typeof import('./src/components/Settings/EmailTemplate/EmailTemplatePage.vue')['default']
|
||||||
EmailTemplates: typeof import('./src/components/Settings/EmailTemplate/EmailTemplates.vue')['default']
|
EmailTemplates: typeof import('./src/components/Settings/EmailTemplate/EmailTemplates.vue')['default']
|
||||||
EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.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']
|
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
|
||||||
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
|
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
|
||||||
ErrorPage: typeof import('./src/components/ErrorPage.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']
|
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
||||||
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
|
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
|
||||||
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
||||||
LucideInfo: typeof import('~icons/lucide/info')['default']
|
LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
LucidePenLine: typeof import('~icons/lucide/pen-line')['default']
|
||||||
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
LucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
|
||||||
LucideSearch: typeof import('~icons/lucide/search')['default']
|
|
||||||
LucideX: typeof import('~icons/lucide/x')['default']
|
|
||||||
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
|
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
|
||||||
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
||||||
MenuIcon: typeof import('./src/components/Icons/MenuIcon.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']
|
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
|
||||||
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
|
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
|
||||||
Popover: typeof import('./src/components/frappe-ui/Popover.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']
|
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
|
||||||
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
||||||
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"@tiptap/extension-paragraph": "^2.12.0",
|
"@tiptap/extension-paragraph": "^2.12.0",
|
||||||
"@twilio/voice-sdk": "^2.10.2",
|
"@twilio/voice-sdk": "^2.10.2",
|
||||||
"@vueuse/integrations": "^10.3.0",
|
"@vueuse/integrations": "^10.3.0",
|
||||||
"frappe-ui": "^0.1.166",
|
"frappe-ui": "^0.1.171",
|
||||||
"gemoji": "^8.1.0",
|
"gemoji": "^8.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.1",
|
"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>
|
<template #left-header>
|
||||||
<ViewBreadcrumbs routeName="Dashboard" />
|
<ViewBreadcrumbs routeName="Dashboard" />
|
||||||
</template>
|
</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>
|
</LayoutHeader>
|
||||||
|
|
||||||
<div class="p-5 pb-3 flex items-center gap-4">
|
<div class="p-5 pb-2 flex items-center gap-4">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
v-if="!showDatePicker"
|
v-if="!showDatePicker"
|
||||||
:options="options"
|
:options="options"
|
||||||
@ -83,81 +118,51 @@
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-5 pt-2 w-full overflow-y-scroll">
|
<div class="w-full overflow-y-scroll">
|
||||||
<div class="transition-all animate-fade-in duration-300">
|
<DashboardGrid
|
||||||
<div
|
class="pt-1"
|
||||||
v-if="!numberCards.loading"
|
v-if="!dashboardItems.loading && dashboardItems.data"
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4"
|
v-model="dashboardItems.data"
|
||||||
>
|
:editing="editing"
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AddChartModal
|
||||||
|
v-if="showAddChartModal"
|
||||||
|
v-model="showAddChartModal"
|
||||||
|
v-model:items="dashboardItems.data"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { copy } from '@/utils'
|
||||||
import { getLastXDays, formatter, formatRange } from '@/utils/dashboard'
|
import { getLastXDays, formatter, formatRange } from '@/utils/dashboard'
|
||||||
import {
|
import {
|
||||||
AxisChart,
|
|
||||||
DonutChart,
|
|
||||||
NumberChart,
|
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
createResource,
|
createResource,
|
||||||
DateRangePicker,
|
DateRangePicker,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, provide } from 'vue'
|
||||||
|
|
||||||
const { users, getUser, isManager, isAdmin } = usersStore()
|
const { users, getUser, isManager, isAdmin } = usersStore()
|
||||||
|
|
||||||
|
const editing = ref(false)
|
||||||
|
|
||||||
const showDatePicker = ref(false)
|
const showDatePicker = ref(false)
|
||||||
const datePickerRef = ref(null)
|
const datePickerRef = ref(null)
|
||||||
const preset = ref('Last 30 Days')
|
const preset = ref('Last 30 Days')
|
||||||
|
const showAddChartModal = ref(false)
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
period: getLastXDays(),
|
period: getLastXDays(),
|
||||||
@ -177,19 +182,7 @@ const toDate = computed(() => {
|
|||||||
function updateFilter(key: string, value: any, callback?: () => void) {
|
function updateFilter(key: string, value: any, callback?: () => void) {
|
||||||
filters[key] = value
|
filters[key] = value
|
||||||
callback?.()
|
callback?.()
|
||||||
reload()
|
dashboardItems.reload()
|
||||||
}
|
|
||||||
|
|
||||||
function reload() {
|
|
||||||
numberCards.reload()
|
|
||||||
salesTrend.reload()
|
|
||||||
funnelConversion.reload()
|
|
||||||
dealsBySalesperson.reload()
|
|
||||||
dealsByTerritory.reload()
|
|
||||||
lostDealReasons.reload()
|
|
||||||
forecastedRevenue.reload()
|
|
||||||
dealsByStage.reload()
|
|
||||||
leadsBySource.reload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = computed(() => [
|
const options = computed(() => [
|
||||||
@ -202,7 +195,7 @@ const options = computed(() => [
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
preset.value = 'Last 7 Days'
|
preset.value = 'Last 7 Days'
|
||||||
filters.period = getLastXDays(7)
|
filters.period = getLastXDays(7)
|
||||||
reload()
|
dashboardItems.reload()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -210,7 +203,7 @@ const options = computed(() => [
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
preset.value = 'Last 30 Days'
|
preset.value = 'Last 30 Days'
|
||||||
filters.period = getLastXDays(30)
|
filters.period = getLastXDays(30)
|
||||||
reload()
|
dashboardItems.reload()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -218,7 +211,7 @@ const options = computed(() => [
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
preset.value = 'Last 60 Days'
|
preset.value = 'Last 60 Days'
|
||||||
filters.period = getLastXDays(60)
|
filters.period = getLastXDays(60)
|
||||||
reload()
|
dashboardItems.reload()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -226,7 +219,7 @@ const options = computed(() => [
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
preset.value = 'Last 90 Days'
|
preset.value = 'Last 90 Days'
|
||||||
filters.period = getLastXDays(90)
|
filters.period = getLastXDays(90)
|
||||||
reload()
|
dashboardItems.reload()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -242,9 +235,9 @@ const options = computed(() => [
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const numberCards = createResource({
|
const dashboardItems = createResource({
|
||||||
url: 'crm.api.dashboard.get_number_card_data',
|
url: 'crm.api.dashboard.get_dashboard',
|
||||||
cache: ['Analytics', 'NumberCards'],
|
cache: ['Analytics', 'ManagerDashboard'],
|
||||||
makeParams() {
|
makeParams() {
|
||||||
return {
|
return {
|
||||||
from_date: fromDate.value,
|
from_date: fromDate.value,
|
||||||
@ -255,275 +248,50 @@ const numberCards = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const salesTrend = createResource({
|
const dirty = computed(() => {
|
||||||
url: 'crm.api.dashboard.get_sales_trend_data',
|
if (!editing.value) return false
|
||||||
cache: ['Analytics', 'SalesTrend'],
|
return JSON.stringify(dashboardItems.data) !== JSON.stringify(oldItems.value)
|
||||||
makeParams() {
|
})
|
||||||
return {
|
|
||||||
from_date: fromDate.value,
|
const oldItems = ref([])
|
||||||
to_date: toDate.value,
|
|
||||||
user: filters.user,
|
provide('fromDate', fromDate)
|
||||||
}
|
provide('toDate', toDate)
|
||||||
},
|
provide('filters', filters)
|
||||||
auto: true,
|
|
||||||
transform(data = []) {
|
function enableEditing() {
|
||||||
return {
|
editing.value = true
|
||||||
data: data,
|
oldItems.value = copy(dashboardItems.data)
|
||||||
title: __('Sales trend'),
|
}
|
||||||
subtitle: __('Daily performance of leads, deals, and wins'),
|
|
||||||
xAxis: {
|
function cancel() {
|
||||||
title: __('Date'),
|
editing.value = false
|
||||||
key: 'date',
|
dashboardItems.data = copy(oldItems.value)
|
||||||
type: 'time' as const,
|
}
|
||||||
timeGrain: 'day' as const,
|
|
||||||
},
|
const saveDashboard = createResource({
|
||||||
yAxis: {
|
url: 'frappe.client.set_value',
|
||||||
title: __('Count'),
|
method: 'POST',
|
||||||
},
|
onSuccess: () => {
|
||||||
series: [
|
dashboardItems.reload()
|
||||||
{ name: 'leads', type: 'line' as const, showDataPoints: true },
|
editing.value = false
|
||||||
{ name: 'deals', type: 'line' as const, showDataPoints: true },
|
|
||||||
{ name: 'won_deals', type: 'line' as const, showDataPoints: true },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const funnelConversion = createResource({
|
function save() {
|
||||||
url: 'crm.api.dashboard.get_funnel_conversion_data',
|
const dashboardItemsCopy = copy(dashboardItems.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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const dealsBySalesperson = createResource({
|
dashboardItemsCopy.forEach((item: any) => {
|
||||||
url: 'crm.api.dashboard.get_deals_by_salesperson',
|
delete item.data
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const dealsByTerritory = createResource({
|
saveDashboard.submit({
|
||||||
url: 'crm.api.dashboard.get_deals_by_territory',
|
doctype: 'CRM Dashboard',
|
||||||
cache: ['Analytics', 'DealsByTerritory'],
|
name: 'Manager Dashboard',
|
||||||
makeParams() {
|
fieldname: 'layout',
|
||||||
return {
|
value: JSON.stringify(dashboardItemsCopy),
|
||||||
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',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
usePageMeta(() => {
|
usePageMeta(() => {
|
||||||
return { title: __('CRM Dashboard') }
|
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:
|
dependencies:
|
||||||
"@floating-ui/utils" "^0.2.8"
|
"@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":
|
"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.6.7":
|
||||||
version "1.6.12"
|
version "1.6.12"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.12.tgz#6333dcb5a8ead3b2bf82f33d6bc410e95f54e556"
|
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/core" "^1.6.0"
|
||||||
"@floating-ui/utils" "^0.2.9"
|
"@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":
|
"@floating-ui/utils@^0.2.8":
|
||||||
version "0.2.8"
|
version "0.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
|
||||||
@ -1022,6 +1042,11 @@
|
|||||||
local-pkg "^1.0.0"
|
local-pkg "^1.0.0"
|
||||||
mlly "^1.7.4"
|
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":
|
"@internationalized/date@^3.5.0":
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.7.0.tgz#23a4956308ee108e308517a7137c69ab8f5f2ad9"
|
resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.7.0.tgz#23a4956308ee108e308517a7137c69ab8f5f2ad9"
|
||||||
@ -1095,6 +1120,11 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@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":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
|
||||||
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
|
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":
|
"@vitejs/plugin-vue-jsx@^3.0.1":
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz#9953fd9456539e1f0f253bf0fcd1289e66c67cd1"
|
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"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
frappe-ui@^0.1.166:
|
frappe-ui@^0.1.171:
|
||||||
version "0.1.166"
|
version "0.1.171"
|
||||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.166.tgz#2664d9373b4751a39144c283be67f219c5eb99e3"
|
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.171.tgz#10c582ea62292461ff37bb0b3ac2269409a373e9"
|
||||||
integrity sha512-VSv2OE/JHa4ReOW0/9SafRzvQ6Dkxa1Bz6u58UU8FvagqpJVorQJlm2854LXuCk1IDV+uulPCr7uxiC8kwcjFw==
|
integrity sha512-hIwban7j7qa+n/F6bZ+B78jYyGGj1gnibR/k0Kdx1SYPCfMdYr2TfZA8ySpbIvqWpeYxCus6nS4MD+wf0DpUOw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/vue" "^1.1.6"
|
"@floating-ui/vue" "^1.1.6"
|
||||||
"@headlessui/vue" "^1.7.14"
|
"@headlessui/vue" "^1.7.14"
|
||||||
@ -2608,6 +2652,7 @@ frappe-ui@^0.1.166:
|
|||||||
dompurify "^3.2.6"
|
dompurify "^3.2.6"
|
||||||
echarts "^5.6.0"
|
echarts "^5.6.0"
|
||||||
feather-icons "^4.28.0"
|
feather-icons "^4.28.0"
|
||||||
|
grid-layout-plus "^1.1.0"
|
||||||
highlight.js "^11.11.1"
|
highlight.js "^11.11.1"
|
||||||
idb-keyval "^6.2.0"
|
idb-keyval "^6.2.0"
|
||||||
lowlight "^3.3.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"
|
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==
|
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:
|
has-bigints@^1.0.2:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe"
|
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"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
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:
|
internal-slot@^1.0.7, internal-slot@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
|
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user