Compare commits

..

1210 Commits

Author SHA1 Message Date
Hussain Nagaria
453dccb33f
fix: revert local ruff comment 2025-10-20 21:27:28 +05:30
Hussain Nagaria
f85a8cc281 feat: document link to failed lead logs 2025-10-19 18:59:53 +05:30
Hussain Nagaria
d62229b9be chore: source in list view and filter 2025-10-19 18:59:37 +05:30
Hussain Nagaria
38e10f91cb feat: field based de-dupe 2025-10-19 18:57:18 +05:30
Hussain Nagaria
1b87635d3d feat: also store form id in lead 2025-10-19 18:37:42 +05:30
Hussain Nagaria
176c7c73bb refactor: class based Sync source 2025-10-19 18:34:35 +05:30
Hussain Nagaria
91b8a48c0e perf: use cached doc 2025-10-19 18:07:16 +05:30
Hussain Nagaria
45bc5925d1 feat: failed lead sync log 2025-10-19 17:43:27 +05:30
Hussain Nagaria
eb0b189d4c refactor: facebook icon instead of image and misc ui fixes 2025-10-19 17:07:05 +05:30
Hussain Nagaria
49b2c66299 fix: reactivity issues 2025-10-19 16:57:29 +05:30
Hussain Nagaria
4e5c2f5426 refactor: better var name 2025-10-19 16:51:31 +05:30
Hussain Nagaria
595c1b0b11 fix: double toasts 2025-10-19 16:50:53 +05:30
Hussain Nagaria
b6f8cbd5eb fix: field name 2025-10-19 16:29:24 +05:30
Hussain Nagaria
872f271eb1 refactor: unified new/edit form for sync source 2025-10-19 16:28:26 +05:30
Hussain Nagaria
5fc0ca9bf0 fix: Autocomplete in grid should set string 2025-10-19 16:17:58 +05:30
Hussain Nagaria
19c69d1495 fix: reload Link on filters change 2025-10-19 14:53:54 +05:30
Hussain Nagaria
5feaddcd6f chore: cleanup sources list 2025-10-18 17:07:54 +05:30
Hussain Nagaria
56de0ce0d9 chore: use long queue 2025-10-08 12:52:07 +05:30
Hussain Nagaria
24f800c8e2 feat: configure background sync frequency 2025-10-08 12:49:47 +05:30
Hussain Nagaria
094869d2e7 refactor: tab to section with display depends on 2025-10-08 12:10:25 +05:30
Hussain Nagaria
373102d182 feat: autocomplete CRM Lead fields in mapping table 2025-10-08 12:06:40 +05:30
Hussain Nagaria
d69b86bcaf feat(Grid): support for field overrides and Autocomplete 2025-10-08 12:06:09 +05:30
Hussain Nagaria
4148640472 feat: update source 2025-10-03 22:11:48 +05:30
Hussain Nagaria
9237398342 fix: send ids instead of objects 2025-10-03 22:11:28 +05:30
Hussain Nagaria
788fe36cfa refactor: disallow two active sources for same form 2025-10-03 21:18:57 +05:30
Hussain Nagaria
cd366839fc feat: configure page and form from CRM UI 2025-10-02 21:57:24 +05:30
Hussain Nagaria
6e1dd04819 fix: default is enabled for source 2025-10-02 18:25:40 +05:30
Hussain Nagaria
d125d12a36 feat: update source page
* also redirect here once a new source is created
2025-10-02 18:02:28 +05:30
Hussain Nagaria
6db4d94e8a chore: remove debug log 2025-10-02 13:01:44 +05:30
Hussain Nagaria
b12c55531f feat: create new sync source 2025-10-02 12:59:45 +05:30
Hussain Nagaria
ddabd82f21 refactor: name to be set by user 2025-10-02 12:21:11 +05:30
Hussain Nagaria
f435d74d0a refactor: access token is now a password field 2025-09-30 19:36:44 +05:30
Hussain Nagaria
5e5929ef40 feat: List UI and basic select source type UI 2025-09-30 19:16:22 +05:30
Hussain Nagaria
7d2ccc2e58 chore: boilerplate frontend code 2025-09-29 19:59:31 +05:30
Hussain Nagaria
9cd86e99c3 feat: de-duplicate leads 2025-09-29 19:30:25 +05:30
Hussain Nagaria
3ac4a582f5 feat: map fields for syncing 2025-09-29 18:59:55 +05:30
Hussain Nagaria
d0cccc2e61 feat: FB Lead Sync first cut 2025-09-26 20:04:10 +05:30
Shariq Ansari
b1b7c5d246
Merge pull request #1279 from frappe/l10n_develop3 2025-09-25 21:52:44 +05:30
Shariq Ansari
4b6d2fb9b6
Merge pull request #1284 from shariquerik/upgrade-frappe-ui 2025-09-25 21:52:06 +05:30
Shariq Ansari
171060df8a build(deps): bump frappeui to 0.1.201 2025-09-25 21:51:02 +05:30
Shariq Ansari
96b28cf11a
Merge pull request #1282 from shariquerik/validate-mandatory-field 2025-09-25 21:47:43 +05:30
Shariq Ansari
dbcda4c548 fix: add validation for mandatory fields in useDocument 2025-09-25 21:41:02 +05:30
Shariq Ansari
9e92282e25 chore: Norwegian Bokmal translations 2025-09-25 02:59:56 +05:30
Shariq Ansari
0138716e07 chore: Serbian (Latin) translations 2025-09-25 02:59:42 +05:30
Shariq Ansari
589c95e263 chore: Serbian (Cyrillic) translations 2025-09-25 02:59:37 +05:30
Shariq Ansari
a4e2663b24 chore: Norwegian Bokmal translations 2025-09-24 02:56:03 +05:30
Shariq Ansari
f91ac266ca chore: Norwegian Bokmal translations 2025-09-23 02:12:50 +05:30
Shariq Ansari
415d5410ba chore: Danish translations 2025-09-23 02:12:49 +05:30
Shariq Ansari
9fc20a3078 chore: Esperanto translations 2025-09-23 02:12:47 +05:30
Shariq Ansari
43dadfe746 chore: Croatian translations 2025-09-23 02:12:46 +05:30
Shariq Ansari
56071d5e0d chore: Thai translations 2025-09-23 02:12:44 +05:30
Shariq Ansari
b0e79d0aef chore: Persian translations 2025-09-23 02:12:43 +05:30
Shariq Ansari
5848649955 chore: Vietnamese translations 2025-09-23 02:12:42 +05:30
Shariq Ansari
3c5ee979f8 chore: Chinese Simplified translations 2025-09-23 02:12:40 +05:30
Shariq Ansari
2acd7476c8 chore: Turkish translations 2025-09-23 02:12:39 +05:30
Shariq Ansari
66c5865828 chore: Russian translations 2025-09-23 02:12:37 +05:30
Shariq Ansari
103a137af1 chore: Portuguese translations 2025-09-23 02:12:36 +05:30
Shariq Ansari
52cc70d704 chore: Dutch translations 2025-09-23 02:12:35 +05:30
Shariq Ansari
d72dcee7b6 chore: Hungarian translations 2025-09-23 02:12:33 +05:30
Shariq Ansari
b473b27f9a chore: Czech translations 2025-09-23 02:12:32 +05:30
Shariq Ansari
8805560144 chore: Arabic translations 2025-09-23 02:12:30 +05:30
Shariq Ansari
3f0c4e9614 chore: Spanish translations 2025-09-23 02:12:29 +05:30
Shariq Ansari
2b13c3f277 chore: French translations 2025-09-23 02:12:27 +05:30
Shariq Ansari
b6fa3bf32b chore: German translations 2025-09-23 02:12:26 +05:30
Shariq Ansari
ae9e59aa00 chore: Serbian (Latin) translations 2025-09-23 02:12:25 +05:30
Shariq Ansari
4533becc62 chore: Bosnian translations 2025-09-23 02:12:23 +05:30
Shariq Ansari
57bd9fe70a chore: Indonesian translations 2025-09-23 02:12:22 +05:30
Shariq Ansari
013c21a5d1 chore: Portuguese, Brazilian translations 2025-09-23 02:12:21 +05:30
Shariq Ansari
9a780039e5 chore: Swedish translations 2025-09-23 02:12:19 +05:30
Shariq Ansari
1bd62289dc chore: Serbian (Cyrillic) translations 2025-09-23 02:12:18 +05:30
Shariq Ansari
f7382f40ac chore: Polish translations 2025-09-23 02:12:17 +05:30
Shariq Ansari
3c870ce042 chore: Italian translations 2025-09-23 02:12:15 +05:30
Shariq Ansari
c05c2393f8
Merge pull request #1266 from frappe/pot_develop_2025-09-21 2025-09-22 15:10:14 +05:30
Shariq Ansari
c16f2a286f
Merge pull request #1262 from frappe/l10n_develop3 2025-09-22 15:09:58 +05:30
Shariq Ansari
07994ec071
Merge pull request #1272 from shariquerik/timezone-fix 2025-09-22 15:09:15 +05:30
Shariq Ansari
96c0c99939 build(deps): bump frappeui to 0.1.200 2025-09-22 15:01:48 +05:30
Shariq Ansari
ce632c69c1 chore: Norwegian Bokmal translations 2025-09-22 02:17:19 +05:30
frappe-pr-bot
625e472303 chore: update POT file 2025-09-21 09:35:13 +00:00
Shariq Ansari
93ed6fcddd chore: Norwegian Bokmal translations 2025-09-21 01:59:49 +05:30
Shariq Ansari
e3eff7f78d chore: Norwegian Bokmal translations 2025-09-20 02:02:45 +05:30
Shariq Ansari
394da5e002 chore: Norwegian Bokmal translations 2025-09-19 02:05:23 +05:30
Shariq Ansari
887ae4ef57
Merge pull request #1256 from shariquerik/better-settings 2025-09-18 15:28:46 +05:30
Shariq Ansari
536d657e5b
Merge pull request #1257 from frappe/l10n_develop3 2025-09-18 15:11:59 +05:30
Shariq Ansari
d687a2eb56 refactor: adjust padding and improve layout for Settings component 2025-09-18 14:43:44 +05:30
Shariq Ansari
1044adc494 refactor: adjust padding and improve layout for Currency and Forecasting settings components 2025-09-18 13:36:06 +05:30
Shariq Ansari
9f95a3a2b2 refactor: update styling and improve layout for assignment rules components 2025-09-18 13:29:44 +05:30
Shariq Ansari
fca831b92e chore: Portuguese translations 2025-09-18 01:57:09 +05:30
Shariq Ansari
69f8090311 refactor: replace EmailMultiSelect with FormControl for inviting users by email 2025-09-17 13:14:27 +05:30
Shariq Ansari
ac34ac9b87 refactor: remove TemplateOption component usage and simplify dropdown options in multiple components 2025-09-17 13:01:49 +05:30
Shariq Ansari
129f8a00b6 refactor: update Vite configuration to support dynamic loading of frappe-ui in development mode 2025-09-17 12:27:55 +05:30
Shariq Ansari
6328b6941b revert: create dynamic alias to use components from frontend vue apps 2025-09-17 12:27:55 +05:30
Shariq Ansari
fbc9e37036 refactor: reduce gap in Brand logo and Favicon sections for improved layout 2025-09-17 12:27:55 +05:30
Shariq Ansari
149901f605 refactor: remove icon-left from Update button in multiple settings components 2025-09-17 12:27:55 +05:30
Shariq Ansari
7e21a5fee2 feat: Auto update expected deal value based on products value 2025-09-17 12:27:17 +05:30
Shariq Ansari
f4ff6bbdf3 refactor: add ForecastingSettings component and remove GeneralSettingsPage component 2025-09-17 12:27:17 +05:30
Shariq Ansari
9150233173 fix: add auto-update expected deal value checkbox in FCRM settings 2025-09-17 12:27:17 +05:30
Shariq Ansari
186584c1ac refactor: CurrencySettings component 2025-09-17 12:27:17 +05:30
Shariq Ansari
3752c61157 refactor: update BrandSettings component to improve logo and favicon handling 2025-09-17 12:27:04 +05:30
Shariq Ansari
a6ecc5cfed refactor: HomeActions component 2025-09-17 12:27:04 +05:30
Shariq Ansari
84e0fe30a9 refactor: BrandSettings component 2025-09-17 12:26:46 +05:30
Shariq Ansari
03acea69b1 refactor: enhance Settings component structure 2025-09-17 12:23:03 +05:30
Shariq Ansari
e19f750831 refactor: clean up ImageUploader component and improve label handling 2025-09-17 12:23:03 +05:30
Shariq Ansari
a52bfee98d
Merge pull request #1252 from pratikb64/fix/assignment-rule-ui 2025-09-17 12:17:14 +05:30
Pratik Badhe
41ef219d0a revert: yarn.lock file 2025-09-17 06:29:00 +00:00
Shariq Ansari
0a2f9e31c0
Merge branch 'develop' into fix/assignment-rule-ui 2025-09-17 11:42:23 +05:30
Shariq Ansari
69bcf0846c
Merge pull request #1253 from frappe/l10n_develop3 2025-09-17 11:37:33 +05:30
Shariq Ansari
b6e3cdfc37 chore: Norwegian Bokmal translations 2025-09-17 01:33:08 +05:30
Shariq Ansari
c0171c0555 chore: Danish translations 2025-09-17 01:33:07 +05:30
Shariq Ansari
6f154e191a chore: Esperanto translations 2025-09-17 01:33:06 +05:30
Shariq Ansari
552e500a31 chore: Croatian translations 2025-09-17 01:33:04 +05:30
Shariq Ansari
bf6940a6ff chore: Thai translations 2025-09-17 01:33:03 +05:30
Shariq Ansari
dc9b07b02a chore: Persian translations 2025-09-17 01:33:01 +05:30
Shariq Ansari
0a45094c33 chore: Vietnamese translations 2025-09-17 01:33:00 +05:30
Shariq Ansari
247d8e043e chore: Chinese Simplified translations 2025-09-17 01:32:58 +05:30
Shariq Ansari
73a1ecd418 chore: Turkish translations 2025-09-17 01:32:57 +05:30
Shariq Ansari
77e7bb011b chore: Russian translations 2025-09-17 01:32:55 +05:30
Shariq Ansari
9233e77ab8 chore: Portuguese translations 2025-09-17 01:32:54 +05:30
Shariq Ansari
32e5d56ef1 chore: Dutch translations 2025-09-17 01:32:52 +05:30
Shariq Ansari
cea6b6c6b4 chore: Hungarian translations 2025-09-17 01:32:51 +05:30
Shariq Ansari
2d636d7ffb chore: Czech translations 2025-09-17 01:32:49 +05:30
Shariq Ansari
9d9caf2856 chore: Arabic translations 2025-09-17 01:32:48 +05:30
Shariq Ansari
4ff4f3c5b5 chore: Spanish translations 2025-09-17 01:32:46 +05:30
Shariq Ansari
8167e1388d chore: French translations 2025-09-17 01:32:45 +05:30
Shariq Ansari
8e3cf3846a chore: German translations 2025-09-17 01:32:43 +05:30
Shariq Ansari
2d8ada04c8 chore: Serbian (Latin) translations 2025-09-17 01:32:42 +05:30
Shariq Ansari
4fba2353cf chore: Bosnian translations 2025-09-17 01:32:41 +05:30
Shariq Ansari
f251d83e97 chore: Indonesian translations 2025-09-17 01:32:39 +05:30
Shariq Ansari
283b34662e chore: Portuguese, Brazilian translations 2025-09-17 01:32:38 +05:30
Shariq Ansari
9bcfcf4ac7 chore: Swedish translations 2025-09-17 01:32:36 +05:30
Shariq Ansari
d80bbcd33d chore: Serbian (Cyrillic) translations 2025-09-17 01:32:34 +05:30
Shariq Ansari
42ee5ea64d chore: Polish translations 2025-09-17 01:32:33 +05:30
Shariq Ansari
1472a7f33d chore: Italian translations 2025-09-17 01:32:32 +05:30
Shariq Ansari
aabcb9b7ce
Merge pull request #1242 from frappe/l10n_develop3 2025-09-16 14:19:05 +05:30
Shariq Ansari
9e8a247dee
Merge pull request #1245 from frappe/pot_develop_2025-09-14 2025-09-16 14:18:32 +05:30
Shariq Ansari
2e5f4a9d22 chore: Danish translations 2025-09-16 01:17:10 +05:30
Shariq Ansari
100d01334a chore: Persian translations 2025-09-16 01:17:07 +05:30
Shariq Ansari
03ab96d94f chore: Portuguese translations 2025-09-16 01:17:02 +05:30
Shariq Ansari
6f640f5eee chore: Norwegian Bokmal translations 2025-09-15 00:48:54 +05:30
frappe-pr-bot
1627cf1e54 chore: update POT file 2025-09-14 09:35:17 +00:00
Shariq Ansari
376917bc75 chore: Norwegian Bokmal translations 2025-09-14 00:49:30 +05:30
Shariq Ansari
54753d3274 chore: Portuguese translations 2025-09-14 00:49:28 +05:30
Shariq Ansari
3c3108a9c1 chore: Turkish translations 2025-09-12 00:21:46 +05:30
Shariq Ansari
e0cfae1eb3 chore: Norwegian Bokmal translations 2025-09-11 00:24:54 +05:30
Shariq Ansari
fa03245eff chore: Norwegian Bokmal translations 2025-09-09 23:38:04 +05:30
Shariq Ansari
f253392ba7 chore: Serbian (Latin) translations 2025-09-09 23:38:03 +05:30
Shariq Ansari
6e608d845b chore: Serbian (Cyrillic) translations 2025-09-09 23:38:02 +05:30
Pratik Badhe
db577afc56 fix: paddings and labels 2025-09-09 09:00:31 +00:00
Shariq Ansari
3b32b9a766
Merge pull request #1240 from frappe/l10n_develop3 2025-09-09 14:27:02 +05:30
Shariq Ansari
ffd2452675 chore: Norwegian Bokmal translations 2025-09-08 23:35:30 +05:30
Shariq Ansari
b2949afd33
Merge pull request #1237 from frappe/l10n_develop3 2025-09-08 11:19:09 +05:30
Shariq Ansari
097c58b991 chore: Norwegian Bokmal translations 2025-09-07 23:04:40 +05:30
Shariq Ansari
64bf702b62 chore: Danish translations 2025-09-07 23:04:39 +05:30
Shariq Ansari
7e27e9f45e chore: Esperanto translations 2025-09-07 23:04:37 +05:30
Shariq Ansari
ee19b344ec chore: Croatian translations 2025-09-07 23:04:36 +05:30
Shariq Ansari
eb70553fea chore: Thai translations 2025-09-07 23:04:35 +05:30
Shariq Ansari
9e4d268b50 chore: Persian translations 2025-09-07 23:04:34 +05:30
Shariq Ansari
d92c25ab4e chore: Vietnamese translations 2025-09-07 23:04:33 +05:30
Shariq Ansari
9975be4ff7 chore: Chinese Simplified translations 2025-09-07 23:04:31 +05:30
Shariq Ansari
2442f6f2ad chore: Turkish translations 2025-09-07 23:04:30 +05:30
Shariq Ansari
e39a20f652 chore: Russian translations 2025-09-07 23:04:29 +05:30
Shariq Ansari
dcca47f3ca chore: Portuguese translations 2025-09-07 23:04:28 +05:30
Shariq Ansari
df8aaea374 chore: Dutch translations 2025-09-07 23:04:26 +05:30
Shariq Ansari
c3ac80afa8 chore: Hungarian translations 2025-09-07 23:04:25 +05:30
Shariq Ansari
8d59359ef5 chore: Czech translations 2025-09-07 23:04:24 +05:30
Shariq Ansari
53202f4a5b chore: Arabic translations 2025-09-07 23:04:23 +05:30
Shariq Ansari
792db27252 chore: Spanish translations 2025-09-07 23:04:22 +05:30
Shariq Ansari
c9bcbcf1d0 chore: French translations 2025-09-07 23:04:20 +05:30
Shariq Ansari
79a0c02f03 chore: German translations 2025-09-07 23:04:19 +05:30
Shariq Ansari
2a5dbfb75d chore: Serbian (Latin) translations 2025-09-07 23:04:18 +05:30
Shariq Ansari
b7443ff1bb chore: Bosnian translations 2025-09-07 23:04:17 +05:30
Shariq Ansari
5ee9d5787a chore: Indonesian translations 2025-09-07 23:04:15 +05:30
Shariq Ansari
426dc836ca chore: Portuguese, Brazilian translations 2025-09-07 23:04:14 +05:30
Shariq Ansari
d528710379 chore: Swedish translations 2025-09-07 23:04:13 +05:30
Shariq Ansari
0a4cfa6055 chore: Serbian (Cyrillic) translations 2025-09-07 23:04:12 +05:30
Shariq Ansari
1ca85157ab chore: Polish translations 2025-09-07 23:04:10 +05:30
Shariq Ansari
d8162a1dc4 chore: Italian translations 2025-09-07 23:04:09 +05:30
Shariq Ansari
9487edf05f
Merge pull request #1234 from shariquerik/org-fix 2025-09-07 19:25:19 +05:30
Shariq Ansari
30d95a6582 fix: remove unnecessary class from Button component in TaskModal and add click.stop to Notes dropdown 2025-09-07 18:56:31 +05:30
Shariq Ansari
24b580150a fix: add 'interactjs' to optimizeDeps include list in vite.config.js 2025-09-07 18:50:58 +05:30
Shariq Ansari
1b7af2096f fix: update organization logo handling and add beforeFieldChange functionality to rename organization 2025-09-07 18:50:43 +05:30
Shariq Ansari
843f844e2c
Merge pull request #1230 from frappe/l10n_develop3 2025-09-07 17:59:20 +05:30
Shariq Ansari
ed7d739291
Merge pull request #1231 from frappe/pot_develop_2025-09-07 2025-09-07 17:49:48 +05:30
frappe-pr-bot
591076bf27 chore: update POT file 2025-09-07 09:35:13 +00:00
Shariq Ansari
891d78c3b6 chore: Norwegian Bokmal translations 2025-09-06 22:31:01 +05:30
Shariq Ansari
4baee8351b chore: Norwegian Bokmal translations 2025-09-05 22:35:31 +05:30
Shariq Ansari
0653c2293c
Merge pull request #1225 from shariquerik/calendar-fixes 2025-09-05 19:13:54 +05:30
Shariq Ansari
509bdd08dd
Merge pull request #1224 from frappe/l10n_develop3 2025-09-05 16:16:59 +05:30
Shariq Ansari
93db75b835
Merge pull request #1226 from shariquerik/contact-fix 2025-09-05 15:45:13 +05:30
Shariq Ansari
af6970569f Revert "fix: add PrimaryDropdown and PrimaryDropdownItem components for enhanced dropdown functionality in forms"
This reverts commit 816bc700ede63c2e9d12f70cea1e90c26f11acc1.
2025-09-05 15:42:51 +05:30
Shariq Ansari
50708ebe32 fix: if contact email is updated it is updating previously opened contact 2025-09-05 15:34:36 +05:30
Shariq Ansari
816bc700ed fix: add PrimaryDropdown and PrimaryDropdownItem components for enhanced dropdown functionality in forms 2025-09-05 15:29:30 +05:30
Shariq Ansari
6e8228a82c fix: inject events resource into CalendarEventPanel for improved event handling 2025-09-05 12:24:46 +05:30
Shariq Ansari
54e4819c71 fix: prevent saving event if no changes were made 2025-09-05 12:06:14 +05:30
Shariq Ansari
f573db2fe0 fix: add isDialogOpen utility function and update keyboard shortcuts to respect dialog state 2025-09-05 11:56:24 +05:30
Shariq Ansari
a45c150a3d fix: format event times to HH:mm on update 2025-09-05 11:47:34 +05:30
Shariq Ansari
46a7a9c495 fix: refactor attendee input to use Combobox component for improved UX 2025-09-05 11:40:17 +05:30
Shariq Ansari
09ff459751 fix: remove unused daily header template from Calendar component 2025-09-04 23:00:06 +05:30
Shariq Ansari
1d249b8fff fix: disable create button when event data is not modified 2025-09-04 22:44:32 +05:30
Shariq Ansari
5eaf828758 fix: only show non converted leads 2025-09-04 22:37:34 +05:30
Shariq Ansari
0eb07f0242 fix: remove unused link reference fields from EventModal component 2025-09-04 22:32:05 +05:30
Shariq Ansari
023d949577 chore: Portuguese translations 2025-09-04 22:29:29 +05:30
Shariq Ansari
abec857dd2 fix: enable double-click to edit details in CalendarEventPanel component 2025-09-04 22:27:31 +05:30
Shariq Ansari
58914d7053 fix: remove conditional rendering for Calendar component when events are present 2025-09-04 22:23:53 +05:30
Shariq Ansari
c93909523d fix: update button visibility on hover in KanbanView component 2025-09-04 22:23:42 +05:30
Shariq Ansari
800f3f1453
Merge pull request #1220 from frappe/l10n_develop3 2025-09-04 17:51:30 +05:30
Shariq Ansari
9c1d0b3d56
Merge pull request #1221 from aftabshaikh-agkiya/fix/address-modal 2025-09-04 17:50:54 +05:30
Shariq Ansari
f9f405cc00
chore: removed commented code 2025-09-04 17:49:52 +05:30
Shariq Ansari
181439be1d
Merge pull request #748 from shariquerik/google-calendar 2025-09-04 13:34:48 +05:30
Shariq Ansari
1d2328ced1 build(deps): bump frappeui to 0.1.197 2025-09-04 13:23:36 +05:30
Shariq Ansari
b9b073601b build(deps): bump frappeui to 0.1.196 2025-09-04 13:12:12 +05:30
Shariq Ansari
feec676632 fix: change default size of Autocomplete component from 'md' to 'sm' 2025-09-04 11:57:38 +05:30
Shariq Ansari
2b0c43677e fix: add TimePicker component support for Time fieldtype in Grid and Field components 2025-09-03 23:09:18 +05:30
Shariq Ansari
c51ee63008 fix: update subtle variant classes in Autocomplete component for improved styling 2025-09-03 23:08:19 +05:30
Shariq Ansari
0cd527f6ef chore: Norwegian Bokmal translations 2025-09-03 21:49:47 +05:30
Shariq Ansari
fead8c3876 chore: Danish translations 2025-09-03 21:49:45 +05:30
Shariq Ansari
1e6270df44 chore: Esperanto translations 2025-09-03 21:49:43 +05:30
Shariq Ansari
7c9e9b954d chore: Croatian translations 2025-09-03 21:49:41 +05:30
Shariq Ansari
6b2a1e7ff1 chore: Thai translations 2025-09-03 21:49:39 +05:30
Shariq Ansari
4aa506985f chore: Persian translations 2025-09-03 21:49:37 +05:30
Shariq Ansari
496659af0a chore: Vietnamese translations 2025-09-03 21:49:35 +05:30
Shariq Ansari
9669b6d54c chore: Chinese Simplified translations 2025-09-03 21:49:33 +05:30
Shariq Ansari
d406f7345c chore: Turkish translations 2025-09-03 21:49:31 +05:30
Shariq Ansari
f838ef56a6 chore: Russian translations 2025-09-03 21:49:29 +05:30
Shariq Ansari
77c5cb40e9 chore: Portuguese translations 2025-09-03 21:49:28 +05:30
Shariq Ansari
0e1a4f0006 chore: Dutch translations 2025-09-03 21:49:25 +05:30
Shariq Ansari
c4b5c56fe4 chore: Hungarian translations 2025-09-03 21:49:24 +05:30
Shariq Ansari
9066ed0688 chore: Czech translations 2025-09-03 21:49:22 +05:30
Shariq Ansari
9a189aa586 chore: Arabic translations 2025-09-03 21:49:20 +05:30
Shariq Ansari
aa28206f57 chore: Spanish translations 2025-09-03 21:49:18 +05:30
Shariq Ansari
628be7cb7c chore: French translations 2025-09-03 21:49:16 +05:30
Shariq Ansari
b74f92c86e chore: German translations 2025-09-03 21:49:14 +05:30
Shariq Ansari
9190974293 chore: Serbian (Latin) translations 2025-09-03 21:49:12 +05:30
Shariq Ansari
ffae4b10b9 chore: Bosnian translations 2025-09-03 21:49:10 +05:30
Shariq Ansari
b64baa2390 chore: Indonesian translations 2025-09-03 21:49:09 +05:30
Shariq Ansari
87c4d38c77 chore: Portuguese, Brazilian translations 2025-09-03 21:49:07 +05:30
Shariq Ansari
bf32cbd3e8 chore: Swedish translations 2025-09-03 21:49:05 +05:30
Shariq Ansari
6d929d0e7e chore: Serbian (Cyrillic) translations 2025-09-03 21:49:03 +05:30
Shariq Ansari
df31d8820c chore: Polish translations 2025-09-03 21:49:01 +05:30
Shariq Ansari
4bd5e5a9cc chore: Italian translations 2025-09-03 21:48:59 +05:30
Shariq Ansari
f38775bbe5 fix: update EmailMultiSelect styles for better responsiveness and layout 2025-09-03 19:11:21 +05:30
Shariq Ansari
7a6caf2389 fix: update fetchEvent function to handle oldMode parameter and improve event duplication logic 2025-09-03 18:59:25 +05:30
Shariq Ansari
bbb2f8757e fix: remove Popover component and update type declarations 2025-09-03 18:42:21 +05:30
Shariq Ansari
1effb6bc58 fix: remove unused watch import from Tasks.vue 2025-09-03 18:38:12 +05:30
Shariq Ansari
20318d0d13 refactor: replace MultiSelectEmailInput and MultiSelectUserInput with EmailMultiSelect component
- Removed MultiSelectEmailInput.vue and MultiSelectUserInput.vue components.
- Introduced EmailMultiSelect.vue component for handling email selection.
- Updated EmailEditor.vue, AddExistingUserModal.vue, InviteUserPage.vue, and Users.vue to use EmailMultiSelect.
- Adjusted props and validation logic in the new EmailMultiSelect component.
- Removed unused Dropdown.vue component.
2025-09-03 18:30:49 +05:30
Shariq Ansari
610a5cd40b fix: update component imports and improve Dropdown properties in AudioPlayer and ViewBreadcrumbs 2025-09-03 17:02:09 +05:30
Shariq Ansari
6c30596dd1 fix: handle empty state for events 2025-09-03 16:27:04 +05:30
Shariq Ansari
5eddfbe9b3 fix: remove theme management from theme store and update UserDropdown to use useTheme composable from frappe-ui 2025-09-03 15:29:30 +05:30
Shariq Ansari
ed1c448fd7 fix: remove email templates routes from the router configuration 2025-09-03 13:37:44 +05:30
Aftab Shaikh
32993af090 fix: address creation not returning to organization modal 2025-09-03 13:08:26 +05:30
Shariq Ansari
346643bc6d fix: add ShortcutTooltip component and integrate it for keyboard shortcuts in CalendarEventPanel and Calendar pages 2025-09-03 13:05:54 +05:30
Shariq Ansari
f2ce3165dd fix: created keyboard shortcut composable to handle shortcut implementations 2025-09-03 13:05:54 +05:30
Shariq Ansari
11d1b3a67a fix: update duplicate event title logic 2025-09-03 13:05:54 +05:30
Shariq Ansari
74f6f65210 fix: enhance button tooltips and refactor button structure in CalendarEventPanel 2025-09-03 13:05:54 +05:30
Shariq Ansari
557dc1f94c fix: add keyboard shortcuts for event creation and management in Calendar components 2025-09-03 13:05:54 +05:30
Shariq Ansari
1e99192448 fix: refactor event handling and validation logic in CalendarEventPanel and EventModal components 2025-09-03 13:05:54 +05:30
Shariq Ansari
8031964d3d fix: added attendee, color, link in event modal and fixed existing bug 2025-09-03 13:05:54 +05:30
Shariq Ansari
52d99ebf20 fix: refactor Attendee component for improved search functionality and option selection 2025-09-03 13:04:37 +05:30
Shariq Ansari
da7ee0926f fix: refactor EventArea to improve event rendering and participant handling 2025-09-03 13:04:37 +05:30
Shariq Ansari
ae5a1ceae5 fix: use TimePicker from frappeui 2025-09-03 13:04:37 +05:30
Shariq Ansari
2503dea30f fix: update date formatting in DatePicker components and remove unused formatter imports 2025-09-03 13:04:37 +05:30
Shariq Ansari
92b79c2195 fix: reload event after editing event 2025-09-03 13:04:37 +05:30
Shariq Ansari
c69a468e35 fix: synchronize event updates and expose updateEvent method in CalendarEventPanel 2025-09-03 13:04:37 +05:30
Shariq Ansari
4b4b188827 fix: enhance TimePicker to support HH:MM:SS input and truncate seconds 2025-09-03 13:04:37 +05:30
Shariq Ansari
223cbf4020 fix: handle syncing of event 2025-09-03 13:04:37 +05:30
Shariq Ansari
0d1a4effdb refactor: enhance CalendarEventPanel layout with transition effects 2025-09-03 13:04:37 +05:30
Shariq Ansari
369d9fcd63 fix: update DateMonthYearPicker formatter to use currentMonthYear 2025-09-03 13:04:37 +05:30
Shariq Ansari
8af4a4ecf2 refactor: remove DateMonthYearPicker component and use it from frappe-ui 2025-09-03 13:04:37 +05:30
Shariq Ansari
b6a15ab96e fix: add 'Today' button to quickly reset calendar date 2025-09-03 13:04:37 +05:30
Shariq Ansari
0386df262e feat: refactor TimePicker and CalendarEventPanel for improved time selection and validation 2025-09-03 13:04:37 +05:30
Shariq Ansari
032b0d3723 fix: adjust pageLength for event resource 2025-09-03 13:04:37 +05:30
Shariq Ansari
4cb8789786 feat: integrate DateMonthYearPicker into Calendar for improved date selection 2025-09-03 13:04:36 +05:30
Shariq Ansari
957aa4e2e2 feat: add DateMonthYearPicker component for enhanced date selection 2025-09-03 13:04:36 +05:30
Shariq Ansari
65f11d7c8f feat: enhance attendee display with toggle for showing all participants 2025-09-03 13:04:36 +05:30
Shariq Ansari
2950e8c993 fix: enhance attendee list styling with max height and overflow 2025-09-03 13:04:36 +05:30
Shariq Ansari
9976b9617f feat: add Attendee component and integrate into CalendarEventPanel
- Introduced Attendee component for managing event participants.
- Updated CalendarEventPanel to include Attendee component for adding participants.
- Enhanced event creation and updating logic to handle event participants.
- Updated Calendar.vue to ensure participant contacts are created if missing.
2025-09-03 13:04:36 +05:30
Shariq Ansari
eada826503 fix: make timepicker working with custom options 2025-09-03 13:04:36 +05:30
Shariq Ansari
bff1b6156f feat: duplicate event from lead/deal events tab 2025-09-03 13:04:36 +05:30
Shariq Ansari
9cd6b142d7 fix: delete event from lead/deal 2025-09-03 13:04:36 +05:30
Shariq Ansari
ea644c22f1 feat: added events tab in lead/deal page
create & update events from lead/deal page
link events with lead or deal
2025-09-03 13:04:36 +05:30
Shariq Ansari
b6e8d83c3b fix: enhance event submission logic to show details after successful insert and update 2025-09-03 13:04:36 +05:30
Shariq Ansari
68ac2b80ff feat: keep event in sync and show discard changes option if dirty 2025-09-03 13:04:36 +05:30
Shariq Ansari
3f4601efa0 refactor: update event title logic and improve saveEvent function structure 2025-09-03 13:04:36 +05:30
Shariq Ansari
6ec2c1e805 fix: use single click instead of double click to open new event panel 2025-09-03 13:04:36 +05:30
Shariq Ansari
202ba3c856 chore: rearrange code 2025-09-03 13:04:36 +05:30
Shariq Ansari
aa7d9affdb feat: enhance event creation and updating logic in Calendar component 2025-09-03 13:04:36 +05:30
Shariq Ansari
10e3adfd18 refactor: remove CalendarEventDetails component and update CalendarEventPanel for improved event handling 2025-09-03 13:04:36 +05:30
Shariq Ansari
682e445288 refactor: remove redundant logic in updateEvent function in Calendar component 2025-09-03 13:04:36 +05:30
Shariq Ansari
38b838ec97 feat: add duplicate functionality to CalendarEventDetails and CalendarEventPanel components 2025-09-03 13:04:36 +05:30
Shariq Ansari
1cc972ea8b feat: improve event description display in CalendarEventDetails component 2025-09-03 13:04:36 +05:30
Shariq Ansari
f9f1c2a437 refactor: streamline event handling and improve onSuccess callbacks in Calendar 2025-09-03 13:04:36 +05:30
Shariq Ansari
1c432d8610 feat: add CalendarEventDetails component and integrate it into Calendar for event management 2025-09-03 13:04:36 +05:30
Shariq Ansari
55c61cbd80 feat: enhance CalendarEventPanel and TimePicker with improved date and time selection UI 2025-09-03 13:04:36 +05:30
Shariq Ansari
27c54461de feat: refactor color selection in CalendarEventPanel to use a computed colors array 2025-09-03 13:04:36 +05:30
Shariq Ansari
2162ed588a fix: reset active event before showing event panel in Calendar 2025-09-03 13:04:36 +05:30
Shariq Ansari
593cf4ab5a feat: enhance CalendarEventPanel with color selection and improve event handling in Calendar 2025-09-03 13:04:36 +05:30
Shariq Ansari
b73bca354a fix: update event handling in CalendarEventPanel and adjust button action in Calendar 2025-09-03 13:04:36 +05:30
Shariq Ansari
60b5665981 feat: add CalendarEventPanel and TimePicker components for event management in Calendar 2025-09-03 13:04:36 +05:30
Shariq Ansari
e109b59a55 refactor: update component declarations and improve Calendar daily header styling 2025-09-03 13:04:36 +05:30
Shariq Ansari
09c48d76ce fix: comment out TimePicker and ColorPicker components in CalendarModal 2025-09-03 13:04:13 +05:30
Shariq Ansari
f73fbcafdd fix: validate time and date before setting 2025-09-03 13:04:13 +05:30
Shariq Ansari
02da09633d fix: converted snake_case to camelCase 2025-09-03 13:04:13 +05:30
Shariq Ansari
32d8dcf80a fix: reload events on create, update & delete 2025-09-03 13:04:13 +05:30
Shariq Ansari
d520586e87 fix: added color picker 2025-09-03 13:04:13 +05:30
Shariq Ansari
28217968e1 fix: added eventType in modal 2025-09-03 13:04:13 +05:30
Shariq Ansari
21bd24f614 fix: made title required 2025-09-03 13:04:12 +05:30
Shariq Ansari
8f8235e9d9 fix: edit and add event modal with title, description, timepicker & datepicker 2025-09-03 13:02:11 +05:30
Shariq Ansari
85b4f63bc7 fix: get from to time in 24 hour format 2025-09-03 13:02:11 +05:30
Shariq Ansari
743ffc0cf2 fix: show new/edit event modal 2025-09-03 13:02:11 +05:30
Shariq Ansari
7ea8c60e5d fix: confirm before deleting event 2025-09-03 13:02:11 +05:30
Shariq Ansari
19e699ea54 fix: calculate from & to date and create/update with more details 2025-09-03 13:02:11 +05:30
Shariq Ansari
fa105079d7 fix: get more event details and cache request 2025-09-03 13:02:11 +05:30
Shariq Ansari
8d79956f3c fix: added daily header title slot to fix margin 2025-09-03 13:02:11 +05:30
Shariq Ansari
bfba0258ba fix: only get logged in users events 2025-09-03 13:02:11 +05:30
Shariq Ansari
15e8bf06fd fix: removed static events 2025-09-03 13:02:11 +05:30
Shariq Ansari
0f0becd096 feat: Calendar View 2025-09-03 13:02:11 +05:30
Shariq Ansari
9040d50c3c
Merge pull request #1192 from patleYashpal/fix-popup-deletion 2025-09-03 12:46:36 +05:30
Shariq Ansari
899481d752
chore: indentation fix 2025-09-03 12:42:19 +05:30
Shariq Ansari
8a1ebeb52d
chore: indentation fix 2025-09-03 12:42:08 +05:30
Shariq Ansari
4f08cb95e8
Merge pull request #1198 from anup-dh/anup-dh-patch-1 2025-09-03 12:09:52 +05:30
Shariq Ansari
736c956ec2
Merge pull request #1206 from pratikb64/feat/assignment-rule 2025-09-03 12:08:36 +05:30
Shariq Ansari
eb787b2d6f
Merge pull request #1125 from naaa760/bulk-delete-leads 2025-09-03 12:08:21 +05:30
Shariq Ansari
8d0d234ac1
chore: removed comment 2025-09-03 12:06:21 +05:30
Shariq Ansari
7fda0db51b
chore: removed comment 2025-09-03 12:06:08 +05:30
Shariq Ansari
a1d4853d1c
Merge pull request #1202 from frappe/l10n_develop3 2025-09-03 11:55:08 +05:30
Shariq Ansari
6c41a4d0a0
Merge pull request #1204 from frappe/pot_develop_2025-08-31 2025-09-03 11:55:01 +05:30
Shariq Ansari
cedafc82dc
Discard changes to yarn.lock 2025-09-03 11:47:04 +05:30
Shariq Ansari
443070886e
fix: delete yarn.lock 2025-09-03 11:46:06 +05:30
Pratik Badhe
3d6111627b fix: handle notification deletion 2025-09-03 05:24:45 +00:00
Shariq Ansari
33a7f50fed chore: Norwegian Bokmal translations 2025-09-02 21:52:21 +05:30
Shariq Ansari
6ca2f83c05 chore: Danish translations 2025-09-02 21:52:20 +05:30
Shariq Ansari
c4f76cea6c chore: Hungarian translations 2025-09-02 21:52:18 +05:30
Pratik Badhe
26d49d7ae0 Merge remote-tracking branch 'upstream/develop' into bulk-delete-leads 2025-09-02 05:26:14 +00:00
Shariq Ansari
22369825e8 chore: Norwegian Bokmal translations 2025-09-01 21:50:25 +05:30
Shariq Ansari
27b286c6de chore: Hungarian translations 2025-09-01 21:50:14 +05:30
Pratik Badhe
0c5684905f feat: add assignment rule 2025-09-01 06:22:56 +00:00
Shariq Ansari
3fb888561b chore: Norwegian Bokmal translations 2025-08-31 21:39:41 +05:30
frappe-pr-bot
f2f0a4ba2b chore: update POT file 2025-08-31 09:35:10 +00:00
Shariq Ansari
50bf6be95f chore: Norwegian Bokmal translations 2025-08-29 20:11:45 +05:30
anup-dh
486a06baf0
Added support for outgoing whatsapp messages from other apps
made changes so that:
1. whatsapp message is created (app: frappe_whatsapp) even if no phone number match is found in CRM Deal & CRM Lead.
2. Also assigns reference doctype and name for Outgoing whatsappe messages which are not sent via CRM app.
3. added a "if doctype and" check to make sure notify_agent does not fail whatsapp message creation if whatsapp message is being created as per step 1.
2025-08-29 17:37:18 +05:45
Shariq Ansari
214b91a64b
Merge pull request #1196 from frappe/l10n_develop3
chore: sync translations from crowdin
2025-08-29 13:13:58 +05:30
Shariq Ansari
84aacb0717 chore: Norwegian Bokmal translations 2025-08-28 19:17:13 +05:30
Shariq Ansari
4c1a4f956e chore: Danish translations 2025-08-28 19:17:11 +05:30
Shariq Ansari
9f1d75817e chore: Swedish translations 2025-08-28 19:17:05 +05:30
Shariq Ansari
3b61c0ba8f
Merge pull request #1191 from frappe/l10n_develop3 2025-08-28 12:07:06 +05:30
Shariq Ansari
1706c05f93 chore: Norwegian Bokmal translations 2025-08-27 19:17:35 +05:30
Shariq Ansari
f3bd3de81c chore: Danish translations 2025-08-27 19:17:33 +05:30
Shariq Ansari
91187f4db0 chore: Croatian translations 2025-08-27 19:17:32 +05:30
Shariq Ansari
0d5ad337f6 chore: Thai translations 2025-08-27 19:17:30 +05:30
Shariq Ansari
d3fa9cf98a chore: Persian translations 2025-08-27 19:17:28 +05:30
Shariq Ansari
2251d46711 chore: Turkish translations 2025-08-27 19:17:24 +05:30
Shariq Ansari
962418c37d chore: Spanish translations 2025-08-27 19:17:15 +05:30
Shariq Ansari
1d633eabdc chore: German translations 2025-08-27 19:17:12 +05:30
Shariq Ansari
5a0e68b0e8 chore: Bosnian translations 2025-08-27 19:17:09 +05:30
Shariq Ansari
76f9843251 chore: Portuguese, Brazilian translations 2025-08-27 19:17:05 +05:30
Shariq Ansari
1a6a1854fc chore: Polish translations 2025-08-27 19:17:00 +05:30
Yashpal Patle
b55bf0918f fixed popup deletion 2025-08-26 18:18:39 +00:00
Shariq Ansari
5f9fb5a68f chore: Esperanto translations 2025-08-26 19:20:55 +05:30
Shariq Ansari
469177182a chore: Croatian translations 2025-08-26 19:20:54 +05:30
Shariq Ansari
a72e580e1f chore: Thai translations 2025-08-26 19:20:52 +05:30
Shariq Ansari
9c88ba879d chore: Persian translations 2025-08-26 19:20:50 +05:30
Shariq Ansari
9d45dea8b3 chore: Vietnamese translations 2025-08-26 19:20:48 +05:30
Shariq Ansari
f1732780cc chore: Chinese Simplified translations 2025-08-26 19:20:47 +05:30
Shariq Ansari
ae70eee7e0 chore: Turkish translations 2025-08-26 19:20:45 +05:30
Shariq Ansari
03277e240e chore: Russian translations 2025-08-26 19:20:43 +05:30
Shariq Ansari
510f7301fd chore: Portuguese translations 2025-08-26 19:20:41 +05:30
Shariq Ansari
32f42a4009 chore: Dutch translations 2025-08-26 19:20:40 +05:30
Shariq Ansari
267016cff9 chore: Hungarian translations 2025-08-26 19:20:38 +05:30
Shariq Ansari
fc02800a35 chore: Czech translations 2025-08-26 19:20:37 +05:30
Shariq Ansari
b33f09c76b chore: Arabic translations 2025-08-26 19:20:35 +05:30
Shariq Ansari
2d68d1d29a chore: Spanish translations 2025-08-26 19:20:33 +05:30
Shariq Ansari
ec14798d9c chore: French translations 2025-08-26 19:20:31 +05:30
Shariq Ansari
a2e438ceea chore: German translations 2025-08-26 19:20:30 +05:30
Shariq Ansari
e606e56ce7 chore: Serbian (Latin) translations 2025-08-26 19:20:28 +05:30
Shariq Ansari
a8a75f6e97 chore: Bosnian translations 2025-08-26 19:20:27 +05:30
Shariq Ansari
35e07b321c chore: Indonesian translations 2025-08-26 19:20:25 +05:30
Shariq Ansari
28ce5762f8 chore: Portuguese, Brazilian translations 2025-08-26 19:20:24 +05:30
Shariq Ansari
97367afed1 chore: Swedish translations 2025-08-26 19:20:22 +05:30
Shariq Ansari
d1b229c459 chore: Serbian (Cyrillic) translations 2025-08-26 19:20:20 +05:30
Shariq Ansari
335f73a0e6 chore: Polish translations 2025-08-26 19:20:19 +05:30
Shariq Ansari
ef38c4a882 chore: Italian translations 2025-08-26 19:20:17 +05:30
Shariq Ansari
132b631a36
Merge pull request #1182 from shariquerik/kanban-public-save 2025-08-26 12:25:53 +05:30
Shariq Ansari
d1ccbc6541
Merge pull request #1185 from pratikb64/fix/deal-read-only-fields 2025-08-26 12:23:59 +05:30
Pratik Badhe
7807c12114 fix: remove "read only" flag for primary email, mobile & phone no 2025-08-26 06:46:54 +00:00
Shariq Ansari
779032be50
Merge pull request #1179 from frappe/l10n_develop3 2025-08-26 12:12:10 +05:30
Shariq Ansari
e78d08a6d4
Merge pull request #1178 from frappe/pot_develop_2025-08-24 2025-08-26 12:11:56 +05:30
Shariq Ansari
4301cd4806 fix: do not allow saving public view kanban changes for non managers 2025-08-26 12:09:26 +05:30
Shariq Ansari
5ab41a5ebb chore: Indonesian translations 2025-08-24 19:03:52 +05:30
Shariq Ansari
e7c09b6f2f chore: Italian translations 2025-08-24 19:03:48 +05:30
frappe-pr-bot
bf63bd54df chore: update POT file 2025-08-24 09:35:30 +00:00
Shariq Ansari
f486a664ca
Merge pull request #1173 from shariquerik/dropdown-item-fix-1 2025-08-21 11:32:45 +05:30
Shariq Ansari
41118eda97 fix: update button state to use document properties 2025-08-21 11:27:24 +05:30
Shariq Ansari
cd96171a4c fix: reorder idx if grid row is reordered 2025-08-21 11:27:12 +05:30
Shariq Ansari
83c1abd707
Merge pull request #1133 from wasim3357/develop 2025-08-20 14:34:13 +05:30
Shariq Ansari
574d3ec70e
Merge pull request #1167 from frappe/l10n_develop3 2025-08-20 13:20:20 +05:30
Shariq Ansari
18c1bb1ad4
Merge pull request #1169 from shariquerik/dropdown-item-fix 2025-08-20 13:14:29 +05:30
Shariq Ansari
8ea94765ce fix: dropdown item is not updating 2025-08-20 12:46:30 +05:30
Shariq Ansari
8d7a5f22fd chore: Esperanto translations 2025-08-19 18:10:04 +05:30
Shariq Ansari
a31ff74999 chore: Croatian translations 2025-08-19 18:10:02 +05:30
Shariq Ansari
c60979f1ab chore: Thai translations 2025-08-19 18:10:00 +05:30
Shariq Ansari
b5739efbbc chore: Persian translations 2025-08-19 18:09:58 +05:30
Shariq Ansari
96e014e6ca chore: Vietnamese translations 2025-08-19 18:09:57 +05:30
Shariq Ansari
d0020b8a90 chore: Chinese Simplified translations 2025-08-19 18:09:55 +05:30
Shariq Ansari
f32b86e7e7 chore: Turkish translations 2025-08-19 18:09:54 +05:30
Shariq Ansari
7cab5d9815 chore: Russian translations 2025-08-19 18:09:52 +05:30
Shariq Ansari
a6a22aa393 chore: Portuguese translations 2025-08-19 18:09:51 +05:30
Shariq Ansari
fd098a0766 chore: Dutch translations 2025-08-19 18:09:49 +05:30
Shariq Ansari
36326526d5 chore: Hungarian translations 2025-08-19 18:09:48 +05:30
Shariq Ansari
f20a903e78 chore: Czech translations 2025-08-19 18:09:47 +05:30
Shariq Ansari
0f0564066d chore: Arabic translations 2025-08-19 18:09:45 +05:30
Shariq Ansari
7b6a4d3b30 chore: Spanish translations 2025-08-19 18:09:44 +05:30
Shariq Ansari
175c450559 chore: French translations 2025-08-19 18:09:42 +05:30
Shariq Ansari
5b79141dd5 chore: German translations 2025-08-19 18:09:41 +05:30
Shariq Ansari
97de6543c2 chore: Serbian (Latin) translations 2025-08-19 18:09:40 +05:30
Shariq Ansari
bc424265e0 chore: Bosnian translations 2025-08-19 18:09:38 +05:30
Shariq Ansari
15d057b63c chore: Indonesian translations 2025-08-19 18:09:37 +05:30
Shariq Ansari
27a121c270 chore: Portuguese, Brazilian translations 2025-08-19 18:09:35 +05:30
Shariq Ansari
d99a4bc3ff chore: Swedish translations 2025-08-19 18:09:34 +05:30
Shariq Ansari
d6251adae3 chore: Serbian (Cyrillic) translations 2025-08-19 18:09:32 +05:30
Shariq Ansari
388d3e9369 chore: Polish translations 2025-08-19 18:09:31 +05:30
Shariq Ansari
f8f0800f97 chore: Italian translations 2025-08-19 18:09:29 +05:30
Shariq Ansari
9b9d87757c
Merge pull request #1164 from shariquerik/fixes-6 2025-08-19 13:37:29 +05:30
Shariq Ansari
247a7c4da6 fix: remove read-only attribute from TwiML SID field 2025-08-19 13:36:38 +05:30
Shariq Ansari
f2d87fa801 fix: add immediate execution to watch for assignees updates 2025-08-19 13:33:48 +05:30
Shariq Ansari
e7534c9b15
Merge pull request #1161 from shariquerik/fixes-5 2025-08-19 12:26:32 +05:30
Shariq Ansari
edd0ec5f68 fix: update Dropdown styling in SLASection component 2025-08-19 12:21:30 +05:30
Shariq Ansari
a76bd2cab2 fix: align action buttons in GridFieldsEditorModal 2025-08-19 11:56:24 +05:30
Shariq Ansari
25d9d562e6 fix: convert to deal modals's convert button 2025-08-19 11:53:28 +05:30
Shariq Ansari
e8c331dfff
Merge pull request #1158 from shariquerik/twilio-fix-1 2025-08-19 11:12:30 +05:30
Shariq Ansari
afa96c330b fix: reject button is rotated 2025-08-19 11:11:50 +05:30
Shariq Ansari
4992cdda74
Merge pull request #1155 from shariquerik/dashboard-cache-fix 2025-08-19 10:47:17 +05:30
Shariq Ansari
c2a1a1b1d2 fix: remove unnecessary cache from dashboardItems resource 2025-08-19 10:46:31 +05:30
Shariq Ansari
89cce5160c
Merge pull request #1152 from shariquerik/fixes-4 2025-08-19 02:18:51 +05:30
Shariq Ansari
5f0bb46bf4 fix: prevent adding a column with undefined value in ColumnSettings 2025-08-19 01:51:57 +05:30
Shariq Ansari
f4551a92c5 refactor: note/task modal 2025-08-19 01:28:57 +05:30
Shariq Ansari
e9c197f46e fix: minor fix 2025-08-19 01:18:34 +05:30
Shariq Ansari
3a756630f3 fix: grid settings/edit-row button alignment 2025-08-19 00:59:24 +05:30
Shariq Ansari
a77bfd2aca refactor: update layout structure for CRM Deal-Data Fields to show products table 2025-08-19 00:58:52 +05:30
Shariq Ansari
1cebc1fed8 fix: ensure reactive access to document title in Lead component 2025-08-19 00:40:34 +05:30
Shariq Ansari
1a90876500 refactor: improve layout and structure of quick filter components 2025-08-19 00:30:30 +05:30
Shariq Ansari
c4065b95b8 patch: create default loast reasons 2025-08-19 00:03:52 +05:30
Shariq Ansari
1a74d6a280
Merge pull request #1130 from frappe/pot_develop_2025-08-10 2025-08-18 22:51:35 +05:30
Shariq Ansari
0523920fd0
Merge branch 'develop' into pot_develop_2025-08-10 2025-08-18 22:50:51 +05:30
Shariq Ansari
a8ae2e551e
Merge pull request #1129 from frappe/l10n_develop3 2025-08-18 22:48:41 +05:30
Shariq Ansari
aa0df7be79
Merge pull request #1142 from frappe/pot_develop_2025-08-17 2025-08-18 22:48:04 +05:30
Shariq Ansari
49ddcda2b6
Merge pull request #1145 from shariquerik/frappeui-update-1 2025-08-18 22:42:03 +05:30
Shariq Ansari
d173d5584a
Merge pull request #1144 from shariquerik/helpdesk-integration 2025-08-18 22:41:21 +05:30
Shariq Ansari
1679a67dc6 build(deps): bump frappeui to 0.1.189 2025-08-18 22:36:31 +05:30
Shariq Ansari
31163a1b2e
Merge branch 'develop' into helpdesk-integration 2025-08-18 22:20:14 +05:30
Shariq Ansari
5f32e46759 feat: add customer creation and invitation functionality in Helpdesk CRM 2025-08-18 22:17:58 +05:30
Shariq Ansari
91bdb02867
Merge pull request #1143 from shariquerik/fixes-3 2025-08-18 21:54:33 +05:30
Shariq Ansari
f8aa6cab78 fix: update ordering in deals by territory and salesperson queries 2025-08-18 17:57:10 +05:30
Shariq Ansari
8a62ff38af feat: implement validation and script creation for Helpdesk CRM settings 2025-08-18 17:51:14 +05:30
Shariq Ansari
8350752f56 feat: add Helpdesk icon and settings page 2025-08-18 17:35:19 +05:30
Shariq Ansari
a182bee57f feat: add Helpdesk CRM Settings doctype 2025-08-18 17:34:50 +05:30
Shariq Ansari
c5a8df19ae refactor: update tooltip bindings to use translation function and improve dropdown actions 2025-08-18 16:05:07 +05:30
Shariq Ansari
38b6674cc1 refactor: replace NestedPopover with Popover component across multiple files 2025-08-18 15:02:05 +05:30
frappe-pr-bot
73b2c36bbc chore: update POT file 2025-08-17 09:36:55 +00:00
Shariq Ansari
672c5eb733 refactor: Button components across multiple files to use icon/left-icon/right-icon prop 2025-08-16 00:12:12 +05:30
Shariq Ansari
948ce99482 fix: show dashboard to all 2025-08-15 20:27:57 +05:30
Shariq Ansari
3791e2ae70 fix: lead/deal status dropdown is not renderring 2025-08-15 20:24:44 +05:30
Shariq Ansari
68d1172b8f fix: add toast import to CurrencySettings component 2025-08-15 19:58:01 +05:30
Shariq Ansari
c94e61bfce fix: adjust UserDropdown padding in AppSidebar 2025-08-15 19:57:10 +05:30
Shariq Ansari
8b557a5963 fix: doc was not associated with value 2025-08-15 19:56:34 +05:30
Shariq Ansari
d4f99a411c chore: Persian translations 2025-08-12 16:52:02 +05:30
wasim3357
ea2e44a2be
fix(Org Modal): Setting loading false in case of Validation Error
In this case user can add the details and save the form again instead of refreshing and then trying again for fresh.
2025-08-11 17:42:38 +05:30
Shariq Ansari
4884ca0bd6 chore: Persian translations 2025-08-11 16:09:13 +05:30
Shariq Ansari
2a7c9ef9e8 chore: Italian translations 2025-08-11 16:08:56 +05:30
Shariq Ansari
cb3f67f231 chore: Italian translations 2025-08-10 16:08:37 +05:30
frappe-pr-bot
00e3bd12cc chore: update POT file 2025-08-10 09:37:28 +00:00
Shariq Ansari
d539bc075f chore: German translations 2025-08-09 15:35:45 +05:30
Shariq Ansari
818fd6fcdd chore: Italian translations 2025-08-09 15:35:44 +05:30
Shariq Ansari
5dc3a364a4
Merge pull request #1126 from frappe/l10n_develop3 2025-08-08 16:59:37 +05:30
Shariq Ansari
840eb664ce chore: Italian translations 2025-08-08 15:37:59 +05:30
naaa760
51823d1b88 Added input validation
Added document existence checks
Added reference field validation
Changed None to empty strings for reference fields
2025-08-08 14:12:36 +05:30
Shariq Ansari
8e0536ee11
Merge pull request #1121 from pratikb64/fix/create-quotation 2025-08-08 14:05:17 +05:30
Shariq Ansari
5648e5eb77
Merge pull request #1116 from frappe/l10n_develop3 2025-08-08 13:57:55 +05:30
Pratik Badhe
940e8d24c7 fix: create quotation 2025-08-08 08:22:33 +00:00
Shariq Ansari
4813a861a6 chore: Serbian (Latin) translations 2025-08-07 15:37:29 +05:30
Shariq Ansari
afb30a256e chore: Serbian (Cyrillic) translations 2025-08-07 15:37:28 +05:30
Shariq Ansari
499888b4a7
Merge pull request #1112 from frappe/l10n_develop3 2025-08-06 12:42:22 +05:30
Shariq Ansari
227359da62 chore: Croatian translations 2025-08-05 14:47:19 +05:30
Shariq Ansari
022b14c830 chore: Bosnian translations 2025-08-05 14:47:01 +05:30
Shariq Ansari
58f970c6b1 chore: Swedish translations 2025-08-05 14:46:57 +05:30
Shariq Ansari
d83ce2f276 chore: Esperanto translations 2025-08-04 14:05:04 +05:30
Shariq Ansari
0c8f36bc36 chore: Croatian translations 2025-08-04 14:05:03 +05:30
Shariq Ansari
1286ecfc8f chore: Thai translations 2025-08-04 14:05:01 +05:30
Shariq Ansari
24ebe94730 chore: Persian translations 2025-08-04 14:05:00 +05:30
Shariq Ansari
90b0e0d7b6 chore: Vietnamese translations 2025-08-04 14:04:58 +05:30
Shariq Ansari
ebd115c129 chore: Chinese Simplified translations 2025-08-04 14:04:57 +05:30
Shariq Ansari
5f544416ef chore: Turkish translations 2025-08-04 14:04:56 +05:30
Shariq Ansari
5a7a7a7257 chore: Russian translations 2025-08-04 14:04:54 +05:30
Shariq Ansari
8e8bf4ed5e chore: Portuguese translations 2025-08-04 14:04:52 +05:30
Shariq Ansari
7244f69c7f chore: Dutch translations 2025-08-04 14:04:51 +05:30
Shariq Ansari
d5b992d736 chore: Hungarian translations 2025-08-04 14:04:49 +05:30
Shariq Ansari
38cb777458 chore: Czech translations 2025-08-04 14:04:48 +05:30
Shariq Ansari
1a569d45de chore: Arabic translations 2025-08-04 14:04:46 +05:30
Shariq Ansari
93a710f8c5 chore: Spanish translations 2025-08-04 14:04:45 +05:30
Shariq Ansari
5414bbb190 chore: French translations 2025-08-04 14:04:43 +05:30
Shariq Ansari
c70aac4f31 chore: German translations 2025-08-04 14:04:42 +05:30
Shariq Ansari
9471966bee chore: Serbian (Latin) translations 2025-08-04 14:04:40 +05:30
Shariq Ansari
055a39cc0d chore: Bosnian translations 2025-08-04 14:04:39 +05:30
Shariq Ansari
4f075d4b23 chore: Indonesian translations 2025-08-04 14:04:37 +05:30
Shariq Ansari
526c43c655 chore: Portuguese, Brazilian translations 2025-08-04 14:04:36 +05:30
Shariq Ansari
084b446ae1 chore: Swedish translations 2025-08-04 14:04:35 +05:30
Shariq Ansari
de9c04ff18 chore: Serbian (Cyrillic) translations 2025-08-04 14:04:33 +05:30
Shariq Ansari
2fb09126c0 chore: Polish translations 2025-08-04 14:04:32 +05:30
Shariq Ansari
8944295474 chore: Italian translations 2025-08-04 14:04:31 +05:30
Shariq Ansari
85348188d2
Merge pull request #1108 from frappe/pot_develop_2025-08-03 2025-08-04 12:33:25 +05:30
Shariq Ansari
a68b405c61
Merge pull request #1104 from shariquerik/assign-to-refactor 2025-08-04 12:33:02 +05:30
Shariq Ansari
2a18a556bf refactor: update Activities component for improved z-index layering 2025-08-04 12:30:48 +05:30
frappe-pr-bot
fa14d4ad15 chore: update POT file 2025-08-03 09:38:21 +00:00
Shariq Ansari
44deb5878c
Merge pull request #1103 from frappe/l10n_develop3
chore: sync translations from crowdin
2025-08-03 13:59:40 +05:30
Shariq Ansari
2601c1d059 chore: Persian translations 2025-08-03 13:43:25 +05:30
Shariq Ansari
7e51d96379 chore: Indonesian translations 2025-08-03 13:43:12 +05:30
Shariq Ansari
4c1eaf507d refactor: update z-index values for activity components for better layering 2025-08-01 17:56:07 +05:30
Shariq Ansari
76149b0c79 refactor: update Activities component to scroll on success 2025-08-01 17:44:35 +05:30
Shariq Ansari
564d2b1ac5 refactor: update AssignTo component to use correct doctype for CRM Deal 2025-08-01 17:37:02 +05:30
Shariq Ansari
faef5cb866 refactor: update AssignTo component to use docname prop and streamline assignment logic 2025-08-01 16:56:06 +05:30
Shariq Ansari
3ed2c4812a refactor: do not update modified field 2025-08-01 16:52:51 +05:30
Shariq Ansari
4c53dd6ea3 chore: Persian translations 2025-08-01 13:09:10 +05:30
Shariq Ansari
5010cccc71 refactor: replace json.loads with frappe.parse_json for assignees in remove_assignments function 2025-08-01 13:03:14 +05:30
Shariq Ansari
aeb3f150c5 refactor: assignment modal for bulk assign 2025-08-01 12:54:16 +05:30
Shariq Ansari
995f356419 refactor: Assign to as popover instead of dialog 2025-08-01 12:50:22 +05:30
Shariq Ansari
c64dcb43b4
Merge pull request #1095 from frappe/l10n_develop3 2025-07-31 16:38:53 +05:30
Shariq Ansari
7dafba9fc3
Merge pull request #1100 from shariquerik/bug-fix 2025-07-31 16:32:03 +05:30
Shariq Ansari
abc501825a fix: update delete button click handler in Deal component 2025-07-31 16:30:25 +05:30
Shariq Ansari
3ad2a56efb
Merge pull request #1094 from C-L-STARK/patch-1 2025-07-31 16:28:38 +05:30
Shariq Ansari
c53e486bf0 fix: update init scripts for consistent Redis configuration and branch usage 2025-07-31 16:02:00 +05:30
Shariq Ansari
819a669922 refactor: simplify AssignmentModal component by streamlining dialog options and enhancing assignee management 2025-07-31 13:32:31 +05:30
Shariq Ansari
d852fe1e9f
Merge pull request #1096 from shariquerik/refactor-2 2025-07-30 18:13:41 +05:30
Shariq Ansari
d431d5b4b1 refactor: remove unused get_lead function and related imports from api.py 2025-07-30 17:55:47 +05:30
Shariq Ansari
ba99d14f68 refactor: clean up unused variables and streamline tab management in Deal, Lead, and MobileLead components 2025-07-30 17:50:16 +05:30
Shariq Ansari
7e42599b49 refactor: update mobile lead/deal components 2025-07-30 17:31:22 +05:30
Shariq Ansari
c38c190d42 fix: update docname references to use dealId and leadId in Deal and Lead components 2025-07-30 16:43:40 +05:30
Shariq Ansari
6e8d869afb fix: update whatsappMessages auto property based on whatsappEnabled state 2025-07-30 16:24:19 +05:30
Shariq Ansari
63d6062673 refactor: update button icon rendering in AttachmentArea component 2025-07-30 16:12:21 +05:30
Shariq Ansari
af830b8782 refactor: use doc in activity & child components 2025-07-30 16:11:49 +05:30
Shariq Ansari
0605cf5fd0 refactor: updateField & deleteDeal/Lead 2025-07-30 15:16:13 +05:30
Shariq Ansari
4acb4dd3a7 fix: remount on route change with param and hash change 2025-07-30 15:13:38 +05:30
Shariq Ansari
72c31b9f21 chore: Croatian translations 2025-07-30 13:11:43 +05:30
Shariq Ansari
f2b1c24d19 chore: Serbian (Latin) translations 2025-07-30 13:11:28 +05:30
Shariq Ansari
f86bb0ada2 chore: Bosnian translations 2025-07-30 13:11:27 +05:30
Shariq Ansari
ec4866f39a chore: Indonesian translations 2025-07-30 13:11:26 +05:30
Shariq Ansari
7961b614f1 chore: Swedish translations 2025-07-30 13:11:24 +05:30
Shariq Ansari
901d84d070 chore: Serbian (Cyrillic) translations 2025-07-30 13:11:22 +05:30
Shariq Ansari
0144bc109a fix: use document.doc instead of lead.data/deal.data 2025-07-30 12:56:27 +05:30
Shariq Ansari
ca5d82f5be fix: handle errors when creating document resources 2025-07-30 12:50:35 +05:30
C.L.STARK
b6a6152a49
Update init.sh
Fixed: docker setup error in crm-frappe container.
2025-07-30 05:11:40 +08:00
Shariq Ansari
5e19a83f8a
Merge pull request #1090 from frappe/l10n_develop3 2025-07-29 16:10:10 +05:30
Shariq Ansari
cd30e9d533
Merge pull request #1091 from shariquerik/modified-error 2025-07-29 14:13:59 +05:30
Shariq Ansari
ea0011771b fix: reload doc after sending email 2025-07-29 14:08:11 +05:30
Shariq Ansari
94b0077b2a chore: Esperanto translations 2025-07-29 12:32:06 +05:30
Shariq Ansari
ea815d0147 chore: Croatian translations 2025-07-29 12:32:04 +05:30
Shariq Ansari
0ae048d396 chore: Thai translations 2025-07-29 12:32:03 +05:30
Shariq Ansari
972558396e chore: Persian translations 2025-07-29 12:32:02 +05:30
Shariq Ansari
c218241e80 chore: Vietnamese translations 2025-07-29 12:32:00 +05:30
Shariq Ansari
e616e69aa4 chore: Chinese Simplified translations 2025-07-29 12:31:59 +05:30
Shariq Ansari
2627471b23 chore: Turkish translations 2025-07-29 12:31:58 +05:30
Shariq Ansari
a2c42ee5a7 chore: Russian translations 2025-07-29 12:31:56 +05:30
Shariq Ansari
8d89caeba7 chore: Portuguese translations 2025-07-29 12:31:55 +05:30
Shariq Ansari
b0393b532c chore: Dutch translations 2025-07-29 12:31:54 +05:30
Shariq Ansari
2ea875f2cb chore: Hungarian translations 2025-07-29 12:31:53 +05:30
Shariq Ansari
d1d2900847 chore: Czech translations 2025-07-29 12:31:51 +05:30
Shariq Ansari
98d851d76d chore: Arabic translations 2025-07-29 12:31:50 +05:30
Shariq Ansari
30fdc1db4c chore: Spanish translations 2025-07-29 12:31:49 +05:30
Shariq Ansari
12df15154d chore: French translations 2025-07-29 12:31:47 +05:30
Shariq Ansari
0bf6c01f91 chore: German translations 2025-07-29 12:31:46 +05:30
Shariq Ansari
497263f367 chore: Serbian (Latin) translations 2025-07-29 12:31:44 +05:30
Shariq Ansari
cd458d6d22 chore: Bosnian translations 2025-07-29 12:31:43 +05:30
Shariq Ansari
a2b55166ed chore: Indonesian translations 2025-07-29 12:31:41 +05:30
Shariq Ansari
570d31b6d1 chore: Portuguese, Brazilian translations 2025-07-29 12:31:40 +05:30
Shariq Ansari
2483dd6828 chore: Swedish translations 2025-07-29 12:31:38 +05:30
Shariq Ansari
1adef98c57 chore: Serbian (Cyrillic) translations 2025-07-29 12:31:37 +05:30
Shariq Ansari
dfd3c8f2bf chore: Polish translations 2025-07-29 12:31:36 +05:30
Shariq Ansari
015c592978 chore: Italian translations 2025-07-29 12:31:34 +05:30
Shariq Ansari
8984cea367
Merge pull request #1087 from shariquerik/bug 2025-07-29 11:34:25 +05:30
Shariq Ansari
c3cf63dfb7 fix: format NumberChart component for better readability 2025-07-29 11:32:49 +05:30
Shariq Ansari
c532b61ef6 fix: fields should not be mandatory in single doctype 2025-07-29 11:32:37 +05:30
Shariq Ansari
491fbb7801
Merge pull request #1084 from shariquerik/exchange_rate_fix-1 2025-07-28 18:05:34 +05:30
Shariq Ansari
4c7a40d8bc fix: fixed labels 2025-07-28 18:01:36 +05:30
Shariq Ansari
c866d1e836
Merge pull request #1082 from shariquerik/exchange_rate_fix 2025-07-28 17:57:59 +05:30
Shariq Ansari
c72160b2a4 fix: remove unused Link component import from GeneralSettings.vue 2025-07-28 17:52:58 +05:30
Shariq Ansari
3143f14e0b
Merge branch 'develop' into exchange_rate_fix 2025-07-28 17:51:03 +05:30
Shariq Ansari
e06a365029 fix: update description for mandatory fields in deal value forecasting 2025-07-28 17:49:52 +05:30
Shariq Ansari
36e79e49da fix: added exchange rate provider & currency together in settings modal 2025-07-28 17:48:26 +05:30
Shariq Ansari
5bee7022b2 fix: removed currency exchange rate settings and moved it to crm settings 2025-07-28 17:47:20 +05:30
Shariq Ansari
e578513eaf fix: Improve error handling in document update process 2025-07-28 16:43:46 +05:30
Meer Uzair
8de2a69a99 fix: Correct field name from 'agent' to 'user' in CRM Telephony Agent creation
- The `CRM Telephony Agent` doctype requires the field `user` (not `agent`) as per its schema (`reqd: 1` + `autoname: field:user`).
- This fixes the `ValidationError: User is required` by using the correct field name when creating a new agent.
2025-07-28 16:23:52 +05:30
frappe-pr-bot
890299c2f3 chore: update POT file 2025-07-28 16:23:52 +05:30
Shariq Ansari
2a6f1c402b chore: Serbian (Latin) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
25d2b1889f chore: Serbian (Cyrillic) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
ee843fae26 chore: Serbian (Latin) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
fea35205b7 chore: Bosnian translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
ee28543180 chore: Indonesian translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
4d7a66f1d8 chore: Swedish translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
3b9ff8d58f chore: Serbian (Cyrillic) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
b46d5a4e5e fix: update exchange rate fetching logic to include service provider context and improve error handling 2025-07-28 16:20:08 +05:30
Shariq Ansari
23a823f2bb feat: enhance document handling with error triggering and settings helpers via app 2025-07-28 16:19:49 +05:30
Shariq Ansari
56ef00536a
Merge pull request #1077 from MeerUzairWasHere/patch-1 2025-07-28 15:56:07 +05:30
Shariq Ansari
0081006a64
Merge pull request #1076 from frappe/pot_develop_2025-07-27 2025-07-28 15:53:51 +05:30
Shariq Ansari
1c98d81c2c
Merge pull request #1074 from frappe/l10n_develop3 2025-07-28 15:53:33 +05:30
Shariq Ansari
7e9d9a5fed fix: moved get_exchange_rate api to exchange rate settings 2025-07-28 13:00:31 +05:30
Shariq Ansari
45826e0a88 fix: added currency exchange rate settings 2025-07-28 12:59:34 +05:30
Shariq Ansari
8815433962 chore: Serbian (Latin) translations 2025-07-28 12:31:04 +05:30
Shariq Ansari
e4261820d4 chore: Serbian (Cyrillic) translations 2025-07-28 12:31:03 +05:30
Shariq Ansari
5bfcaf4809 fix: get latest exchange rate 2025-07-28 12:12:34 +05:30
Meer Uzair
7f4f6ff651
fix: Correct field name from 'agent' to 'user' in CRM Telephony Agent creation
- The `CRM Telephony Agent` doctype requires the field `user` (not `agent`) as per its schema (`reqd: 1` + `autoname: field:user`).
- This fixes the `ValidationError: User is required` by using the correct field name when creating a new agent.
2025-07-27 22:58:43 +05:30
frappe-pr-bot
7d9b7e92fb chore: update POT file 2025-07-27 09:38:13 +00:00
Shariq Ansari
63963414c1 chore: Serbian (Latin) translations 2025-07-26 12:04:14 +05:30
Shariq Ansari
f0ee245f75 chore: Bosnian translations 2025-07-26 12:04:13 +05:30
Shariq Ansari
a4710a8b85 chore: Indonesian translations 2025-07-26 12:04:12 +05:30
Shariq Ansari
201ad61e6d chore: Swedish translations 2025-07-26 12:04:10 +05:30
Shariq Ansari
bbb9eefc9a chore: Serbian (Cyrillic) translations 2025-07-26 12:04:08 +05:30
Shariq Ansari
d53b503805
Merge pull request #1070 from shariquerik/log-call-fix 2025-07-24 13:14:47 +05:30
Shariq Ansari
b1d90952c4 refactor: MultiActionButton dropdown 2025-07-24 13:10:58 +05:30
Shariq Ansari
742bd6e213 fix: update label for call log actions to "Log a Call" 2025-07-24 12:47:14 +05:30
Shariq Ansari
e0abb53d4c
Merge pull request #1068 from shariquerik/primary-mobile-no-fix 2025-07-24 12:32:26 +05:30
Shariq Ansari
8e08e6f415 fix: make mobile no primary on create of contact 2025-07-24 12:29:54 +05:30
Shariq Ansari
e883ea1346
Merge pull request #1066 from shariquerik/compatibility 2025-07-24 11:56:24 +05:30
Shariq Ansari
0327b37d2f docs: add compatibility section with supported Frappe and ERPNext versions in README 2025-07-24 11:54:01 +05:30
Shariq Ansari
699feead15
Merge pull request #1064 from frappe/l10n_develop3 2025-07-24 11:28:50 +05:30
Shariq Ansari
a08fabaed9 chore: Bosnian translations 2025-07-24 11:26:11 +05:30
Shariq Ansari
61e276df37 chore: Croatian translations 2025-07-24 11:26:09 +05:30
Shariq Ansari
076762e2f3
Merge pull request #1059 from shariquerik/refactor-1 2025-07-23 13:35:55 +05:30
Shariq Ansari
9761989ea4 fix: improve exchange rate fetching with retry logic and default return value 2025-07-23 13:33:41 +05:30
Shariq Ansari
5ae7698704
Merge pull request #1057 from frappe/l10n_develop3 2025-07-23 13:23:30 +05:30
Shariq Ansari
c84ac29332 refactor: rename update_close_date to update_closed_date and adjust related logic 2025-07-23 13:21:02 +05:30
Shariq Ansari
cf8a1ce8a3 refactor: cache assignees 2025-07-23 13:12:12 +05:30
Shariq Ansari
1507701981 chore: Serbian (Latin) translations 2025-07-23 10:58:29 +05:30
Shariq Ansari
b4796efed1 chore: Bosnian translations 2025-07-23 10:58:28 +05:30
Shariq Ansari
273f7d7f07 chore: Croatian translations 2025-07-23 10:58:26 +05:30
Shariq Ansari
8474630f4c chore: Thai translations 2025-07-23 10:58:25 +05:30
Shariq Ansari
907ec5fa3c chore: Persian translations 2025-07-23 10:58:24 +05:30
Shariq Ansari
43001bee9f chore: Indonesian translations 2025-07-23 10:58:22 +05:30
Shariq Ansari
5479a6b885 chore: Portuguese, Brazilian translations 2025-07-23 10:58:21 +05:30
Shariq Ansari
594f7922f9 chore: Vietnamese translations 2025-07-23 10:58:19 +05:30
Shariq Ansari
7aa9f768ea chore: Chinese Simplified translations 2025-07-23 10:58:18 +05:30
Shariq Ansari
b796a30e1f chore: Turkish translations 2025-07-23 10:58:16 +05:30
Shariq Ansari
5508eb013a chore: Swedish translations 2025-07-23 10:58:14 +05:30
Shariq Ansari
a3c7af7c8c chore: Serbian (Cyrillic) translations 2025-07-23 10:58:13 +05:30
Shariq Ansari
d689bf21d2 chore: Russian translations 2025-07-23 10:58:12 +05:30
Shariq Ansari
05ea067361 chore: Portuguese translations 2025-07-23 10:58:10 +05:30
Shariq Ansari
2af8710a6f chore: Polish translations 2025-07-23 10:58:08 +05:30
Shariq Ansari
6abc9f7f9f chore: Dutch translations 2025-07-23 10:58:07 +05:30
Shariq Ansari
7de21b7015 chore: Italian translations 2025-07-23 10:58:06 +05:30
Shariq Ansari
f9bae5d8ff chore: Hungarian translations 2025-07-23 10:58:04 +05:30
Shariq Ansari
29322577aa chore: German translations 2025-07-23 10:58:03 +05:30
Shariq Ansari
a4f96c7c5b chore: Czech translations 2025-07-23 10:58:01 +05:30
Shariq Ansari
b3fe85b8a0 chore: Arabic translations 2025-07-23 10:58:00 +05:30
Shariq Ansari
efc4213363 chore: Spanish translations 2025-07-23 10:57:59 +05:30
Shariq Ansari
b983648f35 chore: French translations 2025-07-23 10:57:57 +05:30
Shariq Ansari
e62eac91d1
Merge pull request #1054 from frappe/l10n_develop3 2025-07-22 10:53:04 +05:30
Shariq Ansari
e6f1bd50db chore: Esperanto translations 2025-07-22 10:52:15 +05:30
Shariq Ansari
c5458fb29c chore: Serbian (Latin) translations 2025-07-22 10:52:14 +05:30
Shariq Ansari
8e3daf482d chore: Bosnian translations 2025-07-22 10:52:13 +05:30
Shariq Ansari
03edd1a011 chore: Croatian translations 2025-07-22 10:52:11 +05:30
Shariq Ansari
246ed83a3d chore: Thai translations 2025-07-22 10:52:10 +05:30
Shariq Ansari
53440476c4 chore: Persian translations 2025-07-22 10:52:09 +05:30
Shariq Ansari
14f3fd3cd7 chore: Indonesian translations 2025-07-22 10:52:08 +05:30
Shariq Ansari
6c9d56808b chore: Portuguese, Brazilian translations 2025-07-22 10:52:06 +05:30
Shariq Ansari
08b3107d92 chore: Vietnamese translations 2025-07-22 10:52:05 +05:30
Shariq Ansari
dc8fa27ea1 chore: Chinese Simplified translations 2025-07-22 10:52:04 +05:30
Shariq Ansari
445d7050b7 chore: Turkish translations 2025-07-22 10:52:02 +05:30
Shariq Ansari
bfe0df4df3 chore: Swedish translations 2025-07-22 10:52:01 +05:30
Shariq Ansari
71bcca71cd chore: Serbian (Cyrillic) translations 2025-07-22 10:52:00 +05:30
Shariq Ansari
53864ac12c chore: Russian translations 2025-07-22 10:51:59 +05:30
Shariq Ansari
f438500a57 chore: Portuguese translations 2025-07-22 10:51:57 +05:30
Shariq Ansari
cef8aacf2f chore: Polish translations 2025-07-22 10:51:56 +05:30
Shariq Ansari
1ab414c4bf chore: Dutch translations 2025-07-22 10:51:54 +05:30
Shariq Ansari
fdffd7bf3a chore: Italian translations 2025-07-22 10:51:53 +05:30
Shariq Ansari
6fc52aba6d chore: Hungarian translations 2025-07-22 10:51:52 +05:30
Shariq Ansari
dfd4cc068a chore: German translations 2025-07-22 10:51:51 +05:30
Shariq Ansari
5ae21c8580 chore: Czech translations 2025-07-22 10:51:49 +05:30
Shariq Ansari
178a934fa3 chore: Arabic translations 2025-07-22 10:51:48 +05:30
Shariq Ansari
1ce508a4b1 chore: Spanish translations 2025-07-22 10:51:47 +05:30
Shariq Ansari
d0678ec7c2 chore: French translations 2025-07-22 10:51:46 +05:30
Shariq Ansari
8036ffeafc
Merge pull request #1053 from shariquerik/updated-crowdin-yml 2025-07-22 10:39:58 +05:30
Shariq Ansari
a5a37c6181 fix: updated crowdin.yml 2025-07-22 10:38:28 +05:30
Shariq Ansari
945cdc0ae3
Merge pull request #1050 from frappe/pot_develop_2025-07-20 2025-07-22 10:22:17 +05:30
frappe-pr-bot
8de2e89b68 chore: update POT file 2025-07-20 09:37:27 +00:00
Shariq Ansari
0f49470bf6
Merge pull request #1041 from shariquerik/dashboard-fix-1 2025-07-15 15:11:01 +05:30
Shariq Ansari
7826565ce7 fix: reset to default 2025-07-15 15:09:48 +05:30
Shariq Ansari
713571469b fix: only show edit button to system manager 2025-07-15 14:55:10 +05:30
Shariq Ansari
2f34fdd409 fix: set status correctly 2025-07-15 14:52:19 +05:30
Shariq Ansari
6fefa16ac3
Merge pull request #1033 from shariquerik/editable-dashboard 2025-07-15 13:28:50 +05:30
Shariq Ansari
b344f412c9 fix: forecasting chart is breaking if no data 2025-07-15 13:21:46 +05:30
Shariq Ansari
1e245e7719
Merge pull request #1038 from shariquerik/to-status-type-1 2025-07-15 12:39:07 +05:30
Shariq Ansari
28facd66c4 fix: to status type is not accessible 2025-07-15 12:37:44 +05:30
Shariq Ansari
c8f01f08ed fix: updated default manager dashboard 2025-07-15 12:33:54 +05:30
Shariq Ansari
a89525f77e
Merge pull request #1036 from shariquerik/to-status-type 2025-07-15 12:18:21 +05:30
Shariq Ansari
38a2fa87c3 fix: to status type is not accessible 2025-07-15 12:17:00 +05:30
Shariq Ansari
46e6ed2e6f fix: minor fix 2025-07-15 12:11:08 +05:30
Shariq Ansari
266952c404 fix: decreased rowHeight to 42 2025-07-15 12:07:19 +05:30
Shariq Ansari
b77e59589a build(deps): bump frappeui to 0.1.171 2025-07-15 12:06:41 +05:30
Shariq Ansari
1a5ae397dc
Merge pull request #1034 from shariquerik/exchange_rate_org 2025-07-15 11:51:21 +05:30
Shariq Ansari
7c4718ad02 fix: removed currency exchange and added exchange rate field 2025-07-15 11:41:02 +05:30
Shariq Ansari
d79341b6d9 fix: renamed blank card to spacer 2025-07-14 20:29:46 +05:30
Shariq Ansari
84738ba00c fix: allow force reset manager dashboard 2025-07-14 20:22:43 +05:30
Shariq Ansari
3b0a8d8e4b fix: updated default manager dashboard 2025-07-14 20:16:06 +05:30
Shariq Ansari
2584cca128 fix: minor fixes 2025-07-14 20:12:52 +05:30
Shariq Ansari
05b8cea206 fix: create default manager dashboard if not exists 2025-07-14 19:14:18 +05:30
Shariq Ansari
6e3d23a8e1 fix: add default Manager Dashboard on install 2025-07-14 18:28:51 +05:30
Shariq Ansari
2a38d0fb5f fix: disabel save button if not dirty, reset to old items if cancel 2025-07-14 18:15:04 +05:30
Shariq Ansari
97724c776b build(deps): bump frappeui to 0.1.170 2025-07-14 17:58:01 +05:30
Shariq Ansari
9b072058cc build(deps): bump frappeui to 0.1.170 2025-07-14 17:57:43 +05:30
Shariq Ansari
d2e65feaa6 fix: save edited dashboard 2025-07-14 17:41:00 +05:30
Shariq Ansari
37c2d3a2b0 feat: allow adding existing charts 2025-07-14 17:40:13 +05:30
Shariq Ansari
0909423fe9 fix: rename card to chart 2025-07-14 17:38:17 +05:30
Shariq Ansari
139bcb101c fix: added remove chart button 2025-07-14 17:37:36 +05:30
Shariq Ansari
0f06715d0c fix: use tooltip from data 2025-07-14 17:36:50 +05:30
Shariq Ansari
4a783fcba8 fix: show edit button to manager 2025-07-14 14:22:53 +05:30
Shariq Ansari
fd38f0ac98 fix: load dashboard layout from CRM Dashboard doctype and render 2025-07-14 14:16:10 +05:30
Shariq Ansari
ed2208fe75 fix: added crm dashboard doctype to store dashboard layout 2025-07-14 14:13:50 +05:30
Shariq Ansari
4320142132 fix: update status type patch was not working 2025-07-14 12:27:14 +05:30
Shariq Ansari
160649bf97 fix: use GridLayout from frappe-ui to display dashboard 2025-07-14 12:18:14 +05:30
Shariq Ansari
e7a2efd14a
Merge pull request #1029 from frappe/pot_develop_2025-07-13 2025-07-13 15:21:23 +05:30
Shariq Ansari
81614418d4
Merge pull request #1030 from shariquerik/deal-status-type 2025-07-13 15:15:19 +05:30
frappe-pr-bot
bb08f3d377 chore: update POT file 2025-07-13 09:38:02 +00:00
Shariq Ansari
5232da6ec3 fix: updated funnel query to get status change count 2025-07-13 13:07:31 +05:30
Shariq Ansari
e59547da30 fix: store deal status type in status log 2025-07-13 12:43:03 +05:30
Shariq Ansari
de85ccfc51 fix: added deals by ongoing & won stages bar 2025-07-13 12:35:16 +05:30
Shariq Ansari
f82019e510 fix: used closed_date instead of closed_on and set closed_date if status type is Won 2025-07-13 12:11:50 +05:30
Shariq Ansari
7fc26a5202 fix: used expected closed date & deal value for forecasting 2025-07-13 12:09:35 +05:30
Shariq Ansari
dcb1e47564 fix: added closed_date removed closed_on & added expected_deal_value & expected_closure_date field 2025-07-13 12:02:34 +05:30
Shariq Ansari
61259f3d2e fix: avg time to close a deal number card 2025-07-13 11:52:04 +05:30
Shariq Ansari
2dd2608c09 fix: added two more number cards 2025-07-13 11:35:14 +05:30
Shariq Ansari
81dc4e1138 fix: get ongoing deals and won deals based on closed_on date 2025-07-11 17:09:21 +05:30
Shariq Ansari
cb1f9f760c fix: use status.type instead of status in all query 2025-07-11 17:00:59 +05:30
Shariq Ansari
51530b7608 fix: show lost reason modal if status of type Lost is set 2025-07-11 16:35:47 +05:30
Shariq Ansari
4e6d4a1d77 patch: added patch to update deal status type 2025-07-11 16:16:40 +05:30
Shariq Ansari
efc5dd93e9 fix: added type in default deak status while installing 2025-07-11 16:07:52 +05:30
Shariq Ansari
210a9d8d06 fix: added type of deal status field 2025-07-11 16:05:57 +05:30
Shariq Ansari
9bd855ee2e
Merge pull request #1024 from shariquerik/save-lead 2025-07-10 18:33:18 +05:30
Shariq Ansari
970c215f40 fix: cannot save data fields in lead page 2025-07-10 18:27:01 +05:30
Shariq Ansari
7d157046ac
Merge pull request #1022 from shariquerik/dashboard-fix 2025-07-10 17:39:14 +05:30
Shariq Ansari
1ae7018f79 fix: apply user filter if sales user 2025-07-10 17:38:36 +05:30
Shariq Ansari
6802567291
Merge pull request #979 from nextchamp-saqib/dashboard 2025-07-10 17:21:58 +05:30
Shariq Ansari
cbc127e947 fix: better description 2025-07-10 17:20:25 +05:30
Shariq Ansari
d91d4765b5 fix: make autocomplete non clickable if disabled 2025-07-10 17:06:58 +05:30
Shariq Ansari
328959cc39 style: better spacing 2025-07-10 16:59:09 +05:30
Shariq Ansari
36320f61ab fix: remove mandatory from currency field in crm settings 2025-07-10 16:36:02 +05:30
Shariq Ansari
faeacb9a7d chore: minor fix 2025-07-10 16:27:00 +05:30
Shariq Ansari
5dcd416007 fix: added forecasting & currency setting in general settings 2025-07-10 16:15:27 +05:30
Shariq Ansari
33e4072430 fix: added disabled & placement prop in Link component 2025-07-10 16:14:32 +05:30
Shariq Ansari
c7fbd6f8f1 refactor: general settings 2025-07-10 15:27:55 +05:30
Shariq Ansari
a5d3694386 refactor: get exchange rate api 2025-07-10 15:23:59 +05:30
Shariq Ansari
b3075416e2 fix: show dashboard to manager only 2025-07-10 13:59:30 +05:30
Shariq Ansari
743d97d690
Merge pull request #1018 from zaqouttahir/fix-bug-edit-bulk 2025-07-10 12:29:07 +05:30
Shariq Ansari
2cb09dde4b fix: use exchange rate in deal to calculate the deal value 2025-07-09 18:22:52 +05:30
Shariq Ansari
d7ba5a5f62 fix: store current day exchange rate when currency is updated in deal & organization 2025-07-09 18:22:13 +05:30
Shariq Ansari
a00bba35f8 fix: remove crm currency exchange doctype 2025-07-09 18:20:49 +05:30
Shariq Ansari
71db65d21c fix: make currency read only once set 2025-07-09 17:49:44 +05:30
Shariq Ansari
37d820a67c chore: updated/added number card tooltip 2025-07-09 17:47:56 +05:30
Shariq Ansari
4f02f0a4d7 fix: convert to system currency and show deal value 2025-07-09 15:53:06 +05:30
Shariq Ansari
f4b81b3761 fix: added date field in currency exchange doctype 2025-07-09 14:48:49 +05:30
Shariq Ansari
0be737914a fix: store currency exchange in deal & organization 2025-07-09 14:38:56 +05:30
Shariq Ansari
1b0d966db0 fix: created currency exchange doctype 2025-07-09 14:37:16 +05:30
Shariq Ansari
27f87883f7 fix: fieldtype of value is not changing based on selected field's fieldtype 2025-07-08 19:16:58 +05:30
Shariq Ansari
f747e076ab fix: only show sales user filter to manager 2025-07-08 12:58:46 +05:30
Shariq Ansari
4b12918ba5 fix: added filters and translated titles 2025-07-08 12:29:20 +05:30
zaqouttahir
c104b1b8b4 fix: set fieldname to handle edit value modal 2025-07-07 13:13:04 +03:00
Shariq Ansari
9d4106cd81 chore: cleanup 2025-07-07 14:33:11 +05:30
Shariq Ansari
eddf8c9295 fix: show avg time number card based on closed_on date 2025-07-07 14:30:29 +05:30
Shariq Ansari
6b3e42a44e
Merge pull request #1015 from frappe/pot_develop_2025-07-06 2025-07-07 14:16:21 +05:30
Shariq Ansari
9b1d4832b6
Merge branch 'develop' into dashboard 2025-07-06 15:25:40 +05:30
Shariq Ansari
4d2f054e40 fix: added avg time to close number card 2025-07-06 15:24:43 +05:30
Shariq Ansari
6450b69ae7 fix: capture closed on datetime when deal marked as Won 2025-07-06 15:24:23 +05:30
frappe-pr-bot
223187c7ea chore: update POT file 2025-07-06 09:36:54 +00:00
Shariq Ansari
3b34f73cb3 fix: added more charts for dashboard 2025-07-06 14:44:12 +05:30
Shariq Ansari
40c5c92230
Merge pull request #1013 from shariquerik/primary-mobile-no 2025-07-05 14:33:10 +05:30
Shariq Ansari
6760798f18 fix: useDocument in organization page 2025-07-05 14:17:26 +05:30
Shariq Ansari
42ea1ad16e fix: useDocument in contact page 2025-07-05 14:09:37 +05:30
Shariq Ansari
96200aebe6 fix: update primary mobile_no & email in deal if contact is updated 2025-07-05 13:21:21 +05:30
Shariq Ansari
bcfe4b6a49 fix: made mobile_no, email & phone readonly since it captures primary contacts data 2025-07-05 12:30:40 +05:30
Shariq Ansari
cb92e5e68d
Merge branch 'develop' into dashboard 2025-07-03 14:28:28 +05:30
Shariq Ansari
1fa6b5bb51 fix: added first cut queries for some charts and number cards 2025-07-03 14:27:21 +05:30
Shariq Ansari
cafc4fb22f
Merge pull request #1009 from shariquerik/products-table-fix 2025-07-02 17:50:58 +05:30
Shariq Ansari
39eb5600d9 fix: grid field is not getting set 2025-07-02 17:45:46 +05:30
Shariq Ansari
0b97462dc9
Merge pull request #1007 from shariquerik/lost-reason-fix 2025-07-02 17:26:54 +05:30
Shariq Ansari
cab80edf60 fix: check reason.reason not reason 2025-07-02 17:15:59 +05:30
Shariq Ansari
6f3b58d1a5
Merge pull request #1005 from shariquerik/frappeui-update 2025-07-02 17:09:33 +05:30
Shariq Ansari
fc89c7b93c build(deps): bump frappeui to 0.1.166 2025-07-02 17:03:28 +05:30
Shariq Ansari
4a57c4eb84
Merge pull request #1003 from shariquerik/default-lost-reason 2025-07-02 16:59:23 +05:30
Shariq Ansari
96cbdea820 fix: add default lost reason on install 2025-07-02 16:57:26 +05:30
Shariq Ansari
a3a54aef94
Merge pull request #984 from shariquerik/lost-reasons 2025-07-02 16:03:30 +05:30
Shariq Ansari
c96e5ff6c5 fix: update default probability from deal status 2025-07-02 15:58:12 +05:30
Shariq Ansari
144470877d fix: allow creating lost reason from lost reason modal field 2025-07-02 15:42:25 +05:30
Shariq Ansari
391844512a fix: intercept data tab's before save and side panel's before field change to show lost reason modal 2025-07-02 15:28:21 +05:30
Shariq Ansari
d89c304b13 fix: show lost reason modal if status changed to lost 2025-07-02 14:11:11 +05:30
Shariq Ansari
881126c7f1 refactor: statusOptions code 2025-07-02 14:10:14 +05:30
Shariq Ansari
5bbec00803 fix: renamed other_lost_reason to lost_notes 2025-07-02 13:59:34 +05:30
Shariq Ansari
7730e46cfc fix: removed unused triggerOnChange 2025-07-02 13:58:51 +05:30
Shariq Ansari
97b2253e9d fix: made lost notes as text and non mandatory if lost reason is not Other 2025-07-02 11:10:53 +05:30
Shariq Ansari
1afb2a783b
Merge branch 'develop' into lost-reasons 2025-07-01 19:34:35 +05:30
Shariq Ansari
0fdbfa3ad4
Merge pull request #998 from shariquerik/prettydate-fix 2025-07-01 19:23:24 +05:30
Shariq Ansari
a7dc5e05b3 fix: show absolute day count not in decimels 2025-07-01 19:20:04 +05:30
Shariq Ansari
92d7280728 chore: resolved conflict 2025-07-01 16:53:40 +05:30
Shariq Ansari
5d01b88a1e feat: created lost reason doctype 2025-07-01 16:51:01 +05:30
Shariq Ansari
2b47e3f4c9
Merge pull request #994 from shariquerik/forecasting-fix-1 2025-07-01 16:49:48 +05:30
Shariq Ansari
485360f291 fix: show forcasted sales section in sidepanel if forecasting is enabled 2025-07-01 16:37:04 +05:30
Shariq Ansari
17fdbb05ce fix: add mandatory fields in convert to deal modal if not added 2025-07-01 16:05:08 +05:30
Shariq Ansari
adc22efcb1 fix: show error message on convert to deal modal 2025-07-01 16:04:40 +05:30
Shariq Ansari
4c70b1a06b fix: mandatory error 2025-07-01 15:06:02 +05:30
Shariq Ansari
4f58aa110a fix: made deal value mandatory if forecasting is enabled 2025-07-01 15:05:37 +05:30
Shariq Ansari
4d3fe722e8 fix: added default probability to Lost status 2025-07-01 13:16:40 +05:30
Shariq Ansari
6320e580ae refactor: moved convert to deal modal into separate component 2025-07-01 13:07:36 +05:30
Shariq Ansari
611f4cde70 fix: prettyDate is not accurate 2025-07-01 12:58:57 +05:30
Shariq Ansari
6d3268a61e fix: add default probabilities in deal status 2025-07-01 12:07:44 +05:30
Shariq Ansari
bf0a1ecebd
Merge pull request #991 from shariquerik/edit-call-log 2025-07-01 11:52:18 +05:30
Shariq Ansari
693c086930 fix: show edit call log button in call log details modal 2025-07-01 11:49:42 +05:30
Shariq Ansari
7c307a9134
Merge pull request #988 from shariquerik/call-log-on-before-create 2025-06-30 20:06:01 +05:30
Shariq Ansari
aae7e0e36c fix: pass reference doc to call log modal to get reference doc in on before create 2025-06-30 20:00:05 +05:30
Shariq Ansari
2014a3d6de
Merge pull request #985 from shariquerik/on-before-create 2025-06-30 19:41:35 +05:30
Shariq Ansari
2e5c1bc3b5 fix: added on before create hook in all modals 2025-06-30 19:16:24 +05:30
Shariq Ansari
ac13b7a3bd fix: added on before create hook in call log modal 2025-06-30 18:43:19 +05:30
Shariq Ansari
6b7bdf5afb feat: added on before create hook in document.js 2025-06-30 18:42:20 +05:30
Shariq Ansari
3eba628a8b
Merge branch 'develop' into dashboard 2025-06-30 12:57:13 +05:30
Shariq Ansari
9949478b36 fix: added breadcrumb and made header sticky 2025-06-30 12:56:47 +05:30
Shariq Ansari
ff657ec34c
Merge pull request #980 from frappe/pot_develop_2025-06-29 2025-06-30 12:42:55 +05:30
Shariq Ansari
da4698d431
Merge branch 'develop' into pot_develop_2025-06-29 2025-06-30 12:42:12 +05:30
Shariq Ansari
20d47ae323
Merge pull request #978 from shariquerik/dynamic-app-alias 2025-06-30 11:59:29 +05:30
Shariq Ansari
f4f799f636 feat: create dynamic alias to use components from frontend vue apps 2025-06-30 11:49:31 +05:30
frappe-pr-bot
cc411f036d chore: update POT file 2025-06-29 09:36:46 +00:00
Saqib Ansari
62d5c2a91f feat: initialize dashboard boilerplate 2025-06-28 10:57:58 +05:30
Shariq Ansari
8350c5ee36
Merge pull request #971 from shariquerik/email-template-settings 2025-06-26 17:54:04 +05:30
Shariq Ansari
65435cf2b5 fix: delete icon issue & more cleanup 2025-06-26 17:49:10 +05:30
Shariq Ansari
af4c64e633 build(deps): bump frappeui to 0.1.162 2025-06-26 17:11:37 +05:30
Shariq Ansari
41b913debe fix: cannot change role of user with Admin access 2025-06-26 17:07:26 +05:30
Shariq Ansari
a3b9368953 fix: give Sales Manager & Sales User role if System Manager access is given 2025-06-26 16:54:33 +05:30
Shariq Ansari
28ece820ed style: better spacing 2025-06-26 16:46:23 +05:30
Shariq Ansari
cca420b1a0 style: minor changes 2025-06-25 21:19:49 +05:30
Shariq Ansari
05803c79b4 fix: make email template row clickable 2025-06-25 17:02:41 +05:30
Shariq Ansari
5932ccafec fix: add new email template from email selector modal 2025-06-25 15:23:30 +05:30
Shariq Ansari
7cee017e20 fix: removed email template page and related components 2025-06-25 15:15:34 +05:30
Shariq Ansari
b15a8d9c8a fix: added email template icon 2025-06-25 15:03:32 +05:30
Shariq Ansari
7e6d5c3e54 fix: Duplicate email template 2025-06-24 19:46:27 +05:30
Shariq Ansari
dd3d297dab fix: Edit email template 2025-06-24 19:37:13 +05:30
Shariq Ansari
e4f728d809 fix: Create email template 2025-06-24 18:52:36 +05:30
Shariq Ansari
cd7bab9184 feat: Create/Edit & List page for email template & implemented delete from list 2025-06-24 18:52:00 +05:30
Shariq Ansari
ec6b1558b1 fix: only show search if users are more than 10 2025-06-24 15:36:54 +05:30
Shariq Ansari
1c3ee8b557 refactor: added email templates in settings modal 2025-06-24 15:35:48 +05:30
Shariq Ansari
1db7f69f89
Merge pull request #960 from shariquerik/users-fix-1 2025-06-24 12:12:20 +05:30
Shariq Ansari
3c1ce1fe27 fix: make header sticky 2025-06-24 12:11:39 +05:30
Shariq Ansari
2d05b6a282
Merge pull request #957 from shariquerik/users-fix 2025-06-24 11:45:46 +05:30
Shariq Ansari
b5ed9692df fix: user with System Manager role is admin 2025-06-24 11:44:13 +05:30
Shariq Ansari
9a326d791b
Merge pull request #953 from shariquerik/users 2025-06-23 21:00:32 +05:30
Shariq Ansari
7fbd240d97 fix: added search in users page 2025-06-23 20:55:35 +05:30
Shariq Ansari
58d4691354
Merge pull request #846 from pratikb64/delete-from-record-view 2025-06-23 13:50:19 +05:30
Pratik
594295b7c8 style: switch button position 2025-06-23 08:13:06 +00:00
Shariq Ansari
2c45673f54
Merge pull request #845 from shariquerik/agents 2025-06-23 13:22:56 +05:30
Shariq Ansari
7827afe606
Merge pull request #946 from kalungia/dev 2025-06-23 13:21:59 +05:30
Shariq Ansari
bc7498e02b
Merge branch 'develop' into dev 2025-06-23 13:20:01 +05:30
Shariq Ansari
e5dd85aefb
Merge pull request #948 from frappe/pot_develop_2025-06-22 2025-06-23 13:19:09 +05:30
Shariq Ansari
cf1fce3dc0 fix: renaming fix and removed CRM User code 2025-06-23 13:18:16 +05:30
Pratik
480cc07cd9 refactor: remove unnecessary functions & components 2025-06-23 07:35:15 +00:00
Shariq Ansari
84d4327e80 fix: use change password modal in place 2025-06-23 13:04:12 +05:30
Pratik
34102ef6ef refactor: change labels & function names 2025-06-23 05:15:40 +00:00
Pratik
ca985a0b76 Merge branch 'develop' of https://github.com/pratikb64/crm into delete-from-record-view 2025-06-23 04:26:50 +00:00
frappe-pr-bot
4b4a154261 chore: update POT file 2025-06-22 09:36:52 +00:00
Shariq Ansari
e957327877 fix: use crmUsers in all link field 2025-06-20 18:57:09 +05:30
Shariq Ansari
2fdea90ad4 fix: loading state in Users page 2025-06-20 18:55:42 +05:30
Shariq Ansari
ad1aee9c9e fix: add user is actually add role 2025-06-20 18:55:14 +05:30
Shariq Ansari
bd7451e86f fix: if role is set to sales user then remove modules and set FCRM 2025-06-20 17:56:14 +05:30
Shariq Ansari
0230360145 fix: use text-ink-gray-8 instead of 9 2025-06-20 17:47:07 +05:30
Shariq Ansari
eee1190f10 fix: dark mode fixes for email account setting 2025-06-20 17:42:10 +05:30
Shariq Ansari
96c8aae01e chore: fixed warning 2025-06-20 17:25:14 +05:30
Shariq Ansari
0ad65be961 fix: layout change 2025-06-20 17:20:12 +05:30
Shariq Ansari
0f8d484e28 fix: existingEmail is a list 2025-06-20 17:18:36 +05:30
Shariq Ansari
364c369199 fix: updated users page to update user directly and removed unnecessary code 2025-06-20 16:46:41 +05:30
Shariq Ansari
901bcb8460 fix: removed CRM User doctype and moved api's to user.py 2025-06-20 16:45:48 +05:30
Shariq Ansari
d06ac91052 fix: use multi select user input and show already exist error if user with email exist or invited 2025-06-20 16:43:48 +05:30
Shariq Ansari
f818a4c1d6 fix: created multi select user input 2025-06-20 16:42:45 +05:30
Shariq Ansari
85191e10c8 fix: use crmUsers in comment box 2025-06-20 16:41:08 +05:30
Shariq Ansari
001a3231e1 fix: get users and crm users 2025-06-20 15:50:47 +05:30
Shariq Ansari
d951dff5a9 fix: added tooltip with shortcut 2025-06-19 15:08:53 +05:30
Shariq Ansari
dc82f837aa fix: moved change password modal in global modals 2025-06-19 15:07:57 +05:30
Shariq Ansari
7f1db0b444 fix: change password validation messsage 2025-06-19 12:56:50 +05:30
Abraham Kalungi
a317950567 fix: prevent TypeError when concatenating first and last name in WhatsApp messages
The last name on CRM Leads can be empty. In cases where it is, an error occurs: can only concatenate str (not "NoneType") to str. This prevents retrieving messages until a last name is added.
2025-06-18 15:31:00 +02:00
Shariq Ansari
4c7269e357 fix: updated components.d.ts 2025-06-17 23:46:53 +05:30
Shariq Ansari
15fd763de8 fix: use validateIsImageFile from utils 2025-06-17 23:46:28 +05:30
Shariq Ansari
0c314674fc
Merge branch 'develop' into agents 2025-06-17 23:42:42 +05:30
Shariq Ansari
efd03141f0
chore: resolved conflict 2025-06-17 23:42:20 +05:30
Shariq Ansari
675bcb549d
Discard changes to frontend/src/components/Settings/ProfileImageEditor.vue 2025-06-17 23:40:58 +05:30
Shariq Ansari
56425254a9 fix: updated components.d.ts 2025-06-17 23:35:51 +05:30
Shariq Ansari
22856351fd fix: only show users to manager 2025-06-17 23:35:51 +05:30
Shariq Ansari
dd1229309f fix: updated components.d.ts 2025-06-17 23:35:51 +05:30
Shariq Ansari
fad7c5985c refactor: profile page 2025-06-17 23:34:45 +05:30
Shariq Ansari
3234102e55 fix: capture onboarding step event of setting up password 2025-06-17 23:34:45 +05:30
Shariq Ansari
fb2f105520 feat: update password modal 2025-06-17 23:34:45 +05:30
Shariq Ansari
03abe0b5cd fix: create crm user on accepting invite 2025-06-17 23:34:44 +05:30
Shariq Ansari
6873c6db4e feat: add existing users 2025-06-17 23:34:44 +05:30
Shariq Ansari
08bab927a2 fix: filter out existing emails 2025-06-17 23:34:44 +05:30
Shariq Ansari
d244567b30 fix: open invite user page from users add new agent dropdown option 2025-06-17 23:34:44 +05:30
Shariq Ansari
2b1b21d2e2 fix: show role options based on logged in user's role 2025-06-17 23:34:44 +05:30
Shariq Ansari
12213de478 fix: show role options based on logged in user's role 2025-06-17 23:34:44 +05:30
Shariq Ansari
2a2c832e0b fix: updated roles in Invite user page 2025-06-17 23:34:44 +05:30
Shariq Ansari
b534aae70b fix: renamed component names from Agent to User 2025-06-17 23:34:44 +05:30
Shariq Ansari
6d3e4406ae fix: renamed & added role with filter 2025-06-17 23:34:44 +05:30
Shariq Ansari
463d60b650 fix: renamed Agent to User in Settings 2025-06-17 23:34:44 +05:30
Shariq Ansari
e9812495e9 fix: update agent fields based on user 2025-06-17 23:34:44 +05:30
Shariq Ansari
0a836c78bb fix: activate/deactivate agent 2025-06-17 23:34:44 +05:30
Shariq Ansari
5c7f835e4c fix: renamed invite members to invite agent 2025-06-17 23:34:44 +05:30
Shariq Ansari
346849631e fix: show and allow changing role from agents settings page 2025-06-17 23:34:44 +05:30
Shariq Ansari
bf166bdaad fix: added get user role in users store 2025-06-17 23:34:44 +05:30
Shariq Ansari
123f183f68 fix: removed x button from settings modal 2025-06-17 23:34:44 +05:30
Shariq Ansari
bea1505c63 fix: added Agents page in settings 2025-06-17 23:34:44 +05:30
Shariq Ansari
fac5ed5579 feat: added crm agent doctype 2025-06-17 23:34:44 +05:30
Shariq Ansari
96fefbd8a3
Merge pull request #937 from shariquerik/forecasting-fix 2025-06-16 17:48:08 +05:30
Shariq Ansari
548018997e build(deps): bump frappeui to 0.1.156 2025-06-16 17:42:28 +05:30
Shariq Ansari
9f3477e1cd fix: updated button with icon 2025-06-16 17:42:01 +05:30
Shariq Ansari
baa03246e6 fix: fixed breaking button with icon and open email box 2025-06-16 17:06:06 +05:30
Shariq Ansari
a17b1cd0e2 fix: added mandatory field error toast 2025-06-16 16:32:15 +05:30
Shariq Ansari
49d82870c4 fix: hide calendar icon in side panel date fields 2025-06-16 13:41:11 +05:30
Shariq Ansari
6a72a4467a build(deps): bump frappeui to 0.1.154 2025-06-16 13:40:53 +05:30
Shariq Ansari
efbed6e0b6 fix: renamed Expected Closure Date to Close Date 2025-06-16 11:44:08 +05:30
Shariq Ansari
5270670b65
Merge pull request #933 from frappe/pot_develop_2025-06-15 2025-06-16 11:39:47 +05:30
frappe-pr-bot
b82f4ca02b chore: update POT file 2025-06-15 09:36:57 +00:00
Pratik Badhe
0f451c7e3a
Merge pull request #930 from pratikb64/git-error 2025-06-13 18:26:31 +05:30
Pratik
824dc8dcdd fix: git command error 2025-06-13 12:51:04 +00:00
Shariq Ansari
20405be86c
Merge pull request #920 from shariquerik/forecasting 2025-06-13 15:04:23 +05:30
Shariq Ansari
4edfa951dc fix: asterisk is not visible if label is big enough 2025-06-13 15:02:50 +05:30
Shariq Ansari
029c16d1d0 fix: circular import 2025-06-13 14:46:44 +05:30
Shariq Ansari
b3acff8cba fix: create standard forecasting script on install and added in patch 2025-06-13 14:42:09 +05:30
Shariq Ansari
a98b0e3a00 fix: set close date to now if status is Won 2025-06-13 14:20:50 +05:30
Shariq Ansari
b1cbcbd98d
Merge pull request #923 from shariquerik/lead-org-avatar-fix 2025-06-12 18:09:41 +05:30
Shariq Ansari
e079980598 fix: removed leads organization logo 2025-06-12 18:07:07 +05:30
Shariq Ansari
2e27c0459c fix: set close date reqd as 1 or 0 based on enabled_forecasting 2025-06-11 19:30:08 +05:30
Shariq Ansari
d87a237789 feat: added enable forecasting settings 2025-06-11 19:03:22 +05:30
Shariq Ansari
e9e0aa357b fix: trigger on change on status change 2025-06-11 14:20:11 +05:30
Shariq Ansari
9af300bba8 fix: added throwError global method 2025-06-11 14:19:16 +05:30
Shariq Ansari
7d79cbf5bd fix: update and reset value in triggerOnChange method 2025-06-11 14:18:25 +05:30
Shariq Ansari
fdca27bb81 fix: added probability field in deal status 2025-06-11 13:01:22 +05:30
Shariq Ansari
01f0213693 fix: added deal value field 2025-06-11 13:00:02 +05:30
Shariq Ansari
5d29a49120
Merge pull request #916 from mahsem/develop 2025-06-11 11:13:25 +05:30
mahsem
33e6b80d5a
fix: add context for Integrations 2025-06-09 22:11:22 +02:00
Shariq Ansari
100d931535
Merge pull request #913 from shariquerik/future-date-time-ago 2025-06-09 16:51:02 +05:30
Shariq Ansari
dba6dd1983 fix: future date is not captured in pretty date 2025-06-09 16:49:09 +05:30
Shariq Ansari
dc8898e1da
Merge pull request #907 from frappe/pot_develop_2025-06-08 2025-06-09 14:09:56 +05:30
frappe-pr-bot
ed79bf55eb chore: update POT file 2025-06-08 09:36:10 +00:00
Shariq Ansari
fb644f5fbe
Merge pull request #905 from NagariaHussain/fix/image-upload
refactor: DRY up validate image file
2025-06-08 12:49:03 +05:30
Hussain Nagaria
ab409dfd2c fix: yet another unused import due to merge conflict 2025-06-08 12:37:15 +05:30
Hussain Nagaria
42285dd911 fix: unused import due to merge conflict 2025-06-08 12:36:39 +05:30
Md Hussain Nagaria
db2a6c65b7
Merge branch 'develop' into fix/image-upload 2025-06-08 12:34:30 +05:30
Hussain Nagaria
c6ad10857a refactor: DRY up validate image file
* Also, allows more types of image files
2025-06-08 12:30:51 +05:30
Shariq Ansari
f128a55f97
Merge pull request #902 from shariquerik/onload-onsave 2025-06-06 21:15:42 +05:30
Shariq Ansari
2a817e5861 fix: only load assignee if docname is passed 2025-06-06 21:14:20 +05:30
Shariq Ansari
5e616f1a50 refactor: statusOptions code 2025-06-06 21:09:27 +05:30
Shariq Ansari
c6e9d71e1f fix: allow snake & camel case for on load, on save, convert to deal, on create lead 2025-06-06 21:01:20 +05:30
Shariq Ansari
f58d44bf9c fix: use document.doc in status dropdown 2025-06-06 20:49:58 +05:30
Shariq Ansari
f72ab39c93 fix: onLoad & onSave 2025-06-06 20:35:25 +05:30
Shariq Ansari
6c706e6162
Merge pull request #899 from shariquerik/minor-fix 2025-06-06 17:51:43 +05:30
Shariq Ansari
8f81d207b8 fix: activity is not loading 2025-06-06 17:50:48 +05:30
Shariq Ansari
b74c5f384d
Merge pull request #896 from shariquerik/custom-actions 2025-06-06 17:24:13 +05:30
Shariq Ansari
df412d51fe fix: add custom actions using class based script in mobile view 2025-06-06 17:19:27 +05:30
Shariq Ansari
8942bb7e48 feat: add custom statuses using class based script 2025-06-06 17:17:11 +05:30
Shariq Ansari
ca60679126 feat: add custom actions using class based script 2025-06-06 16:38:39 +05:30
Shariq Ansari
8db846ad5d fix: trigger onload method if controller is loaded 2025-06-06 16:37:55 +05:30
Shariq Ansari
bb6a90058b
Merge pull request #894 from shariquerik/refactor-assignees 2025-06-06 14:58:47 +05:30
Shariq Ansari
44df09fac2 fix: removed setuAssignees code 2025-06-06 14:06:02 +05:30
Shariq Ansari
e214ce8bfb refactor: render assignees from document.js
reload assignees if lead_owner/deal_owner is changed
2025-06-05 18:55:28 +05:30
Shariq Ansari
6d281922e4 fix: load assignees in document.js 2025-06-05 18:53:57 +05:30
Shariq Ansari
58f09331b0 fix: remove multiple assignees not working 2025-06-05 18:52:39 +05:30
Shariq Ansari
9780a6b63e
Merge pull request #891 from shariquerik/fixes-2 2025-06-05 16:14:38 +05:30
Shariq Ansari
71f764c224 refactor: set default values for new lead, deal and contact 2025-06-05 16:08:51 +05:30
Shariq Ansari
9362997246 refactor: organization modal code refactor 2025-06-05 16:08:13 +05:30
Shariq Ansari
28ea88f61e refactor: call log modal code refactor 2025-06-05 16:07:40 +05:30
Shariq Ansari
a25ff14dd4 fix: activity is not loading 2025-06-05 15:31:36 +05:30
Shariq Ansari
d86caee7af
Merge pull request #887 from shariquerik/new-doc-issue 2025-06-05 13:56:57 +05:30
Shariq Ansari
c4caabe722 fix: moved address modal to global modals and control it using modals.js 2025-06-04 19:15:12 +05:30
Shariq Ansari
8dcb77634b fix: moved quick entry modal related logic to modals.js & GlobalModals for all pages 2025-06-04 19:14:15 +05:30
Shariq Ansari
c4feed116d fix: handle new document for lead/deal/contact/organization 2025-06-04 19:00:57 +05:30
Shariq Ansari
e220767179 fix: global modals not working in mobile view 2025-06-04 18:44:51 +05:30
Shariq Ansari
571126c36d fix: moved modal related code to modal.js 2025-06-04 18:34:19 +05:30
Shariq Ansari
832323f25e fix: handle new document for call log 2025-06-04 12:50:19 +05:30
Shariq Ansari
3b73432d8c fix: handle controllers for new document 2025-06-04 12:49:14 +05:30
Shariq Ansari
3aa341370b fix: await scripts.list.promise 2025-06-04 12:48:20 +05:30
Ankush Menat
895da1a812
fix: remove invasive settings (#884) 2025-06-02 19:03:54 +05:30
Shariq Ansari
d34ee6fe48
Merge pull request #875 from Ocheretovich/patch-2 2025-06-02 12:37:56 +05:30
Shariq Ansari
7298fe378c
Merge pull request #878 from frappe/pot_develop_2025-06-01 2025-06-02 12:37:01 +05:30
Shariq Ansari
2da0b48c29
Merge pull request #874 from Ocheretovich/patch-1 2025-06-02 12:36:01 +05:30
frappe-pr-bot
165509f5a0 chore: update POT file 2025-06-01 09:36:30 +00:00
Ocheretovich
c9b9dbb092
Update README.md 2025-05-29 13:06:52 +03:00
Ocheretovich
0cc1d5da8f
Update README.md 2025-05-29 13:03:21 +03:00
Pratik
c70dced268 refactor: internationalization & code clean up 2025-05-29 06:02:35 +00:00
Pratik
df698387dc Merge remote-tracking branch 'origin/develop' into delete-from-record-view 2025-05-29 05:54:17 +00:00
Pratik
716dc056d6 refactor: move delete button 2025-05-29 05:52:36 +00:00
Pratik Badhe
cf91f3f72a
Merge pull request #872 from pratikb64/export-filter-fix 2025-05-28 17:18:19 +05:30
Pratik
51b87d0ac6 fix: export with filter 2025-05-28 11:09:22 +00:00
Shariq Ansari
c83d7adddd
Merge pull request #868 from shariquerik/prettydate 2025-05-28 13:58:41 +05:30
Shariq Ansari
549665bc61 fix: use prettydate method instead of useTimeAgo 2025-05-28 13:50:01 +05:30
Pratik
7f5f43f0c2 style: fix dark mode styles 2025-05-27 05:22:30 +00:00
Pratik
af41469d58 feat: add list view & handle bulk delete, unlink 2025-05-26 15:24:24 +00:00
Shariq Ansari
43e1309bd8
Merge pull request #865 from shariquerik/form-script-fix 2025-05-26 17:56:12 +05:30
Shariq Ansari
91f7cf05fc fix: handle script load while setting up script 2025-05-26 17:50:24 +05:30
Shariq Ansari
875431a620 fix: moved setupHelperMethods from setupFormController to evaluateFormClass 2025-05-26 16:57:02 +05:30
Shariq Ansari
db0c0d98bc fix: pass getDoc function instead of document.doc to keep the reactivity 2025-05-26 16:46:24 +05:30
Shariq Ansari
5406f4a11b
Merge pull request #863 from shariquerik/convert-to-deal-script 2025-05-26 15:40:01 +05:30
Shariq Ansari
bfdd3273fe feat: intercept convert to deal via form script 2025-05-26 14:30:15 +05:30
Shariq Ansari
8798103e7e
Merge pull request #859 from frappe/pot_develop_2025-05-25 2025-05-26 12:08:46 +05:30
Shariq Ansari
203b5ab1ac
Merge branch 'develop' into pot_develop_2025-05-25 2025-05-26 12:04:31 +05:30
Shariq Ansari
ed1b26207b
Merge pull request #858 from shariquerik/create-call-log-script 2025-05-26 11:58:49 +05:30
frappe-pr-bot
e0166a08e2 chore: update POT file 2025-05-25 09:35:42 +00:00
Shariq Ansari
8af4e9b5e8 feat: intercept create lead from call log via form script 2025-05-23 21:49:12 +05:30
Shariq Ansari
900c1d3570
Merge pull request #856 from shariquerik/table-multiselect-fix 2025-05-23 20:39:42 +05:30
Shariq Ansari
b95a17a4e0 fix: set default value as empty array 2025-05-23 20:26:43 +05:30
Shariq Ansari
0f0b012a44
Merge pull request #849 from shariquerik/communication-date 2025-05-22 18:53:20 +05:30
Shariq Ansari
b291f82e4d fix: show communication date instead of creation 2025-05-22 18:13:07 +05:30
Pratik Badhe
86b7222916
Merge pull request #847 from pratikb64/filter-selected-filters 2025-05-22 16:41:57 +05:30
Pratik
7a12b80dd2 fix: hide selected filters from filter list 2025-05-22 10:50:39 +00:00
Pratik
4a836a58ee feat: handle bulk delete 2025-05-22 07:15:22 +00:00
Pratik
b47fc5b93b feat: handle linked docs while deleting 2025-05-21 14:20:48 +00:00
Shariq Ansari
f3b9103a51
Merge pull request #843 from shariquerik/about 2025-05-21 18:00:12 +05:30
Shariq Ansari
dc3ccdddd4 fix: added about link in standard_dropdown_items in hook.py 2025-05-21 17:56:54 +05:30
Shariq Ansari
807eb4a7d9 fix: removed doc & telegram link from user dropdown 2025-05-21 17:51:26 +05:30
Shariq Ansari
a24283eb5e feat: added action to open about modal in user dropdown 2025-05-21 17:50:53 +05:30
Shariq Ansari
fd7116b2e1 feat: show about details in about modal 2025-05-21 17:49:09 +05:30
Shariq Ansari
2e1289df28
Merge pull request #841 from shariquerik/update-toast 2025-05-20 14:37:38 +05:30
Shariq Ansari
6064ca5a4f fix: use toast.create api instead of createToast 2025-05-20 14:35:02 +05:30
Shariq Ansari
3db1b3c0f3 chore: update frappe-ui 2025-05-20 14:21:47 +05:30
Shariq Ansari
06ffa203ef
Merge pull request #838 from shariquerik/invite-member-fix-5 2025-05-20 14:09:38 +05:30
Shariq Ansari
dd1db8f782 fix: added update your password step in onboarding 2025-05-20 14:07:26 +05:30
Shariq Ansari
fe8e309399 feat: added password control 2025-05-20 14:07:15 +05:30
Shariq Ansari
e7a20374c7 fix: set default value as 0 in int field 2025-05-20 14:05:58 +05:30
Shariq Ansari
4cfa0f512b fix: do not show contacts in dropdown in invite member page 2025-05-20 14:05:47 +05:30
Shariq Ansari
64b4f6b759 fix: only set FCRM module if user is Sales User 2025-05-20 14:05:37 +05:30
Shariq Ansari
2d421e6052 fix: allow read permission for form script 2025-05-20 14:05:14 +05:30
Shariq Ansari
cd8dd683fa
Merge pull request #837 from frappe/revert-836-invite-member-fix-3 2025-05-20 13:54:44 +05:30
Shariq Ansari
a2bdc7ab93
Revert "fix: Invite Member Page" 2025-05-20 13:53:35 +05:30
Shariq Ansari
d4132c2411
Merge pull request #836 from shariquerik/invite-member-fix-3 2025-05-20 13:47:44 +05:30
Shariq Ansari
4c6e273268 fix: added update your password step in onboarding 2025-05-20 13:26:02 +05:30
Shariq Ansari
043f174e05 fix: updated components.d.ts 2025-05-20 13:24:42 +05:30
Shariq Ansari
26e9fac1ed feat: added password control 2025-05-20 13:23:26 +05:30
Shariq Ansari
88f33db249 fix: set default value as 0 in int field 2025-05-20 13:17:52 +05:30
Shariq Ansari
55a67bbc0c fix: do not show contacts in dropdown in invite member page 2025-05-20 13:17:20 +05:30
Shariq Ansari
08f042589d fix: only set FCRM module if user is Sales User 2025-05-20 13:16:58 +05:30
Shariq Ansari
52f540a014 fix: allow read permission for form script 2025-05-20 13:15:15 +05:30
Shariq Ansari
e85ef93480
Merge pull request #835 from frappe/revert-833-invite-member-fix-2 2025-05-20 13:09:32 +05:30
Shariq Ansari
a757f80263
Revert "fix: Invite Member Page" 2025-05-20 13:04:02 +05:30
Shariq Ansari
b9b8ff0e10
Merge pull request #833 from shariquerik/invite-member-fix-2 2025-05-20 12:47:27 +05:30
Shariq Ansari
e0aad074ec fix: added update your password step in onboarding 2025-05-19 21:58:03 +05:30
Shariq Ansari
ad88b4e046 feat: added password control 2025-05-19 21:02:10 +05:30
Shariq Ansari
5156814e7a fix: set default value as 0 in int field 2025-05-19 20:41:06 +05:30
Shariq Ansari
f988d16215 fix: use toast.create api instead of createToast 2025-05-19 20:40:11 +05:30
Shariq Ansari
f5a3fccad3 fix: do not show contacts in dropdown in invite member page 2025-05-19 19:05:37 +05:30
Shariq Ansari
e3f0079578 fix: only set FCRM module if user is Sales User 2025-05-19 18:02:30 +05:30
Shariq Ansari
b831ea3c47 fix: allow read permission for form script 2025-05-19 17:58:17 +05:30
Shariq Ansari
a88545b8b9
Merge pull request #831 from shariquerik/onboarding-fixes 2025-05-19 16:51:44 +05:30
Shariq Ansari
44523a0392 fix: store firstLead & firstDeal per user 2025-05-19 16:44:42 +05:30
Shariq Ansari
dbc207a9a6
Merge branch 'develop' into onboarding-fixes 2025-05-19 16:23:35 +05:30
Shariq Ansari
e68d861ee5
Merge pull request #828 from shariquerik/esm-toast 2025-05-19 16:21:16 +05:30
Shariq Ansari
7851bbadfa
Merge branch 'develop' into esm-toast 2025-05-19 16:13:55 +05:30
Shariq Ansari
9223d00af3
Merge pull request #826 from frappe/pot_develop_2025-05-18 2025-05-19 16:12:59 +05:30
Shariq Ansari
740c21532a fix: update Vue compiler options for custom lucide elements 2025-05-19 16:11:19 +05:30
Shariq Ansari
9fdd8bbc17 fix: add @tiptap/extension-paragraph dependency version 2.12.0 2025-05-19 15:56:43 +05:30
Shariq Ansari
0978fa58a2 fix: wrap layout and dialogs in FrappeUIProvider 2025-05-19 15:55:54 +05:30
Shariq Ansari
1395a12d32 build(deps): bump frappeui to 0.1.145 2025-05-19 15:55:37 +05:30
Shariq Ansari
9aab0e7417 fix: update package.json and config files to use ES module syntax 2025-05-19 15:55:07 +05:30
Shariq Ansari
ddc5810c71 fix: change heading to paragraph in invitation email template 2025-05-19 13:40:26 +05:30
Shariq Ansari
21c349e1d7 fix: added dependsOn value on dependent step to gray out 2025-05-19 13:39:06 +05:30
Shariq Ansari
a7784c2985 fix: get filtered steps based on condition 2025-05-19 13:06:38 +05:30
frappe-pr-bot
0cc69d90f0 chore: update POT file 2025-05-18 09:35:38 +00:00
Shariq Ansari
f125737d30
Merge pull request #821 from shariquerik/call-issue 2025-05-15 11:25:13 +05:30
Shariq Ansari
18aef2376a fix: cannot make call 2025-05-15 11:24:27 +05:30
Shariq Ansari
c8287ff107
Merge pull request #818 from shariquerik/contact-not-loading-1 2025-05-15 01:18:00 +05:30
Shariq Ansari
baf344a697 fix: remove updateField event from various components 2025-05-15 01:15:25 +05:30
Shariq Ansari
8c94049e3c fix: contact/organization page not loading 2025-05-15 01:01:48 +05:30
Shariq Ansari
646c76c3cb
Merge pull request #814 from shariquerik/product-details 2025-05-14 23:42:46 +05:30
Shariq Ansari
adbb9f5765 fix: pass doctype argument to get_product_details_script in create_product_details_script 2025-05-14 20:13:34 +05:30
Shariq Ansari
d3a6cc968f fix: add patch to create default scripts 2025-05-14 20:00:48 +05:30
Shariq Ansari
d6ff40cc6a chore: formatting fix 2025-05-14 19:58:39 +05:30
Shariq Ansari
fdd6c46b5f fix: create product detail script on install 2025-05-14 19:57:51 +05:30
Shariq Ansari
26c892c2a0 fix: added products table in crm lead 2025-05-14 19:57:09 +05:30
Shariq Ansari
3516e1ff44 fix: update field visibility logic and disable inputs based on read-only status 2025-05-14 19:08:29 +05:30
Shariq Ansari
0047077074 feat: enhance FormattedInput component with description slot and useAttrs for better attribute handling 2025-05-14 19:01:43 +05:30
Shariq Ansari
8459fac184 fix: set discount amount and net amount fields to read-only 2025-05-14 17:16:32 +05:30
Shariq Ansari
afe828f012 fix: update mandatory field indicator color and replace FormControl with FormattedInput for various field types 2025-05-14 16:55:19 +05:30
Shariq Ansari
60ed0a2043 fix: cache controller on document level not on doctype level 2025-05-14 15:21:47 +05:30
Shariq Ansari
2c9bc07dec fix: added default percent & int to 0 2025-05-14 15:21:07 +05:30
Shariq Ansari
91ba11b565 feat: added callback to update link field value after creating new 2025-05-14 14:04:25 +05:30
Shariq Ansari
8f79427720 fix: select text on focus 2025-05-14 13:34:21 +05:30
Shariq Ansari
32f3aaf38f fix: show formatted percent, currency & float only when not focused 2025-05-14 13:19:54 +05:30
Shariq Ansari
76aaf7f37d feat: added global create document modal for link field 2025-05-12 19:27:41 +05:30
Shariq Ansari
7d37c606cc fix: added columns for product code field 2025-05-12 17:51:36 +05:30
Shariq Ansari
6bce89f277 fix: right aligned number fields 2025-05-12 17:47:49 +05:30
Shariq Ansari
5420fcfe29 fix: render correct currency format 2025-05-12 17:45:05 +05:30
Shariq Ansari
8507c20481 fix: do not show qty and other fields 2025-05-12 17:20:28 +05:30
Shariq Ansari
914dd8bf93 fix: handle this.doc.getRow effectively 2025-05-12 16:37:33 +05:30
Shariq Ansari
960ebdc727 fix: handle field change for float, percent & currency 2025-05-12 16:34:48 +05:30
Shariq Ansari
74ef956638 fix: removed total quantity field 2025-05-12 16:32:09 +05:30
Shariq Ansari
a6323f42af fix: added logic to update amount, net amount, total and net total 2025-05-12 11:58:07 +05:30
Shariq Ansari
bc1c20c91f fix: added crm products table in crm deal and total field 2025-05-12 11:58:07 +05:30
Shariq Ansari
43297373ed fix: added crm product doctype 2025-05-12 11:58:07 +05:30
Shariq Ansari
5228755f7f fix: show formatted percent, currency & float in grid 2025-05-12 11:58:07 +05:30
Shariq Ansari
7ded0a0742 fix: show formatted percent, currency if read only 2025-05-12 11:53:03 +05:30
Shariq Ansari
d74ff9ab62
Merge pull request #811 from shariquerik/required-field-modal-fix 2025-05-12 11:30:32 +05:30
Shariq Ansari
6ef27106df
Merge branch 'develop' into required-field-modal-fix 2025-05-12 11:19:37 +05:30
Shariq Ansari
35a27101c1 fix: error if section is removed and saved 2025-05-12 11:18:47 +05:30
Shariq Ansari
6fbe75c8ad
Merge pull request #803 from frappe/pot_develop_2025-05-04 2025-05-09 20:30:58 +05:30
Shariq Ansari
89fd754efc
Merge pull request #806 from shariquerik/form-script-refactor 2025-05-09 20:24:49 +05:30
Shariq Ansari
576763fe5b fix: enhance error and warning messages with localization support 2025-05-09 18:00:46 +05:30
Shariq Ansari
c67ec08e1a fix: update toast messages for document update success and error handling 2025-05-09 17:53:28 +05:30
Shariq Ansari
6f49573f2f fix: add loading state check to prevent rendering issues in SidePanelLayout 2025-05-09 17:50:43 +05:30
Shariq Ansari
12c3290f19 fix: streamline trigger functions to use a unified handler for controller actions 2025-05-09 17:38:10 +05:30
Shariq Ansari
53c0706a3a feat: implement runSequentially utility for sequential function execution 2025-05-09 17:06:41 +05:30
Shariq Ansari
556386e446 fix: cache controllers and use Promise.all for concurrent execution 2025-05-09 15:22:40 +05:30
Shariq Ansari
07b2d9f792 fix: loop through controllers with multiple instances of multiple scripts and run trigger methods 2025-05-08 18:32:56 +05:30
Shariq Ansari
a2081da296 fix: provide array of instances in controllers if multiple script exist 2025-05-08 18:32:13 +05:30
Shariq Ansari
dde7db9489 fix: remove deprecated setupForm warning and error handling 2025-05-07 19:07:54 +05:30
Shariq Ansari
f947f55fc6 fix: do not show non value fields in dropdown 2025-05-07 19:05:10 +05:30
Shariq Ansari
7bbac6c703 fix: use dayjs for date field default value 2025-05-07 18:26:23 +05:30
Shariq Ansari
420ecb6147 fix: update all fields default value 2025-05-07 18:18:14 +05:30
Shariq Ansari
dcb2787498 feat: handle default value in grid 2025-05-07 18:12:22 +05:30
Shariq Ansari
336083a00f feat: added trigger function on row add & remove 2025-05-07 18:06:38 +05:30
Shariq Ansari
727d0a9acd fix: add doctype, idx, parent, parenttype & parentfield in new grid row 2025-05-07 16:59:22 +05:30
Shariq Ansari
29894ffcca fix: handle commented class declations 2025-05-07 16:38:01 +05:30
Shariq Ansari
e804fa39ba fix: exclude Float & Currency from read only formcontrol 2025-05-07 14:01:39 +05:30
Shariq Ansari
f866284240 fix: allow empty actions 2025-05-07 13:59:55 +05:30
Shariq Ansari
9e3124d29e fix: added triggerOnRefresh & getActions method 2025-05-07 12:15:49 +05:30
Shariq Ansari
d7e0eb09b3 fix: getRow should be available in parent & child instances 2025-05-07 12:14:47 +05:30
Shariq Ansari
5fcd447bc8 fix: added this.meta 2025-05-07 12:13:56 +05:30
Shariq Ansari
6f04b85663 fix: added this.doc.trigger & this.doc.getRow with row.trigger 2025-05-06 13:03:56 +05:30
Shariq Ansari
47262761fe fix: handle section.contacts also 2025-05-06 12:59:31 +05:30
frappe-pr-bot
b46e7a2185 chore: update POT file 2025-05-04 09:35:32 +00:00
Shariq Ansari
2d484c1ad2 fix: handle onchange of grid row field in modal 2025-05-03 15:35:10 +05:30
Shariq Ansari
275fa90a4d fix: added trigger method to call methods from same or different class instance 2025-05-02 18:25:51 +05:30
Shariq Ansari
f8956c70bf fix: handle onchange of grid row field 2025-05-02 16:56:57 +05:30
Shariq Ansari
39fa9c78f8 fix: parse multiple class in form script 2025-05-02 15:56:47 +05:30
Shariq Ansari
d96a29543e fix: added deprecation warning if using old formScript syntax 2025-05-02 08:03:42 +05:30
Shariq Ansari
d2d4abe91f fix: avoid none values 2025-05-02 07:28:40 +05:30
Shariq Ansari
5f567cf138 fix: added change emit in Table bulti select 2025-05-02 06:51:52 +05:30
Shariq Ansari
7bf7d94127 fix: added fieldChange method in almost all fieldtypes 2025-05-01 18:25:25 +05:30
Shariq Ansari
5b8d0d2aeb fix: check if script exist 2025-05-01 18:07:40 +05:30
Shariq Ansari
d37e585205 fix: trigger on change in Field & SidePanelLayout for Select field 2025-05-01 18:03:04 +05:30
Shariq Ansari
a30503ca5f fix: use document to load doc data in sidepanel layout 2025-05-01 18:01:53 +05:30
Shariq Ansari
e65899e384 fix: use document to load doc data in DataFields 2025-05-01 17:57:17 +05:30
Shariq Ansari
16a3f3d66c fix: created triggerOnChange method 2025-05-01 17:56:18 +05:30
Shariq Ansari
1e2f325c55 fix: setup form script in document.js 2025-05-01 17:55:54 +05:30
Shariq Ansari
ccd240f4e8 fix: created document composable to get any doctype record 2025-05-01 17:54:13 +05:30
Shariq Ansari
7b34c5eb66 fix: load script and setup class instances 2025-05-01 17:52:11 +05:30
Shariq Ansari
6da3761e76 fix: check if setupForm exist 2025-05-01 17:27:00 +05:30
Shariq Ansari
b03abdd2eb fix: get scripts api 2025-05-01 17:22:26 +05:30
Shariq Ansari
6ea4e985ef
Merge pull request #787 from frappe/pot_develop_2025-04-27 2025-04-28 12:28:29 +05:30
frappe-pr-bot
699d6cb08c chore: update POT file 2025-04-27 09:35:22 +00:00
Pratik Badhe
ac70deaf19
Merge pull request #781 from pratikb64/call-log-fix
fix: international call log issue
2025-04-23 16:12:59 +05:30
Pratik
4907db44eb fix: international call log issue 2025-04-23 15:54:54 +05:30
Pratik Badhe
81154d1f50
Merge pull request #776 from pratikb64/email-acc-localization
chore: add localization support for email account settings
2025-04-22 15:39:14 +05:30
Pratik
5eb46f6b6c chore: add localization support for email account settings 2025-04-22 15:33:28 +05:30
Shariq Ansari
001a6617f5
Merge pull request #771 from shariquerik/contact-not-loading 2025-04-22 13:11:42 +05:30
Shariq Ansari
c009373a43 fix: do not show error page while loading 2025-04-22 12:59:24 +05:30
Shariq Ansari
cef20e37c2 fix: contact page not loading 2025-04-22 12:57:20 +05:30
Shariq Ansari
20d16c6a32
Merge pull request #759 from frappe/pot_develop_2025-04-20 2025-04-21 14:36:29 +05:30
Shariq Ansari
2fc3daee70
Merge branch 'develop' into pot_develop_2025-04-20 2025-04-21 14:30:57 +05:30
Shariq Ansari
a7955ba9c5
Merge pull request #761 from shariquerik/data-tab-dirty-fix 2025-04-21 11:53:36 +05:30
Shariq Ansari
84e773eab9 fix: do not show error page while loading 2025-04-21 11:46:42 +05:30
Shariq Ansari
da4d3032be fix: mark data tab form dirty by watching field updates 2025-04-21 11:46:19 +05:30
frappe-pr-bot
d89e71ac2f chore: update POT file 2025-04-20 09:35:21 +00:00
Pratik Badhe
de806ee6d9
Merge pull request #753 from pratikb64/email-account-dark-mode
fix: dark mode email account css
2025-04-16 18:10:27 +05:30
Pratik
9c45877999 fix: dark mode email account css 2025-04-16 18:00:28 +05:30
Shariq Ansari
2059ecdb40
Merge pull request #726 from pratikb64/fix-export-logic 2025-04-14 11:19:59 +05:30
Shariq Ansari
52d66b5de4
Merge branch 'develop' into fix-export-logic 2025-04-14 11:15:25 +05:30
Shariq Ansari
fb9b026ad6 fix: restrict app in apps page if no access to FCRM module 2025-04-14 11:05:43 +05:30
Shariq Ansari
8f1b6f6b67
Merge pull request #742 from shariquerik/restrict-app-if-no-module-access-2 2025-04-14 10:36:12 +05:30
Shariq Ansari
0bd448a399 revert: restrict app in apps page if no access to FCRM module 2025-04-14 10:35:28 +05:30
Shariq Ansari
2b395a05ea
Merge pull request #734 from frappe/pot_develop_2025-04-13 2025-04-13 20:14:27 +05:30
Shariq Ansari
dce17de000
Merge pull request #735 from shariquerik/restrict-app-if-no-module-access-1 2025-04-13 20:07:47 +05:30
Shariq Ansari
3881179f72 fix: restrict app in apps page if no access to FCRM module 2025-04-13 19:59:23 +05:30
frappe-pr-bot
da0a502756 chore: update POT file 2025-04-13 09:36:51 +00:00
Pratik
cbf00e29ac refactor: make function names clearer 2025-04-11 18:09:15 +05:30
Pratik
a466766c5c refactor: remove unnecessary watchers 2025-04-08 18:22:44 +05:30
Shariq Ansari
a4781509c4
Merge branch 'develop' into fix-export-logic 2025-04-08 16:37:26 +05:30
Shariq Ansari
8a9361d822 revert: module validation 2025-04-08 16:01:23 +05:30
Shariq Ansari
e2522a492a
Merge pull request #728 from shariquerik/restrict-doc-access
fix: added ErrorPage if user does not have access to doc
2025-04-08 15:41:10 +05:30
Shariq Ansari
bab551c511
Merge branch 'develop' into restrict-doc-access 2025-04-08 15:39:37 +05:30
Shariq Ansari
c63bb16704 ci: added backport to main-hotfix ci 2025-04-08 15:36:32 +05:30
Shariq Ansari
fa56dc4791 fix: show error page if there is no access 2025-04-08 15:28:54 +05:30
Shariq Ansari
e92ee3b730 fix: check read access before loading data 2025-04-08 15:28:19 +05:30
Shariq Ansari
bb794f4887 fix: added ErrorPage component 2025-04-08 15:27:50 +05:30
Pratik
a227389e3e fix: export logic 2025-04-08 15:07:27 +05:30
Shariq Ansari
d9f0b067ca
Merge pull request #722 from shariquerik/added-mergify
ci: added mergify.yml for backport
2025-04-07 21:17:59 +05:30
Shariq Ansari
c0b708462a ci: added mergify.yml for backport 2025-04-07 18:00:59 +05:30
Shariq Ansari
adb0dfff47
Merge pull request #721 from shariquerik/restrict-app-if-no-module-access
fix: restrict app in apps page if no access to FCRM module
2025-04-07 17:37:02 +05:30
Shariq Ansari
6139cb5cb9 fix: restrict app in apps page if no access to FCRM module 2025-04-07 17:31:17 +05:30
Shariq Ansari
61d7924c54
Merge pull request #701 from frappe/pot_develop_2025-03-30
chore: update POT file
2025-04-07 16:51:16 +05:30
Shariq Ansari
899b09ac40
Merge branch 'develop' into pot_develop_2025-03-30 2025-04-07 16:51:07 +05:30
Shariq Ansari
debc9fc1cb
Merge pull request #716 from shariquerik/make-create-call
fix: Create & Make call
2025-04-07 16:49:38 +05:30
Shariq Ansari
5c76adedf3
Merge pull request #712 from shariquerik/dynamic-link
feat: Dynamic Link field support
2025-04-07 16:49:30 +05:30
Shariq Ansari
1ebb26e4c2
Merge pull request #708 from frappe/pot_develop_2025-04-06
chore: update POT file
2025-04-07 16:44:59 +05:30
Shariq Ansari
67378c1f52
Merge pull request #719 from pratikb64/default-assigned-to
fix: default "assigned to" in deals and leads list view
2025-04-07 16:44:32 +05:30
Pratik
469a22ef5f fix: default "assigned to" in deals and leads list view 2025-04-07 16:37:19 +05:30
Shariq Ansari
fdceb51fdc fix: added multi action button to make and create call 2025-04-07 15:34:46 +05:30
Shariq Ansari
97a132e05f fix: show call tab always 2025-04-07 15:32:34 +05:30
Shariq Ansari
26fabddcbe fix: handle feather icon in multi action button 2025-04-07 15:32:09 +05:30
Shariq Ansari
40370067b2 fix: dynamic variant 2025-04-07 14:13:55 +05:30
Shariq Ansari
f0bf6962e7 fix: do not show dropdown if only one option 2025-04-07 14:07:41 +05:30
Shariq Ansari
3b432a0209 fix: added multi action button 2025-04-07 13:58:58 +05:30
Shariq Ansari
c7a03922a0 feat: Dynamic Link field support 2025-04-07 13:16:52 +05:30
frappe-pr-bot
e70b4c091e chore: update POT file 2025-04-06 09:35:28 +00:00
Pratik Badhe
7e38d5e405
Merge pull request #707 from pratikb64/kanban-filter-fix
fix: kanban filter
2025-04-04 17:14:46 +05:30
Pratik
f810e82b45 fix: kanban filter 2025-04-04 17:07:54 +05:30
Pratik Badhe
dff9f93a6b
Merge pull request #704 from pratikb64/make-fields-mandatory
fix: add mandatory fields
2025-04-04 10:26:29 +05:30
Shariq Ansari
c4109ad6ac build(deps): bump frappeui to 0.1.123 2025-04-04 10:09:18 +05:30
Pratik
7a6efb900e fix: add mandatory fields 2025-04-01 17:26:46 +05:30
frappe-pr-bot
e080e47a35 chore: update POT file 2025-03-30 09:35:00 +00:00
Pratik Badhe
82599f91d8
Merge pull request #698 from pratikb64/email-settings-fix
fix: ui alignment
2025-03-28 15:34:36 +05:30
Pratik
8fa156f625 fix: ui alignment 2025-03-28 15:33:40 +05:30
Pratik Badhe
55112cefa9
Merge pull request #697 from pratikb64/email-setting-fix
fix: broken images
2025-03-27 17:38:28 +05:30
Pratik
152c7c8a91 fix: broken images 2025-03-27 17:37:39 +05:30
Pratik Badhe
aa1c0da80e
Merge pull request #696 from pratikb64/add-email-setting
feat: add email account
2025-03-27 15:33:20 +05:30
Pratik
87174f207d feat: add email account 2025-03-27 15:32:37 +05:30
Shariq Ansari
400f879d29 fix: only allow invite by email for Sales Manager & Sales User role 2025-03-26 14:44:40 +05:30
110 changed files with 5313 additions and 1177 deletions

45
.mergify.yml Normal file
View File

@ -0,0 +1,45 @@
pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- and:
- author!=shariquerik
- author!=frappe-pr-bot
- author!=mergify[bot]
- or:
- base=main
actions:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on develop branch.
- name: backport to develop
conditions:
- label="backport develop"
actions:
backport:
branches:
- develop
assignees:
- "{{ author }}"
- name: backport to main-hotfix
conditions:
- label="backport main-hotfix"
actions:
backport:
branches:
- main-hotfix
assignees:
- "{{ author }}"
- name: backport to main
conditions:
- label="backport main"
actions:
backport:
branches:
- main
assignees:
- "{{ author }}"

View File

@ -1,4 +1,4 @@
__version__ = "1.53.1"
__version__ = "2.0.0-dev"
__title__ = "Frappe CRM"

View File

@ -1,9 +1,9 @@
import frappe
from bs4 import BeautifulSoup
from frappe.config import get_modules_from_all_apps_for_user
from frappe.core.api.file import get_max_file_size
from frappe.translate import get_all_translations
from frappe.utils import cstr, split_emails, validate_email_address
from frappe.utils.modules import get_modules_from_all_apps_for_user
from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD
@ -106,6 +106,7 @@ def invite_by_email(emails: str, role: str):
if not emails:
return
email_string = validate_email_address(emails, throw=False)
email_list = split_emails(email_string)
if not email_list:

View File

@ -1008,7 +1008,7 @@ def get_deals_by_territory(from_date="", to_date="", user=""):
WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s
{deal_conds}
GROUP BY d.territory
ORDER BY value DESC
ORDER BY deals DESC, value DESC
""",
{"from": from_date, "to": to_date},
as_dict=True,
@ -1065,7 +1065,7 @@ def get_deals_by_salesperson(from_date="", to_date="", user=""):
WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s
{deal_conds}
GROUP BY d.deal_owner
ORDER BY value DESC
ORDER BY deals DESC, value DESC
""",
{"from": from_date, "to": to_date},
as_dict=True,

View File

@ -420,16 +420,23 @@ def get_data(
rows.append(field)
for kc in kanban_columns:
column_filters = {column_field: kc.get("name")}
# Start with base filters
column_filters = []
# Convert and add the main filters first
if filters:
base_filters = convert_filter_to_tuple(doctype, filters)
column_filters.extend(base_filters)
# Add the column-specific filter
if column_field and kc.get("name"):
column_filters.append([doctype, column_field, "=", kc.get("name")])
order = kc.get("order")
if (column_field in filters and filters.get(column_field) != kc.get("name")) or kc.get("delete"):
if kc.get("delete"):
column_data = []
else:
column_filters.update(filters.copy())
page_length = 20
if kc.get("page_length"):
page_length = kc.get("page_length")
page_length = kc.get("page_length", 20)
if order:
column_data = get_records_based_on_order(
@ -439,26 +446,20 @@ def get_data(
column_data = frappe.get_list(
doctype,
fields=rows,
filters=convert_filter_to_tuple(doctype, column_filters),
filters=column_filters,
order_by=order_by,
page_length=page_length,
)
new_filters = filters.copy()
new_filters.update({column_field: kc.get("name")})
all_count = frappe.get_list(
doctype,
filters=convert_filter_to_tuple(doctype, new_filters),
filters=column_filters,
fields="count(*) as total_count",
)[0].total_count
kc["all_count"] = all_count
kc["count"] = len(column_data)
for d in column_data:
getCounts(d, doctype)
if order:
column_data = sorted(
column_data,
@ -807,6 +808,7 @@ def remove_doc_link(doctype, docname):
"reference_doctype": "",
"reference_name": "",
}
if linked_doc_data.get("notification_type_doctype") == linked_doc_data.get("reference_doctype"):
delete_references.update(delete_notification_type)
@ -854,6 +856,7 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
remove_contact_link(item["doctype"], item["docname"])
else:
remove_doc_link(item["doctype"], item["docname"])
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):

View File

@ -55,7 +55,11 @@
"first_response_time",
"first_responded_on",
"log_tab",
"status_change_log"
"status_change_log",
"syncing_tab",
"facebook_lead_id",
"column_break_ixmu",
"facebook_form_id"
],
"fields": [
{
@ -325,13 +329,33 @@
"label": "Net Total",
"options": "currency",
"read_only": 1
},
{
"fieldname": "syncing_tab",
"fieldtype": "Tab Break",
"label": "Syncing"
},
{
"fieldname": "facebook_lead_id",
"fieldtype": "Data",
"label": "Facebook Lead ID",
"unique": 1
},
{
"fieldname": "column_break_ixmu",
"fieldtype": "Column Break"
},
{
"fieldname": "facebook_form_id",
"fieldtype": "Data",
"label": "Facebook Form ID"
}
],
"grid_page_length": 50,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-14 19:51:06.184569",
"modified": "2025-10-19 18:36:24.683076",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lead",

View File

@ -14,6 +14,53 @@ from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
class CRMLead(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from crm.fcrm.doctype.crm_products.crm_products import CRMProducts
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import CRMStatusChangeLog
from frappe.types import DF
annual_revenue: DF.Currency
communication_status: DF.Link | None
converted: DF.Check
email: DF.Data | None
facebook_form_id: DF.Data | None
facebook_lead_id: DF.Data | None
first_name: DF.Data
first_responded_on: DF.Datetime | None
first_response_time: DF.Duration | None
gender: DF.Link | None
image: DF.AttachImage | None
industry: DF.Link | None
job_title: DF.Data | None
last_name: DF.Data | None
lead_name: DF.Data | None
lead_owner: DF.Link | None
middle_name: DF.Data | None
mobile_no: DF.Data | None
naming_series: DF.Literal["CRM-LEAD-.YYYY.-"]
net_total: DF.Currency
no_of_employees: DF.Literal["1-10", "11-50", "51-200", "201-500", "501-1000", "1000+"]
organization: DF.Data | None
phone: DF.Data | None
products: DF.Table[CRMProducts]
response_by: DF.Datetime | None
salutation: DF.Link | None
sla: DF.Link | None
sla_creation: DF.Datetime | None
sla_status: DF.Literal["", "First Response Due", "Failed", "Fulfilled"]
source: DF.Link | None
status: DF.Link
status_change_log: DF.Table[CRMStatusChangeLog]
territory: DF.Link | None
total: DF.Currency
website: DF.Data | None
# end: auto-generated types
def before_validate(self):
self.set_sla()

View File

@ -135,7 +135,7 @@ def get_quotation_url(crm_deal, organization):
"party_name": crm_deal,
"company": erpnext_crm_settings.erpnext_company,
"contact_person": contact,
"customer_address": address
"customer_address": address,
}
else:
site_url = erpnext_crm_settings.get("erpnext_site_url")
@ -147,14 +147,11 @@ def get_quotation_url(crm_deal, organization):
"party_name": prospect,
"company": erpnext_crm_settings.erpnext_company,
"contact_person": contact,
"customer_address": address
"customer_address": address,
}
# Filter out None values and build query string
query_string = "&".join(
f"{key}={value}" for key, value in params.items()
if value is not None
)
query_string = "&".join(f"{key}={value}" for key, value in params.items() if value is not None)
return f"{base_url}?{query_string}"

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Helpdesk CRM Settings", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,102 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-18 17:25:49.638398",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"column_break_idaw",
"is_helpdesk_in_different_site",
"helpdesk_site_url",
"helpdesk_site_apis_section",
"api_key",
"column_break_tqsm",
"api_secret"
],
"fields": [
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"fieldname": "column_break_idaw",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "enabled",
"fieldname": "is_helpdesk_in_different_site",
"fieldtype": "Check",
"label": "Is Helpdesk installed on a different site?"
},
{
"depends_on": "eval:doc.enabled && doc.is_helpdesk_in_different_site",
"fieldname": "helpdesk_site_url",
"fieldtype": "Data",
"label": "Helpdesk Site URL",
"mandatory_depends_on": "is_helpdesk_in_different_site"
},
{
"depends_on": "enabled",
"fieldname": "helpdesk_site_apis_section",
"fieldtype": "Section Break",
"label": "Helpdesk Site API's"
},
{
"depends_on": "eval:doc.enabled && doc.is_helpdesk_in_different_site",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
"mandatory_depends_on": "is_helpdesk_in_different_site"
},
{
"fieldname": "column_break_tqsm",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enabled && doc.is_helpdesk_in_different_site",
"fieldname": "api_secret",
"fieldtype": "Password",
"label": "API Secret",
"mandatory_depends_on": "is_helpdesk_in_different_site"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-08-18 17:33:38.616328",
"modified_by": "Administrator",
"module": "FCRM",
"name": "Helpdesk CRM Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,178 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class HelpdeskCRMSettings(Document):
def validate(self):
if self.enabled:
self.validate_if_helpdesk_installed()
self.create_helpdesk_script()
def validate_if_helpdesk_installed(self):
if not self.is_helpdesk_in_different_site:
if "helpdesk" not in frappe.get_installed_apps():
frappe.throw(_("Helpdesk is not installed in the current site"))
def create_helpdesk_script(self):
if not frappe.db.exists("CRM Form Script", "Helpdesk Integration Script"):
script = get_helpdesk_script()
frappe.get_doc(
{
"doctype": "CRM Form Script",
"name": "Helpdesk Integration Script",
"dt": "CRM Deal",
"view": "Form",
"script": script,
"enabled": 1,
"is_standard": 1,
}
).insert()
@frappe.whitelist()
def create_customer_in_helpdesk(name, email):
helpdesk_crm_settings = frappe.get_single("Helpdesk CRM Settings")
if not helpdesk_crm_settings.enabled:
frappe.throw(_("Helpdesk is not integrated with the CRM"))
if not helpdesk_crm_settings.is_helpdesk_in_different_site:
# from helpdesk.integrations.crm.api import create_customer
return create_customer(name, email)
def get_helpdesk_script():
return """class CRMDeal {
onLoad() {
this.actions.push(
{
group: "Helpdesk",
hideLabel: true,
items: [
{
label: "Create customer in Helpdesk",
onClick: () => {
call('crm.fcrm.doctype.helpdesk_crm_settings.helpdesk_crm_settings.create_customer_in_helpdesk', {
name: this.doc.organization,
email: this.doc.email
}).then((a) => {
toast.success("Customer created successfully, " + a.customer)
})
}
}
]
}
)
}
}"""
# Helpdesk methods TODO: move to helpdesk.integrations.crm.api
def create_customer(name, email):
customer = frappe.db.exists("HD Customer", name)
if not customer:
customer = frappe.get_doc(
{
"doctype": "HD Customer",
"customer_name": name,
}
)
customer.insert(ignore_permissions=True, ignore_if_duplicate=True)
else:
customer = frappe.get_doc("HD Customer", customer)
contact = frappe.db.exists("Contact", {"email_id": email})
if contact:
contact = frappe.get_doc("Contact", contact)
contact.append(
"links", {"link_doctype": "HD Customer", "link_name": customer.name}
)
contact.save(ignore_permissions=True)
else:
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": email.split("@")[0],
"email_ids": [{"email_id": email, "is_primary": 1}],
"links": [{"link_doctype": "HD Customer", "link_name": customer.name}],
}
)
contact.insert(ignore_permissions=True)
if not frappe.db.exists("User", contact.email_id):
invite_user(contact.name)
else:
base_url = frappe.utils.get_url() + "/helpdesk"
frappe.sendmail(
recipients=[contact.email_id],
subject="Welcome existing user to Helpdesk",
message=f"""
<h1>Hello,</h1>
<button>{base_url}</button>
""",
now=True,
)
return {"customer": customer.name, "contact": contact.name}
def invite_user(contact: str):
contact = frappe.get_doc("Contact", contact)
contact.check_permission()
if not contact.email_id:
frappe.throw(_("Please set Email Address"))
user = frappe.get_doc(
{
"doctype": "User",
"first_name": contact.first_name,
"last_name": contact.last_name,
"email": contact.email_id,
"user_type": "Website User",
"send_welcome_email": 0
}
).insert()
contact.user = user.name
contact.save(ignore_permissions=True)
send_welcome_mail_to_user(user)
return user.name
def send_welcome_mail_to_user(user):
from frappe.utils import get_url
from frappe.utils.user import get_user_fullname
link = user.reset_password()
frappe.cache.hset("redirect_after_login", user.name, "/helpdesk")
site_url = get_url()
subject = _("Welcome to Helpdesk")
created_by = get_user_fullname(frappe.session["user"])
if created_by == "Guest":
created_by = "Administrator"
args = {
"first_name": user.first_name or user.last_name or "user",
"last_name": user.last_name,
"user": user.name,
"title": subject,
"login_url": get_url(),
"created_by": created_by,
"site_url": site_url,
"link": link
}
frappe.sendmail(
recipients=[user.email],
subject=subject,
template="helpdesk_invitation",
args=args,
now=True,
)

