Merge pull request #1260 from frappe/mergify/bp/main-hotfix/pr-1256

This commit is contained in:
Shariq Ansari 2025-09-18 15:44:32 +05:30 committed by GitHub
commit fabd362b2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 817 additions and 905 deletions

View File

@ -25,7 +25,7 @@ class CRMDeal(Document):
add_status_change_log(self)
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
self.closed_date = frappe.utils.nowdate()
self.validate_forcasting_fields()
self.validate_forecasting_fields()
self.validate_lost_reason()
self.update_exchange_rate()
@ -151,9 +151,21 @@ class CRMDeal(Document):
if not self.probability or self.probability == 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_default_probability()
self.update_expected_deal_value()
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
if not self.expected_deal_value or self.expected_deal_value == 0:
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)

View File

@ -8,6 +8,7 @@
"defaults_tab",
"restore_defaults",
"enable_forecasting",
"auto_update_expected_deal_value",
"currency_tab",
"currency",
"exchange_rate_provider_section",
@ -105,12 +106,19 @@
{
"fieldname": "column_break_vqck",
"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,
"issingle": 1,
"links": [],
"modified": "2025-07-29 11:26:50.420614",
"modified": "2025-09-16 17:33:26.406549",
"modified_by": "Administrator",
"module": "FCRM",
"name": "FCRM Settings",

View File

@ -33,7 +33,7 @@ declare module 'vue' {
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.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']
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.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']
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.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']
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.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']
Filter: typeof import('./src/components/Filter.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']
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.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']
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.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']
HeartIcon: typeof import('./src/components/Icons/HeartIcon.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']
IconPicker: typeof import('./src/components/IconPicker.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']
SortBy: typeof import('./src/components/SortBy.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']
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']

View File

@ -110,7 +110,6 @@
</template>
<script setup>
import { TemplateOption } from '@/utils'
import {
Autocomplete,
Button,
@ -191,31 +190,17 @@ const dropdownOptions = computed(() => {
options.push({
label: __('Remove'),
component: (props) =>
TemplateOption({
option: __('Remove'),
icon: 'trash-2',
active: props.active,
variant: 'danger',
onClick: () => {
emit('remove')
},
}),
variant: 'red',
onClick: () => emit('remove'),
condition: () => !props.isGroup,
})
options.push({
label: __('Remove group'),
component: (props) =>
TemplateOption({
option: __('Remove group'),
icon: 'trash-2',
active: props.active,
variant: 'danger',
onClick: () => {
emit('remove')
},
}),
variant: 'red',
onClick: () => emit('remove'),
condition: () => props.isGroup,
})

View File

@ -1,5 +1,5 @@
<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">
<CFCondition
v-if="Array.isArray(condition)"

View File

@ -1,7 +1,6 @@
<template>
<FileUploader
:file-types="image_type"
class="text-base"
@success="
(file) => {
$emit('upload', file.file_url)
@ -10,21 +9,28 @@
>
<template v-slot="{ progress, uploading, openFileSelector }">
<div class="flex items-end space-x-1">
<Button @click="openFileSelector">
{{
<Button
@click="openFileSelector"
:iconLeft="uploading ? 'cloud-upload' : ImageUpIcon"
:label="
uploading
? `Uploading ${progress}%`
? __('Uploading {0}%', [progress])
: image_url
? 'Change'
: 'Upload'
}}
</Button>
<Button v-if="image_url" @click="$emit('remove')">Remove</Button>
? __('Change')
: __('Upload')
"
/>
<Button
v-if="image_url"
:label="__('Remove')"
@click="$emit('remove')"
/>
</div>
</template>
</FileUploader>
</template>
<script setup>
import ImageUpIcon from '~icons/lucide/image-up'
import { FileUploader, Button } from 'frappe-ui'
const prop = defineProps({
@ -33,10 +39,6 @@ const prop = defineProps({
type: String,
default: 'image/*',
},
label: {
type: String,
default: '',
},
})
const emit = defineEmits(['upload', 'remove'])
</script>

View 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>

View File

@ -6,10 +6,7 @@
}}</span>
<span class="text-p-sm text-ink-gray-6">
{{
__(
'Define who receives the {0} and how theyre distributed among agents.',
[documentType],
)
__('Choose how {0} are assigned among salespeople.', [documentType])
}}
</span>
</div>
@ -26,7 +23,7 @@
</div>
<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,
])
}}
@ -36,7 +33,7 @@
<Popover placement="bottom-end">
<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] 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()"
>
<div>
@ -84,7 +81,7 @@
{{ __('Assignees') }}
</div>
<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>
<AssigneeSearch @addAssignee="validateAssignmentRule('users')" />

