Merge pull request #1260 from frappe/mergify/bp/main-hotfix/pr-1256
This commit is contained in:
commit
fabd362b2a
@ -25,7 +25,7 @@ class CRMDeal(Document):
|
|||||||
add_status_change_log(self)
|
add_status_change_log(self)
|
||||||
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
|
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
|
||||||
self.closed_date = frappe.utils.nowdate()
|
self.closed_date = frappe.utils.nowdate()
|
||||||
self.validate_forcasting_fields()
|
self.validate_forecasting_fields()
|
||||||
self.validate_lost_reason()
|
self.validate_lost_reason()
|
||||||
self.update_exchange_rate()
|
self.update_exchange_rate()
|
||||||
|
|
||||||
@ -151,9 +151,21 @@ class CRMDeal(Document):
|
|||||||
if not self.probability or self.probability == 0:
|
if not self.probability or self.probability == 0:
|
||||||
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
|
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
|
||||||
|
|
||||||
def validate_forcasting_fields(self):
|
def update_expected_deal_value(self):
|
||||||
|
"""
|
||||||
|
Update the expected deal value based on the net total or total.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
frappe.db.get_single_value("FCRM Settings", "auto_update_expected_deal_value")
|
||||||
|
and (self.net_total or self.total)
|
||||||
|
and self.expected_deal_value
|
||||||
|
):
|
||||||
|
self.expected_deal_value = self.net_total or self.total
|
||||||
|
|
||||||
|
def validate_forecasting_fields(self):
|
||||||
self.update_closed_date()
|
self.update_closed_date()
|
||||||
self.update_default_probability()
|
self.update_default_probability()
|
||||||
|
self.update_expected_deal_value()
|
||||||
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
||||||
if not self.expected_deal_value or self.expected_deal_value == 0:
|
if not self.expected_deal_value or self.expected_deal_value == 0:
|
||||||
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)
|
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"defaults_tab",
|
"defaults_tab",
|
||||||
"restore_defaults",
|
"restore_defaults",
|
||||||
"enable_forecasting",
|
"enable_forecasting",
|
||||||
|
"auto_update_expected_deal_value",
|
||||||
"currency_tab",
|
"currency_tab",
|
||||||
"currency",
|
"currency",
|
||||||
"exchange_rate_provider_section",
|
"exchange_rate_provider_section",
|
||||||
@ -105,12 +106,19 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_vqck",
|
"fieldname": "column_break_vqck",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"description": "Automatically update \"Expected Deal Value\" based on the total value of associated products in a deal",
|
||||||
|
"fieldname": "auto_update_expected_deal_value",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Auto Update Expected Deal Value"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-29 11:26:50.420614",
|
"modified": "2025-09-16 17:33:26.406549",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "FCRM Settings",
|
"name": "FCRM Settings",
|
||||||
|
|||||||
10
frontend/components.d.ts
vendored
10
frontend/components.d.ts
vendored
@ -33,7 +33,7 @@ declare module 'vue' {
|
|||||||
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
||||||
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
|
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
|
||||||
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
|
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
|
||||||
BrandSettings: typeof import('./src/components/Settings/General/BrandSettings.vue')['default']
|
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
|
||||||
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
|
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
|
||||||
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
|
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
|
||||||
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
|
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
|
||||||
@ -63,7 +63,7 @@ declare module 'vue' {
|
|||||||
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
||||||
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||||
CurrencySettings: typeof import('./src/components/Settings/General/CurrencySettings.vue')['default']
|
CurrencySettings: typeof import('./src/components/Settings/CurrencySettings.vue')['default']
|
||||||
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
||||||
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
|
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
|
||||||
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
||||||
@ -127,11 +127,10 @@ declare module 'vue' {
|
|||||||
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
||||||
Filter: typeof import('./src/components/Filter.vue')['default']
|
Filter: typeof import('./src/components/Filter.vue')['default']
|
||||||
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
||||||
|
ForecastingSettings: typeof import('./src/components/Settings/ForecastingSettings.vue')['default']
|
||||||
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
|
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
|
||||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
||||||
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
||||||
GeneralSettings: typeof import('./src/components/Settings/General/GeneralSettings.vue')['default']
|
|
||||||
GeneralSettingsPage: typeof import('./src/components/Settings/General/GeneralSettingsPage.vue')['default']
|
|
||||||
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
|
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
|
||||||
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
||||||
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
||||||
@ -142,7 +141,7 @@ declare module 'vue' {
|
|||||||
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
|
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
|
||||||
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
|
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
|
||||||
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
|
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
|
||||||
HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default']
|
HomeActions: typeof import('./src/components/Settings/HomeActions.vue')['default']
|
||||||
Icon: typeof import('./src/components/Icon.vue')['default']
|
Icon: typeof import('./src/components/Icon.vue')['default']
|
||||||
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
|
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
|
||||||
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
|
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
|
||||||
@ -229,6 +228,7 @@ declare module 'vue' {
|
|||||||
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
|
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
|
||||||
SortBy: typeof import('./src/components/SortBy.vue')['default']
|
SortBy: typeof import('./src/components/SortBy.vue')['default']
|
||||||
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
|
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
|
||||||
|
SparkleIcon: typeof import('./src/components/Icons/SparkleIcon.vue')['default']
|
||||||
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
|
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
|
||||||
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
|
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
|
||||||
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']
|
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']
|
||||||
|
|||||||
@ -110,7 +110,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { TemplateOption } from '@/utils'
|
|
||||||
import {
|
import {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
Button,
|
Button,
|
||||||
@ -191,31 +190,17 @@ const dropdownOptions = computed(() => {
|
|||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
label: __('Remove'),
|
label: __('Remove'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
variant: 'red',
|
||||||
option: __('Remove'),
|
onClick: () => emit('remove'),
|
||||||
icon: 'trash-2',
|
|
||||||
active: props.active,
|
|
||||||
variant: 'danger',
|
|
||||||
onClick: () => {
|
|
||||||
emit('remove')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
condition: () => !props.isGroup,
|
condition: () => !props.isGroup,
|
||||||
})
|
})
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
label: __('Remove group'),
|
label: __('Remove group'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
variant: 'red',
|
||||||
option: __('Remove group'),
|
onClick: () => emit('remove'),
|
||||||
icon: 'trash-2',
|
|
||||||
active: props.active,
|
|
||||||
variant: 'danger',
|
|
||||||
onClick: () => {
|
|
||||||
emit('remove')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
condition: () => props.isGroup,
|
condition: () => props.isGroup,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-lg border border-gray-300 p-3 flex flex-col gap-4 w-full">
|
<div class="rounded-lg border border-outline-gray-2 p-3 flex flex-col gap-4 w-full">
|
||||||
<template v-for="(condition, i) in props.conditions" :key="condition.field">
|
<template v-for="(condition, i) in props.conditions" :key="condition.field">
|
||||||
<CFCondition
|
<CFCondition
|
||||||
v-if="Array.isArray(condition)"
|
v-if="Array.isArray(condition)"
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
:file-types="image_type"
|
:file-types="image_type"
|
||||||
class="text-base"
|
|
||||||
@success="
|
@success="
|
||||||
(file) => {
|
(file) => {
|
||||||
$emit('upload', file.file_url)
|
$emit('upload', file.file_url)
|
||||||
@ -10,21 +9,28 @@
|
|||||||
>
|
>
|
||||||
<template v-slot="{ progress, uploading, openFileSelector }">
|
<template v-slot="{ progress, uploading, openFileSelector }">
|
||||||
<div class="flex items-end space-x-1">
|
<div class="flex items-end space-x-1">
|
||||||
<Button @click="openFileSelector">
|
<Button
|
||||||
{{
|
@click="openFileSelector"
|
||||||
|
:iconLeft="uploading ? 'cloud-upload' : ImageUpIcon"
|
||||||
|
:label="
|
||||||
uploading
|
uploading
|
||||||
? `Uploading ${progress}%`
|
? __('Uploading {0}%', [progress])
|
||||||
: image_url
|
: image_url
|
||||||
? 'Change'
|
? __('Change')
|
||||||
: 'Upload'
|
: __('Upload')
|
||||||
}}
|
"
|
||||||
</Button>
|
/>
|
||||||
<Button v-if="image_url" @click="$emit('remove')">Remove</Button>
|
<Button
|
||||||
|
v-if="image_url"
|
||||||
|
:label="__('Remove')"
|
||||||
|
@click="$emit('remove')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ImageUpIcon from '~icons/lucide/image-up'
|
||||||
import { FileUploader, Button } from 'frappe-ui'
|
import { FileUploader, Button } from 'frappe-ui'
|
||||||
|
|
||||||
const prop = defineProps({
|
const prop = defineProps({
|
||||||
@ -33,10 +39,6 @@ const prop = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'image/*',
|
default: 'image/*',
|
||||||
},
|
},
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['upload', 'remove'])
|
const emit = defineEmits(['upload', 'remove'])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
16
frontend/src/components/Icons/SparkleIcon.vue
Normal file
16
frontend/src/components/Icons/SparkleIcon.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M3.37543 1.93494L4.21632 2.21494C4.35232 2.26027 4.44388 2.38738 4.44388 2.53138C4.44388 2.67538 4.35143 2.80249 4.21543 2.84783L3.37454 3.12783L3.09365 3.9696C3.04921 4.10472 2.92121 4.19716 2.7781 4.19716C2.63499 4.19716 2.50787 4.1056 2.46254 3.9696L2.18165 3.12783L1.34076 2.84783C1.20476 2.80249 1.11232 2.67538 1.11232 2.53138C1.11232 2.38738 1.20476 2.26027 1.34076 2.21494L2.18165 1.93494L2.46254 1.09316C2.55321 0.82116 3.00387 0.82116 3.09454 1.09316L3.37543 1.93494ZM8.44852 1.33394C8.3643 1.16325 8.19046 1.05518 8.00012 1.05518C7.80978 1.05518 7.63595 1.16325 7.55173 1.33394L5.67697 5.13368L1.48388 5.74214C1.29552 5.76947 1.13901 5.90137 1.08017 6.08238C1.02133 6.26339 1.07036 6.46211 1.20665 6.59497L4.24065 9.55281L3.52421 13.7284C3.49203 13.916 3.56913 14.1056 3.7231 14.2174C3.87706 14.3293 4.08119 14.3441 4.24966 14.2555L8.11188 12.2253C8.35631 12.0968 8.4503 11.7945 8.32181 11.5501C8.19333 11.3057 7.89102 11.2117 7.64659 11.3402L4.68114 12.899L5.2707 9.46284C5.29853 9.30065 5.24477 9.13514 5.12693 9.02027L2.63025 6.58626L6.08082 6.08555C6.24373 6.06191 6.38457 5.95959 6.45741 5.81196L8.00012 2.6852L9.54284 5.81196C9.61568 5.95959 9.75652 6.06191 9.91943 6.08555L13.37 6.58625L11.6235 8.2887C11.4258 8.48146 11.4218 8.79802 11.6145 8.99575C11.8073 9.19349 12.1239 9.19752 12.3216 9.00476L14.7936 6.59498C14.9299 6.46212 14.9789 6.2634 14.9201 6.08239C14.8612 5.90138 14.7047 5.76947 14.5164 5.74214L10.3233 5.13368L8.44852 1.33394ZM13.4744 11.9911L12.3517 11.6168L11.9775 10.4942C11.8557 10.1315 11.2557 10.1315 11.1339 10.4942L10.7597 11.6168L9.63702 11.9911C9.45569 12.0515 9.33302 12.2213 9.33302 12.4124C9.33302 12.6035 9.45569 12.7733 9.63702 12.8337L10.7597 13.2079L11.1339 14.3306C11.1944 14.5119 11.365 14.6346 11.5561 14.6346C11.7472 14.6346 11.917 14.5119 11.9784 14.3306L12.3526 13.2079L13.4752 12.8337C13.6566 12.7733 13.7792 12.6035 13.7792 12.4124C13.7792 12.2213 13.6566 12.0515 13.4752 11.9911H13.4744ZM13.3333 2.88883C13.3333 3.25702 13.0349 3.5555 12.6667 3.5555C12.2985 3.5555 12 3.25702 12 2.88883C12 2.52064 12.2985 2.22217 12.6667 2.22217C13.0349 2.22217 13.3333 2.52064 13.3333 2.88883Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -6,10 +6,7 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-p-sm text-ink-gray-6">
|
<span class="text-p-sm text-ink-gray-6">
|
||||||
{{
|
{{
|
||||||
__(
|
__('Choose how {0} are assigned among salespeople.', [documentType])
|
||||||
'Define who receives the {0} and how they’re distributed among agents.',
|
|
||||||
[documentType],
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -26,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-p-sm text-ink-gray-6 mt-1">
|
<div class="text-p-sm text-ink-gray-6 mt-1">
|
||||||
{{
|
{{
|
||||||
__('Choose how {0} are distributed among selected assignees.', [
|
__('Choose how {0} are assigned among the selected assignees.', [
|
||||||
documentType,
|
documentType,
|
||||||
])
|
])
|
||||||
}}
|
}}
|
||||||
@ -36,7 +33,7 @@
|
|||||||
<Popover placement="bottom-end">
|
<Popover placement="bottom-end">
|
||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] select-none min-w-40"
|
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-outline-gray-2 bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] select-none min-w-40"
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@ -84,7 +81,7 @@
|
|||||||
{{ __('Assignees') }}
|
{{ __('Assignees') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-p-sm text-ink-gray-6 mt-1">
|
<div class="text-p-sm text-ink-gray-6 mt-1">
|
||||||
{{ __('Choose who receives the {0}.', [documentType]) }}
|
{{ __('Select the assignees for {0}.', [documentType]) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AssigneeSearch @addAssignee="validateAssignmentRule('users')" />
|
<AssigneeSearch @addAssignee="validateAssignmentRule('users')" />
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-11 items-center gap-4 cursor-pointer hover:bg-gray-50 rounded"
|
class="flex p-3 items-center justify-between cursor-pointer hover:bg-surface-menu-bar rounded"
|
||||||
>
|
>
|
||||||
<div class="w-full py-3 pl-2 col-span-7" @click="updateStep('view', data)">
|
<div class="w-7/12" @click="updateStep('view', data)">
|
||||||
<div class="text-base text-ink-gray-7 font-medium">{{ data.name }}</div>
|
<div class="text-base text-ink-gray-7 font-medium">{{ data.name }}</div>
|
||||||
<div
|
<div
|
||||||
v-if="data.description && data.description.length > 0"
|
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"
|
class="text-p-base w-full text-ink-gray-5 mt-0.5 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||||
>
|
>
|
||||||
{{ data.description }}
|
{{ data.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
<div class="w-3/12">
|
||||||
<Select
|
<Select
|
||||||
class="w-max bg-transparent -ml-2 border-0 text-ink-gray-6 focus-visible:!ring-0 bg-none"
|
class="w-max -ml-2 bg-transparent border-0 text-ink-gray-6 focus-visible:!ring-0 bg-none"
|
||||||
:options="priorityOptions"
|
:options="priorityOptions"
|
||||||
v-model="data.priority"
|
v-model="data.priority"
|
||||||
@update:modelValue="onPriorityChange"
|
@update:modelValue="onPriorityChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center w-full pr-2 col-span-2">
|
<div class="flex justify-between items-center w-2/12">
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
:modelValue="!data.disabled"
|
:modelValue="!data.disabled"
|
||||||
@ -72,7 +72,6 @@ import {
|
|||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { inject, ref } from 'vue'
|
import { inject, ref } from 'vue'
|
||||||
import { TemplateOption } from '@/utils'
|
|
||||||
|
|
||||||
const assignmentRulesList = inject('assignmentRulesList')
|
const assignmentRulesList = inject('assignmentRulesList')
|
||||||
const updateStep = inject('updateStep')
|
const updateStep = inject('updateStep')
|
||||||
@ -128,29 +127,19 @@ const dropdownOptions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Delete'),
|
label: __('Delete'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
onClick: (e) => {
|
||||||
option: __('Delete'),
|
e.preventDefault()
|
||||||
icon: 'trash-2',
|
e.stopImmediatePropagation()
|
||||||
active: props.active,
|
isConfirmingDelete.value = true
|
||||||
onClick: (e) => {
|
},
|
||||||
e.preventDefault()
|
|
||||||
e.stopImmediatePropagation()
|
|
||||||
isConfirmingDelete.value = true
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
condition: () => !isConfirmingDelete.value,
|
condition: () => !isConfirmingDelete.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Confirm Delete'),
|
label: __('Confirm Delete'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
theme: 'red',
|
||||||
option: __('Confirm Delete'),
|
onClick: () => deleteAssignmentRule(),
|
||||||
icon: 'trash-2',
|
|
||||||
active: props.active,
|
|
||||||
theme: 'danger',
|
|
||||||
onClick: () => deleteAssignmentRule(),
|
|
||||||
}),
|
|
||||||
condition: () => isConfirmingDelete.value,
|
condition: () => isConfirmingDelete.value,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
|
||||||
v-if="getAssignmentRuleData.loading"
|
|
||||||
class="flex items-center h-full justify-center"
|
|
||||||
>
|
|
||||||
<LoadingIndicator class="w-4" />
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="!getAssignmentRuleData.loading"
|
v-if="!getAssignmentRuleData.loading"
|
||||||
class="sticky top-0 z-10 bg-white pb-6 px-10 py-8"
|
class="flex flex-col h-full gap-6 px-6 py-8 text-ink-gray-8"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full">
|
<div class="flex items-center justify-between px-2 w-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -47,276 +41,282 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="overflow-y-auto px-2">
|
||||||
<div v-if="!getAssignmentRuleData.loading" class="overflow-y-auto px-10 pb-8">
|
<div class="grid grid-cols-2 gap-5">
|
||||||
<div class="grid grid-cols-2 gap-5">
|
<div>
|
||||||
<div>
|
<FormControl
|
||||||
<FormControl
|
:type="'text'"
|
||||||
:type="'text'"
|
size="sm"
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
:placeholder="__('Name')"
|
|
||||||
:label="__('Name')"
|
|
||||||
v-model="assignmentRuleData.assignmentRuleName"
|
|
||||||
required
|
|
||||||
maxlength="50"
|
|
||||||
@change="validateAssignmentRule('assignmentRuleName')"
|
|
||||||
/>
|
|
||||||
<ErrorMessage
|
|
||||||
:message="assignmentRuleErrors.assignmentRuleName"
|
|
||||||
class="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<FormLabel :label="__('Priority')" />
|
|
||||||
<Popover>
|
|
||||||
<template #target="{ togglePopover }">
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] cursor-default"
|
|
||||||
@click="togglePopover()"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{{
|
|
||||||
priorityOptions.find(
|
|
||||||
(option) => option.value == assignmentRuleData.priority,
|
|
||||||
)?.label
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<FeatherIcon name="chevron-down" class="size-4" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body="{ togglePopover }">
|
|
||||||
<div
|
|
||||||
class="p-1 text-ink-gray-6 top-1 absolute w-full bg-white shadow-2xl rounded"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="option in priorityOptions"
|
|
||||||
:key="option.value"
|
|
||||||
class="p-2 cursor-pointer hover:bg-gray-50 text-base flex items-center justify-between rounded"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
assignmentRuleData.priority = option.value
|
|
||||||
togglePopover()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
<FeatherIcon
|
|
||||||
v-if="assignmentRuleData.priority == option.value"
|
|
||||||
name="check"
|
|
||||||
class="size-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormControl
|
|
||||||
:type="'textarea'"
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
:placeholder="__('Description')"
|
|
||||||
:label="__('Description')"
|
|
||||||
required
|
|
||||||
maxlength="250"
|
|
||||||
@change="validateAssignmentRule('description')"
|
|
||||||
v-model="assignmentRuleData.description"
|
|
||||||
/>
|
|
||||||
<ErrorMessage
|
|
||||||
:message="assignmentRuleErrors.description"
|
|
||||||
class="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<FormLabel :label="__('Apply on')" />
|
|
||||||
<Select
|
|
||||||
:options="[
|
|
||||||
{
|
|
||||||
label: 'Lead',
|
|
||||||
value: 'CRM Lead',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Deal',
|
|
||||||
value: 'CRM Deal',
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
v-model="assignmentRuleData.documentType"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class="my-8" />
|
|
||||||
<div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<span class="text-lg font-semibold text-ink-gray-8">{{
|
|
||||||
__('Assignment condition')
|
|
||||||
}}</span>
|
|
||||||
<div class="flex items-center justify-between gap-6">
|
|
||||||
<span class="text-p-sm text-ink-gray-6">
|
|
||||||
{{
|
|
||||||
__('Choose which {0} are affected by this assignment rule.', [
|
|
||||||
documentType,
|
|
||||||
])
|
|
||||||
}}
|
|
||||||
<a
|
|
||||||
class="font-medium underline"
|
|
||||||
href="https://docs.frappe.io/crm/assignment-rule"
|
|
||||||
target="_blank"
|
|
||||||
>{{ __('Learn about conditions') }}</a
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<div v-if="isOldSla && step.data">
|
|
||||||
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
|
|
||||||
<template #target>
|
|
||||||
<div
|
|
||||||
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
|
|
||||||
>
|
|
||||||
<span>{{ __('Old Condition') }}</span>
|
|
||||||
<FeatherIcon name="info" class="size-4" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body-main>
|
|
||||||
<div
|
|
||||||
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
|
|
||||||
>
|
|
||||||
<code>{{ assignmentRuleData.assignCondition }}</code>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-gray-300 rounded-md p-3 py-4"
|
|
||||||
v-if="!useNewUI && assignmentRuleData.assignCondition"
|
|
||||||
>
|
|
||||||
<span class="text-p-sm">
|
|
||||||
{{ __('Conditions for this rule were created from') }}
|
|
||||||
<a :href="deskUrl" target="_blank" class="underline">{{
|
|
||||||
__('desk')
|
|
||||||
}}</a>
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
:label="__('I understand, add conditions')"
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
theme="gray"
|
:placeholder="__('Name')"
|
||||||
@click="useNewUI = true"
|
:label="__('Name')"
|
||||||
|
v-model="assignmentRuleData.assignmentRuleName"
|
||||||
|
required
|
||||||
|
maxlength="50"
|
||||||
|
@change="validateAssignmentRule('assignmentRuleName')"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<AssignmentRulesSection
|
|
||||||
:conditions="assignmentRuleData.assignConditionJson"
|
|
||||||
name="assignCondition"
|
|
||||||
:errors="assignmentRuleErrors.assignConditionError"
|
|
||||||
:doctype="assignmentRuleData.documentType"
|
|
||||||
v-else
|
|
||||||
/>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
:message="assignmentRuleErrors.assignCondition"
|
:message="assignmentRuleErrors.assignmentRuleName"
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<FormLabel :label="__('Priority')" />
|
||||||
|
<Popover>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-outline-gray-2 bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] cursor-default"
|
||||||
|
@click="togglePopover()"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
priorityOptions.find(
|
||||||
|
(option) => option.value == assignmentRuleData.priority,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<FeatherIcon name="chevron-down" class="size-4" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body="{ togglePopover }">
|
||||||
|
<div
|
||||||
|
class="p-1 text-ink-gray-6 top-1 absolute w-full bg-white shadow-2xl rounded"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="option in priorityOptions"
|
||||||
|
:key="option.value"
|
||||||
|
class="p-2 cursor-pointer hover:bg-gray-50 text-base flex items-center justify-between rounded"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
assignmentRuleData.priority = option.value
|
||||||
|
togglePopover()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="assignmentRuleData.priority == option.value"
|
||||||
|
name="check"
|
||||||
|
class="size-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormControl
|
||||||
|
:type="'textarea'"
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
:placeholder="__('Description')"
|
||||||
|
:label="__('Description')"
|
||||||
|
required
|
||||||
|
maxlength="250"
|
||||||
|
@change="validateAssignmentRule('description')"
|
||||||
|
v-model="assignmentRuleData.description"
|
||||||
|
/>
|
||||||
|
<ErrorMessage
|
||||||
|
:message="assignmentRuleErrors.description"
|
||||||
|
class="mt-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<FormLabel :label="__('Apply on')" />
|
||||||
|
<Select
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: 'Lead',
|
||||||
|
value: 'CRM Lead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deal',
|
||||||
|
value: 'CRM Deal',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
v-model="assignmentRuleData.documentType"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<hr class="my-8" />
|
||||||
<hr class="my-8" />
|
<div>
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex flex-col gap-1">
|
<span class="text-lg font-semibold text-ink-gray-8">{{
|
||||||
<span class="text-lg font-semibold text-ink-gray-8">{{
|
__('Assignment condition')
|
||||||
__('Unassignment condition')
|
}}</span>
|
||||||
}}</span>
|
<div class="flex items-center justify-between gap-6">
|
||||||
<div class="flex items-center justify-between gap-6">
|
<span class="text-p-sm text-ink-gray-6">
|
||||||
<span class="text-p-sm text-ink-gray-6">
|
{{
|
||||||
{{
|
__('Choose which {0} are affected by this assignment rule.', [
|
||||||
__('Choose which {0} are affected by this un-assignment rule.', [
|
documentType,
|
||||||
documentType,
|
])
|
||||||
])
|
}}
|
||||||
}}
|
<a
|
||||||
<a
|
class="font-medium underline"
|
||||||
class="font-medium underline"
|
href="https://docs.frappe.io/crm/assignment-rule"
|
||||||
href="https://docs.frappe.io/crm/assignment-rule"
|
target="_blank"
|
||||||
target="_blank"
|
>{{ __('Learn about conditions') }}</a
|
||||||
>{{ __('Learn about conditions') }}</a
|
>
|
||||||
>
|
</span>
|
||||||
</span>
|
<div v-if="isOldSla && step.data">
|
||||||
|
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
|
||||||
|
<template #target>
|
||||||
|
<div
|
||||||
|
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
|
||||||
|
>
|
||||||
|
<span>{{ __('Old Condition') }}</span>
|
||||||
|
<FeatherIcon name="info" class="size-4" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body-main>
|
||||||
|
<div
|
||||||
|
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
|
||||||
|
>
|
||||||
|
<code>{{ assignmentRuleData.assignCondition }}</code>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
<div
|
<div
|
||||||
v-if="isOldSla && step.data && assignmentRuleData.unassignCondition"
|
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-outline-gray-2 rounded-md p-3 py-4"
|
||||||
|
v-if="!useNewUI && assignmentRuleData.assignCondition"
|
||||||
>
|
>
|
||||||
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
|
<span class="text-p-sm">
|
||||||
<template #target>
|
{{ __('Conditions for this rule were created from') }}
|
||||||
<div
|
<a :href="deskUrl" target="_blank" class="underline">{{
|
||||||
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
|
__('desk')
|
||||||
>
|
}}</a>
|
||||||
<span> {{ __('Old Condition') }} </span>
|
{{
|
||||||
<FeatherIcon name="info" class="size-4" />
|
__(
|
||||||
</div>
|
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
|
||||||
</template>
|
)
|
||||||
<template #body-main>
|
}}
|
||||||
<div
|
</span>
|
||||||
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
|
<Button
|
||||||
>
|
:label="__('I understand, add conditions')"
|
||||||
<code>{{ assignmentRuleData.unassignCondition }}</code>
|
variant="subtle"
|
||||||
</div>
|
theme="gray"
|
||||||
</template>
|
@click="useNewUI = true"
|
||||||
</Popover>
|
/>
|
||||||
|
</div>
|
||||||
|
<AssignmentRulesSection
|
||||||
|
:conditions="assignmentRuleData.assignConditionJson"
|
||||||
|
name="assignCondition"
|
||||||
|
:errors="assignmentRuleErrors.assignConditionError"
|
||||||
|
:doctype="assignmentRuleData.documentType"
|
||||||
|
v-else
|
||||||
|
/>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<ErrorMessage
|
||||||
|
:message="assignmentRuleErrors.assignCondition"
|
||||||
|
class="mt-2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5">
|
<hr class="my-8" />
|
||||||
<div
|
<div>
|
||||||
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-gray-300 rounded-md p-3 py-4"
|
<div class="flex flex-col gap-1">
|
||||||
v-if="!useNewUI && assignmentRuleData.unassignCondition"
|
<span class="text-lg font-semibold text-ink-gray-8">{{
|
||||||
>
|
__('Unassignment condition')
|
||||||
<span class="text-p-sm">
|
}}</span>
|
||||||
{{ __('Conditions for this rule were created from') }}
|
<div class="flex items-center justify-between gap-6">
|
||||||
<a :href="deskUrl" target="_blank" class="underline">{{
|
<span class="text-p-sm text-ink-gray-6">
|
||||||
__('desk')
|
{{
|
||||||
}}</a>
|
__(
|
||||||
{{
|
'Choose which {0} are affected by this un-assignment rule.',
|
||||||
__(
|
[documentType],
|
||||||
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
|
)
|
||||||
)
|
}}
|
||||||
}}
|
<a
|
||||||
</span>
|
class="font-medium underline"
|
||||||
<Button
|
href="https://docs.frappe.io/crm/assignment-rule"
|
||||||
:label="__('I understand, add conditions')"
|
target="_blank"
|
||||||
variant="subtle"
|
>{{ __('Learn about conditions') }}</a
|
||||||
theme="gray"
|
>
|
||||||
@click="useNewUI = true"
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
isOldSla && step.data && assignmentRuleData.unassignCondition
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
|
||||||
|
<template #target>
|
||||||
|
<div
|
||||||
|
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
|
||||||
|
>
|
||||||
|
<span> {{ __('Old Condition') }} </span>
|
||||||
|
<FeatherIcon name="info" class="size-4" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body-main>
|
||||||
|
<div
|
||||||
|
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
|
||||||
|
>
|
||||||
|
<code>{{ assignmentRuleData.unassignCondition }}</code>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<div
|
||||||
|
v-if="!useNewUI && assignmentRuleData.unassignCondition"
|
||||||
|
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-outline-gray-2 rounded-md p-3 py-4"
|
||||||
|
>
|
||||||
|
<span class="text-p-sm">
|
||||||
|
{{ __('Conditions for this rule were created from') }}
|
||||||
|
<a :href="deskUrl" target="_blank" class="underline">
|
||||||
|
{{ __('desk') }}
|
||||||
|
</a>
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
:label="__('I understand, add conditions')"
|
||||||
|
variant="subtle"
|
||||||
|
theme="gray"
|
||||||
|
@click="useNewUI = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AssignmentRulesSection
|
||||||
|
v-else
|
||||||
|
:conditions="assignmentRuleData.unassignConditionJson"
|
||||||
|
name="unassignCondition"
|
||||||
|
:errors="assignmentRuleErrors.unassignConditionError"
|
||||||
|
:doctype="assignmentRuleData.documentType"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AssignmentRulesSection
|
|
||||||
:conditions="assignmentRuleData.unassignConditionJson"
|
|
||||||
name="unassignCondition"
|
|
||||||
:errors="assignmentRuleErrors.unassignConditionError"
|
|
||||||
:doctype="assignmentRuleData.documentType"
|
|
||||||
v-else
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<hr class="my-8" />
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-semibold text-ink-gray-8">{{
|
||||||
|
__('Assignment Schedule')
|
||||||
|
}}</span>
|
||||||
|
<span class="text-p-sm text-ink-gray-6">
|
||||||
|
{{
|
||||||
|
__('Choose the days of the week when this rule should be active.')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<AssignmentSchedule />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="my-8" />
|
||||||
|
<AssigneeRules />
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-8" />
|
</div>
|
||||||
<div>
|
<div v-else class="flex items-center h-full justify-center">
|
||||||
<div class="flex flex-col gap-1">
|
<LoadingIndicator class="w-4" />
|
||||||
<span class="text-lg font-semibold text-ink-gray-8">{{
|
|
||||||
__('Assignment Schedule')
|
|
||||||
}}</span>
|
|
||||||
<span class="text-p-sm text-ink-gray-6">
|
|
||||||
{{
|
|
||||||
__('Choose the days of the week when this rule should be active.')
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-6">
|
|
||||||
<AssignmentSchedule />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class="my-8" />
|
|
||||||
<AssigneeRules />
|
|
||||||
</div>
|
</div>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
v-model="showConfirmDialog.show"
|
v-model="showConfirmDialog.show"
|
||||||
|
|||||||
@ -1,35 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-8 sticky top-0">
|
<div class="flex h-full flex-col gap-6 p-6 text-ink-gray-8">
|
||||||
<div class="flex items-start justify-between">
|
<!-- Header -->
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex justify-between px-2 pt-2">
|
||||||
<h1 class="text-xl font-semibold text-ink-gray-8">
|
<div class="flex flex-col gap-1 w-9/12">
|
||||||
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
{{ __('Assignment rules') }}
|
{{ __('Assignment rules') }}
|
||||||
</h1>
|
</h2>
|
||||||
<p class="text-p-base text-ink-gray-6 max-w-md">
|
<p class="text-p-base text-ink-gray-6">
|
||||||
{{
|
{{
|
||||||
__(
|
__(
|
||||||
'Assignment Rules automatically route leads or deals to the right team members based on predefined conditions.',
|
'Assignment rules automatically assign lead/deal to the right sales user based on predefined conditions',
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
||||||
:label="__('Create new')"
|
<Button
|
||||||
theme="gray"
|
:label="__('New')"
|
||||||
variant="solid"
|
icon-left="plus"
|
||||||
@click="goToNew()"
|
variant="solid"
|
||||||
icon-left="plus"
|
@click="goToNew()"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assignment rules list -->
|
||||||
|
<div class="overflow-y-auto">
|
||||||
|
<AssignmentRulesList />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="overflow-y-auto px-8 pb-6">
|
|
||||||
<AssignmentRulesList />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource } from 'frappe-ui'
|
|
||||||
import AssignmentRulesList from './AssignmentRulesList.vue'
|
import AssignmentRulesList from './AssignmentRulesList.vue'
|
||||||
|
import { createResource } from 'frappe-ui'
|
||||||
import { inject, provide } from 'vue'
|
import { inject, provide } from 'vue'
|
||||||
|
|
||||||
const updateStep = inject('updateStep')
|
const updateStep = inject('updateStep')
|
||||||
|
|||||||
@ -8,25 +8,27 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
v-if="assignmentRulesList.data?.length === 0"
|
v-if="assignmentRulesList.data?.length === 0"
|
||||||
class="flex items-center justify-center rounded-md border border-gray-200 p-4"
|
class="flex items-center justify-center rounded-md border border-outline-gray-2 p-4"
|
||||||
>
|
>
|
||||||
<div class="text-sm text-ink-gray-7">
|
<div class="text-sm text-ink-gray-7">
|
||||||
{{ __('No items in the list') }}
|
{{ __('No items in the list') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="grid grid-cols-11 items-center gap-4 text-sm text-gray-600">
|
<div class="flex items-center py-2 px-4 text-sm text-ink-gray-5">
|
||||||
<div class="col-span-7 ml-2">{{ __('Assignment rule') }}</div>
|
<div class="w-7/12">{{ __('Assignment rule') }}</div>
|
||||||
<div class="col-span-2">{{ __('Priority') }}</div>
|
<div class="w-3/12">{{ __('Priority') }}</div>
|
||||||
<div class="col-span-2">{{ __('Enabled') }}</div>
|
<div class="w-2/12">{{ __('Enabled') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="mt-2 mx-2" />
|
<div class="h-px border-t mx-4 border-outline-gray-modals" />
|
||||||
<div
|
<div class="overflow-y-auto px-2">
|
||||||
v-for="assignmentRule in assignmentRulesList.data"
|
<template
|
||||||
:key="assignmentRule.name"
|
v-for="(assignmentRule, i) in assignmentRulesList.data"
|
||||||
>
|
:key="assignmentRule.name"
|
||||||
<AssignmentRuleListItem :data="assignmentRule" />
|
>
|
||||||
<hr class="mx-2" />
|
<AssignmentRuleListItem :data="assignmentRule" />
|
||||||
|
<hr v-if="assignmentRulesList.data.length !== i + 1" class="mx-2" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="props.conditions.length == 0"
|
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"
|
class="flex p-4 items-center cursor-pointer justify-center gap-2 text-sm border border-outline-gray-2 text-gray-600 rounded-md"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
props.conditions.push(['', '', ''])
|
props.conditions.push(['', '', ''])
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-md border px-2 border-gray-300 text-sm">
|
<div class="rounded-md border px-2 border-outline-gray-2 text-sm">
|
||||||
<div
|
<div
|
||||||
class="grid p-2 px-4 items-center"
|
class="grid p-2 px-4 items-center"
|
||||||
style="grid-template-columns: 3fr 1fr"
|
style="grid-template-columns: 3fr 1fr"
|
||||||
@ -9,7 +9,7 @@
|
|||||||
:key="column.key"
|
:key="column.key"
|
||||||
class="text-gray-600 overflow-hidden whitespace-nowrap text-ellipsis"
|
class="text-gray-600 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
{{ column.label }}
|
{{ __(column.label) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
@ -24,10 +24,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ErrorMessage } from 'frappe-ui'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import AssignmentScheduleItem from './AssignmentScheduleItem.vue'
|
import AssignmentScheduleItem from './AssignmentScheduleItem.vue'
|
||||||
import { inject } from 'vue'
|
import { ErrorMessage } from 'frappe-ui'
|
||||||
|
import { onMounted, ref, inject } from 'vue'
|
||||||
|
|
||||||
const assignmentRuleData = inject('assignmentRuleData')
|
const assignmentRuleData = inject('assignmentRuleData')
|
||||||
const assignmentRuleErrors = inject('assignmentRuleErrors')
|
const assignmentRuleErrors = inject('assignmentRuleErrors')
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
class="grid py-3.5 px-4 items-center"
|
class="grid py-3.5 px-4 items-center"
|
||||||
style="grid-template-columns: 3fr 1fr"
|
style="grid-template-columns: 3fr 1fr"
|
||||||
>
|
>
|
||||||
<div class="text-ink-gray-7 font-medium">{{ data.day }}</div>
|
<div class="text-ink-gray-7 font-medium">{{ __(data.day) }}</div>
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<Switch v-model="data.active" @update:model-value="toggleDay" />
|
<Switch v-model="data.active" @update:model-value="toggleDay" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,27 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
|
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex px-2 justify-between">
|
<div class="flex justify-between px-2 text-ink-gray-8">
|
||||||
<div class="flex items-center gap-1 -ml-4 w-9/12">
|
<div class="flex flex-col gap-1">
|
||||||
<Button
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
variant="ghost"
|
{{ __('Brand settings') }}
|
||||||
icon-left="chevron-left"
|
</h2>
|
||||||
:label="__('Brand settings')"
|
<p class="text-p-base text-ink-gray-6">
|
||||||
size="md"
|
{{ __('Configure your brand name, logo, and favicon') }}
|
||||||
@click="() => emit('updateStep', 'general-settings')"
|
</p>
|
||||||
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !justify-start"
|
|
||||||
/>
|
|
||||||
<Badge
|
|
||||||
v-if="settings.isDirty"
|
|
||||||
:label="__('Not Saved')"
|
|
||||||
variant="subtle"
|
|
||||||
theme="orange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
||||||
<Button
|
<Button
|
||||||
:label="__('Update')"
|
:label="__('Update')"
|
||||||
icon-left="plus"
|
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:disabled="!settings.isDirty"
|
:disabled="!settings.isDirty"
|
||||||
:loading="settings.loading"
|
:loading="settings.loading"
|
||||||
@ -36,35 +27,30 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
class="w-1/2"
|
class="w-1/2"
|
||||||
|
size="md"
|
||||||
v-model="settings.doc.brand_name"
|
v-model="settings.doc.brand_name"
|
||||||
:label="__('Brand name')"
|
:label="__('Brand name')"
|
||||||
|
:placeholder="__('Enter brand name')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- logo -->
|
<!-- logo -->
|
||||||
<div class="flex flex-col justify-between gap-4">
|
<div class="flex flex-col justify-between gap-4">
|
||||||
<span class="text-base font-semibold text-ink-gray-8">
|
<div class="flex items-center flex-1 gap-5">
|
||||||
{{ __('Logo') }}
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-1 gap-5">
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center rounded border border-outline-gray-modals px-10 py-2"
|
class="flex items-center justify-center rounded border border-outline-gray-modals size-20"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="settings.doc?.brand_logo || '/assets/crm/images/logo.png'"
|
v-if="settings.doc?.brand_logo"
|
||||||
|
:src="settings.doc?.brand_logo"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
class="size-8 rounded"
|
class="size-8 rounded"
|
||||||
/>
|
/>
|
||||||
|
<ImageIcon v-else class="size-5 text-ink-gray-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col gap-2">
|
<div class="flex flex-1 flex-col gap-1">
|
||||||
<ImageUploader
|
<span class="text-base font-medium">{{ __('Brand logo') }}</span>
|
||||||
label="Favicon"
|
<span class="text-p-base text-ink-gray-6">
|
||||||
image_type="image/ico"
|
|
||||||
:image_url="settings.doc?.brand_logo"
|
|
||||||
@upload="(url) => (settings.doc.brand_logo = url)"
|
|
||||||
@remove="() => (settings.doc.brand_logo = '')"
|
|
||||||
/>
|
|
||||||
<span class="text-p-sm text-ink-gray-6">
|
|
||||||
{{
|
{{
|
||||||
__(
|
__(
|
||||||
'Appears in the left sidebar. Recommended size is 32x32 px in PNG or SVG',
|
'Appears in the left sidebar. Recommended size is 32x32 px in PNG or SVG',
|
||||||
@ -72,33 +58,34 @@
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<ImageUploader
|
||||||
|
image_type="image/ico"
|
||||||
|
:image_url="settings.doc?.brand_logo"
|
||||||
|
@upload="(url) => (settings.doc.brand_logo = url)"
|
||||||
|
@remove="() => (settings.doc.brand_logo = '')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- favicon -->
|
<!-- favicon -->
|
||||||
<div class="flex flex-col justify-between gap-4">
|
<div class="flex flex-col justify-between gap-4">
|
||||||
<span class="text-base font-semibold text-ink-gray-8">
|
<div class="flex items-center flex-1 gap-5">
|
||||||
{{ __('Favicon') }}
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-1 gap-5">
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center rounded border border-outline-gray-modals px-10 py-2"
|
class="flex items-center justify-center rounded border border-outline-gray-modals size-20"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="settings.doc?.favicon || '/assets/crm/images/logo.png'"
|
v-if="settings.doc?.favicon"
|
||||||
|
:src="settings.doc?.favicon"
|
||||||
alt="Favicon"
|
alt="Favicon"
|
||||||
class="size-8 rounded"
|
class="size-8 rounded"
|
||||||
/>
|
/>
|
||||||
|
<ImageIcon v-else class="size-5 text-ink-gray-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col gap-2">
|
<div class="flex flex-1 flex-col gap-1">
|
||||||
<ImageUploader
|
<span class="text-base font-medium">{{ __('Favicon') }}</span>
|
||||||
label="Favicon"
|
<span class="text-p-base text-ink-gray-6">
|
||||||
image_type="image/ico"
|
|
||||||
:image_url="settings.doc?.favicon"
|
|
||||||
@upload="(url) => (settings.doc.favicon = url)"
|
|
||||||
@remove="() => (settings.doc.favicon = '')"
|
|
||||||
/>
|
|
||||||
<span class="text-p-sm text-ink-gray-6">
|
|
||||||
{{
|
{{
|
||||||
__(
|
__(
|
||||||
'Appears next to the title in your browser tab. Recommended size is 32x32 px in PNG or ICO',
|
'Appears next to the title in your browser tab. Recommended size is 32x32 px in PNG or ICO',
|
||||||
@ -106,20 +93,25 @@
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<ImageUploader
|
||||||
|
image_type="image/ico"
|
||||||
|
:image_url="settings.doc?.favicon"
|
||||||
|
@upload="(url) => (settings.doc.favicon = url)"
|
||||||
|
@remove="() => (settings.doc.favicon = '')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="errorMessage">
|
|
||||||
<ErrorMessage :message="__(errorMessage)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ImageIcon from '~icons/lucide/image'
|
||||||
import ImageUploader from '@/components/Controls/ImageUploader.vue'
|
import ImageUploader from '@/components/Controls/ImageUploader.vue'
|
||||||
import { FormControl, ErrorMessage } from 'frappe-ui'
|
import { FormControl } from 'frappe-ui'
|
||||||
import { getSettings } from '@/stores/settings'
|
import { getSettings } from '@/stores/settings'
|
||||||
import { showSettings } from '@/composables/settings'
|
import { showSettings } from '@/composables/settings'
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const { _settings: settings, setupBrand } = getSettings()
|
const { _settings: settings, setupBrand } = getSettings()
|
||||||
|
|
||||||
@ -131,7 +123,4 @@ function updateSettings() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['updateStep'])
|
|
||||||
const errorMessage = ref('')
|
|
||||||
</script>
|
</script>
|
||||||
@ -1,27 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
|
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex px-2 justify-between">
|
<div class="flex justify-between px-2 text-ink-gray-8">
|
||||||
<div class="flex items-center gap-1 -ml-4 w-9/12">
|
<div class="flex flex-col gap-1">
|
||||||
<Button
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
variant="ghost"
|
{{ __('Currency & Exchange rate provider') }}
|
||||||
icon-left="chevron-left"
|
</h2>
|
||||||
:label="__('Currency & Exchange rate provider')"
|
<p class="text-p-base text-ink-gray-6">
|
||||||
size="md"
|
{{
|
||||||
@click="() => emit('updateStep', 'general-settings')"
|
__('Configure the currency and exchange rate provider for your CRM')
|
||||||
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !justify-start"
|
}}
|
||||||
/>
|
</p>
|
||||||
<Badge
|
|
||||||
v-if="settings.isDirty"
|
|
||||||
:label="__('Not Saved')"
|
|
||||||
variant="subtle"
|
|
||||||
theme="orange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
||||||
<Button
|
<Button
|
||||||
:label="__('Update')"
|
:label="__('Update')"
|
||||||
icon-left="plus"
|
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:disabled="!settings.isDirty"
|
:disabled="!settings.isDirty"
|
||||||
:loading="settings.loading"
|
:loading="settings.loading"
|
||||||
@ -32,7 +25,7 @@
|
|||||||
|
|
||||||
<!-- Fields -->
|
<!-- Fields -->
|
||||||
<div class="flex flex-1 flex-col overflow-y-auto">
|
<div class="flex flex-1 flex-col overflow-y-auto">
|
||||||
<div class="flex items-center justify-between gap-8 p-3">
|
<div class="flex items-center justify-between gap-8 py-3 px-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
||||||
{{ __('Currency') }}
|
{{ __('Currency') }}
|
||||||
@ -61,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-px border-t mx-2 border-outline-gray-modals" />
|
<div class="h-px border-t mx-2 border-outline-gray-modals" />
|
||||||
<div class="flex items-center justify-between gap-8 p-3">
|
<div class="flex items-center justify-between gap-8 py-3 px-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
||||||
{{ __('Exchange rate provider') }}
|
{{ __('Exchange rate provider') }}
|
||||||
@ -131,17 +124,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ErrorMessage, toast } from 'frappe-ui'
|
import { ErrorMessage, FormControl, toast } from 'frappe-ui'
|
||||||
import { getSettings } from '@/stores/settings'
|
import { getSettings } from '@/stores/settings'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { showSettings } from '@/composables/settings'
|
import { showSettings } from '@/composables/settings'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import FormControl from 'frappe-ui/src/components/FormControl/FormControl.vue'
|
|
||||||
|
|
||||||
const { _settings: settings } = getSettings()
|
const { _settings: settings } = getSettings()
|
||||||
const { $dialog } = globalStore()
|
const { $dialog } = globalStore()
|
||||||
|
|
||||||
const emit = defineEmits(['updateStep'])
|
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
function updateSettings() {
|
function updateSettings() {
|
||||||
@ -12,7 +12,11 @@
|
|||||||
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
|
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
<div class="flex item-center space-x-4 w-3/12 justify-end">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Switch size="sm" v-model="template.enabled" />
|
||||||
|
<span class="text-sm text-ink-gray-7">{{ __('Enabled') }}</span>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
:label="__('Update')"
|
:label="__('Update')"
|
||||||
icon-left="plus"
|
icon-left="plus"
|
||||||
@ -26,13 +30,6 @@
|
|||||||
|
|
||||||
<!-- Fields -->
|
<!-- Fields -->
|
||||||
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
|
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
|
||||||
<div
|
|
||||||
class="flex justify-between items-center cursor-pointer border-b py-3"
|
|
||||||
@click="() => (template.enabled = !template.enabled)"
|
|
||||||
>
|
|
||||||
<div class="text-base text-ink-gray-7">{{ __('Enabled') }}</div>
|
|
||||||
<Switch v-model="template.enabled" @click.stop />
|
|
||||||
</div>
|
|
||||||
<div class="flex sm:flex-row flex-col gap-4">
|
<div class="flex sm:flex-row flex-col gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|||||||
@ -148,7 +148,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { TemplateOption } from '@/utils'
|
|
||||||
import {
|
import {
|
||||||
TextInput,
|
TextInput,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -223,43 +222,28 @@ function getDropdownOptions(template) {
|
|||||||
let options = [
|
let options = [
|
||||||
{
|
{
|
||||||
label: __('Duplicate'),
|
label: __('Duplicate'),
|
||||||
component: (props) =>
|
icon: 'copy',
|
||||||
TemplateOption({
|
onClick: () => emit('updateStep', 'new-template', { ...template }),
|
||||||
option: __('Duplicate'),
|
|
||||||
icon: 'copy',
|
|
||||||
active: props.active,
|
|
||||||
onClick: () => emit('updateStep', 'new-template', { ...template }),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Delete'),
|
label: __('Delete'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
onClick: (e) => {
|
||||||
option: __('Delete'),
|
e.preventDefault()
|
||||||
icon: 'trash-2',
|
e.stopPropagation()
|
||||||
active: props.active,
|
confirmDelete.value = true
|
||||||
onClick: (e) => {
|
},
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
confirmDelete.value = true
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
condition: () => !confirmDelete.value,
|
condition: () => !confirmDelete.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Confirm Delete'),
|
label: __('Confirm Delete'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
theme: 'red',
|
||||||
option: __('Confirm Delete'),
|
onClick: () => deleteTemplate(template),
|
||||||
icon: 'trash-2',
|
|
||||||
active: props.active,
|
|
||||||
theme: 'danger',
|
|
||||||
onClick: () => deleteTemplate(template),
|
|
||||||
}),
|
|
||||||
condition: () => confirmDelete.value,
|
condition: () => confirmDelete.value,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return options.filter((option) => option.condition?.() || true)
|
return options
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -14,7 +14,11 @@
|
|||||||
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
|
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
<div class="flex item-center space-x-4 w-3/12 justify-end">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Switch size="sm" v-model="template.enabled" />
|
||||||
|
<span class="text-sm text-ink-gray-7">{{ __('Enabled') }}</span>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
:label="templateData?.name ? __('Duplicate') : __('Create')"
|
:label="templateData?.name ? __('Duplicate') : __('Create')"
|
||||||
icon-left="plus"
|
icon-left="plus"
|
||||||
@ -26,13 +30,6 @@
|
|||||||
|
|
||||||
<!-- Fields -->
|
<!-- Fields -->
|
||||||
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
|
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
|
||||||
<div
|
|
||||||
class="flex justify-between items-center cursor-pointer border-b py-3"
|
|
||||||
@click="() => (template.enabled = !template.enabled)"
|
|
||||||
>
|
|
||||||
<div class="text-base text-ink-gray-7">{{ __('Enabled') }}</div>
|
|
||||||
<Switch v-model="template.enabled" @click.stop />
|
|
||||||
</div>
|
|
||||||
<div class="flex sm:flex-row flex-col gap-4">
|
<div class="flex sm:flex-row flex-col gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|||||||
93
frontend/src/components/Settings/ForecastingSettings.vue
Normal file
93
frontend/src/components/Settings/ForecastingSettings.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col gap-6 py-8 px-6 text-ink-gray-8">
|
||||||
|
<div class="flex flex-col gap-1 px-2">
|
||||||
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
|
{{ __('Forecasting') }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-p-base text-ink-gray-6">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Configure forecasting feature to help predict sales performance and growth',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col overflow-y-auto">
|
||||||
|
<div class="flex items-center justify-between py-3 px-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
||||||
|
{{ __('Enable forecasting') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-p-sm text-ink-gray-5 truncate">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Makes "Expected Closure Date" and "Expected Deal Value" mandatory for deal value forecasting',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
v-model="settings.doc.enable_forecasting"
|
||||||
|
@click.stop="toggleForecasting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-px border-t mx-2 border-outline-gray-modals" />
|
||||||
|
<div class="flex items-center justify-between py-3 px-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
||||||
|
{{ __('Auto update expected deal value') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-p-sm text-ink-gray-5 truncate">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Automatically update "Expected Deal Value" based on the total value of associated products in a deal',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
v-model="settings.doc.auto_update_expected_deal_value"
|
||||||
|
@click.stop="autoUpdateExpectedDealValue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { getSettings } from '@/stores/settings'
|
||||||
|
import { Switch, toast } from 'frappe-ui'
|
||||||
|
|
||||||
|
const { _settings: settings } = getSettings()
|
||||||
|
|
||||||
|
function toggleForecasting() {
|
||||||
|
settings.save.submit(null, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(
|
||||||
|
settings.doc.enable_forecasting
|
||||||
|
? __('Forecasting enabled successfully')
|
||||||
|
: __('Forecasting disabled successfully'),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoUpdateExpectedDealValue() {
|
||||||
|
settings.save.submit(null, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(
|
||||||
|
settings.doc.auto_update_expected_deal_value
|
||||||
|
? __('Auto update of expected deal value enabled')
|
||||||
|
: __('Auto update of expected deal value disabled'),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,105 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
|
||||||
{{ __('General') }}
|
|
||||||
</h2>
|
|
||||||
<p class="text-p-base text-ink-gray-6">
|
|
||||||
{{ __('Configure general settings for your CRM') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col overflow-y-auto">
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
|
|
||||||
@click="toggleForecasting()"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
|
||||||
{{ __('Enable forecasting') }}
|
|
||||||
</div>
|
|
||||||
<div class="text-p-sm text-ink-gray-5 truncate">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'Makes "Expected Closure Date" and "Expected Deal Value" mandatory for deal value forecasting',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Switch
|
|
||||||
size="sm"
|
|
||||||
v-model="settings.doc.enable_forecasting"
|
|
||||||
@click.stop="toggleForecasting(settings.doc.enable_forecasting)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-px border-t mx-2 border-outline-gray-modals" />
|
|
||||||
<template v-for="(setting, i) in settingsList" :key="setting.name">
|
|
||||||
<li
|
|
||||||
class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
|
|
||||||
@click="() => emit('updateStep', setting.name)"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
|
||||||
{{ __(setting.label) }}
|
|
||||||
</div>
|
|
||||||
<div class="text-p-sm text-ink-gray-5 truncate">
|
|
||||||
{{ __(setting.description) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FeatherIcon name="chevron-right" class="text-ink-gray-7 size-4" />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<div
|
|
||||||
v-if="settingsList.length !== i + 1"
|
|
||||||
class="h-px border-t mx-2 border-outline-gray-modals"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { getSettings } from '@/stores/settings'
|
|
||||||
import { Switch, toast } from 'frappe-ui'
|
|
||||||
|
|
||||||
const emit = defineEmits(['updateStep'])
|
|
||||||
|
|
||||||
const { _settings: settings } = getSettings()
|
|
||||||
|
|
||||||
const settingsList = [
|
|
||||||
{
|
|
||||||
name: 'currency-settings',
|
|
||||||
label: 'Currency & Exchange rate provider',
|
|
||||||
description:
|
|
||||||
'Configure the currency and exchange rate provider for your CRM',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'brand-settings',
|
|
||||||
label: 'Brand settings',
|
|
||||||
description: 'Configure your brand name, logo and favicon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'home-actions',
|
|
||||||
label: 'Home actions',
|
|
||||||
description: 'Configure actions that appear on the home dropdown',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function toggleForecasting(value) {
|
|
||||||
settings.doc.enable_forecasting =
|
|
||||||
value !== undefined ? value : !settings.doc.enable_forecasting
|
|
||||||
|
|
||||||
settings.save.submit(null, {
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success(
|
|
||||||
settings.doc.enable_forecasting
|
|
||||||
? __('Forecasting enabled successfully')
|
|
||||||
: __('Forecasting disabled successfully'),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<component :is="getComponent(step)" :data="data" @updateStep="updateStep" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import GeneralSettings from './GeneralSettings.vue'
|
|
||||||
import CurrencySettings from './CurrencySettings.vue'
|
|
||||||
import BrandSettings from './BrandSettings.vue'
|
|
||||||
import HomeActions from './HomeActions.vue'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const step = ref('general-settings')
|
|
||||||
const data = ref(null)
|
|
||||||
|
|
||||||
function updateStep(newStep, _data) {
|
|
||||||
step.value = newStep
|
|
||||||
data.value = _data
|
|
||||||
}
|
|
||||||
|
|
||||||
function getComponent(step) {
|
|
||||||
switch (step) {
|
|
||||||
case 'general-settings':
|
|
||||||
return GeneralSettings
|
|
||||||
case 'currency-settings':
|
|
||||||
return CurrencySettings
|
|
||||||
case 'brand-settings':
|
|
||||||
return BrandSettings
|
|
||||||
case 'home-actions':
|
|
||||||
return HomeActions
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,21 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
|
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between text-ink-gray-8">
|
||||||
<div class="flex gap-1 -ml-4 w-9/12">
|
<div class="flex flex-col gap-1">
|
||||||
<Button
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
variant="ghost"
|
{{ __('Home actions') }}
|
||||||
icon-left="chevron-left"
|
</h2>
|
||||||
:label="__('Home actions')"
|
<p class="text-p-base text-ink-gray-6">
|
||||||
size="md"
|
{{ __('Configure actions that appear on the home dropdown') }}
|
||||||
@click="() => emit('updateStep', 'general-settings')"
|
</p>
|
||||||
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
||||||
<Button
|
<Button
|
||||||
:label="__('Update')"
|
:label="__('Update')"
|
||||||
icon-left="plus"
|
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:disabled="!document.isDirty"
|
:disabled="!document.isDirty"
|
||||||
:loading="document.loading"
|
:loading="document.loading"
|
||||||
@ -25,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fields -->
|
<!-- Fields -->
|
||||||
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
|
<div class="flex flex-1 flex-col overflow-y-auto">
|
||||||
<Grid
|
<Grid
|
||||||
v-model="document.doc.dropdown_items"
|
v-model="document.doc.dropdown_items"
|
||||||
doctype="CRM Dropdown Item"
|
doctype="CRM Dropdown Item"
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
|
<div class="flex h-full flex-col gap-6 py-8 px-6 text-ink-gray-8">
|
||||||
<div class="flex justify-between">
|
<div class="flex px-2 justify-between">
|
||||||
<div class="flex flex-col gap-1 w-9/12">
|
<div class="flex flex-col gap-1 w-9/12">
|
||||||
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
{{ __('Send invites to') }}
|
{{ __('Send invites to') }}
|
||||||
@ -23,26 +23,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col gap-8 overflow-y-auto">
|
<div class="flex-1 flex flex-col px-2 gap-8 overflow-y-auto">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-ink-gray-5 mb-1.5">
|
<FormControl
|
||||||
{{ __('Invite by email') }}
|
type="textarea"
|
||||||
</label>
|
label="Invite by email"
|
||||||
<div
|
placeholder="user1@example.com, user2@example.com, ..."
|
||||||
class="p-2 group bg-surface-gray-2 hover:bg-surface-gray-3 rounded"
|
@input="updateInvitees($event.target.value)"
|
||||||
>
|
:debounce="100"
|
||||||
<MultiSelectUserInput
|
:disabled="inviteByEmail.loading"
|
||||||
class="flex-1"
|
:description="
|
||||||
inputClass="!bg-surface-gray-2 hover:!bg-surface-gray-3 group-hover:!bg-surface-gray-3"
|
__(
|
||||||
:placeholder="__('john@doe.com')"
|
'You can invite multiple users by comma separating their email addresses',
|
||||||
v-model="invitees"
|
)
|
||||||
:validate="validateEmail"
|
"
|
||||||
:error-message="
|
/>
|
||||||
(value) => __('{0} is an invalid email address', [value])
|
|
||||||
"
|
|
||||||
:fetchUsers="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="userExistMessage || inviteeExistMessage"
|
v-if="userExistMessage || inviteeExistMessage"
|
||||||
class="text-xs text-ink-red-3 mt-1.5"
|
class="text-xs text-ink-red-3 mt-1.5"
|
||||||
@ -100,15 +95,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import MultiSelectUserInput from '@/components/Controls/MultiSelectUserInput.vue'
|
|
||||||
import { validateEmail, convertArrayToString } from '@/utils'
|
import { validateEmail, convertArrayToString } from '@/utils'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import {
|
import { createListResource, createResource, FormControl } from 'frappe-ui'
|
||||||
createListResource,
|
|
||||||
createResource,
|
|
||||||
FormControl,
|
|
||||||
Tooltip,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
@ -208,6 +197,15 @@ const pendingInvitations = createListResource({
|
|||||||
doctype: 'CRM Invitation',
|
doctype: 'CRM Invitation',
|
||||||
filters: { status: 'Pending' },
|
filters: { status: 'Pending' },
|
||||||
fields: ['name', 'email', 'role'],
|
fields: ['name', 'email', 'role'],
|
||||||
|
pageLength: 999,
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function updateInvitees(value) {
|
||||||
|
const emails = value
|
||||||
|
.split(',')
|
||||||
|
.map((email) => email.trim())
|
||||||
|
.filter((email) => validateEmail(email))
|
||||||
|
invitees.value = emails
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -7,31 +7,33 @@
|
|||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||||
<div class="flex flex-col p-2 w-52 shrink-0 bg-surface-gray-2">
|
<div class="flex flex-col p-1 w-52 shrink-0 bg-surface-gray-2">
|
||||||
<h1 class="px-2 pt-2 mb-3 text-lg font-semibold text-ink-gray-8">
|
<h1 class="px-3 pt-3 pb-2 text-lg font-semibold text-ink-gray-8">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div v-for="tab in tabs">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<template v-for="tab in tabs" :key="tab.label">
|
||||||
v-if="!tab.hideLabel"
|
<div
|
||||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-ink-gray-5 transition-all duration-300 ease-in-out"
|
v-if="!tab.hideLabel"
|
||||||
>
|
class="py-[7px] px-2 my-1 flex cursor-pointer gap-1.5 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
|
||||||
<span>{{ __(tab.label) }}</span>
|
>
|
||||||
</div>
|
<span>{{ __(tab.label) }}</span>
|
||||||
<nav class="space-y-1">
|
</div>
|
||||||
<SidebarLink
|
<nav class="space-y-1 px-1">
|
||||||
v-for="i in tab.items"
|
<SidebarLink
|
||||||
:icon="i.icon"
|
v-for="i in tab.items"
|
||||||
:label="__(i.label)"
|
:icon="i.icon"
|
||||||
class="w-full"
|
:label="__(i.label)"
|
||||||
:class="
|
class="w-full"
|
||||||
activeTab?.label == i.label
|
:class="
|
||||||
? 'bg-surface-selected shadow-sm hover:bg-surface-selected'
|
activeTab?.label == i.label
|
||||||
: 'hover:bg-surface-gray-3'
|
? 'bg-surface-selected shadow-sm hover:bg-surface-selected'
|
||||||
"
|
: 'hover:bg-surface-gray-3'
|
||||||
@click="activeSettingsPage = i.label"
|
"
|
||||||
/>
|
@click="activeSettingsPage = i.label"
|
||||||
</nav>
|
/>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-1 overflow-y-auto bg-surface-modal">
|
<div class="flex flex-col flex-1 overflow-y-auto bg-surface-modal">
|
||||||
@ -42,6 +44,9 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import CircleDollarSignIcon from '~icons/lucide/circle-dollar-sign'
|
||||||
|
import TrendingUpDownIcon from '~icons/lucide/trending-up-down'
|
||||||
|
import SparkleIcon from '@/components/Icons/SparkleIcon.vue'
|
||||||
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||||
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
|
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
@ -49,11 +54,14 @@ 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 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 InviteUserPage from '@/components/Settings/InviteUserPage.vue'
|
import InviteUserPage from '@/components/Settings/InviteUserPage.vue'
|
||||||
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
||||||
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
||||||
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
|
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
|
||||||
|
import BrandSettings from '@/components/Settings/BrandSettings.vue'
|
||||||
|
import HomeActions from '@/components/Settings/HomeActions.vue'
|
||||||
|
import ForecastingSettings from '@/components/Settings/ForecastingSettings.vue'
|
||||||
|
import CurrencySettings from '@/components/Settings/CurrencySettings.vue'
|
||||||
import EmailTemplatePage from '@/components/Settings/EmailTemplate/EmailTemplatePage.vue'
|
import EmailTemplatePage from '@/components/Settings/EmailTemplate/EmailTemplatePage.vue'
|
||||||
import TelephonySettings from '@/components/Settings/TelephonySettings.vue'
|
import TelephonySettings from '@/components/Settings/TelephonySettings.vue'
|
||||||
import EmailConfig from '@/components/Settings/EmailConfig.vue'
|
import EmailConfig from '@/components/Settings/EmailConfig.vue'
|
||||||
@ -76,7 +84,7 @@ const user = computed(() => getUser() || {})
|
|||||||
const tabs = computed(() => {
|
const tabs = computed(() => {
|
||||||
let _tabs = [
|
let _tabs = [
|
||||||
{
|
{
|
||||||
label: __('Settings'),
|
label: __('Personal Settings'),
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -89,12 +97,32 @@ const tabs = computed(() => {
|
|||||||
}),
|
}),
|
||||||
component: markRaw(ProfileSettings),
|
component: markRaw(ProfileSettings),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('System Configuration'),
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
label: __('General'),
|
label: __('Forecasting'),
|
||||||
icon: 'settings',
|
component: markRaw(ForecastingSettings),
|
||||||
component: markRaw(GeneralSettingsPage),
|
icon: TrendingUpDownIcon,
|
||||||
condition: () => isManager(),
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: __('Currency & Exchange Rate'),
|
||||||
|
icon: CircleDollarSignIcon,
|
||||||
|
component: markRaw(CurrencySettings),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Brand Settings'),
|
||||||
|
icon: SparkleIcon,
|
||||||
|
component: markRaw(BrandSettings),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
condition: () => isManager(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('User Management'),
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
label: __('Users'),
|
label: __('Users'),
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
@ -107,6 +135,12 @@ const tabs = computed(() => {
|
|||||||
component: markRaw(InviteUserPage),
|
component: markRaw(InviteUserPage),
|
||||||
condition: () => isManager(),
|
condition: () => isManager(),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
condition: () => isManager(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Email Settings'),
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
label: __('Email Accounts'),
|
label: __('Email Accounts'),
|
||||||
icon: Email2Icon,
|
icon: Email2Icon,
|
||||||
@ -118,6 +152,11 @@ const tabs = computed(() => {
|
|||||||
icon: EmailTemplateIcon,
|
icon: EmailTemplateIcon,
|
||||||
component: markRaw(EmailTemplatePage),
|
component: markRaw(EmailTemplatePage),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Automation & Rules'),
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
label: __('Assignment rules'),
|
label: __('Assignment rules'),
|
||||||
icon: markRaw(h(SettingsIcon2, { class: 'rotate-90' })),
|
icon: markRaw(h(SettingsIcon2, { class: 'rotate-90' })),
|
||||||
@ -125,6 +164,17 @@ const tabs = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: __('Customization'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: __('Home Actions'),
|
||||||
|
component: markRaw(HomeActions),
|
||||||
|
icon: 'home',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
condition: () => isManager(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: __('Integrations', null, 'FCRM'),
|
label: __('Integrations', null, 'FCRM'),
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@ -169,8 +169,16 @@
|
|||||||
import AddExistingUserModal from '@/components/Modals/AddExistingUserModal.vue'
|
import AddExistingUserModal from '@/components/Modals/AddExistingUserModal.vue'
|
||||||
import { activeSettingsPage } from '@/composables/settings'
|
import { activeSettingsPage } from '@/composables/settings'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { TemplateOption, DropdownOption } from '@/utils'
|
import { DropdownOption } from '@/utils'
|
||||||
import { Avatar, TextInput, toast, call, FeatherIcon, Tooltip } from 'frappe-ui'
|
import {
|
||||||
|
Dropdown,
|
||||||
|
Avatar,
|
||||||
|
TextInput,
|
||||||
|
toast,
|
||||||
|
call,
|
||||||
|
FeatherIcon,
|
||||||
|
Tooltip,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
const { users, isAdmin, isManager } = usersStore()
|
const { users, isAdmin, isManager } = usersStore()
|
||||||
@ -208,29 +216,19 @@ function getMoreOptions(user) {
|
|||||||
let options = [
|
let options = [
|
||||||
{
|
{
|
||||||
label: __('Remove'),
|
label: __('Remove'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
onClick: (e) => {
|
||||||
option: __('Remove'),
|
e.preventDefault()
|
||||||
icon: 'trash-2',
|
e.stopPropagation()
|
||||||
active: props.active,
|
confirmRemove.value = true
|
||||||
onClick: (e) => {
|
},
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
confirmRemove.value = true
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
condition: () => !confirmRemove.value,
|
condition: () => !confirmRemove.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Confirm Remove'),
|
label: __('Confirm Remove'),
|
||||||
component: (props) =>
|
icon: 'trash-2',
|
||||||
TemplateOption({
|
theme: 'red',
|
||||||
option: __('Confirm Remove'),
|
onClick: () => removeUser(user, true),
|
||||||
icon: 'trash-2',
|
|
||||||
active: props.active,
|
|
||||||
theme: 'danger',
|
|
||||||
onClick: () => removeUser(user, true),
|
|
||||||
}),
|
|
||||||
condition: () => confirmRemove.value,
|
condition: () => confirmRemove.value,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -242,38 +240,35 @@ function getDropdownOptions(user) {
|
|||||||
let options = [
|
let options = [
|
||||||
{
|
{
|
||||||
label: __('Admin'),
|
label: __('Admin'),
|
||||||
component: (props) =>
|
component: () =>
|
||||||
DropdownOption({
|
DropdownOption({
|
||||||
option: __('Admin'),
|
option: __('Admin'),
|
||||||
icon: 'shield',
|
icon: 'shield',
|
||||||
active: props.active,
|
|
||||||
selected: user.role === 'System Manager',
|
selected: user.role === 'System Manager',
|
||||||
onClick: () => updateRole(user, 'System Manager'),
|
|
||||||
}),
|
}),
|
||||||
|
onClick: () => updateRole(user, 'System Manager'),
|
||||||
condition: () => isAdmin(),
|
condition: () => isAdmin(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Manager'),
|
label: __('Manager'),
|
||||||
component: (props) =>
|
component: () =>
|
||||||
DropdownOption({
|
DropdownOption({
|
||||||
option: __('Manager'),
|
option: __('Manager'),
|
||||||
icon: 'briefcase',
|
icon: 'briefcase',
|
||||||
active: props.active,
|
|
||||||
selected: user.role === 'Sales Manager',
|
selected: user.role === 'Sales Manager',
|
||||||
onClick: () => updateRole(user, 'Sales Manager'),
|
|
||||||
}),
|
}),
|
||||||
|
onClick: () => updateRole(user, 'Sales Manager'),
|
||||||
condition: () => isManager(),
|
condition: () => isManager(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Sales User'),
|
label: __('Sales User'),
|
||||||
component: (props) =>
|
component: () =>
|
||||||
DropdownOption({
|
DropdownOption({
|
||||||
option: __('Sales User'),
|
option: __('Sales User'),
|
||||||
icon: 'user-check',
|
icon: 'user-check',
|
||||||
active: props.active,
|
|
||||||
selected: user.role === 'Sales User',
|
selected: user.role === 'Sales User',
|
||||||
onClick: () => updateRole(user, 'Sales User'),
|
|
||||||
}),
|
}),
|
||||||
|
onClick: () => updateRole(user, 'Sales User'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -462,23 +462,12 @@ export function runSequentially(functions) {
|
|||||||
}, Promise.resolve())
|
}, Promise.resolve())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownOption({
|
export function DropdownOption({ option, icon, selected }) {
|
||||||
active,
|
|
||||||
option,
|
|
||||||
theme,
|
|
||||||
icon,
|
|
||||||
onClick,
|
|
||||||
selected,
|
|
||||||
}) {
|
|
||||||
return h(
|
return h(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
class: [
|
class:
|
||||||
active ? 'bg-surface-gray-2' : 'text-ink-gray-8',
|
'group flex w-full text-ink-gray-8 justify-between items-center rounded-md px-2 py-2 text-sm hover:bg-surface-gray-2',
|
||||||
'group flex w-full justify-between items-center rounded-md px-2 py-2 text-sm',
|
|
||||||
theme == 'danger' ? 'text-ink-red-3 hover:bg-ink-red-1' : '',
|
|
||||||
],
|
|
||||||
onClick: !selected ? onClick : null,
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
h('div', { class: 'flex gap-2' }, [
|
h('div', { class: 'flex gap-2' }, [
|
||||||
@ -501,30 +490,6 @@ export function DropdownOption({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TemplateOption({ active, option, theme, icon, onClick }) {
|
|
||||||
return h(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
class: [
|
|
||||||
active ? 'bg-surface-gray-2 text-ink-gray-8' : 'text-ink-gray-7',
|
|
||||||
'group flex w-full gap-2 items-center rounded-md px-2 py-2 text-sm',
|
|
||||||
theme == 'danger' ? 'text-ink-red-3 hover:bg-ink-red-1' : '',
|
|
||||||
],
|
|
||||||
onClick: onClick,
|
|
||||||
},
|
|
||||||
[
|
|
||||||
icon
|
|
||||||
? h(FeatherIcon, {
|
|
||||||
name: icon,
|
|
||||||
class: ['h-4 w-4 shrink-0'],
|
|
||||||
'aria-hidden': true,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
h('span', { class: 'whitespace-nowrap' }, option),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copy(obj) {
|
export function copy(obj) {
|
||||||
if (!obj) return obj
|
if (!obj) return obj
|
||||||
return JSON.parse(JSON.stringify(obj))
|
return JSON.parse(JSON.stringify(obj))
|
||||||
|
|||||||
@ -2,137 +2,122 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
|
||||||
import frappeui from 'frappe-ui/vite'
|
import frappeui from 'frappe-ui/vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
function appPath(app) {
|
|
||||||
const root = path.resolve(__dirname, '../..') // points to apps
|
|
||||||
const frontendPaths = [
|
|
||||||
// Standard frontend structure: appname/frontend/src
|
|
||||||
path.join(root, app, 'frontend', 'src'),
|
|
||||||
// Desk-based apps: appname/desk/src
|
|
||||||
path.join(root, app, 'desk', 'src'),
|
|
||||||
// Alternative frontend structures
|
|
||||||
path.join(root, app, 'client', 'src'),
|
|
||||||
path.join(root, app, 'ui', 'src'),
|
|
||||||
// Direct src structure: appname/src
|
|
||||||
path.join(root, app, 'src'),
|
|
||||||
]
|
|
||||||
return frontendPaths.find((srcPath) => fs.existsSync(srcPath)) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasApp(app) {
|
|
||||||
return fs.existsSync(appPath(app))
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of frontend apps used in this project
|
|
||||||
let apps = []
|
|
||||||
|
|
||||||
const alias = [
|
|
||||||
// Default "@" for this app
|
|
||||||
{
|
|
||||||
find: '@',
|
|
||||||
replacement: path.resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
|
|
||||||
// App-specific aliases like @helpdesk, @hrms, etc.
|
|
||||||
...apps.map((app) =>
|
|
||||||
hasApp(app)
|
|
||||||
? { find: `@${app}`, replacement: appPath(app) }
|
|
||||||
: { find: `@${app}`, replacement: `virtual:${app}` },
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
const defineFlags = Object.fromEntries(
|
|
||||||
apps.map((app) => [
|
|
||||||
`__HAS_${app.toUpperCase()}__`,
|
|
||||||
JSON.stringify(hasApp(app)),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
|
|
||||||
const virtualStubPlugin = {
|
|
||||||
name: 'virtual-empty-modules',
|
|
||||||
resolveId(id) {
|
|
||||||
if (id.startsWith('virtual:')) return '\0' + id
|
|
||||||
},
|
|
||||||
load(id) {
|
|
||||||
if (id.startsWith('\0virtual:')) {
|
|
||||||
return 'export default {}; export const missing = true;'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Generated app aliases:', alias)
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(async ({ mode }) => {
|
||||||
define: defineFlags,
|
const isDev = mode === 'development'
|
||||||
plugins: [
|
const frappeui = await importFrappeUIPlugin(isDev)
|
||||||
frappeui({
|
|
||||||
frappeProxy: true,
|
const config = {
|
||||||
lucideIcons: true,
|
plugins: [
|
||||||
jinjaBootData: true,
|
frappeui({
|
||||||
buildConfig: {
|
frappeProxy: true,
|
||||||
indexHtmlPath: '../crm/www/crm.html',
|
lucideIcons: true,
|
||||||
emptyOutDir: true,
|
jinjaBootData: true,
|
||||||
sourcemap: true,
|
buildConfig: {
|
||||||
},
|
indexHtmlPath: '../crm/www/crm.html',
|
||||||
}),
|
emptyOutDir: true,
|
||||||
vue(),
|
sourcemap: true,
|
||||||
vueJsx(),
|
},
|
||||||
VitePWA({
|
}),
|
||||||
registerType: 'autoUpdate',
|
vue(),
|
||||||
devOptions: {
|
vueJsx(),
|
||||||
enabled: true,
|
VitePWA({
|
||||||
},
|
registerType: 'autoUpdate',
|
||||||
manifest: {
|
devOptions: {
|
||||||
display: 'standalone',
|
enabled: true,
|
||||||
name: 'Frappe CRM',
|
},
|
||||||
short_name: 'Frappe CRM',
|
manifest: {
|
||||||
start_url: '/crm',
|
display: 'standalone',
|
||||||
description:
|
name: 'Frappe CRM',
|
||||||
'Modern & 100% Open-source CRM tool to supercharge your sales operations',
|
short_name: 'Frappe CRM',
|
||||||
icons: [
|
start_url: '/crm',
|
||||||
{
|
description:
|
||||||
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
|
'Modern & 100% Open-source CRM tool to supercharge your sales operations',
|
||||||
sizes: '192x192',
|
icons: [
|
||||||
type: 'image/png',
|
{
|
||||||
purpose: 'any',
|
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
|
||||||
},
|
sizes: '192x192',
|
||||||
{
|
type: 'image/png',
|
||||||
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
|
purpose: 'any',
|
||||||
sizes: '192x192',
|
},
|
||||||
type: 'image/png',
|
{
|
||||||
purpose: 'maskable',
|
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
|
||||||
},
|
sizes: '192x192',
|
||||||
{
|
type: 'image/png',
|
||||||
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
|
purpose: 'maskable',
|
||||||
sizes: '512x512',
|
},
|
||||||
type: 'image/png',
|
{
|
||||||
purpose: 'any',
|
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
|
||||||
},
|
sizes: '512x512',
|
||||||
{
|
type: 'image/png',
|
||||||
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
|
purpose: 'any',
|
||||||
sizes: '512x512',
|
},
|
||||||
type: 'image/png',
|
{
|
||||||
purpose: 'maskable',
|
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
|
||||||
},
|
sizes: '512x512',
|
||||||
],
|
type: 'image/png',
|
||||||
},
|
purpose: 'maskable',
|
||||||
}),
|
},
|
||||||
virtualStubPlugin,
|
],
|
||||||
],
|
},
|
||||||
resolve: { alias },
|
}),
|
||||||
optimizeDeps: {
|
|
||||||
include: [
|
|
||||||
'feather-icons',
|
|
||||||
'showdown',
|
|
||||||
'tailwind.config.js',
|
|
||||||
'prosemirror-state',
|
|
||||||
'prosemirror-view',
|
|
||||||
'lowlight',
|
|
||||||
'interactjs'
|
|
||||||
],
|
],
|
||||||
},
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: [
|
||||||
|
'feather-icons',
|
||||||
|
'showdown',
|
||||||
|
'tailwind.config.js',
|
||||||
|
'prosemirror-state',
|
||||||
|
'prosemirror-view',
|
||||||
|
'lowlight',
|
||||||
|
'interactjs',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add local frappe-ui alias only in development if the local frappe-ui exists
|
||||||
|
if (isDev) {
|
||||||
|
try {
|
||||||
|
// Check if the local frappe-ui directory exists
|
||||||
|
const fs = await import('node:fs')
|
||||||
|
const localFrappeUIPath = path.resolve(__dirname, '../frappe-ui')
|
||||||
|
if (fs.existsSync(localFrappeUIPath)) {
|
||||||
|
config.resolve.alias['frappe-ui'] = localFrappeUIPath
|
||||||
|
} else {
|
||||||
|
console.warn('Local frappe-ui directory not found, using npm package')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'Error checking for local frappe-ui, using npm package:',
|
||||||
|
error.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function importFrappeUIPlugin(isDev) {
|
||||||
|
if (isDev) {
|
||||||
|
try {
|
||||||
|
const module = await import('../frappe-ui/vite')
|
||||||
|
return module.default
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'Local frappe-ui not found, falling back to npm package:',
|
||||||
|
error.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to npm package if local import fails
|
||||||
|
const module = await import('frappe-ui/vite')
|
||||||
|
return module.default
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": ["frontend", "frappe-ui"],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "cd frontend && yarn install",
|
"postinstall": "cd frontend && yarn install",
|
||||||
"dev": "cd frontend && yarn dev",
|
"dev": "cd frontend && yarn dev",
|
||||||
"build": "cd frontend && yarn build",
|
"build": "cd frontend && yarn build",
|
||||||
"disable-workspaces": "sed -i '' 's/\"workspaces\"/\"aworkspaces\"/g' package.json",
|
"upgrade-frappeui": "cd frontend && yarn add frappe-ui@latest && cd .."
|
||||||
"enable-workspaces": "sed -i '' 's/\"aworkspaces\"/\"workspaces\"/g' package.json && rm -rf node_modules ./frontend/node_modules/ frappe-ui/node_modules/ && yarn install",
|
|
||||||
"upgrade-frappeui": "cd frontend && yarn add frappe-ui@latest && cd ..",
|
|
||||||
"disable-workspaces-and-upgrade-frappeui": "yarn disable-workspaces && yarn upgrade-frappeui"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user