View File

@ -0,0 +1,21 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestHelpdeskCRMSettings(IntegrationTestCase):
"""
Integration tests for HelpdeskCRMSettings.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -22,6 +22,8 @@ add_to_apps_screen = [
}
]
export_python_type_annotations = True
# Includes in <head>
# ------------------
@ -167,23 +169,28 @@ doc_events = {
# Scheduled Tasks
# ---------------
# scheduler_events = {
# "all": [
# "crm.tasks.all"
# ],
# "daily": [
# "crm.tasks.daily"
# ],
# "hourly": [
# "crm.tasks.hourly"
# ],
# "weekly": [
# "crm.tasks.weekly"
# ],
# "monthly": [
# "crm.tasks.monthly"
# ],
# }
scheduler_events = {
"daily_long": [
"crm.tasks.sync_leads_from_sources_daily"
],
"hourly_long": [
"crm.tasks.sync_leads_from_sources_hourly"
],
"monthly_long": [
"crm.tasks.sync_leads_from_sources_monthly"
],
"cron": {
"*/5 * * * *": [
"crm.tasks.sync_leads_from_sources_5_minutes"
],
"*/10 * * * *": [
"crm.tasks.sync_leads_from_sources_10_minutes"
],
"*/15 * * * *": [
"crm.tasks.sync_leads_from_sources_15_minutes"
],
}
}
# Testing
# -------

View File

View File

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Facebook Lead Form", {
refresh(frm) {
//
},
});

View File

@ -0,0 +1,81 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:id",
"creation": "2025-09-26 19:01:48.325681",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"page",
"id",
"column_break_ahyo",
"form_name",
"section_break_iqhq",
"questions"
],
"fields": [
{
"fieldname": "page",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Page",
"options": "Facebook Page",
"reqd": 1
},
{
"fieldname": "id",
"fieldtype": "Data",
"label": "ID",
"unique": 1
},
{
"fieldname": "column_break_ahyo",
"fieldtype": "Column Break"
},
{
"fieldname": "form_name",
"fieldtype": "Data",
"label": "Form Name"
},
{
"fieldname": "section_break_iqhq",
"fieldtype": "Section Break"
},
{
"fieldname": "questions",
"fieldtype": "Table",
"label": "Questions",
"options": "Facebook Lead Form Question"
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-09-29 18:50:19.215513",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Facebook Lead Form",
"naming_rule": "By fieldname",
"owner": "hussain@frappe.io",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "form_name"
}

View File

@ -0,0 +1,24 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class FacebookLeadForm(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from crm.lead_syncing.doctype.facebook_lead_form_question.facebook_lead_form_question import FacebookLeadFormQuestion
from frappe.types import DF
form_name: DF.Data | None
id: DF.Data | None
page: DF.Link
questions: DF.Table[FacebookLeadFormQuestion]
# end: auto-generated types
pass

View File

@ -0,0 +1,22 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestFacebookLeadForm(IntegrationTestCase):
"""
Integration tests for FacebookLeadForm.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -0,0 +1,67 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-09-26 19:45:47.696180",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"id",
"column_break_hgde",
"key",
"type",
"mapped_to_crm_field"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "column_break_hgde",
"fieldtype": "Column Break"
},
{
"fieldname": "key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Key",
"reqd": 1
},
{
"fieldname": "type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Type"
},
{
"fieldname": "id",
"fieldtype": "Data",
"label": "ID"
},
{
"default": "Not Synced",
"fieldname": "mapped_to_crm_field",
"fieldtype": "Autocomplete",
"in_list_view": 1,
"label": "Mapped to CRM Field"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-09-29 18:45:21.800960",
"modified_by": "hussain@frappe.io",
"module": "Lead Syncing",
"name": "Facebook Lead Form Question",
"owner": "hussain@frappe.io",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,27 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class FacebookLeadFormQuestion(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
id: DF.Data | None
key: DF.Data
label: DF.Data | None
mapped_to_crm_field: DF.Autocomplete | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
type: DF.Data | None
# end: auto-generated types
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Facebook Page", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,83 @@
{
"actions": [],
"autoname": "field:id",
"creation": "2025-09-26 18:59:12.833879",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"page_name",
"account_id",
"category",
"column_break_eteo",
"id",
"access_token"
],
"fields": [
{
"fieldname": "category",
"fieldtype": "Data",
"label": "Category"
},
{
"fieldname": "id",
"fieldtype": "Data",
"label": "ID",
"unique": 1
},
{
"fieldname": "account_id",
"fieldtype": "Data",
"label": "Account ID"
},
{
"fieldname": "column_break_eteo",
"fieldtype": "Column Break"
},
{
"fieldname": "access_token",
"fieldtype": "Small Text",
"label": "Access Token"
},
{
"fieldname": "page_name",
"fieldtype": "Data",
"label": "Page Name"
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Facebook Lead Form",
"link_fieldname": "page"
}
],
"modified": "2025-09-26 19:36:59.413214",
"modified_by": "hussain@frappe.io",
"module": "Lead Syncing",
"name": "Facebook Page",
"naming_rule": "By fieldname",
"owner": "hussain@frappe.io",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "page_name",
"track_changes": 1
}

View File

@ -0,0 +1,24 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class FacebookPage(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
access_token: DF.SmallText | None
account_id: DF.Data | None
category: DF.Data | None
id: DF.Data | None
page_name: DF.Data | None
# end: auto-generated types
pass

View File

@ -0,0 +1,22 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestFacebookPage(IntegrationTestCase):
"""
Integration tests for FacebookPage.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Failed Lead Sync Log", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,90 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-10-19 17:29:10.261307",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"type",
"column_break_dhay",
"source",
"section_break_fhot",
"lead_data",
"section_break_knec",
"traceback"
],
"fields": [
{
"default": "Failure",
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
"options": "Duplicate\nFailure",
"read_only": 1
},
{
"fieldname": "lead_data",
"fieldtype": "Code",
"label": "Lead Data",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "column_break_dhay",
"fieldtype": "Column Break"
},
{
"fieldname": "source",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Source",
"options": "Lead Sync Source",
"read_only": 1
},
{
"fieldname": "section_break_fhot",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_knec",
"fieldtype": "Section Break"
},
{
"fieldname": "traceback",
"fieldtype": "Code",
"label": "Traceback",
"read_only": 1
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-10-19 18:59:17.152547",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Failed Lead Sync Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,23 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class FailedLeadSyncLog(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
lead_data: DF.Code | None
source: DF.Link | None
traceback: DF.Code | None
type: DF.Literal["Duplicate", "Failure"]
# end: auto-generated types
pass

View File

@ -0,0 +1,22 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestFailedLeadSyncLog(IntegrationTestCase):
"""
Integration tests for FailedLeadSyncLog.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -0,0 +1,214 @@
import frappe
from frappe.exceptions import ValidationError
from frappe.integrations.utils import make_get_request
FB_GRAPH_API_BASE = "https://graph.facebook.com"
FB_GRAPH_API_VERSION = "v23.0"
class DuplicateLeadError(ValidationError):
pass
def get_fb_graph_api_url(endpoint: str) -> str:
if endpoint.startswith("/"):
endpoint = endpoint[1:]
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
class FacebookSyncSource:
def __init__(
self,
access_token: str,
form_id: str,
source_name: str | None = None,
):
self.access_token = access_token
self.form_id = form_id
self.source_name = source_name
def get_api_url(self, endpoint: str) -> str:
return get_fb_graph_api_url(endpoint)
def sync(self):
leads = self.fetch_leads()
question_to_field_map = self.get_form_questions_mapping()
for lead in leads:
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
crm_lead_data = {
question_to_field_map.get(k): v for k, v in lead_data.items() if k in question_to_field_map
}
crm_lead_data["source"] = "Facebook"
crm_lead_data["facebook_lead_id"] = lead["id"]
crm_lead_data["facebook_form_id"] = self.form_id
try:
self.validate_duplicate_lead(crm_lead_data, question_to_field_map)
frappe.get_doc(
{
"doctype": "CRM Lead",
**crm_lead_data,
}
).insert(ignore_permissions=True)
except (frappe.UniqueValidationError, DuplicateLeadError):
self.create_failure_log(lead, "Duplicate")
except Exception:
self.create_failure_log(lead, traceback=frappe.get_traceback(with_context=True))
self.update_last_synced_at()
def fetch_leads(self):
url = self.get_api_url(f"/{self.form_id}/leads")
if self.last_synced_at:
timestamp = frappe.utils.data.get_timestamp(self.last_synced_at)
filtering = (
f"filtering=[{{'field':'time_created','operator':'GREATER_THAN','value':{timestamp}}}]"
)
url = f"{url}?{filtering}"
return make_get_request(
url,
params={
"access_token": self.access_token,
"fields": "id,created_time,field_data",
"limit": 100000, # TODO: pagination
},
).get("data", [])
def get_form_questions_mapping(self):
form_questions = frappe.db.get_all(
"Facebook Lead Form Question",
filters={"parent": self.form_id},
fields=["key", "mapped_to_crm_field"],
)
return {q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]}
@property
def last_synced_at(self):
return frappe.db.get_value(
"Lead Sync Source", self.source_name or {"facebook_lead_form": self.form_id}, "last_synced_at"
)
def create_failure_log(
self, lead_data: dict | None = None, type: str = "Failure", traceback: str | None = None
):
return frappe.get_doc(
{
"doctype": "Failed Lead Sync Log",
"type": type,
"lead_data": frappe.as_json(lead_data),
"source": self.get_source_name(),
"traceback": traceback,
}
).insert(ignore_permissions=True)
def update_last_synced_at(self):
frappe.db.set_value(
"Lead Sync Source",
self.source_name or {"facebook_lead_form": self.form_id},
"last_synced_at",
frappe.utils.now(),
)
def get_source_name(self):
if self.source_name:
return self.source_name
return frappe.db.get_value("Lead Sync Source", {"facebook_lead_form": self.form_id}, "name")
def validate_duplicate_lead(self, lead_data: dict, field_mapping: dict):
validation_filters = {crm_field: lead_data[crm_field] for crm_field in field_mapping.values()}
validation_filters["facebook_form_id"] = lead_data["facebook_form_id"] # only for this campaign
if frappe.db.exists("CRM Lead", validation_filters):
raise DuplicateLeadError
@frappe.whitelist()
def fetch_and_store_pages_from_facebook(access_token: str) -> list[dict]:
if not access_token:
frappe.throw(frappe._("Access token is required"))
account_details = get_fb_account_details(access_token)
if not account_details.get("id"):
frappe.throw(frappe._("Invalid access token provided for Facebook."))
url = get_fb_graph_api_url("/me/accounts")
pages = make_get_request(url, params={"access_token": access_token}).get("data", [])
for page in pages:
page_id = page["id"]
already_synced = frappe.db.exists("Facebook Page", page_id)
if not already_synced:
create_facebook_page_in_db(page, account_details)
forms = fetch_and_store_leadgen_forms_from_facebook(page_id, page["access_token"])
page["forms"] = forms
return pages
def get_fb_account_details(access_token: str) -> dict:
url = get_fb_graph_api_url("me")
try:
response = make_get_request(url, params={"access_token": access_token})
except Exception as _:
frappe.throw(frappe._("Please check your access token"))
return response
def create_facebook_page_in_db(page: dict, account_details: dict) -> None:
frappe.get_doc(
{
"doctype": "Facebook Page",
"page_name": page["name"],
"id": page["id"],
"category": page["category"],
"access_token": page["access_token"],
"account_id": account_details["id"],
}
).insert(ignore_permissions=True)
def fetch_and_store_leadgen_forms_from_facebook(page_id: str, page_access_token: str) -> list[dict]:
fields = "id,name,questions"
url = get_fb_graph_api_url(f"/{page_id}/leadgen_forms")
forms = make_get_request(
url,
params={
"access_token": page_access_token,
"fields": fields,
"limit": 15000,
},
).get("data", [])
for form in forms:
form_id = form["id"]
already_synced = frappe.db.exists("Facebook Lead Form", form_id)
if already_synced:
continue
create_facebook_lead_form_in_db(form, page_id)
return forms
def create_facebook_lead_form_in_db(form: dict, page_id: str) -> None:
form_doc = frappe.get_doc(
{
"doctype": "Facebook Lead Form",
"form_name": form["name"],
"id": form["id"],
"page": page_id,
"questions": form["questions"],
}
)
form_doc.insert(ignore_permissions=True)
@frappe.whitelist()
def get_pages_with_forms() -> list[dict]:
pages = frappe.db.get_all("Facebook Page", fields=["id", "name"])
for page in pages:
forms = frappe.db.get_all("Facebook Lead Form", filters={"page": page["id"]}, fields=["id", "name"])
page["forms"] = forms
return pages

View File

@ -0,0 +1,12 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Lead Sync Source", {
refresh(frm) {
frm.add_custom_button(__('Sync Now'), () => {
frm.call("sync_leads").then(() => {
frappe.msgprint(__('Lead sync initiated.'));
});
});
},
});