View File

@ -1,25 +1,25 @@
<template>
<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
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 }}
</div>
</div>
<div class="col-span-2">
<div class="w-3/12">
<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"
v-model="data.priority"
@update:modelValue="onPriorityChange"
/>
</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
size="sm"
:modelValue="!data.disabled"
@ -72,7 +72,6 @@ import {
toast,
} from 'frappe-ui'
import { inject, ref } from 'vue'
import { TemplateOption } from '@/utils'
const assignmentRulesList = inject('assignmentRulesList')
const updateStep = inject('updateStep')
@ -128,29 +127,19 @@ const dropdownOptions = [
},
{
label: __('Delete'),
component: (props) =>
TemplateOption({
option: __('Delete'),
icon: 'trash-2',
active: props.active,
onClick: (e) => {
e.preventDefault()
e.stopImmediatePropagation()
isConfirmingDelete.value = true
},
}),
condition: () => !isConfirmingDelete.value,
},
{
label: __('Confirm Delete'),
component: (props) =>
TemplateOption({
option: __('Confirm Delete'),
icon: 'trash-2',
active: props.active,
theme: 'danger',
theme: 'red',
onClick: () => deleteAssignmentRule(),
}),
condition: () => isConfirmingDelete.value,
},
]

View File

@ -1,15 +1,9 @@
<template>
<div
v-if="getAssignmentRuleData.loading"
class="flex items-center h-full justify-center"
>
<LoadingIndicator class="w-4" />
</div>
<div
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">
<Button
variant="ghost"
@ -47,8 +41,7 @@
/>
</div>
</div>
</div>
<div v-if="!getAssignmentRuleData.loading" class="overflow-y-auto px-10 pb-8">
<div class="overflow-y-auto px-2">
<div class="grid grid-cols-2 gap-5">
<div>
<FormControl
@ -72,7 +65,7 @@
<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"
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>
@ -188,7 +181,7 @@
</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"
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"
>
<span class="text-p-sm">
@ -233,9 +226,10 @@
<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 un-assignment rule.', [
documentType,
])
__(
'Choose which {0} are affected by this un-assignment rule.',
[documentType],
)
}}
<a
class="font-medium underline"
@ -245,7 +239,9 @@
>
</span>
<div
v-if="isOldSla && step.data && assignmentRuleData.unassignCondition"
v-if="
isOldSla && step.data && assignmentRuleData.unassignCondition
"
>
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
<template #target>
@ -269,14 +265,14 @@
</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.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>
<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.',
@ -291,11 +287,11 @@
/>
</div>
<AssignmentRulesSection
v-else
:conditions="assignmentRuleData.unassignConditionJson"
name="unassignCondition"
:errors="assignmentRuleErrors.unassignConditionError"
:doctype="assignmentRuleData.documentType"
v-else
/>
</div>
</div>
@ -318,6 +314,10 @@
<hr class="my-8" />
<AssigneeRules />
</div>
</div>
<div v-else class="flex items-center h-full justify-center">
<LoadingIndicator class="w-4" />
</div>
<ConfirmDialog
v-model="showConfirmDialog.show"
:title="showConfirmDialog.title"

View File

