Merge pull request #995 from frappe/mergify/bp/main-hotfix/pr-994
fix: Forecasting fixes (backport #994)
This commit is contained in:
commit
5e0bc3c448
@ -24,7 +24,7 @@ class CRMDeal(Document):
|
|||||||
self.assign_agent(self.deal_owner)
|
self.assign_agent(self.deal_owner)
|
||||||
if self.has_value_changed("status"):
|
if self.has_value_changed("status"):
|
||||||
add_status_change_log(self)
|
add_status_change_log(self)
|
||||||
self.update_close_date()
|
self.validate_forcasting_fields()
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
if self.deal_owner:
|
if self.deal_owner:
|
||||||
@ -141,6 +141,14 @@ class CRMDeal(Document):
|
|||||||
if self.status == "Won" and not self.close_date:
|
if self.status == "Won" and not self.close_date:
|
||||||
self.close_date = frappe.utils.nowdate()
|
self.close_date = frappe.utils.nowdate()
|
||||||
|
|
||||||
|
def validate_forcasting_fields(self):
|
||||||
|
self.update_close_date()
|
||||||
|
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
||||||
|
if not self.deal_value or self.deal_value == 0:
|
||||||
|
frappe.throw(_("Deal Value is required."), frappe.MandatoryError)
|
||||||
|
if not self.close_date:
|
||||||
|
frappe.throw(_("Close Date is required."), frappe.MandatoryError)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_list_data():
|
def default_list_data():
|
||||||
columns = [
|
columns = [
|
||||||
|
|||||||
@ -37,13 +37,14 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "probability",
|
"fieldname": "probability",
|
||||||
"fieldtype": "Percent",
|
"fieldtype": "Percent",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Probability"
|
"label": "Probability"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-11 13:00:34.518808",
|
"modified": "2025-07-01 12:06:42.937440",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal Status",
|
"name": "CRM Deal Status",
|
||||||
|
|||||||
@ -47,6 +47,13 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
|
|||||||
fields = frappe.get_meta(doctype).fields
|
fields = frappe.get_meta(doctype).fields
|
||||||
fields = [field for field in fields if field.fieldname in allowed_fields]
|
fields = [field for field in fields if field.fieldname in allowed_fields]
|
||||||
|
|
||||||
|
required_fields = []
|
||||||
|
|
||||||
|
if type == "Required Fields":
|
||||||
|
required_fields = [
|
||||||
|
field for field in frappe.get_meta(doctype, False).fields if field.reqd and not field.default
|
||||||
|
]
|
||||||
|
|
||||||
for tab in tabs:
|
for tab in tabs:
|
||||||
for section in tab.get("sections"):
|
for section in tab.get("sections"):
|
||||||
if section.get("columns"):
|
if section.get("columns"):
|
||||||
@ -60,6 +67,32 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
|
|||||||
handle_perm_level_restrictions(field, doctype, parent_doctype)
|
handle_perm_level_restrictions(field, doctype, parent_doctype)
|
||||||
column["fields"][column.get("fields").index(field["fieldname"])] = field
|
column["fields"][column.get("fields").index(field["fieldname"])] = field
|
||||||
|
|
||||||
|
# remove field from required_fields if it is already present
|
||||||
|
if (
|
||||||
|
type == "Required Fields"
|
||||||
|
and field.reqd
|
||||||
|
and any(f.get("fieldname") == field.get("fieldname") for f in required_fields)
|
||||||
|
):
|
||||||
|
required_fields = [
|
||||||
|
f for f in required_fields if f.get("fieldname") != field.get("fieldname")
|
||||||
|
]
|
||||||
|
|
||||||
|
if type == "Required Fields" and required_fields and tabs:
|
||||||
|
tabs[-1].get("sections").append(
|
||||||
|
{
|
||||||
|
"label": "Required Fields",
|
||||||
|
"name": "required_fields_section_" + str(random_string(4)),
|
||||||
|
"opened": True,
|
||||||
|
"hideLabel": True,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "required_fields_column_" + str(random_string(4)),
|
||||||
|
"fields": [field.as_dict() for field in required_fields],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return tabs or []
|
return tabs or []
|
||||||
|
|
||||||
|
|
||||||
@ -83,6 +116,8 @@ def get_sidepanel_sections(doctype):
|
|||||||
fields = frappe.get_meta(doctype).fields
|
fields = frappe.get_meta(doctype).fields
|
||||||
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
||||||
|
|
||||||
|
add_forecasting_section(layout, doctype)
|
||||||
|
|
||||||
for section in layout:
|
for section in layout:
|
||||||
section["name"] = section.get("name") or section.get("label")
|
section["name"] = section.get("name") or section.get("label")
|
||||||
for column in section.get("columns") if section.get("columns") else []:
|
for column in section.get("columns") if section.get("columns") else []:
|
||||||
@ -100,6 +135,38 @@ def get_sidepanel_sections(doctype):
|
|||||||
return layout
|
return layout
|
||||||
|
|
||||||
|
|
||||||
|
def add_forecasting_section(layout, doctype):
|
||||||
|
if (
|
||||||
|
doctype == "CRM Deal"
|
||||||
|
and frappe.db.get_single_value("FCRM Settings", "enable_forecasting")
|
||||||
|
and not any(section.get("name") == "forecasted_sales_section" for section in layout)
|
||||||
|
):
|
||||||
|
contacts_section_index = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i, section in enumerate(layout)
|
||||||
|
if section.get("name") == "contacts_section" or section.get("label") == "Contacts"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if contacts_section_index is not None:
|
||||||
|
layout.insert(
|
||||||
|
contacts_section_index + 1,
|
||||||
|
{
|
||||||
|
"name": "forecasted_sales_section",
|
||||||
|
"label": "Forecasted Sales",
|
||||||
|
"opened": True,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "column_" + str(random_string(4)),
|
||||||
|
"fields": ["close_date", "probability", "deal_value"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_perm_level_restrictions(field, doctype, parent_doctype=None):
|
def handle_perm_level_restrictions(field, doctype, parent_doctype=None):
|
||||||
if field.permlevel == 0:
|
if field.permlevel == 0:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -60,7 +60,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "It will make deal's \"Expected Closure Date\" mandatory to get accurate forecasting insights",
|
"description": "It will make deal's \"Close Date\" & \"Deal Value\" mandatory to get accurate forecasting insights",
|
||||||
"fieldname": "enable_forecasting",
|
"fieldname": "enable_forecasting",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Forecasting"
|
"label": "Enable Forecasting"
|
||||||
@ -69,7 +69,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-11 19:12:16.762499",
|
"modified": "2025-07-01 13:20:48.757603",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "FCRM Settings",
|
"name": "FCRM Settings",
|
||||||
|
|||||||
@ -39,6 +39,11 @@ class FCRMSettings(Document):
|
|||||||
"reqd",
|
"reqd",
|
||||||
"close_date",
|
"close_date",
|
||||||
)
|
)
|
||||||
|
delete_property_setter(
|
||||||
|
"CRM Deal",
|
||||||
|
"reqd",
|
||||||
|
"deal_value",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
make_property_setter(
|
make_property_setter(
|
||||||
"CRM Deal",
|
"CRM Deal",
|
||||||
@ -47,6 +52,13 @@ class FCRMSettings(Document):
|
|||||||
1 if self.enable_forecasting else 0,
|
1 if self.enable_forecasting else 0,
|
||||||
"Check",
|
"Check",
|
||||||
)
|
)
|
||||||
|
make_property_setter(
|
||||||
|
"CRM Deal",
|
||||||
|
"deal_value",
|
||||||
|
"reqd",
|
||||||
|
1 if self.enable_forecasting else 0,
|
||||||
|
"Check",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_standard_dropdown_items():
|
def get_standard_dropdown_items():
|
||||||
|
|||||||
@ -68,30 +68,37 @@ def add_default_deal_statuses():
|
|||||||
statuses = {
|
statuses = {
|
||||||
"Qualification": {
|
"Qualification": {
|
||||||
"color": "gray",
|
"color": "gray",
|
||||||
|
"probability": 10,
|
||||||
"position": 1,
|
"position": 1,
|
||||||
},
|
},
|
||||||
"Demo/Making": {
|
"Demo/Making": {
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
|
"probability": 25,
|
||||||
"position": 2,
|
"position": 2,
|
||||||
},
|
},
|
||||||
"Proposal/Quotation": {
|
"Proposal/Quotation": {
|
||||||
"color": "blue",
|
"color": "blue",
|
||||||
|
"probability": 50,
|
||||||
"position": 3,
|
"position": 3,
|
||||||
},
|
},
|
||||||
"Negotiation": {
|
"Negotiation": {
|
||||||
"color": "yellow",
|
"color": "yellow",
|
||||||
|
"probability": 70,
|
||||||
"position": 4,
|
"position": 4,
|
||||||
},
|
},
|
||||||
"Ready to Close": {
|
"Ready to Close": {
|
||||||
"color": "purple",
|
"color": "purple",
|
||||||
|
"probability": 90,
|
||||||
"position": 5,
|
"position": 5,
|
||||||
},
|
},
|
||||||
"Won": {
|
"Won": {
|
||||||
"color": "green",
|
"color": "green",
|
||||||
|
"probability": 100,
|
||||||
"position": 6,
|
"position": 6,
|
||||||
},
|
},
|
||||||
"Lost": {
|
"Lost": {
|
||||||
"color": "red",
|
"color": "red",
|
||||||
|
"probability": 0,
|
||||||
"position": 7,
|
"position": 7,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -103,6 +110,7 @@ def add_default_deal_statuses():
|
|||||||
doc = frappe.new_doc("CRM Deal Status")
|
doc = frappe.new_doc("CRM Deal Status")
|
||||||
doc.deal_status = status
|
doc.deal_status = status
|
||||||
doc.color = statuses[status]["color"]
|
doc.color = statuses[status]["color"]
|
||||||
|
doc.probability = statuses[status]["probability"]
|
||||||
doc.position = statuses[status]["position"]
|
doc.position = statuses[status]["position"]
|
||||||
doc.insert()
|
doc.insert()
|
||||||
|
|
||||||
|
|||||||
@ -12,4 +12,5 @@ crm.patches.v1_0.create_default_sidebar_fields_layout
|
|||||||
crm.patches.v1_0.update_deal_quick_entry_layout
|
crm.patches.v1_0.update_deal_quick_entry_layout
|
||||||
crm.patches.v1_0.update_layouts_to_new_format
|
crm.patches.v1_0.update_layouts_to_new_format
|
||||||
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
||||||
crm.patches.v1_0.create_default_scripts # 13-06-2025
|
crm.patches.v1_0.create_default_scripts # 13-06-2025
|
||||||
|
crm.patches.v1_0.update_deal_status_probabilities
|
||||||
24
crm/patches/v1_0/update_deal_status_probabilities.py
Normal file
24
crm/patches/v1_0/update_deal_status_probabilities.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
deal_statuses = frappe.get_all("CRM Deal Status", fields=["name", "probability", "deal_status"])
|
||||||
|
|
||||||
|
for status in deal_statuses:
|
||||||
|
if status.probability is None or status.probability == 0:
|
||||||
|
if status.deal_status == "Qualification":
|
||||||
|
probability = 10
|
||||||
|
elif status.deal_status == "Demo/Making":
|
||||||
|
probability = 25
|
||||||
|
elif status.deal_status == "Proposal/Quotation":
|
||||||
|
probability = 50
|
||||||
|
elif status.deal_status == "Negotiation":
|
||||||
|
probability = 70
|
||||||
|
elif status.deal_status == "Ready to Close":
|
||||||
|
probability = 90
|
||||||
|
elif status.deal_status == "Won":
|
||||||
|
probability = 100
|
||||||
|
else:
|
||||||
|
probability = 0
|
||||||
|
|
||||||
|
frappe.db.set_value("CRM Deal Status", status.name, "probability", probability)
|
||||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -56,6 +56,7 @@ declare module 'vue' {
|
|||||||
ContactsIcon: typeof import('./src/components/Icons/ContactsIcon.vue')['default']
|
ContactsIcon: typeof import('./src/components/Icons/ContactsIcon.vue')['default']
|
||||||
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
|
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
|
||||||
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
|
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
|
||||||
|
ConvertToDealModal: typeof import('./src/components/Modals/ConvertToDealModal.vue')['default']
|
||||||
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
||||||
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']
|
||||||
|
|||||||
@ -121,7 +121,10 @@ const callBacks = {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
if (err.exc_type == 'MandatoryError') {
|
if (err.exc_type == 'MandatoryError') {
|
||||||
const errorMessage = err.messages
|
const errorMessage = err.messages
|
||||||
.map((msg) => msg.split(': ')[2].trim())
|
.map((msg) => {
|
||||||
|
let arr = msg.split(': ')
|
||||||
|
return arr[arr.length - 1].trim()
|
||||||
|
})
|
||||||
.join(', ')
|
.join(', ')
|
||||||
error.value = __('These fields are required: {0}', [errorMessage])
|
error.value = __('These fields are required: {0}', [errorMessage])
|
||||||
return
|
return
|
||||||
|
|||||||
@ -124,7 +124,10 @@ const callBacks = {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
if (err.exc_type == 'MandatoryError') {
|
if (err.exc_type == 'MandatoryError') {
|
||||||
const errorMessage = err.messages
|
const errorMessage = err.messages
|
||||||
.map((msg) => msg.split(': ')[2].trim())
|
.map((msg) => {
|
||||||
|
let arr = msg.split(': ')
|
||||||
|
return arr[arr.length - 1].trim()
|
||||||
|
})
|
||||||
.join(', ')
|
.join(', ')
|
||||||
error.value = __('These fields are required: {0}', [errorMessage])
|
error.value = __('These fields are required: {0}', [errorMessage])
|
||||||
return
|
return
|
||||||
|
|||||||
247
frontend/src/components/Modals/ConvertToDealModal.vue
Normal file
247
frontend/src/components/Modals/ConvertToDealModal.vue
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
size: 'xl',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Convert'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: convertToDeal,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-header>
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||||
|
{{ __('Convert to Deal') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
v-if="isManager() && !isMobileView"
|
||||||
|
variant="ghost"
|
||||||
|
@click="openQuickEntryModal"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<EditIcon class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<Button icon="x" variant="ghost" @click="show = false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="mb-4 flex items-center gap-2 text-ink-gray-5">
|
||||||
|
<OrganizationsIcon class="h-4 w-4" />
|
||||||
|
<label class="block text-base">{{ __('Organization') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="ml-6 text-ink-gray-9">
|
||||||
|
<div class="flex items-center justify-between text-base">
|
||||||
|
<div>{{ __('Choose Existing') }}</div>
|
||||||
|
<Switch v-model="existingOrganizationChecked" />
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
v-if="existingOrganizationChecked"
|
||||||
|
class="form-control mt-2.5"
|
||||||
|
size="md"
|
||||||
|
:value="existingOrganization"
|
||||||
|
doctype="CRM Organization"
|
||||||
|
@change="(data) => (existingOrganization = data)"
|
||||||
|
/>
|
||||||
|
<div v-else class="mt-2.5 text-base">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'New organization will be created based on the data in details section',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 mt-6 flex items-center gap-2 text-ink-gray-5">
|
||||||
|
<ContactsIcon class="h-4 w-4" />
|
||||||
|
<label class="block text-base">{{ __('Contact') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="ml-6 text-ink-gray-9">
|
||||||
|
<div class="flex items-center justify-between text-base">
|
||||||
|
<div>{{ __('Choose Existing') }}</div>
|
||||||
|
<Switch v-model="existingContactChecked" />
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
v-if="existingContactChecked"
|
||||||
|
class="form-control mt-2.5"
|
||||||
|
size="md"
|
||||||
|
:value="existingContact"
|
||||||
|
doctype="Contact"
|
||||||
|
@change="(data) => (existingContact = data)"
|
||||||
|
/>
|
||||||
|
<div v-else class="mt-2.5 text-base">
|
||||||
|
{{ __("New contact will be created based on the person's details") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dealTabs.data?.length" class="h-px w-full border-t my-6" />
|
||||||
|
|
||||||
|
<FieldLayout
|
||||||
|
v-if="dealTabs.data?.length"
|
||||||
|
:tabs="dealTabs.data"
|
||||||
|
:data="deal.doc"
|
||||||
|
doctype="CRM Deal"
|
||||||
|
/>
|
||||||
|
<ErrorMessage class="mt-4" :message="error" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||||
|
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { sessionStore } from '@/stores/session'
|
||||||
|
import { statusesStore } from '@/stores/statuses'
|
||||||
|
import { showQuickEntryModal, quickEntryProps } from '@/composables/modals'
|
||||||
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
import { capture } from '@/telemetry'
|
||||||
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
|
import { Switch, Dialog, createResource, call } from 'frappe-ui'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
lead: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { statusOptions, getDealStatus } = statusesStore()
|
||||||
|
const { isManager } = usersStore()
|
||||||
|
const { user } = sessionStore()
|
||||||
|
const { updateOnboardingStep } = useOnboarding('frappecrm')
|
||||||
|
|
||||||
|
const existingContactChecked = ref(false)
|
||||||
|
const existingOrganizationChecked = ref(false)
|
||||||
|
|
||||||
|
const existingContact = ref('')
|
||||||
|
const existingOrganization = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const { triggerConvertToDeal } = useDocument('CRM Lead', props.lead.name)
|
||||||
|
const { document: deal } = useDocument('CRM Deal')
|
||||||
|
|
||||||
|
async function convertToDeal() {
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
if (existingContactChecked.value && !existingContact.value) {
|
||||||
|
error.value = __('Please select an existing contact')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingOrganizationChecked.value && !existingOrganization.value) {
|
||||||
|
error.value = __('Please select an existing organization')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingContactChecked.value && existingContact.value) {
|
||||||
|
existingContact.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingOrganizationChecked.value && existingOrganization.value) {
|
||||||
|
existingOrganization.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
await triggerConvertToDeal?.(props.lead, deal.doc, () => (show.value = false))
|
||||||
|
|
||||||
|
let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
||||||
|
lead: props.lead.name,
|
||||||
|
deal: deal.doc,
|
||||||
|
existing_contact: existingContact.value,
|
||||||
|
existing_organization: existingOrganization.value,
|
||||||
|
}).catch((err) => {
|
||||||
|
if (err.exc_type == 'MandatoryError') {
|
||||||
|
const errorMessage = err.messages
|
||||||
|
.map((msg) => {
|
||||||
|
let arr = msg.split(': ')
|
||||||
|
return arr[arr.length - 1].trim()
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
|
if (errorMessage.toLowerCase().includes('required')) {
|
||||||
|
error.value = __(errorMessage)
|
||||||
|
} else {
|
||||||
|
error.value = __('{0} is required', [errorMessage])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
error.value = __('Error converting to deal: {0}', [err.messages?.[0]])
|
||||||
|
})
|
||||||
|
if (_deal) {
|
||||||
|
show.value = false
|
||||||
|
existingContactChecked.value = false
|
||||||
|
existingOrganizationChecked.value = false
|
||||||
|
existingContact.value = ''
|
||||||
|
existingOrganization.value = ''
|
||||||
|
error.value = ''
|
||||||
|
updateOnboardingStep('convert_lead_to_deal', true, false, () => {
|
||||||
|
localStorage.setItem('firstDeal' + user, _deal)
|
||||||
|
})
|
||||||
|
capture('convert_lead_to_deal')
|
||||||
|
router.push({ name: 'Deal', params: { dealId: _deal } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dealStatuses = computed(() => {
|
||||||
|
let statuses = statusOptions('deal')
|
||||||
|
if (!deal.doc?.status) {
|
||||||
|
deal.doc.status = statuses[0].value
|
||||||
|
}
|
||||||
|
return statuses
|
||||||
|
})
|
||||||
|
|
||||||
|
const dealTabs = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['RequiredFields', 'CRM Deal'],
|
||||||
|
params: { doctype: 'CRM Deal', type: 'Required Fields' },
|
||||||
|
auto: true,
|
||||||
|
transform: (_tabs) => {
|
||||||
|
let hasFields = false
|
||||||
|
let parsedTabs = _tabs?.forEach((tab) => {
|
||||||
|
tab.sections?.forEach((section) => {
|
||||||
|
section.columns?.forEach((column) => {
|
||||||
|
column.fields?.forEach((field) => {
|
||||||
|
hasFields = true
|
||||||
|
if (field.fieldname == 'status') {
|
||||||
|
field.fieldtype = 'Select'
|
||||||
|
field.options = dealStatuses.value
|
||||||
|
field.prefix = getDealStatus(deal.doc.status).color
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.fieldtype === 'Table') {
|
||||||
|
deal.doc[field.fieldname] = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return hasFields ? parsedTabs : []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function openQuickEntryModal() {
|
||||||
|
showQuickEntryModal.value = true
|
||||||
|
quickEntryProps.value = {
|
||||||
|
doctype: 'CRM Deal',
|
||||||
|
onlyRequired: true,
|
||||||
|
}
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -26,7 +26,10 @@ export function useDocument(doctype, docname) {
|
|||||||
let errorMessage = __('Error updating document')
|
let errorMessage = __('Error updating document')
|
||||||
if (err.exc_type == 'MandatoryError') {
|
if (err.exc_type == 'MandatoryError') {
|
||||||
const fieldName = err.messages
|
const fieldName = err.messages
|
||||||
.map((msg) => msg.split(': ')[2].trim())
|
.map((msg) => {
|
||||||
|
let arr = msg.split(': ')
|
||||||
|
return arr[arr.length - 1].trim()
|
||||||
|
})
|
||||||
.join(', ')
|
.join(', ')
|
||||||
errorMessage = __('Mandatory field error: {0}', [fieldName])
|
errorMessage = __('Mandatory field error: {0}', [fieldName])
|
||||||
}
|
}
|
||||||
@ -193,8 +196,7 @@ export function useDocument(doctype, docname) {
|
|||||||
async function triggerConvertToDeal() {
|
async function triggerConvertToDeal() {
|
||||||
const args = Array.from(arguments)
|
const args = Array.from(arguments)
|
||||||
const handler = async function () {
|
const handler = async function () {
|
||||||
await (this.convertToDeal?.(...args) ||
|
await (this.convertToDeal?.(...args) || this.convert_to_deal?.(...args))
|
||||||
this.on_convert_to_deal?.(...args))
|
|
||||||
}
|
}
|
||||||
await trigger(handler)
|
await trigger(handler)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -216,108 +216,11 @@
|
|||||||
:errorTitle="errorTitle"
|
:errorTitle="errorTitle"
|
||||||
:errorMessage="errorMessage"
|
:errorMessage="errorMessage"
|
||||||
/>
|
/>
|
||||||
<Dialog
|
<ConvertToDealModal
|
||||||
|
v-if="showConvertToDealModal"
|
||||||
v-model="showConvertToDealModal"
|
v-model="showConvertToDealModal"
|
||||||
:options="{
|
:lead="lead.data"
|
||||||
size: 'xl',
|
/>
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: __('Convert'),
|
|
||||||
variant: 'solid',
|
|
||||||
onClick: convertToDeal,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #body-header>
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
|
||||||
{{ __('Convert to Deal') }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
v-if="isManager() && !isMobileView"
|
|
||||||
variant="ghost"
|
|
||||||
class="w-7"
|
|
||||||
@click="openQuickEntryModal"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<EditIcon class="h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="w-7"
|
|
||||||
@click="showConvertToDealModal = false"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<FeatherIcon name="x" class="h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body-content>
|
|
||||||
<div class="mb-4 flex items-center gap-2 text-ink-gray-5">
|
|
||||||
<OrganizationsIcon class="h-4 w-4" />
|
|
||||||
<label class="block text-base">{{ __('Organization') }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="ml-6 text-ink-gray-9">
|
|
||||||
<div class="flex items-center justify-between text-base">
|
|
||||||
<div>{{ __('Choose Existing') }}</div>
|
|
||||||
<Switch v-model="existingOrganizationChecked" />
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
v-if="existingOrganizationChecked"
|
|
||||||
class="form-control mt-2.5"
|
|
||||||
size="md"
|
|
||||||
:value="existingOrganization"
|
|
||||||
doctype="CRM Organization"
|
|
||||||
@change="(data) => (existingOrganization = data)"
|
|
||||||
/>
|
|
||||||
<div v-else class="mt-2.5 text-base">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'New organization will be created based on the data in details section',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4 mt-6 flex items-center gap-2 text-ink-gray-5">
|
|
||||||
<ContactsIcon class="h-4 w-4" />
|
|
||||||
<label class="block text-base">{{ __('Contact') }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="ml-6 text-ink-gray-9">
|
|
||||||
<div class="flex items-center justify-between text-base">
|
|
||||||
<div>{{ __('Choose Existing') }}</div>
|
|
||||||
<Switch v-model="existingContactChecked" />
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
v-if="existingContactChecked"
|
|
||||||
class="form-control mt-2.5"
|
|
||||||
size="md"
|
|
||||||
:value="existingContact"
|
|
||||||
doctype="Contact"
|
|
||||||
@change="(data) => (existingContact = data)"
|
|
||||||
/>
|
|
||||||
<div v-else class="mt-2.5 text-base">
|
|
||||||
{{ __("New contact will be created based on the person's details") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="dealTabs.data?.length" class="h-px w-full border-t my-6" />
|
|
||||||
|
|
||||||
<FieldLayout
|
|
||||||
v-if="dealTabs.data?.length"
|
|
||||||
:tabs="dealTabs.data"
|
|
||||||
:data="deal"
|
|
||||||
doctype="CRM Deal"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
<FilesUploader
|
<FilesUploader
|
||||||
v-if="lead.data?.name"
|
v-if="lead.data?.name"
|
||||||
v-model="showFilesUploader"
|
v-model="showFilesUploader"
|
||||||
@ -354,40 +257,28 @@ import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
|||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||||
import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
||||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
|
||||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
|
||||||
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Activities from '@/components/Activities/Activities.vue'
|
import Activities from '@/components/Activities/Activities.vue'
|
||||||
import AssignTo from '@/components/AssignTo.vue'
|
import AssignTo from '@/components/AssignTo.vue'
|
||||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
|
||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
|
import ConvertToDealModal from '@/components/Modals/ConvertToDealModal.vue'
|
||||||
import {
|
import {
|
||||||
openWebsite,
|
openWebsite,
|
||||||
setupCustomizations,
|
setupCustomizations,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
validateIsImageFile,
|
validateIsImageFile,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { showQuickEntryModal, quickEntryProps } from '@/composables/modals'
|
|
||||||
import { getView } from '@/utils/view'
|
import { getView } from '@/utils/view'
|
||||||
import { getSettings } from '@/stores/settings'
|
import { getSettings } from '@/stores/settings'
|
||||||
import { sessionStore } from '@/stores/session'
|
|
||||||
import { usersStore } from '@/stores/users'
|
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { getMeta } from '@/stores/meta'
|
import { getMeta } from '@/stores/meta'
|
||||||
import { useDocument } from '@/data/document'
|
import { useDocument } from '@/data/document'
|
||||||
import {
|
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||||
whatsappEnabled,
|
|
||||||
callEnabled,
|
|
||||||
isMobileView,
|
|
||||||
} from '@/composables/settings'
|
|
||||||
import { capture } from '@/telemetry'
|
|
||||||
import {
|
import {
|
||||||
createResource,
|
createResource,
|
||||||
FileUploader,
|
FileUploader,
|
||||||
@ -395,26 +286,20 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Avatar,
|
Avatar,
|
||||||
Tabs,
|
Tabs,
|
||||||
Switch,
|
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
call,
|
call,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useActiveTabManager } from '@/composables/useActiveTabManager'
|
import { useActiveTabManager } from '@/composables/useActiveTabManager'
|
||||||
|
|
||||||
const { brand } = getSettings()
|
const { brand } = getSettings()
|
||||||
const { user } = sessionStore()
|
|
||||||
const { isManager } = usersStore()
|
|
||||||
const { $dialog, $socket, makeCall } = globalStore()
|
const { $dialog, $socket, makeCall } = globalStore()
|
||||||
const { statusOptions, getLeadStatus, getDealStatus } = statusesStore()
|
const { statusOptions, getLeadStatus } = statusesStore()
|
||||||
const { doctypeMeta } = getMeta('CRM Lead')
|
const { doctypeMeta } = getMeta('CRM Lead')
|
||||||
|
|
||||||
const { updateOnboardingStep } = useOnboarding('frappecrm')
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@ -428,6 +313,12 @@ const props = defineProps({
|
|||||||
const errorTitle = ref('')
|
const errorTitle = ref('')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const showDeleteLinkedDocModal = ref(false)
|
const showDeleteLinkedDocModal = ref(false)
|
||||||
|
const showConvertToDealModal = ref(false)
|
||||||
|
|
||||||
|
const { triggerOnChange, assignees, document } = useDocument(
|
||||||
|
'CRM Lead',
|
||||||
|
props.leadId,
|
||||||
|
)
|
||||||
|
|
||||||
const lead = createResource({
|
const lead = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
||||||
@ -630,64 +521,6 @@ async function deleteLeadWithModal(name) {
|
|||||||
showDeleteLinkedDocModal.value = true
|
showDeleteLinkedDocModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to Deal
|
|
||||||
const showConvertToDealModal = ref(false)
|
|
||||||
const existingContactChecked = ref(false)
|
|
||||||
const existingOrganizationChecked = ref(false)
|
|
||||||
|
|
||||||
const existingContact = ref('')
|
|
||||||
const existingOrganization = ref('')
|
|
||||||
|
|
||||||
const { triggerConvertToDeal, triggerOnChange, assignees, document } =
|
|
||||||
useDocument('CRM Lead', props.leadId)
|
|
||||||
|
|
||||||
async function convertToDeal() {
|
|
||||||
if (existingContactChecked.value && !existingContact.value) {
|
|
||||||
toast.error(__('Please select an existing contact'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingOrganizationChecked.value && !existingOrganization.value) {
|
|
||||||
toast.error(__('Please select an existing organization'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingContactChecked.value && existingContact.value) {
|
|
||||||
existingContact.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingOrganizationChecked.value && existingOrganization.value) {
|
|
||||||
existingOrganization.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
await triggerConvertToDeal?.(
|
|
||||||
lead.data,
|
|
||||||
deal,
|
|
||||||
() => (showConvertToDealModal.value = false),
|
|
||||||
)
|
|
||||||
|
|
||||||
let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
|
||||||
lead: lead.data.name,
|
|
||||||
deal,
|
|
||||||
existing_contact: existingContact.value,
|
|
||||||
existing_organization: existingOrganization.value,
|
|
||||||
}).catch((err) => {
|
|
||||||
toast.error(__('Error converting to deal: {0}', [err.messages?.[0]]))
|
|
||||||
})
|
|
||||||
if (_deal) {
|
|
||||||
showConvertToDealModal.value = false
|
|
||||||
existingContactChecked.value = false
|
|
||||||
existingOrganizationChecked.value = false
|
|
||||||
existingContact.value = ''
|
|
||||||
existingOrganization.value = ''
|
|
||||||
updateOnboardingStep('convert_lead_to_deal', true, false, () => {
|
|
||||||
localStorage.setItem('firstDeal' + user, _deal)
|
|
||||||
})
|
|
||||||
capture('convert_lead_to_deal')
|
|
||||||
router.push({ name: 'Deal', params: { dealId: _deal } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const activities = ref(null)
|
const activities = ref(null)
|
||||||
|
|
||||||
function openEmailBox() {
|
function openEmailBox() {
|
||||||
@ -698,54 +531,6 @@ function openEmailBox() {
|
|||||||
nextTick(() => (activities.value.emailBox.show = true))
|
nextTick(() => (activities.value.emailBox.show = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
const deal = reactive({})
|
|
||||||
|
|
||||||
const dealStatuses = computed(() => {
|
|
||||||
let statuses = statusOptions('deal')
|
|
||||||
if (!deal.status) {
|
|
||||||
deal.status = statuses[0].value
|
|
||||||
}
|
|
||||||
return statuses
|
|
||||||
})
|
|
||||||
|
|
||||||
const dealTabs = createResource({
|
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
|
||||||
cache: ['RequiredFields', 'CRM Deal'],
|
|
||||||
params: { doctype: 'CRM Deal', type: 'Required Fields' },
|
|
||||||
auto: true,
|
|
||||||
transform: (_tabs) => {
|
|
||||||
let hasFields = false
|
|
||||||
let parsedTabs = _tabs?.forEach((tab) => {
|
|
||||||
tab.sections?.forEach((section) => {
|
|
||||||
section.columns?.forEach((column) => {
|
|
||||||
column.fields?.forEach((field) => {
|
|
||||||
hasFields = true
|
|
||||||
if (field.fieldname == 'status') {
|
|
||||||
field.fieldtype = 'Select'
|
|
||||||
field.options = dealStatuses.value
|
|
||||||
field.prefix = getDealStatus(deal.status).color
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.fieldtype === 'Table') {
|
|
||||||
deal[field.fieldname] = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return hasFields ? parsedTabs : []
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function openQuickEntryModal() {
|
|
||||||
showQuickEntryModal.value = true
|
|
||||||
quickEntryProps.value = {
|
|
||||||
doctype: 'CRM Deal',
|
|
||||||
onlyRequired: true,
|
|
||||||
}
|
|
||||||
showConvertToDealModal.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadAssignees(data) {
|
function reloadAssignees(data) {
|
||||||
if (data?.hasOwnProperty('lead_owner')) {
|
if (data?.hasOwnProperty('lead_owner')) {
|
||||||
assignees.reload()
|
assignees.reload()
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export function prettyDate(date, mini = false) {
|
|||||||
let nowDatetime = dayjs().tz(localTimezone || systemTimezone)
|
let nowDatetime = dayjs().tz(localTimezone || systemTimezone)
|
||||||
let diff = nowDatetime.diff(date, 'seconds')
|
let diff = nowDatetime.diff(date, 'seconds')
|
||||||
|
|
||||||
let dayDiff = Math.floor(diff / 86400)
|
let dayDiff = diff / 86400
|
||||||
|
|
||||||
if (isNaN(dayDiff)) return ''
|
if (isNaN(dayDiff)) return ''
|
||||||
|
|
||||||
@ -93,15 +93,15 @@ export function prettyDate(date, mini = false) {
|
|||||||
// Return short format of time difference
|
// Return short format of time difference
|
||||||
if (dayDiff < 0) {
|
if (dayDiff < 0) {
|
||||||
if (Math.abs(dayDiff) < 1) {
|
if (Math.abs(dayDiff) < 1) {
|
||||||
if (diff < 60) {
|
if (Math.abs(diff) < 60) {
|
||||||
return __('now')
|
return __('now')
|
||||||
} else if (diff < 3600) {
|
} else if (Math.abs(diff) < 3600) {
|
||||||
return __('in {0} m', [Math.floor(diff / 60)])
|
return __('in {0} m', [Math.floor(Math.abs(diff) / 60)])
|
||||||
} else if (diff < 86400) {
|
} else if (Math.abs(diff) < 86400) {
|
||||||
return __('in {0} h', [Math.floor(diff / 3600)])
|
return __('in {0} h', [Math.floor(Math.abs(diff) / 3600)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Math.abs(dayDiff) == 1) {
|
if (Math.abs(dayDiff) >= 1 && Math.abs(dayDiff) < 1.5) {
|
||||||
return __('tomorrow')
|
return __('tomorrow')
|
||||||
} else if (Math.abs(dayDiff) < 7) {
|
} else if (Math.abs(dayDiff) < 7) {
|
||||||
return __('in {0} d', [Math.abs(dayDiff)])
|
return __('in {0} d', [Math.abs(dayDiff)])
|
||||||
@ -112,7 +112,7 @@ export function prettyDate(date, mini = false) {
|
|||||||
} else {
|
} else {
|
||||||
return __('in {0} y', [Math.floor(Math.abs(dayDiff) / 365)])
|
return __('in {0} y', [Math.floor(Math.abs(dayDiff) / 365)])
|
||||||
}
|
}
|
||||||
} else if (dayDiff == 0) {
|
} else if (dayDiff >= 0 && dayDiff < 1) {
|
||||||
if (diff < 60) {
|
if (diff < 60) {
|
||||||
return __('now')
|
return __('now')
|
||||||
} else if (diff < 3600) {
|
} else if (diff < 3600) {
|
||||||
@ -135,19 +135,19 @@ export function prettyDate(date, mini = false) {
|
|||||||
// Return long format of time difference
|
// Return long format of time difference
|
||||||
if (dayDiff < 0) {
|
if (dayDiff < 0) {
|
||||||
if (Math.abs(dayDiff) < 1) {
|
if (Math.abs(dayDiff) < 1) {
|
||||||
if (diff < 60) {
|
if (Math.abs(diff) < 60) {
|
||||||
return __('just now')
|
return __('just now')
|
||||||
} else if (diff < 120) {
|
} else if (Math.abs(diff) < 120) {
|
||||||
return __('in 1 minute')
|
return __('in 1 minute')
|
||||||
} else if (diff < 3600) {
|
} else if (Math.abs(diff) < 3600) {
|
||||||
return __('in {0} minutes', [Math.floor(diff / 60)])
|
return __('in {0} minutes', [Math.floor(Math.abs(diff) / 60)])
|
||||||
} else if (diff < 7200) {
|
} else if (Math.abs(diff) < 7200) {
|
||||||
return __('in 1 hour')
|
return __('in 1 hour')
|
||||||
} else if (diff < 86400) {
|
} else if (Math.abs(diff) < 86400) {
|
||||||
return __('in {0} hours', [Math.floor(diff / 3600)])
|
return __('in {0} hours', [Math.floor(Math.abs(diff) / 3600)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Math.abs(dayDiff) == 1) {
|
if (Math.abs(dayDiff) >= 1 && Math.abs(dayDiff) < 1.5) {
|
||||||
return __('tomorrow')
|
return __('tomorrow')
|
||||||
} else if (Math.abs(dayDiff) < 7) {
|
} else if (Math.abs(dayDiff) < 7) {
|
||||||
return __('in {0} days', [Math.abs(dayDiff)])
|
return __('in {0} days', [Math.abs(dayDiff)])
|
||||||
@ -160,7 +160,7 @@ export function prettyDate(date, mini = false) {
|
|||||||
} else {
|
} else {
|
||||||
return __('in {0} years', [Math.floor(Math.abs(dayDiff) / 365)])
|
return __('in {0} years', [Math.floor(Math.abs(dayDiff) / 365)])
|
||||||
}
|
}
|
||||||
} else if (dayDiff == 0) {
|
} else if (dayDiff >= 0 && dayDiff < 1) {
|
||||||
if (diff < 60) {
|
if (diff < 60) {
|
||||||
return __('just now')
|
return __('just now')
|
||||||
} else if (diff < 120) {
|
} else if (diff < 120) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user