View File

@ -0,0 +1,118 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "prompt",
"creation": "2025-09-26 18:51:41.145560",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"type",
"access_token",
"column_break_lwcw",
"last_synced_at",
"enabled",
"background_sync_frequency",
"facebook_section",
"facebook_page",
"column_break_zukm",
"facebook_lead_form"
],
"fields": [
{
"default": "Facebook",
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Facebook",
"reqd": 1
},
{
"fieldname": "column_break_lwcw",
"fieldtype": "Column Break"
},
{
"fieldname": "last_synced_at",
"fieldtype": "Datetime",
"label": "Last Synced At",
"read_only": 1
},
{
"fieldname": "access_token",
"fieldtype": "Password",
"label": "Access Token",
"length": 500,
"reqd": 1
},
{
"fieldname": "facebook_page",
"fieldtype": "Link",
"label": "Facebook Page",
"options": "Facebook Page"
},
{
"fieldname": "facebook_lead_form",
"fieldtype": "Link",
"label": "Facebook Lead Form",
"options": "Facebook Lead Form",
"unique": 1
},
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled?"
},
{
"depends_on": "eval:doc.type===\"Facebook\"",
"fieldname": "facebook_section",
"fieldtype": "Section Break",
"label": "Facebook"
},
{
"fieldname": "column_break_zukm",
"fieldtype": "Column Break"
},
{
"default": "Hourly",
"fieldname": "background_sync_frequency",
"fieldtype": "Select",
"label": "Background Sync Frequency",
"options": "Every 5 Minutes\nEvery 10 Minutes\nEvery 15 Minutes\nHourly\nDaily\nMonthly",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Failed Lead Sync Log",
"link_fieldname": "source"
}
],
"modified": "2025-10-19 18:57:54.288252",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Lead Sync Source",
"naming_rule": "Set by user",
"owner": "hussain@frappe.io",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,67 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from crm.lead_syncing.doctype.lead_sync_source.facebook import (
FacebookSyncSource,
fetch_and_store_pages_from_facebook,
)
class LeadSyncSource(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
access_token: DF.Password
background_sync_frequency: DF.Literal["Every 5 Minutes", "Every 10 Minutes", "Every 15 Minutes", "Hourly", "Daily", "Monthly"]
enabled: DF.Check
facebook_lead_form: DF.Link | None
facebook_page: DF.Link | None
last_synced_at: DF.Datetime | None
type: DF.Literal["Facebook"]
# end: auto-generated types
def validate(self):
self.validate_same_fb_form_active()
def validate_same_fb_form_active(self):
if not self.enabled:
return
if not self.facebook_lead_form:
return
already_active = frappe.db.exists(
"Lead Sync Source",
{"enabled": 1, "facebook_lead_form": self.facebook_lead_form, "name": ["!=", self.name]},
)
if already_active:
frappe.throw(frappe._("A lead sync source is already enabled for this Facebook Lead Form!"))
def before_insert(self):
if self.type == "Facebook" and self.access_token:
fetch_and_store_pages_from_facebook(self.access_token)
# rest of the source types can be added here
@frappe.whitelist()
def sync_leads(self):
self._sync_leads()
# frappe.enqueue_doc(self.doctype, self.name, "_sync_leads", queue="long")
def _sync_leads(self):
if self.type == "Facebook" and self.access_token:
if not self.facebook_lead_form:
frappe.throw(frappe._("Please select a lead gen form before syncing!"))
FacebookSyncSource(
self.get_password("access_token"),
self.facebook_lead_form
).sync()

View File

@ -0,0 +1,22 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestLeadSyncSource(IntegrationTestCase):
"""
Integration tests for LeadSyncSource.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -1 +1,2 @@
FCRM
FCRM
Lead Syncing

32
crm/tasks.py Normal file
View File

@ -0,0 +1,32 @@
import frappe
def sync_leads_from_all_enabled_sources(frequency: str | None = None) -> None:
enabled_sources = frappe.get_all(
"Lead Sync Source", filters={"enabled": 1, "background_sync_frequency": frequency}, pluck="name"
)
for source in enabled_sources:
lead_sync_source = frappe.get_cached_doc("Lead Sync Source", source)
try:
lead_sync_source._sync_leads()
except Exception as _:
frappe.log_error(f"Error syncing leads for source {source}")
def sync_leads_from_sources_5_minutes() -> None:
sync_leads_from_all_enabled_sources("Every 5 Minutes")
def sync_leads_from_sources_10_minutes() -> None:
sync_leads_from_all_enabled_sources("Every 10 Minutes")
def sync_leads_from_sources_15_minutes() -> None:
sync_leads_from_all_enabled_sources("Every 15 Minutes")
def sync_leads_from_sources_hourly() -> None:
sync_leads_from_all_enabled_sources("Hourly")
def sync_leads_from_sources_daily() -> None:
sync_leads_from_all_enabled_sources("Daily")
def sync_leads_from_sources_monthly() -> None:
sync_leads_from_all_enabled_sources("Monthly")

View File

@ -0,0 +1,24 @@
<p>
{{_("Hello")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},
</p>
{% set site_link = "<a href='" + site_url + "'>" + site_url + "</a>" %}
<p>{{_("A new account has been created for you at {0}").format(site_link)}}.</p>
<p>{{_("Your login id is")}}: <b>{{ user }}</b>
<p>{{_("Click on the link below to complete your registration and set a new password")}}.</p>
<p style="margin: 15px 0px;">
<a href="{{ link }}" rel="nofollow" class="btn btn-primary">{{ _("Complete Registration") }}</a>
</p>
{% if created_by != "Administrator" %}
<br>
<p style="margin-top: 15px">
{{_("Thanks")}},<br>
{{ created_by }}
</p>
{% endif %}
<br>
<p>
{{_("You can also copy-paste following link in your browser")}}<br>
<a href="{{ link }}">{{ link }}</a>
</p>

View File

@ -23,18 +23,30 @@ declare module 'vue' {
AppSidebar: typeof import('./src/components/Layouts/AppSidebar.vue')['default']
ArrowUpRightIcon: typeof import('./src/components/Icons/ArrowUpRightIcon.vue')['default']
AscendingIcon: typeof import('./src/components/Icons/AscendingIcon.vue')['default']
AssigneeRules: typeof import('./src/components/Settings/AssignmentRules/AssigneeRules.vue')['default']
AssigneeSearch: typeof import('./src/components/Settings/AssignmentRules/AssigneeSearch.vue')['default']
AssignmentModal: typeof import('./src/components/Modals/AssignmentModal.vue')['default']
AssignmentRuleListItem: typeof import('./src/components/Settings/AssignmentRules/AssignmentRuleListItem.vue')['default']
AssignmentRulePage: typeof import('./src/components/Settings/AssignmentRules/AssignmentRulePage.vue')['default']
AssignmentRules: typeof import('./src/components/Settings/AssignmentRules/AssignmentRules.vue')['default']
AssignmentRulesList: typeof import('./src/components/Settings/AssignmentRules/AssignmentRulesList.vue')['default']
AssignmentRulesSection: typeof import('./src/components/Settings/AssignmentRules/AssignmentRulesSection.vue')['default']
AssignmentRuleView: typeof import('./src/components/Settings/AssignmentRules/AssignmentRuleView.vue')['default']
AssignmentSchedule: typeof import('./src/components/Settings/AssignmentRules/AssignmentSchedule.vue')['default']
AssignmentScheduleItem: typeof import('./src/components/Settings/AssignmentRules/AssignmentScheduleItem.vue')['default']
AssignTo: typeof import('./src/components/AssignTo.vue')['default']
AssignToBody: typeof import('./src/components/AssignToBody.vue')['default']
AttachmentArea: typeof import('./src/components/Activities/AttachmentArea.vue')['default']
AttachmentIcon: typeof import('./src/components/Icons/AttachmentIcon.vue')['default']
AttachmentItem: typeof import('./src/components/AttachmentItem.vue')['default']
Attendee: typeof import('./src/components/Calendar/Attendee.vue')['default']
AudioPlayer: typeof import('./src/components/Activities/AudioPlayer.vue')['default']
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/BrandSettings.vue')['default']
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
CalendarEventPanel: typeof import('./src/components/Calendar/CalendarEventPanel.vue')['default']
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
CallLogDetailModal: typeof import('./src/components/Modals/CallLogDetailModal.vue')['default']
@ -43,6 +55,8 @@ declare module 'vue' {
CallUI: typeof import('./src/components/Telephony/CallUI.vue')['default']
CameraIcon: typeof import('./src/components/Icons/CameraIcon.vue')['default']
CertificateIcon: typeof import('./src/components/Icons/CertificateIcon.vue')['default']
CFCondition: typeof import('./src/components/ConditionsFilter/CFCondition.vue')['default']
CFConditions: typeof import('./src/components/ConditionsFilter/CFConditions.vue')['default']
ChangePasswordModal: typeof import('./src/components/Modals/ChangePasswordModal.vue')['default']
CheckCircleIcon: typeof import('./src/components/Icons/CheckCircleIcon.vue')['default']
CheckIcon: typeof import('./src/components/Icons/CheckIcon.vue')['default']
@ -75,6 +89,7 @@ declare module 'vue' {
DealsListView: typeof import('./src/components/ListViews/DealsListView.vue')['default']
DeclinedCallIcon: typeof import('./src/components/Icons/DeclinedCallIcon.vue')['default']
DeleteLinkedDocModal: typeof import('./src/components/DeleteLinkedDocModal.vue')['default']
DescriptionIcon: typeof import('./src/components/Icons/DescriptionIcon.vue')['default']
DesendingIcon: typeof import('./src/components/Icons/DesendingIcon.vue')['default']
DesktopLayout: typeof import('./src/components/Layouts/DesktopLayout.vue')['default']
DetailsIcon: typeof import('./src/components/Icons/DetailsIcon.vue')['default']
@ -84,11 +99,11 @@ declare module 'vue' {
DoubleCheckIcon: typeof import('./src/components/Icons/DoubleCheckIcon.vue')['default']
DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default']
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default']
Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default']
DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default']
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
EditIcon: typeof import('./src/components/Icons/EditIcon.vue')['default']
EditLeadSyncSource: typeof import('./src/components/Settings/LeadSyncing/EditLeadSyncSource.vue')['default']
EditValueModal: typeof import('./src/components/Modals/EditValueModal.vue')['default']
Email2Icon: typeof import('./src/components/Icons/Email2Icon.vue')['default']
EmailAccountCard: typeof import('./src/components/Settings/EmailAccountCard.vue')['default']
@ -101,6 +116,7 @@ declare module 'vue' {
EmailEdit: typeof import('./src/components/Settings/EmailEdit.vue')['default']
EmailEditor: typeof import('./src/components/EmailEditor.vue')['default']
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
EmailMultiSelect: typeof import('./src/components/Controls/EmailMultiSelect.vue')['default']
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
EmailTemplateIcon: typeof import('./src/components/Icons/EmailTemplateIcon.vue')['default']
EmailTemplatePage: typeof import('./src/components/Settings/EmailTemplate/EmailTemplatePage.vue')['default']
@ -109,9 +125,13 @@ declare module 'vue' {
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
ErrorPage: typeof import('./src/components/ErrorPage.vue')['default']
EventArea: typeof import('./src/components/Activities/EventArea.vue')['default']
EventIcon: typeof import('./src/components/Icons/EventIcon.vue')['default']
EventModal: typeof import('./src/components/Modals/EventModal.vue')['default']
ExotelCallUI: typeof import('./src/components/Telephony/ExotelCallUI.vue')['default']
ExportIcon: typeof import('./src/components/Icons/ExportIcon.vue')['default']
ExternalLinkIcon: typeof import('./src/components/Icons/ExternalLinkIcon.vue')['default']
FacebookIcon: typeof import('./src/components/Icons/FacebookIcon.vue')['default']
FadedScrollableDiv: typeof import('./src/components/FadedScrollableDiv.vue')['default']
Field: typeof import('./src/components/FieldLayout/Field.vue')['default']
FieldLayout: typeof import('./src/components/FieldLayout/FieldLayout.vue')['default']
@ -140,6 +160,8 @@ declare module 'vue' {
GroupBy: typeof import('./src/components/GroupBy.vue')['default']
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
HelpdeskIcon: typeof import('./src/components/Icons/HelpdeskIcon.vue')['default']
HelpdeskSettings: typeof import('./src/components/Settings/HelpdeskSettings.vue')['default']
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
HomeActions: typeof import('./src/components/Settings/HomeActions.vue')['default']
Icon: typeof import('./src/components/Icon.vue')['default']
@ -158,6 +180,10 @@ declare module 'vue' {
LeadModal: typeof import('./src/components/Modals/LeadModal.vue')['default']
LeadsIcon: typeof import('./src/components/Icons/LeadsIcon.vue')['default']
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
LeadSyncSettings: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSettings.vue')['default']
LeadSyncSourceForm: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSourceForm.vue')['default']
LeadSyncSourcePage: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSourcePage.vue')['default']
LeadSyncSources: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSources.vue')['default']
LightningIcon: typeof import('./src/components/Icons/LightningIcon.vue')['default']
Link: typeof import('./src/components/Controls/Link.vue')['default']
LinkedDocsListView: typeof import('./src/components/ListViews/LinkedDocsListView.vue')['default']
@ -168,6 +194,10 @@ declare module 'vue' {
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
LucideChevronLeft: typeof import('~icons/lucide/chevron-left')['default']
LucideCopy: typeof import('~icons/lucide/copy')['default']
LucideTrash2: typeof import('~icons/lucide/trash2')['default']
LucideX: typeof import('~icons/lucide/x')['default']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
@ -179,10 +209,9 @@ declare module 'vue' {
MoneyIcon: typeof import('./src/components/Icons/MoneyIcon.vue')['default']
MultiActionButton: typeof import('./src/components/MultiActionButton.vue')['default']
MultipleAvatar: typeof import('./src/components/MultipleAvatar.vue')['default']
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.vue')['default']
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
NewEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/NewEmailTemplate.vue')['default']
NewLeadSyncSource: typeof import('./src/components/Settings/LeadSyncing/NewLeadSyncSource.vue')['default']
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
NoteIcon: typeof import('./src/components/Icons/NoteIcon.vue')['default']
NoteModal: typeof import('./src/components/Modals/NoteModal.vue')['default']
@ -194,12 +223,12 @@ declare module 'vue' {
OutboundCallIcon: typeof import('./src/components/Icons/OutboundCallIcon.vue')['default']
Password: typeof import('./src/components/Controls/Password.vue')['default']
PauseIcon: typeof import('./src/components/Icons/PauseIcon.vue')['default']
PeopleIcon: typeof import('./src/components/Icons/PeopleIcon.vue')['default']
PhoneIcon: typeof import('./src/components/Icons/PhoneIcon.vue')['default']
PinIcon: typeof import('./src/components/Icons/PinIcon.vue')['default']
PlaybackSpeedIcon: typeof import('./src/components/Icons/PlaybackSpeedIcon.vue')['default']
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
PrimaryDropdown: typeof import('./src/components/PrimaryDropdown.vue')['default']
PrimaryDropdownItem: typeof import('./src/components/PrimaryDropdownItem.vue')['default']
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
@ -219,7 +248,9 @@ declare module 'vue' {
SelectIcon: typeof import('./src/components/Icons/SelectIcon.vue')['default']
Settings: typeof import('./src/components/Settings/Settings.vue')['default']
SettingsIcon: typeof import('./src/components/Icons/SettingsIcon.vue')['default']
SettingsIcon2: typeof import('./src/components/Icons/SettingsIcon2.vue')['default']
SettingsPage: typeof import('./src/components/Settings/SettingsPage.vue')['default']
ShortcutTooltip: typeof import('./src/components/ShortcutTooltip.vue')['default']
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
SidePanelLayout: typeof import('./src/components/SidePanelLayout.vue')['default']
SidePanelLayoutEditor: typeof import('./src/components/SidePanelLayoutEditor.vue')['default']

View File

@ -1,6 +1,6 @@
<template>
<FrappeUIProvider>
<Layout v-if="session().isLoggedIn">
<Layout class="isolate" v-if="session().isLoggedIn">
<router-view :key="$route.fullPath"/>
</Layout>
<Dialogs />
@ -10,9 +10,8 @@
<script setup>
import { Dialogs } from '@/utils/dialogs'
import { sessionStore as session } from '@/stores/session'
import { setTheme } from '@/stores/theme'
import { FrappeUIProvider, setConfig } from 'frappe-ui'
import { computed, defineAsyncComponent, onMounted } from 'vue'
import { computed, defineAsyncComponent } from 'vue'
const MobileLayout = defineAsyncComponent(
() => import('./components/Layouts/MobileLayout.vue'),
@ -28,8 +27,6 @@ const Layout = computed(() => {
}
})
onMounted(() => setTheme())
setConfig('systemTimezone', window.timezone?.system || null)
setConfig('localTimezone', window.timezone?.user || null)
</script>

View File

@ -21,6 +21,9 @@
<LoadingIndicator class="h-6 w-6" />
<span>{{ __('Loading...') }}</span>
</div>
<div v-else-if="title == 'Events'" class="h-full activity">
<EventArea :doctype="doctype" :docname="docname" />
</div>
<div
v-else-if="
activities?.length ||
@ -435,6 +438,7 @@
<AllModals
ref="modalRef"
v-model="all_activities"
v-model:events="events"
:doctype="doctype"
:doc="doc"
/>
@ -463,11 +467,13 @@ import UserAvatar from '@/components/UserAvatar.vue'
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue'
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import EventArea from '@/components/Activities/EventArea.vue'
import WhatsAppArea from '@/components/Activities/WhatsAppArea.vue'
import WhatsAppBox from '@/components/Activities/WhatsAppBox.vue'
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
@ -754,6 +760,9 @@ function timelineIcon(activity_type, is_lead) {
case 'comment':
icon = CommentIcon
break
case 'event':
icon = CalendarIcon
break
case 'incoming_call':
icon = InboundCallIcon
break
@ -783,7 +792,7 @@ watch([reload, reload_email], ([reload_value, reload_email_value]) => {
})
function scroll(hash) {
if (['tasks', 'notes'].includes(route.hash?.slice(1))) return
if (['tasks', 'notes', 'events'].includes(route.hash?.slice(1))) return
setTimeout(() => {
let el
if (!hash) {

View File

@ -25,6 +25,16 @@
variant="solid"
:options="callActions"
/>
<Button
v-else-if="title == 'Events'"
variant="solid"
@click="modalRef.showEvent()"
>
<template #prefix>
<EventIcon class="h-4 w-4" />
</template>
<span>{{ __('Schedule an event') }}</span>
</Button>
<Button
v-else-if="title == 'Notes'"
variant="solid"
@ -75,6 +85,7 @@
import MultiActionButton from '@/components/MultiActionButton.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue'
import CommentIcon from '@/components/Icons/CommentIcon.vue'
import EventIcon from '@/components/Icons/EventIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
@ -112,6 +123,11 @@ const defaultActions = computed(() => {
label: __('New Comment'),
onClick: () => (props.emailBox.showComment = true),
},
{
icon: h(EventIcon, { class: 'h-4 w-4' }),
label: __('Schedule an event'),
onClick: () => props.modalRef.showEvent(),
},
{
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
label: __('Log a Call'),

View File

@ -22,21 +22,36 @@
:referenceDoc="referenceDoc"
:options="{ afterInsert: () => activities.reload() }"
/>
<EventModal
v-if="showEventModal"
v-model="showEventModal"
:event="activeEvent"
:doctype="doctype"
:docname="doc?.name"
/>
</template>
<script setup>
import TaskModal from '@/components/Modals/TaskModal.vue'
import NoteModal from '@/components/Modals/NoteModal.vue'
import CallLogModal from '@/components/Modals/CallLogModal.vue'
import EventModal from '@/components/Modals/EventModal.vue'
import { showEventModal, activeEvent } from '@/composables/event'
import { call } from 'frappe-ui'
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const props = defineProps({
doctype: String,
doc: Object,
})
const activities = defineModel()
const doc = defineModel('doc')
// Event
function showEvent(e) {
showEventModal.value = true
activeEvent.value = e
}
// Tasks
const showTaskModal = ref(false)
@ -115,6 +130,7 @@ function redirect(tabName) {
}
defineExpose({
showEvent,
showTask,
deleteTask,
updateTaskStatus,

View File

@ -89,7 +89,7 @@ import VolumnHighIcon from '@/components/Icons/VolumnHighIcon.vue'
import MuteIcon from '@/components/Icons/MuteIcon.vue'
import PlaybackSpeedIcon from '@/components/Icons/PlaybackSpeedIcon.vue'
import PlaybackSpeedOption from '@/components/Activities/PlaybackSpeedOption.vue'
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
import { Dropdown } from 'frappe-ui'
import { computed, h, ref } from 'vue'
const props = defineProps({

View File

@ -0,0 +1,109 @@
<template>
<div v-if="events.length" v-for="(event, i) in events" :key="event.name">
<div
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-3 sm:px-10"
>
<div
class="z-0 relative flex justify-center before:absolute before:left-[50%] before:-z-[1] before:top-0 before:border-l before:border-outline-gray-modals"
:class="i != events.length - 1 ? 'before:h-full' : 'before:h-4'"
>
<div
class="flex h-8 w-7 items-center justify-center bg-surface-white text-ink-gray-8"
>
<CalendarIcon class="h-4 w-4" />
</div>
</div>
<div class="mb-5">
<div
class="mb-1 flex items-center justify-stretch gap-2 py-1 text-base"
>
<div class="inline-flex items-center flex-wrap gap-1 text-ink-gray-5">
<Avatar
:image="event.owner.image"
:label="event.owner.label"
size="md"
/>
<span class="font-medium text-ink-gray-8 ml-1">
{{ event.owner.label }}
</span>
<span>{{ 'has created an event' }}</span>
</div>
<div class="ml-auto whitespace-nowrap">
<Tooltip :text="formatDate(event.creation)">
<div class="text-sm text-ink-gray-5">
{{ __(timeAgo(event.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div
class="flex gap-2 border cursor-pointer border-outline-gray-modals rounded-lg bg-surface-cards px-2.5 py-2.5 text-ink-gray-9"
@click="showEvent(event)"
>
<div
class="flex w-[2px] rounded-lg"
:style="{ backgroundColor: event.color || '#30A66D' }"
/>
<div class="flex-1 flex flex-col gap-1 text-base">
<div
class="flex items-center justify-between gap-2 font-medium text-ink-gray-7"
>
<div>{{ event.subject }}</div>
<MultipleAvatar
v-if="event.participants?.length > 1"
:avatars="event.participants"
size="sm"
/>
</div>
<div
class="flex justify-between gap-2 items-center text-ink-gray-6"
>
<div>
{{
startEndTime(event.starts_on, event.ends_on, event.all_day)
}}
</div>
<div>{{ startDate(event.starts_on) }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
v-else
class="flex h-full flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-ink-gray-4"
>
<CalendarIcon class="h-10 w-10" />
<span>{{ __('No Events Scheduled') }}</span>
<Button :label="__('Schedule an Event')" @click="showEvent()" />
</div>
</template>
<script setup>
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue'
import { useEvent, showEventModal, activeEvent } from '@/composables/event'
import { formatDate, timeAgo } from '@/utils'
import { Tooltip, Avatar } from 'frappe-ui'
const props = defineProps({
doctype: {
type: String,
default: '',
},
docname: {
type: String,
default: '',
},
})
function showEvent(e = {}) {
showEventModal.value = true
activeEvent.value = e
}
const { events, startEndTime, startDate } = useEvent(
props.doctype,
props.docname,
)
</script>

View File

@ -0,0 +1,334 @@
<template>
<div>
<!-- Combobox Input -->
<div class="flex items-center w-full text-ink-gray-8 [&>div]:w-full">
<ComboboxRoot
:model-value="tempSelection"
:open="showOptions"
@update:open="(o) => (showOptions = o)"
@update:modelValue="onSelect"
:ignore-filter="true"
>
<ComboboxAnchor
class="flex w-full text-base items-center gap-1 rounded border border-outline-gray-2 bg-surface-white hover:border-outline-gray-3 focus:border-outline-gray-4 focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 px-2 py-1"
:class="[size === 'sm' ? 'h-7' : 'h-8 ', inputClass]"
@click="showOptions = true"
>
<ComboboxInput
ref="search"
autocomplete="off"
class="bg-transparent p-0 outline-none border-0 text-base text-ink-gray-8 h-full placeholder:text-ink-gray-4 w-full focus:outline-none focus:ring-0 focus:border-0"
:placeholder="placeholder"
:value="query"
@input="onInput"
@keydown.enter.prevent="handleEnter"
@keydown.escape.stop="showOptions = false"
/>
<FeatherIcon
name="chevron-down"
class="h-4 text-ink-gray-5 cursor-pointer"
@click.stop="showOptions = !showOptions"
/>
</ComboboxAnchor>
<ComboboxPortal>
<ComboboxContent
class="z-10 mt-1 min-w-48 w-full max-w-md bg-surface-modal overflow-hidden rounded-lg shadow-2xl ring-1 ring-black ring-opacity-5"
position="popper"
:align="'start'"
@openAutoFocus.prevent
@closeAutoFocus.prevent
>
<ComboboxViewport class="max-h-60 overflow-auto p-1.5">
<ComboboxEmpty
class="flex gap-2 rounded px-2 py-1 text-base text-ink-gray-5"
>
<FeatherIcon v-if="fetchContacts" name="search" class="h-4" />
{{ emptyStateText }}
</ComboboxEmpty>
<ComboboxItem
v-for="option in options"
:key="option.value"
:value="option.value"
class="text-base leading-none text-ink-gray-7 rounded flex items-center px-2 py-1 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-surface-gray-3 cursor-pointer"
@mousedown.prevent="onSelect(option.value, option)"
>
<UserAvatar class="mr-2" :user="option.value" size="lg" />
<div class="flex flex-col gap-1 p-1 text-ink-gray-8">
<div class="text-base font-medium">{{ option.label }}</div>
<div class="text-sm text-ink-gray-5">{{ option.value }}</div>
</div>
</ComboboxItem>
</ComboboxViewport>
</ComboboxContent>
</ComboboxPortal>
</ComboboxRoot>
</div>
<!-- Selected Attendees -->
<div
v-if="values.length"
class="flex flex-col gap-2 mt-2 max-h-[165px] overflow-y-auto"
ref="optionsRef"
>
<Button
ref="emails"
v-for="att in values"
:key="att.email"
:label="att.email"
theme="gray"
class="rounded-full w-fit"
:tooltip="getTooltip(att.email)"
>
<template #prefix>
<UserAvatar :user="att.email" class="-ml-1 !size-5.5" />
</template>
<template #suffix>
<FeatherIcon
class="h-3.5"
name="x"
@click.stop="removeValue(att.email)"
/>
</template>
</Button>
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
</div>
</div>
</template>
<script setup>
import UserAvatar from '@/components/UserAvatar.vue'
import { createResource } from 'frappe-ui'
import {
ComboboxRoot,
ComboboxAnchor,
ComboboxInput,
ComboboxPortal,
ComboboxContent,
ComboboxViewport,
ComboboxItem,
ComboboxEmpty,
} from 'reka-ui'
import { ref, computed, nextTick } from 'vue'
import { watchDebounced } from '@vueuse/core'
const props = defineProps({
validate: {
type: Function,
default: null,
},
variant: {
type: String,
default: 'subtle',
},
size: {
type: String,
default: 'sm',
},
placeholder: {
type: String,
default: 'Add attendee',
},
inputClass: {
type: String,
default: '',
},
errorMessage: {
type: Function,
default: (value) => `${value} is an Invalid value`,
},
fetchContacts: {
type: Boolean,
default: true,
},
existingEmails: {
type: Array,
default: () => [],
},
})
const values = defineModel()
const emails = ref([])
const search = ref(null)
const error = ref(null)
const info = ref(null)
const query = ref('')
const text = ref('')
const showOptions = ref(false)
const optionsRef = ref(null)
const tempSelection = ref(null)
const metaByEmail = computed(() => {
const out = {}
const source = values.value || []
for (const a of source) {
if (a?.email) out[a.email] = a
}
return out
})
function getTooltip(email) {
const m = metaByEmail.value[email]
if (!m) return email
const parts = []
if (m.reference_doctype) parts.push(m.reference_doctype)
if (m.reference_docname) parts.push(m.reference_docname)
return parts.length ? parts.join(': ') : email
}
watchDebounced(
query,
(val) => {
val = val || ''
if (text.value === val && options.value?.length) return
text.value = val
reload(val)
},
{ debounce: 300, immediate: true },
)
const filterOptions = createResource({
url: 'crm.api.contact.search_emails',
method: 'POST',
cache: [text.value, 'Contact'],
params: { txt: text.value },
transform: (data) => {
let allData = data.map((option) => {
let fullName = option[0]
let email = option[1]
let name = option[2]
return {
label: fullName || name || email,
name: name,
value: email,
}
})
// Filter out existing emails
if (props.existingEmails?.length) {
allData = allData.filter((option) => {
return !props.existingEmails.includes(option.value)
})
}
return allData
},
})
const options = computed(() => {
let searchedContacts = props.fetchContacts ? filterOptions.data : []
if (!searchedContacts?.length && query.value) {
searchedContacts.push({
name: 'new',
label: query.value,
value: query.value,
})
}
return searchedContacts || []
})
const emptyStateText = computed(() =>
props.fetchContacts
? __('No results found')
: __('Type an email address to add attendee'),
)
function reload(val) {
if (!props.fetchContacts) return
filterOptions.update({
params: { txt: val },
})
filterOptions.reload()
}
function onSelect(val, fullOption = null) {
if (!val) return
const optionObj = fullOption ||
options.value.find((o) => o.value === val) || {
name: 'new',
label: val,
value: val,
}
addValue(optionObj)
if (!error.value) {
query.value = ''
tempSelection.value = null
showOptions.value = false
nextTick(() => setFocus())
}
}
function handleEnter() {
if (query.value) {
onSelect(query.value, {
name: 'new',
label: query.value,
value: query.value,
})
}
}
function onInput(e) {
query.value = e.target.value
showOptions.value = true
}
const addValue = (option) => {
// Safeguard for falsy option
if (!option || !option.value) return
error.value = null
info.value = null
const current = Array.isArray(values.value) ? values.value.slice() : []
const existing = new Set(current.map((a) => a.email))
const raw = option.value || ''
const parts = raw.split(',')
const hasMultiple = parts.length > 1
for (let p of parts) {
p = p.trim()
if (!p) continue
if (existing.has(p)) {
info.value = __('email already exists')
continue
}
if (props.validate && !props.validate(p)) {
error.value = props.errorMessage(p)
query.value = p
continue
}
existing.add(p)
const entry = { email: p }
if (option.name && !hasMultiple) {
entry.reference_docname = option.name
}
current.push(entry)
}
values.value = current
// Scroll to the bottom so the last added value is visible
nextTick(() => {
// use requestAnimationFrame to ensure DOM paint
requestAnimationFrame(() => {
const el = optionsRef.value
if (el) {
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
}
})
})
}
const removeValue = (email) => {
values.value = (values.value || []).filter((a) => a.email !== email)
}
function setFocus() {
search.value?.focus?.()
}
defineExpose({ setFocus })
</script>

View File

@ -0,0 +1,719 @@
<template>
<div v-if="show" class="flex flex-col w-[352px] text-base">
<!-- Event Header -->
<div
class="flex items-center justify-between p-4.5 text-ink-gray-7 text-lg font-medium"
>
<div
class="flex items-center gap-x-2"
:class="mode == 'edit' && 'cursor-pointer hover:text-ink-gray-8'"
@click="mode == 'edit' && details()"
>
<LucideChevronLeft v-if="mode == 'edit'" class="size-4" />
{{ __(title) }}
</div>
<div class="flex items-center gap-x-1">
<ShortcutTooltip
v-if="mode == 'details'"
:label="__('Edit event')"
combo="Enter"
>
<Button :icon="EditIcon" variant="ghost" @click="editDetails" />
</ShortcutTooltip>
<ShortcutTooltip
v-if="mode === 'edit' || mode === 'details'"
:label="__('Delete event')"
combo="Delete"
:alt-combos="['Backspace']"
>
<Button icon="trash-2" variant="ghost" @click="deleteEvent" />
</ShortcutTooltip>
<ShortcutTooltip
v-if="mode === 'edit' || mode === 'details'"
:label="__('Duplicate event')"
combo="Mod+D"
>
<Button icon="copy" variant="ghost" @click="duplicateEvent" />
</ShortcutTooltip>
<ShortcutTooltip :label="__('Close panel')" combo="Esc">
<Button icon="x" variant="ghost" @click="close" />
</ShortcutTooltip>
</div>
</div>
<!-- Event Details -->
<div v-if="mode == 'details'" class="flex flex-col overflow-y-auto">
<div
class="flex items-start gap-2 px-4.5 py-3 pb-0"
@dblclick="editDetails"
>
<div
class="mx-0.5 my-[5px] size-2.5 rounded-full cursor-pointer"
:style="{
backgroundColor: _event.color || '#30A66D',
}"
/>
<div class="flex flex-col gap-[3px]">
<div class="text-ink-gray-8 font-semibold text-xl">
{{ _event.title || __('(No title)') }}
</div>
<div class="text-ink-gray-6 text-p-base">{{ formattedDateTime }}</div>
</div>
</div>
<div
v-if="_event.referenceDocname"
class="mx-4.5 my-2.5 border-t border-outline-gray-1"
/>
<div
v-if="_event.referenceDocname"
class="flex items-center px-4.5 py-1 text-ink-gray-7"
>
<component
:is="_event.referenceDoctype == 'CRM Lead' ? LeadsIcon : DealsIcon"
class="size-4"
/>
<Link
class="[&_button]:bg-surface-white [&_button]:select-text [&_button]:text-ink-gray-7 [&_button]:cursor-text"
v-model="_event.referenceDocname"
:doctype="_event.referenceDoctype"
:disabled="true"
/>
<Button variant="ghost" @click="redirect">
<template #icon>
<ArrowUpRightIcon class="size-4 text-ink-gray-7" />
</template>
</Button>
</div>
<div
v-if="peoples.length"
class="mx-4.5 my-2.5 border-t border-outline-gray-1"
/>
<div v-if="peoples.length" class="px-4.5 py-2">
<div class="flex gap-3 text-ink-gray-7 mb-3">
<PeopleIcon class="size-4" />
<div>{{ __('{0} Attendees', [peoples.length + 1]) }}</div>
</div>
<div class="flex flex-col gap-2 -ml-1">
<Button
:key="_event.owner"
variant="ghost"
theme="gray"
class="rounded-full w-fit !h-8.5 !pr-3"
:tooltip="__('Owner: {0}', [_event.owner?.label])"
>
<template #default>
<div class="flex flex-col justify-start items-start text-sm">
<div>{{ _event.owner?.label }}</div>
<div class="text-ink-gray-5">{{ __('Organizer') }}</div>
</div>
</template>
<template #prefix>
<UserAvatar :user="_event.owner?.value" class="-ml-1 !size-5" />
</template>
</Button>
<Button
v-for="att in displayedPeoples"
:key="att.email"
:label="att.email"
variant="ghost"
theme="gray"
class="rounded-full w-fit !text-sm"
:tooltip="getTooltip(att)"
>
<template #prefix>
<UserAvatar :user="att.email" class="-ml-1 !size-5" />
</template>
</Button>
<Button
v-if="!showAllParticipants && peoples.length > 2"
variant="ghost"
:label="__('See all participants')"
iconLeft="more-horizontal"
class="!justify-start w-fit"
@click="showAllParticipants = true"
/>
<Button
v-else-if="showAllParticipants"
variant="ghost"
:label="__('Show less')"
iconLeft="chevron-up"
class="!justify-start w-fit"
@click="showAllParticipants = false"
/>
</div>
</div>
<div
v-if="_event.description && _event.description !== '<p></p>'"
class="mx-4.5 my-2.5 border-t border-outline-gray-1"
/>
<div v-if="_event.description && _event.description !== '<p></p>'">
<div class="flex gap-2 items-center text-ink-gray-7 px-4.5 py-1">
<DescriptionIcon class="size-4" />
{{ __('Description') }}
</div>
<div
class="px-4.5 py-2 text-ink-gray-7 text-p-base"
v-html="_event.description"
/>
</div>
</div>
<!-- Event new, duplicate & edit -->
<div v-else class="flex flex-col overflow-y-auto">
<div class="flex gap-2 items-center px-4.5 py-3">
<Dropdown class="ml-1" :options="colors">
<div
class="flex items-center justify-center size-7 shrink-0 border border-outline-gray-2 bg-surface-white hover:border-outline-gray-3 hover:shadow-sm rounded cursor-pointer"
>
<div
class="size-2.5 rounded-full cursor-pointer"
:style="{
backgroundColor: _event.color || '#30A66D',
}"
/>
</div>
</Dropdown>
<TextInput
ref="eventTitle"
class="w-full"
variant="outline"
v-model="_event.title"
:debounce="500"
:placeholder="__('Event title')"
@change="sync"
@keyup.enter="saveEvent"
/>
</div>
<div class="flex justify-between py-2.5 px-4.5 text-ink-gray-6">
<div class="flex items-center">
<Switch v-model="_event.isFullDay" @update:model-value="sync" />
<div class="ml-2">
{{ __('All day') }}
</div>
</div>
<!-- <div class="flex items-center gap-1.5 text-ink-gray-5">
<LucideEarth class="size-4" />
{{ __('GMT+5:30') }}
</div> -->
</div>
<div
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
>
<div class="">{{ __('Date') }}</div>
<div class="flex items-center gap-x-1.5">
<DatePicker
:class="['[&_input]:w-[216px]']"
variant="outline"
:value="_event.fromDate"
:format="'MMM D, YYYY'"
:placeholder="__('May 1, 2025')"
:clearable="false"
@update:modelValue="(date) => updateDate(date, true)"
>
<template #suffix="{ togglePopover }">
<FeatherIcon
name="chevron-down"
class="h-4 w-4 cursor-pointer"
@click="togglePopover"
/>
</template>
</DatePicker>
</div>
</div>
<div
v-if="!_event.isFullDay"
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
>
<div class="w-20">{{ __('Time') }}</div>
<div class="flex items-center gap-x-1.5">
<TimePicker
v-if="!_event.isFullDay"
class="max-w-[105px]"
variant="outline"
:modelValue="_event.fromTime"
:placeholder="__('Start Time')"
@update:modelValue="(time) => updateTime(time, true)"
/>
<TimePicker
class="max-w-[105px]"
variant="outline"
:modelValue="_event.toTime"
:options="toOptions"
:placeholder="__('End Time')"
placement="bottom-end"
@update:modelValue="(time) => updateTime(time)"
/>
</div>
</div>
<div class="mx-4.5 my-2.5 border-t border-outline-gray-1" />
<div
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
>
<div class="">{{ __('Link') }}</div>
<div class="flex items-center gap-x-1.5">
<FormControl
class="w-[216px]"
type="select"
:options="[
{
label: '',
value: '',
},
{
label: __('Lead'),
value: 'CRM Lead',
},
{
label: __('Deal'),
value: 'CRM Deal',
},
]"
v-model="_event.referenceDoctype"
variant="outline"
:placeholder="__('Add Lead or Deal')"
@change="
() => {
_event.referenceDocname = ''
sync()
}
"
/>
</div>
</div>
<div
v-if="_event.referenceDoctype"
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
>
<div class="">
{{ _event.referenceDoctype == 'CRM Lead' ? __('Lead') : __('Deal') }}
</div>
<div class="flex items-center gap-x-1.5">
<Link
class="w-[220px]"
v-model="_event.referenceDocname"
:doctype="_event.referenceDoctype"
:filters="
_event.referenceDoctype === 'CRM Lead' ? { converted: 0 } : {}
"
variant="outline"
@update:model-value="sync"
/>
</div>
</div>
<div class="mx-4.5 my-2.5 border-t border-outline-gray-1" />
<Attendee
class="px-4.5 py-[7px]"
v-model="peoples"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
"
/>
<div class="mx-4.5 my-2.5 border-t border-outline-gray-1" />
<div class="px-4.5 py-3">
<div class="flex items-center gap-x-2 border rounded py-1">
<TextEditor
editor-class="!prose-sm overflow-auto min-h-[22px] max-h-32 px-2.5 rounded placeholder-ink-gray-4 focus:bg-surface-white focus:ring-0 text-ink-gray-8 transition-colors"
:bubbleMenu="true"
:content="_event.description"
@change="
(val) => {
_event.description = val
sync()
}
"
:placeholder="__('Add description')"
/>
</div>
</div>
</div>
<div v-if="mode != 'details'" class="px-4.5 py-3">
<ErrorMessage class="my-2" :message="error" />
<div class="w-full">
<Button
variant="solid"
class="w-full"
:disabled="!dirty"
:loading="
mode === 'edit' ? events.setValue.loading : events.insert.loading
"
@click="saveEvent"
>
{{
mode === 'edit'
? __('Save')
: mode === 'duplicate'
? __('Duplicate event')
: __('Create event')
}}
</Button>
</div>
</div>
</div>
</template>
<script setup>
import PeopleIcon from '@/components/Icons/PeopleIcon.vue'
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
import Link from '@/components/Controls/Link.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import DescriptionIcon from '@/components/Icons/DescriptionIcon.vue'
import { globalStore } from '@/stores/global'
import { validateEmail } from '@/utils'
import {
normalizeParticipants,
buildEndTimeOptions,
computeAutoToTime,
validateTimeRange,
parseEventDoc,
} from '@/composables/event'
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
import {
TextInput,
Switch,
DatePicker,
TimePicker,
TextEditor,
ErrorMessage,
Dropdown,
dayjs,
CalendarColorMap as colorMap,
CalendarActiveEvent as activeEvent,
createDocumentResource,
} from 'frappe-ui'
import ShortcutTooltip from '@/components/ShortcutTooltip.vue'
import { ref, computed, watch, h, inject } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({
mode: {
type: String,
default: 'details',
},
})
const emit = defineEmits([
'save',
'edit',
'delete',
'details',
'close',
'sync',
'duplicate',
])
const router = useRouter()
const { $dialog } = globalStore()
const show = defineModel()
const event = defineModel('event')
const events = inject('events')
const _event = ref({})
const peoples = computed({
get() {
return _event.value.event_participants || []
},
set(list) {
_event.value.event_participants = normalizeParticipants(list)
sync()
},
})
const title = computed(() => {
if (props.mode === 'details') return __('Event details')
if (props.mode === 'edit') return __('Editing event')
if (props.mode === 'new') return __('New event')
return __('Duplicate event')
})
const eventTitle = ref(null)
const error = ref(null)
const showAllParticipants = ref(false)
const eventResource = ref({})
const oldEvent = ref(null)
const dirty = computed(() => {
return JSON.stringify(oldEvent.value) !== JSON.stringify(_event.value)
})
const displayedPeoples = computed(() => {
if (showAllParticipants.value) return peoples.value
return peoples.value.slice(0, 2)
})
watch(
[() => props.mode, () => event.value],
([mode, event], [oldMode, oldEvent]) => {
error.value = null
focusOnTitle()
fetchEvent(oldMode)
},
{ immediate: true },
)
function fetchEvent(oldMode) {
if (
event.value.id &&
event.value.id !== 'new-event' &&
event.value.id !== 'duplicate-event'
) {
eventResource.value = createDocumentResource({
doctype: 'Event',
name: event.value.id,
fields: ['*'],
onSuccess: (data) => {
_event.value = parseEventDoc(data)
oldEvent.value = { ..._event.value }
},
})
if (eventResource.value.doc && !event.value.reloadEvent) {
_event.value = parseEventDoc(eventResource.value.doc)
oldEvent.value = { ..._event.value }
} else {
eventResource.value.reload()
}
} else {
_event.value = event.value
if (oldMode !== props.mode) {
oldEvent.value = { ...event.value }
}
if (event.value.id === 'duplicate-event' && oldMode !== 'duplicate') {
_event.value.title = _event.value.title + ' (Copy)'
}
}
showAllParticipants.value = false
}
function focusOnTitle() {
setTimeout(() => {
if (['edit', 'new', 'duplicate'].includes(props.mode)) {
eventTitle.value?.el?.focus()
}
}, 100)
}
function sync() {
emit('sync', _event.value.id, _event.value)
}
function updateDate(d) {
_event.value.fromDate = d
_event.value.toDate = d
sync()
}
function updateTime(t, fromTime = false) {
error.value = null
const prevTo = _event.value.toTime
if (fromTime) {
_event.value.fromTime = t
if (!_event.value.toTime || _event.value.toTime <= t) {
_event.value.toTime = computeAutoToTime(t)
}
} else {
_event.value.toTime = t
}
const { valid, error: err } = validateTimeRange({
fromDate: _event.value.fromDate,
fromTime: _event.value.fromTime,
toTime: _event.value.toTime,
isFullDay: _event.value.isFullDay,
})
if (!valid) {
error.value = err
_event.value.toTime = prevTo
} else {
sync()
}
}
function saveEvent() {
if (!dirty.value) return
error.value = null
if (!_event.value.title) {
error.value = __('Title is required')
eventTitle.value.el.focus()
return
}
const { valid, error: err } = validateTimeRange({
fromDate: _event.value.fromDate,
fromTime: _event.value.fromTime,
toTime: _event.value.toTime,
isFullDay: _event.value.isFullDay,
})
if (!valid) {
error.value = err
return
}
oldEvent.value = { ..._event.value }
sync()
emit('save', _event.value)
}
function editDetails() {
emit('edit', _event.value)
}
function duplicateEvent() {
if (dirty.value) {
showDiscardChangesModal(() => reset())
} else {
emit('duplicate', _event.value)
}
}
function deleteEvent() {
emit('delete', _event.value.id)
}
function details() {
if (dirty.value) {
showDiscardChangesModal(() => reset())
} else {
emit('details', _event.value)
}
}
function close() {
const _close = () => {
show.value = false
activeEvent.value = ''
emit('close', _event.value)
}
if (dirty.value) {
showDiscardChangesModal(() => {
reset()
if (['new-event', 'duplicate-event'].includes(_event.value.id)) _close()
})
} else {
_close()
}
}
function reset() {
Object.assign(_event.value, oldEvent.value)
sync()
}
function showDiscardChangesModal(action) {
$dialog({
title: __('Discard unsaved changes?'),
message: __(
'Are you sure you want to discard unsaved changes to this event?',
),
actions: [
{
label: __('Cancel'),
onClick: (close) => {
close()
},
},
{
label: __('Discard'),
variant: 'solid',
onClick: (close) => {
action()
close()
},
},
],
})
}
const formattedDateTime = computed(() => {
const date = dayjs(_event.value.fromDate)
if (_event.value.isFullDay) {
return `${__('All day')} - ${date.format('ddd, D MMM YYYY')}`
}
const start = dayjs(_event.value.fromDate + ' ' + _event.value.fromTime)
const end = dayjs(_event.value.toDate + ' ' + _event.value.toTime)
return `${start.format('h:mm a')} - ${end.format('h:mm a')} ${date.format('ddd, D MMM YYYY')}`
})
const colors = Object.keys(colorMap).map((color) => ({
label: color.charAt(0).toUpperCase() + color.slice(1),
value: colorMap[color].color,
icon: h('div', {
class: '!size-2.5 rounded-full',
style: { backgroundColor: colorMap[color].color },
}),
onClick: () => {
_event.value.color = colorMap[color].color
sync()
},
}))
function redirect() {
if (_event.value.referenceDocname) {
let name = _event.value.referenceDoctype === 'CRM Lead' ? 'Lead' : 'Deal'
let params =
_event.value.referenceDoctype == 'CRM Lead'
? { leadId: _event.value.referenceDocname }
: { dealId: _event.value.referenceDocname }
router.push({ name, params })
}
}
function getTooltip(m) {
if (!m) return email
const parts = []
if (m.reference_doctype) parts.push(m.reference_doctype)
if (m.reference_docname) parts.push(m.reference_docname)
return parts.length ? parts.join(': ') : email
}
const toOptions = computed(() => buildEndTimeOptions(_event.value.fromTime))
function updateEvent(_e) {
Object.assign(_event.value, _e)
}
defineExpose({ updateEvent })
// Keyboard shortcuts
useKeyboardShortcuts({
active: () => show.value,
shortcuts: [
{ keys: 'Escape', action: () => close() },
{
keys: 'Enter',
guard: () =>
['details', 'edit'].includes(props.mode) && props.mode === 'details',
action: () => editDetails(),
},
{
keys: ['Delete', 'Backspace'],
guard: () => ['details', 'edit'].includes(props.mode),
action: () => deleteEvent(),
},
{
match: (e) =>
['details', 'edit'].includes(props.mode) &&
(e.metaKey || e.ctrlKey) &&
!e.shiftKey &&
!e.altKey &&
e.key.toLowerCase() === 'd',
action: () => duplicateEvent(),
},
],
})
</script>

View File

@ -0,0 +1,16 @@
export function allTimeSlots() {
const out = []
for (let h = 0; h < 24; h++) {
for (const m of [0, 15, 30, 45]) {
const hh = String(h).padStart(2, '0')
const mm = String(m).padStart(2, '0')
const ampm = h >= 12 ? 'pm' : 'am'
const hour12 = h % 12 === 0 ? 12 : h % 12
out.push({
value: `${hh}:${mm}`,
label: `${hour12}:${mm} ${ampm}`,
})
}
}
return out
}

View File

@ -0,0 +1,314 @@
<template>
<div>
<div class="flex flex-wrap gap-1">
<Button
ref="emails"
v-for="value in values"
:key="value"
:label="value"
theme="gray"
variant="subtle"
:class="{
'rounded bg-surface-white hover:!bg-surface-gray-1 focus-visible:ring-outline-gray-4':
variant === 'subtle',
}"
@keydown.delete.capture.stop="removeLastValue"
>
<template #suffix>
<FeatherIcon
class="h-3.5"
name="x"
@click.stop="removeValue(value)"
/>
</template>
</Button>
<div class="flex-1">
<ComboboxRoot
:model-value="tempSelection"
:open="showOptions"
@update:open="(o) => (showOptions = o)"
@update:modelValue="onSelect"
:ignore-filter="true"
>
<ComboboxAnchor
class="flex h-7 max-w-full w-auto items-center gap-2 rounded px-2 py-1 border border-transparent"
:class="[
variant == 'ghost'
? 'bg-surface-white hover:bg-surface-white'
: 'bg-surface-gray-2 hover:bg-surface-gray-3',
inputClass,
]"
>
<ComboboxInput
ref="search"
:value="query"
autocomplete="off"
class="bg-transparent p-0 outline-none border-0 text-base text-ink-gray-8 h-full placeholder:text-ink-gray-4 w-full focus:outline-none focus:ring-0 focus:border-0"
:placeholder="placeholder"
@focus="showOptions = true"
@input="onInput"
@keydown.delete.capture.stop="removeLastValue"
@keydown.enter.prevent="handleEnter"
/>
</ComboboxAnchor>
<ComboboxPortal>
<ComboboxContent
class="z-10 mt-1 min-w-48 w-auto max-w-96 bg-surface-modal overflow-hidden rounded-lg shadow-2xl ring-1 ring-black ring-opacity-5"
position="popper"
:align="'start'"
@openAutoFocus.prevent
@closeAutoFocus.prevent
>
<ComboboxViewport class="max-h-60 overflow-auto p-1.5">
<ComboboxEmpty
class="flex gap-2 rounded px-2 py-1 text-base text-ink-gray-5"
>
<FeatherIcon
v-if="showSearchIcon"
name="search"
class="h-4"
/>
{{ emptyStateText }}
</ComboboxEmpty>
<ComboboxItem
v-for="option in options"
:key="option.value"
:value="option.value"
class="text-base leading-none text-ink-gray-7 rounded flex items-center px-2 py-1 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-surface-gray-3 cursor-pointer"
@mousedown.prevent="onSelect(option.value)"
>
<UserAvatar class="mr-2" :user="option.value" size="lg" />
<div class="flex flex-col gap-1 p-1 text-ink-gray-8">
<div class="text-base font-medium">{{ option.label }}</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
</div>
</div>
</ComboboxItem>
</ComboboxViewport>
</ComboboxContent>
</ComboboxPortal>
</ComboboxRoot>
</div>
</div>
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
<div
v-if="info"
class="whitespace-pre-line text-sm text-ink-blue-3 mt-2 pl-2"
>
{{ info }}
</div>
</div>
</template>
<script setup>
// Generic multi-source (users / contacts / free) multi-select email-like input
import UserAvatar from '@/components/UserAvatar.vue'
import { usersStore } from '@/stores/users'
import { createResource } from 'frappe-ui'
import {
ComboboxRoot,
ComboboxAnchor,
ComboboxInput,
ComboboxPortal,
ComboboxContent,
ComboboxViewport,
ComboboxItem,
ComboboxEmpty,
} from 'reka-ui'
import { ref, computed, nextTick } from 'vue'
import { watchDebounced } from '@vueuse/core'
const props = defineProps({
// Behaviour
mode: { type: String, default: null }, // 'users' | 'contacts' | 'free' (fallback to legacy flags)
fetchUsers: { type: Boolean, default: false },
fetchContacts: { type: Boolean, default: false },
existingEmails: { type: Array, default: () => [] },
validate: { type: Function, default: null },
errorMessage: {
type: Function,
default: (value) => `${value} is an Invalid value`,
},
emptyPlaceholder: { type: String, default: __('No results found') },
// UI
variant: { type: String, default: 'subtle' },
placeholder: { type: String, default: '' },
inputClass: { type: String, default: '' },
})
// v-model values
const values = defineModel()
// Determine effective mode (backwards compatibility with old components)
const effectiveMode = computed(() => {
if (props.mode) return props.mode
if (props.fetchUsers) return 'users'
if (props.fetchContacts) return 'contacts'
return 'free'
})
// Common state
const emails = ref([])
const search = ref(null)
const error = ref(null)
const info = ref(null)
const query = ref('')
const showOptions = ref(false)
const tempSelection = ref(null)
// Users data
const { users } = usersStore()
// Contacts resource (only if needed)
const filterOptions = ref(null)
const lastLoadedQuery = ref('')
if (effectiveMode.value === 'contacts') {
filterOptions.value = createResource({
url: 'crm.api.contact.search_emails',
method: 'POST',
cache: ['ContactEmails'],
params: { txt: '' },
transform: (data) => {
let allData = (data || []).map((option) => {
const fullName = option[0]
const email = option[1]
const name = option[2]
return { label: fullName || name || email, value: email }
})
if (props.existingEmails?.length) {
allData = allData.filter((o) => !props.existingEmails.includes(o.value))
}
return allData
},
})
watchDebounced(
query,
(val) => {
val = val || ''
if (lastLoadedQuery.value === val && options.value?.length) return
lastLoadedQuery.value = val
reload(val)
},
{ debounce: 300, immediate: true },
)
}
function reload(val) {
if (effectiveMode.value !== 'contacts' || !filterOptions.value) return
filterOptions.value.update({ params: { txt: val } })
filterOptions.value.reload()
}
// Options computed
const options = computed(() => {
const mode = effectiveMode.value
if (mode === 'users') {
let list = users?.data?.allUsers || []
list = list.map((u) => ({
label: u.full_name || u.name || u.email,
value: u.email,
}))
if (props.existingEmails?.length) {
list = list.filter((o) => !props.existingEmails.includes(o.value))
}
if (query.value) {
const q = query.value.toLowerCase()
list = list.filter(
(o) =>
o.label?.toLowerCase().includes(q) ||
o.value?.toLowerCase().includes(q),
)
}
return list
}
if (mode === 'contacts') {
const list = filterOptions.value?.data ? [...filterOptions.value.data] : []
if (!list.length && query.value) {
list.push({ label: query.value, value: query.value })
}
return list
}
// Free / manual mode
return query.value ? [{ label: query.value, value: query.value }] : []
})
const showSearchIcon = computed(() => effectiveMode.value !== 'free')
const emptyStateText = computed(() => {
if (effectiveMode.value === 'free') return __(props.emptyPlaceholder)
return options.value.length ? '' : __(props.emptyPlaceholder)
})
function addValue(input) {
if (!input) return
error.value = null
info.value = null
const parts = input
.split(',')
.map((p) => p.trim())
.filter(Boolean)
for (const email of parts) {
if (values.value?.includes(email)) {
info.value = __('email already exists')
continue
}
if (props.validate && !props.validate(email)) {
error.value = props.errorMessage(email)
query.value = email
break
}
if (!values.value) values.value = [email]
else values.value.push(email)
}
}
function removeValue(value) {
values.value = values.value.filter((v) => v !== value)
}
function removeLastValue() {
if (query.value) return
let emailRef = emails.value[emails.value.length - 1]?.rootRef
if (document.activeElement === emailRef) {
values.value.pop()
nextTick(() => {
if (values.value.length) {
emailRef = emails.value[emails.value.length - 1].rootRef
emailRef?.focus()
} else {
setFocus()
}
})
} else {
emailRef?.focus()
}
}
function setFocus() {
search.value?.focus?.()
}
defineExpose({ setFocus })
function onInput(e) {
query.value = e.target.value
showOptions.value = true
}
function onSelect(val) {
if (!val) return
addValue(val)
if (!error.value) {
query.value = ''
tempSelection.value = null
showOptions.value = false
nextTick(() => setFocus())
}
}
function handleEnter() {
if (query.value) onSelect(query.value)
}
</script>

View File

@ -178,21 +178,27 @@
@change="(e) => fieldChange(e.target.checked, field, row)"
/>
</div>
<TimePicker
v-else-if="field.fieldtype === 'Time'"
:value="row[field.fieldname]"
variant="outline"
:format="getFormat('', '', false, true, false)"
input-class="border-none text-sm text-ink-gray-8"
@change="(v) => fieldChange(v, field, row)"
/>
<DatePicker
v-else-if="field.fieldtype === 'Date'"
:value="row[field.fieldname]"
icon-left=""
variant="outline"
:formatter="(date) => getFormat(date, '', true)"
:format="getFormat('', '', true, false, false)"
input-class="border-none text-sm text-ink-gray-8"
@change="(v) => fieldChange(v, field, row)"
/>
<DateTimePicker
v-else-if="field.fieldtype === 'Datetime'"
:value="row[field.fieldname]"
icon-left=""
variant="outline"
:formatter="(date) => getFormat(date, '', true, true)"
:format="getFormat('', '', true, true, false)"
input-class="border-none text-sm text-ink-gray-8"
@change="(v) => fieldChange(v, field, row)"
/>
@ -265,6 +271,16 @@
:disabled="Boolean(field.read_only)"
@change="fieldChange(flt($event.target.value), field, row)"
/>
<Autocomplete
v-else-if="field.fieldtype === 'Autocomplete'"
class="text-sm text-ink-gray-8"
:modelValue="row[field.fieldname]"
@update:modelValue="(v) => row[field.fieldname] = typeof v == 'object' ? v.value : v"
@change="(v) => fieldChange(typeof v == 'object' ? v.value : v, field, row)"
:options="field.options"
:placeholder="field.placeholder"
:disabled="Boolean(field.read_only)"
/>
<FormControl
v-else
class="text-sm text-ink-gray-8"
@ -349,10 +365,12 @@ import { createDocument } from '@/composables/document'
import {
FormControl,
Checkbox,
TimePicker,
DateTimePicker,
DatePicker,
Tooltip,
dayjs,
Autocomplete
} from 'frappe-ui'
import Draggable from 'vuedraggable'
import { ref, reactive, computed, inject, provide } from 'vue'
@ -374,6 +392,10 @@ const props = defineProps({
type: String,
required: true,
},
overrides: {
type: Object,
default: () => ({}),
}
})
const triggerOnChange = inject('triggerOnChange', () => {})
@ -442,11 +464,18 @@ function getFieldObj(field) {
})
}
return {
const fieldObjWithFilters ={
...field,
filters: field.link_filters && JSON.parse(field.link_filters),
placeholder: field.placeholder || field.label,
}
return {
...fieldObjWithFilters,
...props.overrides.fields?.find(
(f) => f.fieldname === field.fieldname,
),
}
}
const gridTemplateColumns = computed(() => {

View File

@ -127,6 +127,14 @@ watchDebounced(
{ debounce: 300, immediate: true },
)
watchDebounced(
() => props.filters,
() => {
reload('', true)
},
{ debounce: 300, immediate: true },
)
const options = createResource({
url: 'frappe.desk.search.search_link',
cache: [props.doctype, text.value, props.hideMe, props.filters],
@ -154,13 +162,14 @@ const options = createResource({
},
})
function reload(val) {
function reload(val, force=false) {
if (!props.doctype) return
if (
!force &&
options.data?.length &&
val === options.params?.txt &&
props.doctype === options.params?.doctype
)
)
return
options.update({

View File

@ -1,304 +0,0 @@
<template>
<div>
<div class="flex flex-wrap gap-1">
<Button
ref="emails"
v-for="value in values"
:key="value"
:label="value"
theme="gray"
variant="subtle"
:class="{
'rounded bg-surface-white hover:!bg-surface-gray-1 focus-visible:ring-outline-gray-4':
variant === 'subtle',
}"
@keydown.delete.capture.stop="removeLastValue"
>
<template #suffix>
<FeatherIcon
class="h-3.5"
name="x"
@click.stop="removeValue(value)"
/>
</template>
</Button>
<div class="flex-1">
<Combobox v-model="selectedValue" nullable>
<Popover class="w-full" v-model:show="showOptions">
<template #target="{ togglePopover }">
<ComboboxInput
ref="search"
class="search-input form-input w-full border-none focus:border-none focus:!shadow-none focus-visible:!ring-0"
:class="[
variant == 'ghost'
? 'bg-surface-white hover:bg-surface-white'
: 'bg-surface-gray-2 hover:bg-surface-gray-3',
inputClass,
]"
:placeholder="placeholder"
type="text"
:value="query"
@change="
(e) => {
query = e.target.value
showOptions = true
}
"
autocomplete="off"
@focus="() => togglePopover()"
@keydown.delete.capture.stop="removeLastValue"
/>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div
class="mt-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<ComboboxOptions
class="p-1.5 max-h-[12rem] overflow-y-auto"
static
>
<div
v-if="!options.length"
class="flex gap-2 rounded px-2 py-1 text-base text-ink-gray-5"
>
<FeatherIcon
v-if="fetchContacts"
name="search"
class="h-4"
/>
{{
fetchContacts
? __('No results found')
: __('Type an email address to invite')
}}
</div>
<ComboboxOption
v-for="option in options"
:key="option.value"
:value="option"
v-slot="{ active }"
>
<li
:class="[
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
{ 'bg-surface-gray-3': active },
]"
>
<UserAvatar
class="mr-2"
:user="option.value"
size="lg"
/>
<div class="flex flex-col gap-1 p-1 text-ink-gray-8">
<div class="text-base font-medium">
{{ option.label }}
</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
</div>
</div>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</template>
</Popover>
</Combobox>
</div>
</div>
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
<div
v-if="info"
class="whitespace-pre-line text-sm text-ink-blue-3 mt-2 pl-2"
>
{{ info }}
</div>
</div>
</template>
<script setup>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
import UserAvatar from '@/components/UserAvatar.vue'
import Popover from '@/components/frappe-ui/Popover.vue'
import { createResource } from 'frappe-ui'
import { ref, computed, nextTick } from 'vue'
import { watchDebounced } from '@vueuse/core'
const props = defineProps({
validate: {
type: Function,
default: null,
},
variant: {
type: String,
default: 'subtle',
},
placeholder: {
type: String,
default: '',
},
inputClass: {
type: String,
default: '',
},
errorMessage: {
type: Function,
default: (value) => `${value} is an Invalid value`,
},
fetchContacts: {
type: Boolean,
default: true,
},
existingEmails: {
type: Array,
default: () => [],
},
})
const values = defineModel()
const emails = ref([])
const search = ref(null)
const error = ref(null)
const info = ref(null)
const query = ref('')
const text = ref('')
const showOptions = ref(false)
const selectedValue = computed({
get: () => query.value || '',
set: (val) => {
query.value = ''
if (val) {
showOptions.value = false
}
val?.value && addValue(val.value)
},
})
watchDebounced(
query,
(val) => {
val = val || ''
if (text.value === val && options.value?.length) return
text.value = val
reload(val)
},
{ debounce: 300, immediate: true },
)
const filterOptions = createResource({
url: 'crm.api.contact.search_emails',
method: 'POST',
cache: [text.value, 'Contact'],
params: { txt: text.value },
transform: (data) => {
let allData = data.map((option) => {
let fullName = option[0]
let email = option[1]
let name = option[2]
return {
label: fullName || name || email,
value: email,
}
})
// Filter out existing emails
if (props.existingEmails?.length) {
allData = allData.filter((option) => {
return !props.existingEmails.includes(option.value)
})
}
return allData
},
})
const options = computed(() => {
let searchedContacts = props.fetchContacts ? filterOptions.data : []
if (!searchedContacts?.length && query.value) {
searchedContacts.push({
label: query.value,
value: query.value,
})
}
return searchedContacts || []
})
function reload(val) {
if (!props.fetchContacts) return
filterOptions.update({
params: { txt: val },
})
filterOptions.reload()
}
const addValue = (value) => {
error.value = null
info.value = null
if (value) {
const splitValues = value.split(',')
splitValues.forEach((value) => {
value = value.trim()
if (value) {
// check if value is not already in the values array
if (!values.value?.includes(value)) {
// check if value is valid
if (value && props.validate && !props.validate(value)) {
error.value = props.errorMessage(value)
query.value = value
return
}
// add value to values array
if (!values.value) {
values.value = [value]
} else {
values.value.push(value)
}
value = value.replace(value, '')
} else {
info.value = __('email already exists')
}
}
})
!error.value && (value = '')
}
}
const removeValue = (value) => {
values.value = values.value.filter((v) => v !== value)
}
const removeLastValue = () => {
if (query.value) return
let emailRef = emails.value[emails.value.length - 1]?.$el
if (document.activeElement === emailRef) {
values.value.pop()
nextTick(() => {
if (values.value.length) {
emailRef = emails.value[emails.value.length - 1].$el
emailRef?.focus()
} else {
setFocus()
}
})
} else {
emailRef?.focus()
}
}
function setFocus() {
search.value.$el.focus()
}
defineExpose({ setFocus })
</script>

View File

@ -1,278 +0,0 @@
<template>
<div>
<div class="flex flex-wrap gap-1">
<Button
ref="emails"
v-for="value in values"
:key="value"
:label="value"
theme="gray"
variant="subtle"
:class="{
'rounded bg-surface-white hover:!bg-surface-gray-1 focus-visible:ring-outline-gray-4':
variant === 'subtle',
}"
@keydown.delete.capture.stop="removeLastValue"
>
<template #suffix>
<FeatherIcon
class="h-3.5"
name="x"
@click.stop="removeValue(value)"
/>
</template>
</Button>
<div class="flex-1">
<Combobox v-model="selectedValue" nullable>
<Popover class="w-full" v-model:show="showOptions">
<template #target="{ togglePopover }">
<ComboboxInput
ref="search"
class="search-input form-input w-full border-none focus:border-none focus:!shadow-none focus-visible:!ring-0"
:class="[
variant == 'ghost'
? 'bg-surface-white hover:bg-surface-white'
: 'bg-surface-gray-2 hover:bg-surface-gray-3',
inputClass,
]"
:placeholder="placeholder"
type="text"
:value="query"
@change="
(e) => {
query = e.target.value
showOptions = true
}
"
autocomplete="off"
@focus="() => togglePopover()"
@keydown.delete.capture.stop="removeLastValue"
/>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div
class="mt-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<ComboboxOptions
class="p-1.5 max-h-[12rem] overflow-y-auto"
static
>
<div
v-if="!options.length"
class="flex gap-2 rounded px-2 py-1 text-base text-ink-gray-5"
>
<FeatherIcon
v-if="fetchUsers"
name="search"
class="h-4"
/>
{{
fetchUsers
? __('No results found')
: __('Type an email address to invite')
}}
</div>
<ComboboxOption
v-for="option in options"
:key="option.value"
:value="option"
v-slot="{ active }"
>
<li
:class="[
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
{ 'bg-surface-gray-3': active },
]"
>
<UserAvatar
class="mr-2"
:user="option.value"
size="lg"
/>
<div class="flex flex-col gap-1 p-1 text-ink-gray-8">
<div class="text-base font-medium">
{{ option.label }}
</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
</div>
</div>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</template>
</Popover>
</Combobox>
</div>
</div>
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
<div
v-if="info"
class="whitespace-pre-line text-sm text-ink-blue-3 mt-2 pl-2"
>
{{ info }}
</div>
</div>
</template>
<script setup>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
import UserAvatar from '@/components/UserAvatar.vue'
import Popover from '@/components/frappe-ui/Popover.vue'
import { usersStore } from '@/stores/users'
import { ref, computed, nextTick } from 'vue'
const props = defineProps({
validate: {
type: Function,
default: null,
},
variant: {
type: String,
default: 'subtle',
},
placeholder: {
type: String,
default: '',
},
inputClass: {
type: String,
default: '',
},
errorMessage: {
type: Function,
default: (value) => `${value} is an Invalid value`,
},
fetchUsers: {
type: Boolean,
default: true,
},
existingEmails: {
type: Array,
default: () => [],
},
})
const values = defineModel()
const { users } = usersStore()
const emails = ref([])
const search = ref(null)
const error = ref(null)
const info = ref(null)
const query = ref('')
const showOptions = ref(false)
const selectedValue = computed({
get: () => query.value || '',
set: (val) => {
query.value = ''
if (val) {
showOptions.value = false
}
val?.value && addValue(val.value)
},
})
const options = computed(() => {
let userEmails = props.fetchUsers ? users?.data?.allUsers : []
if (props.fetchUsers) {
userEmails = userEmails.map((user) => ({
label: user.full_name || user.name || user.email,
value: user.email,
}))
if (props.existingEmails?.length) {
userEmails = userEmails.filter((option) => {
return !props.existingEmails.includes(option.value)
})
}
if (query.value) {
userEmails = userEmails.filter(
(option) =>
option.label.toLowerCase().includes(query.value.toLowerCase()) ||
option.value.toLowerCase().includes(query.value.toLowerCase()),
)
}
} else if (!userEmails?.length && query.value) {
userEmails.push({
label: query.value,
value: query.value,
})
}
return userEmails || []
})
const addValue = (value) => {
error.value = null
info.value = null
if (value) {
const splitValues = value.split(',')
splitValues.forEach((value) => {
value = value.trim()
if (value) {
// check if value is not already in the values array
if (!values.value?.includes(value)) {
// check if value is valid
if (value && props.validate && !props.validate(value)) {
error.value = props.errorMessage(value)
query.value = value
return
}
// add value to values array
if (!values.value) {
values.value = [value]
} else {
values.value.push(value)
}
value = value.replace(value, '')
} else {
info.value = __('email already exists')
}
}
})
!error.value && (value = '')
}
}
const removeValue = (value) => {
values.value = values.value.filter((v) => v !== value)
}
const removeLastValue = () => {
if (query.value) return
let emailRef = emails.value[emails.value.length - 1]?.$el
if (document.activeElement === emailRef) {
values.value.pop()
nextTick(() => {
if (values.value.length) {
emailRef = emails.value[emails.value.length - 1].$el
emailRef?.focus()
} else {
setFocus()
}
})
} else {
emailRef?.focus()
}
}
function setFocus() {
search.value.$el.focus()
}
defineExpose({ setFocus })
</script>

View File

@ -20,11 +20,12 @@
<div class="flex flex-col gap-3">
<div class="sm:mx-10 mx-4 flex items-center gap-2 border-t pt-2.5">
<span class="text-xs text-ink-gray-4">{{ __('TO') }}:</span>
<MultiSelectEmailInput
<EmailMultiSelect
class="flex-1"
variant="ghost"
v-model="toEmails"
:validate="validateEmail"
:fetchContacts="true"
:error-message="
(value) => __('{0} is an invalid email address', [value])
"
@ -54,11 +55,12 @@
</div>
<div v-if="cc" class="sm:mx-10 mx-4 flex items-center gap-2">
<span class="text-xs text-ink-gray-4">{{ __('CC') }}:</span>
<MultiSelectEmailInput
<EmailMultiSelect
ref="ccInput"
class="flex-1"
variant="ghost"
v-model="ccEmails"
:fetchContacts="true"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
@ -67,11 +69,12 @@
</div>
<div v-if="bcc" class="sm:mx-10 mx-4 flex items-center gap-2">
<span class="text-xs text-ink-gray-4">{{ __('BCC') }}:</span>
<MultiSelectEmailInput
<EmailMultiSelect
ref="bccInput"
class="flex-1"
variant="ghost"
v-model="bccEmails"
:fetchContacts="true"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
@ -179,7 +182,7 @@ import SmileIcon from '@/components/Icons/SmileIcon.vue'
import EmailTemplateIcon from '@/components/Icons/EmailTemplateIcon.vue'
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
import AttachmentItem from '@/components/AttachmentItem.vue'
import MultiSelectEmailInput from '@/components/Controls/MultiSelectEmailInput.vue'
import EmailMultiSelect from '@/components/Controls/EmailMultiSelect.vue'
import EmailTemplateSelectorModal from '@/components/Modals/EmailTemplateSelectorModal.vue'
import { TextEditorBubbleMenu, TextEditor, FileUploader, call } from 'frappe-ui'
import { capture } from '@/telemetry'

View File

@ -130,10 +130,18 @@
</Tooltip>
</template>
</Link>
<TimePicker
v-else-if="field.fieldtype === 'Time'"
:value="data[field.fieldname]"
:format="getFormat('', '', false, true, false)"
:placeholder="getPlaceholder(field)"
input-class="border-none"
@change="(v) => fieldChange(v, field)"
/>
<DateTimePicker
v-else-if="field.fieldtype === 'Datetime'"
:value="data[field.fieldname]"
:formatter="(date) => getFormat(date, '', true, true)"
:format="getFormat('', '', true, true, false)"
:placeholder="getPlaceholder(field)"
input-class="border-none"
@change="(v) => fieldChange(v, field)"
@ -141,7 +149,7 @@
<DatePicker
v-else-if="field.fieldtype === 'Date'"
:value="data[field.fieldname]"
:formatter="(date) => getFormat(date, '', true)"
:format="getFormat('', '', true, false, false)"
:placeholder="getPlaceholder(field)"
input-class="border-none"
@change="(v) => fieldChange(v, field)"
@ -225,7 +233,7 @@ import { flt } from '@/utils/numberFormat.js'
import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users'
import { useDocument } from '@/data/document'
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
import { Tooltip, DatePicker, DateTimePicker, TimePicker } from 'frappe-ui'
import { computed, provide, inject } from 'vue'
const props = defineProps({

View File

@ -0,0 +1,14 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.66699 12.7109C7.89499 12.7574 8.06641 12.9594 8.06641 13.2012C8.06625 13.4428 7.89489 13.6449 7.66699 13.6914L7.56641 13.7012H1.5C1.22406 13.7011 1.00018 13.4771 1 13.2012C1 12.9251 1.22395 12.7013 1.5 12.7012H7.56641L7.66699 12.7109ZM14.6006 9.24414C14.8284 9.29081 15 9.4928 15 9.73438C14.9999 9.97585 14.8283 10.178 14.6006 10.2246L14.5 10.2344H1.5C1.22403 10.2343 1.00013 10.0103 1 9.73438C1 9.4583 1.22395 9.23448 1.5 9.23438H14.5L14.6006 9.24414ZM3.56934 2.45996C3.78682 2.52747 3.96526 2.69406 4.04297 2.91699L5.2168 6.29199C5.31837 6.58437 5.10155 6.88965 4.79199 6.88965H4.77441C4.57659 6.88946 4.40241 6.75957 4.34473 6.57031L4.11133 5.80664H2.53613L2.30273 6.57129C2.24483 6.7604 2.06986 6.88965 1.87207 6.88965H1.85742C1.54804 6.88951 1.33114 6.5843 1.43262 6.29199L2.59961 2.93066C2.70432 2.62909 2.98841 2.42694 3.30762 2.42676H3.56934V2.45996ZM14.6006 5.77734C14.8283 5.82404 15 6.02602 15 6.26758C14.9999 6.50907 14.8283 6.71112 14.6006 6.75781L14.5 6.76758H7.56641C7.29038 6.7675 7.06648 6.5436 7.06641 6.26758C7.06641 5.99149 7.29033 5.76766 7.56641 5.76758H14.5L14.6006 5.77734ZM2.75 5.1084H3.89844L3.35254 3.31738H3.29688L2.75 5.1084ZM14.6006 2.31055C14.8283 2.35723 14.9999 2.55931 15 2.80078C15 3.04233 14.8283 3.24432 14.6006 3.29102L14.5 3.30078H7.56641C7.29033 3.3007 7.06641 3.07687 7.06641 2.80078C7.06651 2.52478 7.2904 2.30086 7.56641 2.30078H14.5L14.6006 2.31055Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -1,20 +1,15 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 5C1 2.79086 2.79086 1 5 1H13C15.2091 1 17 2.79086 17 5V13C17 15.2091 15.2091 17 13 17H5C2.79086 17 1 15.2091 1 13V5Z"
stroke="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.7819 6.27142H11.5136H8.02453H6.28001V4.84002H11.7819V6.27142ZM8.02451 9.62623V11.5944H11.8267V13.0258H6.27999V8.19484H8.02451H11.5135V9.62623H8.02451Z"
fill="currentColor"
/>
</svg>
</template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.6611 8.2289V9.77773H7.88672V11.9066H11.999V13.4545H6V8.2289H11.6611ZM11.9512 4.6V6.14883H6V4.6H11.9512Z"
fill="currentColor"
/>
<rect x="1.5" y="1.5" width="15" height="15" rx="3.5" stroke="currentColor" />
</svg>
</template>

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="M5.5 1C5.5 0.723858 5.27614 0.5 5 0.5C4.72386 0.5 4.5 0.723858 4.5 1V2.00057C3.42774 2.00446 2.83574 2.03488 2.36942 2.27248C1.89901 2.51217 1.51656 2.89462 1.27688 3.36502C1.00439 3.8998 1.00439 4.59987 1.00439 6V9.0642C1 9.33719 1 9.64625 1 10V11C1 12.4001 1 13.1002 1.27248 13.635C1.51217 14.1054 1.89462 14.4878 2.36502 14.7275C2.8998 15 3.59987 15 5 15L5.00439 15L11 15L11.0044 15L11.2588 14.9999C12.4914 14.9989 13.138 14.983 13.6394 14.7275C14.1098 14.4878 14.4922 14.1054 14.7319 13.635C15.0044 13.1002 15.0044 12.4001 15.0044 11V6C15.0044 4.59987 15.0044 3.8998 14.7319 3.36502C14.4922 2.89462 14.1098 2.51217 13.6394 2.27248C13.1718 2.03423 12.5778 2.0043 11.5 2.00054V1C11.5 0.723858 11.2761 0.5 11 0.5C10.7239 0.5 10.5 0.723858 10.5 1V2H5.5V1ZM10.5 4V3H5.5V4C5.5 4.27614 5.27614 4.5 5 4.5C4.72386 4.5 4.5 4.27614 4.5 4V3.00063C4.05122 3.0023 3.71688 3.00843 3.44383 3.03074C3.08879 3.05975 2.92633 3.11105 2.82341 3.16349C2.54117 3.3073 2.31169 3.53677 2.16788 3.81901C2.11544 3.92194 2.06414 4.0844 2.03513 4.43944C2.00517 4.80615 2.00439 5.28343 2.00439 6V6.49671C2.11748 6.41228 2.23805 6.33718 2.36502 6.27248C2.8998 6 3.59987 6 5 6H11C12.4001 6 13.1002 6 13.635 6.27248C13.7653 6.33886 13.8888 6.41619 14.0044 6.5033V6C14.0044 5.28343 14.0036 4.80615 13.9737 4.43944C13.9446 4.0844 13.8933 3.92194 13.8409 3.81901C13.6971 3.53677 13.4676 3.3073 13.1854 3.16349C13.0825 3.11105 12.92 3.05975 12.565 3.03074C12.2901 3.00829 11.9532 3.00222 11.5 3.00059V4C11.5 4.27614 11.2761 4.5 11 4.5C10.7239 4.5 10.5 4.27614 10.5 4ZM3.44383 13.9693C3.75328 13.9945 4.14147 13.999 4.68573 13.9998L4.87281 14L5.00439 14L11 14L11.0044 14L11.2621 13.9999C11.8362 13.9993 12.2405 13.9954 12.5606 13.9693C12.9156 13.9403 13.0781 13.889 13.181 13.8365C13.4632 13.6927 13.6927 13.4632 13.8365 13.181C13.889 13.0781 13.9403 12.9156 13.9693 12.5606C13.9992 12.1938 14 11.7166 14 11V10C14 9.28343 13.9992 8.80615 13.9693 8.43944C13.9403 8.0844 13.889 7.92194 13.8365 7.81901C13.6927 7.53677 13.4632 7.3073 13.181 7.16349C13.0781 7.11105 12.9156 7.05975 12.5606 7.03074C12.1939 7.00078 11.7166 7 11 7H5C4.28343 7 3.80615 7.00078 3.43944 7.03074C3.0844 7.05975 2.92194 7.11105 2.81901 7.16349C2.53677 7.3073 2.3073 7.53677 2.16349 7.81901C2.11105 7.92194 2.05975 8.0844 2.03074 8.43944C2.01608 8.61883 2.00841 8.82469 2.00439 9.07208V11C2.00439 11.7166 2.00517 12.1938 2.03513 12.5606C2.06414 12.9156 2.11544 13.0781 2.16788 13.181C2.31169 13.4632 2.54117 13.6927 2.82341 13.8365C2.92633 13.889 3.08879 13.9403 3.44383 13.9693ZM6.8125 10.4375C6.8125 9.78166 7.34416 9.25 8 9.25C8.65584 9.25 9.1875 9.78166 9.1875 10.4375C9.1875 11.0933 8.65584 11.625 8 11.625C7.34416 11.625 6.8125 11.0933 6.8125 10.4375ZM8 8.25C6.79188 8.25 5.8125 9.22938 5.8125 10.4375C5.8125 11.6456 6.79188 12.625 8 12.625C9.20812 12.625 10.1875 11.6456 10.1875 10.4375C10.1875 9.22938 9.20812 8.25 8 8.25Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -0,0 +1,26 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg9"
width="666.66669" height="666.66718" viewBox="0 0 666.66668 666.66717">
<defs id="defs13">
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath25">
<path d="M 0,700 H 700 V 0 H 0 Z" id="path23" />
</clipPath>
</defs>
<g id="g17" transform="matrix(1.3333333,0,0,-1.3333333,-133.33333,799.99999)">
<g id="g19">
<g id="g21" clip-path="url(#clipPath25)">
<g id="g27" transform="translate(600,350)">
<path
d="m 0,0 c 0,138.071 -111.929,250 -250,250 -138.071,0 -250,-111.929 -250,-250 0,-117.245 80.715,-215.622 189.606,-242.638 v 166.242 h -51.552 V 0 h 51.552 v 32.919 c 0,85.092 38.508,124.532 122.048,124.532 15.838,0 43.167,-3.105 54.347,-6.211 V 81.986 c -5.901,0.621 -16.149,0.932 -28.882,0.932 -40.993,0 -56.832,-15.528 -56.832,-55.9 V 0 h 81.659 l -14.028,-76.396 h -67.631 V -248.169 C -95.927,-233.218 0,-127.818 0,0"
style="fill:#0866ff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path29" />
</g>
<g id="g31" transform="translate(447.9175,273.6036)">
<path
d="M 0,0 14.029,76.396 H -67.63 v 27.019 c 0,40.372 15.838,55.899 56.831,55.899 12.733,0 22.981,-0.31 28.882,-0.931 v 69.253 c -11.18,3.106 -38.509,6.212 -54.347,6.212 -83.539,0 -122.048,-39.441 -122.048,-124.533 V 76.396 h -51.552 V 0 h 51.552 v -166.242 c 19.343,-4.798 39.568,-7.362 60.394,-7.362 10.254,0 20.358,0.632 30.288,1.831 L -67.63,0 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path33" />
</g>
</g>
</g>
</g>
</svg>
</template>

View File

@ -0,0 +1,15 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="1.5" y="1.5" width="15" height="15" rx="3.5" stroke="currentColor" />
<path
d="M13.7928 8.0619V5H4.29999V6.39494H12.3621V7.72014C11.787 7.88056 11.37 8.39669 11.37 9.00349C11.37 9.61029 11.787 10.1194 12.3621 10.2799V11.6051L5.79999 11.6051V7.96425H4.29999V13H13.8V9.9381L12.9444 9.34525V8.66173L13.8 8.06888L13.7928 8.0619Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -0,0 +1,14 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 1C11.866 1 15 4.13401 15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1ZM10.75 9.5C10.0172 9.5 9.50422 9.65253 9.12793 9.84277C8.74778 10.035 8.48265 10.2774 8.24707 10.5049C8.08807 10.6584 8.00001 10.8573 8 11.0352V14C10.4873 14 12.6207 12.4863 13.5303 10.3301C12.9601 9.92399 12.0744 9.5 10.75 9.5ZM4.75 9.5C4.01981 9.5 3.50767 9.6516 3.13184 9.84082C2.85955 9.97794 2.64585 10.1409 2.45996 10.3057C3.24038 12.1787 4.94236 13.5697 7 13.915V11.0352C7.00001 10.7133 7.099 10.4099 7.25781 10.1514C6.69171 9.81028 5.88188 9.50007 4.75 9.5ZM8 2C4.68629 2 2 4.68629 2 8C2 8.43945 2.04801 8.8677 2.1377 9.28027C2.29548 9.1649 2.47567 9.05099 2.68164 8.94727C3.2047 8.68387 3.87233 8.5 4.75 8.5C6.21316 8.50007 7.25578 8.94284 7.96582 9.41309C8.16037 9.25535 8.39427 9.09305 8.67676 8.9502C9.20055 8.68539 9.86925 8.5 10.75 8.5C12.1371 8.5 13.144 8.89812 13.8477 9.33789C13.9457 8.90751 14 8.46009 14 8C14 4.68629 11.3137 2 8 2ZM10.5 5.5C11.1875 5.5 11.75 6.0625 11.75 6.75C11.75 7.4375 11.1875 8 10.5 8C9.8125 8 9.25 7.4375 9.25 6.75C9.25 6.0625 9.8125 5.5 10.5 5.5ZM6 4.5C6.825 4.5 7.5 5.175 7.5 6C7.5 6.825 6.825 7.5 6 7.5C5.175 7.5 4.5 6.825 4.5 6C4.5 5.175 5.175 4.5 6 4.5Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -56,7 +56,7 @@
<Dropdown :options="actions(column)">
<template #default>
<Button
class="hidden group-hover:flex"
class="opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto transition-opacity"
icon="more-horizontal"
variant="ghost"
/>

View File

@ -157,6 +157,7 @@ import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import NotificationsIcon from '@/components/Icons/NotificationsIcon.vue'
@ -233,6 +234,11 @@ const links = [
icon: TaskIcon,
to: 'Tasks',
},
{
label: 'Calendar',
icon: CalendarIcon,
to: 'Calendar',
},
{
label: 'Call Logs',
icon: PhoneIcon,

View File

@ -6,8 +6,8 @@
>
<template #body-content>
<div class="flex gap-1 border rounded mb-4 p-2 text-ink-gray-5">
<FeatherIcon name="info" class="size-3.5" />
<p class="text-sm">
<FeatherIcon name="info" class="size-3.5 mt-0.5" />
<p class="text-p-sm">
{{
__(
'Add existing system users to this CRM. Assign them a role to grant access with their current credentials.',
@ -21,13 +21,14 @@
</label>
<div class="p-2 group bg-surface-gray-2 hover:bg-surface-gray-3 rounded">
<MultiSelectUserInput
<EmailMultiSelect
v-if="users?.data?.crmUsers?.length"
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="newUsers"
:validate="validateEmail"
:fetchUsers="true"
:existingEmails="[
...users.data.crmUsers.map((user) => user.name),
'admin@example.com',
@ -35,6 +36,7 @@
:error-message="
(value) => __('{0} is an invalid email address', [value])
"
:emptyPlaceholder="__('No users found')"
/>
</div>
<FormControl
@ -61,7 +63,7 @@
</template>
<script setup>
import MultiSelectUserInput from '@/components/Controls/MultiSelectUserInput.vue'
import EmailMultiSelect from '@/components/Controls/EmailMultiSelect.vue'
import { validateEmail } from '@/utils'
import { usersStore } from '@/stores/users'
import { createResource, toast } from 'frappe-ui'

View File

@ -0,0 +1,439 @@
<template>
<Dialog v-model="show" :options="{ size: 'xl' }">
<template #body-header>
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center space-x-2">
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{
mode === 'edit'
? __('Edit an event')
: mode === 'duplicate'
? __('Duplicate an event')
: __('Create an event')
}}
</h3>
</div>
<div class="flex gap-1">
<Button v-if="mode === 'edit'" variant="ghost" @click="deleteEvent">
<template #icon>
<LucideTrash2 class="h-4 w-4 text-ink-gray-9" />
</template>
</Button>
<Button
v-if="mode === 'edit'"
variant="ghost"
@click="duplicateEvent"
>
<template #icon>
<LucideCopy class="h-4 w-4 text-ink-gray-9" />
</template>
</Button>
<Button variant="ghost" @click="show = false">
<template #icon>
<LucideX class="h-4 w-4 text-ink-gray-9" />
</template>
</Button>
</div>
</div>
</template>
<template #body-content>
<div class="flex flex-col gap-4">
<div class="flex items-center">
<div class="text-base text-ink-gray-7 w-3/12">
{{ __('Title') }}
</div>
<div class="flex gap-1 w-9/12">
<Dropdown class="" :options="colors">
<div
class="flex items-center justify-center size-7 shrink-0 border border-outline-gray-2 bg-surface-white hover:border-outline-gray-3 hover:shadow-sm rounded cursor-pointer"
>
<div
class="size-2.5 rounded-full cursor-pointer"
:style="{
backgroundColor: _event.color || '#30A66D',
}"
/>
</div>
</Dropdown>
<TextInput
class="w-full"
ref="title"
size="sm"
v-model="_event.title"
:placeholder="__('Call with John Doe')"
variant="outline"
required
/>
</div>
</div>
<div class="flex items-center">
<div class="text-base text-ink-gray-7 w-3/12">
{{ __('All day') }}
</div>
<Switch v-model="_event.isFullDay" />
</div>
<div class="border-t border-outline-gray-1" />
<div class="flex items-center">
<div class="text-base text-ink-gray-7 w-3/12">
{{ __('Date & Time') }}
</div>
<div class="flex gap-2 w-9/12">
<DatePicker
:class="[_event.isFullDay ? 'w-full' : 'w-[158px]']"
variant="outline"
:value="_event.fromDate"
:format="'MMM D, YYYY'"
:placeholder="__('May 1, 2025')"
:clearable="false"
@update:modelValue="(date) => updateDate(date, true)"
>
<template #suffix="{ togglePopover }">
<FeatherIcon
name="chevron-down"
class="h-4 w-4 cursor-pointer"
@click="togglePopover"
/>
</template>
</DatePicker>
<TimePicker
v-if="!_event.isFullDay"
class="max-w-[112px]"
variant="outline"
:modelValue="_event.fromTime"
:placeholder="__('Start Time')"
@update:modelValue="(time) => updateTime(time, true)"
/>
<TimePicker
v-if="!_event.isFullDay"
class="max-w-[112px]"
variant="outline"
:modelValue="_event.toTime"
:options="toOptions"
:placeholder="__('End Time')"
placement="bottom-end"
@update:modelValue="(time) => updateTime(time)"
/>
</div>
</div>
<div class="flex items-start">
<div class="text-base text-ink-gray-7 mt-1.5 w-3/12">
{{ __('Attendees') }}
</div>
<div class="w-9/12">
<Attendee
v-model="peoples"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
"
/>
</div>
</div>
<div class="flex">
<div class="mt-2 text-base text-ink-gray-7 w-3/12">
{{ __('Description') }}
</div>
<div class="w-9/12">
<TextEditor
editor-class="!prose-sm overflow-auto min-h-[80px] max-h-80 py-1.5 px-2 rounded border border-outline-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-3 hover:border-outline-gray-modals hover:shadow-sm 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"
:bubbleMenu="true"
:content="_event.description"
@change="(val) => (_event.description = val)"
:placeholder="__('Add description.')"
/>
</div>
</div>
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div>
</template>
<template #actions>
<div v-if="eventsResource" class="flex gap-2 justify-end">
<Button :label="__('Cancel')" @click="show = false" />
<Button
variant="solid"
:label="
mode === 'edit'
? __('Update')
: mode === 'duplicate'
? __('Duplicate')
: __('Create')
"
:disabled="!dirty"
:loading="
mode === 'edit'
? eventsResource.setValue.loading
: eventsResource.insert.loading
"
@click="update"
/>
</div>
</template>
</Dialog>
</template>
<script setup>
import Attendee from '@/components/Calendar/Attendee.vue'
import {
Switch,
TextEditor,
ErrorMessage,
Dialog,
DatePicker,
TimePicker,
dayjs,
Dropdown,
} from 'frappe-ui'
import { globalStore } from '@/stores/global'
import { validateEmail } from '@/utils'
import {
useEvent,
normalizeParticipants,
buildEndTimeOptions,
computeAutoToTime,
validateTimeRange,
} from '@/composables/event'
import { CalendarColorMap as colorMap } from 'frappe-ui'
import { onMounted, ref, computed, h } from 'vue'
const props = defineProps({
event: {
type: Object,
default: () => ({}),
},
doctype: {
type: String,
default: '',
},
docname: {
type: String,
default: '',
},
})
const { $dialog } = globalStore()
const show = defineModel()
const { eventsResource } = useEvent(props.doctype, props.docname)
const title = ref(null)
const error = ref(null)
const mode = computed(() => {
return _event.value.id == 'duplicate'
? 'duplicate'
: _event.value.id
? 'edit'
: 'create'
})
const oldEvent = ref({})
const _event = ref({
title: '',
description: '',
fromDate: '',
toDate: '',
fromTime: '',
toTime: '',
isFullDay: false,
eventType: 'Public',
color: 'green',
referenceDoctype: '',
referenceDocname: '',
event_participants: [],
})
const dirty = computed(() => {
return JSON.stringify(_event.value) !== JSON.stringify(oldEvent.value)
})
const peoples = computed({
get() {
return _event.value.event_participants || []
},
set(list) {
_event.value.event_participants = normalizeParticipants(list)
},
})
onMounted(() => {
if (props.event) {
let start = dayjs(props.event.starts_on)
let end = dayjs(props.event.ends_on)
if (!props.event.name) {
start = dayjs()
end = dayjs().add(1, 'hour')
}
_event.value = {
id: props.event.name || '',
title: props.event.subject,
description: props.event.description,
fromDate: start.format('YYYY-MM-DD'),
toDate: end.format('YYYY-MM-DD'),
fromTime: start.format('HH:mm'),
toTime: end.format('HH:mm'),
isFullDay: props.event.all_day,
eventType: props.event.event_type,
color: props.event.color,
referenceDoctype: props.event.reference_doctype,
referenceDocname: props.event.reference_docname,
event_participants: props.event.event_participants || [],
}
oldEvent.value = JSON.parse(JSON.stringify(_event.value))
setTimeout(() => title.value?.el?.focus(), 100)
}
})
function updateDate(d) {
_event.value.fromDate = d
_event.value.toDate = d
}
function updateTime(t, fromTime = false) {
error.value = null
const prevTo = _event.value.toTime
if (fromTime) {
_event.value.fromTime = t
if (!_event.value.toTime || _event.value.toTime <= t) {
_event.value.toTime = computeAutoToTime(t)
}
} else {
_event.value.toTime = t
}
const { valid, error: err } = validateTimeRange({
fromDate: _event.value.fromDate,
fromTime: _event.value.fromTime,
toTime: _event.value.toTime,
isFullDay: _event.value.isFullDay,
})
if (!valid) {
error.value = err
_event.value.toTime = prevTo
}
}
function update() {
error.value = null
if (!_event.value.title) {
error.value = __('Title is required')
title.value.el.focus()
return
}
const { valid, error: err } = validateTimeRange({
fromDate: _event.value.fromDate,
fromTime: _event.value.fromTime,
toTime: _event.value.toTime,
isFullDay: _event.value.isFullDay,
})
if (!valid) {
error.value = err
return
}
if (_event.value.id && _event.value.id !== 'duplicate') {
updateEvent()
} else {
createEvent()
}
}
function createEvent() {
eventsResource.insert.submit(
{
subject: _event.value.title,
description: _event.value.description,
starts_on: _event.value.fromDate + ' ' + _event.value.fromTime,
ends_on: _event.value.toDate + ' ' + _event.value.toTime,
all_day: _event.value.isFullDay || false,
event_type: _event.value.eventType,
color: _event.value.color,
reference_doctype: props.doctype,
reference_docname: props.docname,
event_participants: _event.value.event_participants,
},
{
onSuccess: async () => {
await eventsResource.reload()
show.value = false
},
},
)
}
function updateEvent() {
if (!_event.value.id) {
error.value = __('Event ID is required')
return
}
eventsResource.setValue.submit(
{
name: _event.value.id,
subject: _event.value.title,
description: _event.value.description,
starts_on: _event.value.fromDate + ' ' + _event.value.fromTime,
ends_on: _event.value.toDate + ' ' + _event.value.toTime,
all_day: _event.value.isFullDay,
event_type: _event.value.eventType,
color: _event.value.color,
reference_doctype: props.doctype,
reference_docname: props.docname,
event_participants: _event.value.event_participants,
},
{
onSuccess: async () => {
await eventsResource.reload()
show.value = false
},
},
)
}
function duplicateEvent() {
if (!_event.value.id) return
_event.value.id = 'duplicate'
_event.value.title = _event.value.title + ' (Copy)'
setTimeout(() => title.value?.el?.focus(), 100)
}
function deleteEvent() {
if (!_event.value.id) return
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete this event?'),
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
eventsResource.delete.submit(_event.value.id, {
onSuccess: async () => {
await eventsResource.reload()
show.value = false
close()
},
})
},
},
],
})
}
const toOptions = computed(() => buildEndTimeOptions(_event.value.fromTime))
const colors = Object.keys(colorMap).map((c) => ({
label: c.charAt(0).toUpperCase() + c.slice(1),
value: colorMap[c].color,
icon: h('div', {
class: '!size-2.5 rounded-full',
style: { backgroundColor: colorMap[c].color },
}),
onClick: () => (_event.value.color = colorMap[c].color),
}))
</script>

View File

@ -176,4 +176,4 @@ function openAddressModal(_address) {
address: _address,
}
}
</script>
</script>

View File

@ -21,9 +21,11 @@
<template #body-content>
<div class="flex flex-col gap-4">
<div>
<FormControl
<div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Title') }}
</div>
<TextInput
ref="title"
:label="__('Title')"
v-model="_task.title"
:placeholder="__('Call with John Doe')"
required
@ -225,8 +227,8 @@ async function updateTask() {
function render() {
editMode.value = false
setTimeout(() => title.value?.el?.focus?.(), 100)
nextTick(() => {
title.value?.el?.focus?.()
_task.value = { ...props.task }
if (_task.value.title) {
editMode.value = true

View File

@ -29,6 +29,7 @@
</template>
<script setup>
import { DropdownOption } from '@/utils'
import { Dropdown } from 'frappe-ui'
import { computed, ref } from 'vue'
const props = defineProps({

View File

@ -18,7 +18,7 @@
@click="handleSelect(s)"
>
<EmailProviderIcon
:service-name="s.name"
:label="s.name"
:logo="s.icon"
:selected="selectedService?.name === s?.name"
/>

View File

@ -9,7 +9,7 @@
<div class="w-fit">
<EmailProviderIcon
:logo="emailIcon[accountData.service]"
:service-name="accountData.service"
:label="accountData.service"
/>
</div>
<!-- banner for setting up email account -->

View File

@ -5,8 +5,8 @@
>
<img :src="logo" class="w-4 h-4" />
</div>
<p v-if="serviceName" class="text-xs text-center text-ink-gray-6 mt-2">
{{ serviceName }}
<p v-if="label" class="text-xs text-center text-ink-gray-6 mt-2">
{{ label }}
</p>
</template>
@ -16,7 +16,7 @@ defineProps({
type: String,
required: true,
},
serviceName: {
label: {
type: String,
default: '',
},

View File

@ -128,7 +128,6 @@ import {
FormControl,
Switch,
toast,
call,
createResource,
} from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue'

View File

@ -0,0 +1,11 @@
<template>
<SettingsPage
doctype="Helpdesk CRM Settings"
:title="__('Helpdesk settings')"
:successMessage="__('Helpdesk settings updated')"
class="p-8"
/>
</template>
<script setup>
import SettingsPage from '@/components/Settings/SettingsPage.vue'
</script>

View File

@ -0,0 +1,327 @@
<template>
<div class="flex h-full flex-col gap-6 text-ink-gray-8">
<!-- Header -->
<div class="flex justify-between px-2 pt-2">
<div class="flex gap-1 -ml-4 w-9/12">
<Button
variant="ghost"
icon-left="chevron-left"
:label="isLocal ? __('New Lead Sync Source') : syncSource.name"
size="md"
@click="() => emit('updateStep', 'source-list')"
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-4 w-3/12 justify-end">
<div class="flex items-center space-x-2">
<Switch size="sm" v-model="syncSource.enabled" />
<span class="text-sm text-ink-gray-7">{{ __('Enabled') }}</span>
</div>
<Button
:label="isLocal ? __('Create') : __('Update')"
icon-left="plus"
variant="solid"
:loading="sources.setValue.loading || sources.insert.loading || docResource?.loading"
@click="createOrUpdateSource"
/>
</div>
</div>
<!-- Form -->
<div class="grid grid-cols-2 gap-4">
<FormControl
type="autocomplete"
required="true"
v-model="syncSource.type"
:options="supportedSourceTypes"
:label="__('Source Type')"
:placeholder="__('Select Source Type')"
>
<template v-if="syncSource.type" #prefix>
<component
class="mr-2 size-4"
:is="syncSource.type.icon"
/>
</template>
<template #item-prefix="{ option }">
<component
class="size-4"
:is="option.icon"
/>
</template>
</FormControl>
<FormControl
type="text"
v-if="isLocal"
required="true"
v-model="syncSource.name"
:label="__('Source Name')"
:placeholder="__('Enter Source Name')"
/>
<FormControl
v-if="fieldsMap.background_sync_frequency"
type="select"
required="true"
:options="fieldsMap.background_sync_frequency.options"
v-model="syncSource.background_sync_frequency"
:label="__('Background Sync Frequency')"
/>
<FormControl
type="password"
required="true"
v-model="syncSource.access_token"
:label="__('Access Token')"
:placeholder="__('Enter Access Token')"
/>
<Link
v-if="!isLocal"
label="Facebook Page"
v-model="syncSource.facebook_page"
doctype="Facebook Page"
/>
<Link
v-if="!isLocal && syncSource.facebook_page"
label="Lead Form"
v-model="syncSource.facebook_lead_form"
doctype="Facebook Lead Form"
:filters="{
'page': syncSource.facebook_page
}"
/>
</div>
<!-- Mapping Grid -->
<div v-if="syncSource.facebook_lead_form && mappingFormDocResource && mappingFormDocResource.document?.doc">
<Grid
v-model="mappingFormDocResource.document.doc.questions"
v-model:parent="mappingFormDocResource.document.doc"
doctype="Facebook Lead Form Question"
parentDoctype="Facebook Lead Form"
parentFieldname="questions"
:overrides="{
fields: [
{'fieldname': 'mapped_to_crm_field', 'options': getCRMLeadFields, 'placeholder': __('Not Synced')}
]
}"
/>
</div>
</div>
</template>
<script setup>
import { useDocument } from "@/data/document";
import { onMounted, inject, ref, computed, watch } from "vue";
import { supportedSourceTypes } from "./leadSyncSourceConfig";
import {
Button,
FormControl,
Switch,
Avatar,
toast,
createResource,
} from "frappe-ui";
import { getMeta } from "@/stores/meta";
import Link from "@/components/Controls/Link.vue";
import Grid from "@/components/Controls/Grid.vue";
const props = defineProps({
sourceData: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["updateStep"]);
const docResource = ref(null);
const mappingFormDocResource = ref(null);
const sourceDoc = computed(() => {
if (!docResource.value) return;
return docResource.value?.document?.doc;
});
const { meta, getFields } = getMeta("Lead Sync Source");
const fields = ref(getFields());
watch(
() => meta.data,
() => {
fields.value = getFields();
},
);
const fieldsMap = computed(() => {
if (!fields.value) return {};
const map = {};
for (const field of fields.value) {
map[field.fieldname] = field;
}
return map;
});
const sources = inject("sources");
const syncSource = ref({
name: "",
type: "",
access_token: "",
facebook_page: "",
facebook_lead_form: "",
enabled: true,
background_sync_frequency:
fieldsMap.value.background_sync_frequency?.default || "Hourly",
});
const isLocal = ref(true);
function updateSource(data) {
sources.setValue.submit(
{
name: syncSource.value.name,
...data,
},
{
onSuccess: () => {
if (docResource.value) {
docResource.value.document.reload();
}
mappingFormDocResource.value.document.save.submit();
},
onError(e) {
toast.error(e.messages[0] || __("Error updating Lead Sync Source"));
},
},
);
}
function createSource() {
sources.insert.submit(
{
...syncSource.value,
type: syncSource.value.type.value,
},
{
onSuccess: (newDoc) => {
toast.success(__("Lead Sync Source created successfully"));
isLocal.value = false;
docResource.value = useDocument("Lead Sync Source", newDoc.name);
},
onError(error) {
toast.error(error.messages[0] || __("Error creating Lead Sync Source"));
},
},
);
}
function createOrUpdateSource() {
if (isLocal.value) {
createSource();
} else {
updateSource({
...syncSource.value,
type: syncSource.value.type.value,
});
}
}
onMounted(() => {
if (props.sourceData?.name) {
Object.assign(syncSource.value, props.sourceData);
isLocal.value = false; // edit form
docResource.value = useDocument("Lead Sync Source", props.sourceData.name);
}
if (syncSource.value.facebook_lead_form) {
mappingFormDocResource.value = useDocument(
"Facebook Lead Form",
syncSource.value.facebook_lead_form,
);
}
});
watch(
() => sourceDoc.value,
(newDoc) => {
if (newDoc) {
Object.assign(syncSource.value, {
...newDoc,
type:
supportedSourceTypes.find((type) => type.value === newDoc.type) ||
newDoc.type,
});
mappingFormDocResource.value = useDocument(
"Facebook Lead Form",
syncSource.value.facebook_lead_form,
);
}
},
);
watch(
() => syncSource.value.facebook_page,
(_, oldValue) => {
if (!oldValue) return; // on mount, the value changes from empty
syncSource.value.facebook_lead_form = "";
},
);
watch(
() => syncSource.value.facebook_lead_form,
(newVal) => {
if (newVal) {
mappingFormDocResource.value = useDocument(
"Facebook Lead Form",
newVal,
);
} else {
mappingFormDocResource.value = null;
}
},
);
const leadFields = createResource({
url: "crm.api.doc.get_fields_meta",
params: {
doctype: "CRM Lead",
as_array: true,
},
cache: ["fieldsMeta", "CRM Lead"],
auto: true,
transform: (data) => {
let restrictedFields = [
"name",
"owner",
"creation",
"modified",
"modified_by",
"docstatus",
"_comments",
"_user_tags",
"_assign",
"_liked_by",
];
console.log("data", data);
return data.filter((field) => !restrictedFields.includes(field.fieldname));
},
});
const getCRMLeadFields = computed(() => {
if (leadFields.data) {
return leadFields.data.map((field) => ({
label: field.label,
value: field.fieldname,
}));
}
return [];
});
</script>

View File

@ -0,0 +1,52 @@
<template>
<div class="flex-1 p-6">
<LeadSyncSourceForm
v-if="step === 'new-source'"
:sourceData="source"
@updateStep="updateStep"
/>
<LeadSyncSources
v-else-if="step === 'source-list'"
@updateStep="updateStep"
/>
<LeadSyncSourceForm
v-else-if="step === 'edit-source'"
:sourceData="source"
@updateStep="updateStep"
/>
</div>
</template>
<script setup>
import LeadSyncSources from "./LeadSyncSources.vue"
import LeadSyncSourceForm from "./LeadSyncSourceForm.vue";
import { createListResource } from 'frappe-ui'
import { provide, ref } from 'vue'
const step = ref('source-list')
const source = ref(null)
const sources = createListResource({
type: 'list',
doctype: 'Lead Sync Source',
cache: 'lead_sync_sources',
fields: [
'name',
'enabled',
'type',
'last_synced_at',
'facebook_lead_form'
],
auto: true,
orderBy: 'modified desc',
pageLength: 20,
})
provide('sources', sources)
function updateStep(newStep, data) {
step.value = newStep
source.value = data
}
</script>

View File

@ -0,0 +1,220 @@
<template>
<div class="flex h-full flex-col gap-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">
{{ __('Lead sources') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{
__(
'Add, edit, and manage sources for automatic lead syncing to your CRM',
)
}}
</p>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('New')"
icon-left="plus"
variant="solid"
@click="emit('updateStep', 'new-source')"
/>
</div>
</div>
<!-- loading state -->
<div
v-if="sources.loading"
class="flex mt-28 justify-between w-full h-full"
>
<Button
:loading="sources.loading"
variant="ghost"
class="w-full"
size="2xl"
/>
</div>
<!-- Empty State -->
<div
v-if="!sources.loading && !sources.data?.length"
class="flex justify-between w-full h-full"
>
<div
class="text-ink-gray-4 border border-dashed rounded w-full flex items-center justify-center"
>
{{ __('No lead sources found') }}
</div>
</div>
<!-- Lead source list -->
<div
class="flex flex-col overflow-hidden"
v-if="!sources.loading && sources.data?.length"
>
<div class="flex items-center py-2 px-4 text-sm text-ink-gray-5">
<div class="w-4/6">{{ __('Name') }}</div>
<div class="w-1/6">{{ __('Source') }}</div>
<div class="w-1/6">{{ __('Enabled') }}</div>
</div>
<div class="h-px border-t mx-4 border-outline-gray-modals" />
<ul class="overflow-y-auto px-2">
<template v-for="(source, i) in sourcesList" :key="source.name">
<li
class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
@click="() => emit('updateStep', 'edit-source', { ...source })"
>
<div class="flex flex-col w-4/6 pr-5">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ source.name }}
</div>
</div>
<div class="flex flex-col w-1/6 pr-5">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ source.type }}
</div>
</div>
<div class="flex items-center justify-between w-1/6">
<Switch
size="sm"
v-model="source.enabled"
@update:model-value="toggleLeadSyncSourceEnabled(source)"
@click.stop
/>
<Dropdown
class=""
:options="getDropdownOptions(source)"
placement="right"
:button="{
icon: 'more-horizontal',
variant: 'ghost',
onblur: (e) => {
e.stopPropagation()
confirmDelete = false
},
}"
@click.stop
/>
</div>
</li>
<div
v-if="sourcesList.length !== i + 1"
class="h-px border-t mx-2 border-outline-gray-modals"
/>
</template>
<!-- Load More Button -->
<div
v-if="!sources.loading && sources.hasNextPage"
class="flex justify-center"
>
<Button
class="mt-3.5 p-2"
@click="() => sources.next()"
:loading="sources.loading"
:label="__('Load More')"
icon-left="refresh-cw"
/>
</div>
</ul>
</div>
</div>
</template>
<script setup>
import {
TextInput,
FormControl,
Switch,
Dropdown,
FeatherIcon,
toast,
} from "frappe-ui";
import { ref, computed, inject } from "vue";
const emit = defineEmits(["updateStep"]);
const sources = inject("sources");
const search = ref("");
const confirmDelete = ref(false);
const sourcesList = computed(() => {
let list = sources.data || [];
if (search.value) {
list = list.filter(
(source) =>
source.name.toLowerCase().includes(search.value.toLowerCase()) ||
source.subject.toLowerCase().includes(search.value.toLowerCase()),
);
}
return list;
});
function toggleLeadSyncSourceEnabled(source) {
sources.setValue.submit(
{
name: source.name,
enabled: source.enabled ? 1 : 0,
},
{
onSuccess: () => {
toast.success(
source.enabled
? __('Source enabled successfully')
: __('Source disabled successfully'),
)
},
onError: (error) => {
toast.error(error.messages[0] || __('Failed to update source'))
// Revert the change if there was an error
source.enabled = !source.enabled
},
},
)
}
function deleteLeadSource(source) {
confirmDelete.value = false;
sources.delete.submit(source.name, {
onSuccess: () => {
toast.success(__("Lead Sync Source deleted successfully"));
},
onError: (error) => {
toast.error(error.messages[0] || __("Failed to delete Lead Sync Source"));
},
});
}
function getDropdownOptions(source) {
let options = [
{
label: __("Duplicate"),
icon: "copy",
onClick: () => emit("updateStep", "new-source", { ...source }),
},
{
label: __("Delete"),
icon: "trash-2",
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
confirmDelete.value = true;
},
condition: () => !confirmDelete.value,
},
{
label: __("Confirm Delete"),
icon: "trash-2",
theme: "red",
onClick: () => deleteLeadSource(source),
condition: () => confirmDelete.value,
},
];
return options;
}
</script>

View File

@ -0,0 +1,32 @@
import LogoFacebook from '@/components/Icons/FacebookIcon.vue';
export const supportedSourceTypes = [
{
label: 'Facebook',
value: 'Facebook',
icon: LogoFacebook,
info: __("You will need a Meta developer account and an access token to sync leads from Facebook. Read more "),
link: 'https://www.facebook.com/business/help/503306463479099?id=2190812977867143',
custom: false,
}
]
export const sourceIcon = {
'Facebook': LogoFacebook
}
export const fbSourceFields = [
{
name: "name",
label: __("Name"),
type: "text",
placeholder: __("Add a name for your source"),
},
{
name: "access_token",
label: __("Access Token"),
type: "password",
placeholder: __("Enter your Facebook Access Token"),
}
];

View File

@ -49,6 +49,7 @@ 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 HelpdeskIcon from '@/components/Icons/HelpdeskIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue'
import EmailTemplateIcon from '@/components/Icons/EmailTemplateIcon.vue'
@ -58,6 +59,8 @@ 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 HelpdeskSettings from '@/components/Settings/HelpdeskSettings.vue'
import LeadSyncSourcePage from '@/components/Settings/LeadSyncing/LeadSyncSourcePage.vue'
import BrandSettings from '@/components/Settings/BrandSettings.vue'
import HomeActions from '@/components/Settings/HomeActions.vue'
import ForecastingSettings from '@/components/Settings/ForecastingSettings.vue'
@ -196,6 +199,18 @@ const tabs = computed(() => {
component: markRaw(ERPNextSettings),
condition: () => isManager(),
},
{
label: __('Helpdesk'),
icon: HelpdeskIcon,
component: markRaw(HelpdeskSettings),
condition: () => isManager(),
},
{
label: __('Lead Syncing'),
icon: 'refresh-cw',
component: markRaw(LeadSyncSourcePage),
condition: () => isManager(),
},
],
condition: () => isManager() || isTelephonyAgent(),
},

View File

@ -1,12 +1,12 @@
import { validateEmail } from '../../utils'
const LogoGmail = '/images/gmail.png'
const LogoOutlook = '/images/outlook.png'
const LogoSendgrid = '/images/sendgrid.png'
const LogoSparkpost = '/images/sparkpost.webp'
const LogoYahoo = '/images/yahoo.png'
const LogoYandex = '/images/yandex.png'
const LogoFrappeMail = '/images/frappe-mail.svg'
import LogoGmail from '@/images/gmail.png'
import LogoOutlook from '@/images/outlook.png'
import LogoSendgrid from '@/images/sendgrid.png'
import LogoSparkpost from '@/images/sparkpost.webp'
import LogoYahoo from '@/images/yahoo.png'
import LogoYandex from '@/images/yandex.png'
import LogoFrappeMail from '@/images/frappe-mail.svg'
const fixedFields = [
{

View File

@ -0,0 +1,104 @@
<template>
<Tooltip v-if="!disabled">
<template #body>
<div
class="rounded bg-surface-gray-7 py-1.5 px-2 text-xs text-ink-white shadow-xl"
>
<span class="flex items-center gap-1">
<span>{{ label }}</span>
<!-- Primary combos (one or many) -->
<template
v-for="(combo, idx) in primaryCombosDisplay"
:key="'prim-' + idx + combo"
>
<KeyboardShortcut
bg
class="!bg-surface-gray-5 !text-ink-gray-2 px-1"
:combo="combo"
/>
</template>
<!-- Alternate combos / equivalents -->
<template
v-for="(alt, idx) in altCombosDisplay"
:key="'alt-' + idx + alt"
>
<KeyboardShortcut
bg
class="!bg-surface-gray-5 !text-ink-gray-2 px-1"
:combo="alt"
/>
</template>
</span>
</div>
</template>
<slot />
</Tooltip>
<slot v-else />
</template>
<script setup lang="ts">
import { Tooltip, KeyboardShortcut } from 'frappe-ui'
import { computed } from 'vue'
interface Props {
label: string
combo?: string | string[]
altCombos?: string[]
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
combo: '',
altCombos: () => [],
disabled: false,
})
const isMac = computed(() => {
if (typeof navigator === 'undefined') return false
const platform =
(navigator as any).userAgentData?.platform || navigator.platform || ''
if (/Mac|iPod|iPhone|iPad/i.test(platform)) return true
return /Mac OS X|Macintosh|iPhone|iPad|iPod/i.test(navigator.userAgent || '')
})
function normalizeCombo(raw: string): string {
if (!raw) return ''
if (/^mod\+/i.test(raw)) {
const rest = raw.split('+').slice(1).join('+')
return (isMac.value ? 'Cmd' : 'Ctrl') + '+' + rest
}
return raw
}
function normalizeList(list: string | string[]): string[] {
const arr = Array.isArray(list) ? list : list ? [list] : []
return arr.map(normalizeCombo)
}
// Dedupe Backspace/Delete (prefer Backspace) on macOS
function dedupeMacDeleteVariants(primary: string[], alts: string[]) {
if (!isMac.value) return { primary, alts }
const all = [...primary, ...alts]
if (all.includes('Delete') && all.includes('Backspace')) {
return {
primary: primary.filter((k) => k !== 'Delete'),
alts: alts.filter((k) => k !== 'Delete'),
}
}
return { primary, alts }
}
// Base normalized lists
const normalizedPrimary = computed(() => normalizeList(props.combo))
const normalizedAlt = computed(() => props.altCombos.map(normalizeCombo))
// Apply dedupe once to both arrays to avoid circular dependency
const deduped = computed(() =>
dedupeMacDeleteVariants(normalizedPrimary.value, normalizedAlt.value),
)
const primaryCombosDisplay = computed(() => deduped.value.primary)
const altCombosDisplay = computed(() => deduped.value.alts)
defineOptions({ name: 'ShortcutTooltip' })
</script>

View File

@ -173,19 +173,26 @@
@change="(v) => fieldChange(v, field)"
:onCreate="field.create"
/>
<div
v-else-if="field.fieldtype === 'Time'"
class="form-control"
>
<TimePicker
:value="doc[field.fieldname]"
:format="getFormat('', '', false, true, false)"
:placeholder="field.placeholder"
@change="(v) => fieldChange(v, field)"
/>
</div>
<div
v-else-if="field.fieldtype === 'Datetime'"
class="form-control"
>
<DateTimePicker
icon-left=""
:value="doc[field.fieldname]"
:formatter="
(date) => getFormat(date, '', true, true)
"
:format="getFormat('', '', true, true, false)"
:placeholder="field.placeholder"
placement="left-start"
:hideIcon="true"
@change="(v) => fieldChange(v, field)"
/>
</div>
@ -194,12 +201,10 @@
class="form-control"
>
<DatePicker
icon-left=""
:value="doc[field.fieldname]"
:formatter="(date) => getFormat(date, '', true)"
:format="getFormat('', '', true, false, false)"
:placeholder="field.placeholder"
placement="left-start"
:hideIcon="true"
@change="(v) => fieldChange(v, field)"
/>
</div>
@ -322,7 +327,7 @@ import { usersStore } from '@/stores/users'
import { isMobileView } from '@/composables/settings'
import { getFormat, evaluateDependsOnValue } from '@/utils'
import { flt } from '@/utils/numberFormat.js'
import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
import { Tooltip, DateTimePicker, DatePicker, TimePicker } from 'frappe-ui'
import { useDocument } from '@/data/document'
import { ref, computed, getCurrentInstance } from 'vue'

View File

@ -58,8 +58,7 @@ import { getSettings } from '@/stores/settings'
import { showSettings, isMobileView } from '@/composables/settings'
import { showAboutModal } from '@/composables/modals'
import { confirmLoginToFrappeCloud } from '@/composables/frappecloud'
import { Dropdown } from 'frappe-ui'
import { theme, toggleTheme } from '@/stores/theme'
import { Dropdown, useTheme } from 'frappe-ui'
import { computed, h, markRaw } from 'vue'
const props = defineProps({
@ -72,6 +71,7 @@ const props = defineProps({
const { settings, brand } = getSettings()
const { logout } = sessionStore()
const { getUser } = usersStore()
const { currentTheme, toggleTheme } = useTheme()
const user = computed(() => getUser() || {})
@ -134,7 +134,7 @@ function getStandardItem(item) {
}
case 'toggle_theme':
return {
icon: theme.value === 'dark' ? 'sun' : item.icon,
icon: currentTheme.value === 'dark' ? 'sun' : item.icon,
label: __(item.label),
onClick: toggleTheme,
}

View File

@ -60,13 +60,14 @@
class="flex flex-row-reverse gap-2 items-center min-w-11"
>
<Dropdown
placement="right-start"
side="right"
:offset="15"
:options="viewControls.viewActions(item, close)"
>
<template #default>
<Button
variant="ghost"
class="!size-5 hidden group-hover:block"
class="!size-5 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto transition-opacity"
icon="more-horizontal"
@click.stop
/>

View File

@ -157,7 +157,7 @@ const props = defineProps({
},
size: {
type: String,
default: 'md',
default: 'sm',
},
variant: {
type: String,
@ -282,7 +282,7 @@ const inputClasses = computed(() => {
let variant = props.disabled ? 'disabled' : props.variant
let variantClasses = {
subtle:
'border border-gray-100 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',
'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',
outline:
'border border-outline-gray-2 bg-surface-white placeholder-ink-gray-4 hover:border-outline-gray-3 hover:shadow-sm 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',
disabled: [

View File

@ -1,166 +0,0 @@
<template>
<Menu as="div" class="relative inline-block text-left" v-slot="{ open }">
<Popover
:transition="dropdownTransition"
:show="open"
:placement="popoverPlacement"
>
<template #target="{ togglePopover }">
<MenuButton as="template">
<slot v-if="$slots.default" v-bind="{ open, togglePopover }" />
<Button v-else :active="open" v-bind="button">
{{ button ? button?.label || null : 'Options' }}
</Button>
</MenuButton>
</template>
<template #body>
<div
class="mt-2 min-w-40 divide-y divide-outline-gray-modals rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
:class="{
'mt-2': ['bottom', 'left', 'right'].includes(placement),
'ml-2': placement == 'right-start',
}"
>
<MenuItems
class="min-w-40 divide-y divide-outline-gray-modals"
:class="{
'left-0 origin-top-left': placement == 'left',
'right-0 origin-top-right': placement == 'right',
'inset-x-0 origin-top': placement == 'center',
'mt-0 origin-top-right': placement == 'right-start',
}"
>
<div v-for="group in groups" :key="group.key" class="p-1.5">
<div
v-if="group.group && !group.hideLabel"
class="flex h-7 items-center px-2 text-sm font-medium text-ink-gray-4"
>
{{ group.group }}
</div>
<MenuItem
v-for="item in group.items"
:key="item.label"
v-slot="{ active }"
>
<slot name="item" v-bind="{ item, active }">
<component
v-if="item.component"
:is="item.component"
:active="active"
/>
<button
v-else
:class="[
active ? 'bg-surface-gray-3' : 'text-ink-gray-6',
'group flex h-7 w-full items-center rounded px-2 text-base',
]"
@click="item.onClick"
>
<FeatherIcon
v-if="item.icon && typeof item.icon === 'string'"
:name="item.icon"
class="mr-2 h-4 w-4 flex-shrink-0 text-ink-gray-7"
aria-hidden="true"
/>
<component
class="mr-2 h-4 w-4 flex-shrink-0 text-ink-gray-7"
v-else-if="item.icon"
:is="item.icon"
/>
<span class="whitespace-nowrap text-ink-gray-7">
{{ item.label }}
</span>
</button>
</slot>
</MenuItem>
</div>
</MenuItems>
<div v-if="slots.footer" class="border-t p-1.5">
<slot name="footer"></slot>
</div>
</div>
</template>
</Popover>
</Menu>
</template>
<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { Popover, Button, FeatherIcon } from 'frappe-ui'
import { computed, useSlots } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({
button: {
type: Object,
default: null,
},
options: {
type: Array,
default: () => [],
},
placement: {
type: String,
default: 'left',
},
})
const router = useRouter()
const slots = useSlots()
const dropdownTransition = {
enterActiveClass: 'transition duration-100 ease-out',
enterFromClass: 'transform scale-95 opacity-0',
enterToClass: 'transform scale-100 opacity-100',
leaveActiveClass: 'transition duration-75 ease-in',
leaveFromClass: 'transform scale-100 opacity-100',
leaveToClass: 'transform scale-95 opacity-0',
}
const groups = computed(() => {
let groups = props.options[0]?.group
? props.options
: [{ group: '', items: props.options }]
return groups.map((group, i) => {
return {
key: i,
group: group.group,
hideLabel: group.hideLabel || false,
items: filterOptions(group.items),
}
})
})
const popoverPlacement = computed(() => {
if (props.placement === 'left') return 'bottom-start'
if (props.placement === 'right') return 'bottom-end'
if (props.placement === 'center') return 'bottom-center'
if (props.placement === 'right-start') return 'right-start'
return 'bottom'
})
function normalizeDropdownItem(option) {
let onClick = option.onClick || null
if (!onClick && option.route && router) {
onClick = () => router.push(option.route)
}
return {
name: option.name,
label: option.label,
icon: option.icon,
group: option.group,
component: option.component,
onClick,
}
}
function filterOptions(options) {
return (options || [])
.filter(Boolean)
.filter((option) => (option.condition ? option.condition() : true))
.map((option) => normalizeDropdownItem(option))
}
</script>

View File

@ -1,277 +0,0 @@
<template>
<div ref="reference">
<div
ref="target"
:class="['flex', $attrs.class]"
@click="updatePosition"
@focusin="updatePosition"
@keydown="updatePosition"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<slot
name="target"
v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
/>
</div>
<teleport to="#frappeui-popper-root">
<div
ref="popover"
class="relative z-[100]"
:class="[popoverContainerClass, popoverClass]"
:style="{ minWidth: targetWidth ? targetWidth + 'px' : null }"
@mouseover="pointerOverTargetOrPopup = true"
@mouseleave="onMouseleave"
>
<transition v-bind="popupTransition">
<div v-show="isOpen">
<slot
name="body"
v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
>
<div class="rounded-lg border border-gray-100 bg-surface-white shadow-xl">
<slot
name="body-main"
v-bind="{
togglePopover,
updatePosition,
open,
close,
isOpen,
}"
/>
</div>
</slot>
</div>
</transition>
</div>
</teleport>
</div>
</template>
<script>
import { createPopper } from '@popperjs/core'
export default {
name: 'Popover',
inheritAttrs: false,
props: {
show: {
default: undefined,
},
trigger: {
type: String,
default: 'click', // click, hover
},
hoverDelay: {
type: Number,
default: 0,
},
leaveDelay: {
type: Number,
default: 0,
},
placement: {
type: String,
default: 'bottom-start',
},
popoverClass: [String, Object, Array],
transition: {
default: null,
},
hideOnBlur: {
default: true,
},
},
emits: ['open', 'close', 'update:show'],
expose: ['open', 'close'],
data() {
return {
popoverContainerClass: 'body-container',
showPopup: false,
targetWidth: null,
pointerOverTargetOrPopup: false,
}
},
watch: {
show(val) {
if (val) {
this.open()
} else {
this.close()
}
},
},
created() {
if (typeof window === 'undefined') return
if (!document.getElementById('frappeui-popper-root')) {
const root = document.createElement('div')
root.id = 'frappeui-popper-root'
document.body.appendChild(root)
}
},
mounted() {
this.listener = (e) => {
const clickedElement = e.target
const reference = this.$refs.reference
const popoverBody = this.$refs.popover
const insideClick =
clickedElement === reference ||
clickedElement === popoverBody ||
reference?.contains(clickedElement) ||
popoverBody?.contains(clickedElement)
if (insideClick) {
return
}
const root = document.getElementById('frappeui-popper-root')
const insidePopoverRoot = root.contains(clickedElement)
if (!insidePopoverRoot) {
return this.close()
}
const bodyClass = `.${this.popoverContainerClass}`
const clickedElementBody = clickedElement?.closest(bodyClass)
const currentPopoverBody = reference?.closest(bodyClass)
const isSiblingClicked =
clickedElementBody &&
currentPopoverBody &&
clickedElementBody === currentPopoverBody
if (isSiblingClicked) {
this.close()
}
}
if (this.hideOnBlur) {
document.addEventListener('click', this.listener)
document.addEventListener('mousedown', this.listener)
}
this.$nextTick(() => {
this.targetWidth = this.$refs['target'].clientWidth
})
},
beforeDestroy() {
this.popper && this.popper.destroy()
document.removeEventListener('click', this.listener)
document.removeEventListener('mousedown', this.listener)
},
computed: {
showPropPassed() {
return this.show != null
},
isOpen: {
get() {
if (this.showPropPassed) {
return this.show
}
return this.showPopup
},
set(val) {
val = Boolean(val)
if (this.showPropPassed) {
this.$emit('update:show', val)
} else {
this.showPopup = val
}
if (val === false) {
this.$emit('close')
} else if (val === true) {
this.$emit('open')
}
},
},
popupTransition() {
let templates = {
default: {
enterActiveClass: 'transition duration-150 ease-out',
enterFromClass: 'translate-y-1 opacity-0',
enterToClass: 'translate-y-0 opacity-100',
leaveActiveClass: 'transition duration-150 ease-in',
leaveFromClass: 'translate-y-0 opacity-100',
leaveToClass: 'translate-y-1 opacity-0',
},
}
if (typeof this.transition === 'string') {
return templates[this.transition]
}
return this.transition
},
},
methods: {
setupPopper() {
if (!this.popper) {
this.popper = createPopper(this.$refs.reference, this.$refs.popover, {
placement: this.placement,
})
} else {
this.updatePosition()
}
},
updatePosition() {
this.popper && this.popper.update()
},
togglePopover(flag) {
if (flag instanceof Event) {
flag = null
}
if (flag == null) {
flag = !this.isOpen
}
flag = Boolean(flag)
if (flag) {
this.open()
} else {
this.close()
}
},
open() {
this.isOpen = true
this.$nextTick(() => this.setupPopper())
},
close() {
this.isOpen = false
},
onMouseover() {
this.pointerOverTargetOrPopup = true
if (this.leaveTimer) {
clearTimeout(this.leaveTimer)
this.leaveTimer = null
}
if (this.trigger === 'hover') {
if (this.hoverDelay) {
this.hoverTimer = setTimeout(() => {
if (this.pointerOverTargetOrPopup) {
this.open()
}
}, Number(this.hoverDelay) * 1000)
} else {
this.open()
}
}
},
onMouseleave(e) {
this.pointerOverTargetOrPopup = false
if (this.hoverTimer) {
clearTimeout(this.hoverTimer)
this.hoverTimer = null
}
if (this.trigger === 'hover') {
if (this.leaveTimer) {
clearTimeout(this.leaveTimer)
}
if (this.leaveDelay) {
this.leaveTimer = setTimeout(() => {
if (!this.pointerOverTargetOrPopup) {
this.close()
}
}, Number(this.leaveDelay) * 1000)
} else {
if (!this.pointerOverTargetOrPopup) {
this.close()
}
}
}
},
},
}
</script>

View File

@ -0,0 +1,237 @@
import { usersStore } from '@/stores/users'
import { dayjs, createListResource } from 'frappe-ui'
import { sameArrayContents } from '@/utils'
import { computed, ref } from 'vue'
import { allTimeSlots } from '@/components/Calendar/utils'
export const showEventModal = ref(false)
export const activeEvent = ref(null)
export function useEvent(doctype, docname) {
const { getUser } = usersStore()
const eventsResource = createListResource({
doctype: 'Event',
cache: ['calendar', docname],
fields: [
'name',
'status',
'subject',
'description',
'starts_on',
'ends_on',
'all_day',
'event_type',
'color',
'owner',
'reference_doctype',
'reference_docname',
'creation',
],
filters: {
reference_doctype: doctype,
reference_docname: docname,
},
auto: true,
orderBy: 'creation desc',
onSuccess: (d) => {
console.log(d)
},
})
const eventParticipantsResource = createListResource({
doctype: 'Event Participants',
fields: ['*'],
parent: 'Event',
})
const events = computed(() => {
if (!eventsResource.data) return []
const eventNames = eventsResource.data.map((e) => e.name)
if (
!eventParticipantsResource.data?.length ||
eventsParticipantIsUpdated(eventNames)
) {
eventParticipantsResource.update({
filters: {
parenttype: 'Event',
parentfield: 'event_participants',
parent: ['in', eventNames],
},
})
!eventParticipantsResource.list.loading &&
eventParticipantsResource.reload()
} else {
eventsResource.data.forEach((event) => {
if (typeof event.owner !== 'object') {
event.owner = {
label: getUser(event.owner).full_name,
image: getUser(event.owner).user_image,
name: event.owner,
}
}
event.event_participants = [
...eventParticipantsResource.data.filter(
(participant) => participant.parent === event.name,
),
]
event.participants = [
event.owner,
...eventParticipantsResource.data
.filter((participant) => participant.parent === event.name)
.map((participant) => ({
label: getUser(participant.email).full_name || participant.email,
image: getUser(participant.email).user_image || '',
name: participant.email,
})),
]
})
}
return eventsResource.data
})
function eventsParticipantIsUpdated(eventNames) {
const parentFilter = eventParticipantsResource.filters?.parent?.[1]
if (eventNames.length && !sameArrayContents(parentFilter, eventNames))
return true
let d = eventsResource.setValue.data
if (!d) return false
let newParticipants = d.event_participants.map((p) => p.name)
let oldParticipants = eventParticipantsResource.data
.filter((p) => p.parent === d.name)
.map((p) => p.name)
return !sameArrayContents(newParticipants, oldParticipants)
}
const startEndTime = (
startTime,
endTime,
isFullDay = false,
format = 'h:mm a',
) => {
const start = dayjs(startTime)
const end = dayjs(endTime)
if (isFullDay) return __('All day')
return `${start.format(format)} - ${end.format(format)}`
}
const startDate = (startTime, format = 'ddd, D MMM YYYY') => {
const start = dayjs(startTime)
return start.format(format)
}
return {
eventsResource,
eventParticipantsResource,
events,
startEndTime,
startDate,
}
}
export function normalizeParticipants(list = []) {
const seen = new Set()
const out = []
for (const a of list || []) {
if (!a?.email || seen.has(a.email)) continue
seen.add(a.email)
out.push({
email: a.email,
reference_doctype: a.reference_doctype || 'Contact',
reference_docname: a.reference_docname || '',
})
}
return out
}
export function formatDuration(mins) {
if (mins < 60) return __('{0} mins', [mins])
let hours = mins / 60
if (hours % 1 !== 0 && hours % 1 !== 0.5) {
hours = hours.toFixed(2)
}
if (Number.isInteger(hours)) {
return hours === 1 ? __('1 hr') : __('{0} hrs', [hours])
}
return `${hours} hrs`
}
export function buildEndTimeOptions(fromTime) {
const timeSlots = allTimeSlots()
if (!fromTime) return timeSlots
const startIndex = timeSlots.findIndex((o) => o.value > fromTime)
if (startIndex === -1) return []
const [fh, fm] = fromTime.split(':').map((n) => parseInt(n))
const fromTotal = fh * 60 + fm
return timeSlots.slice(startIndex).map((o) => {
const [th, tm] = o.value.split(':').map((n) => parseInt(n))
const toTotal = th * 60 + tm
const duration = toTotal - fromTotal
return { ...o, label: `${o.label} (${formatDuration(duration)})` }
})
}
export function computeAutoToTime(fromTime) {
if (!fromTime) return ''
const [hour, minute] = fromTime.split(':').map((n) => parseInt(n))
let nh = hour + 1
let nm = minute
if (nh >= 24) {
nh = 23
nm = 59
}
return `${String(nh).padStart(2, '0')}:${String(nm).padStart(2, '0')}`
}
export function validateTimeRange({ fromDate, fromTime, toTime, isFullDay }) {
if (isFullDay) return { valid: true, error: null }
if (!fromTime || !toTime) {
return { valid: false, error: __('Start and end time are required') }
}
const start = dayjs(fromDate + ' ' + fromTime)
const end = dayjs(fromDate + ' ' + toTime)
if (!start.isValid() || !end.isValid()) {
return { valid: false, error: __('Invalid start or end time') }
}
if (end.diff(start, 'minute') <= 0) {
return { valid: false, error: __('End time should be after start time') }
}
return { valid: true, error: null }
}
export function parseEventDoc(doc) {
if (!doc) return {}
const { getUser } = usersStore()
return {
id: doc.name,
title: doc.subject,
description: doc.description,
status: doc.status,
fromDate: dayjs(doc.starts_on).format('YYYY-MM-DD'),
toDate: dayjs(doc.ends_on).format('YYYY-MM-DD'),
fromTime: dayjs(doc.starts_on).format('HH:mm'),
toTime: dayjs(doc.ends_on).format('HH:mm'),
isFullDay: doc.all_day,
eventType: doc.event_type,
color: doc.color,
referenceDoctype: doc.reference_doctype,
referenceDocname: doc.reference_docname,
event_participants: doc.event_participants || [],
owner: doc.owner
? {
label: getUser(doc.owner).full_name,
image: getUser(doc.owner).user_image,
value: doc.owner,
}
: null,
}
}

View File

@ -0,0 +1,78 @@
import { onMounted, onBeforeUnmount, unref } from 'vue'
import { isDialogOpen } from '@/utils/dialogs'
/**
* Generic global keyboard shortcuts composable.
*
* Usage:
* useKeyboardShortcuts({
* active: () => true, // boolean | () => boolean (reactive allowed)
* shortcuts: [
* { keys: 'Escape', action: close },
* { keys: ['Delete', 'Backspace'], action: onDelete },
* { match: e => (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'd', action: duplicate }
* ],
* ignoreTyping: true // skip when focus is in input/textarea/contenteditable (default true)
* })
*/
export function useKeyboardShortcuts(options) {
const {
active = true,
shortcuts = [],
ignoreTyping = true,
target = typeof window !== 'undefined' ? window : null,
skipWhenDialogOpen = true,
} = options || {}
function isTypingEvent(e) {
if (!ignoreTyping) return false
const el = e.target
if (!el) return false
const tag = el.tagName
return (
el.isContentEditable ||
tag === 'INPUT' ||
tag === 'TEXTAREA' ||
tag === 'SELECT' ||
(el.closest && el.closest('[contenteditable="true"]'))
)
}
function matchShortcut(def, e) {
if (def.match) return def.match(e)
let keys = def.keys
if (!keys) return false
if (!Array.isArray(keys)) keys = [keys]
return keys.some((k) => k === e.key)
}
function handler(e) {
if (!target) return
const isActive = typeof active === 'function' ? active() : unref(active)
if (!isActive) return
if (isTypingEvent(e)) return
if (skipWhenDialogOpen && isDialogOpen()) return
for (const def of shortcuts) {
if (!def) continue
if (def.guard && !def.guard(e)) continue
if (matchShortcut(def, e)) {
if (def.preventDefault !== false) e.preventDefault()
if (def.stopPropagation) e.stopPropagation()
def.action && def.action(e)
break
}
}
}
onMounted(() => {
target && target.addEventListener('keydown', handler)
})
onBeforeUnmount(() => {
target && target.removeEventListener('keydown', handler)
})
return {
stop: () => target && target.removeEventListener('keydown', handler),
}
}

View File

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 763 B

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 215 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Some files were not shown because too many files have changed in this diff Show More