@ -1,35 +1,39 @@
<template>
<div class="p-8 sticky top-0">
<div class="flex items-start justify-between">
<div class="flex flex-col gap-1">
<h1 class="text-xl font-semibold text-ink-gray-8">
<div class="flex h-full flex-col gap-6 p-6 text-ink-gray-8">
<!-- Header -->
<div class="flex justify-between px-2 pt-2">
<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') }}
</h1>
<p class="text-p-base text-ink-gray-6 max-w-md">
</h2>
<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>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('Create new')"
theme="gray"
:label="__('New')"
icon-left="plus"
variant="solid"
@click="goToNew()"
icon-left="plus"
/>
</div>
</div>
<div class="overflow-y-auto px-8 pb-6">
<!-- Assignment rules list -->
<div class="overflow-y-auto">
<AssignmentRulesList />
</div>
</div>
</template>
<script setup>
import { createResource } from 'frappe-ui'
import AssignmentRulesList from './AssignmentRulesList.vue'
import { createResource } from 'frappe-ui'
import { inject, provide } from 'vue'
const updateStep = inject('updateStep')

View File

@ -8,25 +8,27 @@
<div v-else>
<div
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">
{{ __('No items in the list') }}
</div>
</div>
<div v-else>
<div class="grid grid-cols-11 items-center gap-4 text-sm text-gray-600">
<div class="col-span-7 ml-2">{{ __('Assignment rule') }}</div>
<div class="col-span-2">{{ __('Priority') }}</div>
<div class="col-span-2">{{ __('Enabled') }}</div>
<div class="flex items-center py-2 px-4 text-sm text-ink-gray-5">
<div class="w-7/12">{{ __('Assignment rule') }}</div>
<div class="w-3/12">{{ __('Priority') }}</div>
<div class="w-2/12">{{ __('Enabled') }}</div>
</div>
<hr class="mt-2 mx-2" />
<div
v-for="assignmentRule in assignmentRulesList.data"
<div class="h-px border-t mx-4 border-outline-gray-modals" />
<div class="overflow-y-auto px-2">
<template
v-for="(assignmentRule, i) in assignmentRulesList.data"
:key="assignmentRule.name"
>
<AssignmentRuleListItem :data="assignmentRule" />
<hr class="mx-2" />
<hr v-if="assignmentRulesList.data.length !== i + 1" class="mx-2" />
</template>
</div>
</div>
</div>

View File

