Merge remote-tracking branch 'origin/develop' into delete-from-record-view
This commit is contained in:
commit
df698387dc
@ -124,6 +124,7 @@ def get_deal_activities(name):
|
|||||||
activity = {
|
activity = {
|
||||||
"activity_type": "communication",
|
"activity_type": "communication",
|
||||||
"communication_type": communication.communication_type,
|
"communication_type": communication.communication_type,
|
||||||
|
"communication_date": communication.communication_date or communication.creation,
|
||||||
"creation": communication.creation,
|
"creation": communication.creation,
|
||||||
"data": {
|
"data": {
|
||||||
"subject": communication.subject,
|
"subject": communication.subject,
|
||||||
@ -255,6 +256,7 @@ def get_lead_activities(name):
|
|||||||
activity = {
|
activity = {
|
||||||
"activity_type": "communication",
|
"activity_type": "communication",
|
||||||
"communication_type": communication.communication_type,
|
"communication_type": communication.communication_type,
|
||||||
|
"communication_date": communication.communication_date or communication.creation,
|
||||||
"creation": communication.creation,
|
"creation": communication.creation,
|
||||||
"data": {
|
"data": {
|
||||||
"subject": communication.subject,
|
"subject": communication.subject,
|
||||||
|
|||||||
@ -190,11 +190,20 @@ def get_call_log(name):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_lead_from_call_log(call_log):
|
def create_lead_from_call_log(call_log, lead_details=None):
|
||||||
lead = frappe.new_doc("CRM Lead")
|
lead = frappe.new_doc("CRM Lead")
|
||||||
lead.first_name = "Lead from call " + call_log.get("from")
|
lead_details = frappe.parse_json(lead_details or "{}")
|
||||||
lead.mobile_no = call_log.get("from")
|
|
||||||
lead.lead_owner = frappe.session.user
|
if not lead_details.get("lead_owner"):
|
||||||
|
lead_details["lead_owner"] = frappe.session.user
|
||||||
|
if not lead_details.get("mobile_no"):
|
||||||
|
lead_details["mobile_no"] = call_log.get("from") or ""
|
||||||
|
if not lead_details.get("first_name"):
|
||||||
|
lead_details["first_name"] = "Lead from call " + (
|
||||||
|
lead_details.get("mobile_no") or call_log.get("name")
|
||||||
|
)
|
||||||
|
|
||||||
|
lead.update(lead_details)
|
||||||
lead.save(ignore_permissions=True)
|
lead.save(ignore_permissions=True)
|
||||||
|
|
||||||
# link call log with lead
|
# link call log with lead
|
||||||
|
|||||||
@ -24,9 +24,15 @@ class FCRMSettings(Document):
|
|||||||
standard_old_items = [d.name1 for d in old_items if d.is_standard]
|
standard_old_items = [d.name1 for d in old_items if d.is_standard]
|
||||||
deleted_standard_items = set(standard_old_items) - set(standard_new_items)
|
deleted_standard_items = set(standard_old_items) - set(standard_new_items)
|
||||||
if deleted_standard_items:
|
if deleted_standard_items:
|
||||||
|
standard_dropdown_items = get_standard_dropdown_items()
|
||||||
|
if not deleted_standard_items.intersection(standard_dropdown_items):
|
||||||
|
return
|
||||||
frappe.throw(_("Cannot delete standard items {0}").format(", ".join(deleted_standard_items)))
|
frappe.throw(_("Cannot delete standard items {0}").format(", ".join(deleted_standard_items)))
|
||||||
|
|
||||||
|
|
||||||
|
def get_standard_dropdown_items():
|
||||||
|
return [item.get("name1") for item in frappe.get_hooks("standard_dropdown_items")]
|
||||||
|
|
||||||
|
|
||||||
def after_migrate():
|
def after_migrate():
|
||||||
sync_table("dropdown_items", "standard_dropdown_items")
|
sync_table("dropdown_items", "standard_dropdown_items")
|
||||||
|
|||||||
24
crm/hooks.py
24
crm/hooks.py
@ -264,22 +264,6 @@ standard_dropdown_items = [
|
|||||||
"route": "#",
|
"route": "#",
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name1": "support_link",
|
|
||||||
"label": "Support",
|
|
||||||
"type": "Route",
|
|
||||||
"icon": "life-buoy",
|
|
||||||
"route": "https://t.me/frappecrm",
|
|
||||||
"is_standard": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name1": "docs_link",
|
|
||||||
"label": "Docs",
|
|
||||||
"type": "Route",
|
|
||||||
"icon": "book-open",
|
|
||||||
"route": "https://docs.frappe.io/crm",
|
|
||||||
"is_standard": 1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name1": "toggle_theme",
|
"name1": "toggle_theme",
|
||||||
"label": "Toggle theme",
|
"label": "Toggle theme",
|
||||||
@ -303,6 +287,14 @@ standard_dropdown_items = [
|
|||||||
"route": "#",
|
"route": "#",
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name1": "about",
|
||||||
|
"label": "About",
|
||||||
|
"type": "Route",
|
||||||
|
"icon": "info",
|
||||||
|
"route": "#",
|
||||||
|
"is_standard": 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name1": "separator",
|
"name1": "separator",
|
||||||
"label": "",
|
"label": "",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,10 @@
|
|||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# GNU GPLv3 License. See license.txt
|
# GNU GPLv3 License. See license.txt
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import safe_decode
|
||||||
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
|
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
|
||||||
from frappe.utils import cint, get_system_timezone
|
from frappe.utils import cint, get_system_timezone
|
||||||
from frappe.utils.telemetry import capture
|
from frappe.utils.telemetry import capture
|
||||||
@ -43,9 +45,41 @@ def get_boot():
|
|||||||
"user": frappe.db.get_value("User", frappe.session.user, "time_zone")
|
"user": frappe.db.get_value("User", frappe.session.user, "time_zone")
|
||||||
or get_system_timezone(),
|
or get_system_timezone(),
|
||||||
},
|
},
|
||||||
|
"app_version": get_app_version(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_default_route():
|
def get_default_route():
|
||||||
return "/crm"
|
return "/crm"
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_version():
|
||||||
|
app = "crm"
|
||||||
|
branch = run_git_command(f"cd ../apps/{app} && git rev-parse --abbrev-ref HEAD")
|
||||||
|
commit = run_git_command(f"git -C ../apps/{app} rev-parse --short=7 HEAD")
|
||||||
|
tag = run_git_command(f"git -C ../apps/{app} describe --tags --abbrev=0")
|
||||||
|
dirty = run_git_command(f"git -C ../apps/{app} diff --quiet || echo 'dirty'") == "dirty"
|
||||||
|
commit_date = run_git_command(f"git -C ../apps/{app} log -1 --format=%cd")
|
||||||
|
commit_message = run_git_command(f"git -C ../apps/{app} log -1 --pretty=%B")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"branch": branch,
|
||||||
|
"commit": commit,
|
||||||
|
"commit_date": commit_date,
|
||||||
|
"commit_message": commit_message,
|
||||||
|
"tag": tag,
|
||||||
|
"dirty": dirty,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_git_command(command):
|
||||||
|
try:
|
||||||
|
with open(os.devnull, "wb") as null_stream:
|
||||||
|
result = subprocess.check_output(command, shell=True, stdin=null_stream, stderr=null_stream)
|
||||||
|
return safe_decode(result).strip()
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(
|
||||||
|
title="Git Command Error",
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|||||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -158,6 +158,7 @@ declare module 'vue' {
|
|||||||
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
||||||
LucideInfo: typeof import('~icons/lucide/info')['default']
|
LucideInfo: typeof import('~icons/lucide/info')['default']
|
||||||
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
||||||
|
LucideSearch: typeof import('~icons/lucide/search')['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']
|
||||||
|
|||||||
@ -22,9 +22,9 @@
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
:theme="status.color"
|
:theme="status.color"
|
||||||
/>
|
/>
|
||||||
<Tooltip :text="formatDate(activity.creation)">
|
<Tooltip :text="formatDate(activity.communication_date)">
|
||||||
<div class="text-sm text-ink-gray-5">
|
<div class="text-sm text-ink-gray-5">
|
||||||
{{ __(timeAgo(activity.creation)) }}
|
{{ __(timeAgo(activity.communication_date)) }}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="flex gap-0.5">
|
<div class="flex gap-0.5">
|
||||||
|
|||||||
@ -64,7 +64,10 @@ const emit = defineEmits(['change'])
|
|||||||
|
|
||||||
const { getFields } = getMeta(props.doctype)
|
const { getFields } = getMeta(props.doctype)
|
||||||
|
|
||||||
const values = defineModel()
|
const values = defineModel({
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
})
|
||||||
|
|
||||||
const valuesRef = ref([])
|
const valuesRef = ref([])
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|||||||
@ -206,7 +206,7 @@
|
|||||||
v-else
|
v-else
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:value="data[field.fieldname]"
|
:value="getDataValue(data[field.fieldname], field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
:description="field.description"
|
:description="field.description"
|
||||||
@change="fieldChange($event.target.value, field)"
|
@change="fieldChange($event.target.value, field)"
|
||||||
@ -340,6 +340,13 @@ function fieldChange(value, df) {
|
|||||||
triggerOnChange(df.fieldname)
|
triggerOnChange(df.fieldname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDataValue(value, field) {
|
||||||
|
if (field.fieldtype === 'Duration') {
|
||||||
|
return value || 0
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.form-control.prefix select) {
|
:deep(.form-control.prefix select) {
|
||||||
|
|||||||
@ -126,7 +126,7 @@
|
|||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
value=""
|
value=""
|
||||||
:options="filterableFields.data"
|
:options="availableFilters"
|
||||||
@change="(e) => setfilter(e)"
|
@change="(e) => setfilter(e)"
|
||||||
:placeholder="__('First name')"
|
:placeholder="__('First name')"
|
||||||
>
|
>
|
||||||
@ -217,6 +217,19 @@ const filters = computed(() => {
|
|||||||
return convertFilters(filterableFields.data, allFilters)
|
return convertFilters(filterableFields.data, allFilters)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const availableFilters = computed(() => {
|
||||||
|
if (!filterableFields.data) return []
|
||||||
|
|
||||||
|
const selectedFieldNames = new Set()
|
||||||
|
for (const filter of filters.value) {
|
||||||
|
selectedFieldNames.add(filter.fieldname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterableFields.data.filter(
|
||||||
|
(field) => !selectedFieldNames.has(field.fieldname),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
function removeCommonFilters(commonFilters, allFilters) {
|
function removeCommonFilters(commonFilters, allFilters) {
|
||||||
for (const key in commonFilters) {
|
for (const key in commonFilters) {
|
||||||
if (commonFilters.hasOwnProperty(key) && allFilters.hasOwnProperty(key)) {
|
if (commonFilters.hasOwnProperty(key) && allFilters.hasOwnProperty(key)) {
|
||||||
|
|||||||
21
frontend/src/components/Icons/TelegramIcon.vue
Normal file
21
frontend/src/components/Icons/TelegramIcon.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="SVGRepo_tracerCarrier"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<path
|
||||||
|
d="M26.67,38.57l-.82,11.54A2.88,2.88,0,0,0,28.14,49l5.5-5.26,11.42,8.35c2.08,1.17,3.55.56,4.12-1.92l7.49-35.12h0c.66-3.09-1.08-4.33-3.16-3.55l-44,16.85C6.47,29.55,6.54,31.23,9,32l11.26,3.5L45.59,20.71c1.23-.83,2.36-.37,1.44.44Z"
|
||||||
|
stroke-linecap="round"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
100
frontend/src/components/Modals/AboutModal.vue
Normal file
100
frontend/src/components/Modals/AboutModal.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model="show" :options="{ size: 'sm' }">
|
||||||
|
<template #body>
|
||||||
|
<div class="p-4 pt-5">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<CRMLogo class="mb-3 size-12" />
|
||||||
|
<h3 class="font-semibold text-xl text-ink-gray-9">Frappe CRM</h3>
|
||||||
|
<div class="flex items-center mt-1">
|
||||||
|
<div class="text-base text-ink-gray-6">
|
||||||
|
{{ appVersion.branch != 'main' ? appVersion.branch : '' }}
|
||||||
|
<template v-if="appVersion.branch != 'main'">
|
||||||
|
({{ appVersion.commit }})
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ appVersion.tag }}</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
:text="`${appVersion.commit_message} - ${appVersion.commit_date}`"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<LucideInfo class="size-3.5 text-ink-gray-8 ml-1" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="border-t my-3 mx-2" />
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
v-for="link in links"
|
||||||
|
:key="link.label"
|
||||||
|
class="flex py-2 px-2 hover:bg-surface-gray-1 rounded cursor-pointer"
|
||||||
|
target="_blank"
|
||||||
|
:href="link.url"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-if="link.icon"
|
||||||
|
:is="link.icon"
|
||||||
|
class="size-4 mr-2 text-ink-gray-7"
|
||||||
|
/>
|
||||||
|
<span class="text-base text-ink-gray-8">
|
||||||
|
{{ link.label }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<hr class="border-t my-3 mx-2" />
|
||||||
|
<p class="text-sm text-ink-gray-6 px-2 mt-2">
|
||||||
|
© Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Tooltip } from 'frappe-ui'
|
||||||
|
import CRMLogo from '@/components/Icons/CRMLogo.vue'
|
||||||
|
import LucideGlobe from '~icons/lucide/globe'
|
||||||
|
import LucideGitHub from '~icons/lucide/github'
|
||||||
|
import LucideHeadset from '~icons/lucide/headset'
|
||||||
|
import LucideBug from '~icons/lucide/bug'
|
||||||
|
import LucideBookOpen from '~icons/lucide/book-open'
|
||||||
|
import TelegramIcon from '@/components/Icons/TelegramIcon.vue'
|
||||||
|
|
||||||
|
let show = defineModel()
|
||||||
|
|
||||||
|
let links = [
|
||||||
|
{
|
||||||
|
label: __('Website'),
|
||||||
|
url: 'https://frappe.io/crm',
|
||||||
|
icon: LucideGlobe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('GitHub Repository'),
|
||||||
|
url: 'https://github.com/frappe/crm',
|
||||||
|
icon: LucideGitHub,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Documentation'),
|
||||||
|
url: 'https://docs.frappe.io/crm',
|
||||||
|
icon: LucideBookOpen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Telegram Channel'),
|
||||||
|
url: 'https://t.me/frappecrm',
|
||||||
|
icon: TelegramIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Report an Issue'),
|
||||||
|
url: 'https://github.com/frappe/crm/issues',
|
||||||
|
icon: LucideBug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Contact Support'),
|
||||||
|
url: 'https://support.frappe.io',
|
||||||
|
icon: LucideHeadset,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
let appVersion = window.app_version
|
||||||
|
</script>
|
||||||
@ -172,8 +172,9 @@ import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
|
|||||||
import { getCallLogDetail } from '@/utils/callLog'
|
import { getCallLogDetail } from '@/utils/callLog'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { isMobileView } from '@/composables/settings'
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
import { FeatherIcon, Dropdown, Avatar, Tooltip, call } from 'frappe-ui'
|
import { FeatherIcon, Dropdown, Avatar, Tooltip, call } from 'frappe-ui'
|
||||||
import { ref, computed, h, nextTick } from 'vue'
|
import { ref, computed, h, nextTick, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { isManager } = usersStore()
|
const { isManager } = usersStore()
|
||||||
@ -289,9 +290,19 @@ const detailFields = computed(() => {
|
|||||||
.filter((detail) => (detail.condition ? detail.condition() : true))
|
.filter((detail) => (detail.condition ? detail.condition() : true))
|
||||||
})
|
})
|
||||||
|
|
||||||
function createLead() {
|
const d = ref({})
|
||||||
|
const leadDetails = ref({})
|
||||||
|
|
||||||
|
async function createLead() {
|
||||||
|
await d.value.triggerOnCreateLead?.(
|
||||||
|
callLog.value?.data,
|
||||||
|
leadDetails.value,
|
||||||
|
() => (show.value = false),
|
||||||
|
)
|
||||||
|
|
||||||
call('crm.fcrm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', {
|
call('crm.fcrm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', {
|
||||||
call_log: callLog.value?.data,
|
call_log: callLog.value?.data,
|
||||||
|
lead_details: leadDetails.value,
|
||||||
}).then((d) => {
|
}).then((d) => {
|
||||||
if (d) {
|
if (d) {
|
||||||
router.push({ name: 'Lead', params: { leadId: d } })
|
router.push({ name: 'Lead', params: { leadId: d } })
|
||||||
@ -351,6 +362,14 @@ async function addTaskToCallLog(_task, insert_mode = false) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => callLog.value?.data?.name,
|
||||||
|
(value) => {
|
||||||
|
if (!value) return
|
||||||
|
d.value = useDocument('CRM Call Log', value)
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -12,16 +12,19 @@
|
|||||||
v-model="showQuickEntryModal"
|
v-model="showQuickEntryModal"
|
||||||
:doctype="quickEntryDoctype"
|
:doctype="quickEntryDoctype"
|
||||||
/>
|
/>
|
||||||
|
<AboutModal v-model="showAboutModal" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import CreateDocumentModal from '@/components/Modals/CreateDocumentModal.vue'
|
import CreateDocumentModal from '@/components/Modals/CreateDocumentModal.vue'
|
||||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||||
|
import AboutModal from '@/components/Modals/AboutModal.vue'
|
||||||
import {
|
import {
|
||||||
showCreateDocumentModal,
|
showCreateDocumentModal,
|
||||||
createDocumentDoctype,
|
createDocumentDoctype,
|
||||||
createDocumentData,
|
createDocumentData,
|
||||||
createDocumentCallback,
|
createDocumentCallback,
|
||||||
} from '@/composables/document'
|
} from '@/composables/document'
|
||||||
|
import { showAboutModal } from '@/composables/settings'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const showQuickEntryModal = ref(false)
|
const showQuickEntryModal = ref(false)
|
||||||
|
|||||||
@ -55,7 +55,11 @@ import Apps from '@/components/Apps.vue'
|
|||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { getSettings } from '@/stores/settings'
|
import { getSettings } from '@/stores/settings'
|
||||||
import { showSettings, isMobileView } from '@/composables/settings'
|
import {
|
||||||
|
showSettings,
|
||||||
|
isMobileView,
|
||||||
|
showAboutModal,
|
||||||
|
} from '@/composables/settings'
|
||||||
import { confirmLoginToFrappeCloud } from '@/composables/frappecloud'
|
import { confirmLoginToFrappeCloud } from '@/composables/frappecloud'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
import { theme, toggleTheme } from '@/stores/theme'
|
import { theme, toggleTheme } from '@/stores/theme'
|
||||||
@ -131,20 +135,6 @@ function getStandardItem(item) {
|
|||||||
return {
|
return {
|
||||||
component: markRaw(Apps),
|
component: markRaw(Apps),
|
||||||
}
|
}
|
||||||
case 'support_link':
|
|
||||||
return {
|
|
||||||
icon: item.icon,
|
|
||||||
label: __(item.label),
|
|
||||||
onClick: () =>
|
|
||||||
window.open(item.route, item.open_in_new_window ? '_blank' : ''),
|
|
||||||
}
|
|
||||||
case 'docs_link':
|
|
||||||
return {
|
|
||||||
icon: item.icon,
|
|
||||||
label: __(item.label),
|
|
||||||
onClick: () =>
|
|
||||||
window.open(item.route, item.open_in_new_window ? '_blank' : ''),
|
|
||||||
}
|
|
||||||
case 'toggle_theme':
|
case 'toggle_theme':
|
||||||
return {
|
return {
|
||||||
icon: theme.value === 'dark' ? 'sun' : item.icon,
|
icon: theme.value === 'dark' ? 'sun' : item.icon,
|
||||||
@ -165,6 +155,12 @@ function getStandardItem(item) {
|
|||||||
onClick: () => confirmLoginToFrappeCloud(),
|
onClick: () => confirmLoginToFrappeCloud(),
|
||||||
condition: () => !isMobileView.value && window.is_fc_site,
|
condition: () => !isMobileView.value && window.is_fc_site,
|
||||||
}
|
}
|
||||||
|
case 'about':
|
||||||
|
return {
|
||||||
|
icon: item.icon,
|
||||||
|
label: __(item.label),
|
||||||
|
onClick: () => (showAboutModal.value = true),
|
||||||
|
}
|
||||||
case 'logout':
|
case 'logout':
|
||||||
return {
|
return {
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
|
|||||||
@ -566,7 +566,7 @@ async function exportRows() {
|
|||||||
page_length = list.value.data.total_count
|
page_length = list.value.data.total_count
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = `/api/method/frappe.desk.reportview.export_query?file_format_type=${export_type.value}&title=${props.doctype}&doctype=${props.doctype}&fields=${fields}&filters=${filters}&order_by=${order_by}&page_length=${page_length}&start=0&view=Report&with_comment_count=1`
|
let url = `/api/method/frappe.desk.reportview.export_query?file_format_type=${export_type.value}&title=${props.doctype}&doctype=${props.doctype}&fields=${fields}&filters=${encodeURIComponent(filters)}&order_by=${order_by}&page_length=${page_length}&start=0&view=Report&with_comment_count=1`
|
||||||
|
|
||||||
// Add selected items parameter if rows are selected
|
// Add selected items parameter if rows are selected
|
||||||
if (selectedRows.value?.length && !export_all.value) {
|
if (selectedRows.value?.length && !export_all.value) {
|
||||||
@ -752,6 +752,7 @@ const quickFilterOptions = computed(() => {
|
|||||||
let fields = getFields()
|
let fields = getFields()
|
||||||
if (!fields) return []
|
if (!fields) return []
|
||||||
|
|
||||||
|
let existingQuickFilters = newQuickFilters.value.map((f) => f.fieldname)
|
||||||
let restrictedFieldtypes = [
|
let restrictedFieldtypes = [
|
||||||
'Tab Break',
|
'Tab Break',
|
||||||
'Section Break',
|
'Section Break',
|
||||||
@ -766,6 +767,7 @@ const quickFilterOptions = computed(() => {
|
|||||||
]
|
]
|
||||||
let options = fields
|
let options = fields
|
||||||
.filter((f) => f.label && !restrictedFieldtypes.includes(f.fieldtype))
|
.filter((f) => f.label && !restrictedFieldtypes.includes(f.fieldtype))
|
||||||
|
.filter((f) => !existingQuickFilters.includes(f.fieldname))
|
||||||
.map((field) => ({
|
.map((field) => ({
|
||||||
label: field.label,
|
label: field.label,
|
||||||
value: field.fieldname,
|
value: field.fieldname,
|
||||||
|
|||||||
@ -42,3 +42,5 @@ export const isMobileView = computed(() => window.innerWidth < 768)
|
|||||||
|
|
||||||
export const showSettings = ref(false)
|
export const showSettings = ref(false)
|
||||||
export const activeSettingsPage = ref('')
|
export const activeSettingsPage = ref('')
|
||||||
|
|
||||||
|
export const showAboutModal = ref(false)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export function useDocument(doctype, docname) {
|
|||||||
documentsCache[doctype][docname] = createDocumentResource({
|
documentsCache[doctype][docname] = createDocumentResource({
|
||||||
doctype: doctype,
|
doctype: doctype,
|
||||||
name: docname,
|
name: docname,
|
||||||
onSuccess: () => setupFormScript(),
|
onSuccess: async () => await setupFormScript(),
|
||||||
setValue: {
|
setValue: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(__('Document updated successfully'))
|
toast.success(__('Document updated successfully'))
|
||||||
@ -27,23 +27,49 @@ export function useDocument(doctype, docname) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupFormScript() {
|
async function setupFormScript() {
|
||||||
if (controllersCache[doctype]?.[docname]) return
|
if (
|
||||||
|
controllersCache[doctype] &&
|
||||||
|
typeof controllersCache[doctype][docname] === 'object'
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!controllersCache[doctype]) {
|
if (!controllersCache[doctype]) {
|
||||||
controllersCache[doctype] = {}
|
controllersCache[doctype] = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
controllersCache[doctype][docname] = setupScript(
|
controllersCache[doctype][docname] = {}
|
||||||
documentsCache[doctype][docname],
|
|
||||||
)
|
const controllersArray = await setupScript(documentsCache[doctype][docname])
|
||||||
|
|
||||||
|
if (!controllersArray || controllersArray.length === 0) return
|
||||||
|
|
||||||
|
const organizedControllers = {}
|
||||||
|
for (const controller of controllersArray) {
|
||||||
|
const controllerKey = controller.constructor.name // e.g., "CRMLead", "CRMProducts"
|
||||||
|
if (!organizedControllers[controllerKey]) {
|
||||||
|
organizedControllers[controllerKey] = []
|
||||||
|
}
|
||||||
|
organizedControllers[controllerKey].push(controller)
|
||||||
|
}
|
||||||
|
controllersCache[doctype][docname] = organizedControllers
|
||||||
}
|
}
|
||||||
|
|
||||||
function getControllers(row = null) {
|
function getControllers(row = null) {
|
||||||
const _doctype = row?.doctype || doctype
|
const _doctype = row?.doctype || doctype
|
||||||
return (controllersCache[doctype]?.[docname] || []).filter(
|
const controllerKey = _doctype.replace(/\s+/g, '')
|
||||||
(c) => c.constructor.name === _doctype.replace(/\s+/g, ''),
|
|
||||||
)
|
const docControllers = controllersCache[doctype]?.[docname]
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof docControllers === 'object' &&
|
||||||
|
docControllers !== null &&
|
||||||
|
!Array.isArray(docControllers)
|
||||||
|
) {
|
||||||
|
return docControllers[controllerKey] || []
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
async function triggerOnRefresh() {
|
async function triggerOnRefresh() {
|
||||||
@ -97,6 +123,22 @@ export function useDocument(doctype, docname) {
|
|||||||
await trigger(handler, rows[0])
|
await trigger(handler, rows[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function triggerOnCreateLead() {
|
||||||
|
const args = Array.from(arguments)
|
||||||
|
const handler = async function () {
|
||||||
|
await this.on_create_lead?.(...args)
|
||||||
|
}
|
||||||
|
await trigger(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerConvertToDeal() {
|
||||||
|
const args = Array.from(arguments)
|
||||||
|
const handler = async function () {
|
||||||
|
await this.convert_to_deal?.(...args)
|
||||||
|
}
|
||||||
|
await trigger(handler)
|
||||||
|
}
|
||||||
|
|
||||||
async function trigger(taskFn, row = null) {
|
async function trigger(taskFn, row = null) {
|
||||||
const controllers = getControllers(row)
|
const controllers = getControllers(row)
|
||||||
if (!controllers.length) return
|
if (!controllers.length) return
|
||||||
@ -130,5 +172,7 @@ export function useDocument(doctype, docname) {
|
|||||||
triggerOnRowRemove,
|
triggerOnRowRemove,
|
||||||
triggerOnRefresh,
|
triggerOnRefresh,
|
||||||
setupFormScript,
|
setupFormScript,
|
||||||
|
triggerOnCreateLead,
|
||||||
|
triggerConvertToDeal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,15 +20,23 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
doctypeScripts[doctype][script.name] = script || {}
|
doctypeScripts[doctype][script.name] = script || {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(
|
||||||
|
`Error loading CRM Form Scripts for ${doctype} (view: ${view}):`,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!doctypeScripts[doctype] && !scripts.loading) {
|
if (!doctypeScripts[doctype] && !scripts.loading) {
|
||||||
scripts.fetch()
|
scripts.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupScript(document, helpers = {}) {
|
async function setupScript(document, helpers = {}) {
|
||||||
let scripts = doctypeScripts[doctype]
|
await scripts.promise
|
||||||
if (!scripts) return null
|
|
||||||
|
let scriptDefs = doctypeScripts[doctype]
|
||||||
|
if (!scriptDefs || Object.keys(scriptDefs).length === 0) return null
|
||||||
|
|
||||||
const { $dialog, $socket, makeCall } = globalStore()
|
const { $dialog, $socket, makeCall } = globalStore()
|
||||||
|
|
||||||
@ -42,7 +50,7 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
makePhoneCall: makeCall,
|
makePhoneCall: makeCall,
|
||||||
}
|
}
|
||||||
|
|
||||||
return setupMultipleFormControllers(scripts, document, helpers)
|
return setupMultipleFormControllers(scriptDefs, document, helpers)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupMultipleFormControllers(scriptStrings, document, helpers) {
|
function setupMultipleFormControllers(scriptStrings, document, helpers) {
|
||||||
@ -126,10 +134,10 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
return meta[doctype]
|
return meta[doctype]
|
||||||
}
|
}
|
||||||
|
|
||||||
setupHelperMethods(FormClass, document)
|
const getDoc = () => document.doc
|
||||||
|
|
||||||
if (isChildDoctype) {
|
if (isChildDoctype) {
|
||||||
instance.doc = createDocProxy(document.doc, parentInstance, instance)
|
instance.doc = createDocProxy(getDoc, parentInstance, instance)
|
||||||
|
|
||||||
if (!parentInstance._childInstances) {
|
if (!parentInstance._childInstances) {
|
||||||
parentInstance._childInstances = []
|
parentInstance._childInstances = []
|
||||||
@ -137,22 +145,21 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
|
|
||||||
parentInstance._childInstances.push(instance)
|
parentInstance._childInstances.push(instance)
|
||||||
} else {
|
} else {
|
||||||
instance.doc = createDocProxy(document.doc, instance)
|
instance.doc = createDocProxy(getDoc, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupHelperMethods(FormClass, document) {
|
function setupHelperMethods(FormClass) {
|
||||||
if (typeof FormClass.prototype.getRow !== 'function') {
|
if (typeof FormClass.prototype.getRow !== 'function') {
|
||||||
FormClass.prototype.getRow = function (parentField, idx) {
|
FormClass.prototype.getRow = function (parentField, idx) {
|
||||||
let data = document.doc
|
|
||||||
idx = idx || this.currentRowIdx
|
idx = idx || this.currentRowIdx
|
||||||
|
|
||||||
let dt = null
|
let dt = null
|
||||||
|
|
||||||
if (this instanceof Array) {
|
if (this instanceof Array) {
|
||||||
const { getFields } = getMeta(data.doctype)
|
const { getFields } = getMeta(this.doc.doctype)
|
||||||
let fields = getFields()
|
let fields = getFields()
|
||||||
let field = fields.find((f) => f.fieldname === parentField)
|
let field = fields.find((f) => f.fieldname === parentField)
|
||||||
dt = field?.options?.replace(/\s+/g, '')
|
dt = field?.options?.replace(/\s+/g, '')
|
||||||
@ -162,13 +169,13 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data[parentField]) {
|
if (!this.doc[parentField]) {
|
||||||
console.warn(
|
console.warn(
|
||||||
__('⚠️ No data found for parent field: {0}', [parentField]),
|
__('⚠️ No data found for parent field: {0}', [parentField]),
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const row = data[parentField].find((r) => r.idx === idx)
|
const row = this.doc[parentField].find((r) => r.idx === idx)
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@ -180,7 +187,7 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
row.parent = row.parent || data.name
|
row.parent = row.parent || this.doc.name
|
||||||
|
|
||||||
if (this instanceof Array && dt) {
|
if (this instanceof Array && dt) {
|
||||||
return createDocProxy(
|
return createDocProxy(
|
||||||
@ -220,46 +227,76 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
const FormClass = new Function(...helperKeys, wrappedScript)(
|
const FormClass = new Function(...helperKeys, wrappedScript)(
|
||||||
...helperValues,
|
...helperValues,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
setupHelperMethods(FormClass)
|
||||||
|
|
||||||
return FormClass
|
return FormClass
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDocProxy(data, instance, childInstance = null) {
|
function createDocProxy(source, instance, childInstance = null) {
|
||||||
return new Proxy(data, {
|
const isFunction = typeof source === 'function'
|
||||||
get(target, prop) {
|
const getCurrentData = () => (isFunction ? source() : source)
|
||||||
if (prop === 'trigger') {
|
|
||||||
if ('trigger' in data) {
|
return new Proxy(
|
||||||
console.warn(
|
{},
|
||||||
__(
|
{
|
||||||
'⚠️ Avoid using "trigger" as a field name — it conflicts with the built-in trigger() method.',
|
get(target, prop) {
|
||||||
),
|
const currentDocData = getCurrentData()
|
||||||
|
if (!currentDocData) return undefined
|
||||||
|
|
||||||
|
if (prop === 'trigger') {
|
||||||
|
if (currentDocData && 'trigger' in currentDocData) {
|
||||||
|
console.warn(
|
||||||
|
__(
|
||||||
|
'⚠️ Avoid using "trigger" as a field name — it conflicts with the built-in trigger() method.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (methodName, ...args) => {
|
||||||
|
const method = instance[methodName]
|
||||||
|
if (typeof method === 'function') {
|
||||||
|
return method.apply(instance, args)
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
__('⚠️ Method "{0}" not found in class.', [methodName]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === 'getRow') {
|
||||||
|
return instance.getRow.bind(
|
||||||
|
childInstance || instance._childInstances || instance,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (methodName, ...args) => {
|
return currentDocData[prop]
|
||||||
const method = instance[methodName]
|
},
|
||||||
if (typeof method === 'function') {
|
set(target, prop, value) {
|
||||||
return method.apply(instance, args)
|
const currentDocData = getCurrentData()
|
||||||
} else {
|
if (!currentDocData) return false
|
||||||
console.warn(
|
|
||||||
__('⚠️ Method "{0}" not found in class.', [methodName]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop === 'getRow') {
|
currentDocData[prop] = value
|
||||||
return instance.getRow.bind(
|
return true
|
||||||
childInstance || instance._childInstances || instance,
|
},
|
||||||
)
|
has(target, prop) {
|
||||||
}
|
const currentDocData = getCurrentData()
|
||||||
|
if (!currentDocData) return false
|
||||||
return target[prop]
|
return prop in currentDocData
|
||||||
|
},
|
||||||
|
ownKeys(target) {
|
||||||
|
const currentDocData = getCurrentData()
|
||||||
|
if (!currentDocData) return []
|
||||||
|
return Reflect.ownKeys(currentDocData)
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(target, prop) {
|
||||||
|
const currentDocData = getCurrentData()
|
||||||
|
if (!currentDocData) return undefined
|
||||||
|
return Reflect.getOwnPropertyDescriptor(currentDocData, prop)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
set(target, prop, value) {
|
)
|
||||||
target[prop] = value
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -374,6 +374,7 @@ 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 {
|
import {
|
||||||
whatsappEnabled,
|
whatsappEnabled,
|
||||||
callEnabled,
|
callEnabled,
|
||||||
@ -638,6 +639,8 @@ const existingOrganizationChecked = ref(false)
|
|||||||
const existingContact = ref('')
|
const existingContact = ref('')
|
||||||
const existingOrganization = ref('')
|
const existingOrganization = ref('')
|
||||||
|
|
||||||
|
const { triggerConvertToDeal } = useDocument('CRM Lead', props.leadId)
|
||||||
|
|
||||||
async function convertToDeal() {
|
async function convertToDeal() {
|
||||||
if (existingContactChecked.value && !existingContact.value) {
|
if (existingContactChecked.value && !existingContact.value) {
|
||||||
toast.error(__('Please select an existing contact'))
|
toast.error(__('Please select an existing contact'))
|
||||||
@ -657,6 +660,12 @@ async function convertToDeal() {
|
|||||||
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', {
|
let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
||||||
lead: lead.data.name,
|
lead: lead.data.name,
|
||||||
deal,
|
deal,
|
||||||
|
|||||||
@ -2,9 +2,8 @@ import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
|||||||
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { gemoji } from 'gemoji'
|
import { gemoji } from 'gemoji'
|
||||||
import { useTimeAgo } from '@vueuse/core'
|
|
||||||
import { getMeta } from '@/stores/meta'
|
import { getMeta } from '@/stores/meta'
|
||||||
import { toast, dayjsLocal, dayjs } from 'frappe-ui'
|
import { toast, dayjsLocal, dayjs, getConfig } from 'frappe-ui'
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
|
|
||||||
export function formatTime(seconds) {
|
export function formatTime(seconds) {
|
||||||
@ -65,7 +64,85 @@ export function getFormat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function timeAgo(date) {
|
export function timeAgo(date) {
|
||||||
return useTimeAgo(date).value
|
return prettyDate(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowserTimezone() {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prettyDate(date, mini = false) {
|
||||||
|
if (!date) return ''
|
||||||
|
|
||||||
|
let systemTimezone = getConfig('systemTimezone')
|
||||||
|
let localTimezone = getConfig('localTimezone') || getBrowserTimezone()
|
||||||
|
|
||||||
|
if (typeof date == 'string') {
|
||||||
|
date = dayjsLocal(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
let nowDatetime = dayjs().tz(localTimezone || systemTimezone)
|
||||||
|
let diff = nowDatetime.diff(date, 'seconds')
|
||||||
|
|
||||||
|
let dayDiff = Math.floor(diff / 86400)
|
||||||
|
|
||||||
|
if (isNaN(dayDiff) || dayDiff < 0) return ''
|
||||||
|
|
||||||
|
if (mini) {
|
||||||
|
// Return short format of time difference
|
||||||
|
if (dayDiff == 0) {
|
||||||
|
if (diff < 60) {
|
||||||
|
return __('now')
|
||||||
|
} else if (diff < 3600) {
|
||||||
|
return __('{0} m', [Math.floor(diff / 60)])
|
||||||
|
} else if (diff < 86400) {
|
||||||
|
return __('{0} h', [Math.floor(diff / 3600)])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dayDiff < 7) {
|
||||||
|
return __('{0} d', [dayDiff])
|
||||||
|
} else if (dayDiff < 31) {
|
||||||
|
return __('{0} w', [Math.floor(dayDiff / 7)])
|
||||||
|
} else if (dayDiff < 365) {
|
||||||
|
return __('{0} M', [Math.floor(dayDiff / 30)])
|
||||||
|
} else {
|
||||||
|
return __('{0} y', [Math.floor(dayDiff / 365)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Return long format of time difference
|
||||||
|
if (dayDiff == 0) {
|
||||||
|
if (diff < 60) {
|
||||||
|
return __('just now')
|
||||||
|
} else if (diff < 120) {
|
||||||
|
return __('1 minute ago')
|
||||||
|
} else if (diff < 3600) {
|
||||||
|
return __('{0} minutes ago', [Math.floor(diff / 60)])
|
||||||
|
} else if (diff < 7200) {
|
||||||
|
return __('1 hour ago')
|
||||||
|
} else if (diff < 86400) {
|
||||||
|
return __('{0} hours ago', [Math.floor(diff / 3600)])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dayDiff == 1) {
|
||||||
|
return __('yesterday')
|
||||||
|
} else if (dayDiff < 7) {
|
||||||
|
return __('{0} days ago', [dayDiff])
|
||||||
|
} else if (dayDiff < 14) {
|
||||||
|
return __('1 week ago')
|
||||||
|
} else if (dayDiff < 31) {
|
||||||
|
return __('{0} weeks ago', [Math.floor(dayDiff / 7)])
|
||||||
|
} else if (dayDiff < 62) {
|
||||||
|
return __('1 month ago')
|
||||||
|
} else if (dayDiff < 365) {
|
||||||
|
return __('{0} months ago', [Math.floor(dayDiff / 30)])
|
||||||
|
} else if (dayDiff < 730) {
|
||||||
|
return __('1 year ago')
|
||||||
|
} else {
|
||||||
|
return __('{0} years ago', [Math.floor(dayDiff / 365)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function taskStatusOptions(action, data) {
|
export function taskStatusOptions(action, data) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user