Merge branch 'main-hotfix' into mergify/bp/main-hotfix/pr-1252
This commit is contained in:
commit
7ef00965fa
32
crm/api/assignment_rule.py
Normal file
32
crm/api/assignment_rule.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_assignment_rules_list():
|
||||||
|
assignment_rules = []
|
||||||
|
for docname in frappe.get_all(
|
||||||
|
"Assignment Rule", filters={"document_type": ["in", ["CRM Lead", "CRM Deal"]]}
|
||||||
|
):
|
||||||
|
doc = frappe.get_value(
|
||||||
|
"Assignment Rule",
|
||||||
|
docname,
|
||||||
|
fieldname=[
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"disabled",
|
||||||
|
"priority",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
users_exists = bool(frappe.db.exists("Assignment Rule User", {"parent": docname.name}))
|
||||||
|
assignment_rules.append({**doc, "users_exists": users_exists})
|
||||||
|
return assignment_rules
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def duplicate_assignment_rule(docname, new_name):
|
||||||
|
doc = frappe.get_doc("Assignment Rule", docname)
|
||||||
|
doc.name = new_name
|
||||||
|
doc.assignment_rule_name = new_name
|
||||||
|
doc.insert()
|
||||||
|
return doc
|
||||||
@ -25,6 +25,8 @@ def after_install(force=False):
|
|||||||
add_standard_dropdown_items()
|
add_standard_dropdown_items()
|
||||||
add_default_scripts()
|
add_default_scripts()
|
||||||
create_default_manager_dashboard(force)
|
create_default_manager_dashboard(force)
|
||||||
|
create_assignment_rule_custom_fields()
|
||||||
|
add_assignment_rule_property_setters()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@ -421,3 +423,80 @@ def add_default_scripts():
|
|||||||
for doctype in ["CRM Lead", "CRM Deal"]:
|
for doctype in ["CRM Lead", "CRM Deal"]:
|
||||||
create_product_details_script(doctype)
|
create_product_details_script(doctype)
|
||||||
create_forecasting_script()
|
create_forecasting_script()
|
||||||
|
|
||||||
|
|
||||||
|
def add_assignment_rule_property_setters():
|
||||||
|
"""Add a property setter to the Assignment Rule DocType for assign_condition and unassign_condition."""
|
||||||
|
|
||||||
|
default_fields = {
|
||||||
|
"doctype": "Property Setter",
|
||||||
|
"doctype_or_field": "DocField",
|
||||||
|
"doc_type": "Assignment Rule",
|
||||||
|
"property_type": "Data",
|
||||||
|
"is_system_generated": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not frappe.db.exists("Property Setter", {"name": "Assignment Rule-assign_condition-depends_on"}):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
**default_fields,
|
||||||
|
"name": "Assignment Rule-assign_condition-depends_on",
|
||||||
|
"field_name": "assign_condition",
|
||||||
|
"property": "depends_on",
|
||||||
|
"value": "eval: !doc.assign_condition_json",
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
else:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Property Setter",
|
||||||
|
{"name": "Assignment Rule-assign_condition-depends_on"},
|
||||||
|
"value",
|
||||||
|
"eval: !doc.assign_condition_json",
|
||||||
|
)
|
||||||
|
if not frappe.db.exists("Property Setter", {"name": "Assignment Rule-unassign_condition-depends_on"}):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
**default_fields,
|
||||||
|
"name": "Assignment Rule-unassign_condition-depends_on",
|
||||||
|
"field_name": "unassign_condition",
|
||||||
|
"property": "depends_on",
|
||||||
|
"value": "eval: !doc.unassign_condition_json",
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
else:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Property Setter",
|
||||||
|
{"name": "Assignment Rule-unassign_condition-depends_on"},
|
||||||
|
"value",
|
||||||
|
"eval: !doc.unassign_condition_json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_assignment_rule_custom_fields():
|
||||||
|
if not frappe.get_meta("Assignment Rule").has_field("assign_condition_json"):
|
||||||
|
click.secho("* Installing Custom Fields in Assignment Rule")
|
||||||
|
|
||||||
|
create_custom_fields(
|
||||||
|
{
|
||||||
|
"Assignment Rule": [
|
||||||
|
{
|
||||||
|
"description": "Autogenerated field by CRM App",
|
||||||
|
"fieldname": "assign_condition_json",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Assign Condition JSON",
|
||||||
|
"insert_after": "assign_condition",
|
||||||
|
"depends_on": "eval: doc.assign_condition_json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Autogenerated field by CRM App",
|
||||||
|
"fieldname": "unassign_condition_json",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Unassign Condition JSON",
|
||||||
|
"insert_after": "unassign_condition",
|
||||||
|
"depends_on": "eval: doc.unassign_condition_json",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.clear_cache(doctype="Assignment Rule")
|
||||||
|
|||||||
@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: frappe\n"
|
"Project-Id-Version: frappe\n"
|
||||||
"Report-Msgid-Bugs-To: shariq@frappe.io\n"
|
"Report-Msgid-Bugs-To: shariq@frappe.io\n"
|
||||||
"POT-Creation-Date: 2025-09-14 09:35+0000\n"
|
"POT-Creation-Date: 2025-09-14 09:35+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-16 20:02\n"
|
"PO-Revision-Date: 2025-09-17 20:27\n"
|
||||||
"Last-Translator: shariq@frappe.io\n"
|
"Last-Translator: shariq@frappe.io\n"
|
||||||
"Language-Team: Portuguese\n"
|
"Language-Team: Portuguese\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
@ -588,7 +588,7 @@ msgstr "Atribuído a"
|
|||||||
|
|
||||||
#: frontend/src/components/Settings/AssignmentRules/AssigneeRules.vue:5
|
#: frontend/src/components/Settings/AssignmentRules/AssigneeRules.vue:5
|
||||||
msgid "Assignee Rules"
|
msgid "Assignee Rules"
|
||||||
msgstr ""
|
msgstr "Regras de Atribuição"
|
||||||
|
|
||||||
#: frontend/src/components/Settings/AssignmentRules/AssigneeRules.vue:75
|
#: frontend/src/components/Settings/AssignmentRules/AssigneeRules.vue:75
|
||||||
msgid "Assignees"
|
msgid "Assignees"
|
||||||
|
|||||||
@ -15,4 +15,5 @@ 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
|
crm.patches.v1_0.update_deal_status_probabilities
|
||||||
crm.patches.v1_0.update_deal_status_type
|
crm.patches.v1_0.update_deal_status_type
|
||||||
crm.patches.v1_0.create_default_lost_reasons
|
crm.patches.v1_0.create_default_lost_reasons
|
||||||
|
crm.patches.v1_0.add_fields_in_assignment_rule
|
||||||
|
|||||||
9
crm/patches/v1_0/add_fields_in_assignment_rule.py
Normal file
9
crm/patches/v1_0/add_fields_in_assignment_rule.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from crm.install import (
|
||||||
|
add_assignment_rule_property_setters,
|
||||||
|
create_assignment_rule_custom_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
create_assignment_rule_custom_fields()
|
||||||
|
add_assignment_rule_property_setters()
|
||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -2,4 +2,5 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
components.d.ts
|
||||||
469
frontend/src/components/ConditionsFilter/CFCondition.vue
Normal file
469
frontend/src/components/ConditionsFilter/CFCondition.vue
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex gap-2"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'items-center': !props.isGroup,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex gap-2 w-full"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'items-center justify-between': !props.isGroup,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div :class="'text-end text-base text-gray-600'">
|
||||||
|
<div v-if="props.itemIndex == 0" class="min-w-[66px] text-start">
|
||||||
|
{{ __('Where') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="min-w-[66px] flex items-start">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
class="w-max"
|
||||||
|
@click="toggleConjunction"
|
||||||
|
icon-right="refresh-cw"
|
||||||
|
:disabled="props.itemIndex > 2"
|
||||||
|
:label="conjunction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!props.isGroup" class="flex items-center gap-2 w-full">
|
||||||
|
<div id="fieldname" class="w-full">
|
||||||
|
<Autocomplete
|
||||||
|
:options="filterableFields.data"
|
||||||
|
v-model="props.condition[0]"
|
||||||
|
:placeholder="__('Field')"
|
||||||
|
@update:modelValue="updateField"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="operator">
|
||||||
|
<FormControl
|
||||||
|
v-if="!props.condition[0]"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
:placeholder="__('operator')"
|
||||||
|
class="w-[100px]"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
:disabled="!props.condition[0]"
|
||||||
|
type="select"
|
||||||
|
v-model="props.condition[1]"
|
||||||
|
@change="updateOperator"
|
||||||
|
:options="getOperators()"
|
||||||
|
class="w-max min-w-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="value" class="w-full">
|
||||||
|
<FormControl
|
||||||
|
v-if="!props.condition[0]"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
:placeholder="__('condition')"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
v-else
|
||||||
|
:is="getValueControl()"
|
||||||
|
v-model="props.condition[2]"
|
||||||
|
@change="updateValue"
|
||||||
|
:placeholder="__('condition')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CFConditions
|
||||||
|
v-if="props.isGroup && !(props.level == 2 || props.level == 4)"
|
||||||
|
:conditions="props.condition"
|
||||||
|
:isChild="true"
|
||||||
|
:level="props.level"
|
||||||
|
:disableAddCondition="props.disableAddCondition"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
v-if="props.isGroup && (props.level == 2 || props.level == 4)"
|
||||||
|
@click="show = true"
|
||||||
|
:label="__('Open nested conditions')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="'w-max'">
|
||||||
|
<Dropdown placement="right" :options="dropdownOptions">
|
||||||
|
<Button variant="ghost" icon="more-horizontal" />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{ size: '3xl', title: __('Nested conditions') }"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<CFConditions
|
||||||
|
:conditions="props.condition"
|
||||||
|
:isChild="true"
|
||||||
|
:level="props.level"
|
||||||
|
:disableAddCondition="props.disableAddCondition"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { TemplateOption } from '@/utils'
|
||||||
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
DateRangePicker,
|
||||||
|
DateTimePicker,
|
||||||
|
Dialog,
|
||||||
|
Dropdown,
|
||||||
|
FormControl,
|
||||||
|
Rating,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, defineEmits, h, ref } from 'vue'
|
||||||
|
import GroupIcon from '~icons/lucide/group'
|
||||||
|
import UnGroupIcon from '~icons/lucide/ungroup'
|
||||||
|
import CFConditions from './CFConditions.vue'
|
||||||
|
import { filterableFields } from './filterableFields'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
const show = ref(false)
|
||||||
|
const emit = defineEmits([
|
||||||
|
'remove',
|
||||||
|
'unGroupConditions',
|
||||||
|
'toggleConjunction',
|
||||||
|
'turnIntoGroup',
|
||||||
|
])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
condition: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isChild: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
itemIndex: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
level: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
isGroup: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
conjunction: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
disableAddCondition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const dropdownOptions = computed(() => {
|
||||||
|
const options = []
|
||||||
|
|
||||||
|
if (!props.isGroup && props.level < 4) {
|
||||||
|
options.push({
|
||||||
|
label: __('Turn into a group'),
|
||||||
|
icon: () => h(GroupIcon),
|
||||||
|
onClick: () => {
|
||||||
|
emit('turnIntoGroup')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.isGroup) {
|
||||||
|
options.push({
|
||||||
|
label: __('Ungroup conditions'),
|
||||||
|
icon: () => h(UnGroupIcon),
|
||||||
|
onClick: () => {
|
||||||
|
emit('unGroupConditions')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
label: __('Remove'),
|
||||||
|
component: (props) =>
|
||||||
|
TemplateOption({
|
||||||
|
option: __('Remove'),
|
||||||
|
icon: 'trash-2',
|
||||||
|
active: props.active,
|
||||||
|
variant: 'danger',
|
||||||
|
onClick: () => {
|
||||||
|
emit('remove')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
condition: () => !props.isGroup,
|
||||||
|
})
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
label: __('Remove group'),
|
||||||
|
component: (props) =>
|
||||||
|
TemplateOption({
|
||||||
|
option: __('Remove group'),
|
||||||
|
icon: 'trash-2',
|
||||||
|
active: props.active,
|
||||||
|
variant: 'danger',
|
||||||
|
onClick: () => {
|
||||||
|
emit('remove')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
condition: () => props.isGroup,
|
||||||
|
})
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const typeCheck = ['Check']
|
||||||
|
const typeLink = ['Link', 'Dynamic Link']
|
||||||
|
const typeNumber = ['Float', 'Int', 'Currency', 'Percent']
|
||||||
|
const typeSelect = ['Select']
|
||||||
|
const typeString = ['Data', 'Long Text', 'Small Text', 'Text Editor', 'Text']
|
||||||
|
const typeDate = ['Date', 'Datetime']
|
||||||
|
const typeRating = ['Rating']
|
||||||
|
|
||||||
|
function toggleConjunction() {
|
||||||
|
emit('toggleConjunction', props.conjunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateField = (field) => {
|
||||||
|
props.condition[0] = field?.fieldname
|
||||||
|
resetConditionValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetConditionValue = () => {
|
||||||
|
props.condition[2] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValueControl() {
|
||||||
|
const [field, operator] = props.condition
|
||||||
|
if (!field) return null
|
||||||
|
const fieldData = filterableFields.data?.find((f) => f.fieldname == field)
|
||||||
|
if (!fieldData) return null
|
||||||
|
const { fieldtype, options } = fieldData
|
||||||
|
if (operator == 'is') {
|
||||||
|
return h(FormControl, {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Set',
|
||||||
|
value: 'set',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Not Set',
|
||||||
|
value: 'not set',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} else if (['like', 'not like', 'in', 'not in'].includes(operator)) {
|
||||||
|
return h(FormControl, { type: 'text' })
|
||||||
|
} else if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
|
||||||
|
const _options =
|
||||||
|
fieldtype == 'Check' ? ['Yes', 'No'] : getSelectOptions(options)
|
||||||
|
return h(FormControl, {
|
||||||
|
type: 'select',
|
||||||
|
options: _options.map((o) => ({
|
||||||
|
label: o,
|
||||||
|
value: o,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
} else if (typeLink.includes(fieldtype)) {
|
||||||
|
if (fieldtype == 'Dynamic Link') {
|
||||||
|
return h(FormControl, { type: 'text' })
|
||||||
|
}
|
||||||
|
return h(Link, {
|
||||||
|
class: 'form-control',
|
||||||
|
doctype: options,
|
||||||
|
value: props.condition[2],
|
||||||
|
})
|
||||||
|
} else if (typeNumber.includes(fieldtype)) {
|
||||||
|
return h(FormControl, { type: 'number' })
|
||||||
|
} else if (typeDate.includes(fieldtype) && operator == 'between') {
|
||||||
|
return h(DateRangePicker, { value: props.condition[2], iconLeft: '' })
|
||||||
|
} else if (typeDate.includes(fieldtype)) {
|
||||||
|
return h(fieldtype == 'Date' ? DatePicker : DateTimePicker, {
|
||||||
|
value: props.condition[2],
|
||||||
|
iconLeft: '',
|
||||||
|
})
|
||||||
|
} else if (typeRating.includes(fieldtype)) {
|
||||||
|
return h(Rating, {
|
||||||
|
modelValue: props.condition[2] || 0,
|
||||||
|
class: 'truncate',
|
||||||
|
'update:modelValue': (v) => updateValue(v),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return h(FormControl, { type: 'text' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValue(value) {
|
||||||
|
value = value.target ? value.target.value : value
|
||||||
|
if (props.condition[1] === 'between') {
|
||||||
|
props.condition[2] = [value.split(',')[0], value.split(',')[1]]
|
||||||
|
} else {
|
||||||
|
props.condition[2] = value + ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectOptions(options) {
|
||||||
|
return options.split('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOperator(event) {
|
||||||
|
let oldOperatorValue = event.target._value
|
||||||
|
let newOperatorValue = event.target.value
|
||||||
|
props.condition[1] = event.target.value
|
||||||
|
if (!isSameTypeOperator(oldOperatorValue, newOperatorValue)) {
|
||||||
|
props.condition[2] = getDefaultValue(props.condition[0])
|
||||||
|
}
|
||||||
|
resetConditionValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOperators() {
|
||||||
|
let options = []
|
||||||
|
const field = props.condition[0]
|
||||||
|
if (!field) return options
|
||||||
|
const fieldData = filterableFields.data?.find((f) => f.fieldname == field)
|
||||||
|
if (!fieldData) return options
|
||||||
|
const { fieldtype, fieldname } = fieldData
|
||||||
|
if (typeString.includes(fieldtype)) {
|
||||||
|
options.push(
|
||||||
|
...[
|
||||||
|
{ label: 'Equals', value: '==' },
|
||||||
|
{ label: 'Not Equals', value: '!=' },
|
||||||
|
{ label: 'Like', value: 'like' },
|
||||||
|
{ label: 'Not Like', value: 'not like' },
|
||||||
|
{ label: 'In', value: 'in' },
|
||||||
|
{ label: 'Not In', value: 'not in' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (fieldname === '_assign') {
|
||||||
|
options = [
|
||||||
|
{ label: 'Like', value: 'like' },
|
||||||
|
{ label: 'Not Like', value: 'not like' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (typeNumber.includes(fieldtype)) {
|
||||||
|
options.push(
|
||||||
|
...[
|
||||||
|
{ label: 'Equals', value: '==' },
|
||||||
|
{ label: 'Not Equals', value: '!=' },
|
||||||
|
{ label: 'Like', value: 'like' },
|
||||||
|
{ label: 'Not Like', value: 'not like' },
|
||||||
|
{ label: 'In', value: 'in' },
|
||||||
|
{ label: 'Not In', value: 'not in' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
{ label: '<', value: '<' },
|
||||||
|
{ label: '>', value: '>' },
|
||||||
|
{ label: '<=', value: '<=' },
|
||||||
|
{ label: '>=', value: '>=' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeSelect.includes(fieldtype)) {
|
||||||
|
options.push(
|
||||||
|
...[
|
||||||
|
{ label: 'Equals', value: '==' },
|
||||||
|
{ label: 'Not Equals', value: '!=' },
|
||||||
|
{ label: 'In', value: 'in' },
|
||||||
|
{ label: 'Not In', value: 'not in' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeLink.includes(fieldtype)) {
|
||||||
|
options.push(
|
||||||
|
...[
|
||||||
|
{ label: 'Equals', value: '==' },
|
||||||
|
{ label: 'Not Equals', value: '!=' },
|
||||||
|
{ label: 'Like', value: 'like' },
|
||||||
|
{ label: 'Not Like', value: 'not like' },
|
||||||
|
{ label: 'In', value: 'in' },
|
||||||
|
{ label: 'Not In', value: 'not in' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeCheck.includes(fieldtype)) {
|
||||||
|
options.push(...[{ label: 'Equals', value: '==' }])
|
||||||
|
}
|
||||||
|
if (['Duration'].includes(fieldtype)) {
|
||||||
|
options.push(
|
||||||
|
...[
|
||||||
|
{ label: 'Like', value: 'like' },
|
||||||
|
{ label: 'Not Like', value: 'not like' },
|
||||||
|
{ label: 'In', value: 'in' },
|
||||||
|
{ label: 'Not In', value: 'not in' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeDate.includes(fieldtype)) {
|
||||||
|
options.push(
|
||||||
|
...[
|
||||||
|
{ label: 'Equals', value: '==' },
|
||||||
|
{ label: 'Not Equals', value: '!=' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
{ label: '>', value: '>' },
|
||||||
|
{ label: '<', value: '<' },
|
||||||
|
{ label: '>=', value: '>=' },
|
||||||
|
{ label: '<=', value: '<=' },
|
||||||
|
{ label: 'Between', value: 'between' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeRating.includes(fieldtype)) {
|
||||||
|
options.push(
|
||||||
|
...[
|
||||||
|
{ label: 'Equals', value: '==' },
|
||||||
|
{ label: 'Not Equals', value: '!=' },
|
||||||
|
{ label: 'Is', value: 'is' },
|
||||||
|
{ label: '>', value: '>' },
|
||||||
|
{ label: '<', value: '<' },
|
||||||
|
{ label: '>=', value: '>=' },
|
||||||
|
{ label: '<=', value: '<=' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const op = options.find((o) => o.value == props.condition[1])
|
||||||
|
props.condition[1] = op?.value || options[0].value
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultValue(field) {
|
||||||
|
if (typeSelect.includes(field.fieldtype)) {
|
||||||
|
return getSelectOptions(field.options)[0]
|
||||||
|
}
|
||||||
|
if (typeCheck.includes(field.fieldtype)) {
|
||||||
|
return 'Yes'
|
||||||
|
}
|
||||||
|
if (typeDate.includes(field.fieldtype)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeRating.includes(field.fieldtype)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameTypeOperator(oldOperator, newOperator) {
|
||||||
|
let textOperators = ['==', '!=', 'in', 'not in', '>', '<', '>=', '<=']
|
||||||
|
if (
|
||||||
|
textOperators.includes(oldOperator) &&
|
||||||
|
textOperators.includes(newOperator)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
142
frontend/src/components/ConditionsFilter/CFConditions.vue
Normal file
142
frontend/src/components/ConditionsFilter/CFConditions.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border border-gray-300 p-3 flex flex-col gap-4 w-full">
|
||||||
|
<template v-for="(condition, i) in props.conditions" :key="condition.field">
|
||||||
|
<CFCondition
|
||||||
|
v-if="Array.isArray(condition)"
|
||||||
|
:condition="condition"
|
||||||
|
:isChild="props.isChild"
|
||||||
|
:itemIndex="i"
|
||||||
|
@remove="removeCondition(condition)"
|
||||||
|
@unGroupConditions="unGroupConditions(condition)"
|
||||||
|
:level="props.level + 1"
|
||||||
|
@toggleConjunction="toggleConjunction"
|
||||||
|
:isGroup="isGroupCondition(condition[0])"
|
||||||
|
:conjunction="getConjunction()"
|
||||||
|
@turnIntoGroup="turnIntoGroup(condition)"
|
||||||
|
:disableAddCondition="props.disableAddCondition"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div v-if="props.isChild" class="flex">
|
||||||
|
<Dropdown v-slot="{ open }" :options="dropdownOptions">
|
||||||
|
<Button
|
||||||
|
:disabled="props.disableAddCondition"
|
||||||
|
:label="__('Add condition')"
|
||||||
|
icon-left="plus"
|
||||||
|
:icon-right="open ? 'chevron-up' : 'chevron-down'"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Button, Dropdown } from 'frappe-ui'
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
import CFCondition from './CFCondition.vue'
|
||||||
|
import { filterableFields } from './filterableFields'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
conditions: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isChild: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
level: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
disableAddCondition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const getConjunction = () => {
|
||||||
|
let conjunction = 'and'
|
||||||
|
props.conditions.forEach((condition) => {
|
||||||
|
if (typeof condition == 'string') {
|
||||||
|
conjunction = condition
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return conjunction
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnIntoGroup = (condition) => {
|
||||||
|
props.conditions.splice(props.conditions.indexOf(condition), 1, [condition])
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGroupCondition = (condition) => {
|
||||||
|
return Array.isArray(condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: __('Add condition'),
|
||||||
|
onClick: () => {
|
||||||
|
const conjunction = getConjunction()
|
||||||
|
props.conditions.push(conjunction, ['', '', ''])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if (props.level < 3) {
|
||||||
|
options.push({
|
||||||
|
label: __('Add condition group'),
|
||||||
|
onClick: () => {
|
||||||
|
const conjunction = getConjunction()
|
||||||
|
props.conditions.push(conjunction, [[]])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
function removeCondition(condition) {
|
||||||
|
const conditionIndex = props.conditions.indexOf(condition)
|
||||||
|
if (conditionIndex == 0) {
|
||||||
|
props.conditions.splice(conditionIndex, 2)
|
||||||
|
} else {
|
||||||
|
props.conditions.splice(conditionIndex - 1, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unGroupConditions(condition) {
|
||||||
|
const conjunction = getConjunction()
|
||||||
|
const newConditions = condition.map((c) => {
|
||||||
|
if (typeof c == 'string') {
|
||||||
|
return conjunction
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
|
||||||
|
const index = props.conditions.indexOf(condition)
|
||||||
|
if (index !== -1) {
|
||||||
|
props.conditions.splice(index, 1, ...newConditions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConjunction(conjunction) {
|
||||||
|
for (let i = 0; i < props.conditions.length; i++) {
|
||||||
|
if (typeof props.conditions[i] == 'string') {
|
||||||
|
props.conditions[i] = conjunction == 'and' ? 'or' : 'and'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.doctype,
|
||||||
|
(doctype) => {
|
||||||
|
filterableFields.submit({
|
||||||
|
doctype,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
17
frontend/src/components/ConditionsFilter/filterableFields.ts
Normal file
17
frontend/src/components/ConditionsFilter/filterableFields.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { createResource } from 'frappe-ui'
|
||||||
|
|
||||||
|
export const filterableFields = createResource({
|
||||||
|
url: 'crm.api.doc.get_filterable_fields',
|
||||||
|
transform: (data) => {
|
||||||
|
data = data
|
||||||
|
.filter((field) => !field.fieldname.startsWith('_'))
|
||||||
|
.map((field) => {
|
||||||
|
return {
|
||||||
|
label: field.label,
|
||||||
|
value: field.fieldname,
|
||||||
|
...field,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
})
|
||||||
19
frontend/src/components/Icons/SettingsIcon2.vue
Normal file
19
frontend/src/components/Icons/SettingsIcon2.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-settings2-icon lucide-settings-2"
|
||||||
|
>
|
||||||
|
<path d="M14 17H5" />
|
||||||
|
<path d="M19 7h-9" />
|
||||||
|
<circle cx="17" cy="17" r="3" />
|
||||||
|
<circle cx="7" cy="7" r="3" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<Combobox :multiple="true">
|
||||||
|
<Popover placement="bottom-end">
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
icon-left="plus"
|
||||||
|
@click="togglePopover()"
|
||||||
|
:label="__('Add Assignee')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #body="{ togglePopover }">
|
||||||
|
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl w-60">
|
||||||
|
<div class="relative px-1.5 pt-0.5">
|
||||||
|
<ComboboxInput
|
||||||
|
ref="search"
|
||||||
|
class="form-input w-full"
|
||||||
|
type="text"
|
||||||
|
@change="(e) => debouncedQuery(e.target.value)"
|
||||||
|
:value="query"
|
||||||
|
autocomplete="off"
|
||||||
|
:placeholder="__('Search')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
|
||||||
|
@click="query = ''"
|
||||||
|
>
|
||||||
|
<FeatherIcon name="x" class="w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ComboboxOptions class="my-2 max-h-64 overflow-y-auto px-1.5" static>
|
||||||
|
<ComboboxOption
|
||||||
|
v-show="usersList.length > 0"
|
||||||
|
v-for="user in usersList"
|
||||||
|
:key="user.username"
|
||||||
|
:value="user"
|
||||||
|
as="template"
|
||||||
|
v-slot="{ active }"
|
||||||
|
@click="
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
addAssignee(user)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="flex items-center rounded p-1.5 w-full text-base"
|
||||||
|
:class="{ 'bg-gray-100': active }"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 items-center w-full select-none">
|
||||||
|
<Avatar
|
||||||
|
:shape="'circle'"
|
||||||
|
:image="user.user_image"
|
||||||
|
:label="user.full_name"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="font-semibold text-ink-gray-7">
|
||||||
|
{{ user.full_name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-6">{{ user.email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
<li
|
||||||
|
v-if="usersList.length == 0"
|
||||||
|
class="mt-1.5 rounded-md p-1.5 text-base text-gray-600"
|
||||||
|
>
|
||||||
|
{{ __('No results found') }}
|
||||||
|
</li>
|
||||||
|
</ComboboxOptions>
|
||||||
|
<div class="border-t p-1.5 pb-0.5 *:w-full">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon-left="plus"
|
||||||
|
class="w-full"
|
||||||
|
:label="__('Invite agent')"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
inviteAgent()
|
||||||
|
togglePopover()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</Combobox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxOption,
|
||||||
|
ComboboxOptions,
|
||||||
|
} from '@headlessui/vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import { Avatar, Popover } from 'frappe-ui'
|
||||||
|
import { computed, inject, ref } from 'vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { globalStore } from '@/stores/global'
|
||||||
|
import { activeSettingsPage } from '@/composables/settings'
|
||||||
|
|
||||||
|
const emit = defineEmits(['addAssignee'])
|
||||||
|
const query = ref('')
|
||||||
|
const { users } = usersStore()
|
||||||
|
const { $dialog } = globalStore()
|
||||||
|
const assignmentRuleData = inject('assignmentRuleData')
|
||||||
|
|
||||||
|
const debouncedQuery = useDebounceFn((val) => {
|
||||||
|
query.value = val
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
const usersList = computed(() => {
|
||||||
|
let filteredUsers =
|
||||||
|
users.data?.crmUsers?.filter((user) => user.name !== 'Administrator') || []
|
||||||
|
|
||||||
|
return filteredUsers
|
||||||
|
.filter(
|
||||||
|
(user) =>
|
||||||
|
user.name?.includes(query.value) ||
|
||||||
|
user.full_name?.includes(query.value),
|
||||||
|
)
|
||||||
|
.filter((user) => {
|
||||||
|
return !assignmentRuleData.value.users.some((u) => u.user === user.email)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const addAssignee = (user) => {
|
||||||
|
const userExists = assignmentRuleData.value.users.some(
|
||||||
|
(u) => u.user === user.user,
|
||||||
|
)
|
||||||
|
if (!userExists) {
|
||||||
|
assignmentRuleData.value.users.push({
|
||||||
|
full_name: user.full_name,
|
||||||
|
email: user.email,
|
||||||
|
user_image: user.user_image,
|
||||||
|
user: user.email,
|
||||||
|
})
|
||||||
|
emit('addAssignee', user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteAgent = () => {
|
||||||
|
$dialog({
|
||||||
|
title: __('Invite agent'),
|
||||||
|
message: __(
|
||||||
|
'You will be redirected to invite user page, unsaved changes will be lost.',
|
||||||
|
),
|
||||||
|
variant: 'solid',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Go to invite page'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: (close) => {
|
||||||
|
activeSettingsPage.value = 'Invite User'
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-11 items-center gap-4 cursor-pointer hover:bg-gray-50 rounded"
|
||||||
|
>
|
||||||
|
<div class="w-full py-3 pl-2 col-span-7" @click="updateStep('view', data)">
|
||||||
|
<div class="text-base text-ink-gray-7 font-medium">{{ data.name }}</div>
|
||||||
|
<div
|
||||||
|
v-if="data.description && data.description.length > 0"
|
||||||
|
class="text-sm w-full text-ink-gray-5 mt-1 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||||
|
>
|
||||||
|
{{ data.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<Select
|
||||||
|
class="w-max bg-transparent -ml-2 border-0 text-ink-gray-6 focus-visible:!ring-0 bg-none"
|
||||||
|
:options="priorityOptions"
|
||||||
|
v-model="data.priority"
|
||||||
|
@update:modelValue="onPriorityChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center w-full pr-2 col-span-2">
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
:modelValue="!data.disabled"
|
||||||
|
@update:modelValue="onToggle"
|
||||||
|
/>
|
||||||
|
<Dropdown placement="right" :options="dropdownOptions">
|
||||||
|
<Button
|
||||||
|
icon="more-horizontal"
|
||||||
|
variant="ghost"
|
||||||
|
@click="isConfirmingDelete = false"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
:options="{ title: __('Duplicate Assignment Rule') }"
|
||||||
|
v-model="duplicateDialog.show"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<FormControl
|
||||||
|
:label="__('New Assignment Rule Name')"
|
||||||
|
type="text"
|
||||||
|
v-model="duplicateDialog.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
:label="__('Close')"
|
||||||
|
@click="duplicateDialog.show = false"
|
||||||
|
/>
|
||||||
|
<Button variant="solid" :label="__('Duplicate')" @click="duplicate()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
createResource,
|
||||||
|
Dialog,
|
||||||
|
Dropdown,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
toast,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { inject, ref } from 'vue'
|
||||||
|
import { TemplateOption } from '@/utils'
|
||||||
|
|
||||||
|
const assignmentRulesList = inject('assignmentRulesList')
|
||||||
|
const updateStep = inject('updateStep')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const priorityOptions = [
|
||||||
|
{ label: 'Low', value: '0' },
|
||||||
|
{ label: 'Low-Medium', value: '1' },
|
||||||
|
{ label: 'Medium', value: '2' },
|
||||||
|
{ label: 'Medium-High', value: '3' },
|
||||||
|
{ label: 'High', value: '4' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const duplicateDialog = ref({
|
||||||
|
show: false,
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const isConfirmingDelete = ref(false)
|
||||||
|
|
||||||
|
const deleteAssignmentRule = () => {
|
||||||
|
createResource({
|
||||||
|
url: 'frappe.client.delete',
|
||||||
|
params: {
|
||||||
|
doctype: 'Assignment Rule',
|
||||||
|
name: props.data.name,
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
assignmentRulesList.reload()
|
||||||
|
isConfirmingDelete.value = false
|
||||||
|
toast.success(__('Assignment rule deleted'))
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownOptions = [
|
||||||
|
{
|
||||||
|
label: __('Duplicate'),
|
||||||
|
onClick: () => {
|
||||||
|
duplicateDialog.value = {
|
||||||
|
show: true,
|
||||||
|
name: props.data.name + ' (Copy)',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: 'copy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Delete'),
|
||||||
|
component: (props) =>
|
||||||
|
TemplateOption({
|
||||||
|
option: __('Delete'),
|
||||||
|
icon: 'trash-2',
|
||||||
|
active: props.active,
|
||||||
|
onClick: (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
isConfirmingDelete.value = true
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
condition: () => !isConfirmingDelete.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Confirm Delete'),
|
||||||
|
component: (props) =>
|
||||||
|
TemplateOption({
|
||||||
|
option: __('Confirm Delete'),
|
||||||
|
icon: 'trash-2',
|
||||||
|
active: props.active,
|
||||||
|
theme: 'danger',
|
||||||
|
onClick: () => deleteAssignmentRule(),
|
||||||
|
}),
|
||||||
|
condition: () => isConfirmingDelete.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const duplicate = () => {
|
||||||
|
createResource({
|
||||||
|
url: 'crm.api.assignment_rule.duplicate_assignment_rule',
|
||||||
|
params: {
|
||||||
|
docname: props.data.name,
|
||||||
|
new_name: duplicateDialog.value.name,
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
assignmentRulesList.reload()
|
||||||
|
toast.success(__('Assignment rule duplicated'))
|
||||||
|
duplicateDialog.value.show = false
|
||||||
|
duplicateDialog.value.name = ''
|
||||||
|
updateStep('view', data)
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPriorityChange = () => {
|
||||||
|
setAssignmentRuleValue('priority', props.data.priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onToggle = () => {
|
||||||
|
if (!props.data.users_exists && props.data.disabled) {
|
||||||
|
toast.error(__('Cannot enable rule without adding users in it'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setAssignmentRuleValue('disabled', !props.data.disabled, 'status')
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAssignmentRuleValue = (key, value, fieldName = undefined) => {
|
||||||
|
createResource({
|
||||||
|
url: 'frappe.client.set_value',
|
||||||
|
params: {
|
||||||
|
doctype: 'Assignment Rule',
|
||||||
|
name: props.data.name,
|
||||||
|
fieldname: key,
|
||||||
|
value: value,
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
assignmentRulesList.reload()
|
||||||
|
toast.success(__('Assignment rule {0} updated', [fieldName || key]))
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<AssignmentRules v-if="step.screen === 'list'" />
|
||||||
|
<AssignmentRuleView v-else-if="step.screen === 'view'" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, provide } from 'vue'
|
||||||
|
import AssignmentRules from './AssignmentRules.vue'
|
||||||
|
import AssignmentRuleView from './AssignmentRuleView.vue'
|
||||||
|
|
||||||
|
const step = ref({ screen: 'list', data: null })
|
||||||
|
|
||||||
|
provide('step', step)
|
||||||
|
provide('updateStep', updateStep)
|
||||||
|
|
||||||
|
function updateStep(newStep, data) {
|
||||||
|
step.value = { screen: newStep, data }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="assignmentRulesList.loading && !assignmentRulesList.data"
|
||||||
|
class="flex items-center justify-center mt-12"
|
||||||
|
>
|
||||||
|
<LoadingIndicator class="w-4" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-if="assignmentRulesList.data?.length === 0"
|
||||||
|
class="flex items-center justify-center rounded-md border border-gray-200 p-4"
|
||||||
|
>
|
||||||
|
<div class="text-sm text-ink-gray-7">
|
||||||
|
{{ __('No items in the list') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="grid grid-cols-11 items-center gap-4 text-sm text-gray-600">
|
||||||
|
<div class="col-span-7 ml-2">{{ __('Assignment rule') }}</div>
|
||||||
|
<div class="col-span-2">{{ __('Priority') }}</div>
|
||||||
|
<div class="col-span-2">{{ __('Enabled') }}</div>
|
||||||
|
</div>
|
||||||
|
<hr class="mt-2 mx-2" />
|
||||||
|
<div
|
||||||
|
v-for="assignmentRule in assignmentRulesList.data"
|
||||||
|
:key="assignmentRule.name"
|
||||||
|
>
|
||||||
|
<AssignmentRuleListItem :data="assignmentRule" />
|
||||||
|
<hr class="mx-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { LoadingIndicator } from 'frappe-ui'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import AssignmentRuleListItem from './AssignmentRuleListItem.vue'
|
||||||
|
|
||||||
|
const assignmentRulesList = inject('assignmentRulesList')
|
||||||
|
</script>
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<CFConditions
|
||||||
|
v-if="props.conditions.length > 0"
|
||||||
|
:conditions="props.conditions"
|
||||||
|
:level="0"
|
||||||
|
:disableAddCondition="props.errors !== ''"
|
||||||
|
:doctype="props.doctype"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="props.conditions.length == 0"
|
||||||
|
class="flex p-4 items-center cursor-pointer justify-center gap-2 text-sm border border-gray-300 text-gray-600 rounded-md"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
props.conditions.push(['', '', ''])
|
||||||
|
validateAssignmentRule(props.name)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
{{ __('Add a condition') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between mt-2">
|
||||||
|
<div class="" v-if="props.conditions.length > 0">
|
||||||
|
<Dropdown v-slot="{ open }" :options="dropdownOptions">
|
||||||
|
<Button
|
||||||
|
:disabled="props.errors !== ''"
|
||||||
|
:icon-right="open ? 'chevron-up' : 'chevron-down'"
|
||||||
|
:label="__('Add condition')"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<ErrorMessage v-if="props.conditions.length > 0" :message="props.errors" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Button, Dropdown, ErrorMessage, FeatherIcon } from 'frappe-ui'
|
||||||
|
import { watchDebounced } from '@vueuse/core'
|
||||||
|
import { validateConditions } from '@/utils'
|
||||||
|
import CFConditions from '../../ConditionsFilter/CFConditions.vue'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
conditions: Array,
|
||||||
|
name: String,
|
||||||
|
errors: String,
|
||||||
|
doctype: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateAssignmentRule = inject('validateAssignmentRule')
|
||||||
|
|
||||||
|
const getConjunction = () => {
|
||||||
|
let conjunction = 'and'
|
||||||
|
props.conditions.forEach((condition) => {
|
||||||
|
if (typeof condition == 'string') {
|
||||||
|
conjunction = condition
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return conjunction
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownOptions = [
|
||||||
|
{
|
||||||
|
label: __('Add condition'),
|
||||||
|
onClick: () => {
|
||||||
|
addCondition()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Add condition group'),
|
||||||
|
onClick: () => {
|
||||||
|
const conjunction = getConjunction()
|
||||||
|
props.conditions.push(conjunction, [[]])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const addCondition = () => {
|
||||||
|
const isValid = validateConditions(props.conditions)
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const conjunction = getConjunction()
|
||||||
|
|
||||||
|
props.conditions.push(conjunction, ['', '', ''])
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
() => [...props.conditions],
|
||||||
|
() => {
|
||||||
|
validateAssignmentRule(props.name)
|
||||||
|
},
|
||||||
|
{ deep: true, debounce: 300 },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-md border px-2 border-gray-300 text-sm">
|
||||||
|
<div
|
||||||
|
class="grid p-2 px-4 items-center"
|
||||||
|
style="grid-template-columns: 3fr 1fr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="column in columns"
|
||||||
|
:key="column.key"
|
||||||
|
class="text-gray-600 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||||
|
>
|
||||||
|
{{ column.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<AssignmentScheduleItem
|
||||||
|
v-for="(day, index) in days"
|
||||||
|
:key="day.day"
|
||||||
|
:data="day"
|
||||||
|
:isLast="index === days.length - 1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ErrorMessage :message="assignmentRuleErrors.assignmentDays" class="mt-2" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ErrorMessage } from 'frappe-ui'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import AssignmentScheduleItem from './AssignmentScheduleItem.vue'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
|
||||||
|
const assignmentRuleData = inject('assignmentRuleData')
|
||||||
|
const assignmentRuleErrors = inject('assignmentRuleErrors')
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
label: 'Days',
|
||||||
|
key: 'day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Active',
|
||||||
|
key: 'active',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const days = ref([
|
||||||
|
{
|
||||||
|
day: 'Monday',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 'Tuesday',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 'Wednesday',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 'Thursday',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 'Friday',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 'Saturday',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 'Sunday',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
assignmentRuleData.value.assignmentDays.forEach((day) => {
|
||||||
|
const workDay = days.value.find((d) => d.day === day)
|
||||||
|
if (workDay) {
|
||||||
|
workDay.active = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="grid py-3.5 px-4 items-center"
|
||||||
|
style="grid-template-columns: 3fr 1fr"
|
||||||
|
>
|
||||||
|
<div class="text-ink-gray-7 font-medium">{{ data.day }}</div>
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<Switch v-model="data.active" @update:model-value="toggleDay" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr v-if="!isLast" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Switch } from 'frappe-ui'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
|
||||||
|
const assignmentRuleData = inject('assignmentRuleData')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isLast: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleDay = (isActive) => {
|
||||||
|
const dayIndex = assignmentRuleData.value.assignmentDays.findIndex(
|
||||||
|
(d) => d === props.data.day,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isActive && dayIndex === -1) {
|
||||||
|
assignmentRuleData.value.assignmentDays.push(props.data.day)
|
||||||
|
} else {
|
||||||
|
assignmentRuleData.value.assignmentDays.splice(dayIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -3,6 +3,7 @@
|
|||||||
v-model="showSettings"
|
v-model="showSettings"
|
||||||
:options="{ size: '5xl' }"
|
:options="{ size: '5xl' }"
|
||||||
@close="activeSettingsPage = ''"
|
@close="activeSettingsPage = ''"
|
||||||
|
:disableOutsideClickToClose="disableSettingModalOutsideClick"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||||
@ -46,6 +47,7 @@ import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
|
|||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import EmailTemplateIcon from '@/components/Icons/EmailTemplateIcon.vue'
|
import EmailTemplateIcon from '@/components/Icons/EmailTemplateIcon.vue'
|
||||||
|
import SettingsIcon2 from '@/components/Icons/SettingsIcon2.vue'
|
||||||
import Users from '@/components/Settings/Users.vue'
|
import Users from '@/components/Settings/Users.vue'
|
||||||
import GeneralSettingsPage from '@/components/Settings/General/GeneralSettingsPage.vue'
|
import GeneralSettingsPage from '@/components/Settings/General/GeneralSettingsPage.vue'
|
||||||
import InviteUserPage from '@/components/Settings/InviteUserPage.vue'
|
import InviteUserPage from '@/components/Settings/InviteUserPage.vue'
|
||||||
@ -61,9 +63,11 @@ import {
|
|||||||
isWhatsappInstalled,
|
isWhatsappInstalled,
|
||||||
showSettings,
|
showSettings,
|
||||||
activeSettingsPage,
|
activeSettingsPage,
|
||||||
|
disableSettingModalOutsideClick,
|
||||||
} from '@/composables/settings'
|
} from '@/composables/settings'
|
||||||
import { Dialog, Avatar } from 'frappe-ui'
|
import { Dialog, Avatar } from 'frappe-ui'
|
||||||
import { ref, markRaw, computed, watch, h } from 'vue'
|
import { ref, markRaw, computed, watch, h } from 'vue'
|
||||||
|
import AssignmentRulePage from './AssignmentRules/AssignmentRulePage.vue'
|
||||||
|
|
||||||
const { isManager, isTelephonyAgent, getUser } = usersStore()
|
const { isManager, isTelephonyAgent, getUser } = usersStore()
|
||||||
|
|
||||||
@ -114,6 +118,11 @@ const tabs = computed(() => {
|
|||||||
icon: EmailTemplateIcon,
|
icon: EmailTemplateIcon,
|
||||||
component: markRaw(EmailTemplatePage),
|
component: markRaw(EmailTemplatePage),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: __('Assignment rules'),
|
||||||
|
icon: markRaw(h(SettingsIcon2, { class: 'rotate-90' })),
|
||||||
|
component: markRaw(AssignmentRulePage),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -41,4 +41,7 @@ export const mobileSidebarOpened = ref(false)
|
|||||||
export const isMobileView = computed(() => window.innerWidth < 768)
|
export const isMobileView = computed(() => window.innerWidth < 768)
|
||||||
|
|
||||||
export const showSettings = ref(false)
|
export const showSettings = ref(false)
|
||||||
|
|
||||||
|
export const disableSettingModalOutsideClick = ref(false)
|
||||||
|
|
||||||
export const activeSettingsPage = ref('')
|
export const activeSettingsPage = ref('')
|
||||||
|
|||||||
@ -529,3 +529,163 @@ export function copy(obj) {
|
|||||||
if (!obj) return obj
|
if (!obj) return obj
|
||||||
return JSON.parse(JSON.stringify(obj))
|
return JSON.parse(JSON.stringify(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const convertToConditions = ({ conditions, fieldPrefix }) => {
|
||||||
|
if (!conditions || conditions.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const processCondition = (condition) => {
|
||||||
|
if (typeof condition === 'string') {
|
||||||
|
return condition.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(condition)) {
|
||||||
|
// Nested condition group
|
||||||
|
if (Array.isArray(condition[0])) {
|
||||||
|
const nestedStr = convertToConditions({
|
||||||
|
conditions: condition,
|
||||||
|
fieldPrefix,
|
||||||
|
})
|
||||||
|
return `(${nestedStr})`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple condition: [fieldname, operator, value]
|
||||||
|
const [field, operator, value] = condition
|
||||||
|
const fieldAccess = fieldPrefix ? `${fieldPrefix}.${field}` : field
|
||||||
|
|
||||||
|
const operatorMap = {
|
||||||
|
equals: '==',
|
||||||
|
'=': '==',
|
||||||
|
'==': '==',
|
||||||
|
'!=': '!=',
|
||||||
|
'not equals': '!=',
|
||||||
|
'<': '<',
|
||||||
|
'<=': '<=',
|
||||||
|
'>': '>',
|
||||||
|
'>=': '>=',
|
||||||
|
in: 'in',
|
||||||
|
'not in': 'not in',
|
||||||
|
like: 'like',
|
||||||
|
'not like': 'not like',
|
||||||
|
is: 'is',
|
||||||
|
'is not': 'is not',
|
||||||
|
between: 'between',
|
||||||
|
}
|
||||||
|
|
||||||
|
let op = operatorMap[operator.toLowerCase()] || operator
|
||||||
|
|
||||||
|
if (
|
||||||
|
(op === '==' || op === '!=') &&
|
||||||
|
(String(value).toLowerCase() === 'yes' ||
|
||||||
|
String(value).toLowerCase() === 'no')
|
||||||
|
) {
|
||||||
|
let checkVal = String(value).toLowerCase() === 'yes'
|
||||||
|
if (op === '!=') {
|
||||||
|
checkVal = !checkVal
|
||||||
|
}
|
||||||
|
return checkVal ? fieldAccess : `not ${fieldAccess}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op === 'is' && String(value).toLowerCase() === 'set') {
|
||||||
|
return fieldAccess
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(op === 'is' && String(value).toLowerCase() === 'not set') ||
|
||||||
|
(op === 'is not' && String(value).toLowerCase() === 'set')
|
||||||
|
) {
|
||||||
|
return `not ${fieldAccess}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op === 'like') {
|
||||||
|
return `(${fieldAccess} and "${value}" in ${fieldAccess})`
|
||||||
|
}
|
||||||
|
if (op === 'not like') {
|
||||||
|
return `(${fieldAccess} and "${value}" not in ${fieldAccess})`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
op === 'between' &&
|
||||||
|
typeof value === 'string' &&
|
||||||
|
value.includes(',')
|
||||||
|
) {
|
||||||
|
const [start, end] = value.split(',').map((v) => v.trim())
|
||||||
|
return `(${fieldAccess} >= "${start}" and ${fieldAccess} <= "${end}")`
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueStr = ''
|
||||||
|
if (op === 'in' || op === 'not in') {
|
||||||
|
let items
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
items = value.map((v) => `"${String(v).trim()}"`)
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
items = value.split(',').map((v) => `"${v.trim()}"`)
|
||||||
|
} else {
|
||||||
|
items = [`"${String(value).trim()}"`]
|
||||||
|
}
|
||||||
|
valueStr = `[${items.join(', ')}]`
|
||||||
|
return `(${fieldAccess} and ${fieldAccess} ${op} ${valueStr})`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
valueStr = `"${value.replace(/"/g, '\\"')}"`
|
||||||
|
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
valueStr = String(value)
|
||||||
|
} else if (value === null || value === undefined) {
|
||||||
|
return op === '==' || op === 'is' ? `not ${fieldAccess}` : fieldAccess
|
||||||
|
} else {
|
||||||
|
valueStr = `"${String(value).replace(/"/g, '\\"')}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${fieldAccess} ${op} ${valueStr}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = conditions.map(processCondition)
|
||||||
|
return parts.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateConditions(conditions) {
|
||||||
|
if (!Array.isArray(conditions)) return false
|
||||||
|
|
||||||
|
// Handle simple condition [field, operator, value]
|
||||||
|
if (
|
||||||
|
conditions.length === 3 &&
|
||||||
|
typeof conditions[0] === 'string' &&
|
||||||
|
typeof conditions[1] === 'string'
|
||||||
|
) {
|
||||||
|
return conditions[0] !== '' && conditions[1] !== '' && conditions[2] !== ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through conditions and logical operators
|
||||||
|
for (let i = 0; i < conditions.length; i++) {
|
||||||
|
const item = conditions[i]
|
||||||
|
|
||||||
|
// Skip logical operators (they will be validated by their position)
|
||||||
|
if (item === 'and' || item === 'or') {
|
||||||
|
// Ensure logical operators are not at start/end and not consecutive
|
||||||
|
if (
|
||||||
|
i === 0 ||
|
||||||
|
i === conditions.length - 1 ||
|
||||||
|
conditions[i - 1] === 'and' ||
|
||||||
|
conditions[i - 1] === 'or'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nested conditions (arrays)
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
if (!validateConditions(item)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (item !== undefined && item !== null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditions.length > 0
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user