@ -8,7 +8,7 @@
/>
<div
v-if="props.conditions.length == 0"
class="flex p-4 items-center cursor-pointer justify-center gap-2 text-sm border border-gray-300 text-gray-600 rounded-md"
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="
() => {
props.conditions.push(['', '', ''])

View File

@ -1,5 +1,5 @@
<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
class="grid p-2 px-4 items-center"
style="grid-template-columns: 3fr 1fr"
@ -9,7 +9,7 @@
:key="column.key"
class="text-gray-600 overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ column.label }}
{{ __(column.label) }}
</div>
</div>
<hr />
@ -24,10 +24,9 @@
</template>
<script setup>
import { ErrorMessage } from 'frappe-ui'
import { onMounted, ref } from '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 assignmentRuleErrors = inject('assignmentRuleErrors')

View File

@ -3,7 +3,7 @@
class="grid py-3.5 px-4 items-center"
style="grid-template-columns: 3fr 1fr"
>
<div class="text-ink-gray-7 font-medium">{{ data.day }}</div>
<div class="text-ink-gray-7 font-medium">{{ __(data.day) }}</div>
<div class="flex justify-start">
<Switch v-model="data.active" @update:model-value="toggleDay" />
</div>

View File

@ -1,27 +1,18 @@
<template>
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
<!-- Header -->
<div class="flex px-2 justify-between">
<div class="flex items-center gap-1 -ml-4 w-9/12">
<Button
variant="ghost"
icon-left="chevron-left"
:label="__('Brand settings')"
size="md"
@click="() => emit('updateStep', 'general-settings')"
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 class="flex justify-between px-2 text-ink-gray-8">
<div class="flex flex-col gap-1">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Brand settings') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{ __('Configure your brand name, logo, and favicon') }}
</p>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('Update')"
icon-left="plus"
variant="solid"
:disabled="!settings.isDirty"
:loading="settings.loading"
@ -36,35 +27,30 @@
<FormControl
type="text"
class="w-1/2"
size="md"
v-model="settings.doc.brand_name"
:label="__('Brand name')"
:placeholder="__('Enter brand name')"
/>
</div>
<!-- logo -->
<div class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-8">
{{ __('Logo') }}
</span>
<div class="flex flex-1 gap-5">
<div class="flex items-center flex-1 gap-5">
<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
:src="settings.doc?.brand_logo || '/assets/crm/images/logo.png'"
v-if="settings.doc?.brand_logo"
:src="settings.doc?.brand_logo"
alt="Logo"
class="size-8 rounded"
/>
<ImageIcon v-else class="size-5 text-ink-gray-4" />
</div>
<div class="flex flex-1 flex-col gap-2">
<ImageUploader
label="Favicon"
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">
<div class="flex flex-1 flex-col gap-1">
<span class="text-base font-medium">{{ __('Brand logo') }}</span>
<span class="text-p-base text-ink-gray-6">
{{
__(
'Appears in the left sidebar. Recommended size is 32x32 px in PNG or SVG',
@ -72,33 +58,34 @@
}}
</span>
</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>
<!-- favicon -->
<div class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-8">
{{ __('Favicon') }}
</span>
<div class="flex flex-1 gap-5">
<div class="flex items-center flex-1 gap-5">
<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
:src="settings.doc?.favicon || '/assets/crm/images/logo.png'"
v-if="settings.doc?.favicon"
:src="settings.doc?.favicon"
alt="Favicon"
class="size-8 rounded"
/>
<ImageIcon v-else class="size-5 text-ink-gray-4" />
</div>
<div class="flex flex-1 flex-col gap-2">
<ImageUploader
label="Favicon"
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">
<div class="flex flex-1 flex-col gap-1">
<span class="text-base font-medium">{{ __('Favicon') }}</span>
<span class="text-p-base text-ink-gray-6">
{{
__(
'Appears next to the title in your browser tab. Recommended size is 32x32 px in PNG or ICO',
@ -106,20 +93,25 @@
}}
</span>
</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 v-if="errorMessage">
<ErrorMessage :message="__(errorMessage)" />
</div>
</div>
</template>
<script setup>
import ImageIcon from '~icons/lucide/image'
import ImageUploader from '@/components/Controls/ImageUploader.vue'
import { FormControl, ErrorMessage } from 'frappe-ui'
import { FormControl } from 'frappe-ui'
import { getSettings } from '@/stores/settings'
import { showSettings } from '@/composables/settings'
import { ref } from 'vue'
const { _settings: settings, setupBrand } = getSettings()
@ -131,7 +123,4 @@ function updateSettings() {
},
})
}
const emit = defineEmits(['updateStep'])
const errorMessage = ref('')
</script>

View File

@ -1,27 +1,20 @@
<template>
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
<!-- Header -->
<div class="flex px-2 justify-between">
<div class="flex items-center gap-1 -ml-4 w-9/12">
<Button
variant="ghost"
icon-left="chevron-left"
:label="__('Currency & Exchange rate provider')"
size="md"
@click="() => emit('updateStep', 'general-settings')"
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 class="flex justify-between px-2 text-ink-gray-8">
<div class="flex flex-col gap-1">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Currency & Exchange rate provider') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{
__('Configure the currency and exchange rate provider for your CRM')
}}
</p>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('Update')"
icon-left="plus"
variant="solid"
:disabled="!settings.isDirty"
:loading="settings.loading"
@ -32,7 +25,7 @@
<!-- Fields -->
<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="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Currency') }}
@ -61,7 +54,7 @@
</div>
</div>
<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="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Exchange rate provider') }}
@ -131,17 +124,15 @@
</div>
</template>
<script setup>
import { ErrorMessage, toast } from 'frappe-ui'
import { ErrorMessage, FormControl, toast } from 'frappe-ui'
import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global'
import { showSettings } from '@/composables/settings'
import { ref } from 'vue'
import FormControl from 'frappe-ui/src/components/FormControl/FormControl.vue'
const { _settings: settings } = getSettings()
const { $dialog } = globalStore()
const emit = defineEmits(['updateStep'])
const errorMessage = ref('')
function updateSettings() {

View File

@ -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"
/>
</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
:label="__('Update')"
icon-left="plus"
@ -26,13 +30,6 @@
<!-- Fields -->
<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-1">
<FormControl

View File

@ -148,7 +148,6 @@
</div>
</template>
<script setup>
import { TemplateOption } from '@/utils'
import {
TextInput,
FormControl,
@ -223,43 +222,28 @@ function getDropdownOptions(template) {
let options = [
{
label: __('Duplicate'),
component: (props) =>
TemplateOption({
option: __('Duplicate'),
icon: 'copy',
active: props.active,
onClick: () => emit('updateStep', 'new-template', { ...template }),
}),
},
{
label: __('Delete'),
component: (props) =>
TemplateOption({
option: __('Delete'),
icon: 'trash-2',
active: props.active,
onClick: (e) => {
e.preventDefault()
e.stopPropagation()
confirmDelete.value = true
},
}),
condition: () => !confirmDelete.value,
},
{
label: __('Confirm Delete'),
component: (props) =>
TemplateOption({
option: __('Confirm Delete'),
icon: 'trash-2',
active: props.active,
theme: 'danger',
theme: 'red',
onClick: () => deleteTemplate(template),
}),
condition: () => confirmDelete.value,
},
]
return options.filter((option) => option.condition?.() || true)
return options
}
</script>

View File

@ -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"
/>
</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
:label="templateData?.name ? __('Duplicate') : __('Create')"
icon-left="plus"
@ -26,13 +30,6 @@
<!-- Fields -->
<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-1">
<FormControl

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -1,21 +1,18 @@
<template>
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
<!-- Header -->
<div class="flex justify-between">
<div class="flex gap-1 -ml-4 w-9/12">
<Button
variant="ghost"
icon-left="chevron-left"
:label="__('Home actions')"
size="md"
@click="() => emit('updateStep', 'general-settings')"
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 class="flex justify-between text-ink-gray-8">
<div class="flex flex-col gap-1">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Home actions') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{ __('Configure actions that appear on the home dropdown') }}
</p>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('Update')"
icon-left="plus"
variant="solid"
:disabled="!document.isDirty"
:loading="document.loading"
@ -25,7 +22,7 @@
</div>
<!-- Fields -->
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
<div class="flex flex-1 flex-col overflow-y-auto">
<Grid
v-model="document.doc.dropdown_items"
doctype="CRM Dropdown Item"

View File

@ -1,6 +1,6 @@
<template>
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
<div class="flex justify-between">
<div class="flex h-full flex-col gap-6 py-8 px-6 text-ink-gray-8">
<div class="flex px-2 justify-between">
<div class="flex flex-col gap-1 w-9/12">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Send invites to') }}
@ -23,26 +23,21 @@
/>
</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>
<label class="block text-xs text-ink-gray-5 mb-1.5">
{{ __('Invite by email') }}
</label>
<div
class="p-2 group bg-surface-gray-2 hover:bg-surface-gray-3 rounded"
>
<MultiSelectUserInput
class="flex-1"
inputClass="!bg-surface-gray-2 hover:!bg-surface-gray-3 group-hover:!bg-surface-gray-3"
:placeholder="__('john@doe.com')"
v-model="invitees"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
<FormControl
type="textarea"
label="Invite by email"
placeholder="user1@example.com, user2@example.com, ..."
@input="updateInvitees($event.target.value)"
:debounce="100"
:disabled="inviteByEmail.loading"
:description="
__(
'You can invite multiple users by comma separating their email addresses',
)
"
:fetchUsers="false"
/>
</div>
<div
v-if="userExistMessage || inviteeExistMessage"
class="text-xs text-ink-red-3 mt-1.5"
@ -100,15 +95,9 @@
</div>
</template>
<script setup>
import MultiSelectUserInput from '@/components/Controls/MultiSelectUserInput.vue'
import { validateEmail, convertArrayToString } from '@/utils'
import { usersStore } from '@/stores/users'
import {
createListResource,
createResource,
FormControl,
Tooltip,
} from 'frappe-ui'
import { createListResource, createResource, FormControl } from 'frappe-ui'
import { useOnboarding } from 'frappe-ui/frappe'
import { ref, computed } from 'vue'
@ -208,6 +197,15 @@ const pendingInvitations = createListResource({
doctype: 'CRM Invitation',
filters: { status: 'Pending' },
fields: ['name', 'email', 'role'],
pageLength: 999,
auto: true,
})
function updateInvitees(value) {
const emails = value
.split(',')
.map((email) => email.trim())
.filter((email) => validateEmail(email))
invitees.value = emails
}
</script>

View File

@ -7,18 +7,19 @@
>
<template #body>
<div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex flex-col p-2 w-52 shrink-0 bg-surface-gray-2">
<h1 class="px-2 pt-2 mb-3 text-lg font-semibold text-ink-gray-8">
<div class="flex flex-col p-1 w-52 shrink-0 bg-surface-gray-2">
<h1 class="px-3 pt-3 pb-2 text-lg font-semibold text-ink-gray-8">
{{ __('Settings') }}
</h1>
<div v-for="tab in tabs">
<div class="flex flex-col overflow-y-auto">
<template v-for="tab in tabs" :key="tab.label">
<div
v-if="!tab.hideLabel"
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"
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>
<nav class="space-y-1">
<nav class="space-y-1 px-1">
<SidebarLink
v-for="i in tab.items"
:icon="i.icon"
@ -32,6 +33,7 @@
@click="activeSettingsPage = i.label"
/>
</nav>
</template>
</div>
</div>
<div class="flex flex-col flex-1 overflow-y-auto bg-surface-modal">
@ -42,6 +44,9 @@
</Dialog>
</template>
<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 ERPNextIcon from '@/components/Icons/ERPNextIcon.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 SettingsIcon2 from '@/components/Icons/SettingsIcon2.vue'
import Users from '@/components/Settings/Users.vue'
import GeneralSettingsPage from '@/components/Settings/General/GeneralSettingsPage.vue'
import InviteUserPage from '@/components/Settings/InviteUserPage.vue'
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.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 TelephonySettings from '@/components/Settings/TelephonySettings.vue'
import EmailConfig from '@/components/Settings/EmailConfig.vue'
@ -76,7 +84,7 @@ const user = computed(() => getUser() || {})
const tabs = computed(() => {
let _tabs = [
{
label: __('Settings'),
label: __('Personal Settings'),
hideLabel: true,
items: [
{
@ -89,12 +97,32 @@ const tabs = computed(() => {
}),
component: markRaw(ProfileSettings),
},
],
},
{
label: __('General'),
icon: 'settings',
component: markRaw(GeneralSettingsPage),
label: __('System Configuration'),
items: [
{
label: __('Forecasting'),
component: markRaw(ForecastingSettings),
icon: TrendingUpDownIcon,
},
{
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'),
icon: 'user',
@ -107,6 +135,12 @@ const tabs = computed(() => {
component: markRaw(InviteUserPage),
condition: () => isManager(),
},
],
condition: () => isManager(),
},
{
label: __('Email Settings'),
items: [
{
label: __('Email Accounts'),
icon: Email2Icon,
@ -118,6 +152,11 @@ const tabs = computed(() => {
icon: EmailTemplateIcon,
component: markRaw(EmailTemplatePage),
},
],
},
{
label: __('Automation & Rules'),
items: [
{
label: __('Assignment rules'),
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'),
items: [

View File

@ -169,8 +169,16 @@
import AddExistingUserModal from '@/components/Modals/AddExistingUserModal.vue'
import { activeSettingsPage } from '@/composables/settings'
import { usersStore } from '@/stores/users'
import { TemplateOption, DropdownOption } from '@/utils'
import { Avatar, TextInput, toast, call, FeatherIcon, Tooltip } from 'frappe-ui'
import { DropdownOption } from '@/utils'
import {
Dropdown,
Avatar,
TextInput,
toast,
call,
FeatherIcon,
Tooltip,
} from 'frappe-ui'
import { ref, computed, onMounted } from 'vue'
const { users, isAdmin, isManager } = usersStore()
@ -208,29 +216,19 @@ function getMoreOptions(user) {
let options = [
{
label: __('Remove'),
component: (props) =>
TemplateOption({
option: __('Remove'),
icon: 'trash-2',
active: props.active,
onClick: (e) => {
e.preventDefault()
e.stopPropagation()
confirmRemove.value = true
},
}),
condition: () => !confirmRemove.value,
},
{
label: __('Confirm Remove'),
component: (props) =>
TemplateOption({
option: __('Confirm Remove'),
icon: 'trash-2',
active: props.active,
theme: 'danger',
theme: 'red',
onClick: () => removeUser(user, true),
}),
condition: () => confirmRemove.value,
},
]
@ -242,38 +240,35 @@ function getDropdownOptions(user) {
let options = [
{
label: __('Admin'),
component: (props) =>
component: () =>
DropdownOption({
option: __('Admin'),
icon: 'shield',
active: props.active,
selected: user.role === 'System Manager',
onClick: () => updateRole(user, 'System Manager'),
}),
onClick: () => updateRole(user, 'System Manager'),
condition: () => isAdmin(),
},
{
label: __('Manager'),
component: (props) =>
component: () =>
DropdownOption({
option: __('Manager'),
icon: 'briefcase',
active: props.active,
selected: user.role === 'Sales Manager',
onClick: () => updateRole(user, 'Sales Manager'),
}),
onClick: () => updateRole(user, 'Sales Manager'),
condition: () => isManager(),
},
{
label: __('Sales User'),
component: (props) =>
component: () =>
DropdownOption({
option: __('Sales User'),
icon: 'user-check',
active: props.active,
selected: user.role === 'Sales User',
onClick: () => updateRole(user, 'Sales User'),
}),
onClick: () => updateRole(user, 'Sales User'),
},
]

View File

@ -462,23 +462,12 @@ export function runSequentially(functions) {
}, Promise.resolve())
}
export function DropdownOption({
active,
option,
theme,
icon,
onClick,
selected,
}) {
export function DropdownOption({ option, icon, selected }) {
return h(
'button',
{
class: [
active ? 'bg-surface-gray-2' : 'text-ink-gray-8',
'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,
class:
'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',
},
[
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) {
if (!obj) return obj
return JSON.parse(JSON.stringify(obj))

View File

@ -2,72 +2,15 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from 'path'
import fs from 'fs'
import frappeui from 'frappe-ui/vite'
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/
export default defineConfig({
define: defineFlags,
export default defineConfig(async ({ mode }) => {
const isDev = mode === 'development'
const frappeui = await importFrappeUIPlugin(isDev)
const config = {
plugins: [
frappeui({
frappeProxy: true,
@ -121,9 +64,12 @@ export default defineConfig({
],
},
}),
virtualStubPlugin,
],
resolve: { alias },
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
optimizeDeps: {
include: [
'feather-icons',
@ -132,7 +78,46 @@ export default defineConfig({
'prosemirror-state',
'prosemirror-view',
'lowlight',
'interactjs'
'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
}

View File

@ -1,14 +1,10 @@
{
"private": true,
"type": "module",
"workspaces": ["frontend", "frappe-ui"],
"scripts": {
"postinstall": "cd frontend && yarn install",
"dev": "cd frontend && yarn dev",
"build": "cd frontend && yarn build",
"disable-workspaces": "sed -i '' 's/\"workspaces\"/\"aworkspaces\"/g' package.json",
"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"
"upgrade-frappeui": "cd frontend && yarn add frappe-ui@latest && cd .."
}
}